packages feed

hledger-lib 1.40 → 1.41

raw patch · 44 files changed

+2704/−1219 lines, 44 filesdep +blaze-htmldep +processdep −ghc-debug-stubdep −pagerdep −text-ansidep ~basedep ~base-compatdep ~directoryPVP ok

version bump matches the API change (PVP)

Dependencies added: blaze-html, process

Dependencies removed: ghc-debug-stub, pager, text-ansi

Dependency ranges changed: base, base-compat, directory

API changes (from Hackage documentation)

- Hledger.Data: [jcommodities] :: Journal -> Map CommoditySymbol Commodity
- Hledger.Data: [jinferredcommodities] :: Journal -> Map CommoditySymbol AmountStyle
- Hledger.Data.AccountName: accountNameToBeancount :: AccountName -> BeancountAccountName
- Hledger.Data.AccountName: beancountTopLevelAccounts :: IsString a => [a]
- Hledger.Data.Amount: instance Data.Default.Class.Default Hledger.Data.Amount.AmountFormat
- Hledger.Data.Journal: instance Data.Default.Class.Default Hledger.Data.Types.Journal
- Hledger.Data.Journal: journalConversionAccount :: Journal -> AccountName
- Hledger.Data.Journal: journalInferCostsFromEquity :: Journal -> Either String Journal
- Hledger.Data.Journal: journalMarkRedundantCosts :: Journal -> Either String Journal
- Hledger.Data.Posting: nullsourcepos :: (SourcePos, SourcePos)
- Hledger.Data.Posting: postingAsLinesBeancount :: Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
- Hledger.Data.Posting: postingsAsLinesBeancount :: [Posting] -> [Text]
- Hledger.Data.Posting: showAccountNameBeancount :: Maybe Int -> AccountName -> Text
- Hledger.Data.RawOptions: instance Data.Default.Class.Default Hledger.Data.RawOptions.RawOpts
- Hledger.Data.StringFormat: instance Data.Default.Class.Default Hledger.Data.StringFormat.StringFormat
- Hledger.Data.Transaction: showTransactionBeancount :: Transaction -> Text
- Hledger.Data.Transaction: transactionAddInferredEquityPostings :: Bool -> AccountName -> Transaction -> Transaction
- Hledger.Data.Transaction: transactionInferCostsFromEquity :: Bool -> [AccountName] -> Transaction -> Either String Transaction
- Hledger.Data.Types: [jcommodities] :: Journal -> Map CommoditySymbol Commodity
- Hledger.Data.Types: [jinferredcommodities] :: Journal -> Map CommoditySymbol AmountStyle
- Hledger.Data.Types: instance Data.Default.Class.Default Hledger.Data.Types.DateSpan
- Hledger.Data.Types: instance Data.Default.Class.Default Hledger.Data.Types.Interval
- Hledger.Data.Types: instance Data.Default.Class.Default Hledger.Data.Types.Period
- Hledger.Query: instance Data.Default.Class.Default Hledger.Query.Query
- Hledger.Read.Common: commenttagsp :: TextParser m [Tag]
- Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.AccountListMode
- Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.BalanceAccumulation
- Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.BalanceCalculation
- Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.ReportOpts
- Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.ReportSpec
- Hledger.Reports.ReportTypes: [displayDepth] :: DisplayName -> Int
- Hledger.Reports.ReportTypes: prrDepth :: PeriodicReportRow DisplayName a -> Int
- Hledger.Utils.IO: hasOutputFile :: Bool
- Hledger.Utils.IO: outputFileOption :: Maybe String
- Hledger.Utils.IO: pager :: String -> IO ()
- Hledger.Write.Html: printHtml :: [[Cell (Html ())]] -> Html ()
- Hledger.Write.Spreadsheet: instance GHC.Base.Functor Hledger.Write.Spreadsheet.Cell
- Text.Tabular.AsciiWide: instance Data.Default.Class.Default Text.Tabular.AsciiWide.TableOpts
+ Hledger.Data: DepthSpec :: Maybe Int -> [(Regexp, Int)] -> DepthSpec
+ Hledger.Data: [dsFlatDepth] :: DepthSpec -> Maybe Int
+ Hledger.Data: [dsRegexpDepths] :: DepthSpec -> [(Regexp, Int)]
+ Hledger.Data: [jdeclaredcommodities] :: Journal -> Map CommoditySymbol Commodity
+ Hledger.Data: [jinferredcommoditystyles] :: Journal -> Map CommoditySymbol AmountStyle
+ Hledger.Data: [pdsourcepos] :: PriceDirective -> SourcePos
+ Hledger.Data: data DepthSpec
+ Hledger.Data: isHiddenTagName :: TagName -> Bool
+ Hledger.Data: nullsourcepos :: SourcePos
+ Hledger.Data: nullsourcepospair :: (SourcePos, SourcePos)
+ Hledger.Data: showMarketPrice :: MarketPrice -> String
+ Hledger.Data: showMarketPrices :: [MarketPrice] -> [Char]
+ Hledger.Data: toHiddenTag :: Tag -> HiddenTag
+ Hledger.Data: toHiddenTagName :: TagName -> TagName
+ Hledger.Data: toVisibleTag :: HiddenTag -> Tag
+ Hledger.Data: toVisibleTagName :: TagName -> TagName
+ Hledger.Data: type HiddenTag = -- | A tag whose name begins with _. Tag
+ Hledger.Data.AccountName: accountNameInferTypeExcept :: [AccountType] -> AccountName -> Maybe AccountType
+ Hledger.Data.AccountName: defaultBaseConversionAccount :: IsString a => a
+ Hledger.Data.AccountName: getAccountNameClippedDepth :: DepthSpec -> AccountName -> Maybe Int
+ Hledger.Data.Amount: [displayQuotes] :: AmountFormat -> Bool
+ Hledger.Data.Amount: instance Data.Default.Internal.Default Hledger.Data.Amount.AmountFormat
+ Hledger.Data.Currency: currencies :: [(String, CurrencyCode, CurrencySymbol)]
+ Hledger.Data.Currency: currencyCodeToSymbol :: CurrencyCode -> Maybe CurrencySymbol
+ Hledger.Data.Currency: currencySymbolToCode :: CurrencySymbol -> Maybe CurrencyCode
+ Hledger.Data.Errors: makePriceDirectiveErrorExcerpt :: PriceDirective -> Maybe (PriceDirective -> Text -> Maybe (Int, Maybe Int)) -> (FilePath, Int, Maybe (Int, Maybe Int), Text)
+ Hledger.Data.Journal: defaultBaseConversionAccount :: IsString a => a
+ Hledger.Data.Journal: instance Data.Default.Internal.Default Hledger.Data.Types.Journal
+ Hledger.Data.Journal: journalBaseConversionAccount :: Journal -> AccountName
+ Hledger.Data.Journal: journalTagCostsAndEquityAndMaybeInferCosts :: Bool -> Bool -> Journal -> Either String Journal
+ Hledger.Data.Posting: conversionPostingTagName :: TagName
+ Hledger.Data.Posting: costPostingTagName :: TagName
+ Hledger.Data.Posting: generatedPostingTagName :: TagName
+ Hledger.Data.Posting: generatedTransactionTagName :: TagName
+ Hledger.Data.Posting: modifiedTransactionTagName :: TagName
+ Hledger.Data.Posting: negatePostingAmount :: Posting -> Posting
+ Hledger.Data.Posting: postingAddHiddenAndMaybeVisibleTag :: Bool -> HiddenTag -> Posting -> Posting
+ Hledger.Data.Posting: postingIndent :: Text -> Text
+ Hledger.Data.RawOptions: instance Data.Default.Internal.Default Hledger.Data.RawOptions.RawOpts
+ Hledger.Data.RawOptions: maybeynaopt :: String -> RawOpts -> Maybe YNA
+ Hledger.Data.RawOptions: maybeynopt :: String -> RawOpts -> Maybe Bool
+ Hledger.Data.StringFormat: instance Data.Default.Internal.Default Hledger.Data.StringFormat.StringFormat
+ Hledger.Data.Transaction: payeeAndNoteFromDescription :: Text -> (Text, Text)
+ Hledger.Data.Transaction: payeeAndNoteFromDescription' :: Text -> (Text, Text)
+ Hledger.Data.Transaction: transactionAddHiddenAndMaybeVisibleTag :: Bool -> HiddenTag -> Transaction -> Transaction
+ Hledger.Data.Transaction: transactionAddTags :: Transaction -> [Tag] -> Transaction
+ Hledger.Data.Transaction: transactionInferEquityPostings :: Bool -> AccountName -> Transaction -> Transaction
+ Hledger.Data.Transaction: transactionTagCostsAndEquityAndMaybeInferCosts :: Bool -> Bool -> [AccountName] -> Transaction -> Either String Transaction
+ Hledger.Data.Types: DepthSpec :: Maybe Int -> [(Regexp, Int)] -> DepthSpec
+ Hledger.Data.Types: [dsFlatDepth] :: DepthSpec -> Maybe Int
+ Hledger.Data.Types: [dsRegexpDepths] :: DepthSpec -> [(Regexp, Int)]
+ Hledger.Data.Types: [jdeclaredcommodities] :: Journal -> Map CommoditySymbol Commodity
+ Hledger.Data.Types: [jinferredcommoditystyles] :: Journal -> Map CommoditySymbol AmountStyle
+ Hledger.Data.Types: [pdsourcepos] :: PriceDirective -> SourcePos
+ Hledger.Data.Types: data DepthSpec
+ Hledger.Data.Types: instance Data.Default.Internal.Default Hledger.Data.Types.DateSpan
+ Hledger.Data.Types: instance Data.Default.Internal.Default Hledger.Data.Types.Interval
+ Hledger.Data.Types: instance Data.Default.Internal.Default Hledger.Data.Types.Period
+ Hledger.Data.Types: instance GHC.Base.Monoid Hledger.Data.Types.DepthSpec
+ Hledger.Data.Types: instance GHC.Base.Semigroup Hledger.Data.Types.DepthSpec
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.DepthSpec
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.DepthSpec
+ Hledger.Data.Types: isHiddenTagName :: TagName -> Bool
+ Hledger.Data.Types: nullsourcepos :: SourcePos
+ Hledger.Data.Types: nullsourcepospair :: (SourcePos, SourcePos)
+ Hledger.Data.Types: showMarketPrice :: MarketPrice -> String
+ Hledger.Data.Types: showMarketPrices :: [MarketPrice] -> [Char]
+ Hledger.Data.Types: toHiddenTag :: Tag -> HiddenTag
+ Hledger.Data.Types: toHiddenTagName :: TagName -> TagName
+ Hledger.Data.Types: toVisibleTag :: HiddenTag -> Tag
+ Hledger.Data.Types: toVisibleTagName :: TagName -> TagName
+ Hledger.Data.Types: type HiddenTag = -- | A tag whose name begins with _. Tag
+ Hledger.Query: DepthAcct :: Regexp -> Int -> Query
+ Hledger.Query: instance Data.Default.Internal.Default Hledger.Query.Query
+ Hledger.Query: parseDepthSpec :: Text -> Either RegexError DepthSpec
+ Hledger.Read.Common: [posting_account_tags_] :: InputOpts -> Bool
+ Hledger.Read.Common: commentlinetagsp :: TextParser m [Tag]
+ Hledger.Read.Common: posting_account_tags :: HasInputOpts c_a43az => Lens' c_a43az Bool
+ Hledger.Read.InputOptions: [posting_account_tags_] :: InputOpts -> Bool
+ Hledger.Read.InputOptions: posting_account_tags :: HasInputOpts c_a43az => Lens' c_a43az Bool
+ Hledger.Reports.ReportOptions: [balance_base_url_] :: ReportOpts -> Maybe Text
+ Hledger.Reports.ReportOptions: balance_base_url :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u (Maybe Text)
+ Hledger.Reports.ReportOptions: instance Data.Default.Internal.Default Hledger.Reports.ReportOptions.AccountListMode
+ Hledger.Reports.ReportOptions: instance Data.Default.Internal.Default Hledger.Reports.ReportOptions.BalanceAccumulation
+ Hledger.Reports.ReportOptions: instance Data.Default.Internal.Default Hledger.Reports.ReportOptions.BalanceCalculation
+ Hledger.Reports.ReportOptions: instance Data.Default.Internal.Default Hledger.Reports.ReportOptions.ReportOpts
+ Hledger.Reports.ReportOptions: instance Data.Default.Internal.Default Hledger.Reports.ReportOptions.ReportSpec
+ Hledger.Reports.ReportTypes: [displayIndent] :: DisplayName -> NumberOfIndents
+ Hledger.Reports.ReportTypes: prrIndent :: PeriodicReportRow DisplayName a -> Int
+ Hledger.Utils.IO: Auto :: YNA
+ Hledger.Utils.IO: No :: YNA
+ Hledger.Utils.IO: Yes :: YNA
+ Hledger.Utils.IO: data YNA
+ Hledger.Utils.IO: getOpt :: [String] -> IO (Maybe String)
+ Hledger.Utils.IO: instance GHC.Classes.Eq Hledger.Utils.IO.YNA
+ Hledger.Utils.IO: instance GHC.Show.Show Hledger.Utils.IO.YNA
+ Hledger.Utils.IO: parseYN :: String -> Bool
+ Hledger.Utils.IO: parseYNA :: String -> YNA
+ Hledger.Utils.IO: runPager :: String -> IO ()
+ Hledger.Utils.IO: useColorOnStderrUnsafe :: Bool
+ Hledger.Utils.IO: useColorOnStdoutUnsafe :: Bool
+ Hledger.Utils.IO: warn :: String -> a -> a
+ Hledger.Write.Beancount: accountNameToBeancount :: AccountName -> BeancountAccountName
+ Hledger.Write.Beancount: commodityToBeancount :: CommoditySymbol -> BeancountCommoditySymbol
+ Hledger.Write.Beancount: showBeancountMetadata :: Maybe Int -> BMetadata -> Text
+ Hledger.Write.Beancount: showTransactionBeancount :: Transaction -> Text
+ Hledger.Write.Beancount: tagsToBeancountMetadata :: [Tag] -> [BMetadata]
+ Hledger.Write.Beancount: tests_WriteBeancount :: TestTree
+ Hledger.Write.Html: borderLines :: Lines border => border -> [Text]
+ Hledger.Write.Html: borderStyles :: Lines border => Cell border text -> [Text]
+ Hledger.Write.Html: class (Lines border) => Lines border
+ Hledger.Write.Html: instance Hledger.Write.Html.Lines ()
+ Hledger.Write.Html: instance Hledger.Write.Html.Lines Hledger.Write.Spreadsheet.NumLines
+ Hledger.Write.Html.Attribute: aligncenter :: Text
+ Hledger.Write.Html.Attribute: alignleft :: Text
+ Hledger.Write.Html.Attribute: alignright :: Text
+ Hledger.Write.Html.Attribute: bold :: Text
+ Hledger.Write.Html.Attribute: bottomdoubleborder :: Text
+ Hledger.Write.Html.Attribute: collapse :: Text
+ Hledger.Write.Html.Attribute: concatStyles :: [Text] -> Text
+ Hledger.Write.Html.Attribute: doubleborder :: Text
+ Hledger.Write.Html.Attribute: hpad :: Text
+ Hledger.Write.Html.Attribute: lpad :: Text
+ Hledger.Write.Html.Attribute: rpad :: Text
+ Hledger.Write.Html.Attribute: stylesheet :: [(Text, Text)] -> Text
+ Hledger.Write.Html.Attribute: tableStyle :: [(Text, Text)]
+ Hledger.Write.Html.Attribute: tableStylesheet :: Text
+ Hledger.Write.Html.Attribute: topdoubleborder :: Text
+ Hledger.Write.Html.Attribute: vpad :: Text
+ Hledger.Write.Html.Blaze: formatCell :: Lines border => Cell border Html -> Html
+ Hledger.Write.Html.Blaze: formatRow :: Lines border => [Cell border Html] -> Html
+ Hledger.Write.Html.Blaze: printHtml :: Lines border => [[Cell border Html]] -> Html
+ Hledger.Write.Html.Lucid: formatCell :: Lines border => Cell border Html -> Html
+ Hledger.Write.Html.Lucid: formatRow :: Lines border => [Cell border Html] -> Html
+ Hledger.Write.Html.Lucid: printHtml :: Lines border => [[Cell border Html]] -> Html
+ Hledger.Write.Ods: instance GHC.Classes.Eq Hledger.Write.Ods.DataStyle
+ Hledger.Write.Ods: instance GHC.Classes.Ord Hledger.Write.Ods.DataStyle
+ Hledger.Write.Ods: instance GHC.Show.Show Hledger.Write.Ods.DataStyle
+ Hledger.Write.Spreadsheet: Border :: lines -> Border lines
+ Hledger.Write.Spreadsheet: Class :: Text -> Class
+ Hledger.Write.Spreadsheet: Covered :: Span
+ Hledger.Write.Spreadsheet: DoubleLine :: NumLines
+ Hledger.Write.Spreadsheet: NoLine :: NumLines
+ Hledger.Write.Spreadsheet: NoSpan :: Span
+ Hledger.Write.Spreadsheet: SingleLine :: NumLines
+ Hledger.Write.Spreadsheet: SpanHorizontal :: Int -> Span
+ Hledger.Write.Spreadsheet: SpanVertical :: Int -> Span
+ Hledger.Write.Spreadsheet: TypeInteger :: Type
+ Hledger.Write.Spreadsheet: [borderLeft, borderRight, borderTop, borderBottom] :: Border lines -> lines
+ Hledger.Write.Spreadsheet: [cellAnchor] :: Cell border text -> Text
+ Hledger.Write.Spreadsheet: [cellBorder] :: Cell border text -> Border border
+ Hledger.Write.Spreadsheet: [cellClass] :: Cell border text -> Class
+ Hledger.Write.Spreadsheet: [cellSpan] :: Cell border text -> Span
+ Hledger.Write.Spreadsheet: addHeaderBorders :: [Cell () text] -> [Cell NumLines text]
+ Hledger.Write.Spreadsheet: addRowSpanHeader :: Cell border text -> [[Cell border text]] -> [[Cell border text]]
+ Hledger.Write.Spreadsheet: cellFromAmount :: Lines border => AmountFormat -> (Class, (wb, Amount)) -> Cell border wb
+ Hledger.Write.Spreadsheet: cellFromMixedAmount :: Lines border => AmountFormat -> (Class, MixedAmount) -> Cell border WideBuilder
+ Hledger.Write.Spreadsheet: cellsFromMixedAmount :: Lines border => AmountFormat -> (Class, MixedAmount) -> [Cell border WideBuilder]
+ Hledger.Write.Spreadsheet: class Lines border
+ Hledger.Write.Spreadsheet: data Border lines
+ Hledger.Write.Spreadsheet: data NumLines
+ Hledger.Write.Spreadsheet: data Span
+ Hledger.Write.Spreadsheet: headerCell :: Lines borders => Text -> Cell borders Text
+ Hledger.Write.Spreadsheet: horizontalSpan :: (Lines border, Monoid text) => [a] -> Cell border text -> [Cell border text]
+ Hledger.Write.Spreadsheet: instance Data.Foldable.Foldable Hledger.Write.Spreadsheet.Border
+ Hledger.Write.Spreadsheet: instance GHC.Base.Applicative Hledger.Write.Spreadsheet.Border
+ Hledger.Write.Spreadsheet: instance GHC.Base.Functor (Hledger.Write.Spreadsheet.Cell border)
+ Hledger.Write.Spreadsheet: instance GHC.Base.Functor Hledger.Write.Spreadsheet.Border
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Eq Hledger.Write.Spreadsheet.NumLines
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Eq Hledger.Write.Spreadsheet.Span
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Eq lines => GHC.Classes.Eq (Hledger.Write.Spreadsheet.Border lines)
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Ord Hledger.Write.Spreadsheet.NumLines
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Ord lines => GHC.Classes.Ord (Hledger.Write.Spreadsheet.Border lines)
+ Hledger.Write.Spreadsheet: instance GHC.Show.Show Hledger.Write.Spreadsheet.NumLines
+ Hledger.Write.Spreadsheet: instance GHC.Show.Show lines => GHC.Show.Show (Hledger.Write.Spreadsheet.Border lines)
+ Hledger.Write.Spreadsheet: instance Hledger.Write.Spreadsheet.Lines ()
+ Hledger.Write.Spreadsheet: instance Hledger.Write.Spreadsheet.Lines Hledger.Write.Spreadsheet.NumLines
+ Hledger.Write.Spreadsheet: integerCell :: Lines border => Integer -> Cell border Text
+ Hledger.Write.Spreadsheet: newtype Class
+ Hledger.Write.Spreadsheet: noBorder :: Lines border => Border border
+ Hledger.Write.Spreadsheet: noLine :: Lines border => border
+ Hledger.Write.Spreadsheet: rawTableContent :: [[Cell border text]] -> [[text]]
+ Hledger.Write.Spreadsheet: textFromClass :: Class -> Text
+ Hledger.Write.Spreadsheet: transpose :: [[Cell border text]] -> [[Cell border text]]
+ Hledger.Write.Spreadsheet: transposeCell :: Cell border text -> Cell border text
+ Text.Tabular.AsciiWide: instance Data.Default.Internal.Default Text.Tabular.AsciiWide.TableOpts
+ Text.WideString: WideBuilder :: !Builder -> !Int -> WideBuilder
+ Text.WideString: [wbBuilder] :: WideBuilder -> !Builder
+ Text.WideString: [wbWidth] :: WideBuilder -> !Int
+ Text.WideString: data WideBuilder
+ Text.WideString: instance GHC.Base.Monoid Text.WideString.WideBuilder
+ Text.WideString: instance GHC.Base.Semigroup Text.WideString.WideBuilder
+ Text.WideString: instance GHC.Show.Show Text.WideString.WideBuilder
+ Text.WideString: wbFromText :: Text -> WideBuilder
+ Text.WideString: wbToText :: WideBuilder -> Text
+ Text.WideString: wbUnpack :: WideBuilder -> String
- Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(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: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data: PriceDirective :: Day -> CommoditySymbol -> Amount -> PriceDirective
+ Hledger.Data: PriceDirective :: SourcePos -> Day -> CommoditySymbol -> Amount -> PriceDirective
- Hledger.Data.Account: clipAccountsAndAggregate :: Maybe Int -> [Account] -> [Account]
+ Hledger.Data.Account: clipAccountsAndAggregate :: DepthSpec -> [Account] -> [Account]
- Hledger.Data.AccountName: clipAccountName :: Maybe Int -> AccountName -> AccountName
+ Hledger.Data.AccountName: clipAccountName :: DepthSpec -> AccountName -> AccountName
- Hledger.Data.AccountName: clipOrEllipsifyAccountName :: Maybe Int -> AccountName -> AccountName
+ Hledger.Data.AccountName: clipOrEllipsifyAccountName :: DepthSpec -> AccountName -> AccountName
- Hledger.Data.Amount: AmountFormat :: Bool -> Bool -> Maybe [CommoditySymbol] -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> Bool -> Bool -> AmountFormat
+ Hledger.Data.Amount: AmountFormat :: Bool -> Bool -> Maybe [CommoditySymbol] -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> Bool -> Bool -> Bool -> AmountFormat
- Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP BalancingOpts
+ Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a3Ozu => Lens' c_a3Ozu BalancingOpts
- Hledger.Data.Balancing: class HasBalancingOpts c_a3IqP
+ Hledger.Data.Balancing: class HasBalancingOpts c_a3Ozu
- Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP (Maybe (Map CommoditySymbol AmountStyle))
+ Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a3Ozu => Lens' c_a3Ozu (Maybe (Map CommoditySymbol AmountStyle))
- Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP Bool
+ Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a3Ozu => Lens' c_a3Ozu Bool
- Hledger.Data.Balancing: infer_balancing_costs :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP Bool
+ Hledger.Data.Balancing: infer_balancing_costs :: HasBalancingOpts c_a3Ozu => Lens' c_a3Ozu Bool
- Hledger.Data.JournalChecks: journalCheckRecentAssertions :: Day -> Journal -> Either String ()
+ Hledger.Data.JournalChecks: journalCheckRecentAssertions :: Journal -> Either String ()
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(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: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data.Types: PriceDirective :: Day -> CommoditySymbol -> Amount -> PriceDirective
+ Hledger.Data.Types: PriceDirective :: SourcePos -> Day -> CommoditySymbol -> Amount -> PriceDirective
- Hledger.Query: queryDepth :: Query -> Maybe Int
+ Hledger.Query: queryDepth :: Query -> DepthSpec
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> Bool -> DateSpan -> Bool -> Bool -> Bool -> BalancingOpts -> Bool -> Bool -> Day -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> Bool -> Bool -> DateSpan -> Bool -> Bool -> Bool -> BalancingOpts -> Bool -> Bool -> Day -> InputOpts
- Hledger.Read.Common: aliases :: HasInputOpts c_a3X2k => Lens' c_a3X2k [String]
+ Hledger.Read.Common: aliases :: HasInputOpts c_a43az => Lens' c_a43az [String]
- Hledger.Read.Common: anon__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: anon__ :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: auto__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: auto__ :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: balancingopts :: HasInputOpts c_a3X2k => Lens' c_a3X2k BalancingOpts
+ Hledger.Read.Common: balancingopts :: HasInputOpts c_a43az => Lens' c_a43az BalancingOpts
- Hledger.Read.Common: class HasInputOpts c_a3X2k
+ Hledger.Read.Common: class HasInputOpts c_a43az
- Hledger.Read.Common: defer :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: defer :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: forecast :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe DateSpan)
+ Hledger.Read.Common: forecast :: HasInputOpts c_a43az => Lens' c_a43az (Maybe DateSpan)
- Hledger.Read.Common: infer_costs :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: infer_costs :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: infer_equity :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: infer_equity :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: inputOpts :: HasInputOpts c_a3X2k => Lens' c_a3X2k InputOpts
+ Hledger.Read.Common: inputOpts :: HasInputOpts c_a43az => Lens' c_a43az InputOpts
- Hledger.Read.Common: ioDay :: HasInputOpts c_a3X2k => Lens' c_a3X2k Day
+ Hledger.Read.Common: ioDay :: HasInputOpts c_a43az => Lens' c_a43az Day
- Hledger.Read.Common: mformat :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe StorageFormat)
+ Hledger.Read.Common: mformat :: HasInputOpts c_a43az => Lens' c_a43az (Maybe StorageFormat)
- Hledger.Read.Common: mrules_file :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe FilePath)
+ Hledger.Read.Common: mrules_file :: HasInputOpts c_a43az => Lens' c_a43az (Maybe FilePath)
- Hledger.Read.Common: new__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: new__ :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: new_save :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: new_save :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: pivot :: HasInputOpts c_a3X2k => Lens' c_a3X2k String
+ Hledger.Read.Common: pivot :: HasInputOpts c_a43az => Lens' c_a43az String
- Hledger.Read.Common: rawOptsToInputOpts :: Day -> RawOpts -> InputOpts
+ Hledger.Read.Common: rawOptsToInputOpts :: Day -> Bool -> Bool -> RawOpts -> InputOpts
- Hledger.Read.Common: reportspan :: HasInputOpts c_a3X2k => Lens' c_a3X2k DateSpan
+ Hledger.Read.Common: reportspan :: HasInputOpts c_a43az => Lens' c_a43az DateSpan
- Hledger.Read.Common: strict :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: strict :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.Common: verbose_tags :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.Common: verbose_tags :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> Bool -> DateSpan -> Bool -> Bool -> Bool -> BalancingOpts -> Bool -> Bool -> Day -> InputOpts
+ Hledger.Read.InputOptions: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> Bool -> Bool -> DateSpan -> Bool -> Bool -> Bool -> BalancingOpts -> Bool -> Bool -> Day -> InputOpts
- Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3X2k => Lens' c_a3X2k [String]
+ Hledger.Read.InputOptions: aliases :: HasInputOpts c_a43az => Lens' c_a43az [String]
- Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3X2k => Lens' c_a3X2k BalancingOpts
+ Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a43az => Lens' c_a43az BalancingOpts
- Hledger.Read.InputOptions: class HasInputOpts c_a3X2k
+ Hledger.Read.InputOptions: class HasInputOpts c_a43az
- Hledger.Read.InputOptions: defer :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: defer :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe DateSpan)
+ Hledger.Read.InputOptions: forecast :: HasInputOpts c_a43az => Lens' c_a43az (Maybe DateSpan)
- Hledger.Read.InputOptions: infer_costs :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: infer_costs :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3X2k => Lens' c_a3X2k InputOpts
+ Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a43az => Lens' c_a43az InputOpts
- Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3X2k => Lens' c_a3X2k Day
+ Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a43az => Lens' c_a43az Day
- Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe StorageFormat)
+ Hledger.Read.InputOptions: mformat :: HasInputOpts c_a43az => Lens' c_a43az (Maybe StorageFormat)
- Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe FilePath)
+ Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a43az => Lens' c_a43az (Maybe FilePath)
- Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: new__ :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: new_save :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3X2k => Lens' c_a3X2k String
+ Hledger.Read.InputOptions: pivot :: HasInputOpts c_a43az => Lens' c_a43az String
- Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3X2k => Lens' c_a3X2k DateSpan
+ Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a43az => Lens' c_a43az DateSpan
- Hledger.Read.InputOptions: strict :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: strict :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Read.InputOptions: verbose_tags :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
+ Hledger.Read.InputOptions: verbose_tags :: HasInputOpts c_a43az => Lens' c_a43az Bool
- Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Maybe ConversionOp -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> Bool -> [Text] -> Bool -> Bool -> SortSpec -> Bool -> BalanceCalculation -> BalanceAccumulation -> Maybe Text -> AccountListMode -> Int -> Bool -> 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 -> DepthSpec -> Bool -> Bool -> Bool -> Bool -> StringFormat -> Maybe Text -> Bool -> [Text] -> Bool -> Bool -> SortSpec -> Bool -> BalanceCalculation -> BalanceAccumulation -> Maybe Text -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> Layout -> ReportOpts
- Hledger.Reports.ReportOptions: [depth_] :: ReportOpts -> Maybe Int
+ Hledger.Reports.ReportOptions: [depth_] :: ReportOpts -> DepthSpec
- Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH AccountListMode
+ Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u AccountListMode
- Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH BalanceAccumulation
+ Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u BalanceAccumulation
- Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH BalanceCalculation
+ Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u BalanceCalculation
- Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe Text)
+ Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u (Maybe Text)
- Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a45GH
+ Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a4c6u
- Hledger.Reports.ReportOptions: class HasReportSpec c_a46Nj
+ Hledger.Reports.ReportOptions: class HasReportSpec c_a4dgc
- Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe ConversionOp)
+ Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u (Maybe ConversionOp)
- Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: depth :: HasReportOpts a => ReportableLens' a (Maybe Int)
+ Hledger.Reports.ReportOptions: depth :: HasReportOpts a => ReportableLens' a DepthSpec
- Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe Int)
+ Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u DepthSpec
- Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Int
+ Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Int
- Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH StringFormat
+ Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u StringFormat
- Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Interval
+ Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Interval
- Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Layout
+ Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Layout
- Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe NormalSign)
+ Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u (Maybe NormalSign)
- Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Period
+ Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Period
- Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH [Text]
+ Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u [Text]
- Hledger.Reports.ReportOptions: rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts
+ Hledger.Reports.ReportOptions: rawOptsToReportOpts :: Day -> Bool -> RawOpts -> ReportOpts
- Hledger.Reports.ReportOptions: rawOptsToReportSpec :: Day -> RawOpts -> Either String ReportSpec
+ Hledger.Reports.ReportOptions: rawOptsToReportSpec :: Day -> Bool -> RawOpts -> Either String ReportSpec
- Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH ReportOpts
+ Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u ReportOpts
- Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a46Nj => Lens' c_a46Nj ReportSpec
+ Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a4dgc => Lens' c_a4dgc ReportSpec
- Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a46Nj => Lens' c_a46Nj Day
+ Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a4dgc => Lens' c_a4dgc Day
- Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a46Nj => Lens' c_a46Nj Query
+ Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a4dgc => Lens' c_a4dgc Query
- Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a46Nj => Lens' c_a46Nj [QueryOpt]
+ Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a4dgc => Lens' c_a4dgc [QueryOpt]
- Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a46Nj => Lens' c_a46Nj ReportOpts
+ Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a4dgc => Lens' c_a4dgc ReportOpts
- Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: sortspec :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH SortSpec
+ Hledger.Reports.ReportOptions: sortspec :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u SortSpec
- Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH [Status]
+ Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u [Status]
- Hledger.Reports.ReportOptions: summary_only :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: summary_only :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
+ Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u Bool
- Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe ValuationType)
+ Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a4c6u => Lens' c_a4c6u (Maybe ValuationType)
- Hledger.Reports.ReportTypes: DisplayName :: AccountName -> AccountName -> Int -> DisplayName
+ Hledger.Reports.ReportTypes: DisplayName :: AccountName -> AccountName -> NumberOfIndents -> DisplayName
- Hledger.Utils.IO: colorOption :: String
+ Hledger.Utils.IO: colorOption :: IO YNA
- Hledger.Utils.IO: rgb' :: Word8 -> Word8 -> Word8 -> String -> String
+ Hledger.Utils.IO: rgb' :: Float -> Float -> Float -> String -> String
- Hledger.Utils.IO: useColorOnStderr :: Bool
+ Hledger.Utils.IO: useColorOnStderr :: IO Bool
- Hledger.Utils.IO: useColorOnStdout :: Bool
+ Hledger.Utils.IO: useColorOnStdout :: IO Bool
- Hledger.Write.Ods: printFods :: TextEncoding -> Map Text ((Maybe Int, Maybe Int), [[Cell Text]]) -> Text
+ Hledger.Write.Ods: printFods :: TextEncoding -> Map Text ((Int, Int), [[Cell NumLines Text]]) -> Text
- Hledger.Write.Spreadsheet: Cell :: Type -> Style -> text -> Cell text
+ Hledger.Write.Spreadsheet: Cell :: Type -> Border border -> Style -> Span -> Text -> Class -> text -> Cell border text
- Hledger.Write.Spreadsheet: [cellContent] :: Cell text -> text
+ Hledger.Write.Spreadsheet: [cellContent] :: Cell border text -> text
- Hledger.Write.Spreadsheet: [cellStyle] :: Cell text -> Style
+ Hledger.Write.Spreadsheet: [cellStyle] :: Cell border text -> Style
- Hledger.Write.Spreadsheet: [cellType] :: Cell text -> Type
+ Hledger.Write.Spreadsheet: [cellType] :: Cell border text -> Type
- Hledger.Write.Spreadsheet: data Cell text
+ Hledger.Write.Spreadsheet: data Cell border text
- Hledger.Write.Spreadsheet: defaultCell :: text -> Cell text
+ Hledger.Write.Spreadsheet: defaultCell :: Lines border => text -> Cell border text
- Hledger.Write.Spreadsheet: emptyCell :: Monoid text => Cell text
+ Hledger.Write.Spreadsheet: emptyCell :: (Lines border, Monoid text) => Cell border text

Files

CHANGES.md view
@@ -22,6 +22,39 @@ For user-visible changes, see the hledger package changelog.  +# 2024-12-09 1.41++Breaking changes++- New/refactored modules (Hledger.Write.*) and types (Spreadsheet) to help+  abstract rendering in various output formats, eg HTML, FODS and beancount.+  Spreadsheet is an abstraction for tabular reports, in addition to the+  tabular package we already use; there may be some overlap.+  (Henning Thielemann)++- Rename displayDepth/prrDepth to displayIndent/prrIndent, and make them+  correspond to the number of indentation steps.+  (These are about indentation for rendering, not account depth.) [#2246]++Improvements++- Add Hledger.Data.Currency, currencySymbolToCode, currencyCodeToSymbol+- AmountFormat: add displayQuotes flag to control enclosing quotes+- InputOpts: add `posting_account_tags_` flag to control account tags on postings+- Support ghc 9.10 and base 4.20.+  Note, when built with ghc 9.10.1, hledger error messages are displayed with two extra trailing newlines.++Other API/doc changes++- Hledger.Utils.IO: cleanup; rgb' now takes Float arguments instead of Word8+- rename jinferredcommodities to jinferredcommoditystyles+- rename jcommodities to jdeclaredcommodities+- move/rename nullsourcepos+- document isBlockActive, matcherMatches+- posting*AsLines: fix some docs+++ # 1.40 2024-09-09  Breaking changes
Hledger/Data.hs view
@@ -13,6 +13,7 @@                module Hledger.Data.AccountName,                module Hledger.Data.Amount,                module Hledger.Data.Balancing,+               module Hledger.Data.Currency,                module Hledger.Data.Dates,                module Hledger.Data.Errors,                module Hledger.Data.Journal,@@ -38,6 +39,7 @@ import Hledger.Data.AccountName import Hledger.Data.Amount import Hledger.Data.Balancing+import Hledger.Data.Currency import Hledger.Data.Dates import Hledger.Data.Errors import Hledger.Data.Journal@@ -58,8 +60,9 @@ tests_Data = testGroup "Data" [    tests_AccountName   ,tests_Amount-  ,tests_Dates   ,tests_Balancing+  -- ,tests_Currency+  ,tests_Dates   ,tests_Journal   ,tests_Ledger   ,tests_Posting
Hledger/Data/Account.hs view
@@ -34,7 +34,10 @@  import qualified Data.HashSet as HS import qualified Data.HashMap.Strict as HM-import Data.List (find, foldl', sortOn)+import Data.List (find, sortOn)+#if !MIN_VERSION_base(4,20,0)+import Data.List (foldl')+#endif import Data.List.Extra (groupOn) import qualified Data.Map as M import Data.Ord (Down(..))@@ -172,11 +175,11 @@ -- | Remove subaccounts below the specified depth, aggregating their balance at the depth limit -- (accounts at the depth limit will have any sub-balances merged into their exclusive balance). -- If the depth is Nothing, return the original accounts-clipAccountsAndAggregate :: Maybe Int -> [Account] -> [Account]-clipAccountsAndAggregate Nothing  as = as-clipAccountsAndAggregate (Just d) as = combined+clipAccountsAndAggregate :: DepthSpec -> [Account] -> [Account]+clipAccountsAndAggregate (DepthSpec Nothing []) as = as+clipAccountsAndAggregate d                      as = combined     where-      clipped  = [a{aname=clipOrEllipsifyAccountName (Just d) $ aname a} | a <- as]+      clipped  = [a{aname=clipOrEllipsifyAccountName d $ aname a} | a <- as]       combined = [a{aebalance=maSum $ map aebalance same}                  | same@(a:_) <- groupOn aname clipped] {-
Hledger/Data/AccountName.hs view
@@ -21,7 +21,9 @@   ,accountNameTreeFrom   ,accountSummarisedName   ,accountNameInferType+  ,accountNameInferTypeExcept   ,accountNameType+  ,defaultBaseConversionAccount   ,assetAccountRegex   ,cashAccountRegex   ,liabilityAccountRegex@@ -33,6 +35,7 @@   ,acctsepchar   ,clipAccountName   ,clipOrEllipsifyAccountName+  ,getAccountNameClippedDepth   ,elideAccountName   ,escapeName   ,expandAccountName@@ -53,17 +56,16 @@   ,concatAccountNames   ,accountNameApplyAliases   ,accountNameApplyAliasesMemo-  ,accountNameToBeancount-  ,beancountTopLevelAccounts   ,tests_AccountName ) where  import Control.Applicative ((<|>)) import Control.Monad (foldM)-import Data.Foldable (asum, toList)+import Data.Foldable (asum, find, toList) import qualified Data.List.NonEmpty as NE import qualified Data.Map as M+import Data.Maybe (mapMaybe) import Data.MemoUgly (memo) import qualified Data.Set as S import Data.Text (Text)@@ -74,7 +76,6 @@  import Hledger.Data.Types hiding (asubs) import Hledger.Utils-import Data.Char (isDigit, isLetter) import Data.List (partition)  -- $setup@@ -86,27 +87,9 @@ acctsep :: Text acctsep = T.pack [acctsepchar] --- accountNameComponents :: AccountName -> [String]--- accountNameComponents = splitAtElement acctsepchar--accountNameComponents :: AccountName -> [Text]-accountNameComponents = T.splitOn acctsep--accountNameFromComponents :: [Text] -> AccountName-accountNameFromComponents = T.intercalate acctsep--accountLeafName :: AccountName -> Text-accountLeafName = last . accountNameComponents---- | Truncate all account name components but the last to two characters.-accountSummarisedName :: AccountName -> Text-accountSummarisedName a-  --   length cs > 1 = take 2 (head cs) ++ ":" ++ a'-  | length cs > 1 = T.intercalate ":" (map (T.take 2) $ init cs) <> ":" <> a'-  | otherwise     = a'-    where-      cs = accountNameComponents a-      a' = accountLeafName a+-- The base conversion account name used by --infer-equity,+-- when no other account of type V/Conversion has been declared.+defaultBaseConversionAccount = "equity:conversion"  -- | Regular expressions matching common English top-level account names, -- used as a fallback when account types are not declared.@@ -114,7 +97,7 @@ 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?(:|$)"+conversionAccountRegex = toRegexCI' "^equity:(trade|trades|trading|conversion)(:|$)" revenueAccountRegex    = toRegexCI' "^(income|revenue)s?(:|$)" expenseAccountRegex    = toRegexCI' "^expenses?(:|$)" @@ -131,6 +114,15 @@   | regexMatchText expenseAccountRegex    a = Just Expense   | otherwise                               = Nothing +-- | Like accountNameInferType, but exclude the provided types from the guesses.+-- Used eg to prevent "equity:conversion" being inferred as Conversion when a different+-- account has been declared with that type.+accountNameInferTypeExcept :: [AccountType] -> AccountName -> Maybe AccountType+accountNameInferTypeExcept excludedtypes a =+  case accountNameInferType a of+    Just t | not $ t `elem` excludedtypes -> Just t+    _ -> 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'.@@ -138,6 +130,36 @@ accountNameType atypes a = asum (map (`M.lookup` atypes) $ a : parentAccountNames a)                          <|> accountNameInferType a +-- accountNameComponents :: AccountName -> [String]+-- accountNameComponents = splitAtElement acctsepchar++accountNameComponents :: AccountName -> [Text]+accountNameComponents = T.splitOn acctsep++accountNameFromComponents :: [Text] -> AccountName+accountNameFromComponents = T.intercalate acctsep++accountLeafName :: AccountName -> Text+accountLeafName = last . accountNameComponents++-- | Truncate all account name components but the last to two characters.+accountSummarisedName :: AccountName -> Text+accountSummarisedName a+  --   length cs > 1 = take 2 (head cs) ++ ":" ++ a'+  | length cs > 1 = T.intercalate ":" (map (T.take 2) $ init cs) <> ":" <> a'+  | otherwise     = a'+    where+      cs = accountNameComponents a+      a' = accountLeafName a++-- | The level (depth) of an account name.+--+-- >>> accountNameLevel ""  -- special case+-- 0+-- >>> accountNameLevel "assets"+-- 1+-- >>> accountNameLevel "assets:cash"+-- 2 accountNameLevel :: AccountName -> Int accountNameLevel "" = 0 accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1@@ -315,19 +337,55 @@           | otherwise = done++ss  -- | Keep only the first n components of an account name, where n--- is a positive integer. If n is Just 0, returns the empty string, if n is--- Nothing, return the full name.-clipAccountName :: Maybe Int -> AccountName -> AccountName-clipAccountName Nothing  = id-clipAccountName (Just n) = accountNameFromComponents . take n . accountNameComponents+-- is a positive integer.+clipAccountNameTo :: Int -> AccountName -> AccountName+clipAccountNameTo n = accountNameFromComponents . take n . accountNameComponents --- | Keep only the first n components of an account name, where n--- is a positive integer. If n is Just 0, returns "...", if n is Nothing, return--- the full name.-clipOrEllipsifyAccountName :: Maybe Int -> AccountName -> AccountName-clipOrEllipsifyAccountName (Just 0) = const "..."-clipOrEllipsifyAccountName n        = clipAccountName n+-- | Calculate the depth to which an account name should be clipped for a given+-- 'DepthSpec'.+--+-- First checking whether the account name matches any of the regular+-- expressions controlling depth. If so, clip to the depth of the most specific+-- of those matches, i.e. the one which starts matching the latest as you+-- progress up the parents of the account. Otherwise clip to the flat depth+-- provided, or return the full name if Nothing.+getAccountNameClippedDepth :: DepthSpec -> AccountName -> Maybe Int+getAccountNameClippedDepth (DepthSpec flat regexps) acctName =+    mostSpecificRegexp regexps <|> flat+  where+    -- If any regular expressions match, choose the one with the greatest+    -- specificity and clip to that depth.+    mostSpecificRegexp = fmap snd . foldr takeMax Nothing . mapMaybe matchRegexp+      where+        -- If two regexps match, take the most specific one. If there is a tie,+        -- take the last one (this aligns with the behaviour for flat depths+        -- limiting).+        takeMax (s, d) (Just (s', d')) = Just $ if s'>= s then (s', d') else (s, d)+        takeMax (s, d) Nothing = Just (s, d) +    -- If the regular expression matches the account name, store the specificity and requested depth+    matchRegexp :: (Regexp, Int) -> Maybe (Int, Int)+    matchRegexp (r, d) = if regexMatchText r acctName then Just (getSpecificity r, d) else Nothing+    -- Specificity is the smallest parent of the account which matches the regular expression+    getSpecificity r = maybe maxBound fst $ find (regexMatchText r . snd) acctParents+    acctParents = zip [1..] . initDef [] $ expandAccountName acctName++-- | Clip an account name to a given 'DepthSpec', first checking whether it+-- matches any of the regular expressions controlling depth. If so, clip to the+-- depth of the most specific of those matches, i.e. the one which starts+-- matching the latest as you progress up the parents of the account. Otherwise+-- clip to the flat depth provided, or return the full name if Nothing.+clipAccountName :: DepthSpec -> AccountName -> AccountName+clipAccountName ds a = maybe id clipAccountNameTo (getAccountNameClippedDepth ds a) a++-- | As 'clipAccountName', but return '...' if asked to clip to depth 0.+clipOrEllipsifyAccountName :: DepthSpec -> AccountName -> AccountName+clipOrEllipsifyAccountName ds a = go (getAccountNameClippedDepth ds a)+  where+    go Nothing  = a+    go (Just 0) = "..."+    go (Just n) = clipAccountNameTo n a+ -- | Escape an AccountName for use within a regular expression. -- >>> putStr . T.unpack $ escapeName "First?!#$*?$(*) !@^#*? %)*!@#" -- First\?!#\$\*\?\$\(\*\) !@\^#\*\? %\)\*!@#@@ -358,50 +416,6 @@ -- -- | Does this string look like an exact account-matching regular expression ? --isAccountRegex  :: String -> Bool --isAccountRegex s = take 1 s == "^" && take 5 (reverse s) == ")$|:("--type BeancountAccountName = AccountName-type BeancountAccountNameComponent = AccountName---- Convert a hledger account name to a valid Beancount account name.--- It replaces non-supported characters with @-@ (warning: in extreme cases--- separate accounts could end up with the same name), and it capitalises --- each account name part. It also checks that the first part is one of --- Assets, Liabilities, Equity, Income, or Expenses, and if not it raises an error.--- Account aliases (eg --alias) should be used to set these required--- top-level account names if needed.-accountNameToBeancount :: AccountName -> BeancountAccountName-accountNameToBeancount a =-  -- https://beancount.github.io/docs/beancount_language_syntax.html#accounts-  accountNameFromComponents $-  case map (accountNameComponentToBeancount a) $ accountNameComponents a of-    c:_ | c `notElem` beancountTopLevelAccounts -> error' e-      where-        e = T.unpack $ T.unlines [-          beancountAccountErrorMessage a,-          "For Beancount output, all top-level accounts must be (or be aliased to) one of",-          T.intercalate ", " beancountTopLevelAccounts <> "."-          ]-    cs -> cs--accountNameComponentToBeancount :: AccountName -> AccountName -> BeancountAccountNameComponent-accountNameComponentToBeancount acct part =-  case T.uncons part of-    Just (c,_) | not $ isLetter c -> error' e-      where-        e = unlines [-          T.unpack $ beancountAccountErrorMessage acct,-          "For Beancount output, each account name part must begin with a letter."-          ]-    _ -> textCapitalise part'-      where part' = T.map (\c -> if isBeancountAccountChar c then c else '-') part--beancountAccountErrorMessage :: AccountName -> Text-beancountAccountErrorMessage a = "Could not convert \"" <> a <> "\" to a Beancount account name."--isBeancountAccountChar :: Char -> Bool-isBeancountAccountChar c = c `elem` ("-:"::[Char]) || isLetter c || isDigit c--beancountTopLevelAccounts = ["Assets", "Liabilities", "Equity", "Income", "Expenses"]  tests_AccountName = testGroup "AccountName" [    testCase "accountNameTreeFrom" $ do
Hledger/Data/Amount.hs view
@@ -39,6 +39,7 @@  -} +{-# LANGUAGE CPP  #-} {-# LANGUAGE OverloadedStrings  #-} {-# LANGUAGE RecordWildCards    #-} {-# LANGUAGE NamedFieldPuns #-}@@ -175,7 +176,10 @@ import Data.Decimal (DecimalRaw(..), decimalPlaces, normalizeDecimal, roundTo) import Data.Default (Default(..)) import Data.Foldable (toList)-import Data.List (find, foldl', intercalate, intersperse, mapAccumL, partition)+import Data.List (find, intercalate, intersperse, mapAccumL, partition)+#if !MIN_VERSION_base(4,20,0)+import Data.List (foldl')+#endif import Data.List.NonEmpty (NonEmpty(..), nonEmpty) import qualified Data.Map.Strict as M import qualified Data.Set as S@@ -238,6 +242,7 @@   , displayMaxWidth         :: Maybe Int  -- ^ Maximum width to clip to   , displayCost             :: Bool       -- ^ Whether to display Amounts' costs.   , displayColour           :: Bool       -- ^ Whether to ansi-colourise negative Amounts.+  , displayQuotes           :: Bool       -- ^ Whether to enclose complex symbols in quotes (normally true)   } deriving (Show)  -- | By default, display amounts using @defaultFmt@ amount display options.@@ -256,6 +261,7 @@   , displayMaxWidth         = Nothing   , displayCost             = True   , displayColour           = False+  , displayQuotes           = True   }  -- | Like defaultFmt but show zero amounts with commodity symbol and styling, like non-zero amounts.@@ -651,7 +657,7 @@ showAmountB _ Amount{acommodity="AUTO"} = mempty showAmountB   afmt@AmountFormat{displayCommodity, displayZeroCommodity, displayDigitGroups-                   ,displayForceDecimalMark, displayCost, displayColour}+                   ,displayForceDecimalMark, displayCost, displayColour, displayQuotes}   a@Amount{astyle=style} =     color $ case ascommodityside style of       L -> (if displayCommodity then wbFromText comm <> space else mempty) <> quantity' <> cost@@ -662,7 +668,7 @@       if displayDigitGroups then a else a{astyle=(astyle a){asdigitgroups=Nothing}}     (quantity', comm)       | amountLooksZero a && not displayZeroCommodity = (WideBuilder (TB.singleton '0') 1, "")-      | otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a)+      | otherwise = (quantity, (if displayQuotes then quoteCommoditySymbolIfNeeded else id) $ acommodity a)     space = if not (T.null comm) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty     cost = if displayCost then showAmountCostB afmt a else mempty 
Hledger/Data/Balancing.hs view
@@ -102,10 +102,11 @@             VirtualPosting         -> (l, r)      -- convert this posting's amount to cost,-    -- without getting confused by redundant costs/equity postings+    -- unless it has been marked as a redundant cost (equivalent to some nearby equity conversion postings),+    -- in which case ignore it.     postingBalancingAmount p-      | "_price-matched" `elem` map fst (ptags p) = mixedAmountStripCosts $ pamount p-      | otherwise                                 = mixedAmountCost $ pamount p+      | costPostingTagName `elem` map fst (ptags p) = mixedAmountStripCosts $ pamount p+      | otherwise                                   = mixedAmountCost $ pamount p      -- transaction balancedness is checked at each commodity's display precision     lookszero = mixedAmountLooksZero . atdisplayprecision@@ -792,7 +793,7 @@                (Transaction                   0                   ""-                  nullsourcepos+                  nullsourcepospair                   (fromGregorian 2007 01 28)                   Nothing                   Unmarked@@ -807,7 +808,7 @@                (Transaction                   0                   ""-                  nullsourcepos+                  nullsourcepospair                   (fromGregorian 2007 01 28)                   Nothing                   Unmarked@@ -824,7 +825,7 @@              (Transaction                 0                 ""-                nullsourcepos+                nullsourcepospair                 (fromGregorian 2007 01 28)                 Nothing                 Unmarked@@ -840,7 +841,7 @@              (Transaction                 0                 ""-                nullsourcepos+                nullsourcepospair                 (fromGregorian 2007 01 28)                 Nothing                 Unmarked@@ -858,7 +859,7 @@             (Transaction                0                ""-               nullsourcepos+               nullsourcepospair                (fromGregorian 2011 01 01)                Nothing                Unmarked@@ -875,7 +876,7 @@             (Transaction                0                ""-               nullsourcepos+               nullsourcepospair                (fromGregorian 2011 01 01)                Nothing                Unmarked@@ -894,7 +895,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked@@ -912,7 +913,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked@@ -930,7 +931,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked@@ -945,7 +946,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked@@ -960,7 +961,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked@@ -979,7 +980,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked@@ -997,7 +998,7 @@           Transaction             0             ""-            nullsourcepos+            nullsourcepospair             (fromGregorian 2009 01 01)             Nothing             Unmarked
+ Hledger/Data/Currency.hs view
@@ -0,0 +1,160 @@+{-|++Currency names, symbols and codes.+Reference:+- https://www.xe.com/symbols+- https://www.xe.com/currency++-}++{-# LANGUAGE OverloadedStrings    #-}++module Hledger.Data.Currency (+  currencies,+  currencySymbolToCode,+  currencyCodeToSymbol,+)+where+import qualified Data.Map as M+import           Data.Text (Text)++-- | An ISO 4217 currency code, like EUR. Usually three upper case letters.+type CurrencyCode = Text++-- | A traditional currency symbol, like $. Usually one character, sometimes more.+-- Different from hledger's more general "CommoditySymbol" type.+type CurrencySymbol = Text++-- | Look for a ISO 4217 currency code corresponding to this currency symbol.+--+-- >>> currencySymbolToCode ""+-- Nothing+-- >>> currencySymbolToCode "$"+-- Just "USD"+currencySymbolToCode :: CurrencySymbol -> Maybe CurrencyCode+currencySymbolToCode s = M.lookup s currencyCodesBySymbol++-- | Look for a currency symbol corresponding to this ISO 4217 currency code.+--+-- >>> currencyCodeToSymbol "CZK"  -- Just "Kč"+-- Just "K\269"+currencyCodeToSymbol :: CurrencyCode -> Maybe CurrencySymbol+currencyCodeToSymbol c = M.lookup c currencySymbolsByCode++currencyCodesBySymbol = M.fromList [(s,c) | (_,c,s) <- currencies]+currencySymbolsByCode = M.fromList [(c,s) | (_,c,s) <- currencies]++currencies = [+  -- country and currency name           ISO 4217 code  symbol+  ("Albania Lek",                               "ALL",  "Lek"),+  ("Afghanistan Afghani",                       "AFN",  "؋"),+  ("Argentina Peso",                            "ARS",  "$"),+  ("Aruba Guilder",                             "AWG",  "ƒ"),+  ("Australia Dollar",                          "AUD",  "$"),+  ("Azerbaijan Manat",                          "AZN",  "₼"),+  ("Bahamas Dollar",                            "BSD",  "$"),+  ("Barbados Dollar",                           "BBD",  "$"),+  ("Belarus Ruble",                             "BYN",  "Br"),+  ("Belize Dollar",                             "BZD",  "BZ$"),+  ("Bermuda Dollar",                            "BMD",  "$"),+  ("Bolivia Bolíviano",                         "BOB",  "$b"),+  ("Bosnia and Herzegovina Convertible Mark",   "BAM",  "KM"),+  ("Botswana Pula",                             "BWP",  "P"),+  ("Bulgaria Lev",                              "BGN",  "лв"),+  ("Brazil Real",                               "BRL",  "R$"),+  ("Brunei Darussalam Dollar",                  "BND",  "$"),+  ("Cambodia Riel",                             "KHR",  "៛"),+  ("Canada Dollar",                             "CAD",  "$"),+  ("Cayman Islands Dollar",                     "KYD",  "$"),+  ("Chile Peso",                                "CLP",  "$"),+  ("China Yuan Renminbi",                       "CNY",  "¥"),+  ("Colombia Peso",                             "COP",  "$"),+  ("Costa Rica Colon",                          "CRC",  "₡"),+  ("Croatia Kuna",                              "HRK",  "kn"),+  ("Cuba Peso",                                 "CUP",  "₱"),+  ("Czech Republic Koruna",                     "CZK",  "Kč"),+  ("Denmark Krone",                             "DKK",  "kr"),+  ("Dominican Republic Peso",                   "DOP",  "RD$"),+  ("East Caribbean Dollar",                     "XCD",  "$"),+  ("Egypt Pound",                               "EGP",  "£"),+  ("El Salvador Colon",                         "SVC",  "$"),+  ("Euro Member Countries",                     "EUR",  "€"),+  ("Falkland Islands (Malvinas) Pound",         "FKP",  "£"),+  ("Fiji Dollar",                               "FJD",  "$"),+  ("Ghana Cedi",                                "GHS",  "¢"),+  ("Gibraltar Pound",                           "GIP",  "£"),+  ("Guatemala Quetzal",                         "GTQ",  "Q"),+  ("Guernsey Pound",                            "GGP",  "£"),+  ("Guyana Dollar",                             "GYD",  "$"),+  ("Honduras Lempira",                          "HNL",  "L"),+  ("Hong Kong Dollar",                          "HKD",  "$"),+  ("Hungary Forint",                            "HUF",  "Ft"),+  ("Iceland Krona",                             "ISK",  "kr"),+  ("India Rupee",                               "INR",  "₹"),+  ("Indonesia Rupiah",                          "IDR",  "Rp"),+  ("Iran Rial",                                 "IRR",  "﷼"),+  ("Isle of Man Pound",                         "IMP",  "£"),+  ("Israel Shekel",                             "ILS",  "₪"),+  ("Jamaica Dollar",                            "JMD",  "J$"),+  ("Japan Yen",                                 "JPY",  "¥"),+  ("Jersey Pound",                              "JEP",  "£"),+  ("Kazakhstan Tenge",                          "KZT",  "лв"),+  ("Korea (North) Won",                         "KPW",  "₩"),+  ("Korea (South) Won",                         "KRW",  "₩"),+  ("Kyrgyzstan Som",                            "KGS",  "лв"),+  ("Laos Kip",                                  "LAK",  "₭"),+  ("Lebanon Pound",                             "LBP",  "£"),+  ("Liberia Dollar",                            "LRD",  "$"),+  ("Macedonia Denar",                           "MKD",  "ден"),+  ("Malaysia Ringgit",                          "MYR",  "RM"),+  ("Mauritius Rupee",                           "MUR",  "₨"),+  ("Mexico Peso",                               "MXN",  "$"),+  ("Mongolia Tughrik",                          "MNT",  "₮"),+  ("Mozambique Metical",                        "MZN",  "MT"),+  ("Namibia Dollar",                            "NAD",  "$"),+  ("Nepal Rupee",                               "NPR",  "₨"),+  ("Netherlands Antilles Guilder",              "ANG",  "ƒ"),+  ("New Zealand Dollar",                        "NZD",  "$"),+  ("Nicaragua Cordoba",                         "NIO",  "C$"),+  ("Nigeria Naira",                             "NGN",  "₦"),+  ("Norway Krone",                              "NOK",  "kr"),+  ("Oman Rial",                                 "OMR",  "﷼"),+  ("Pakistan Rupee",                            "PKR",  "₨"),+  ("Panama Balboa",                             "PAB",  "B/."),+  ("Paraguay Guarani",                          "PYG",  "Gs"),+  ("Peru Sol",                                  "PEN",  "S/."),+  ("Philippines Peso",                          "PHP",  "₱"),+  ("Poland Zloty",                              "PLN",  "zł"),+  ("Qatar Riyal",                               "QAR",  "﷼"),+  ("Romania Leu",                               "RON",  "lei"),+  ("Russia Ruble",                              "RUB",  "₽"),+  ("Saint Helena Pound",                        "SHP",  "£"),+  ("Saudi Arabia Riyal",                        "SAR",  "﷼"),+  ("Serbia Dinar",                              "RSD",  "Дин."),+  ("Seychelles Rupee",                          "SCR",  "₨"),+  ("Singapore Dollar",                          "SGD",  "$"),+  ("Solomon Islands Dollar",                    "SBD",  "$"),+  ("Somalia Shilling",                          "SOS",  "S"),+  ("South Africa Rand",                         "ZAR",  "R"),+  ("Sri Lanka Rupee",                           "LKR",  "₨"),+  ("Sweden Krona",                              "SEK",  "kr"),+  ("Switzerland Franc",                         "CHF",  "CHF"),+  ("Suriname Dollar",                           "SRD",  "$"),+  ("Syria Pound",                               "SYP",  "£"),+  ("Taiwan New Dollar",                         "TWD",  "NT$"),+  ("Thailand Baht",                             "THB",  "฿"),+  ("Trinidad and Tobago Dollar",                "TTD",  "TT$"),+  ("Turkey Lira",                               "TRY",  "₺"),+  ("Tuvalu Dollar",                             "TVD",  "$"),+  ("Ukraine Hryvnia",                           "UAH",  "₴"),+  ("United Kingdom Pound",                      "GBP",  "£"),+  ("United States Dollar",                      "USD",  "$"),+  ("Uruguay Peso",                              "UYU",  "$U"),+  ("Uzbekistan Som",                            "UZS",  "лв"),+  ("Venezuela Bolívar",                         "VEF",  "Bs"),+  ("Viet Nam Dong",                             "VND",  "₫"),+  ("Yemen Rial",                                "YER",  "﷼"),+  ("Zimbabwe Dollar",                           "ZWD",  "Z$")+  ]++-- tests_Currency = testGroup "Currency" []
Hledger/Data/Errors.hs view
@@ -7,6 +7,7 @@  module Hledger.Data.Errors (   makeAccountTagErrorExcerpt,+  makePriceDirectiveErrorExcerpt,   makeTransactionErrorExcerpt,   makePostingErrorExcerpt,   makePostingAccountErrorExcerpt,@@ -27,7 +28,10 @@ import Data.Maybe import Safe (headMay) import Hledger.Data.Posting (isVirtual)+import Hledger.Data.Dates (showDate)+import Hledger.Data.Amount (showCommoditySymbol, showAmount) + -- | Given an account name and its account directive, and a problem tag within the latter: -- render it as a megaparsec-style excerpt, showing the original line number and -- marked column or region.@@ -38,10 +42,10 @@ makeAccountTagErrorExcerpt (a, adi) _t = (f, l, merrcols, ex)   -- XXX findtxnerrorcolumns is awkward, I don't think this is the final form   where-    (SourcePos f pos _) = adisourcepos adi+    SourcePos f pos _ = adisourcepos adi     l = unPos pos     txt   = showAccountDirective (a, adi) & textChomp & (<>"\n")-    ex = decorateTagErrorExcerpt l merrcols txt+    ex = decorateExcerpt l merrcols txt     -- Calculate columns which will help highlight the region in the excerpt     -- (but won't exactly match the real data, so won't be shown in the main error line)     merrcols = Nothing@@ -55,9 +59,10 @@   "account " <> a   <> (if not $ T.null adicomment then "    ; " <> adicomment else "") --- | Add megaparsec-style left margin, line number, and optional column marker(s).-decorateTagErrorExcerpt :: Int -> Maybe (Int, Maybe Int) -> Text -> Text-decorateTagErrorExcerpt l mcols txt =+-- | Decorate a data excerpt with megaparsec-style left margin, line number,+-- and marker/underline for the column(s) if known, for inclusion in an error message.+decorateExcerpt :: Int -> Maybe (Int, Maybe Int) -> Text -> Text+decorateExcerpt l mcols txt =   T.unlines $ ls' <> colmarkerline <> map (lineprefix<>) ms   where     (ls,ms) = splitAt 1 $ T.lines txt@@ -70,8 +75,28 @@     lineprefix = T.replicate marginw " " <> "| "       where  marginw = length (show l) + 1 -_showAccountDirective = undefined+-- | Given a problem price directive,+-- and maybe a function to calculate the error region's column(s) (currently ignored):+-- generate a megaparsec-style error message with highlighted excerpt.+-- Returns the source file path, line number, column(s) if known, and the rendered excerpt,+-- or as much of these as possible.+-- Columns will be accurate for the rendered error message, not for the original journal entry.+makePriceDirectiveErrorExcerpt :: PriceDirective -> Maybe (PriceDirective -> Text -> Maybe (Int, Maybe Int)) -> (FilePath, Int, Maybe (Int, Maybe Int), Text)+makePriceDirectiveErrorExcerpt pd _finderrorcolumns = (file, line, merrcols, excerpt)+  where+    SourcePos file pos _ = pdsourcepos pd+    line = unPos pos+    merrcols = Nothing+    excerpt = decorateExcerpt line merrcols $ showPriceDirective pd <> "\n" +showPriceDirective :: PriceDirective -> Text+showPriceDirective PriceDirective{..} = T.unwords [+   "P"+  ,showDate pddate+  ,showCommoditySymbol pdcommodity+  ,T.pack $ showAmount pdamount +  ]+ -- | Given a problem transaction and a function calculating the best -- column(s) for marking the error region: -- render it as a megaparsec-style excerpt, showing the original line number@@ -83,7 +108,7 @@ makeTransactionErrorExcerpt t findtxnerrorcolumns = (f, tl, merrcols, ex)   -- XXX findtxnerrorcolumns is awkward, I don't think this is the final form   where-    (SourcePos f tpos _) = fst $ tsourcepos t+    SourcePos f tpos _ = fst $ tsourcepos t     tl = unPos tpos     txntxt = showTransaction t & textChomp & (<>"\n")     merrcols = findtxnerrorcolumns t
Hledger/Data/Journal.hs view
@@ -1,9 +1,10 @@+{-# LANGUAGE CPP                 #-} {-# LANGUAGE NamedFieldPuns      #-} {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE Rank2Types          #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections       #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE RecordWildCards     #-}  {-| @@ -30,8 +31,7 @@   journalCommodityStylesWith,   journalToCost,   journalInferEquityFromCosts,-  journalInferCostsFromEquity,-  journalMarkRedundantCosts,+  journalTagCostsAndEquityAndMaybeInferCosts,   journalReverse,   journalSetLastReadTime,   journalRenumberAccountDeclarations,@@ -95,11 +95,13 @@   journalAccountTypes,   journalAddAccountTypes,   journalPostingsAddAccountTags,+  defaultBaseConversionAccount,   -- journalPrices,-  journalConversionAccount,+  journalBaseConversionAccount,   journalConversionAccounts,   -- * Misc   canonicalStyleFrom,+   nulljournal,   journalConcat,   journalNumberTransactions,@@ -122,7 +124,10 @@ import Data.Char (toUpper, isDigit) import Data.Default (Default(..)) import Data.Foldable (toList)-import Data.List ((\\), find, foldl', sortBy, union, intercalate)+import Data.List ((\\), find, sortBy, union, intercalate)+#if !MIN_VERSION_base(4,20,0)+import Data.List (foldl')+#endif import Data.List.Extra (nubSort) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList)@@ -149,6 +154,7 @@ import Data.Ord (comparing) import Hledger.Data.Dates (nulldate) import Data.List (sort)+import Data.Function ((&)) -- import Data.Function ((&))  @@ -178,7 +184,7 @@              (length $ jtxns j)              (length accounts)              (show accounts)-             (show $ jinferredcommodities j)+             (show $ jinferredcommoditystyles j)              -- ++ (show $ journalTransactions l)              where accounts = filter (/= "root") $ flatten $ journalAccountNameTree j @@ -198,9 +204,9 @@   ,"jdeclaredaccounttags: "      <> shw jdeclaredaccounttags   ,"jdeclaredaccounttypes: "     <> shw jdeclaredaccounttypes   ,"jaccounttypes: "             <> shw jaccounttypes+  ,"jdeclaredcommodities: "      <> shw jdeclaredcommodities+  ,"jinferredcommoditystyles: "  <> shw jinferredcommoditystyles   ,"jglobalcommoditystyles: "    <> shw jglobalcommoditystyles-  ,"jcommodities: "              <> shw jcommodities-  ,"jinferredcommodities: "      <> shw jinferredcommodities   ,"jpricedirectives: "          <> shw jpricedirectives   ,"jinferredmarketprices: "     <> shw jinferredmarketprices   ,"jtxnmodifiers: "             <> shw jtxnmodifiers@@ -273,11 +279,11 @@     -- ,jglobalcommoditystyles :: M.Map CommoditySymbol AmountStyle     ,jglobalcommoditystyles     = (<>) (jglobalcommoditystyles j1) (jglobalcommoditystyles j2)     ---    -- ,jcommodities           :: M.Map CommoditySymbol Commodity-    ,jcommodities               = (<>) (jcommodities j1) (jcommodities j2)+    -- ,jdeclaredcommodities           :: M.Map CommoditySymbol Commodity+    ,jdeclaredcommodities               = (<>) (jdeclaredcommodities j1) (jdeclaredcommodities j2)     ---    -- ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle-    ,jinferredcommodities       = (<>) (jinferredcommodities j1) (jinferredcommodities j2)+    -- ,jinferredcommoditystyles   :: M.Map CommoditySymbol AmountStyle+    ,jinferredcommoditystyles       = (<>) (jinferredcommoditystyles j1) (jinferredcommoditystyles j2)     --     --     ,jpricedirectives           = jpricedirectives           j1 <> jpricedirectives           j2@@ -337,8 +343,8 @@   ,jdeclaredaccounttypes      = M.empty   ,jaccounttypes              = M.empty   ,jglobalcommoditystyles     = M.empty-  ,jcommodities               = M.empty-  ,jinferredcommodities       = M.empty+  ,jdeclaredcommodities               = M.empty+  ,jinferredcommoditystyles       = M.empty   ,jpricedirectives           = []   ,jinferredmarketprices      = []   ,jtxnmodifiers              = []@@ -398,11 +404,15 @@  -- | Sorted unique commodity symbols declared by commodity directives in this journal. journalCommoditiesDeclared :: Journal -> [CommoditySymbol]-journalCommoditiesDeclared = M.keys . jcommodities+journalCommoditiesDeclared = M.keys . jdeclaredcommodities --- | Sorted unique commodity symbols declared or inferred from this journal.+-- | Sorted unique commodity symbols mentioned in this journal. journalCommodities :: Journal -> S.Set CommoditySymbol-journalCommodities j = M.keysSet (jcommodities j) <> M.keysSet (jinferredcommodities j)+journalCommodities j =+     M.keysSet (jdeclaredcommodities j)+  <> M.keysSet (jinferredcommoditystyles j)+  <> S.fromList (concatMap pdcommodities $ jpricedirectives j)+      where pdcommodities pd = [pdcommodity pd, acommodity $ pdamount pd]  -- | Unique transaction descriptions used in this journal. journalDescriptions :: Journal -> [Text]@@ -571,29 +581,34 @@ journalAddAccountTypes :: Journal -> Journal journalAddAccountTypes j = j{jaccounttypes = journalAccountTypes j} +-- | An account type inherited from the parent account(s),+-- and whether it was originally declared by an account directive (true) or inferred from an account name (false).+type ParentAccountType = (AccountType, Bool)+ -- | 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-    -- 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.-    t' = settypes Nothing t :: Tree (AccountName, Maybe (AccountType, Bool))+    -- Traverse downward through the account tree, applying any explicitly declared account types,+    -- otherwise inferring account types from account names when possible, and propagating account types downward.+    -- Declared account types (possibly inherited from parent) are preferred, inferred types are used as a fallback.+    t' = setTypeHereAndBelow Nothing t :: Tree (AccountName, Maybe (AccountType, Bool))       where-        settypes :: Maybe (AccountType, Bool) -> Tree AccountName -> Tree (AccountName, Maybe (AccountType, Bool))-        settypes mparenttype (Node a subs) = Node (a, mtype) (map (settypes mtype) subs)+        declaredtypes       = M.keys $ jdeclaredaccounttypes j+        declaredtypesbyname = journalDeclaredAccountTypes j & fmap (,True)+        setTypeHereAndBelow :: Maybe ParentAccountType -> Tree AccountName -> Tree (AccountName, Maybe ParentAccountType)+        setTypeHereAndBelow mparenttype (Node a subs) = Node (a, mnewtype) (map (setTypeHereAndBelow mnewtype) subs)           where-            mtype = M.lookup a declaredtypes <|> minferred-              where -                declaredtypes = (,True) <$> journalDeclaredAccountTypes j-                minferred = if maybe False snd mparenttype-                            then mparenttype-                            else (,False) <$> accountNameInferType a <|> mparenttype+            mnewtype = mthisacctdeclaredtype <|> mparentacctdeclaredtype <|> mthisacctinferredtype <|> mparentacctinferredtype+              where+                mthisacctdeclaredtype   = M.lookup a declaredtypesbyname+                mparentacctdeclaredtype = if       fromMaybe False $ snd <$> mparenttype then mparenttype else Nothing+                mparentacctinferredtype = if not $ fromMaybe True  $ snd <$> mparenttype then mparenttype else Nothing+                mthisacctinferredtype   = accountNameInferTypeExcept declaredtypes a & fmap (,False)  -- XXX not sure about this Except logic.. but for now, tests pass --- | Build a map of the account types explicitly declared for each account.+-- | Build a map from account names to explicitly declared account types. journalDeclaredAccountTypes :: Journal -> M.Map AccountName AccountType journalDeclaredAccountTypes Journal{jdeclaredaccounttypes} =   M.fromList $ concat [map (,t) as | (t,as) <- M.toList jdeclaredaccounttypes]@@ -605,18 +620,17 @@ journalPostingsAddAccountTags j = journalMapPostings addtags j   where addtags p = p `postingAddTags` (journalInheritedAccountTags j $ paccount p) --- | The account to use for automatically generated conversion postings in this journal:--- the first of the journalConversionAccounts.-journalConversionAccount :: Journal -> AccountName-journalConversionAccount = headDef defaultConversionAccount . journalConversionAccounts+-- | The account name to use for conversion postings generated by --infer-equity.+-- This is the first account declared with type V/Conversion,+-- or otherwise the defaultBaseConversionAccount (equity:conversion).+journalBaseConversionAccount :: Journal -> AccountName+journalBaseConversionAccount = headDef defaultBaseConversionAccount . journalConversionAccounts --- | All the accounts declared or inferred as Conversion type in this journal.+-- | All the accounts in this journal which are declared or inferred as V/Conversion type.+-- This does not include new account names which might be generated by --infer-equity, currently. journalConversionAccounts :: Journal -> [AccountName] journalConversionAccounts = M.keys . M.filter (==Conversion) . jaccounttypes --- The fallback account to use for automatically generated conversion postings--- if no account is declared with the Conversion type.-defaultConversionAccount = "equity:conversion"  -- Various kinds of filtering on journals. We do it differently depending -- on the command.@@ -881,9 +895,9 @@   globalstyles <> declaredstyles <> defaultcommoditystyle <> inferredstyles   where     globalstyles          = jglobalcommoditystyles j-    declaredstyles        = M.mapMaybe cformat $ jcommodities j+    declaredstyles        = M.mapMaybe cformat $ jdeclaredcommodities j     defaultcommoditystyle = M.fromList $ catMaybes [jparsedefaultcommodity j]-    inferredstyles        = jinferredcommodities j+    inferredstyles        = jinferredcommoditystyles j  -- | Like journalCommodityStyles, but attach a particular rounding strategy to the styles, -- affecting how they will affect display precisions when applied.@@ -891,14 +905,13 @@ journalCommodityStylesWith r = amountStylesSetRounding r . journalCommodityStyles  -- | Collect and save inferred amount styles for each commodity based on--- the posting amounts in that commodity (excluding price amounts), ie:--- "the format of the first amount, adjusted to the highest precision of all amounts".+-- the posting amounts in that commodity (excluding price amounts). -- Can return an error message eg if inconsistent number formats are found. journalInferCommodityStyles :: Journal -> Either String Journal journalInferCommodityStyles j =   case commodityStylesFromAmounts $ journalStyleInfluencingAmounts j of     Left e   -> Left e-    Right cs -> Right j{jinferredcommodities = dbg7 "journalInferCommodityStyles" cs}+    Right cs -> Right j{jinferredcommoditystyles = dbg7 "journalInferCommodityStyles" cs}  -- | Given a list of amounts, in parse order (roughly speaking; see journalStyleInfluencingAmounts), -- build a map from their commodity names to standard commodity@@ -971,7 +984,7 @@ journalInferMarketPricesFromTransactions :: Journal -> Journal journalInferMarketPricesFromTransactions j =   j{jinferredmarketprices =-       dbg4 "jinferredmarketprices" .+       dbg4With (("jinferredmarketprices:\n"<>) . showMarketPrices) $        map priceDirectiveToMarketPrice .        concatMap postingPriceDirectivesFromCost $        journalPostings j@@ -981,32 +994,23 @@ journalToCost :: ConversionOp -> Journal -> Journal journalToCost cost j@Journal{jtxns=ts} = j{jtxns=map (transactionToCost cost) ts} +-- | Identify and tag (1) equity conversion postings and (2) postings which have (or could have ?) redundant costs.+-- And if the addcosts flag is true, also add any costs which can be inferred from equity conversion postings.+-- This is always called before transaction balancing to tag the redundant-cost postings so they can be ignored.+-- With --infer-costs, it is called again after transaction balancing (when it has more information to work with) to infer costs from equity postings.+-- See transactionTagCostsAndEquityAndMaybeInferCosts for more details, and hledger manual > Cost reporting for more background.+journalTagCostsAndEquityAndMaybeInferCosts :: Bool -> Bool -> Journal -> Either String Journal+journalTagCostsAndEquityAndMaybeInferCosts verbosetags addcosts j = do+  let conversionaccts = journalConversionAccounts j+  ts <- mapM (transactionTagCostsAndEquityAndMaybeInferCosts verbosetags addcosts conversionaccts) $ jtxns j+  return j{jtxns=ts}+ -- | Add equity postings inferred from costs, where needed and possible. -- See hledger manual > Cost reporting. journalInferEquityFromCosts :: Bool -> Journal -> Journal-journalInferEquityFromCosts verbosetags j = journalMapTransactions (transactionAddInferredEquityPostings verbosetags equityAcct) j-  where-    equityAcct = journalConversionAccount j---- | Add costs inferred from equity conversion postings, where needed and possible.--- See hledger manual > Cost reporting.-journalInferCostsFromEquity :: Journal -> Either String Journal-journalInferCostsFromEquity j = do-  ts <- mapM (transactionInferCostsFromEquity False conversionaccts) $ jtxns j-  return j{jtxns=ts}-  where conversionaccts = journalConversionAccounts j---- XXX duplication of the above--- | Do just the internal tagging that is normally done by journalInferCostsFromEquity,--- identifying equity conversion postings and, in particular, postings which have redundant costs.--- Tagging the latter is useful as it allows them to be ignored during transaction balancedness checking.--- And that allows journalInferCostsFromEquity to be postponed till after transaction balancing,--- when it will have more information (amounts) to work with.-journalMarkRedundantCosts :: Journal -> Either String Journal-journalMarkRedundantCosts j = do-  ts <- mapM (transactionInferCostsFromEquity True conversionaccts) $ jtxns j-  return j{jtxns=ts}-  where conversionaccts = journalConversionAccounts j+journalInferEquityFromCosts verbosetags j =+  journalMapTransactions (transactionInferEquityPostings verbosetags equityAcct) j+  where equityAcct = journalBaseConversionAccount j  -- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. -- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol@@ -1241,7 +1245,7 @@          {jtxns = [            txnTieKnot $ Transaction {              tindex=0,-             tsourcepos=nullsourcepos,+             tsourcepos=nullsourcepospair,              tdate=fromGregorian 2008 01 01,              tdate2=Nothing,              tstatus=Unmarked,@@ -1258,7 +1262,7 @@           ,            txnTieKnot $ Transaction {              tindex=0,-             tsourcepos=nullsourcepos,+             tsourcepos=nullsourcepospair,              tdate=fromGregorian 2008 06 01,              tdate2=Nothing,              tstatus=Unmarked,@@ -1275,7 +1279,7 @@           ,            txnTieKnot $ Transaction {              tindex=0,-             tsourcepos=nullsourcepos,+             tsourcepos=nullsourcepospair,              tdate=fromGregorian 2008 06 02,              tdate2=Nothing,              tstatus=Unmarked,@@ -1292,7 +1296,7 @@           ,            txnTieKnot $ Transaction {              tindex=0,-             tsourcepos=nullsourcepos,+             tsourcepos=nullsourcepospair,              tdate=fromGregorian 2008 06 03,              tdate2=Nothing,              tstatus=Cleared,@@ -1309,7 +1313,7 @@           ,            txnTieKnot $ Transaction {              tindex=0,-             tsourcepos=nullsourcepos,+             tsourcepos=nullsourcepospair,              tdate=fromGregorian 2008 10 01,              tdate2=Nothing,              tstatus=Unmarked,@@ -1325,7 +1329,7 @@           ,            txnTieKnot $ Transaction {              tindex=0,-             tsourcepos=nullsourcepos,+             tsourcepos=nullsourcepospair,              tdate=fromGregorian 2008 12 31,              tdate2=Nothing,              tstatus=Unmarked,
Hledger/Data/JournalChecks.hs view
@@ -33,11 +33,11 @@ import Hledger.Data.Journal import Hledger.Data.JournalChecks.Ordereddates import Hledger.Data.JournalChecks.Uniqueleafnames-import Hledger.Data.Posting (isVirtual, postingDate, transactionAllTags)+import Hledger.Data.Posting (isVirtual, postingDate, transactionAllTags, conversionPostingTagName, costPostingTagName, postingAsLines, generatedPostingTagName, generatedTransactionTagName, modifiedTransactionTagName) import Hledger.Data.Types-import Hledger.Data.Amount (amountIsZero, amountsRaw, missingamt, amounts)+import Hledger.Data.Amount (amountIsZero, amountsRaw, missingamt, oneLineFmt, showMixedAmountWith) import Hledger.Data.Transaction (transactionPayee, showTransactionLineFirstPart, partitionAndCheckConversionPostings)-import Data.Time (Day, diffDays)+import Data.Time (diffDays) import Hledger.Utils import Data.Ord import Hledger.Data.Dates (showDate)@@ -67,8 +67,7 @@           ,"Consider adding an account directive. Examples:"           ,""           ,"account %s"-          ,"account %s    ; type:A  ; (L,E,R,X,C,V)"-          ]) f l ex (show a) a a+          ]) f l ex (show a) a         where           (f,l,_mcols,ex) = makePostingAccountErrorExcerpt p @@ -78,44 +77,53 @@ journalCheckBalanceAssertions :: Journal -> Either String () journalCheckBalanceAssertions = fmap (const ()) . journalBalanceTransactions defbalancingopts --- | Check that all the commodities used in this journal's postings have been declared--- by commodity directives, returning an error message otherwise.+-- | Check that all the commodities used in this journal's postings and P directives+-- have been declared by commodity directives, returning an error message otherwise. journalCheckCommodities :: Journal -> Either String ()-journalCheckCommodities j = mapM_ checkcommodities (journalPostings j)+journalCheckCommodities j = do+  mapM_ checkPriceDirectiveCommodities $ jpricedirectives j+  mapM_ checkPostingCommodities $ journalPostings j   where-    checkcommodities p =-      case findundeclaredcomm p of-        Nothing -> Right ()-        Just (comm, _) ->-          Left $ printf (unlines [-           "%s:%d:"-          ,"%s"-          ,"Strict commodity checking is enabled, and"-          ,"commodity %s has not been declared."-          ,"Consider adding a commodity directive. Examples:"-          ,""-          ,"commodity %s1000.00"-          ,"commodity 1.000,00 %s"-          ]) f l ex (show comm) comm comm+    firstUndeclaredOf comms = find (`M.notMember` jdeclaredcommodities j) comms++    errmsg = unlines [+        "%s:%d:"+      ,"%s"+      ,"Strict commodity checking is enabled, and"+      ,"commodity %s has not been declared."+      ,"Consider adding a commodity directive. Examples:"+      ,""+      ,"commodity %s1000.00"+      ,"commodity 1.000,00 %s"+      ]++    checkPriceDirectiveCommodities pd@PriceDirective{pdcommodity=c, pdamount=amt} =+      case firstUndeclaredOf [c, acommodity amt] of+        Nothing   -> Right ()+        Just comm -> Left $ printf errmsg f l ex (show comm) comm comm+          where (f,l,_mcols,ex) = makePriceDirectiveErrorExcerpt pd Nothing++    checkPostingCommodities p =+      case firstundeclaredcomm p of+        Nothing                    -> Right ()+        Just (comm, _inpostingamt) -> Left $ printf errmsg f l ex (show comm) comm comm           where             (f,l,_mcols,ex) = makePostingErrorExcerpt p finderrcols       where-        -- Find the first undeclared commodity symbol in this posting's amount-        -- or balance assertion amount, if any. The boolean will be true if-        -- the undeclared symbol was in the posting amount.-        findundeclaredcomm :: Posting -> Maybe (CommoditySymbol, Bool)-        findundeclaredcomm Posting{pamount=amt,pbalanceassertion} =-          case (findundeclared postingcomms, findundeclared assertioncomms) of+        -- Find the first undeclared commodity symbol in this posting's amount or balance assertion amount, if any.+        -- and whether it was in the posting amount.+        -- XXX The latter is currently unused, could be used to refine the error highlighting ?+        firstundeclaredcomm :: Posting -> Maybe (CommoditySymbol, Bool)+        firstundeclaredcomm Posting{pamount=amt,pbalanceassertion} =+          case (firstUndeclaredOf postingcomms, firstUndeclaredOf assertioncomms) of             (Just c, _) -> Just (c, True)             (_, Just c) -> Just (c, False)             _           -> Nothing           where+            assertioncomms = [acommodity a | Just a <- [baamount <$> pbalanceassertion]]             postingcomms = map acommodity $ filter (not . isIgnorable) $ amountsRaw amt               where-                -- Ignore missing amounts and zero amounts without commodity (#1767)-                isIgnorable a = (T.null (acommodity a) && amountIsZero a) || a == missingamt-            assertioncomms = [acommodity a | Just a <- [baamount <$> pbalanceassertion]]-            findundeclared = find (`M.notMember` jcommodities j)+                isIgnorable a = a==missingamt || (amountIsZero a && T.null (acommodity a))  -- #1767          -- Calculate columns suitable for highlighting the excerpt.         -- We won't show these in the main error line as they aren't@@ -215,7 +223,7 @@       ,"tag %s"       ]) --- | Tag names which have special significance to hledger.+-- | Tag names which have special significance to hledger, and need not be declared for `hledger check tags`. -- Keep synced with check-tags.test and hledger manual > Special tags. builtinTags = [    "date"                   -- overrides a posting's date@@ -225,15 +233,17 @@   ,"assert"                 -- appears on txns generated by close --assert   ,"retain"                 -- appears on txns generated by close --retain   ,"start"                  -- appears on txns generated by close --migrate/--close/--open/--assign-  ,"generated-transaction"  -- with --verbose-tags, appears on generated periodic txns-  ,"generated-posting"      -- with --verbose-tags, appears on generated auto postings-  ,"modified"               -- with --verbose-tags, appears on txns which have had auto postings added-  -- hidden tags used internally (and also queryable):-  ,"_generated-transaction" -- always exists on generated periodic txns-  ,"_generated-posting"     -- always exists on generated auto postings-  ,"_modified"              -- always exists on txns which have had auto postings added-  ,"_conversion-matched"    -- exists on postings which have been matched with a nearby @/@@ cost notation   ]+  -- these tags are used in both hidden and visible form+  <> ts <> map toVisibleTagName ts+  where+    ts = [+       generatedTransactionTagName -- marks txns generated by periodic rule+      ,modifiedTransactionTagName  -- marks txns which have had auto postings added+      ,generatedPostingTagName     -- marks postings which have been generated+      ,costPostingTagName          -- marks equity conversion postings which have been matched with a nearby costful posting+      ,conversionPostingTagName    -- marks costful postings which have been matched with a nearby pair of equity conversion postings+      ]  -- | In each tranaction, check that any conversion postings occur in adjacent pairs. journalCheckPairedConversionPostings :: Journal -> Either String ()@@ -255,11 +265,10 @@  -- | Check that accounts with balance assertions have no posting more -- than maxlag days after their latest balance assertion.--- Today's date is provided for error messages.-journalCheckRecentAssertions :: Day -> Journal -> Either String ()-journalCheckRecentAssertions today j =+journalCheckRecentAssertions :: Journal -> Either String ()+journalCheckRecentAssertions j =   let acctps = groupOn paccount $ sortOn paccount $ journalPostings j-  in case mapMaybe (findRecentAssertionError today) acctps of+  in case mapMaybe findRecentAssertionError acctps of     []         -> Right ()     firsterr:_ -> Left firsterr @@ -268,8 +277,8 @@ -- and if any postings are >maxlag days later than the assertion, -- return an error message identifying the first of them. -- Postings on the same date will be handled in parse order (hopefully).-findRecentAssertionError :: Day -> [Posting] -> Maybe String-findRecentAssertionError today ps = do+findRecentAssertionError :: [Posting] -> Maybe String+findRecentAssertionError ps = do   let rps = sortOn (Data.Ord.Down . postingDate) ps   let (afterlatestassertrps, untillatestassertrps) = span (isNothing.pbalanceassertion) rps   latestassertdate <- postingDate <$> headMay untillatestassertrps@@ -278,36 +287,39 @@   let lag = diffDays (postingDate firsterrorp) latestassertdate   let acct = paccount firsterrorp   let (f,l,_mcols,ex) = makePostingAccountErrorExcerpt firsterrorp-  let comm =-        case map acommodity $ amounts $ pamount firsterrorp of-          [] -> ""-          (t:_) | T.length t == 1 -> t-          (t:_) -> t <> " "+  -- let comm =+  --       case map acommodity $ amounts $ pamount firsterrorp of+  --         [] -> ""+  --         (t:_) | T.length t == 1 -> t+  --         (t:_) -> t <> " "   Just $ chomp $ printf     (unlines [       "%s:%d:",       "%s\n",-      "The recentassertions check is enabled, so accounts with balance assertions must",-      "have a balance assertion within %d days of their latest posting.",-      "",-      "In %s,",-      "this posting is %d days later than the balance assertion on %s.",-      "",-      "Consider adding a more recent balance assertion for this account. Eg:",+      -- "The recentassertions check is enabled, so accounts with balance assertions must",+      -- "have a balance assertion within %d days of their latest posting.",+      "The recentassertions check is enabled, so accounts with balance assertions",+      "must have a recent one, not more than %d days older than their latest posting.",+      "In account: %s",+      "the last assertion was on %s, %d days before this latest posting.",+      "Consider adding a new balance assertion to the above posting. Eg:",       "",-      "%s\n    %s    %s0 = %sAMT"+      "%s = BALANCE"       ])     f     l     (textChomp ex)     maxlag     (bold' $ T.unpack acct)-    lag     (showDate latestassertdate)-    (show today)-    acct-    comm-    comm+    lag+    (showposting firsterrorp)+    where+      showposting p =+        headDef "" $ first3 $ postingAsLines False True acctw amtw p{pcomment=""}+        where+          acctw = T.length $ paccount p+          amtw  = length $ showMixedAmountWith oneLineFmt $ pamount p  -- -- | Print the last balance assertion date & status of all accounts with balance assertions. -- printAccountLastAssertions :: Day -> [BalanceAssertionInfo] -> IO ()
Hledger/Data/Ledger.hs view
@@ -97,7 +97,7 @@  -- | All commodities used in this ledger. ledgerCommodities :: Ledger -> [CommoditySymbol]-ledgerCommodities = M.keys . jinferredcommodities . ljournal+ledgerCommodities = M.keys . jinferredcommoditystyles . ljournal  -- tests 
Hledger/Data/PeriodicTransaction.hs view
@@ -20,7 +20,7 @@ import Hledger.Data.Types import Hledger.Data.Dates import Hledger.Data.Amount-import Hledger.Data.Posting (post, commentAddTagNextLine)+import Hledger.Data.Posting (post, generatedTransactionTagName) import Hledger.Data.Transaction  -- $setup@@ -205,13 +205,11 @@           ,tstatus      = ptstatus           ,tcode        = ptcode           ,tdescription = ptdescription-          ,tcomment     = ptcomment &-            (if verbosetags then (`commentAddTagNextLine` ("generated-transaction",period)) else id)-          ,ttags        = pttags &-            (("_generated-transaction",period) :) &-            (if verbosetags then (("generated-transaction" ,period) :) else id)+          ,tcomment     = ptcomment+          ,ttags        = pttags           ,tpostings    = ptpostings           }+        & transactionAddHiddenAndMaybeVisibleTag verbosetags (generatedTransactionTagName, period)     period = "~ " <> ptperiodexpr     -- All the date spans described by this periodic transaction rule.     alltxnspans = splitSpan adjust ptinterval span'
Hledger/Data/Posting.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE NamedFieldPuns #-} {-|  A 'Posting' represents a change (by some 'MixedAmount') of the balance in@@ -8,6 +7,8 @@  -} +{-# LANGUAGE CPP #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-}  module Hledger.Data.Posting (@@ -18,7 +19,6 @@   vpost,   post',   vpost',-  nullsourcepos,   nullassertion,   balassert,   balassertTot,@@ -41,6 +41,7 @@   postingApplyCommodityStyles,   postingStyleAmounts,   postingAddTags,+  postingAddHiddenAndMaybeVisibleTag,   -- * date operations   postingDate,   postingDate2,@@ -54,17 +55,22 @@   commentAddTag,   commentAddTagUnspaced,   commentAddTagNextLine,+  generatedTransactionTagName,+  modifiedTransactionTagName,+  generatedPostingTagName,+  costPostingTagName,+  conversionPostingTagName,+   -- * arithmetic   sumPostings,+  negatePostingAmount,   -- * rendering   showPosting,   showPostingLines,   postingAsLines,   postingsAsLines,-  postingsAsLinesBeancount,-  postingAsLinesBeancount,+  postingIndent,   showAccountName,-  showAccountNameBeancount,   renderCommentLines,   showBalanceAssertion,   -- * misc.@@ -82,7 +88,10 @@ import Data.Function ((&)) import qualified Data.Map as M import Data.Maybe (fromMaybe, isJust, mapMaybe)-import Data.List (foldl', sort, union)+import Data.List (sort, union)+#if !MIN_VERSION_base(4,20,0)+import Data.List (foldl')+#endif import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T@@ -91,7 +100,6 @@ import Data.Time.Calendar (Day) import Safe (maximumBound) import Text.DocLayout (realLength)- import Text.Tabular.AsciiWide hiding (render)  import Hledger.Utils@@ -102,6 +110,17 @@ import Hledger.Data.Valuation  +-- | Special tags hledger sometimes adds to mark various things.+-- These should be hidden tag names, beginning with _.+-- With --verbose-tags, the equivalent visible tags will also be added.+-- These tag names are mentioned in docs and can be matched by user queries, so consider the impact before changing them.+generatedTransactionTagName, modifiedTransactionTagName, costPostingTagName, conversionPostingTagName, generatedPostingTagName :: TagName+generatedTransactionTagName = "_generated-transaction"  -- ^ transactions generated by a periodic txn rule+modifiedTransactionTagName  = "_modified-transaction"   -- ^ transactions modified by an auto posting rule+generatedPostingTagName     = "_generated-posting"      -- ^ postings generated by hledger for one reason or another+costPostingTagName          = "_cost-posting"           -- ^ postings which have or could have a cost that's equivalent to nearby conversion postings+conversionPostingTagName    = "_conversion-posting"     -- ^ postings to an equity account of Conversion type which have an amount that's equivalent to a nearby costful or potentially costful posting+ instance HasAmounts BalanceAssertion where   styleAmounts styles ba@BalanceAssertion{baamount} = ba{baamount=styleAmounts styles baamount} @@ -158,9 +177,6 @@ vpost' :: AccountName -> Amount -> Maybe BalanceAssertion -> Posting vpost' acc amt ass = (post' acc amt ass){ptype=VirtualPosting, pbalanceassertion=ass} -nullsourcepos :: (SourcePos, SourcePos)-nullsourcepos = (SourcePos "" (mkPos 1) (mkPos 1), SourcePos "" (mkPos 2) (mkPos 1))- nullassertion :: BalanceAssertion nullassertion = BalanceAssertion                   {baamount=nullamt@@ -210,11 +226,11 @@     maxacctwidth = maximumBound 0 $ map second3 linesWithWidths     maxamtwidth  = maximumBound 0 $ map third3 linesWithWidths --- | Given a transaction and its postings, render the postings, suitable--- for `print` output. Normally this output will be valid journal syntax which--- hledger can reparse (though it may include no-longer-valid balance assertions).+-- | Render a transaction's postings as indented lines, suitable for `print` output. ----- Explicit amounts are shown, any implicit amounts are not.+-- Normally these will be in valid journal syntax which hledger can reparse+-- (though they may include no-longer-valid balance assertions).+-- Explicit amounts are shown, implicit amounts are not. -- -- Postings with multicommodity explicit amounts are handled as follows: -- if onelineamounts is true, these amounts are shown on one line,@@ -222,12 +238,12 @@ -- Otherwise, they are shown as several similar postings, one per commodity. -- When the posting has a balance assertion, it is attached to the last of these postings. ----- The output will appear to be a balanced transaction.--- Amounts' display precisions, which may have been limited by commodity--- directives, will be increased if necessary to ensure this.--- -- Posting amounts will be aligned with each other, starting about 4 columns -- beyond the widest account name (see postingAsLines for details).+-- The postings will appear balanced (amounts summing to zero).+-- Amounts' display precisions, which may have been limited by commodity directives,+-- will be increased if necessary to ensure this.+-- postingsAsLines :: Bool -> [Posting] -> [Text] postingsAsLines onelineamounts ps = concatMap first3 linesWithWidths   where@@ -236,11 +252,12 @@     maxamtwidth  = maximumBound 0 $ map third3 linesWithWidths  -- | Render one posting, on one or more lines, suitable for `print` output.+-- Also returns the widths calculated for the account and amount fields.+-- -- There will be an indented account name, plus one or more of status flag, -- posting amount, balance assertion, same-line comment, next-line comments. -- -- If the posting's amount is implicit or if elideamount is true, no amount is shown.--- -- If the posting's amount is explicit and multi-commodity, multiple similar -- postings are shown, one for each commodity, to help produce parseable journal syntax. -- Or if onelineamounts is true, such amounts are shown on one line, comma-separated@@ -257,7 +274,6 @@ -- increased if needed to match the posting with the longest account name. -- This is used to align the amounts of a transaction's postings. ----- Also returns the account width and amount width used. postingAsLines :: Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int) postingAsLines elideamount onelineamounts acctwidth amtwidth p =     (concatMap (++ newlinecomments) postingblocks, thisacctwidth, thisamtwidth)@@ -305,7 +321,7 @@             assertion = maybe mempty ((WideBuilder (TB.singleton ' ') 1 <>).showBalanceAssertion) $ pbalanceassertion p      -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned-    statusandaccount = lineIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p+    statusandaccount = postingIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p     thisacctwidth = realLength $ pacctstr p      (samelinecomment, newlinecomments) =@@ -321,91 +337,6 @@     fmt VirtualPosting         = wrap "(" ")" . maybe id (T.takeEnd . subtract 2) w     fmt BalancedVirtualPosting = wrap "[" "]" . maybe id (T.takeEnd . subtract 2) w --- | Like postingsAsLines but generates Beancount journal format.-postingsAsLinesBeancount :: [Posting] -> [Text]-postingsAsLinesBeancount ps = concatMap first3 linesWithWidths-  where-    linesWithWidths = map (postingAsLinesBeancount False maxacctwidth maxamtwidth) ps-    maxacctwidth = maximumBound 0 $ map second3 linesWithWidths-    maxamtwidth  = maximumBound 0 $ map third3  linesWithWidths---- | Like postingAsLines but generates Beancount journal format.-postingAsLinesBeancount  :: Bool -> Int -> Int -> Posting -> ([Text], Int, Int)-postingAsLinesBeancount elideamount acctwidth amtwidth p =-    (concatMap (++ newlinecomments) postingblocks, thisacctwidth, thisamtwidth)-  where-    -- This needs to be converted to strict Text in order to strip trailing-    -- spaces. This adds a small amount of inefficiency, and the only difference-    -- is whether there are trailing spaces in print (and related) reports. This-    -- could be removed and we could just keep everything as a Text Builder, but-    -- would require adding trailing spaces to 42 failing tests.-    postingblocks = [map T.stripEnd . T.lines . TL.toStrict $-                       render [ textCell BottomLeft statusandaccount-                              , textCell BottomLeft "  "-                              , Cell BottomLeft [pad amt]-                              , textCell BottomLeft samelinecomment-                              ]-                    | (amt,_assertion) <- shownAmountsAssertions]-    render = renderRow def{tableBorders=False, borderSpaces=False} . Group NoLine . map Header-    pad amt = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt-      where w = max 12 amtwidth - wbWidth amt  -- min. 12 for backwards compatibility--    pacct = showAccountNameBeancount Nothing $ paccount p-    pstatusandacct p' = if pstatus p' == Pending then "! " else "" <> pacct--    -- currently prices are considered part of the amount string when right-aligning amounts-    -- Since we will usually be calling this function with the knot tied between-    -- amtwidth and thisamtwidth, make sure thisamtwidth does not depend on-    -- amtwidth at all.-    shownAmounts-      | elideamount = [mempty]-      | otherwise   = showMixedAmountLinesB displayopts a'-        where-          displayopts = defaultFmt{ displayZeroCommodity=True, displayForceDecimalMark=True }-          a' = mapMixedAmount amountToBeancount $ pamount p-    thisamtwidth = maximumBound 0 $ map wbWidth shownAmounts--    -- when there is a balance assertion, show it only on the last posting line-    shownAmountsAssertions = zip shownAmounts shownAssertions-      where-        shownAssertions = replicate (length shownAmounts - 1) mempty ++ [assertion]-          where-            assertion = maybe mempty ((WideBuilder (TB.singleton ' ') 1 <>).showBalanceAssertion) $ pbalanceassertion p--    -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned-    statusandaccount = lineIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p-    thisacctwidth = realLength pacct--    (samelinecomment, newlinecomments) =-      case renderCommentLines (pcomment p) of []   -> ("",[])-                                              c:cs -> (c,cs)--type BeancountAmount = Amount---- | Do some best effort adjustments to make an amount that renders--- in a way that Beancount can read: forces the commodity symbol to the right,--- converts a few currency symbols to names, capitalises all letters.-amountToBeancount :: Amount -> BeancountAmount-amountToBeancount a@Amount{acommodity=c,astyle=s,acost=mp} = a{acommodity=c', astyle=s', acost=mp'}-  -- https://beancount.github.io/docs/beancount_language_syntax.html#commodities-currencies-  where-    c' = T.toUpper $-      T.replace "$" "USD" $-      T.replace "€" "EUR" $-      T.replace "¥" "JPY" $-      T.replace "£" "GBP" $-      c-    s' = s{ascommodityside=R, ascommodityspaced=True}-    mp' = costToBeancount <$> mp-      where-        costToBeancount (TotalCost amt) = TotalCost $ amountToBeancount amt-        costToBeancount (UnitCost  amt) = UnitCost  $ amountToBeancount amt---- | Like showAccountName for Beancount journal format.--- Calls accountNameToBeancount first.-showAccountNameBeancount :: Maybe Int -> AccountName -> Text-showAccountNameBeancount w = maybe id T.take w . accountNameToBeancount- -- | Render a transaction or posting's comment as indented, semicolon-prefixed comment lines. -- The first line (unless empty) will have leading space, subsequent lines will have a larger indent. renderCommentLines :: Text -> [Text]@@ -413,14 +344,14 @@   case T.lines t of     []      -> []     [l]     -> [commentSpace $ comment l]        -- single-line comment-    ("":ls) -> "" : map (lineIndent . comment) ls  -- multi-line comment with empty first line-    (l:ls)  -> commentSpace (comment l) : map (lineIndent . comment) ls+    ("":ls) -> "" : map (postingIndent . comment) ls  -- multi-line comment with empty first line+    (l:ls)  -> commentSpace (comment l) : map (postingIndent . comment) ls   where     comment = ("; "<>)  -- | Prepend a suitable indent for a posting (or transaction/posting comment) line.-lineIndent :: Text -> Text-lineIndent = ("    "<>)+postingIndent :: Text -> Text+postingIndent = ("    "<>)  -- | Prepend the space required before a same-line comment. commentSpace :: Text -> Text@@ -450,6 +381,10 @@ sumPostings :: [Posting] -> MixedAmount sumPostings = foldl' (\amt p -> maPlus amt $ pamount p) nullmixedamt +-- | Negate amount in a posting.+negatePostingAmount :: Posting -> Posting+negatePostingAmount = postingTransformAmount negate+ -- | Strip all prices from a Posting. postingStripCosts :: Posting -> Posting postingStripCosts = postingTransformAmount mixedAmountStripCosts@@ -525,9 +460,21 @@           ++ "\n to account name: "++T.unpack paccount++"\n "++e  -- | Add tags to a posting, discarding any for which the posting already has a value.+-- Note this does not add tags to the posting's comment. postingAddTags :: Posting -> [Tag] -> Posting postingAddTags p@Posting{ptags} tags = p{ptags=ptags `union` tags} +-- | Add the given hidden tag to a posting; and with a true argument,+-- also add the equivalent visible tag to the posting's tags and comment fields.+-- If the posting already has these tags (with any value), do nothing.+postingAddHiddenAndMaybeVisibleTag :: Bool -> HiddenTag -> Posting -> Posting+postingAddHiddenAndMaybeVisibleTag verbosetags ht p@Posting{pcomment=c, ptags} =+  (p `postingAddTags` ([ht] <> [vt|verbosetags]))+  {pcomment=if verbosetags && not hadtag then c `commentAddTag` vt else c}+  where+    vt@(vname,_) = toVisibleTag ht+    hadtag = any ((== (T.toLower vname)) . T.toLower . fst) ptags  -- XXX should regex-quote vname+ -- | Apply a specified valuation to this posting's amount, using the -- provided price oracle, commodity styles, and reference dates. -- See amountApplyValuation.@@ -539,44 +486,42 @@ postingToCost :: ConversionOp -> Posting -> Maybe Posting postingToCost NoConversionOp p = Just p postingToCost ToCost         p-  -- If this is a conversion posting with a matched transaction price posting, ignore it-  | "_conversion-matched" `elem` map fst (ptags p) && nocosts = Nothing+  -- If this is an equity conversion posting with an associated cost nearby, ignore it+  | conversionPostingTagName `elem` map fst (ptags p) && nocosts = Nothing   | otherwise = Just $ postingTransformAmount mixedAmountCost p   where     nocosts = (not . any (isJust . acost) . amountsRaw) $ pamount p --- | Generate inferred equity postings from a 'Posting''s costs.--- Make sure not to duplicate them when matching ones exist already.+-- | Generate equity conversion postings corresponding to a 'Posting''s cost(s)+-- (one pair of conversion postings per cost), wherever they don't already exist. postingAddInferredEquityPostings :: Bool -> Text -> Posting -> [Posting] postingAddInferredEquityPostings verbosetags equityAcct p-    | "_price-matched" `elem` map fst (ptags p) = [p]-    | otherwise = taggedPosting : concatMap conversionPostings costs+  -- this posting has no costs+  | null costs = [p]+  -- this posting is already tagged as having associated conversion postings+  | costPostingTagName `elem` map fst (ptags p) = [p]+  -- tag the posting, and for each of its costs, add an equivalent pair of conversion postings after it+  | otherwise =+    postingAddHiddenAndMaybeVisibleTag verbosetags (costPostingTagName,"") p :+    concatMap makeConversionPostings costs   where     costs = filter (isJust . acost) . amountsRaw $ pamount p-    taggedPosting-      | null costs = p-      | otherwise  = p{ ptags = ("_price-matched","") : ptags p }-    conversionPostings amt = case acost amt of-        Nothing -> []-        Just _  -> [ cp{ paccount = accountPrefix <> amtCommodity-                       , pamount = mixedAmount . negate $ amountStripCost amt-                       }-                   , cp{ paccount = accountPrefix <> costCommodity-                       , pamount = mixedAmount cost-                       }-                   ]+    makeConversionPostings amt = case acost amt of+      Nothing -> []+      Just _  -> [ convp{ paccount = accountPrefix <> amtCommodity+                        , pamount = mixedAmount . negate $ amountStripCost amt+                        }+                 , convp{ paccount = accountPrefix <> costCommodity+                        , pamount = mixedAmount cost+                        }+                 ]       where         cost = amountCost amt         amtCommodity  = commodity amt         costCommodity = commodity cost-        cp = p{ pcomment = pcomment p & (if verbosetags then (`commentAddTag` ("generated-posting","conversion")) else id)-              , ptags    =-                   ("_conversion-matched","") : -- implementation-specific internal tag, not for users-                   ("_generated-posting","conversion") :-                   (if verbosetags then [("generated-posting", "conversion")] else [])-              , pbalanceassertion = Nothing-              , poriginal = Nothing-              }+        convp = p{pbalanceassertion=Nothing, poriginal=Nothing}+          & postingAddHiddenAndMaybeVisibleTag verbosetags (conversionPostingTagName,"")+          & postingAddHiddenAndMaybeVisibleTag verbosetags (generatedPostingTagName, "")         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
Hledger/Data/RawOptions.hs view
@@ -26,7 +26,9 @@   posintopt,   maybeintopt,   maybeposintopt,-  maybecharopt+  maybecharopt,+  maybeynopt,+  maybeynaopt, ) where @@ -35,6 +37,8 @@ import Safe (headMay, lastMay, readDef)  import Hledger.Utils+import Data.Char (toLower)+import Data.List (intercalate)   -- | The result of running cmdargs: an association list of option names to string values.@@ -139,3 +143,20 @@                                          ++ " must lie in the range "                                          ++ show minVal ++ " to " ++ show maxVal                                          ++ ", but is " ++ show n++maybeynopt :: String -> RawOpts -> Maybe Bool+maybeynopt name rawopts =+  case maybestringopt name rawopts of+    Just v | map toLower v `elem` ["y","yes","always"] -> Just True+    Just v | map toLower v `elem` ["n","no","never"]   -> Just False+    Just _ -> error' $ name <> " value should be one of " <> (intercalate ", " ["y","yes","n","no"])+    _ -> Nothing++maybeynaopt :: String -> RawOpts -> Maybe YNA+maybeynaopt name rawopts =+  case maybestringopt name rawopts of+    Just v | map toLower v `elem` ["y","yes","always"] -> Just Yes+    Just v | map toLower v `elem` ["n","no","never"]   -> Just No+    Just v | map toLower v `elem` ["a","auto"]         -> Just Auto+    Just _ -> error' $ name <> " value should be one of " <> (intercalate ", " ["y","yes","n","no","a","auto"])+    _ -> Nothing
Hledger/Data/Transaction.hs view
@@ -27,13 +27,18 @@ , transactionTransformPostings , transactionApplyValuation , transactionToCost-, transactionAddInferredEquityPostings-, transactionInferCostsFromEquity+, transactionInferEquityPostings+, transactionTagCostsAndEquityAndMaybeInferCosts , transactionApplyAliases , transactionMapPostings , transactionMapPostingAmounts , transactionAmounts , partitionAndCheckConversionPostings+, transactionAddTags+, transactionAddHiddenAndMaybeVisibleTag+  -- * helpers+, payeeAndNoteFromDescription+, payeeAndNoteFromDescription'   -- nonzerobalanceerror   -- * date operations , transactionDate2@@ -46,7 +51,6 @@ , showTransaction , showTransactionOneLineAmounts , showTransactionLineFirstPart-, showTransactionBeancount , transactionFile   -- * transaction errors , annotateErrorWithTransaction@@ -74,6 +78,8 @@ import Hledger.Data.Valuation import Data.Decimal (normalizeDecimal, decimalPlaces) import Data.Functor ((<&>))+import Data.Function ((&))+import Data.List (union)   instance HasAmounts Transaction where@@ -82,7 +88,7 @@ nulltransaction :: Transaction nulltransaction = Transaction {                     tindex=0,-                    tsourcepos=nullsourcepos,+                    tsourcepos=nullsourcepospair,                     tdate=nulldate,                     tdate2=Nothing,                     tstatus=Unmarked,@@ -176,33 +182,6 @@            | otherwise            = ""     code = if T.null (tcode t) then "" else wrap " (" ")" $ tcode t --- | Like showTransaction, but generates Beancount journal format.-showTransactionBeancount :: Transaction -> Text-showTransactionBeancount t =-  -- https://beancount.github.io/docs/beancount_language_syntax.html-  -- similar to showTransactionHelper, but I haven't bothered with Builder-     firstline <> nl-  <> foldMap ((<> nl)) newlinecomments-  <> foldMap ((<> nl)) (postingsAsLinesBeancount $ tpostings t)-  <> nl-  where-    nl = "\n"-    firstline = T.concat [date, status, payee, note, tags, samelinecomment]-    date = showDate $ tdate t-    status = if tstatus t == Pending then " !" else " *"-    (payee,note) =-      case payeeAndNoteFromDescription' $ tdescription t of-        ("","") -> ("",      ""      )-        ("",n ) -> (""     , wrapq n )-        (p ,"") -> (wrapq p, wrapq "")-        (p ,n ) -> (wrapq p, wrapq n )-      where-        wrapq = wrap " \"" "\"" . escapeDoubleQuotes . escapeBackslash-    tags = T.concat $ map ((" #"<>).fst) $ ttags t-    (samelinecomment, newlinecomments) =-      case renderCommentLines (tcomment t) of []   -> ("",[])-                                              c:cs -> (c,cs)- hasRealPostings :: Transaction -> Bool hasRealPostings = not . null . realPostings @@ -260,10 +239,11 @@ transactionToCost :: ConversionOp -> Transaction -> Transaction transactionToCost cost t = t{tpostings = mapMaybe (postingToCost cost) $ tpostings t} --- | Add inferred equity postings to a 'Transaction' using transaction prices.-transactionAddInferredEquityPostings :: Bool -> AccountName -> Transaction -> Transaction-transactionAddInferredEquityPostings verbosetags equityAcct t =-    t{tpostings=concatMap (postingAddInferredEquityPostings verbosetags equityAcct) $ tpostings t}+-- | For any costs in this 'Transaction' which don't have associated equity conversion postings,+-- generate and add those.+transactionInferEquityPostings :: Bool -> AccountName -> Transaction -> Transaction+transactionInferEquityPostings verbosetags equityAcct t =+  t{tpostings=concatMap (postingAddInferredEquityPostings verbosetags equityAcct) $ tpostings t}  type IdxPosting = (Int, Posting) @@ -273,17 +253,35 @@  label s = ((s <> ": ")++) --- | Add costs inferred from equity postings in this transaction.--- The name(s) of conversion equity accounts should be provided.--- For every adjacent pair of conversion postings, it will first search the postings--- with costs to see if any match. If so, it will tag these as matched.--- If no postings with costs match, it will then search the postings without costs,--- and will match the first such posting which matches one of the conversion amounts.--- If it finds a match, it will add a cost and then tag it.--- If the first argument is true, do a dry run instead: identify and tag--- the costful and conversion postings, but don't add costs.-transactionInferCostsFromEquity :: Bool -> [AccountName] -> Transaction -> Either String Transaction-transactionInferCostsFromEquity dryrun conversionaccts t = first (annotateErrorWithTransaction t . T.unpack) $ do+-- | Add tags to a transaction, discarding any for which it already has a value.+-- Note this does not add tags to the transaction's comment.+transactionAddTags :: Transaction -> [Tag] -> Transaction+transactionAddTags t@Transaction{ttags} tags = t{ttags=ttags `union` tags}++-- | Add the given hidden tag to a transaction; and with a true argument,+-- also add the equivalent visible tag to the transaction's tags and comment fields.+-- If the transaction already has these tags (with any value), do nothing.+transactionAddHiddenAndMaybeVisibleTag :: Bool -> HiddenTag -> Transaction -> Transaction+transactionAddHiddenAndMaybeVisibleTag verbosetags ht t@Transaction{tcomment=c, ttags} =+  (t `transactionAddTags` ([ht] <> [vt|verbosetags]))+  {tcomment=if verbosetags && not hadtag then c `commentAddTagNextLine` vt else c}+  where+    vt@(vname,_) = toVisibleTag ht+    hadtag = any ((== (T.toLower vname)) . T.toLower . fst) ttags  -- XXX should regex-quote vname++-- | Find, associate, and tag the corresponding equity conversion postings and costful or potentially costful postings in this transaction.+-- With a true addcosts argument, also generate and add any equivalent costs that are missing.+-- The (previously detected) names of all equity conversion accounts should be provided.+--+-- For every pair of adjacent conversion postings, this first searches for a posting with equivalent cost (1).+-- If no such posting is found, it then searches the costless postings, for one matching one of the conversion amounts (2).+-- If either of these found a candidate posting, it is tagged with costPostingTagName.+-- Then if in addcosts mode, if a costless posting was found, a cost equivalent to the conversion amounts is added to it.+--+-- The name reflects the complexity of this and its helpers; clarification is ongoing.+--+transactionTagCostsAndEquityAndMaybeInferCosts :: Bool -> Bool -> [AccountName] -> Transaction -> Either String Transaction+transactionTagCostsAndEquityAndMaybeInferCosts verbosetags1 addcosts conversionaccts t = first (annotateErrorWithTransaction t . T.unpack) $ do   -- number the postings   let npostings = zip [0..] $ tpostings t @@ -295,7 +293,7 @@   -- 1. each pair of conversion postings, and the corresponding postings which balance them, are tagged for easy identification   -- 2. each pair of balancing postings which did't have an explicit cost, have had a cost calculated and added to one of them   -- 3. if any ambiguous situation was detected, an informative error is raised-  processposting <- transformIndexedPostingsF (addCostsToPostings dryrun) conversionPairs otherps+  processposting <- transformIndexedPostingsF (tagAndMaybeAddCostsForEquityPostings verbosetags1 addcosts) conversionPairs otherps    -- And if there was no error, use it to modify the transaction's postings.   return t{tpostings = map (snd . processposting) npostings}@@ -304,7 +302,7 @@      -- Generate the tricksy processposting function,     -- which when applied to each posting in turn, rather magically has the effect of-    -- applying addCostsToPostings to each pair of conversion postings in the transaction,+    -- applying tagAndMaybeAddCostsForEquityPostings to each pair of conversion postings in the transaction,     -- matching them with the other postings, tagging them and perhaps adding cost information to the other postings.     -- General type:     -- transformIndexedPostingsF :: (Monad m, Foldable t, Traversable t) =>@@ -314,45 +312,44 @@     --   m (a1 -> a1)     -- Concrete type:     transformIndexedPostingsF ::-      ((IdxPosting, IdxPosting) -> StateT ([IdxPosting],[IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)) ->  -- state update function (addCostsToPostings with the bool applied)+      ((IdxPosting, IdxPosting) -> StateT ([IdxPosting],[IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)) ->  -- state update function (tagAndMaybeAddCostsForEquityPostings with the bool applied)       [(IdxPosting, IdxPosting)] ->   -- initial state: the pairs of adjacent conversion postings in the transaction       ([IdxPosting],[IdxPosting]) ->  -- initial state: the other postings in the transaction, separated into costful and costless       (Either Text (IdxPosting -> IdxPosting))  -- returns an error message or a posting transform function     transformIndexedPostingsF updatefn = evalStateT . fmap (appEndo . foldMap Endo) . traverse (updatefn)      -- A tricksy state update helper for processposting/transformIndexedPostingsF.-    -- Approximately: given a pair of conversion postings to match,+    -- Approximately: given a pair of equity conversion postings to match,     -- and lists of the remaining unmatched costful and costless other postings,-    -- 1. find (and consume) two other postings which match the two conversion postings-    -- 2. add identifying tags to the four postings-    -- 3. add an explicit cost, if missing, to one of the matched other postings-    -- 4. or if there is a problem, raise an informative error or do nothing as appropriate.-    -- Or, if the first argument is true:-    -- do a dry run instead: find and consume, add tags, but don't add costs-    -- (and if there are no costful postings at all, do nothing).-    addCostsToPostings :: Bool -> (IdxPosting, IdxPosting) -> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)-    addCostsToPostings dryrun' ((n1, cp1), (n2, cp2)) = StateT $ \(costps, otherps) -> do+    -- 1. find (and consume) two other postings whose amounts/cost match the two conversion postings+    -- 2. add hidden identifying tags to the conversion postings and the other posting which has (or could have) an equivalent cost+    -- 3. if in add costs mode, and the potential equivalent-cost posting does not have that explicit cost, add it+    -- 4. or if there is a problem, raise an informative error or do nothing, as appropriate.+    -- Or if there are no costful postings at all, do nothing.+    tagAndMaybeAddCostsForEquityPostings :: Bool -> Bool -> (IdxPosting, IdxPosting) -> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)+    tagAndMaybeAddCostsForEquityPostings verbosetags addcosts' ((n1, cp1), (n2, cp2)) = StateT $ \(costps, otherps) -> do       -- Get the two conversion posting amounts, if possible       ca1 <- conversionPostingAmountNoCost cp1       ca2 <- conversionPostingAmountNoCost cp2       let -        -- All costful postings which match the conversion posting pair-        matchingCostPs =+        -- All costful postings whose cost is equivalent to the conversion postings' amounts.+        matchingCostfulPs =           dbg7With (label "matched costful postings".show.length) $            mapMaybe (mapM $ costfulPostingIfMatchesBothAmounts ca1 ca2) costps -        -- All other single-commodity postings whose amount matches at least one of the conversion postings,-        -- with an explicit cost added. Or in dry run mode, all other single-commodity postings.-        matchingOtherPs =+        -- In dry run mode: all other costless, single-commodity postings.+        -- In add costs mode: all other costless, single-commodity postings whose amount matches at least one of the conversion postings,+        -- with the equivalent cost added to one of them. (?)+        matchingCostlessPs =           dbg7With (label "matched costless postings".show.length) $-          if dryrun'-          then [(n,(p, a)) | (n,p) <- otherps, let Just a = postingSingleAmount p]-          else mapMaybe (mapM $ addCostIfMatchesOneAmount ca1 ca2) otherps+          if addcosts'+          then mapMaybe (mapM $ addCostIfMatchesOneAmount ca1 ca2) otherps+          else [(n,(p, a)) | (n,p) <- otherps, let Just a = postingSingleAmount p]          -- A function that adds a cost and/or tag to a numbered posting if appropriate.         postingAddCostAndOrTag np costp (n,p) =-          (n, if | n == np            -> costp `postingAddTags` [("_price-matched","")]-                 | n == n1 || n == n2 -> p     `postingAddTags` [("_conversion-matched","")]+          (n, if | n == np            -> costp & postingAddHiddenAndMaybeVisibleTag verbosetags (costPostingTagName,"")        -- if it's the specified posting number, replace it with the costful posting, and tag it+                 | n == n1 || n == n2 -> p     & postingAddHiddenAndMaybeVisibleTag verbosetags (conversionPostingTagName,"")  -- if it's one of the equity conversion postings, tag it                  | otherwise          -> p)        -- Annotate any errors with the conversion posting pair@@ -362,20 +359,20 @@           -- delete it from the list of costful postings in the state, delete the           -- first matching costless posting from the list of costless postings           -- in the state, and return the transformation function with the new state.-          | [(np, costp)] <- matchingCostPs+          | [(np, costp)] <- matchingCostfulPs           , Just newcostps <- deleteIdx np costps-              -> Right (postingAddCostAndOrTag np costp, (if dryrun' then costps else newcostps, otherps))+              -> Right (postingAddCostAndOrTag np costp, (if addcosts' then newcostps else costps, otherps))            -- If no costful postings match the conversion postings, but some           -- of the costless postings match, check that the first such posting has a           -- different amount from all the others, and if so add a cost to it,           -- then delete it from the list of costless postings in the state,           -- and return the transformation function with the new state.-          | [] <- matchingCostPs-          , (np, (costp, amt)):nps <- matchingOtherPs+          | [] <- matchingCostfulPs+          , (np, (costp, amt)):nps <- matchingCostlessPs           , not $ any (amountsMatch amt . snd . snd) nps           , Just newotherps <- deleteIdx np otherps-              -> Right (postingAddCostAndOrTag np costp, (costps, if dryrun' then otherps else newotherps))+              -> Right (postingAddCostAndOrTag np costp, (costps, if addcosts' then newotherps else otherps))            -- Otherwise, do nothing, leaving the transaction unchanged.           -- We don't want to be over-zealous reporting problems here@@ -445,7 +442,7 @@   -- Left fold processes postings in parse order, so that eg inferred costs   -- will be added to the first (top-most) posting, not the last one.   foldlM select (([], ([], [])), Nothing)-    -- The costless other postings are somehow reversed still; "second (second reverse" fixes that.+    -- The costless other postings are somehow reversed still; "second (second reverse)" fixes that.     <&> fmap (second (second reverse) . fst)   where     select ((cs, others@(ps, os)), Nothing) np@(_, p)@@ -604,7 +601,7 @@                  Transaction                    0                    ""-                   nullsourcepos+                   nullsourcepospair                    (fromGregorian 2007 01 28)                    Nothing                    Unmarked@@ -628,7 +625,7 @@               Transaction                 0                 ""-                nullsourcepos+                nullsourcepospair                 (fromGregorian 2007 01 28)                 Nothing                 Unmarked@@ -651,7 +648,7 @@               Transaction                 0                 ""-                nullsourcepos+                nullsourcepospair                 (fromGregorian 2007 01 28)                 Nothing                 Unmarked@@ -667,7 +664,7 @@               Transaction                 0                 ""-                nullsourcepos+                nullsourcepospair                 (fromGregorian 2010 01 01)                 Nothing                 Unmarked
Hledger/Data/TransactionModifier.hs view
@@ -23,10 +23,10 @@ import Hledger.Data.Types import Hledger.Data.Amount import Hledger.Data.Dates-import Hledger.Data.Transaction (txnTieKnot)+import Hledger.Data.Transaction (txnTieKnot, transactionAddHiddenAndMaybeVisibleTag) import Hledger.Query (Query, filterQuery, matchesAmount, matchesPostingExtra,                       parseQuery, queryIsAmt, queryIsSym, simplifyQuery)-import Hledger.Data.Posting (commentJoin, commentAddTag, postingAddTags)+import Hledger.Data.Posting (commentJoin, commentAddTag, postingAddTags, modifiedTransactionTagName) import Hledger.Utils (dbg6, wrap)  -- $setup@@ -47,14 +47,10 @@ modifyTransactions atypes atags styles d verbosetags tmods ts = do   fs <- mapM (transactionModifierToFunction atypes atags styles d verbosetags) tmods  -- convert modifiers to functions, or return a parse error   let-    modifytxn t = t''+    modifytxn t =+      t' & if t'/=t then transactionAddHiddenAndMaybeVisibleTag verbosetags (modifiedTransactionTagName,"") else id       where         t' = foldr (flip (.)) id fs t  -- apply each function in turn-        t'' = if t' == t-              then t'-              else t'{tcomment=tcomment t' & (if verbosetags then (`commentAddTag` ("modified","")) else id)-                     ,ttags=ttags t' & (("_modified","") :) & (if verbosetags then (("modified","") :) else id)-                     }    Right $ map modifytxn ts 
Hledger/Data/Types.hs view
@@ -33,17 +33,20 @@ where  import GHC.Generics (Generic)+import Data.Bifunctor (first) import Data.Decimal (Decimal, DecimalRaw(..)) import Data.Default (Default(..)) import Data.Functor (($>))-import Data.List (intercalate)+import Data.List (intercalate, sortBy) --XXX https://hackage.haskell.org/package/containers/docs/Data-Map.html --Note: You should use Data.Map.Strict instead of this module if: --You will eventually need all the values stored. --The stored values don't represent large virtual data structures to be lazily computed. import qualified Data.Map as M import Data.Ord (comparing)+import Data.Semigroup (Min(..)) import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar (Day) import Data.Time.Clock.POSIX (POSIXTime) import Data.Time.LocalTime (LocalTime)@@ -53,6 +56,7 @@  import Hledger.Utils.Regex + -- synonyms for various date-related scalars #if MIN_VERSION_time(1,11,0) import Data.Time.Calendar (Year)@@ -153,6 +157,19 @@  type AccountName = Text +-- A specification indicating how to depth-limit+data DepthSpec = DepthSpec {+  dsFlatDepth    :: Maybe Int,+  dsRegexpDepths :: [(Regexp, Int)]+  } deriving (Eq,Show)++-- Semigroup instance consider all regular expressions, but take the minimum of the simple flat depths+instance Semigroup DepthSpec where+    DepthSpec d1 l1 <> DepthSpec d2 l2 = DepthSpec (getMin <$> (Min <$> d1) <> (Min <$> d2)) (l1 ++ l2)++instance Monoid DepthSpec where+    mempty = DepthSpec Nothing []+ data AccountType =     Asset   | Liability@@ -389,8 +406,32 @@ type TagName = Text type TagValue = Text type Tag = (TagName, TagValue)  -- ^ A tag name and (possibly empty) value.+type HiddenTag = Tag            -- ^ A tag whose name begins with _. type DateTag = (TagName, Day) +-- | Add the _ prefix to a normal visible tag's name, making it a hidden tag.+toHiddenTag :: Tag -> HiddenTag+toHiddenTag = first toHiddenTagName++-- | Drop the _ prefix from a hidden tag's name, making it a normal visible tag.+toVisibleTag :: HiddenTag -> Tag+toVisibleTag = first toVisibleTagName++-- | Does this tag name begin with the hidden tag prefix (_) ?+isHiddenTagName :: TagName -> Bool+isHiddenTagName t =+  case T.uncons t of+    Just ('_',_) -> True+    _ -> False++-- | Add the _ prefix to a normal visible tag's name, making it a hidden tag.+toHiddenTagName :: TagName -> TagName+toHiddenTagName = T.cons '_'++-- | Drop the _ prefix from a hidden tag's name, making it a normal visible tag.+toVisibleTagName :: TagName -> TagName+toVisibleTagName = T.drop 1+ -- | The status of a transaction or posting, recorded with a status mark -- (nothing, !, or *). What these mean is ultimately user defined. data Status = Unmarked | Pending | Cleared@@ -401,6 +442,12 @@   show Pending   = "!"   show Cleared   = "*" +nullsourcepos :: SourcePos+nullsourcepos = SourcePos "" (mkPos 1) (mkPos 1)++nullsourcepospair :: (SourcePos, SourcePos)+nullsourcepospair = (SourcePos "" (mkPos 1) (mkPos 1), SourcePos "" (mkPos 2) (mkPos 1))+ -- | A balance assertion is a declaration about an account's expected balance -- at a certain point (posting date and parse order). They provide additional -- error checking and readability to a journal file.@@ -545,7 +592,8 @@ -- It declares two things: a historical exchange rate between two commodities, -- and an amount display style for the second commodity. data PriceDirective = PriceDirective {-   pddate      :: Day+   pdsourcepos :: SourcePos+  ,pddate      :: Day   ,pdcommodity :: CommoditySymbol   ,pdamount    :: Amount   } deriving (Eq,Ord,Generic,Show)@@ -559,50 +607,52 @@   ,mprate :: Quantity           -- ^ One unit of the "from" commodity is worth this quantity of the "to" commodity.   } deriving (Eq,Ord,Generic, Show) +showMarketPrice MarketPrice{..} = unwords [show mpdate, T.unpack mpfrom <> ">" <> T.unpack mpto, show mprate]+showMarketPrices = intercalate "\n" . map ((' ':).showMarketPrice) . sortBy (comparing mpdate)+ -- additional valuation-related types in Valuation.hs --- | A Journal, containing transactions and various other things.--- The basic data model for hledger.+-- | A journal, containing general ledger transactions; also directives and various other things.+-- This is hledger's main data model. ----- This is used during parsing (as the type alias ParsedJournal), and--- then finalised/validated for use as a Journal. Some extra--- parsing-related fields are included for convenience, at least for--- now. In a ParsedJournal these are updated as parsing proceeds, in a--- Journal they represent the final state at end of parsing (used eg--- by the add command).+-- During parsing, it is used as the type alias "ParsedJournal".+-- The jparse* fields are mainly used during parsing and included here for convenience.+-- The list fields described as "in parse order" are usually reversed for efficiency during parsing.+-- After parsing, "journalFinalise" converts ParsedJournal to a finalised "Journal",+-- which has all lists correctly ordered, and much data inference and validation applied. -- data Journal = Journal {-  -- parsing-related data-   jparsedefaultyear      :: Maybe Year                            -- ^ the current default year, specified by the most recent Y directive (or current date)-  ,jparsedefaultcommodity :: Maybe (CommoditySymbol,AmountStyle)   -- ^ the current default commodity and its format, specified by the most recent D directive-  ,jparsedecimalmark      :: Maybe DecimalMark                     -- ^ the character to always parse as decimal point, if set by CsvReader's decimal-mark (or a future journal directive)-  ,jparseparentaccounts   :: [AccountName]                         -- ^ the current stack of parent account names, specified by apply account directives-  ,jparsealiases          :: [AccountAlias]                        -- ^ the current account name aliases in effect, specified by alias directives (& options ?)+  -- parsing-related state+   jparsedefaultyear        :: Maybe Year                             -- ^ the current default year, specified by the most recent Y directive (or current date)+  ,jparsedefaultcommodity   :: Maybe (CommoditySymbol,AmountStyle)    -- ^ the current default commodity and its format, specified by the most recent D directive+  ,jparsedecimalmark        :: Maybe DecimalMark                      -- ^ the character to always parse as decimal point, if set by CsvReader's decimal-mark (or a future journal directive)+  ,jparseparentaccounts     :: [AccountName]                          -- ^ the current stack of parent account names, specified by apply account directives+  ,jparsealiases            :: [AccountAlias]                         -- ^ the current account name aliases in effect, specified by alias directives (& options ?)   -- ,jparsetransactioncount :: Integer                               -- ^ the current count of transactions parsed so far (only journal format txns, currently)-  ,jparsetimeclockentries :: [TimeclockEntry]                       -- ^ timeclock sessions which have not been clocked out-  ,jincludefilestack      :: [FilePath]+  ,jparsetimeclockentries   :: [TimeclockEntry]                       -- ^ timeclock sessions which have not been clocked out+  ,jincludefilestack        :: [FilePath]   -- principal data-  ,jdeclaredpayees        :: [(Payee,PayeeDeclarationInfo)]         -- ^ Payees declared by payee directives, in parse order (after journal finalisation)-  ,jdeclaredtags          :: [(TagName,TagDeclarationInfo)]         -- ^ Tags declared by tag directives, in parse order (after journal finalisation)-  ,jdeclaredaccounts      :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)-  ,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-  ,jpricedirectives       :: [PriceDirective]                       -- ^ Declarations of market prices by P directives, in parse order (after journal finalisation)-  ,jinferredmarketprices  :: [MarketPrice]                          -- ^ Market prices implied by transactions, in parse order (after journal finalisation)-  ,jtxnmodifiers          :: [TransactionModifier]-  ,jperiodictxns          :: [PeriodicTransaction]-  ,jtxns                  :: [Transaction]-  ,jfinalcommentlines     :: Text                                   -- ^ any final trailing comments in the (main) journal file-  ,jfiles                 :: [(FilePath, Text)]                     -- ^ the file path and raw text of the main and-                                                                    --   any included journal files. The main file is first,-                                                                    --   followed by any included files in the order encountered.-                                                                    --   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)+  ,jdeclaredpayees          :: [(Payee,PayeeDeclarationInfo)]         -- ^ Payees declared by payee directives, in parse order.+  ,jdeclaredtags            :: [(TagName,TagDeclarationInfo)]         -- ^ Tags declared by tag directives, in parse order.+  ,jdeclaredaccounts        :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order.+  ,jdeclaredaccounttags     :: M.Map AccountName [Tag]                -- ^ Accounts which were declared with tags, and those tags.+  ,jdeclaredaccounttypes    :: M.Map AccountType [AccountName]        -- ^ Accounts which were declared with a type: tag, grouped by the type.+  ,jaccounttypes            :: M.Map AccountName AccountType          -- ^ All the account types known, from account declarations or account names or parent accounts.+  ,jdeclaredcommodities     :: M.Map CommoditySymbol Commodity        -- ^ Commodities (and their display styles) declared by commodity directives, in parse order.+  ,jinferredcommoditystyles :: M.Map CommoditySymbol AmountStyle      -- ^ Commodity display styles inferred from amounts in the journal.+  ,jglobalcommoditystyles   :: M.Map CommoditySymbol AmountStyle      -- ^ Commodity display styles declared by command line options (sometimes augmented, see the import command).+  ,jpricedirectives         :: [PriceDirective]                       -- ^ P (market price) directives in the journal, in parse order.+  ,jinferredmarketprices    :: [MarketPrice]                          -- ^ Market prices inferred from transactions in the journal, in parse order.+  ,jtxnmodifiers            :: [TransactionModifier]                  -- ^ Auto posting rules declared in the journal.+  ,jperiodictxns            :: [PeriodicTransaction]                  -- ^ Periodic transaction rules declared in the journal.+  ,jtxns                    :: [Transaction]                          -- ^ Transactions recorded in the journal. The important bit.+  ,jfinalcommentlines       :: Text                                   -- ^ any final trailing comments in the (main) journal file+  ,jfiles                   :: [(FilePath, Text)]                     -- ^ the file path and raw text of the main and+                                                                      --   any included journal files. The main file is first,+                                                                      --   followed by any included files in the order encountered.+                                                                      --   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)
Hledger/Data/Valuation.hs view
@@ -48,7 +48,8 @@ import Hledger.Data.Amount import Hledger.Data.Dates (nulldate) import Text.Printf (printf)-import Data.Decimal (decimalPlaces, roundTo)+import Data.Decimal (decimalPlaces, roundTo, Decimal)+import Data.Word (Word8)   ------------------------------------------------------------------------------@@ -94,7 +95,9 @@ journalPriceOracle infer Journal{jpricedirectives, jinferredmarketprices} =   let     declaredprices = map priceDirectiveToMarketPrice jpricedirectives-    inferredprices = if infer then jinferredmarketprices else []+    inferredprices =+      (if infer then jinferredmarketprices else [])+      & traceOrLogAt 2 ("use prices inferred from costs? " <> if infer then "yes" else "no")     makepricegraph = memo $ makePriceGraph declaredprices inferredprices   in     memo $ uncurry3 $ priceLookup makepricegraph@@ -118,7 +121,7 @@       where u = amountSetFullPrecisionUpTo Nothing $ divideAmount n t     _                            -> Nothing   where-    pd = PriceDirective{pddate = d, pdcommodity = fromcomm, pdamount = nullamt}+    pd = PriceDirective{pdsourcepos=nullsourcepos, pddate=d, pdcommodity=fromcomm, pdamount=nullamt}  ------------------------------------------------------------------------------ -- Converting things to value@@ -273,14 +276,17 @@       Just to            ->         -- We have a commodity to convert to. Find the most direct price available,         -- according to the rules described in makePriceGraph.-        let msg = printf "seeking %s to %s price" (showCommoditySymbol from) (showCommoditySymbol to)-        in case -          (traceOrLogAt 2 (msg++" using forward prices") $ -            pricesShortestPath from to forwardprices)-          <|> -          (traceOrLogAt 2 (msg++" using forward and reverse prices") $ -            pricesShortestPath from to allprices)-        of+        let+          msg = printf "seeking %s to %s price" (showCommoditySymbol from) (showCommoditySymbol to)+          prices =+            (traceOrLogAt 2 (msg++" using forward prices") $+             traceOrLogAt 2 ("forward prices:\n" <> showMarketPrices forwardprices) $+             pricesShortestPath from to forwardprices)+            <|>+            (traceOrLogAt 2 (msg++" using forward and reverse prices") $+             traceOrLogAt 2 ("forward and reverse prices:\n" <> showMarketPrices allprices) $+             pricesShortestPath from to $ dbg5 "all forward and reverse prices" allprices)+        in case prices of           Nothing -> Nothing           Just [] -> Nothing           Just ps -> Just (mpto $ last ps, rate)@@ -290,13 +296,18 @@                 -- aggregate all the prices into one                 product rates                 -- product (Decimal's Num instance) normalises, stripping trailing zeros.-                -- Here we undo that (by restoring the old max precision with roundTo), -                -- so that amountValueAtDate can see the original internal precision,-                -- to use as the display precision of calculated value amounts.-                -- (This can add more than the original number of trailing zeros to some prices,-                -- making them seem more precise than they were, but it seems harmless here.)-                & roundTo (maximum $ map decimalPlaces rates)+                -- But we want to preserve even those, since the number of decimal digits+                -- here will guide amountValueAtDate in setting the Amount display precision later.+                -- So we restore them. Or rather, we ensure as many decimal digits as the maximum seen among rates.+                -- (Some prices might end up more precise than they were, but that seems harmless here.)+                & setMinDecimalPlaces (maximum $ map decimalPlaces rates) +-- Ensure this Decimal has at least this many decimal places, adding trailing zeros if necessary.+setMinDecimalPlaces :: Word8 -> Decimal -> Decimal+setMinDecimalPlaces n d+  | decimalPlaces d < n = roundTo n d  -- too few, add some zeros+  | otherwise           = d            -- more than enough, keep as-is+ tests_priceLookup =   let     p y m d from q to = MarketPrice{mpdate=fromGregorian y m d, mpfrom=from, mpto=to, mprate=q}@@ -326,7 +337,7 @@     -- ^ The date on which these prices are in effect.   ,pgEdges :: [Edge]     -- ^ "Forward" exchange rates between commodity pairs, either-    --   declared by P directives or inferred from transaction prices,+    --   declared by P directives or (with --infer-market-prices) inferred from costs,     --   forming the edges of a directed graph.     ,pgEdgesRev :: [Edge]     -- ^ The same edges, plus any additional edges that can be@@ -334,6 +345,7 @@     --     --   In both of these there will be at most one edge between each     --   directed pair of commodities, eg there can be one USD->EUR and one EUR->USD.+    --   ,pgDefaultValuationCommodities :: M.Map CommoditySymbol CommoditySymbol     -- ^ The default valuation commodity for each source commodity.     --   These are used when a valuation commodity is not specified@@ -352,9 +364,8 @@ pricesShortestPath start end edges =   -- at --debug=2 +, print the pretty path and also the detailed prices   let label = printf "shortest path from %s to %s: " (showCommoditySymbol start) (showCommoditySymbol end) in-  fmap (dbg2With (("price chain:\n"++).pshow)) $ -  dbg2With ((label++).(maybe "none found" (pshowpath ""))) $-+  fmap (dbg2With (("price chain:\n"++).showMarketPrices)) $+  dbg2With ((label++).(maybe "none" (pshowpath ""))) $   find [([],edges)]    where
Hledger/Query.hs view
@@ -23,6 +23,7 @@   parseQueryList,   parseQueryTerm,   parseAccountType,+  parseDepthSpec,   -- * modifying   simplifyQuery,   filterQuery,@@ -82,7 +83,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar (Day, fromGregorian )-import Safe (headErr, readDef, readMay, maximumByMay, maximumMay, minimumMay)+import Safe (headErr, readMay, maximumByMay, maximumMay, minimumMay) import Text.Megaparsec (between, noneOf, sepBy, try, (<?>), notFollowedBy) import Text.Megaparsec.Char (char, string, string') @@ -116,6 +117,7 @@   | Acct Regexp               -- ^ match account names infix-matched by this regexp   | Type [AccountType]        -- ^ match accounts whose type is one of these (or with no types, any account)   | Depth Int                 -- ^ match if account depth is less than or equal to this value (or, sometimes used as a display option)+  | DepthAcct Regexp Int      -- ^ match if the account matches and account depth is less than or equal to this value (usually used as a display option)   | Real Bool                 -- ^ match postings with this "realness" value   | Amt OrdPlus Quantity      -- ^ match if the amount's numeric quantity is less than/greater than/equal to/unsignedly equal to some value   | Sym Regexp                -- ^ match if the commodity symbol is fully-matched by this regexp@@ -301,11 +303,7 @@                               Right st -> Right (StatusQ st, []) parseQueryTerm _ (T.stripPrefix "real:" -> Just s) = Right (Real $ parseBool s || T.null s, []) parseQueryTerm _ (T.stripPrefix "amt:" -> Just s) = Right (Amt ord q, []) where (ord, q) = either error id $ parseAmountQueryTerm s  -- PARTIAL:-parseQueryTerm _ (T.stripPrefix "depth:" -> Just s)-  | n >= 0    = Right (Depth n, [])-  | otherwise = Left "depth: should have a positive number"-  where n = readDef 0 (T.unpack s)-+parseQueryTerm _ (T.stripPrefix "depth:" -> Just s) = (,[]) <$> parseDepthSpecQuery s parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = (,[]) . Sym <$> toRegexCI ("^" <> s <> "$") -- support cur: as an alias parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = (,[]) <$> parseTag s parseQueryTerm _ (T.stripPrefix "type:" -> Just s) = (,[]) <$> parseTypeCodes s@@ -473,6 +471,25 @@     return $ Tag tag body   where (n,v) = T.break (=='=') s +parseDepthSpec :: T.Text -> Either RegexError DepthSpec+parseDepthSpec s = do+    let depthString = T.unpack $ if T.null b then a else T.tail b+    depth <- case readMay depthString of+        Just d | d >= 0 -> Right d+        _ -> Left $ "depth: should be a positive number, but received " ++ depthString+    regexp <- mapM toRegexCI $ if T.null b then Nothing else Just a+    return $ case regexp of+      Nothing -> DepthSpec (Just depth) []+      Just r  -> DepthSpec Nothing [(r, depth)]+  where+    (a,b) = T.break (=='=') s++parseDepthSpecQuery :: T.Text -> Either RegexError Query+parseDepthSpecQuery s = do+    DepthSpec flat rs <- parseDepthSpec s+    let regexps = map (uncurry DepthAcct) rs+    return . And $ maybe id (\d -> (Depth d :)) flat regexps+ -- | Parse one or more account type code letters to a query matching any of those types. parseTypeCodes :: T.Text -> Either String Query parseTypeCodes s =@@ -639,6 +656,7 @@  queryIsDepth :: Query -> Bool queryIsDepth (Depth _) = True+queryIsDepth (DepthAcct _ _) = True queryIsDepth _ = False  queryIsReal :: Query -> Bool@@ -749,13 +767,12 @@     compareNothingMax (Just a) (Just b) = compare a b  -- | The depth limit this query specifies, if it has one-queryDepth :: Query -> Maybe Int-queryDepth = minimumMay . queryDepth'-  where-    queryDepth' (Depth d) = [d]-    queryDepth' (Or qs)   = concatMap queryDepth' qs-    queryDepth' (And qs)  = concatMap queryDepth' qs-    queryDepth' _         = []+queryDepth :: Query -> DepthSpec+queryDepth (Or qs)         = foldMap queryDepth qs+queryDepth (And qs)        = foldMap queryDepth qs+queryDepth (Depth d)       = DepthSpec (Just d) []+queryDepth (DepthAcct r d) = DepthSpec Nothing [(r,d)]+queryDepth _               = mempty  -- | The account we are currently focussed on, if any, and whether subaccounts are included. -- Just looks at the first query option.@@ -819,6 +836,7 @@ matchesAccount (And ms) a = all (`matchesAccount` a) ms matchesAccount (Acct r) a = regexMatchText r a matchesAccount (Depth d) a = accountNameLevel a <= d+matchesAccount (DepthAcct r d) a = accountNameLevel a <= d || not (regexMatchText r a) matchesAccount (Tag _ _) _ = False matchesAccount _ _ = True @@ -855,6 +873,7 @@ matchesPosting (StatusQ s) p = postingStatus p == s matchesPosting (Real v) p = v == isReal p matchesPosting q@(Depth _) Posting{paccount=a} = q `matchesAccount` a+matchesPosting q@(DepthAcct _ _) Posting{paccount=a} = q `matchesAccount` a matchesPosting q@(Amt _ _) Posting{pamount=as} = q `matchesMixedAmount` as matchesPosting (Sym r) Posting{pamount=as} = any (matchesCommodity (Sym r) . acommodity) $ amountsRaw as matchesPosting (Tag n v) p = case (reString n, v) of@@ -897,7 +916,8 @@ matchesTransaction (StatusQ s) t = tstatus t == s matchesTransaction (Real v) t = v == hasRealPostings t matchesTransaction q@(Amt _ _) t = any (q `matchesPosting`) $ tpostings t-matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t+matchesTransaction q@(Depth _) t = any (q `matchesPosting`) $ tpostings t+matchesTransaction q@(DepthAcct _ _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction q@(Sym _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction (Tag n v) t = case (reString n, v) of   ("payee", Just v') -> regexMatchText v' $ transactionPayee t
Hledger/Read.hs view
@@ -125,7 +125,7 @@  --- ** imports import qualified Control.Exception as C-import Control.Monad (unless, when)+import Control.Monad (unless, when, forM) import "mtl" Control.Monad.Except (ExceptT(..), runExceptT, liftEither) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Default (def)@@ -159,6 +159,7 @@ import Hledger.Utils import Prelude hiding (getContents, writeFile) import Hledger.Data.JournalChecks (journalStrictChecks)+import Text.Printf (printf)  --- ** doctest setup -- $setup@@ -401,12 +402,15 @@ previousLatestDates :: FilePath -> IO LatestDates previousLatestDates f = do   let latestfile = latestDatesFileFor f-      parsedate s = maybe (fail $ "could not parse date \"" ++ s ++ "\"") return $-                      parsedateM s   exists <- doesFileExist latestfile-  if exists-  then traverse (parsedate . T.unpack . T.strip) . T.lines =<< readFileStrictly latestfile-  else return []+  t <- if exists then readFileStrictly latestfile else return T.empty+  let nls = zip [1::Int ..] $ T.lines t+  fmap catMaybes $ forM nls $ \(n,l) -> do+    let s = T.unpack $ T.strip l+    case (s, parsedateM s) of+      ("", _)       -> return Nothing+      (_,  Nothing) -> error' (printf "%s:%d: invalid date: \"%s\"" latestfile n s)+      (_,  Just d)  -> return $ Just d  -- | Where to save latest transaction dates for the given file path. -- (.latest.FILE)
Hledger/Read/Common.hs view
@@ -99,7 +99,7 @@   emptyorcommentlinep2,   followingcommentp,   transactioncommentp,-  commenttagsp,+  commentlinetagsp,   postingcommentp,    -- ** bracketed dates@@ -192,14 +192,14 @@  -- | Parse an InputOpts from a RawOpts and a provided date. -- This will fail with a usage error if the forecast period expression cannot be parsed.-rawOptsToInputOpts :: Day -> RawOpts -> InputOpts-rawOptsToInputOpts day rawopts =+rawOptsToInputOpts :: Day -> Bool -> Bool -> RawOpts -> InputOpts+rawOptsToInputOpts day usecoloronstdout postingaccttags rawopts =      let noinferbalancingcosts = boolopt "strict" rawopts || stringopt "args" rawopts == "balanced"          -- Do we really need to do all this work just to get the requested end date? This is duplicating         -- much of reportOptsToSpec.-        ropts = rawOptsToReportOpts day rawopts+        ropts = rawOptsToReportOpts day usecoloronstdout rawopts         argsquery = map fst . rights . map (parseQueryTerm day) $ querystring_ ropts         datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery @@ -216,6 +216,7 @@       ,new_save_          = True       ,pivot_             = stringopt "pivot" rawopts       ,forecast_          = forecastPeriodFromRawOpts day rawopts+      ,posting_account_tags_ = postingaccttags       ,verbose_tags_      = boolopt "verbose-tags" rawopts       ,reportspan_        = DateSpan (Exact <$> queryStartDate False datequery) (Exact <$> queryEndDate False datequery)       ,auto_              = boolopt "auto" rawopts@@ -326,13 +327,14 @@ -- Others (commodities, accounts..) are done later by journalStrictChecks. -- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal-journalFinalise iopts@InputOpts{auto_,balancingopts_,infer_costs_,infer_equity_,strict_,verbose_tags_,_ioDay} f txt pj = do+journalFinalise iopts@InputOpts{auto_,balancingopts_,infer_costs_,infer_equity_,strict_,posting_account_tags_,verbose_tags_,_ioDay} f txt pj = do   let     BalancingOpts{commodity_styles_, ignore_assertions_} = balancingopts_     fname = "journalFinalise " <> takeFileName f     lbl = lbl_ fname+    -- Some not so pleasant hacks     -- We want to know when certain checks have been explicitly requested with the check command,-    -- but it does not run until later. For now, hackily inspect the command line with unsafePerformIO.+    -- but it does not run until later. For now, inspect the command line with unsafePerformIO.     checking checkname = "check" `elem` args && checkname `elem` args where args = progArgs     -- We will check ordered dates when "check ordereddates" is used.     checkordereddates = checking "ordereddates"@@ -346,12 +348,11 @@       &   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+            -- XXX does not see conversion accounts generated by journalInferEquityFromCosts below, requiring a workaround in journalCheckAccounts. Do it later ?       &   journalStyleAmounts                            -- Infer and apply commodity styles (but don't round) - should be done early       <&> journalAddForecast verbose_tags_ (forecastPeriod iopts pj)   -- Add forecast transactions if enabled-      <&> journalPostingsAddAccountTags                  -- Add account tags to postings, so they can be matched by auto postings.-      >>= journalMarkRedundantCosts                      -- Mark redundant costs, to help journalBalanceTransactions ignore them.-                                                         -- (Later, journalInferEquityFromCosts will do a similar pass, adding missing equity postings.)-+      <&> (if posting_account_tags_ then journalPostingsAddAccountTags else id)     -- Propagate account tags to postings - unless printing a beancount journal+      >>= journalTagCostsAndEquityAndMaybeInferCosts verbose_tags_ False   -- Tag equity conversion postings and redundant costs, to help journalBalanceTransactions ignore them.       >>= (if auto_ && not (null $ jtxnmodifiers pj)             then journalAddAutoPostings verbose_tags_ _ioDay balancingopts_  -- Add auto postings if enabled, and account tags if needed. Does preliminary transaction balancing.             else pure)@@ -365,8 +366,8 @@       -- <&> dbg9With (("journalFinalise amounts after styling, forecasting, auto postings, transaction balancing"<>).showJournalAmountsDebug)       >>= journalInferCommodityStyles                    -- infer commodity styles once more now that all posting amounts are present       -- >>= Right . dbg0With (pshow.journalCommodityStyles)-      >>= (if infer_costs_  then journalInferCostsFromEquity else pure)     -- Maybe infer costs from equity postings where possible-      <&> (if infer_equity_ then journalInferEquityFromCosts verbose_tags_ else id)  -- Maybe infer equity postings from costs where possible+      >>= (if infer_costs_  then journalTagCostsAndEquityAndMaybeInferCosts verbose_tags_ True else pure)  -- With --infer-costs, infer costs from equity postings where possible+      <&> (if infer_equity_ then journalInferEquityFromCosts verbose_tags_ else id)          -- With --infer-equity, infer equity postings from costs where possible       <&> dbg9With (lbl "amounts after equity-inferring".showJournalAmountsDebug)       <&> journalInferMarketPricesFromTransactions       -- infer market prices from commodity-exchanging transactions       -- <&> traceOrLogAt 6 fname  -- debug logging@@ -437,8 +438,8 @@ -- prior to the current position) commodity directive for the given commodity, if any. getAmountStyle :: CommoditySymbol -> JournalParser m (Maybe AmountStyle) getAmountStyle commodity = do-  Journal{jcommodities} <- get-  let mspecificStyle = M.lookup commodity jcommodities >>= cformat+  Journal{jdeclaredcommodities} <- get+  let mspecificStyle = M.lookup commodity jdeclaredcommodities >>= cformat   mdefaultStyle <- fmap snd <$> getDefaultCommodityAndStyle   return $ listToMaybe $ catMaybes [mspecificStyle, mdefaultStyle] @@ -1307,25 +1308,44 @@ isSameLineCommentStart ';' = True isSameLineCommentStart _   = False --- A parser for (possibly multiline) comments following a journal item.+-- | Parse a comment following a journal item, possibly continued on multiple lines,+-- and return the comment text. ----- Comments following a journal item begin with a semicolon and extend to--- the end of the line. They may span multiple lines; any comment lines --- not on the same line as the journal item must be indented (preceded by--- leading whitespace).+-- >>> rtp followingcommentp ""   -- no comment+-- Right ""+-- >>> rtp followingcommentp ";"    -- just a (empty) same-line comment. newline is added+-- Right "\n"+-- >>> rtp followingcommentp ";  \n"+-- Right "\n"+-- >>> rtp followingcommentp ";\n ;\n"  -- a same-line and a next-line comment+-- Right "\n\n"+-- >>> rtp followingcommentp "\n ;\n"  -- just a next-line comment. Insert an empty same-line comment so the next-line comment doesn't become a same-line comment.+-- Right "\n\n" ----- Like Ledger, we sometimes allow data to be embedded in comments. Eg,--- comments on the account directive and on transactions can contain tags,--- and comments on postings can contain tags and/or bracketed posting dates.--- To handle these variations, this parser takes as parameter a subparser,--- which should consume all input up until the next newline, and which can--- optionally extract some kind of data from it.--- followingcommentp' returns this data along with the full text of the comment.+followingcommentp :: TextParser m Text+followingcommentp =+  fst <$> followingcommentpWith (void $ takeWhileP Nothing (/= '\n'))  -- XXX support \r\n ?++{-# INLINABLE followingcommentp #-}++-- | Parse a following comment, possibly continued on multiple lines,+-- using the provided line parser to parse each line.+-- This returns the comment text, and the combined results from the line parser. ----- See followingcommentp for tests.+-- Following comments begin with a semicolon and extend to the end of the line.+-- They can optionally be continued on the next lines,+-- where each next line begins with an indent and another semicolon.+-- (This parser expects to see these semicolons and indents.) ---followingcommentp' :: (Monoid a, Show a) => TextParser m a -> TextParser m (Text, a)-followingcommentp' contentp = do+-- Like Ledger, we sometimes allow data to be embedded in comments.+-- account directive comments and transaction comments can contain tags,+-- and posting comments can contain tags or bracketed posting dates.+-- This helper lets us handle these variations. +-- The line parser should consume all input up until the next newline.+-- See followingcommentp for some tests.+--+followingcommentpWith :: (Monoid a, Show a) => TextParser m a -> TextParser m (Text, a)+followingcommentpWith contentp = do   skipNonNewlineSpaces   -- there can be 0 or 1 sameLine   sameLine <- try headerp *> ((:[]) <$> match' contentp) <|> pure []@@ -1346,27 +1366,37 @@   where     headerp = char ';' *> skipNonNewlineSpaces -{-# INLINABLE followingcommentp' #-}+{-# INLINABLE followingcommentpWith #-} --- | Parse the text of a (possibly multiline) comment following a journal item.------ >>> rtp followingcommentp ""   -- no comment--- Right ""--- >>> rtp followingcommentp ";"    -- just a (empty) same-line comment. newline is added--- Right "\n"--- >>> rtp followingcommentp ";  \n"--- Right "\n"--- >>> rtp followingcommentp ";\n ;\n"  -- a same-line and a next-line comment--- Right "\n\n"--- >>> rtp followingcommentp "\n ;\n"  -- just a next-line comment. Insert an empty same-line comment so the next-line comment doesn't become a same-line comment.--- Right "\n\n"----followingcommentp :: TextParser m Text-followingcommentp =-  fst <$> followingcommentp' (void $ takeWhileP Nothing (/= '\n'))  -- XXX support \r\n ?-{-# INLINABLE followingcommentp #-} +-- Parse the tags from a single comment line, eg for use with followingcommentpWith.+-- XXX what part of a comment line ? leading whitespace / semicolon or not ?+commentlinetagsp :: TextParser m [Tag]+commentlinetagsp = do+  -- XXX sketchy+  tagName <- (last . T.split isSpace) <$> takeWhileP Nothing (\c -> c /= ':' && c /= '\n')+  atColon tagName <|> pure [] -- if not ':', then either '\n' or EOF +  where+    atColon :: Text -> TextParser m [Tag]+    atColon name = char ':' *> do+      if T.null name+        then commentlinetagsp+        else do+          skipNonNewlineSpaces+          val <- tagValue+          let tag = (name, val)+          (tag:) <$> commentlinetagsp++    tagValue :: TextParser m Text+    tagValue = do+      val <- T.strip <$> takeWhileP Nothing (\c -> c /= ',' && c /= '\n')+      _ <- optional $ char ','+      pure val++{-# INLINABLE commentlinetagsp #-}++ -- | Parse a transaction comment and extract its tags. -- -- The first line of a transaction may be followed by comments, which@@ -1393,34 +1423,10 @@ -- leading and trailing whitespace. -- transactioncommentp :: TextParser m (Text, [Tag])-transactioncommentp = followingcommentp' commenttagsp+transactioncommentp = followingcommentpWith commentlinetagsp {-# INLINABLE transactioncommentp #-} -commenttagsp :: TextParser m [Tag]-commenttagsp = do-  tagName <- (last . T.split isSpace) <$> takeWhileP Nothing (\c -> c /= ':' && c /= '\n')-  atColon tagName <|> pure [] -- if not ':', then either '\n' or EOF -  where-    atColon :: Text -> TextParser m [Tag]-    atColon name = char ':' *> do-      if T.null name-        then commenttagsp-        else do-          skipNonNewlineSpaces-          val <- tagValue-          let tag = (name, val)-          (tag:) <$> commenttagsp--    tagValue :: TextParser m Text-    tagValue = do-      val <- T.strip <$> takeWhileP Nothing (\c -> c /= ',' && c /= '\n')-      _ <- optional $ char ','-      pure val--{-# INLINABLE commenttagsp #-}-- -- | Parse a posting comment and extract its tags and dates. -- -- Postings may be followed by comments, which begin with semicolons and@@ -1473,7 +1479,7 @@   :: Maybe Year -> TextParser m (Text, [Tag], Maybe Day, Maybe Day) postingcommentp mYear = do   (commentText, (tags, dateTags)) <--    followingcommentp' (commenttagsanddatesp mYear)+    followingcommentpWith (commenttagsanddatesp mYear)   let mdate  = snd <$> find ((=="date") .fst) dateTags       mdate2 = snd <$> find ((=="date2").fst) dateTags   pure (commentText, tags, mdate, mdate2)
Hledger/Read/InputOptions.hs view
@@ -34,6 +34,7 @@     ,new_save_          :: Bool                 -- ^ save latest new transactions state for next time ?     ,pivot_             :: String               -- ^ use the given field's value as the account name     ,forecast_          :: Maybe DateSpan       -- ^ span in which to generate forecast transactions+    ,posting_account_tags_  :: Bool             -- ^ propagate account tags to postings ?     ,verbose_tags_      :: Bool                 -- ^ add user-visible tags when generating/modifying transactions & postings ?     ,reportspan_        :: DateSpan             -- ^ a dirty hack keeping the query dates in InputOpts. This rightfully lives in ReportSpec, but is duplicated here.     ,auto_              :: Bool                 -- ^ generate extra postings according to auto posting rules ?@@ -55,6 +56,7 @@     , new_save_          = True     , pivot_             = ""     , forecast_          = Nothing+    , posting_account_tags_ = False     , verbose_tags_      = False     , reportspan_        = nulldatespan     , auto_              = False
Hledger/Read/JournalReader.hs view
@@ -374,7 +374,7 @@       ,jparseparentaccounts   = jparseparentaccounts j       ,jparsedecimalmark      = jparsedecimalmark j       ,jparsealiases          = jparsealiases j-      ,jcommodities           = jcommodities j+      ,jdeclaredcommodities           = jdeclaredcommodities j       -- ,jparsetransactioncount = jparsetransactioncount j       ,jparsetimeclockentries = jparsetimeclockentries j       ,jincludefilestack      = filepath : jincludefilestack j@@ -509,7 +509,7 @@   let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg6 "style from commodity directive" astyle}   if isNothing $ asdecimalmark astyle   then customFailure $ parseErrorAt off pleaseincludedecimalpoint-  else modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})+  else modify' (\j -> j{jdeclaredcommodities=M.insert acommodity comm $ jdeclaredcommodities j})  pleaseincludedecimalpoint :: String pleaseincludedecimalpoint = chomp $ unlines [@@ -535,7 +535,7 @@   subdirectives <- many $ indented (eitherP (formatdirectivep sym) (lift restofline))   let mfmt = lastMay $ lefts subdirectives   let comm = Commodity{csymbol=sym, cformat=mfmt}-  modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j})+  modify' (\j -> j{jdeclaredcommodities=M.insert sym comm $ jdeclaredcommodities j})   where     indented = (lift skipNonNewlineSpaces1 >>) @@ -671,15 +671,16 @@  marketpricedirectivep :: JournalParser m PriceDirective marketpricedirectivep = do+  pos <- getSourcePos   char 'P' <?> "market price"   lift skipNonNewlineSpaces   date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored   lift skipNonNewlineSpaces1   symbol <- lift commoditysymbolp-  lift skipNonNewlineSpaces+  lift skipNonNewlineSpaces1   price <- amountp   lift restofline-  return $ PriceDirective date symbol price+  return $ PriceDirective pos date symbol price  ignoredpricecommoditydirectivep :: JournalParser m () ignoredpricecommoditydirectivep = do@@ -1116,7 +1117,7 @@         [("a:b", AccountDeclarationInfo{adicomment          = "type:asset\n"                                        ,aditags             = [("type","asset")]                                        ,adideclarationorder = 1-                                       ,adisourcepos        = fst nullsourcepos+                                       ,adisourcepos        = nullsourcepos                                        })         ]       ]@@ -1145,6 +1146,7 @@   ,testCase "marketpricedirectivep" $ assertParseEq marketpricedirectivep     "P 2017/01/30 BTC $922.83\n"     PriceDirective{+      pdsourcepos = nullsourcepos,       pddate      = fromGregorian 2017 1 30,       pdcommodity = "BTC",       pdamount    = usd 922.83
Hledger/Read/RulesReader.hs view
@@ -14,6 +14,7 @@ -- stack haddock hledger-lib --fast --no-haddock-deps --haddock-arguments='--ignore-all-exports' --open  --- ** language+{-# LANGUAGE CPP    #-} {-# LANGUAGE FlexibleInstances    #-} {-# LANGUAGE OverloadedStrings    #-} {-# LANGUAGE RecordWildCards      #-}@@ -52,7 +53,10 @@ import Data.Char                  (toLower, isDigit, isSpace, isAlphaNum, ord) import Data.Bifunctor             (first) import Data.Functor               ((<&>))-import Data.List (elemIndex, foldl', mapAccumL, nub, sortOn)+import Data.List (elemIndex, mapAccumL, nub, sortOn)+#if !MIN_VERSION_base(4,20,0)+import Data.List (foldl')+#endif import Data.List.Extra (groupOn) import Data.Maybe (catMaybes, fromMaybe, isJust) import Data.MemoUgly (memo)@@ -76,7 +80,7 @@  import Hledger.Data import Hledger.Utils-import Hledger.Read.Common (aliasesFromOpts, Reader(..), InputOpts(..), amountp, statusp, journalFinalise, accountnamep, commenttagsp )+import Hledger.Read.Common (aliasesFromOpts, Reader(..), InputOpts(..), amountp, statusp, journalFinalise, accountnamep, transactioncommentp, postingcommentp ) import Hledger.Write.Csv import System.Directory (doesFileExist, getHomeDirectory) import Data.Either (fromRight)@@ -304,8 +308,13 @@ -- | A strptime date parsing pattern, as supported by Data.Time.Format. type DateFormat       = Text --- | A prefix for a matcher test, either & or none (implicit or).-data MatcherPrefix = And | Not | None+-- | A representation of a matcher's prefix, which indicates how it should be+-- interpreted or combined with other matchers.+data MatcherPrefix =+    Or      -- ^ no prefix+  | And     -- ^ &+  | Not     -- ^ !+  | AndNot  -- ^ & !   deriving (Show, Eq)  -- | A single test for matching a CSV record, in one way or another.@@ -314,6 +323,14 @@   | FieldMatcher MatcherPrefix CsvFieldReference Regexp         -- ^ match if this regexp matches the referenced CSV field's value   deriving (Show, Eq) +matcherPrefix :: Matcher -> MatcherPrefix+matcherPrefix (RecordMatcher prefix _) = prefix+matcherPrefix (FieldMatcher prefix _ _) = prefix++matcherSetPrefix :: MatcherPrefix -> Matcher -> Matcher+matcherSetPrefix p (RecordMatcher _ r)  = RecordMatcher p r+matcherSetPrefix p (FieldMatcher _ f r) = FieldMatcher p f r+ -- | A conditional block: a set of CSV record matchers, and a sequence -- of rules which will be enabled only if one or more of the matchers -- succeeds.@@ -631,6 +648,9 @@         -- A single matcher, on one line.+-- This tries to parse first as a field matcher, then if that fails, as a whole-record matcher;+-- the goal was to not break legacy whole-record patterns that happened to look a bit like a field matcher+-- (eg, beginning with %, possibly preceded by & or !), or at least not to raise an error. matcherp' :: CsvRulesParser () -> CsvRulesParser Matcher matcherp' end = try (fieldmatcherp end) <|> recordmatcherp end @@ -675,13 +695,18 @@ matcherprefixp :: CsvRulesParser MatcherPrefix matcherprefixp = do   lift $ dbgparse 8 "trying matcherprefixp"-  (char '&' >> lift skipNonNewlineSpaces >> return And) <|> (char '!' >> lift skipNonNewlineSpaces >> return Not) <|> return None+  (do+    char '&' >> lift skipNonNewlineSpaces+    fromMaybe And <$> optional (char '!' >> lift skipNonNewlineSpaces >> return AndNot))+  <|> (char '!' >> lift skipNonNewlineSpaces >> return Not)+  <|> return Or  csvfieldreferencep :: CsvRulesParser CsvFieldReference csvfieldreferencep = do   lift $ dbgparse 8 "trying csvfieldreferencep"   char '%'   T.cons '%' . textQuoteIfNeeded <$> fieldnamep+    -- XXX this parses any generic field name, which may not actually be a valid CSV field name [#2289]  -- A single regular expression regexp :: CsvRulesParser () -> CsvRulesParser Regexp@@ -736,7 +761,7 @@  maybeNegate :: MatcherPrefix -> Bool -> Bool maybeNegate Not origbool = not origbool-maybeNegate _ origbool = origbool+maybeNegate _   origbool = origbool  -- | Given the conversion rules, a CSV record and a hledger field name, find -- either the last applicable `ConditionalBlock`, or the final value template@@ -766,24 +791,28 @@ isBlockActive :: CsvRules -> CsvRecord -> ConditionalBlock -> Bool isBlockActive rules record CB{..} = any (all matcherMatches) $ groupedMatchers cbMatchers   where-    -- does this individual matcher match the current csv record ?+    -- Does this individual matcher match the current csv record ?+    -- A matcher's target can be a specific CSV field, or the "whole record".+    --+    -- In the former case, note that the field reference must be either numeric or+    -- a csv field name declared by a `fields` rule; anything else will emit a warning to stderr+    -- (to reduce confusion when a hledger field name doesn't work; not an error, to avoid breaking legacy rules; see #2289).+    --+    -- In the latter case, the matched value will be a synthetic CSV record.+    -- Note this will not necessarily be the same as the original CSV record:+    -- the field separator will be comma, and quotes enclosing field values,+    -- and any whitespace outside those quotes, will be removed.+    -- (This means that a field containing a comma will now look like two fields.)+    --     matcherMatches :: Matcher -> Bool-    matcherMatches (RecordMatcher prefix pat) = maybeNegate prefix origbool-      where-        pat' = dbg7 "regex" pat-        -- A synthetic whole CSV record to match against. Note, this can be-        -- different from the original CSV data:-        -- - any whitespace surrounding field values is preserved-        -- - any quotes enclosing field values are removed-        -- - and the field separator is always comma-        -- which means that a field containing a comma will look like two fields.-        wholecsvline = dbg7 "wholecsvline" $ T.intercalate "," record-        origbool = regexMatchText pat' wholecsvline-    matcherMatches (FieldMatcher prefix csvfieldref pat) = maybeNegate prefix origbool-      where-        -- the value of the referenced CSV field to match against.-        csvfieldvalue = dbg7 "csvfieldvalue" $ replaceCsvFieldReference rules record csvfieldref-        origbool = regexMatchText pat csvfieldvalue+    matcherMatches = \case+      RecordMatcher prefix             pat -> maybeNegate prefix $ match pat $ T.intercalate "," record+      FieldMatcher  prefix csvfieldref pat -> maybeNegate prefix $ match pat $+        fromMaybe "" $ replaceCsvFieldReference rules record csvfieldref+        -- (warn msg "") where msg = "if "<>T.unpack csvfieldref<>": this should be a name declared with 'fields', or %NUM"+        --  #2289: we'd like to warn the user when an unknown CSV field is being referenced,+        --  but it's useful to ignore it for easier reuse of rules files.+      where match p v = regexMatchText (dbg7 "regex" p) (dbg7 "value" v)      -- | Group matchers into associative pairs based on prefix, e.g.:     --   A@@ -792,14 +821,13 @@     --   D     --   & E     --   => [[A, B], [C], [D, E]]+    --  & ! M (and not M) are converted to ! M (not M) within the and groups.     groupedMatchers :: [Matcher] -> [[Matcher]]     groupedMatchers [] = []-    groupedMatchers (x:xs) = (x:ys) : groupedMatchers zs+    groupedMatchers (m:ms) = (m:ands) : groupedMatchers rest       where-        (ys, zs) = span (\y -> matcherPrefix y == And) xs-        matcherPrefix :: Matcher -> MatcherPrefix-        matcherPrefix (RecordMatcher prefix _) = prefix-        matcherPrefix (FieldMatcher prefix _ _) = prefix+        (andandnots, rest) = span (\a -> matcherPrefix a `elem` [And, AndNot]) ms+        ands = [matcherSetPrefix p a | a <- andandnots, let p = if matcherPrefix a == AndNot then Not else And]  -- | Render a field assignment's template, possibly interpolating referenced -- CSV field values or match groups. Outer whitespace is removed from interpolated values.@@ -809,7 +837,7 @@     (many       (   literaltextp       <|> (matchrefp <&> replaceRegexGroupReference rules record)-      <|> (fieldrefp <&> replaceCsvFieldReference   rules record)+      <|> (fieldrefp <&> replaceCsvFieldReference   rules record <&> fromMaybe "")       )     )     t@@ -842,20 +870,18 @@   in atMay matchgroups group  getMatchGroups :: CsvRules -> CsvRecord -> Matcher -> [Text]-getMatchGroups _ record (RecordMatcher _ regex)  = let-  txt = T.intercalate "," record -- see caveats of wholecsvline, in `isBlockActive`-  in regexMatchTextGroups regex txt-getMatchGroups rules record (FieldMatcher _ fieldref regex) = let-  txt = replaceCsvFieldReference rules record fieldref-  in regexMatchTextGroups regex txt+getMatchGroups _ record (RecordMatcher _ regex) =+  regexMatchTextGroups regex $ T.intercalate "," record -- see caveats in matcherMatches+getMatchGroups rules record (FieldMatcher _ fieldref regex) =+  regexMatchTextGroups regex $ fromMaybe "" $ replaceCsvFieldReference rules record fieldref  -- | 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--- can't find such a field, replace it with the empty string.-replaceCsvFieldReference :: CsvRules -> CsvRecord -> CsvFieldReference -> Text+-- can't find a csv field with that name, return nothing.+replaceCsvFieldReference :: CsvRules -> CsvRecord -> CsvFieldReference -> Maybe Text replaceCsvFieldReference rules record s = case T.uncons s of-    Just ('%', fieldname) -> fromMaybe "" $ csvFieldValue rules record fieldname-    _                     -> s+    Just ('%', fieldname) -> csvFieldValue rules record fieldname+    _                     -> Nothing  -- | Get the (whitespace-stripped) value of a CSV field, identified by its name or -- column number, ("date" or "1"), from the given CSV record, if such a field exists.@@ -1111,7 +1137,13 @@     code        = maybe "" singleline' $ fieldval "code"     description = maybe "" singleline' $ fieldval "description"     comment     = maybe "" unescapeNewlines $ fieldval "comment"-    ttags       = fromRight [] $ rtp commenttagsp comment++    -- Convert some parsed comment text back into following comment syntax,+    -- with the semicolons and indents, so it can be parsed again for tags.+    textToFollowingComment :: Text -> Text+    textToFollowingComment = T.stripStart . T.unlines . map (" ;"<>) . T.lines++    ttags       = fromRight [] $ fmap snd $ rtp transactioncommentp $ textToFollowingComment comment     precomment  = maybe "" unescapeNewlines $ fieldval "precomment"      singleline' = T.unwords . filter (not . T.null) . map T.strip . T.lines@@ -1124,7 +1156,15 @@     p1IsVirtual = (accountNamePostingType <$> fieldval "account1") == Just VirtualPosting     ps = [p | n <- [1..maxpostings]          ,let cmt  = maybe "" unescapeNewlines $ fieldval ("comment"<> T.pack (show n))-         ,let ptags = fromRight [] $ rtp commenttagsp cmt+          -- Tags in the comment will be parsed and attached to the posting.+          -- A posting date, in the date: tag or in brackets, will also be parsed and applied to the posting.+          -- But it must have a year, or it will be ignored.+          -- A secondary posting date will also be ignored.+         ,let (tags,mdate) =+                fromRight ([],Nothing) $+                fmap (\(_,ts,md,_)->(ts,md)) $+                rtp (postingcommentp Nothing) $+                textToFollowingComment cmt          ,let currency = fromMaybe "" (fieldval ("currency"<> T.pack (show n)) <|> fieldval "currency")          ,let mamount  = getAmount rules record currency p1IsVirtual n          ,let mbalance = getBalance rules record currency n@@ -1132,12 +1172,13 @@          ,let acct' | not isfinal && acct==unknownExpenseAccount &&                       fromMaybe False (mamount >>= isNegativeMixedAmount) = unknownIncomeAccount                     | otherwise = acct-         ,let p = nullposting{paccount          = accountNameWithoutPostingType acct'+         ,let p = nullposting{pdate             = mdate+                             ,paccount          = accountNameWithoutPostingType acct'                              ,pamount           = fromMaybe missingmixedamt mamount                              ,ptransaction      = Just t                              ,pbalanceassertion = mkBalanceAssertion rules record <$> mbalance                              ,pcomment          = cmt-                             ,ptags             = ptags+                             ,ptags             = tags                              ,ptype             = accountNamePostingType acct                              }          ]@@ -1310,9 +1351,7 @@       ,showRules rules record       -- ,"the default-currency is: "++fromMaybe "unspecified" (getDirective "default-currency" rules)       ,"the parse error is:      " <> T.pack (customErrorBundlePretty e)-      ,"you may need to \-        \change your amount*, balance*, or currency* rules, \-        \or add or change your skip rule"+      ,"you may need to change your amount*, balance*, or currency* rules, or add or change your skip rule"       ]  -- | Show the values assigned to each journal field.@@ -1492,12 +1531,12 @@      ,testCase "assignment with empty value" $       parseWithState' defrules rulesp "account1 \nif foo\n  account2 foo\n" @?=-        (Right (mkrules $ defrules{rassignments = [("account1","")], rconditionalblocks = [CB{cbMatchers=[RecordMatcher None (toRegex' "foo")],cbAssignments=[("account2","foo")]}]}))+        (Right (mkrules $ defrules{rassignments = [("account1","")], rconditionalblocks = [CB{cbMatchers=[RecordMatcher Or (toRegex' "foo")],cbAssignments=[("account2","foo")]}]}))    ]   ,testGroup "conditionalblockp" [     testCase "space after conditional" $       parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=-        (Right $ CB{cbMatchers=[RecordMatcher None $ toRegexCI' "a"],cbAssignments=[("account2","b")]})+        (Right $ CB{cbMatchers=[RecordMatcher Or $ toRegexCI' "a"],cbAssignments=[("account2","b")]})   ],    testGroup "csvfieldreferencep" [@@ -1509,16 +1548,16 @@   ,testGroup "matcherp" [      testCase "recordmatcherp" $-      parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher None $ toRegexCI' "A A")+      parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher Or $ toRegexCI' "A A")     ,testCase "recordmatcherp.starts-with-&" $       parseWithState' defrules matcherp "& A A\n" @?= (Right $ RecordMatcher And $ toRegexCI' "A A")     ,testCase "fieldmatcherp.starts-with-%" $-      parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher None $ toRegexCI' "description A A")+      parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher Or $ toRegexCI' "description A A")     ,testCase "fieldmatcherp" $-      parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher None "%description" $ toRegexCI' "A A")+      parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher Or "%description" $ toRegexCI' "A A")     ,testCase "fieldmatcherp.starts-with-&" $       parseWithState' defrules matcherp "& %description A A\n" @?= (Right $ FieldMatcher And "%description" $ toRegexCI' "A A")@@ -1533,7 +1572,7 @@      in testCase "toplevel" $ hledgerField rules ["a","b"] "date" @?= (Just "%csvdate") -   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a"] [("date","%csvdate")]]}+   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher Or "%csvdate" $ toRegex' "a"] [("date","%csvdate")]]}     in testCase "conditional" $ hledgerField rules ["a","b"] "date" @?= (Just "%csvdate")     ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher Not "%csvdate" $ toRegex' "a"] [("date","%csvdate")]]}@@ -1542,16 +1581,16 @@    ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher Not "%csvdate" $ toRegex' "b"] [("date","%csvdate")]]}     in testCase "negated-conditional-true" $ hledgerField rules ["a","b"] "date" @?= (Just "%csvdate") -   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher None "%description" $ toRegex' "b"] [("date","%csvdate")]]}+   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher Or "%csvdate" $ toRegex' "a", FieldMatcher Or "%description" $ toRegex' "b"] [("date","%csvdate")]]}     in testCase "conditional-with-or-a" $ hledgerField rules ["a"] "date" @?= (Just "%csvdate") -   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher None "%description" $ toRegex' "b"] [("date","%csvdate")]]}+   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher Or "%csvdate" $ toRegex' "a", FieldMatcher Or "%description" $ toRegex' "b"] [("date","%csvdate")]]}     in testCase "conditional-with-or-b" $ hledgerField rules ["_", "b"] "date" @?= (Just "%csvdate") -   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher And "%description" $ toRegex' "b"] [("date","%csvdate")]]}+   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher Or "%csvdate" $ toRegex' "a", FieldMatcher And "%description" $ toRegex' "b"] [("date","%csvdate")]]}     in testCase "conditional.with-and" $ hledgerField rules ["a", "b"] "date" @?= (Just "%csvdate") -   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher And "%description" $ toRegex' "b", FieldMatcher None "%description" $ toRegex' "c"] [("date","%csvdate")]]}+   ,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher Or "%csvdate" $ toRegex' "a", FieldMatcher And "%description" $ toRegex' "b", FieldMatcher Or "%description" $ toRegex' "c"] [("date","%csvdate")]]}     in testCase "conditional.with-and-or" $ hledgerField rules ["_", "c"] "date" @?= (Just "%csvdate")     ]@@ -1562,9 +1601,9 @@           { rcsvfieldindexes=[ ("date",1), ("description",2) ]           , rassignments=[ ("account2","equity"), ("amount1","1") ]           -- ConditionalBlocks here are in reverse order: mkrules reverses the list-          , rconditionalblocks=[ CB { cbMatchers=[FieldMatcher None "%description" (toRegex' "PREFIX (.*) - (.*)")] +          , rconditionalblocks=[ CB { cbMatchers=[FieldMatcher Or "%description" (toRegex' "PREFIX (.*) - (.*)")]                                     , cbAssignments=[("account1","account:\\1:\\2")] }-                               , CB { cbMatchers=[FieldMatcher None "%description" (toRegex' "PREFIX (.*)")]+                               , CB { cbMatchers=[FieldMatcher Or "%description" (toRegex' "PREFIX (.*)")]                                     , cbAssignments=[("account1","account:\\1"), ("comment1","\\1")] }                                ]           }
Hledger/Reports/AccountTransactionsReport.hs view
@@ -162,6 +162,7 @@       . traceAtWith 5 (("ts4:\n"++).pshowTransactions.map snd)       . sortBy (comparing (Down . fst) <> comparing (Down . tindex . snd))       . map (\t -> (transactionRegisterDate wd reportq thisacctq t, t))+      . map (if invert_ ropts then (\t -> t{tpostings = map negatePostingAmount $ tpostings t}) else id)       $ jtxns acctJournal  pshowTransactions :: [Transaction] -> String
Hledger/Reports/BalanceReport.hs view
@@ -71,7 +71,7 @@     report = multiBalanceReport rspec j     rows = [( prrFullName row             , prrDisplayName row-            , prrDepth row - 1  -- BalanceReport uses 0-based account depths+            , prrIndent row             , prrTotal row             ) | row <- prRows report]     total = prrTotal $ prTotals report@@ -85,7 +85,7 @@       jtxns = [         txnTieKnot Transaction{           tindex=0,-          tsourcepos=nullsourcepos,+          tsourcepos=nullsourcepospair,           tdate=fromGregorian 2008 01 01,           tdate2=Just $ fromGregorian 2009 01 01,           tstatus=Unmarked,@@ -149,7 +149,7 @@        mixedAmount (usd 0))      ,testCase "with --depth=N" $-     (defreportspec{_rsReportOpts=defreportopts{depth_=Just 1}}, samplejournal) `gives`+     (defreportspec{_rsReportOpts=defreportopts{depth_=DepthSpec (Just 1) []}}, samplejournal) `gives`       ([        ("expenses",    "expenses",     0, mixedAmount (usd 2))        ,("income",      "income",      0, mixedAmount (usd (-2)))@@ -222,7 +222,7 @@        ]        ,testCase "accounts report with account pattern o and --depth 1" ~:-       defreportopts{patterns_=["o"],depth_=Just 1} `gives`+       defreportopts{patterns_=["o"],depth_=(Just 1, [])} `gives`        ["                  $1  expenses"        ,"                 $-2  income"        ,"--------------------"
Hledger/Reports/EntriesReport.hs view
@@ -21,7 +21,7 @@ import Data.Time (fromGregorian)  import Hledger.Data-import Hledger.Query (Query(..))+import Hledger.Query (Query(..), filterQuery, queryIsDepth) import Hledger.Reports.ReportOptions import Hledger.Utils @@ -37,7 +37,7 @@ entriesReport rspec@ReportSpec{_rsReportOpts=ropts} =     sortBy (comparing $ transactionDateFn ropts) . jtxns     . journalApplyValuationFromOpts (setDefaultConversionOp NoConversionOp rspec)-    . filterJournalTransactions (_rsQuery rspec)+    . filterJournalTransactions (filterQuery (not.queryIsDepth) $ _rsQuery rspec)  tests_EntriesReport = testGroup "EntriesReport" [   testGroup "entriesReport" [
Hledger/Reports/MultiBalanceReport.hs view
@@ -112,6 +112,8 @@     -- Queries, report/column dates.     (reportspan, colspans) = reportSpan j rspec'     rspec = dbg3 "reportopts" $ makeReportQuery rspec' reportspan+    -- force evaluation order to show price lookup after date spans in debug output (XXX not working)+    -- priceoracle = reportspan `seq` priceoracle0      -- Group postings into their columns.     colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle colspans@@ -287,10 +289,12 @@               filterQueryOrNotQuery (\q -> queryIsAcct q || queryIsType q || queryIsTag q) query      filterbydepth = case accountlistmode_ of-      ALTree -> filter ((depthq `matchesAccount`) . aname)    -- a tree - just exclude deeper accounts-      ALFlat -> clipAccountsAndAggregate (queryDepth depthq)  -- a list - aggregate deeper accounts at the depth limit-                . filter ((0<) . anumpostings)                -- and exclude empty parent accounts-      where depthq = dbg3 "depthq" $ filterQuery queryIsDepth query+        ALTree -> filter (depthMatches . aname)       -- a tree - just exclude deeper accounts+        ALFlat -> clipAccountsAndAggregate depthSpec  -- a list - aggregate deeper accounts at the depth limit+                  . filter ((0<) . anumpostings)      -- and exclude empty parent accounts+      where+        depthSpec = dbg3 "depthq" . queryDepth $ filterQuery queryIsDepth query+        depthMatches name = maybe True (accountNameLevel name <=) $ getAccountNameClippedDepth depthSpec name      accts = filterbydepth $ drop 1 $ accountsFromPostings ps' @@ -400,43 +404,43 @@         rowavg = averageMixedAmounts rowbals     balance = case accountlistmode_ ropts of ALTree -> aibalance; ALFlat -> aebalance --- | Calculate accounts which are to be displayed in the report, as well as--- their name and depth+-- | Calculate accounts which are to be displayed in the report,+-- and their name and their indent level if displayed in tree mode. displayedAccounts :: ReportSpec                   -> Set AccountName                   -> HashMap AccountName (Map DateSpan Account)                   -> HashMap AccountName DisplayName displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts valuedaccts-    | qdepth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1-    | otherwise  = HM.mapWithKey (\a _ -> displayedName a) displayedAccts+    | qdepthIsZero = HM.singleton "..." $ DisplayName "..." "..." 0+    | otherwise    = HM.mapWithKey (\a _ -> displayedName a) displayedAccts   where-    -- Accounts which are to be displayed-    displayedAccts = (if qdepth == 0 then id else HM.filterWithKey keep) valuedaccts-      where-        keep name amts = isInteresting name amts || name `HM.member` interestingParents-     displayedName name = case accountlistmode_ ropts of-        ALTree -> DisplayName name leaf . max 0 $ level - boringParents-        ALFlat -> DisplayName name droppedName 1+        ALTree -> DisplayName name leaf (max 0 $ level - 1 - boringParents)+        ALFlat -> DisplayName name droppedName 0       where-        droppedName = accountNameDrop (drop_ ropts) name-        leaf = accountNameFromComponents . reverse . map accountLeafName $-            droppedName : takeWhile notDisplayed parents--        level = max 0 $ accountNameLevel name - drop_ ropts-        parents = take (level - 1) $ parentAccountNames name+        droppedName   = accountNameDrop (drop_ ropts) name+        leaf          = accountNameFromComponents . reverse . map accountLeafName $+                          droppedName : takeWhile notDisplayed parents+        level         = max 0 $ (accountNameLevel name) - drop_ ropts+        parents       = take (level - 1) $ parentAccountNames name         boringParents = if no_elide_ ropts then 0 else length $ filter notDisplayed parents-        notDisplayed = not . (`HM.member` displayedAccts)+        notDisplayed  = not . (`HM.member` displayedAccts) +    -- Accounts which are to be displayed+    displayedAccts = (if qdepthIsZero then id else HM.filterWithKey keep) valuedaccts+      where+        keep name amts = isInteresting name amts || name `HM.member` interestingParents+     -- Accounts interesting for their own sake     isInteresting name amts =-        d <= qdepth                                  -- Throw out anything too deep+        d <= qdepth                                -- Throw out anything too deep         && ( 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         d = accountNameLevel name+        qdepth = fromMaybe maxBound $ getAccountNameClippedDepth depthspec name         keepWhenEmpty = case accountlistmode_ ropts of             ALFlat -> const True          -- Keep all empty accounts in flat mode             ALTree -> all (null . asubs)  -- Keep only empty leaves in tree mode@@ -454,7 +458,8 @@         minSubs = if no_elide_ ropts then 1 else 2      isZeroRow balance = all (mixedAmountLooksZero . balance)-    qdepth = fromMaybe maxBound $ queryDepth query+    depthspec = queryDepth query+    qdepthIsZero = depthspec == DepthSpec (Just 0) []     numSubs = subaccountTallies . HM.keys $ HM.filterWithKey isInteresting valuedaccts  -- | Sort the rows by amount or by account declaration order.@@ -601,7 +606,7 @@           (eitems, etotal) = r           (PeriodicReport _ aitems atotal) = multiBalanceReport rspec' journal           showw (PeriodicReportRow a lAmt amt amt')-              = (displayFull a, displayName a, displayDepth a, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')+              = (displayFull a, displayName a, displayIndent a, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')       (map showw aitems) @?= (map showw eitems)       showMixedAmountDebug (prrTotal atotal) @?= showMixedAmountDebug etotal -- we only check the sum of the totals   in
Hledger/Reports/PostingsReport.hs view
@@ -71,7 +71,7 @@     where       (reportspan, colspans) = reportSpanBothDates j rspec       whichdate   = whichDate ropts-      mdepth      = queryDepth $ _rsQuery rspec+      depthSpec   = queryDepth $ _rsQuery rspec       multiperiod = interval_ /= NoInterval        -- postings to be included in the report, and similarly-matched postings before the report start date@@ -82,7 +82,7 @@         | multiperiod = [(p', Just period') | (p', period') <- summariseps reportps]         | otherwise   = [(p', Nothing) | p' <- reportps]         where-          summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans+          summariseps = summarisePostingsByInterval whichdate (dsFlatDepth depthSpec) showempty colspans           showempty = empty_ || average_        sortedps = if sortspec_ /= defsortspec then sortPostings ropts sortspec_ displayps else displayps@@ -90,7 +90,7 @@       -- Posting report items ready for display.       items =         dbg4 "postingsReport items" $-        postingsReportItems postings (nullposting,Nothing) whichdate mdepth startbal runningcalc startnum+        postingsReportItems postings (nullposting,Nothing) whichdate depthSpec startbal runningcalc startnum         where           -- In historical mode we'll need a starting balance, which we           -- may be converting to value per hledger_options.m4.md "Effect@@ -180,7 +180,7 @@  -- | Generate postings report line items from a list of postings or (with -- non-Nothing periods attached) summary postings.-postingsReportItems :: [(Posting,Maybe Period)] -> (Posting,Maybe Period) -> WhichDate -> Maybe Int -> MixedAmount -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) -> Int -> [PostingsReportItem]+postingsReportItems :: [(Posting,Maybe Period)] -> (Posting,Maybe Period) -> WhichDate -> DepthSpec -> MixedAmount -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) -> Int -> [PostingsReportItem] postingsReportItems [] _ _ _ _ _ _ = [] postingsReportItems ((p,mperiod):ps) (pprev,mperiodprev) wd d b runningcalcfn itemnum =     i:(postingsReportItems ps (p,mperiod) wd d b' runningcalcfn (itemnum+1))@@ -237,7 +237,7 @@     postingdate = if wd == PrimaryDate then postingDate else postingDate2     b' = maybe (maybe nulldate postingdate $ headMay ps) fromEFDay b     summaryp = nullposting{pdate=Just b'}-    clippedanames = nub $ map (clipAccountName mdepth) anames+    clippedanames = nub $ map (clipAccountName (DepthSpec mdepth [])) anames     summaryps | mdepth == Just 0 = [summaryp{paccount="...",pamount=sumPostings ps}]               | otherwise        = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]     summarypes = map (, dateSpanAsPeriod spn) $ (if showempty then id else filter (not . mixedAmountLooksZero . pamount)) summaryps@@ -248,9 +248,6 @@       where         bal = if isclipped a then aibalance else aebalance         isclipped a' = maybe False (accountNameLevel a' >=) mdepth--negatePostingAmount :: Posting -> Posting-negatePostingAmount = postingTransformAmount negate   -- tests
Hledger/Reports/ReportOptions.hs view
@@ -75,6 +75,7 @@ import Data.Either (fromRight) import Data.Either.Extra (eitherToMaybe) import Data.Functor.Identity (Identity(..))+import Data.List (partition) import Data.List.Extra (find, isPrefixOf, nubSort, stripPrefix) import Data.Maybe (fromMaybe, isJust, isNothing) import qualified Data.Text as T@@ -134,12 +135,13 @@     ,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+    ,depth_            :: DepthSpec     ,date2_            :: Bool     ,empty_            :: Bool     ,no_elide_         :: Bool     ,real_             :: Bool     ,format_           :: StringFormat+    ,balance_base_url_ :: Maybe T.Text     ,pretty_           :: Bool     ,querystring_      :: [T.Text]     --@@ -193,12 +195,13 @@     , conversionop_     = Nothing     , value_            = Nothing     , infer_prices_     = False-    , depth_            = Nothing+    , depth_            = DepthSpec Nothing []     , date2_            = False     , empty_            = False     , no_elide_         = False     , real_             = False     , format_           = def+    , balance_base_url_ = Nothing     , pretty_           = False     , querystring_      = []     , average_          = False@@ -224,14 +227,14 @@     , layout_           = LayoutWide Nothing     } --- | Generate a ReportOpts from raw command-line input, given a day.+-- | Generate a ReportOpts from raw command-line input, given a day and whether to use ANSI colour/styles in standard output. -- This will fail with a usage error if it is passed -- - an invalid --format argument, -- - an invalid --value argument, -- - if --valuechange is called with a valuation type other than -V/--value=end. -- - an invalid --pretty argument,-rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts-rawOptsToReportOpts d rawopts =+rawOptsToReportOpts :: Day -> Bool -> RawOpts -> ReportOpts+rawOptsToReportOpts d usecoloronstdout rawopts =      let formatstring = T.pack <$> maybestringopt "format" rawopts         querystring  = map T.pack $ listofstringopt "args" rawopts  -- doesn't handle an arg like "" right@@ -249,12 +252,13 @@           ,conversionop_     = conversionOpFromRawOpts rawopts           ,value_            = valuationTypeFromRawOpts rawopts           ,infer_prices_     = boolopt "infer-market-prices" rawopts-          ,depth_            = maybeposintopt "depth" rawopts+          ,depth_            = depthFromRawOpts rawopts           ,date2_            = boolopt "date2" rawopts           ,empty_            = boolopt "empty" rawopts           ,no_elide_         = boolopt "no-elide" rawopts           ,real_             = boolopt "real" rawopts           ,format_           = format+          ,balance_base_url_ = T.pack <$> maybestringopt "base-url" rawopts           ,querystring_      = querystring           ,average_          = boolopt "average" rawopts           ,related_          = boolopt "related" rawopts@@ -274,7 +278,7 @@           ,percent_          = boolopt "percent" rawopts           ,invert_           = boolopt "invert" rawopts           ,pretty_           = pretty-          ,color_            = useColorOnStdout -- a lower-level helper+          ,color_            = usecoloronstdout           ,transpose_        = boolopt "transpose" rawopts           ,layout_           = layoutopt rawopts           }@@ -343,7 +347,7 @@     Just "never"  -> Just False     Just "no"     -> Just False     Just "n"      -> Just False-    Just _        -> usageError "--pretty's argument should be \"yes\" or \"no\" (or y, n, always, never)"+    Just _        -> usageError "this argument should be one of y, yes, n, no"     _             -> Nothing  balanceAccumulationOverride :: RawOpts -> Maybe BalanceAccumulation@@ -538,6 +542,21 @@       | n == "value", takeWhile (/=',') v `elem` ["cost", "c"] = Just ToCost  -- keep supporting --value=cost for now       | otherwise                                   = Nothing +-- | Parse the depth arguments. This can be either a flat depth that applies to+-- all accounts, or a regular expression and depth, which only matches certain+-- accounts. If an account name is matched by a regular expression, then the+-- smallest depth is used. Otherwise, if no regular expressions match, then the+-- flat depth is used. If more than one flat depth is supplied, use only the+-- last one.+depthFromRawOpts :: RawOpts -> DepthSpec+depthFromRawOpts rawopts = lastDef mempty flats <> mconcat regexps+  where+    (flats, regexps) = partition (\(DepthSpec f rs) -> isJust f && null rs) depthSpecs+    depthSpecs = case mapM (parseDepthSpec . T.pack) depths of+      Right d -> d+      Left err -> usageError $ "Unable to parse depth specification: " ++ err+    depths = listofstringopt "depth" rawopts+ -- | Select the Transaction date accessor based on --date2. transactionDateFn :: ReportOpts -> (Transaction -> Day) transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate@@ -664,11 +683,13 @@ queryFromFlags ReportOpts{..} = simplifyQuery $ And flagsq   where     flagsq = consIf   Real  real_-           . consJust Depth depth_-           $   [ (if date2_ then Date2 else Date) $ periodAsDateSpan period_+           . consJust Depth flatDepth+           $ map (uncurry DepthAcct) regexpDepths+           ++  [ (if date2_ then Date2 else Date) $ periodAsDateSpan period_                , Or $ map StatusQ statuses_                ]     consIf f b = if b then (f True:) else id+    DepthSpec flatDepth regexpDepths = depth_     consJust f = maybe id ((:) . f)  -- Methods/types needed for --sort argument@@ -888,7 +909,7 @@     statuses = reportOpts.statusesNoUpdate     {-# INLINE statuses #-} -    depth :: ReportableLens' a (Maybe Int)+    depth :: ReportableLens' a DepthSpec     depth = reportOpts.depthNoUpdate     {-# INLINE depth #-} @@ -938,5 +959,5 @@  -- | Generate a ReportSpec from RawOpts and a provided day, or return an error -- string if there are regular expression errors.-rawOptsToReportSpec :: Day -> RawOpts -> Either String ReportSpec-rawOptsToReportSpec day = reportOptsToSpec day . rawOptsToReportOpts day+rawOptsToReportSpec :: Day -> Bool -> RawOpts -> Either String ReportSpec+rawOptsToReportSpec day coloronstdout = reportOptsToSpec day . rawOptsToReportOpts day coloronstdout
Hledger/Reports/ReportTypes.hs view
@@ -31,7 +31,7 @@ , prrShowDebug , prrFullName , prrDisplayName-, prrDepth+, prrIndent , prrAdd ) where @@ -211,36 +211,39 @@   }  --- | A full name, display name, and depth for an account.+-- | The number of indentation steps with which to display a report item.+-- 0 means no indentation. 1 means one indent step, which is normally rendered+-- as two spaces in text output, or two no-break spaces in csv/html output.+type NumberOfIndents = Int++-- | A full name, display name, and indent level for an account. data DisplayName = DisplayName-    { displayFull :: AccountName-    , displayName :: AccountName-    , displayDepth :: Int+    { displayFull   :: AccountName+    , displayName   :: AccountName+    , displayIndent :: NumberOfIndents     } deriving (Show, Eq, Ord)  instance ToJSON DisplayName where     toJSON = toJSON . displayFull     toEncoding = toEncoding . displayFull --- | Construct a flat display name, where the full name is also displayed at--- depth 1+-- | Construct a display name for a list report, where full names are shown unindented. flatDisplayName :: AccountName -> DisplayName-flatDisplayName a = DisplayName a a 1+flatDisplayName a = DisplayName a a 0 --- | Construct a tree display name, where only the leaf is displayed at its--- given depth+-- | Construct a display name for a tree report, where leaf names (possibly prefixed by+-- boring parents) are shown indented). treeDisplayName :: AccountName -> DisplayName treeDisplayName a = DisplayName a (accountLeafName a) (accountNameLevel a) --- | Get the full, canonical, name of a PeriodicReportRow tagged by a--- DisplayName.+-- | Get the full canonical account name from a PeriodicReportRow containing a DisplayName. prrFullName :: PeriodicReportRow DisplayName a -> AccountName prrFullName = displayFull . prrName --- | Get the display name of a PeriodicReportRow tagged by a DisplayName.+-- | Get the account display name from a PeriodicReportRow containing a DisplayName. prrDisplayName :: PeriodicReportRow DisplayName a -> AccountName prrDisplayName = displayName . prrName --- | Get the display depth of a PeriodicReportRow tagged by a DisplayName.-prrDepth :: PeriodicReportRow DisplayName a -> Int-prrDepth = displayDepth . prrName+-- | Get the indent level from a PeriodicReportRow containing a DisplayName.+prrIndent :: PeriodicReportRow DisplayName a -> Int+prrIndent = displayIndent . prrName
Hledger/Utils.hs view
@@ -3,6 +3,8 @@ These are the bottom of hledger's module graph. -} +{-# LANGUAGE CPP #-}+ module Hledger.Utils (    -- * Functions@@ -69,7 +71,10 @@  import Data.Char (toLower) import Data.List (intersperse)-import Data.List.Extra (chunksOf, foldl', foldl1', uncons, unsnoc)+import Data.List.Extra (chunksOf, foldl1', uncons, unsnoc)+#if !MIN_VERSION_base(4,20,0)+import Data.List (foldl')+#endif import qualified Data.Set as Set import qualified Data.Text as T (pack, unpack) import Data.Tree (foldTree, Tree (Node, subForest))
Hledger/Utils/Debug.hs view
@@ -224,11 +224,12 @@                    _                                   -> 0  -- | Whether ghc-debug support is included in this build, and if so, how it will behave.--- When hledger is built with the ghcdebug cabal build flag (normally disabled),+-- When hledger is built with the @ghcdebug@ cabal flag (off by default, because of extra deps), -- it can listen (on unix ?) for connections from ghc-debug clients like ghc-debug-brick, -- for pausing/resuming the program and inspecting memory usage and profile information. ----- This is enabled by running hledger with a negative --debug level, with three different modes:+-- With a ghc-debug-supporting build, ghc-debug can be enabled by running hledger with+-- a negative --debug level. There are three different modes: -- --debug=-1 - run normally (can be paused/resumed by a ghc-debug client), -- --debug=-2 - pause and await client commands at program start (not useful currently), -- --debug=-3 - pause and await client commands at program end.@@ -254,6 +255,7 @@ -- See GhcDebugMode. ghcDebugMode :: GhcDebugMode ghcDebugMode =+#ifdef GHCDEBUG   case debugLevel of     _ | not ghcDebugSupportedInLib -> GDNotSupported     (-1) -> GDNoPause@@ -261,6 +263,9 @@     (-3) -> GDPauseAtEnd     _    -> GDDisabled     -- keep synced with GhcDebugMode+#else+  GDNotSupported+#endif  -- | When ghc-debug support has been built into the program and enabled at runtime with --debug=-N, -- this calls ghc-debug's withGhcDebug; otherwise it's a no-op.
Hledger/Utils/IO.hs view
@@ -1,15 +1,12 @@ {- | -Helpers for pretty-printing haskell values, reading command line arguments,-working with ANSI colours, files, and time.-Uses unsafePerformIO.--Limitations:-When running in GHCI, this module must be reloaded to see environmental changes.-The colour scheme may be somewhat hard-coded.-+General and hledger-specific input/output-related helpers for+pretty-printing haskell values, error reporting, time, files, command line parsing,+terminals, pager output, ANSI colour/styles, etc. -} -{-# LANGUAGE CPP, LambdaCase, PackageImports #-}+{-# LANGUAGE LambdaCase          #-}+{-# LANGUAGE PackageImports      #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Hledger.Utils.IO ( @@ -19,32 +16,54 @@   pprint,   pprint', -  -- * Viewing with pager-  pager,-  setupPager,+  -- * Errors+  error',+  usageError,+  warn, +  -- * Time+  getCurrentLocalTime,+  getCurrentZonedTime,++  -- * Files+  embedFileRelative,+  expandHomePath,+  expandPath,+  expandGlob,+  sortByModTime,+  readFileOrStdinPortably,+  readFileStrictly,+  readFilePortably,+  readHandlePortably,+  -- hereFileRelative,++  -- * Command line parsing+  progArgs,+  getOpt,+  parseYN,+  parseYNA,+  YNA(..),+  -- hasOutputFile,+  -- outputFileOption,+   -- * Terminal size   getTerminalHeightWidth,   getTerminalHeight,   getTerminalWidth, -  -- * Command line arguments-  progArgs,-  outputFileOption,-  hasOutputFile,+  -- * Pager output+  setupPager,+  runPager, -  -- * ANSI color+  -- * ANSI colour/styles++  -- ** hledger-specific+   colorOption,   useColorOnStdout,   useColorOnStderr,-  -- XXX needed for using color/bgColor/colorB/bgColorB, but clashing with UIUtils:-  -- Color(..),-  -- ColorIntensity(..),-  color,-  bgColor,-  colorB,-  bgColorB,-  --+  useColorOnStdoutUnsafe,+  useColorOnStderrUnsafe,   bold',   faint',   black',@@ -64,81 +83,74 @@   brightCyan',   brightWhite',   rgb',++  -- ** Generic++  color,+  bgColor,+  colorB,+  bgColorB,+  -- XXX Types used with color/bgColor/colorB/bgColorB,+  -- not re-exported because clashing with UIUtils:+  -- Color(..),+  -- ColorIntensity(..),+   terminalIsLight,   terminalLightness,   terminalFgColor,   terminalBgColor, -  -- * Errors-  error',-  usageError,--  -- * Files-  embedFileRelative,-  expandHomePath,-  expandPath,-  expandGlob,-  sortByModTime,-  readFileOrStdinPortably,-  readFileStrictly,-  readFilePortably,-  readHandlePortably,-  -- hereFileRelative,--  -- * Time-  getCurrentLocalTime,-  getCurrentZonedTime,-   ) where -import qualified Control.Exception as C (evaluate)-import           Control.Monad (when, forM)+import           Control.Concurrent (forkIO)+import           Control.Exception (catch, evaluate, throwIO)+import           Control.Monad (when, forM, guard, void)+import           Data.Char (toLower) import           Data.Colour.RGBSpace (RGB(RGB)) import           Data.Colour.RGBSpace.HSL (lightness)+import           Data.Colour.SRGB (sRGB) import           Data.FileEmbed (makeRelativeToProject, embedStringFile) import           Data.Functor ((<&>)) import           Data.List hiding (uncons)-import           Data.Maybe (isJust)+import           Data.Maybe (isJust, catMaybes) import           Data.Ord (comparing, Down (Down)) import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB import           Data.Time.Clock (getCurrentTime)-import           Data.Time.LocalTime-  (LocalTime, ZonedTime, getCurrentTimeZone, utcToLocalTime, utcToZonedTime)-import           Data.Word (Word8, Word16)+import           Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone, utcToLocalTime, utcToZonedTime)+import           Data.Word (Word16)+import           Debug.Trace (trace)+import           Foreign.C.Error (Errno(..), ePIPE)+import           GHC.IO.Exception (IOException(..), IOErrorType (ResourceVanished)) import           Language.Haskell.TH.Syntax (Q, Exp)-import           String.ANSI-import           System.Console.ANSI (Color(..),ColorIntensity(..),-  ConsoleLayer(..), SGR(..), hSupportsANSIColor, setSGRCode, getLayerColor)+import           Safe (headMay, maximumDef)+import           System.Console.ANSI (Color(..),ColorIntensity(..), ConsoleLayer(..), SGR(..), hSupportsANSIColor, setSGRCode, getLayerColor, ConsoleIntensity (..)) import           System.Console.Terminal.Size (Window (Window), size)-import           System.Directory (getHomeDirectory, getModificationTime)+import           System.Directory (getHomeDirectory, getModificationTime, findExecutable) import           System.Environment (getArgs, lookupEnv, setEnv) import           System.FilePath (isRelative, (</>)) import "Glob"    System.FilePath.Glob (glob)-import           System.IO-  (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode,-   openFile, stdin, stdout, stderr, universalNewlineMode, utf8_bom, hIsTerminalDevice)+import           System.Info (os)+import           System.IO (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode, openFile, stdin, stdout, stderr, universalNewlineMode, utf8_bom, hIsTerminalDevice, hPutStr, hClose) import           System.IO.Unsafe (unsafePerformIO)-#ifndef mingw32_HOST_OS-import           System.Pager (printOrPage)-#endif-import           Text.Pretty.Simple-  (CheckColorTty(..), OutputOptions(..),-  defaultOutputOptionsDarkBg, defaultOutputOptionsNoColor, pShowOpt, pPrintOpt)+import           System.Process (CreateProcess(..), StdStream(CreatePipe), shell, waitForProcess, withCreateProcess)+import           Text.Pretty.Simple (CheckColorTty(..), OutputOptions(..), defaultOutputOptionsDarkBg, defaultOutputOptionsNoColor, pShowOpt, pPrintOpt)  import Hledger.Utils.Text (WideBuilder(WideBuilder))  --- Pretty showing/printing with pretty-simple +-- Pretty showing/printing+-- using pretty-simple+ -- https://hackage.haskell.org/package/pretty-simple/docs/Text-Pretty-Simple.html#t:OutputOptions  -- | pretty-simple options with colour enabled if allowed. prettyopts =-  (if useColorOnStderr then defaultOutputOptionsDarkBg else defaultOutputOptionsNoColor)+  (if useColorOnStderrUnsafe then defaultOutputOptionsDarkBg else defaultOutputOptionsNoColor)     { outputOptionsIndentAmount = 2     -- , outputOptionsCompact      = True  -- fills lines, but does not respect page width (https://github.com/cdepillabout/pretty-simple/issues/126)     -- , outputOptionsPageWidth    = fromMaybe 80 $ unsafePerformIO getTerminalWidth@@ -151,7 +163,7 @@     }  -- | Pretty show. An easier alias for pretty-simple's pShow.--- This will probably show in colour if useColorOnStderr is true.+-- This will probably show in colour if useColorOnStderrUnsafe is true. pshow :: Show a => a -> String pshow = TL.unpack . pShowOpt prettyopts @@ -160,9 +172,9 @@ pshow' = TL.unpack . pShowOpt prettyoptsNoColor  -- | Pretty print a showable value. An easier alias for pretty-simple's pPrint.--- This will print in colour if useColorOnStderr is true.+-- This will print in colour if useColorOnStderrUnsafe is true. pprint :: Show a => a -> IO ()-pprint = pPrintOpt (if useColorOnStderr then CheckColorTty else NoCheckColorTty) prettyopts+pprint = pPrintOpt (if useColorOnStderrUnsafe then CheckColorTty else NoCheckColorTty) prettyopts  -- | Monochrome version of pprint. This will never print in colour. pprint' :: Show a => a -> IO ()@@ -170,23 +182,191 @@  -- "Avoid using pshow, pprint, dbg* in the code below to prevent infinite loops." (?) --- | Display the given text on the terminal, using the user's $PAGER if the text is taller --- than the current terminal and stdout is interactive and TERM is not "dumb"--- (except on Windows, where a pager will not be used).--- If the text contains ANSI codes, because hledger thinks the current terminal--- supports those, the pager should be configured to display those, otherwise--- users will see junk on screen (#2015).--- We call "setLessR" at hledger startup to make that less likely.-pager :: String -> IO ()-#ifdef mingw32_HOST_OS-pager = putStrLn-#else-printOrPage' s = do  -- an extra check for Emacs users:-  dumbterm <- (== Just "dumb") <$> lookupEnv "TERM"-  if dumbterm then putStrLn s else printOrPage (T.pack s)-pager = printOrPage'-#endif ++-- Errors++-- | Simpler alias for errorWithoutStackTrace+error' :: String -> a+error' = errorWithoutStackTrace . ("Error: " <>)++-- | A version of errorWithoutStackTrace that adds a usage hint.+usageError :: String -> a+usageError = error' . (++ " (use -h to see usage)")++-- | Show a warning message on stderr before returning the given value.+-- Use this when you want to show the user a message on stderr, without stopping the program.+-- Currently we do this very sparingly in hledger; we prefer to either quietly work,+-- or loudly raise an error. Variable output can make scripting harder.+warn :: String -> a -> a+warn msg = trace ("Warning: " <> msg)++++-- Time++getCurrentLocalTime :: IO LocalTime+getCurrentLocalTime = do+  t <- getCurrentTime+  tz <- getCurrentTimeZone+  return $ utcToLocalTime tz t++getCurrentZonedTime :: IO ZonedTime+getCurrentZonedTime = do+  t <- getCurrentTime+  tz <- getCurrentTimeZone+  return $ utcToZonedTime tz t++++-- Files++-- | Expand a tilde (representing home directory) at the start of a file path.+-- ~username is not supported. Can raise an error.+expandHomePath :: FilePath -> IO FilePath+expandHomePath = \case+    ('~':'/':p)  -> (</> p) <$> getHomeDirectory+    ('~':'\\':p) -> (</> p) <$> getHomeDirectory+    ('~':_)      -> ioError $ userError "~USERNAME in paths is not supported"+    p            -> return p++-- | Given a current directory, convert a possibly relative, possibly tilde-containing+-- file path to an absolute one.+-- ~username is not supported. Leaves "-" unchanged. Can raise an error.+expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers+expandPath _ "-" = return "-"+expandPath curdir p = (if isRelative p then (curdir </>) else id) <$> expandHomePath p  -- PARTIAL:++-- | Like expandPath, but treats the expanded path as a glob, and returns+-- zero or more matched absolute file paths, alphabetically sorted.+-- Can raise an error.+expandGlob :: FilePath -> FilePath -> IO [FilePath]+expandGlob curdir p = expandPath curdir p >>= glob <&> sort  -- PARTIAL:++-- | Given a list of existing file paths, sort them by modification time, most recent first.+sortByModTime :: [FilePath] -> IO [FilePath]+sortByModTime fs = do+  ftimes <- forM fs $ \f -> do {t <- getModificationTime f; return (t,f)}+  return $ map snd $ sortBy (comparing Data.Ord.Down) ftimes++-- | Like readFilePortably, but read all of the file before proceeding.+readFileStrictly :: FilePath -> IO T.Text+readFileStrictly f = readFilePortably f >>= \t -> evaluate (T.length t) >> return t++-- | Read text from a file,+-- converting any \r\n line endings to \n,,+-- using the system locale's text encoding,+-- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8.+readFilePortably :: FilePath -> IO T.Text+readFilePortably f =  openFile f ReadMode >>= readHandlePortably++-- | Like readFilePortably, but read from standard input if the path is "-".+readFileOrStdinPortably :: String -> IO T.Text+readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably+  where+    openFileOrStdin :: String -> IOMode -> IO Handle+    openFileOrStdin "-" _ = return stdin+    openFileOrStdin f' m   = openFile f' m++readHandlePortably :: Handle -> IO T.Text+readHandlePortably h = do+  hSetNewlineMode h universalNewlineMode+  menc <- hGetEncoding h+  when (fmap show menc == Just "UTF-8") $  -- XXX no Eq instance, rely on Show+    hSetEncoding h utf8_bom+  T.hGetContents h++-- | Like embedFile, but takes a path relative to the package directory.+embedFileRelative :: FilePath -> Q Exp+embedFileRelative f = makeRelativeToProject f >>= embedStringFile++-- -- | Like hereFile, but takes a path relative to the package directory.+-- -- Similar to embedFileRelative ?+-- hereFileRelative :: FilePath -> Q Exp+-- hereFileRelative f = makeRelativeToProject f >>= hereFileExp+--   where+--     QuasiQuoter{quoteExp=hereFileExp} = hereFile++++-- Command line parsing++-- | The program's command line arguments.+-- Uses unsafePerformIO; tends to stick in GHCI until reloaded,+-- and may or may not detect args provided by a hledger config file.+{-# NOINLINE progArgs #-}+progArgs :: [String]+progArgs = unsafePerformIO getArgs+-- XX currently this affects:+--  the enabling of orderdates and assertions checks in journalFinalise+--  a few cases involving --color (see useColorOnStdoutUnsafe)+--  --debug++-- | Given one or more long or short option names, read the rightmost value of this option from the command line arguments.+-- If the value is missing raise an error.+-- Concatenated short flags (-a -b written as -ab) are not supported.+getOpt :: [String] -> IO (Maybe String)+getOpt names = do+  rargs <- reverse . splitFlagsAndVals <$> getArgs+  let flags = map toFlag names+  return $+    case break ((`elem` flags)) rargs of+      (_,[])        -> Nothing+      ([],flag:_)   -> error' $ flag <> " requires a value"+      (argsafter,_) -> Just $ last argsafter++-- | Given a list of command line arguments, split any of the form --flag=VAL or -fVAL into two list items.+-- Concatenated short flags (-a -b written as -ab) are not supported.+splitFlagsAndVals :: [String] -> [String]+splitFlagsAndVals = concatMap $+  \case+    a@('-':'-':_) | '=' `elem` a -> let (x,y) = break (=='=') a in [x, drop 1 y]+    a@('-':f:_:_) | not $ f=='-' -> [take 2 a, drop 2 a]+    a -> [a]++-- | Convert a short or long flag name to a flag with leading hyphen(s).+toFlag [c] = ['-',c]+toFlag s   = '-':'-':s++-- | Parse y/yes/always or n/no/never to true or false, or with any other value raise an error.+parseYN :: String -> Bool+parseYN s+  | l `elem` ["y","yes","always"] = True+  | l `elem` ["n","no","never"]   = False+  | otherwise = error' $ "value should be one of " <> (intercalate ", " ["y","yes","n","no"])+  where l = map toLower s++data YNA = Yes | No | Auto deriving (Eq,Show)++-- | Parse y/yes/always or n/no/never or a/auto to a YNA choice, or with any other value raise an error.+parseYNA :: String -> YNA+parseYNA s+  | l `elem` ["y","yes","always"] = Yes+  | l `elem` ["n","no","never"]   = No+  | l `elem` ["a","auto"]         = Auto+  | otherwise = error' $ "value should be one of " <> (intercalate ", " ["y","yes","n","no","a","auto"])+  where l = map toLower s++-- | Is there a --output-file or -o option in the command line arguments ?+-- Uses getOpt; sticky in GHCI until reloaded, may not always be affected by a hledger config file, etc.+hasOutputFile :: IO Bool+hasOutputFile = do+  mv <- getOpt ["output-file","o"]+  return $+    case mv of+      Nothing  -> False+      Just "-" -> False+      _        -> True++-- -- | Get the -o/--output-file option's value, if any, from the command line arguments.+-- -- Uses getOpt; sticky in GHCI until reloaded, may not always be affected by a hledger config file, etc.+-- outputFileOption :: IO (Maybe String)+-- outputFileOption = getOpt ["output-file","o"]++++-- Terminal size+ -- | An alternative to ansi-terminal's getTerminalSize, based on -- the more robust-looking terminal-size package. -- Tries to get stdout's terminal's current height and width.@@ -200,170 +380,257 @@ getTerminalWidth :: IO (Maybe Int) getTerminalWidth  = fmap snd <$> getTerminalHeightWidth --- | Make sure our $LESS and $MORE environment variables contain R,--- to help ensure the common pager `less` will show our ANSI output properly.--- less uses $LESS by default, and $MORE when it is invoked as `more`.--- What the original `more` program does, I'm not sure.--- If $PAGER is configured to something else, this probably will have no effect.+++-- Pager output+-- somewhat hledger-specific++-- Configure some preferred options for the `less` pager,+-- by modifying the LESS environment variable in this program's environment.+-- If you are using some other pager, this will have no effect.+-- By default, this sets the following options, appending them to LESS's current value:+--+--   --chop-long-lines+--   --hilite-unread+--   --ignore-case+--   --mouse+--   --no-init+--   --quit-at-eof+--   --quit-if-one-screen+--   --RAW-CONTROL-CHARS+--   --shift=8+--   --squeeze-blank-lines+--   --use-backslash+--   --use-color+--+-- You can choose different options by setting the HLEDGER_LESS variable;+-- if set, its value will be used instead of LESS.+-- Or you can force hledger to use your exact LESS settings,+-- by setting HLEDGER_LESS equal to LESS.+-- setupPager :: IO () setupPager = do   let-    addR var = do-      mv <- lookupEnv var-      setEnv var $ case mv of-        Nothing -> "R"-        Just v  -> ('R':v)-  addR "LESS"-  addR "MORE"+    -- keep synced with doc above+    deflessopts = unwords [+       "--chop-long-lines"+      ,"--hilite-unread"+      ,"--ignore-case"+      ,"--mouse"+      ,"--no-init"+      ,"--quit-at-eof"+      ,"--quit-if-one-screen"+      ,"--RAW-CONTROL-CHARS"+      ,"--shift=8"+      ,"--squeeze-blank-lines"+      ,"--use-backslash"+      ,"--use-color"+      ]+  mhledgerless <- lookupEnv "HLEDGER_LESS"+  mless        <- lookupEnv "LESS"+  setEnv "LESS" $+    case (mhledgerless, mless) of+      (Just hledgerless, _) -> hledgerless+      (_, Just less)        -> unwords [less, deflessopts]+      _                     -> deflessopts --- Command line arguments+-- | Display the given text on the terminal, trying to use a pager ($PAGER, less, or more)+-- when appropriate, otherwise printing to standard output. Uses maybePagerFor.+--+-- hledger's output may contain ANSI style/color codes+-- (if the terminal supports them and they are not disabled by --color=no or NO_COLOR),+-- so the pager should be configured to handle these.+-- setupPager tries to configure that automatically when using the `less` pager.+--+runPager :: String -> IO ()+runPager s = do+  mpager <- maybePagerFor s+  case mpager of+    Nothing -> putStr s+    Just pager -> do+      withCreateProcess (shell pager){std_in=CreatePipe} $+        \mhin _ _ p -> do+          -- Pipe in the text on stdin.+          case mhin of+            Nothing  -> return ()  -- shouldn't happen+            Just hin -> void $ forkIO $   -- Write from another thread to avoid deadlock ? Maybe unneeded, but just in case.+              (hPutStr hin s >> hClose hin)  -- Be sure to close the pipe so the pager knows we're done.+                -- If the pager quits early, we'll receive an EPIPE error; hide that.+                `catch` \(e::IOException) -> case e of+                  IOError{ioe_type=ResourceVanished, ioe_errno=Just ioe, ioe_handle=Just hdl} | Errno ioe==ePIPE, hdl==hin+                    -> return ()+                  _ -> throwIO e+          void $ waitForProcess p --- | The command line arguments that were used at program startup.--- Uses unsafePerformIO.-{-# NOINLINE progArgs #-}-progArgs :: [String]-progArgs = unsafePerformIO getArgs+-- | Should a pager be used for displaying the given text on stdout, and if so, which one ?+-- Uses a pager if findPager finds one and none of the following conditions are true:+-- We're running in a native MS Windows environment like cmd or powershell.+-- Or the --pager=n|no option is in effect.+-- Or the -o/--output-file option is in effect.+-- Or INSIDE_EMACS is set, to something other than "vterm".+-- Or the terminal's current height and width can't be detected.+-- Or the output text is less wide and less tall than the terminal.+maybePagerFor :: String -> IO (Maybe String)+maybePagerFor output = do+  let+    ls = lines output+    oh = length ls+    ow = maximumDef 0 $ map length ls+    windows = os == "mingw32"+  pagerno    <- maybe False (not.parseYN) <$> getOpt ["pager"]+  outputfile <- hasOutputFile+  emacsterm  <- lookupEnv "INSIDE_EMACS" <&> (`notElem` [Nothing, Just "vterm"])+  mhw        <- getTerminalHeightWidth+  mpager     <- findPager+  return $ do+    guard $ not $ windows || pagerno || outputfile || emacsterm+    (th,tw) <- mhw+    guard $ oh > th || ow > tw+    mpager --- | Read the value of the -o/--output-file command line option provided at program startup,--- if any, using unsafePerformIO.-outputFileOption :: Maybe String-outputFileOption =-  -- keep synced with output-file flag definition in hledger:CliOptions.-  let args = progArgs in-  case dropWhile (not . ("-o" `isPrefixOf`)) args of-    -- -oARG-    ('-':'o':v@(_:_)):_ -> Just v-    -- -o ARG-    "-o":v:_ -> Just v-    _ ->-      case dropWhile (/="--output-file") args of-        -- --output-file ARG-        "--output-file":v:_ -> Just v-        _ ->-          case take 1 $ filter ("--output-file=" `isPrefixOf`) args of-            -- --output=file=ARG-            ['-':'-':'o':'u':'t':'p':'u':'t':'-':'f':'i':'l':'e':'=':v] -> Just v-            _ -> Nothing+-- | Try to find a pager executable robustly, safely handling various error conditions+-- like an unset PATH var or the specified pager not being found as an executable.+-- The pager can be specified by a path or program name in the PAGER environment variable.+-- If that is unset or has a problem, "less" is tried, then "more".+-- If successful, the pager's path or program name is returned.+findPager :: IO (Maybe String)  -- XXX probably a ByteString in fact ?+findPager = do+  mpagervar <- lookupEnv "PAGER"+  let pagers = [p | Just p <- [mpagervar]] <> ["less", "more"]+  headMay . catMaybes <$> mapM findExecutable pagers --- | Check whether the -o/--output-file option has been used at program startup--- with an argument other than "-", using unsafePerformIO.-hasOutputFile :: Bool-hasOutputFile = outputFileOption `notElem` [Nothing, Just "-"]--- XXX shouldn't we check that stdout is interactive. instead ? --- ANSI colour -ifAnsi f = if useColorOnStdout then f else id+-- ANSI colour/styles+-- Some of these use unsafePerformIO to read info. --- | Versions of some of text-ansi's string colors/styles which are more careful--- to not print junk onscreen. These use our useColorOnStdout.+-- hledger-specific:++-- | Get the value of the rightmost --color or --colour option from the program's command line arguments.+colorOption :: IO YNA+colorOption = maybe Auto parseYNA <$> getOpt ["color","colour"]++-- | Should ANSI color and styles be used with this output handle ?+-- Considers colorOption, the NO_COLOR environment variable, and hSupportsANSIColor.+useColorOnHandle :: Handle -> IO Bool+useColorOnHandle h = do+  no_color       <- isJust <$> lookupEnv "NO_COLOR"+  supports_color <- hSupportsANSIColor h+  yna            <- colorOption+  return $ yna==Yes || (yna==Auto && not no_color && supports_color)++-- | Should ANSI color and styles be used for standard output ?+-- Considers useColorOnHandle stdout and hasOutputFile.+useColorOnStdout :: IO Bool+useColorOnStdout = do+  nooutputfile <- not <$> hasOutputFile+  usecolor <- useColorOnHandle stdout+  return $ nooutputfile && usecolor++-- | Should ANSI color and styles be used for standard error output ?+-- Considers useColorOnHandle stderr; is not affected by an --output-file option.+useColorOnStderr :: IO Bool+useColorOnStderr = useColorOnHandle stderr++-- | Like useColorOnStdout, but using unsafePerformIO. Useful eg for low-level debug code.+-- Sticky in GHCI until reloaded, may not always be affected by --color in a hledger config file, etc.+useColorOnStdoutUnsafe :: Bool+useColorOnStdoutUnsafe = unsafePerformIO useColorOnStdout++-- | Like useColorOnStdoutUnsafe, but for stderr.+useColorOnStderrUnsafe :: Bool+useColorOnStderrUnsafe = unsafePerformIO useColorOnStderr++-- | Detect whether ANSI should be used on stdout using useColorOnStdoutUnsafe,+-- and if so prepend and append the given SGR codes to a string.+-- Currently used in a few places (the commands list, the demo command, the recentassertions error message);+-- see useColorOnStdoutUnsafe's limitations.+ansiWrapUnsafe :: SGRString -> SGRString -> String -> String+ansiWrapUnsafe pre post s = if useColorOnStdoutUnsafe then pre<>s<>post else s++type SGRString = String++sgrbold          = setSGRCode [SetConsoleIntensity BoldIntensity]+sgrfaint         = setSGRCode [SetConsoleIntensity FaintIntensity]+sgrnormal        = setSGRCode [SetConsoleIntensity NormalIntensity]+sgrresetfg       = setSGRCode [SetDefaultColor Foreground]+sgrblack         = setSGRCode [SetColor Foreground Dull Black]+sgrred           = setSGRCode [SetColor Foreground Dull Red]+sgrgreen         = setSGRCode [SetColor Foreground Dull Green]+sgryellow        = setSGRCode [SetColor Foreground Dull Yellow]+sgrblue          = setSGRCode [SetColor Foreground Dull Blue]+sgrmagenta       = setSGRCode [SetColor Foreground Dull Magenta]+sgrcyan          = setSGRCode [SetColor Foreground Dull Cyan]+sgrwhite         = setSGRCode [SetColor Foreground Dull White]+sgrbrightblack   = setSGRCode [SetColor Foreground Vivid Black]+sgrbrightred     = setSGRCode [SetColor Foreground Vivid Red]+sgrbrightgreen   = setSGRCode [SetColor Foreground Vivid Green]+sgrbrightyellow  = setSGRCode [SetColor Foreground Vivid Yellow]+sgrbrightblue    = setSGRCode [SetColor Foreground Vivid Blue]+sgrbrightmagenta = setSGRCode [SetColor Foreground Vivid Magenta]+sgrbrightcyan    = setSGRCode [SetColor Foreground Vivid Cyan]+sgrbrightwhite   = setSGRCode [SetColor Foreground Vivid White]+sgrrgb r g b     = setSGRCode [SetRGBColor Foreground $ sRGB r g b]++-- | Set various ANSI styles/colours in a string, only if useColorOnStdoutUnsafe says we should. bold' :: String -> String-bold'  = ifAnsi bold+bold'  = ansiWrapUnsafe sgrbold sgrnormal  faint' :: String -> String-faint'  = ifAnsi faint+faint'  = ansiWrapUnsafe sgrfaint sgrnormal  black' :: String -> String-black'  = ifAnsi black+black'  = ansiWrapUnsafe sgrblack sgrresetfg  red' :: String -> String-red'  = ifAnsi red+red'  = ansiWrapUnsafe sgrred sgrresetfg  green' :: String -> String-green'  = ifAnsi green+green'  = ansiWrapUnsafe sgrgreen sgrresetfg  yellow' :: String -> String-yellow'  = ifAnsi yellow+yellow'  = ansiWrapUnsafe sgryellow sgrresetfg  blue' :: String -> String-blue'  = ifAnsi blue+blue'  = ansiWrapUnsafe sgrblue sgrresetfg  magenta' :: String -> String-magenta'  = ifAnsi magenta+magenta'  = ansiWrapUnsafe sgrmagenta sgrresetfg  cyan' :: String -> String-cyan'  = ifAnsi cyan+cyan'  = ansiWrapUnsafe sgrcyan sgrresetfg  white' :: String -> String-white'  = ifAnsi white+white'  = ansiWrapUnsafe sgrwhite sgrresetfg  brightBlack' :: String -> String-brightBlack'  = ifAnsi brightBlack+brightBlack'  = ansiWrapUnsafe sgrbrightblack sgrresetfg  brightRed' :: String -> String-brightRed'  = ifAnsi brightRed+brightRed'  = ansiWrapUnsafe sgrbrightred sgrresetfg  brightGreen' :: String -> String-brightGreen'  = ifAnsi brightGreen+brightGreen'  = ansiWrapUnsafe sgrbrightgreen sgrresetfg  brightYellow' :: String -> String-brightYellow'  = ifAnsi brightYellow+brightYellow'  = ansiWrapUnsafe sgrbrightyellow sgrresetfg  brightBlue' :: String -> String-brightBlue'  = ifAnsi brightBlue+brightBlue'  = ansiWrapUnsafe sgrbrightblue sgrresetfg  brightMagenta' :: String -> String-brightMagenta'  = ifAnsi brightMagenta+brightMagenta'  = ansiWrapUnsafe sgrbrightmagenta sgrresetfg  brightCyan' :: String -> String-brightCyan'  = ifAnsi brightCyan+brightCyan'  = ansiWrapUnsafe sgrbrightcyan sgrresetfg  brightWhite' :: String -> String-brightWhite'  = ifAnsi brightWhite--rgb' :: Word8 -> Word8 -> Word8 -> String -> String-rgb' r g b  = ifAnsi (rgb r g b)---- | Read the value of the --color or --colour command line option provided at program startup--- using unsafePerformIO. If this option was not provided, returns the empty string.-colorOption :: String-colorOption =-  -- similar to debugLevel-  -- keep synced with color/colour flag definition in hledger:CliOptions-  let args = progArgs in-  case dropWhile (/="--color") args of-    -- --color ARG-    "--color":v:_ -> v-    _ ->-      case take 1 $ filter ("--color=" `isPrefixOf`) args of-        -- --color=ARG-        ['-':'-':'c':'o':'l':'o':'r':'=':v] -> v-        _ ->-          case dropWhile (/="--colour") args of-            -- --colour ARG-            "--colour":v:_ -> v-            _ ->-              case take 1 $ filter ("--colour=" `isPrefixOf`) args of-                -- --colour=ARG-                ['-':'-':'c':'o':'l':'o':'u':'r':'=':v] -> v-                _ -> ""---- | Check the IO environment to see if ANSI colour codes should be used on stdout.--- This is done using unsafePerformIO so it can be used anywhere, eg in--- low-level debug utilities, which should be ok since we are just reading.--- The logic is: use color if--- the program was started with --color=yes|always--- or (---   the program was not started with --color=no|never---   and a NO_COLOR environment variable is not defined---   and stdout supports ANSI color---   and -o/--output-file was not used, or its value is "-"--- ).-useColorOnStdout :: Bool-useColorOnStdout = not hasOutputFile && useColorOnHandle stdout+brightWhite'  = ansiWrapUnsafe sgrbrightwhite sgrresetfg --- | Like useColorOnStdout, but checks for ANSI color support on stderr,--- and is not affected by -o/--output-file.-useColorOnStderr :: Bool-useColorOnStderr = useColorOnHandle stderr+rgb' :: Float -> Float -> Float -> String -> String+rgb' r g b  = ansiWrapUnsafe (sgrrgb r g b) sgrresetfg -useColorOnHandle :: Handle -> Bool-useColorOnHandle h = unsafePerformIO $ do-  no_color       <- isJust <$> lookupEnv "NO_COLOR"-  supports_color <- hSupportsANSIColor h-  let coloroption = colorOption-  return $ coloroption `elem` ["always","yes","y"]-       || (coloroption `notElem` ["never","no","n"] && not no_color && supports_color)+-- Generic:  -- | Wrap a string in ANSI codes to set and reset foreground colour. -- ColorIntensity is @Dull@ or @Vivid@ (ie normal and bold).@@ -386,6 +653,7 @@ bgColorB int col (WideBuilder s w) =     WideBuilder (TB.fromString (setSGRCode [SetColor Background int col]) <> s <> TB.fromString (setSGRCode [])) w + -- | Detect whether the terminal currently has a light background colour, -- if possible, using unsafePerformIO. -- If the terminal is transparent, its apparent light/darkness may be different.@@ -410,7 +678,7 @@ terminalColor :: ConsoleLayer -> Maybe (RGB Float) terminalColor = unsafePerformIO . getLayerColor' --- A version of getLayerColor that is less likely to leak escape sequences to output,+-- A version of ansi-terminal's getLayerColor that is less likely to leak escape sequences to output, -- and that returns a RGB of Floats (0..1) that is more compatible with the colour package. -- This does nothing in a non-interactive context (eg when piping stdout to another command), -- inside emacs (emacs shell buffers show the escape sequence for some reason),@@ -425,97 +693,4 @@   where     fractionalRGB :: (Fractional a) => RGB Word16 -> RGB a     fractionalRGB (RGB r g b) = RGB (fromIntegral r / 65535) (fromIntegral g / 65535) (fromIntegral b / 65535)  -- chatgpt---- Errors---- | Simpler alias for errorWithoutStackTrace-error' :: String -> a-error' = errorWithoutStackTrace . ("Error: " <>)---- | A version of errorWithoutStackTrace that adds a usage hint.-usageError :: String -> a-usageError = error' . (++ " (use -h to see usage)")---- Files---- | Expand a tilde (representing home directory) at the start of a file path.--- ~username is not supported. Can raise an error.-expandHomePath :: FilePath -> IO FilePath-expandHomePath = \case-    ('~':'/':p)  -> (</> p) <$> getHomeDirectory-    ('~':'\\':p) -> (</> p) <$> getHomeDirectory-    ('~':_)      -> ioError $ userError "~USERNAME in paths is not supported"-    p            -> return p---- | Given a current directory, convert a possibly relative, possibly tilde-containing--- file path to an absolute one.--- ~username is not supported. Leaves "-" unchanged. Can raise an error.-expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers-expandPath _ "-" = return "-"-expandPath curdir p = (if isRelative p then (curdir </>) else id) <$> expandHomePath p  -- PARTIAL:---- | Like expandPath, but treats the expanded path as a glob, and returns--- zero or more matched absolute file paths, alphabetically sorted.--- Can raise an error.-expandGlob :: FilePath -> FilePath -> IO [FilePath]-expandGlob curdir p = expandPath curdir p >>= glob <&> sort  -- PARTIAL:---- | Given a list of existing file paths, sort them by modification time, most recent first.-sortByModTime :: [FilePath] -> IO [FilePath]-sortByModTime fs = do-  ftimes <- forM fs $ \f -> do {t <- getModificationTime f; return (t,f)}-  return $ map snd $ sortBy (comparing Data.Ord.Down) ftimes---- | Like readFilePortably, but read all of the file before proceeding.-readFileStrictly :: FilePath -> IO T.Text-readFileStrictly f = readFilePortably f >>= \t -> C.evaluate (T.length t) >> return t---- | Read text from a file,--- converting any \r\n line endings to \n,,--- using the system locale's text encoding,--- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8.-readFilePortably :: FilePath -> IO T.Text-readFilePortably f =  openFile f ReadMode >>= readHandlePortably---- | Like readFilePortably, but read from standard input if the path is "-".-readFileOrStdinPortably :: String -> IO T.Text-readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably-  where-    openFileOrStdin :: String -> IOMode -> IO Handle-    openFileOrStdin "-" _ = return stdin-    openFileOrStdin f' m   = openFile f' m--readHandlePortably :: Handle -> IO T.Text-readHandlePortably h = do-  hSetNewlineMode h universalNewlineMode-  menc <- hGetEncoding h-  when (fmap show menc == Just "UTF-8") $  -- XXX no Eq instance, rely on Show-    hSetEncoding h utf8_bom-  T.hGetContents h---- | Like embedFile, but takes a path relative to the package directory.-embedFileRelative :: FilePath -> Q Exp-embedFileRelative f = makeRelativeToProject f >>= embedStringFile---- -- | Like hereFile, but takes a path relative to the package directory.--- -- Similar to embedFileRelative ?--- hereFileRelative :: FilePath -> Q Exp--- hereFileRelative f = makeRelativeToProject f >>= hereFileExp---   where---     QuasiQuoter{quoteExp=hereFileExp} = hereFile---- Time--getCurrentLocalTime :: IO LocalTime-getCurrentLocalTime = do-  t <- getCurrentTime-  tz <- getCurrentTimeZone-  return $ utcToLocalTime tz t--getCurrentZonedTime :: IO ZonedTime-getCurrentZonedTime = do-  t <- getCurrentTime-  tz <- getCurrentTimeZone-  return $ utcToZonedTime tz t- 
Hledger/Utils/Regex.hs view
@@ -1,7 +1,3 @@-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE ScopedTypeVariables   #-} {-|  Easy regular expression helpers, currently based on regex-tdfa. These should:@@ -42,6 +38,12 @@  -} +{-# LANGUAGE CPP                   #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE ScopedTypeVariables   #-}+ module Hledger.Utils.Regex (   -- * Regexp type and constructors    Regexp(reString)@@ -66,7 +68,9 @@ import Data.Aeson (ToJSON(..), Value(String)) import Data.Array ((!), elems, indices) import Data.Char (isDigit)+#if !MIN_VERSION_base(4,20,0) import Data.List (foldl')+#endif import Data.MemoUgly (memo) import Data.Text (Text) import qualified Data.Text as T@@ -163,8 +167,8 @@  -- | Tests whether a Regexp matches a Text. ----- This currently unpacks the Text to a String an works on that. This is due to--- a performance bug in regex-tdfa (#9), which may or may not be relevant here.+-- This currently unpacks the Text to a String, to work around a performance bug+-- in regex-tdfa (#9), which may or may not be relevant here. regexMatchText :: Regexp -> Text -> Bool regexMatchText r = matchTest r . T.unpack 
+ Hledger/Write/Beancount.hs view
@@ -0,0 +1,379 @@+{-|+Helpers for beancount output.+-}++{-# LANGUAGE OverloadedStrings    #-}++module Hledger.Write.Beancount (+  showTransactionBeancount,+  -- postingsAsLinesBeancount,+  -- postingAsLinesBeancount,+  -- showAccountNameBeancount,+  tagsToBeancountMetadata,+  showBeancountMetadata,+  accountNameToBeancount,+  commodityToBeancount,+  -- beancountTopLevelAccounts,++  -- * Tests+  tests_WriteBeancount+)+where++-- import Prelude hiding (Applicative(..))+import Data.Char+import Data.Default (def)+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 Safe (maximumBound)+import Text.DocLayout (realLength)+import Text.Printf+import Text.Tabular.AsciiWide hiding (render)++import Hledger.Utils+import Hledger.Data.Types+import Hledger.Data.AccountName+import Hledger.Data.Amount+import Hledger.Data.Currency (currencySymbolToCode)+import Hledger.Data.Dates (showDate)+import Hledger.Data.Posting (renderCommentLines, showBalanceAssertion, postingIndent)+import Hledger.Data.Transaction (payeeAndNoteFromDescription')+import Data.Function ((&))+import Data.List.Extra (groupOnKey)+import Data.Bifunctor (first)+import Data.List (sort)++--- ** doctest setup+-- $setup+-- >>> :set -XOverloadedStrings++-- | Like showTransaction, but applies various adjustments to produce valid Beancount journal data.+showTransactionBeancount :: Transaction -> Text+showTransactionBeancount t =+  -- https://beancount.github.io/docs/beancount_language_syntax.html+  -- similar to showTransactionHelper, but I haven't bothered with Builder+     firstline <> nl+  <> foldMap ((<> nl).postingIndent.showBeancountMetadata (Just maxmdnamewidth)) mds+  <> foldMap ((<> nl)) newlinecomments+  <> foldMap ((<> nl)) (postingsAsLinesBeancount $ tpostings t)+  <> nl+  where+    firstline = T.concat [date, status, payee, note, samelinecomment]+    date = showDate $ tdate t+    status = if tstatus t == Pending then " !" else " *"+    (payee,note) =+      case payeeAndNoteFromDescription' $ tdescription t of+        ("","") -> ("",      ""      )+        ("",n ) -> (""     , wrapq n )+        (p ,"") -> (wrapq p, wrapq "")+        (p ,n ) -> (wrapq p, wrapq n )+      where+        wrapq = wrap " \"" "\"" . escapeDoubleQuotes . escapeBackslash+    mds = tagsToBeancountMetadata $ ttags t+    maxmdnamewidth = maximum' $ map (T.length . fst) mds+    (samelinecomment, newlinecomments) =+      case renderCommentLines (tcomment t) of []   -> ("",[])+                                              c:cs -> (c,cs)++nl = "\n"++type BMetadata = Tag++-- https://beancount.github.io/docs/beancount_language_syntax.html#metadata-1+-- | Render a Beancount metadata as a metadata line (without the indentation or newline).+-- If a maximum name length is provided, space will be left after the colon+-- so that successive metadata values will all start at the same column.+showBeancountMetadata :: Maybe Int -> BMetadata -> Text+showBeancountMetadata mmaxnamewidth (n,v) =+  fitText (fmap (+2) mmaxnamewidth) Nothing False True (n <> ": ")+  <> toBeancountMetadataValue v++-- | Make a list of tags ready to be rendered as Beancount metadata:+-- Encode and lengthen names, encode values, and combine repeated tags into one.+-- Metadatas will be sorted by (encoded) name and then value.+tagsToBeancountMetadata :: [Tag] -> [BMetadata]+tagsToBeancountMetadata = sort . map (first toBeancountMetadataName) . uniquifyTags . filter (not.isHiddenTagName.fst)++-- | In a list of tags, replace each tag that appears more than once+-- with a single tag with all of the values combined into one, comma-and-space-separated.+-- This function also sorts all tags by name and then value.+uniquifyTags :: [Tag] -> [Tag]+uniquifyTags ts = [(k, T.intercalate ", " $ map snd $ tags) | (k, tags) <- groupOnKey fst $ sort ts]++toBeancountMetadataName :: TagName -> Text+toBeancountMetadataName name =+  prependStartCharIfNeeded $+  case T.uncons name of+    Nothing -> ""+    Just (c,cs) ->+      T.concatMap (\d -> if isBeancountMetadataNameChar d then T.singleton d else toBeancountMetadataNameChar d) $ T.cons c cs+  where+    -- If the name is empty, make it "mm".+    -- If it has only one character, prepend "m".+    -- If the first character is not a valid one, prepend "m".+    prependStartCharIfNeeded t =+      case T.uncons t of+        Nothing -> T.replicate 2 $ T.singleton beancountMetadataDummyNameStartChar+        Just (c,cs) | T.null cs || not (isBeancountMetadataNameStartChar c) -> T.cons beancountMetadataDummyNameStartChar t+        _ -> t++-- | Is this a valid character to start a Beancount metadata name (lowercase letter) ?+isBeancountMetadataNameStartChar :: Char -> Bool+isBeancountMetadataNameStartChar c = isLetter c && islowercase c++-- | Dummy valid starting character to prepend to a Beancount metadata name if needed.+beancountMetadataDummyNameStartChar :: Char+beancountMetadataDummyNameStartChar = 'm'++-- | Is this a valid character in the middle of a Beancount metadata name (a lowercase letter, digit, _ or -) ?+isBeancountMetadataNameChar :: Char -> Bool+isBeancountMetadataNameChar c = (isLetter c && islowercase c) || isDigit c || c `elem` ['_', '-']++-- | Convert a character to one or more characters valid inside a Beancount metadata name.+-- Letters are lowercased, spaces are converted to dashes, and unsupported characters are encoded as c<HEXBYTES>.+toBeancountMetadataNameChar :: Char -> Text+toBeancountMetadataNameChar c+  | isBeancountMetadataNameChar c = T.singleton c+  | isLetter c = T.singleton $ toLower c+  | isSpace c = "-"+  | otherwise = T.pack $ printf "c%x" c++toBeancountMetadataValue :: TagValue -> Text+toBeancountMetadataValue = ("\"" <>) . (<> "\"") . T.concatMap toBeancountMetadataValueChar++-- | Is this a valid character in the middle of a Beancount metadata name (a lowercase letter, digit, _ or -) ?+isBeancountMetadataValueChar :: Char -> Bool+isBeancountMetadataValueChar c = c `notElem` ['"']++-- | Convert a character to one or more characters valid inside a Beancount metadata value:+-- a double quote is encoded as c<HEXBYTES>.+toBeancountMetadataValueChar :: Char -> Text+toBeancountMetadataValueChar c+  | isBeancountMetadataValueChar c = T.singleton c+  | otherwise = T.pack $ printf "c%x" c+++-- | Render a transaction's postings as indented lines, suitable for `print -O beancount` output.+-- See also Posting.postingsAsLines.+postingsAsLinesBeancount :: [Posting] -> [Text]+postingsAsLinesBeancount ps = concatMap first3 linesWithWidths+  where+    linesWithWidths = map (postingAsLinesBeancount False maxacctwidth maxamtwidth) ps+    maxacctwidth = maximumBound 0 $ map second3 linesWithWidths+    maxamtwidth  = maximumBound 0 $ map third3  linesWithWidths++-- | Render one posting, on one or more lines, suitable for `print -O beancount` output.+-- Also returns the widths calculated for the account and amount fields.+-- See also Posting.postingAsLines.+postingAsLinesBeancount  :: Bool -> Int -> Int -> Posting -> ([Text], Int, Int)+postingAsLinesBeancount elideamount acctwidth amtwidth p =+    (concatMap (++ (map ("  "<>) $ metadatalines <> newlinecomments)) postingblocks+    ,thisacctwidth+    ,thisamtwidth+    )+  where+    -- This needs to be converted to strict Text in order to strip trailing+    -- spaces. This adds a small amount of inefficiency, and the only difference+    -- is whether there are trailing spaces in print (and related) reports. This+    -- could be removed and we could just keep everything as a Text Builder, but+    -- would require adding trailing spaces to 42 failing tests.+    postingblocks = [map T.stripEnd . T.lines . TL.toStrict $+                       render [ textCell BottomLeft statusandaccount+                              , textCell BottomLeft "  "+                              , Cell BottomLeft [pad amt]+                              , textCell BottomLeft samelinecomment+                              ]+                    | (amt,_assertion) <- shownAmountsAssertions]+    render = renderRow def{tableBorders=False, borderSpaces=False} . Group NoLine . map Header+    pad amt = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt+      where w = max 12 amtwidth - wbWidth amt  -- min. 12 for backwards compatibility++    pacct = showAccountNameBeancount Nothing $ paccount p+    pstatusandacct p' = if pstatus p' == Pending then "! " else "" <> pacct++    -- currently prices are considered part of the amount string when right-aligning amounts+    -- Since we will usually be calling this function with the knot tied between+    -- amtwidth and thisamtwidth, make sure thisamtwidth does not depend on+    -- amtwidth at all.+    shownAmounts+      | elideamount = [mempty]+      | otherwise   = showMixedAmountLinesB displayopts a'+        where+          displayopts = defaultFmt{ displayZeroCommodity=True, displayForceDecimalMark=True, displayQuotes=False }+          a' = mapMixedAmount amountToBeancount $ pamount p+    thisamtwidth = maximumBound 0 $ map wbWidth shownAmounts++    -- when there is a balance assertion, show it only on the last posting line+    shownAmountsAssertions = zip shownAmounts shownAssertions+      where+        shownAssertions = replicate (length shownAmounts - 1) mempty ++ [assertion]+          where+            assertion = maybe mempty ((WideBuilder (TB.singleton ' ') 1 <>).showBalanceAssertion) $ pbalanceassertion p++    -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned+    statusandaccount = postingIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p+    thisacctwidth = realLength pacct+    mds = tagsToBeancountMetadata $ ptags p+    metadatalines = map (postingIndent . showBeancountMetadata (Just maxtagnamewidth)) mds+      where maxtagnamewidth = maximum' $ map (T.length . fst) mds+    (samelinecomment, newlinecomments) =+      case renderCommentLines (pcomment p) of []   -> ("",[])+                                              c:cs -> (c,cs)++-- | Like showAccountName for Beancount journal format.+-- Calls accountNameToBeancount first.+showAccountNameBeancount :: Maybe Int -> AccountName -> Text+showAccountNameBeancount w = maybe id T.take w . accountNameToBeancount++type BeancountAccountName = AccountName+type BeancountAccountNameComponent = AccountName++-- | Convert a hledger account name to a valid Beancount account name.+-- It replaces spaces with dashes and other non-supported characters with C<HEXBYTES>;+-- prepends the letter A to any part which doesn't begin with a letter or number;+-- adds a second :A part if there is only one part;+-- and capitalises each part.+-- It also checks that the first part is one of the required english+-- account names Assets, Liabilities, Equity, Income, or Expenses, and if not+-- raises an informative error.+-- Ref: https://beancount.github.io/docs/beancount_language_syntax.html#accounts+accountNameToBeancount :: AccountName -> BeancountAccountName+accountNameToBeancount a = b+  where+    cs1 =+      map accountNameComponentToBeancount $ accountNameComponents $+      dbg9 "hledger account name  " a+    cs2 =+      case cs1 of+        c:_ | c `notElem` beancountTopLevelAccounts -> error' e+          where+            e = T.unpack $ T.unlines [+              "bad top-level account: " <> c+              ,"in beancount account name:           " <> accountNameFromComponents cs1+              ,"converted from hledger account name: " <> a+              ,"For Beancount, top-level accounts must be (or be --alias'ed to)"+              ,"one of " <> T.intercalate ", " beancountTopLevelAccounts <> "."+              -- ,"and not: " <> b+              ]+        [c] -> [c, "A"]+        cs  -> cs+    b = dbg9 "beancount account name" $ accountNameFromComponents cs2++accountNameComponentToBeancount :: AccountName -> BeancountAccountNameComponent+accountNameComponentToBeancount acctpart =+  prependStartCharIfNeeded $+  case T.uncons acctpart of+    Nothing -> ""+    Just (c,cs) ->+      textCapitalise $+      T.concatMap (\d -> if isBeancountAccountChar d then (T.singleton d) else T.pack $ charToBeancount d) $ T.cons c cs+  where+    prependStartCharIfNeeded t =+      case T.uncons t of+        Just (c,_) | not $ isBeancountAccountStartChar c -> T.cons beancountAccountDummyStartChar t+        _ -> t++-- | Dummy valid starting character to prepend to Beancount account name parts if needed (A).+beancountAccountDummyStartChar :: Char+beancountAccountDummyStartChar = 'A'++charToBeancount :: Char -> String+charToBeancount c = if isSpace c then "-" else printf "C%x" c++-- XXX these probably allow too much unicode:++-- https://hackage.haskell.org/package/base-4.20.0.1/docs/Data-Char.html#v:isUpperCase would be more correct,+-- but isn't available till base 4.18/ghc 9.6. isUpper is close enough in practice.+isuppercase = isUpper+-- same story, presumably+islowercase = isLower++-- | Is this a valid character to start a Beancount account name part (capital letter or digit) ?+isBeancountAccountStartChar :: Char -> Bool+isBeancountAccountStartChar c = (isLetter c && isuppercase c) || isDigit c++-- | Is this a valid character to appear elsewhere in a Beancount account name part (letter, digit, or -) ?+isBeancountAccountChar :: Char -> Bool+isBeancountAccountChar c = isLetter c || isDigit c || c=='-'++beancountTopLevelAccounts = ["Assets", "Liabilities", "Equity", "Income", "Expenses"]++type BeancountAmount = Amount++-- | Do some best effort adjustments to make an amount that renders+-- in a way that Beancount can read: force the commodity symbol to the right,+-- capitalise all letters, convert a few currency symbols to codes.+amountToBeancount :: Amount -> BeancountAmount+amountToBeancount a@Amount{acommodity=c,astyle=s,acost=mp} = a{acommodity=c', astyle=s', acost=mp'}+  where+    c' = commodityToBeancount c+    s' = s{ascommodityside=R, ascommodityspaced=True}+    mp' = costToBeancount <$> mp+      where+        costToBeancount (TotalCost amt) = TotalCost $ amountToBeancount amt+        costToBeancount (UnitCost  amt) = UnitCost  $ amountToBeancount amt++type BeancountCommoditySymbol = CommoditySymbol++-- | Convert a hledger commodity name to a valid Beancount commodity name.+-- That is: 2-24 uppercase letters / digits / apostrophe / period / underscore / dash,+-- starting with a letter, and ending with a letter or digit.+-- Ref: https://beancount.github.io/docs/beancount_language_syntax.html#commodities-currencies+-- So this:+-- replaces common currency symbols with their ISO 4217 currency codes,+-- capitalises all letters,+-- replaces spaces with dashes and other invalid characters with C<HEXBYTES>,+-- prepends a C if the first character is not a letter,+-- appends a C if the last character is not a letter or digit,+-- and disables hledger's enclosing double quotes.+--+-- >>> commodityToBeancount ""+-- "C"+-- >>> commodityToBeancount "$"+-- "USD"+-- >>> commodityToBeancount "Usd"+-- "USD"+-- >>> commodityToBeancount "\"a1\""+-- "A1"+-- >>> commodityToBeancount "\"A 1!\""+-- "A-1C21"+--+commodityToBeancount :: CommoditySymbol -> BeancountCommoditySymbol+commodityToBeancount com =+  dbg9 "beancount commodity name" $+  let com' = stripquotes com+  in case currencySymbolToCode com' of+    Just code -> code+    Nothing ->+      com'+      & T.toUpper+      & T.concatMap (\d -> if isBeancountCommodityChar d then T.singleton d else T.pack $ charToBeancount d)+      & fixstart+      & fixend+  where+    fixstart bcom = case T.uncons bcom of+      Just (c,_) | isBeancountCommodityStartChar c -> bcom+      _ -> "C" <> bcom+    fixend bcom = case T.unsnoc bcom of+      Just (_,c) | isBeancountCommodityEndChar c -> bcom+      _ -> bcom <> "C"++-- | Is this a valid character in the middle of a Beancount commodity name (a capital letter, digit, or '._-) ?+isBeancountCommodityChar :: Char -> Bool+isBeancountCommodityChar c = (isLetter c && isuppercase c) || isDigit c || c `elem` ['\'', '.', '_', '-']++-- | Is this a valid character to start a Beancount commodity name (a capital letter) ?+isBeancountCommodityStartChar :: Char -> Bool+isBeancountCommodityStartChar c = isLetter c && isuppercase c++-- | Is this a valid character to end a Beancount commodity name (a capital letter or digit) ?+isBeancountCommodityEndChar :: Char -> Bool+isBeancountCommodityEndChar c = (isLetter c && isuppercase c) || isDigit c++--- ** tests++tests_WriteBeancount :: TestTree+tests_WriteBeancount = testGroup "Write.Beancount" [+  ]
Hledger/Write/Html.hs view
@@ -1,39 +1,38 @@ {-# LANGUAGE OverloadedStrings #-} {- |-Export spreadsheet table data as HTML table.--This is derived from <https://hackage.haskell.org/package/classify-frog-0.2.4.3/src/src/Spreadsheet/Format.hs>+Common definitions for Html.Blaze and Html.Lucid -} module Hledger.Write.Html (-    printHtml,+    Lines(..),+    borderStyles,     ) where -import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))+import qualified Hledger.Write.Spreadsheet as Spr+import Hledger.Write.Spreadsheet (Cell(..)) -import qualified Lucid.Base as LucidBase-import qualified Lucid-import Data.Foldable (for_)+import Data.Text (Text)  -printHtml :: [[Cell (Lucid.Html ())]] -> Lucid.Html ()-printHtml table =-    Lucid.table_ $ for_ table $ \row ->-    Lucid.tr_ $ for_ row $ \cell ->-    formatCell cell+borderStyles :: Lines border => Cell border text -> [Text]+borderStyles cell =+    let border field access =+            map (field<>) $ borderLines $ access $ cellBorder cell in+    let leftBorder   = border "border-left:"   Spr.borderLeft   in+    let rightBorder  = border "border-right:"  Spr.borderRight  in+    let topBorder    = border "border-top:"    Spr.borderTop    in+    let bottomBorder = border "border-bottom:" Spr.borderBottom in+    leftBorder++rightBorder++topBorder++bottomBorder -formatCell :: Cell (Lucid.Html ()) -> Lucid.Html ()-formatCell cell =-    let str = cellContent cell in-    case cellStyle cell of-        Head -> Lucid.th_ str-        Body emph ->-            let align =-                    case cellType cell of-                        TypeString -> []-                        TypeDate -> []-                        _ -> [LucidBase.makeAttribute "align" "right"]-                withEmph =-                    case emph of-                        Item -> id-                        Total -> Lucid.b_-            in  Lucid.td_ align $ withEmph str++class (Spr.Lines border) => Lines border where+    borderLines :: border -> [Text]++instance Lines () where+    borderLines () = []++instance Lines Spr.NumLines where+    borderLines prop =+        case prop of+            Spr.NoLine -> []+            Spr.SingleLine -> ["black"]+            Spr.DoubleLine -> ["double black"]
+ Hledger/Write/Html/Attribute.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+Helpers and CSS styles for HTML output.+-}+module Hledger.Write.Html.Attribute (+    stylesheet,+    concatStyles,+    tableStylesheet,+    tableStyle,+    bold,+    doubleborder,+    topdoubleborder,+    bottomdoubleborder,+    alignright,+    alignleft,+    aligncenter,+    collapse,+    lpad,+    rpad,+    hpad,+    vpad,+    ) where++import qualified Data.Text as Text+import Data.Text (Text)+++stylesheet :: [(Text,Text)] -> Text+stylesheet elstyles =+    Text.unlines $+        "" : [el<>" {"<>styles<>"}" | (el,styles) <- elstyles]++concatStyles :: [Text] -> Text+concatStyles = Text.intercalate "; "+++tableStylesheet :: Text+tableStylesheet = stylesheet tableStyle++tableStyle :: [(Text, Text)]+tableStyle =+  [("table", collapse),+   ("th, td", lpad),+   ("th.account, td.account", "padding-left:0;")]++bold, doubleborder, topdoubleborder, bottomdoubleborder :: Text+bold = "font-weight:bold"+doubleborder = "double black"+topdoubleborder    = "border-top:"<>doubleborder+bottomdoubleborder = "border-bottom:"<>doubleborder++alignright, alignleft, aligncenter :: Text+alignright  = "text-align:right"+alignleft   = "text-align:left"+aligncenter = "text-align:center"++collapse :: Text+collapse = "border-collapse:collapse"++lpad, rpad, hpad, vpad :: Text+lpad = "padding-left:1em"+rpad = "padding-right:1em"+hpad = "padding-left:1em; padding-right:1em"+vpad = "padding-top:1em;  padding-bottom:1em"
+ Hledger/Write/Html/Blaze.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+Export spreadsheet table data as HTML table.++This is derived from <https://hackage.haskell.org/package/classify-frog-0.2.4.3/src/src/Spreadsheet/Format.hs>+-}+module Hledger.Write.Html.Blaze (+    printHtml,+    formatRow,+    formatCell,+    ) where++import qualified Hledger.Write.Html.Attribute as Attr+import qualified Hledger.Write.Spreadsheet as Spr+import Hledger.Write.Html (Lines, borderStyles)+import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))++import qualified Text.Blaze.Html4.Transitional.Attributes as HtmlAttr+import qualified Text.Blaze.Html4.Transitional as Html+import qualified Data.Text as Text+import Text.Blaze.Html4.Transitional (Html, toHtml, (!))+import Data.Foldable (traverse_)+++printHtml :: (Lines border) => [[Cell border Html]] -> Html+printHtml table = do+    Html.style $ toHtml $ Attr.tableStylesheet+    Html.table $ traverse_ formatRow table++formatRow:: (Lines border) => [Cell border Html] -> Html+formatRow = Html.tr . traverse_ formatCell++formatCell :: (Lines border) => Cell border Html -> Html+formatCell cell =+    let str = cellContent cell in+    let content =+            if Text.null $ cellAnchor cell+                then str+                else Html.a str !+                        HtmlAttr.href (Html.textValue (cellAnchor cell)) in+    let style =+            case borderStyles cell of+                [] -> []+                ss -> [HtmlAttr.style $ Html.textValue $+                        Attr.concatStyles ss] in+    let class_ =+            map (HtmlAttr.class_ . Html.textValue) $+            filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in+    let span_ makeCell attrs =+            case Spr.cellSpan cell of+                Spr.NoSpan -> foldl (!) makeCell attrs+                Spr.Covered -> pure ()+                Spr.SpanHorizontal n ->+                    foldl (!) makeCell+                        (HtmlAttr.colspan (Html.stringValue $ show n) : attrs)+                Spr.SpanVertical n ->+                    foldl (!) makeCell+                        (HtmlAttr.rowspan (Html.stringValue $ show n) : attrs)+            in+    case cellStyle cell of+        Head -> span_ (Html.th content) (style++class_)+        Body emph ->+            let align =+                    case cellType cell of+                        TypeString -> []+                        TypeDate -> []+                        _ -> [HtmlAttr.align "right"]+                valign =+                    case Spr.cellSpan cell of+                        Spr.SpanVertical n ->+                            if n>1 then [HtmlAttr.valign "top"] else []+                        _ -> []+                withEmph =+                    case emph of+                        Item -> id+                        Total -> Html.b+            in  span_ (Html.td $ withEmph content) $+                style++align++valign++class_
+ Hledger/Write/Html/Lucid.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+Export spreadsheet table data as HTML table.++This is derived from <https://hackage.haskell.org/package/classify-frog-0.2.4.3/src/src/Spreadsheet/Format.hs>+-}+module Hledger.Write.Html.Lucid (+    printHtml,+    formatRow,+    formatCell,+    ) where++import qualified Hledger.Write.Html.Attribute as Attr+import qualified Hledger.Write.Spreadsheet as Spr+import Hledger.Write.Html (Lines, borderStyles)+import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))++import qualified Data.Text as Text+import qualified Lucid.Base as HtmlBase+import qualified Lucid as Html+import Data.Foldable (traverse_)+++type Html = Html.Html ()++printHtml :: (Lines border) => [[Cell border Html]] -> Html+printHtml table = do+    Html.link_ [Html.rel_ "stylesheet", Html.href_ "hledger.css"]+    Html.style_ Attr.tableStylesheet+    Html.table_ $ traverse_ formatRow table++formatRow:: (Lines border) => [Cell border Html] -> Html+formatRow = Html.tr_ . traverse_ formatCell++formatCell :: (Lines border) => Cell border Html -> Html+formatCell cell =+    let str = cellContent cell in+    let content =+            if Text.null $ cellAnchor cell+                then str+                else Html.a_ [Html.href_ $ cellAnchor cell] str in+    let style =+            case borderStyles cell of+                [] -> []+                ss -> [Html.style_ $ Attr.concatStyles ss] in+    let class_ =+            map Html.class_ $+            filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in+    let span_ makeCell attrs cont =+            case Spr.cellSpan cell of+                Spr.NoSpan -> makeCell attrs cont+                Spr.Covered -> pure ()+                Spr.SpanHorizontal n ->+                    makeCell (Html.colspan_ (Text.pack $ show n) : attrs) cont+                Spr.SpanVertical n ->+                    makeCell (Html.rowspan_ (Text.pack $ show n) : attrs) cont+            in+    case cellStyle cell of+        Head -> span_ Html.th_ (style++class_) content+        Body emph ->+            let align =+                    case cellType cell of+                        TypeString -> []+                        TypeDate -> []+                        _ -> [HtmlBase.makeAttribute "align" "right"]+                valign =+                    case Spr.cellSpan cell of+                        Spr.SpanVertical n ->+                            if n>1+                                then [HtmlBase.makeAttribute "valign" "top"]+                                else []+                        _ -> []+                withEmph =+                    case emph of+                        Item -> id+                        Total -> Html.b_+            in  span_ Html.td_ (style++align++valign++class_) $+                withEmph content
Hledger/Write/Ods.hs view
@@ -12,28 +12,34 @@     printFods,     ) where -import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))-import Hledger.Data.Types (CommoditySymbol, AmountPrecision(..))-import Hledger.Data.Types (acommodity, aquantity, astyle, asprecision)+import Prelude hiding (Applicative(..))+import Control.Monad (guard)+import Control.Applicative (Applicative(..))  import qualified Data.Text.Lazy as TL import qualified Data.Text as T import Data.Text (Text) +import qualified Data.Foldable as Fold+import qualified Data.List as List import qualified Data.Map as Map import qualified Data.Set as Set import Data.Foldable (fold) import Data.Map (Map) import Data.Set (Set)-import Data.Maybe (mapMaybe)+import Data.Maybe (catMaybes)  import qualified System.IO as IO import Text.Printf (printf) +import qualified Hledger.Write.Spreadsheet as Spr+import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))+import Hledger.Data.Types (CommoditySymbol, AmountPrecision(..))+import Hledger.Data.Types (acommodity, aquantity, astyle, asprecision)  printFods ::     IO.TextEncoding ->-    Map Text ((Maybe Int, Maybe Int), [[Cell Text]]) -> TL.Text+    Map Text ((Int, Int), [[Cell Spr.NumLines Text]]) -> TL.Text printFods encoding tables =     let fileOpen customStyles =           map (map (\c -> case c of '\'' -> '"'; _ -> c)) $@@ -57,20 +63,6 @@           "  xmlns:field='urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0'" :           "  xmlns:form='urn:oasis:names:tc:opendocument:xmlns:form:1.0'>" :           "<office:styles>" :-          "  <style:style style:name='head' style:family='table-cell'>" :-          "    <style:paragraph-properties fo:text-align='center'/>" :-          "    <style:text-properties fo:font-weight='bold'/>" :-          "  </style:style>" :-          "  <style:style style:name='foot' style:family='table-cell'>" :-          "    <style:text-properties fo:font-weight='bold'/>" :-          "  </style:style>" :-          "  <style:style style:name='amount' style:family='table-cell'>" :-          "    <style:paragraph-properties fo:text-align='end'/>" :-          "  </style:style>" :-          "  <style:style style:name='total-amount' style:family='table-cell'>" :-          "    <style:paragraph-properties fo:text-align='end'/>" :-          "    <style:text-properties fo:font-weight='bold'/>" :-          "  </style:style>" :           "  <number:date-style style:name='iso-date'>" :           "    <number:year number:style='long'/>" :           "    <number:text>-</number:text>" :@@ -78,12 +70,9 @@           "    <number:text>-</number:text>" :           "    <number:day number:style='long'/>" :           "  </number:date-style>" :-          "  <style:style style:name='date' style:family='table-cell'" :-          "      style:data-style-name='iso-date'/>" :-          "  <style:style style:name='foot-date' style:family='table-cell'" :-          "      style:data-style-name='iso-date'>" :-          "    <style:text-properties fo:font-weight='bold'/>" :-          "  </style:style>" :+          "  <number:number-style style:name='integer'>" :+          "    <number:number number:min-integer-digits='1'/>" :+          "  </number:number-style>" :           customStyles ++           "</office:styles>" :           []@@ -99,14 +88,14 @@           "    <config:config-item-map-entry>" :           "     <config:config-item-map-named config:name='Tables'>" :           (fold $-           flip Map.mapWithKey tableNames $ \tableName (mTopRow,mLeftColumn) ->+           flip Map.mapWithKey tableNames $ \tableName (topRow,leftColumn) ->              printf "      <config:config-item-map-entry config:name='%s'>" tableName :-             (flip foldMap mLeftColumn $ \leftColumn ->+             ((guard (leftColumn>0) >>) $                 "       <config:config-item config:name='HorizontalSplitMode' config:type='short'>2</config:config-item>" :                 printf "       <config:config-item config:name='HorizontalSplitPosition' config:type='int'>%d</config:config-item>" leftColumn :                 printf "       <config:config-item config:name='PositionRight' config:type='int'>%d</config:config-item>" leftColumn :                 []) ++-             (flip foldMap mTopRow $ \topRow ->+             ((guard (topRow>0) >>) $                 "       <config:config-item config:name='VerticalSplitMode' config:type='short'>2</config:config-item>" :                 printf "       <config:config-item config:name='VerticalSplitPosition' config:type='int'>%d</config:config-item>" topRow :                 printf "       <config:config-item config:name='PositionBottom' config:type='int'>%d</config:config-item>" topRow :@@ -135,7 +124,7 @@     in  TL.unlines $ map (TL.fromStrict . T.pack) $         fileOpen           (let styles = cellStyles (foldMap (concat.snd) tables) in-           (numberConfig =<< Set.toList (Set.map snd styles))+           (numberConfig =<< Set.toList (foldMap (numberParams.snd) styles))            ++            (cellConfig =<< Set.toList styles)) ++         tableConfig (fmap fst tables) ++@@ -150,18 +139,24 @@         fileClose  -cellStyles :: [Cell Text] -> Set (Emphasis, (CommoditySymbol, AmountPrecision))+dataStyleFromType :: Type -> DataStyle+dataStyleFromType typ =+    case typ of+        TypeString -> DataString+        TypeInteger -> DataInteger+        TypeDate -> DataDate+        TypeAmount amt -> DataAmount (acommodity amt) (asprecision $ astyle amt)+        TypeMixedAmount -> DataMixedAmount++cellStyles ::+    (Ord border) =>+    [Cell border Text] ->+    Set ((Spr.Border border, Style), DataStyle) cellStyles =     Set.fromList .-    mapMaybe (\cell ->-        case cellType cell of-            TypeAmount amt ->-                Just-                    (case cellStyle cell of-                        Body emph -> emph-                        Head -> Total,-                     (acommodity amt, asprecision $ astyle amt))-            _ -> Nothing)+    map (\cell ->+            ((cellBorder cell, cellStyle cell),+             dataStyleFromType $ cellType cell))  numberStyleName :: (CommoditySymbol, AmountPrecision) -> String numberStyleName (comm, prec) =@@ -170,6 +165,10 @@         NaturalPrecision -> "natural"         Precision k -> show k +numberParams :: DataStyle -> Set (CommoditySymbol, AmountPrecision)+numberParams (DataAmount comm prec) = Set.singleton (comm, prec)+numberParams _ = Set.empty+ numberConfig :: (CommoditySymbol, AmountPrecision) -> [String] numberConfig (comm, prec) =     let precStr =@@ -191,45 +190,125 @@         Item -> "item"         Total -> "total" -cellConfig :: (Emphasis, (CommoditySymbol, AmountPrecision)) -> [String]-cellConfig (emph, numParam) =-    let name = numberStyleName numParam in-    let style :: String+cellStyleName :: Style -> String+cellStyleName style =+    case style of+        Head -> "head"+        Body emph -> emphasisName emph++linesName :: Spr.NumLines -> Maybe String+linesName prop =+    case prop of+        Spr.NoLine -> Nothing+        Spr.SingleLine -> Just "single"+        Spr.DoubleLine -> Just "double"++linesStyle :: Spr.NumLines -> String+linesStyle prop =+    case prop of+        Spr.NoLine -> "none"+        Spr.SingleLine -> "1.5pt solid #000000"+        Spr.DoubleLine -> "1.5pt double-thin #000000"++borderLabels :: Spr.Border String+borderLabels = Spr.Border "left" "right" "top" "bottom"++borderName :: Spr.Border Spr.NumLines -> String+borderName border =+    (\bs ->+        case bs of+            [] -> "noborder"+            _ ->+                ("border="++) $ List.intercalate "," $+                map (\(name,num) -> name ++ ':' : num) bs) $+    catMaybes $ Fold.toList $+    liftA2+        (\name numLines -> (,) name <$> linesName numLines)+        borderLabels+        border++borderStyle :: Spr.Border Spr.NumLines -> [String]+borderStyle border =+    if border == Spr.noBorder+        then []+        else (:[]) $+            printf "    <style:table-cell-properties%s/>" $+            (id :: String -> String) $ fold $+            liftA2 (printf " fo:border-%s='%s'") borderLabels $+            fmap linesStyle border++data DataStyle =+      DataString+    | DataInteger+    | DataDate+    | DataAmount CommoditySymbol AmountPrecision+    | DataMixedAmount+    deriving (Eq, Ord, Show)++cellConfig :: ((Spr.Border Spr.NumLines, Style), DataStyle) -> [String]+cellConfig ((border, cstyle), dataStyle) =+    let boldStyle = "    <style:text-properties fo:font-weight='bold'/>"+        alignTop =+            "    <style:table-cell-properties style:vertical-align='top'/>"+        alignParagraph =+            printf "    <style:paragraph-properties fo:text-align='%s'/>"+        moreStyles =+            borderStyle border+            +++            (+            case cstyle of+                Body Item ->+                    alignTop :+                    []+                Body Total ->+                    alignTop :+                    boldStyle :+                    []+                Head ->+                    alignParagraph "center" :+                    boldStyle :+                    []+            )+            +++            (+            case dataStyle of+                DataMixedAmount -> [alignParagraph "end"]+                _ -> []+            )+        style :: String         style =-            printf "style:name='%s-%s' style:data-style-name='number-%s'"-                (emphasisName emph) name name in-    case emph of-        Item ->+            let (styleName,dataStyleName) = styleNames cstyle border dataStyle+            in  printf "style:name='%s'" styleName+                +++                foldMap (printf " style:data-style-name='%s'") dataStyleName+    in+    case moreStyles of+        [] ->             printf "  <style:style style:family='table-cell' %s/>" style :             []-        Total ->+        _ ->             printf "  <style:style style:family='table-cell' %s>" style :-            "    <style:text-properties fo:font-weight='bold'/>" :+            moreStyles ++             "  </style:style>" :             []  -formatCell :: Cell Text -> [String]+formatCell :: Cell Spr.NumLines Text -> [String] formatCell cell =     let style, valueType :: String         style =-          case (cellStyle cell, cellType cell) of-            (Body emph, TypeAmount amt) -> tableStyle $ numberStyle emph amt-            (Body Item, TypeString) -> ""-            (Body Item, TypeMixedAmount) -> tableStyle "amount"-            (Body Item, TypeDate) -> tableStyle "date"-            (Body Total, TypeString) -> tableStyle "foot"-            (Body Total, TypeMixedAmount) -> tableStyle "total-amount"-            (Body Total, TypeDate) -> tableStyle "foot-date"-            (Head, _) -> tableStyle "head"-        numberStyle emph amt =-            printf "%s-%s"-                (emphasisName emph)-                (numberStyleName (acommodity amt, asprecision $ astyle amt))-        tableStyle = printf " table:style-name='%s'"+            printf " table:style-name='%s'" $ fst $+            styleNames+                (cellStyle cell)+                (cellBorder cell)+                (dataStyleFromType $ cellType cell)          valueType =             case cellType cell of+                TypeInteger ->+                    printf+                        "office:value-type='float' office:value='%s'"+                        (cellContent cell)                 TypeAmount amt ->                     printf                         "office:value-type='float' office:value='%s'"@@ -240,11 +319,49 @@                         (cellContent cell)                 _ -> "office:value-type='string'" +        covered =+            case cellSpan cell of+                Spr.Covered -> "covered-"+                _ -> ""++        span_ =+            case cellSpan cell of+                Spr.SpanHorizontal n | n>1 ->+                    printf " table:number-columns-spanned='%d'" n+                Spr.SpanVertical n | n>1 ->+                    printf " table:number-rows-spanned='%d'" n+                _ -> ""++        anchor text =+            if T.null $ Spr.cellAnchor cell+                then text+                else printf "<text:a xlink:href='%s'>%s</text:a>"+                        (escape $ T.unpack $ Spr.cellAnchor cell) text+     in-    printf "<table:table-cell%s %s>" style valueType :-    printf "<text:p>%s</text:p>" (escape $ T.unpack $ cellContent cell) :-    "</table:table-cell>" :+    printf "<table:%stable-cell%s%s %s>" covered style span_ valueType :+    printf "<text:p>%s</text:p>"+        (anchor $ escape $ T.unpack $ cellContent cell) :+    printf "</table:%stable-cell>" covered :     []++styleNames ::+    Style -> Spr.Border Spr.NumLines -> DataStyle -> (String, Maybe String)+styleNames cstyle border dataStyle =+    let cstyleName = cellStyleName cstyle in+    let bordName = borderName border in+    case dataStyle of+        DataDate ->+            (printf "%s-%s-date" cstyleName bordName, Just "iso-date")+        DataInteger ->+            (printf "%s-%s-integer" cstyleName bordName, Just "integer")+        DataAmount comm prec ->+            let name = numberStyleName (comm, prec) in+            (printf "%s-%s-%s" cstyleName bordName name,+                Just $ printf "number-%s" name)+        DataMixedAmount ->+            (printf "%s-%s-mixedamount" cstyleName bordName, Nothing)+        DataString -> (printf "%s-%s" cstyleName bordName, Nothing)  escape :: String -> String escape =
Hledger/Write/Spreadsheet.hs view
@@ -7,15 +7,42 @@     Style(..),     Emphasis(..),     Cell(..),+    Class(Class), textFromClass,+    Span(..),+    Border(..),+    Lines(..),+    NumLines(..),+    noBorder,     defaultCell,+    headerCell,     emptyCell,+    transposeCell,+    transpose,+    horizontalSpan,+    addHeaderBorders,+    addRowSpanHeader,+    rawTableContent,+    cellFromMixedAmount,+    cellsFromMixedAmount,+    cellFromAmount,+    integerCell,     ) where -import Hledger.Data.Types (Amount)+import qualified Hledger.Data.Amount as Amt+import Hledger.Data.Types (Amount, MixedAmount, acommodity)+import Hledger.Data.Amount (AmountFormat) +import qualified Data.List as List+import qualified Data.Text as Text+import Data.Text (Text)+import Text.WideString (WideBuilder) +import Prelude hiding (span)++ data Type =       TypeString+    | TypeInteger     | TypeAmount !Amount     | TypeMixedAmount     | TypeDate@@ -27,23 +54,205 @@ data Emphasis = Item | Total     deriving (Eq, Ord, Show) -data Cell text =++class Lines border where noLine :: border+instance Lines () where noLine = ()+instance Lines NumLines where noLine = NoLine++{- |+The same as Tab.Properties, but has 'Eq' and 'Ord' instances.+We need those for storing 'NumLines' in 'Set's.+-}+data NumLines = NoLine | SingleLine | DoubleLine+    deriving (Eq, Ord, Show)++data Border lines =+    Border {+        borderLeft, borderRight,+        borderTop, borderBottom :: lines+    }+    deriving (Eq, Ord, Show)++instance Functor Border where+    fmap f (Border left right top bottom) =+        Border (f left) (f right) (f top) (f bottom)++instance Applicative Border where+    pure a = Border a a a a+    Border fLeft fRight fTop fBottom <*> Border left right top bottom =+        Border (fLeft left) (fRight right) (fTop top) (fBottom bottom)++instance Foldable Border where+    foldMap f (Border left right top bottom) =+        f left <> f right <> f top <> f bottom++noBorder :: (Lines border) => Border border+noBorder = pure noLine++transposeBorder :: Border lines -> Border lines+transposeBorder (Border left right top bottom) =+    Border top bottom left right+++newtype Class = Class Text++textFromClass :: Class -> Text+textFromClass (Class cls) = cls+++{- |+* 'NoSpan' means a single unmerged cell.++* 'Covered' is a cell if it is part of a horizontally or vertically merged cell.+  We maintain these cells although they are ignored in HTML output.+  In contrast to that, FODS can store covered cells+  and allows to access the hidden cell content via formulas.+  CSV does not support merged cells+  and thus simply writes the content of covered cells.+  Maintaining 'Covered' cells also simplifies transposing.++* @'SpanHorizontal' n@ denotes the first cell in a row+  that is part of a merged cell.+  The merged cell contains @n@ atomic cells, including the first one.+  That is @SpanHorizontal 1@ is actually like @NoSpan@.+  The content of this cell is shown as content of the merged cell.++* @'SpanVertical' n@ starts a vertically merged cell.++The writer functions expect consistent data,+that is, 'Covered' cells must actually be part of a merged cell+and merged cells must only cover 'Covered' cells.+-}+data Span =+      NoSpan+    | Covered+    | SpanHorizontal Int+    | SpanVertical Int+    deriving (Eq)++transposeSpan :: Span -> Span+transposeSpan span =+    case span of+        NoSpan -> NoSpan+        Covered -> Covered+        SpanHorizontal n -> SpanVertical n+        SpanVertical n -> SpanHorizontal n++data Cell border text =     Cell {         cellType :: Type,+        cellBorder :: Border border,         cellStyle :: Style,+        cellSpan :: Span,+        cellAnchor :: Text,+        cellClass :: Class,         cellContent :: text     } -instance Functor Cell where-    fmap f (Cell typ style content) = Cell typ style $ f content+instance Functor (Cell border) where+    fmap f (Cell typ border style span anchor class_ content) =+        Cell typ border style span anchor class_ $ f content -defaultCell :: text -> Cell text+defaultCell :: (Lines border) => text -> Cell border text defaultCell text =     Cell {         cellType = TypeString,+        cellBorder = noBorder,         cellStyle = Body Item,+        cellSpan = NoSpan,+        cellAnchor = mempty,+        cellClass = Class mempty,         cellContent = text     } -emptyCell :: (Monoid text) => Cell text+headerCell :: (Lines borders) => Text -> Cell borders Text+headerCell text = (defaultCell text) {cellStyle = Head}++emptyCell :: (Lines border, Monoid text) => Cell border text emptyCell = defaultCell mempty++transposeCell :: Cell border text -> Cell border text+transposeCell cell =+    cell {+        cellBorder = transposeBorder $ cellBorder cell,+        cellSpan = transposeSpan $ cellSpan cell+    }++transpose :: [[Cell border text]] -> [[Cell border text]]+transpose = List.transpose . map (map transposeCell)+++addHeaderBorders :: [Cell () text] -> [Cell NumLines text]+addHeaderBorders =+    map (\c -> c {cellBorder = noBorder {borderBottom = DoubleLine}})++horizontalSpan ::+    (Lines border, Monoid text) =>+    [a] -> Cell border text -> [Cell border text]+horizontalSpan subCells cell =+    zipWith const+        (cell{cellSpan = SpanHorizontal $ length subCells}+            : repeat (emptyCell {cellSpan = Covered}))+        subCells++addRowSpanHeader ::+    Cell border text ->+    [[Cell border text]] -> [[Cell border text]]+addRowSpanHeader header rows =+    case rows of+        [] -> []+        [row] -> [header:row]+        _ ->+            zipWith (:)+                (header{cellSpan = SpanVertical (length rows)} :+                 repeat header{cellSpan = Covered})+                rows++rawTableContent :: [[Cell border text]] -> [[text]]+rawTableContent = map (map cellContent)++++cellFromMixedAmount ::+    (Lines border) =>+    AmountFormat -> (Class, MixedAmount) -> Cell border WideBuilder+cellFromMixedAmount bopts (cls, mixedAmt) =+    (defaultCell $ Amt.showMixedAmountB bopts mixedAmt) {+        cellClass = cls,+        cellType =+          case Amt.unifyMixedAmount mixedAmt of+            Just amt -> amountType bopts amt+            Nothing -> TypeMixedAmount+    }++cellsFromMixedAmount ::+    (Lines border) =>+    AmountFormat -> (Class, MixedAmount) -> [Cell border WideBuilder]+cellsFromMixedAmount bopts (cls, mixedAmt) =+    map+        (\(str,amt) ->+            (defaultCell str) {+                cellClass = cls,+                cellType = amountType bopts amt+            })+        (Amt.showMixedAmountLinesPartsB bopts mixedAmt)++cellFromAmount ::+    (Lines border) =>+    AmountFormat -> (Class, (wb, Amount)) -> Cell border wb+cellFromAmount bopts (cls, (str,amt)) =+    (defaultCell str) {+        cellClass = cls,+        cellType = amountType bopts amt+    }++amountType :: AmountFormat -> Amount -> Type+amountType bopts amt =+    TypeAmount $+    if Amt.displayCommodity bopts+      then amt+      else amt {acommodity = Text.empty}+++integerCell :: (Lines border) => Integer -> Cell border Text+integerCell k = (defaultCell $ Text.pack $ show k) {cellType = TypeInteger}
Text/Tabular/AsciiWide.hs view
@@ -27,7 +27,7 @@ import Data.Bifunctor (bimap) import Data.Maybe (fromMaybe) import Data.Default (Default(..))-import Data.List (intercalate, intersperse, transpose)+import Data.List (intersperse, transpose) import Data.Semigroup (stimesMonoid) import Data.Text (Text) import qualified Data.Text as T@@ -130,9 +130,9 @@    -- maximum width for each column   sizes = map (fromMaybe 0 . maximumMay . map cellWidth) $ transpose cells2-  renderRs (Header s)   = [s]-  renderRs (Group p hs) = intercalate sep $ map renderRs hs-    where sep = renderHLine VM borders pretty sizes ch2 p+  renderRs =+    concatMap (either (renderHLine VM borders pretty sizes ch2) (:[])) .+    flattenHeader    -- borders and bars   addBorders xs = if borders then bar VT SingleLine : xs ++ [bar VB SingleLine] else xs
hledger-lib.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        1.40+version:        1.41 synopsis:       A library providing the core functionality of hledger description:    This library contains hledger's core functionality.                 It is used by most hledger* packages so that they support the same@@ -45,8 +45,8 @@   type: git   location: https://github.com/simonmichael/hledger -flag ghcdebug-  description: Build with support for attaching a ghc-debug client+flag debug+  description: Build with GHC 9.10+'s stack traces enabled   manual: True   default: False @@ -58,6 +58,7 @@       Hledger.Data.AccountName       Hledger.Data.Amount       Hledger.Data.Balancing+      Hledger.Data.Currency       Hledger.Data.Dates       Hledger.Data.Errors       Hledger.Data.Journal@@ -85,9 +86,13 @@       Hledger.Read.RulesReader       Hledger.Read.TimedotReader       Hledger.Read.TimeclockReader+      Hledger.Write.Beancount       Hledger.Write.Csv       Hledger.Write.Ods       Hledger.Write.Html+      Hledger.Write.Html.Attribute+      Hledger.Write.Html.Blaze+      Hledger.Write.Html.Lucid       Hledger.Write.Spreadsheet       Hledger.Reports       Hledger.Reports.ReportOptions@@ -107,8 +112,8 @@       Hledger.Utils.Test       Hledger.Utils.Text       Text.Tabular.AsciiWide-  other-modules:       Text.WideString+  other-modules:       Paths_hledger_lib   hs-source-dirs:       ./@@ -120,8 +125,9 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.14 && <4.20-    , base-compat+    , base >=4.14 && <4.21+    , base-compat >=0.14.0+    , blaze-html     , blaze-markup >=0.5.1     , bytestring     , call-stack@@ -132,19 +138,20 @@     , containers >=0.5.9     , data-default >=0.5     , deepseq-    , directory+    , directory >=1.2.6.1     , doclayout >=0.3 && <0.6     , extra >=1.6.3     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1     , lucid-    , megaparsec >=7.0.0 && <9.7+    , megaparsec >=7.0.0 && <9.8     , microlens >=0.4     , microlens-th >=0.4     , mtl >=2.2.1     , parser-combinators >=0.4.0     , pretty-simple >4 && <5+    , process     , regex-tdfa     , safe >=0.3.20     , tabular >=0.2@@ -153,7 +160,6 @@     , template-haskell     , terminal-size >=0.3.3     , text >=1.2.4.1-    , text-ansi >=0.2.1     , time >=1.5     , timeit     , transformers >=0.2@@ -161,13 +167,8 @@     , unordered-containers >=0.2     , utf8-string >=0.3.5   default-language: Haskell2010-  if (!(os(windows)))-    build-depends:-        pager >=0.1.1.0-  if (flag(ghcdebug))-    cpp-options: -DGHCDEBUG-    build-depends:-        ghc-debug-stub >=0.6.0.0 && <0.7+  if (flag(debug))+    cpp-options: -DDEBUG  test-suite doctest   type: exitcode-stdio-1.0@@ -183,8 +184,9 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.14 && <4.20-    , base-compat+    , base >=4.14 && <4.21+    , base-compat >=0.14.0+    , blaze-html     , blaze-markup >=0.5.1     , bytestring     , call-stack@@ -195,7 +197,7 @@     , containers >=0.5.9     , data-default >=0.5     , deepseq-    , directory+    , directory >=1.2.6.1     , doclayout >=0.3 && <0.6     , doctest >=0.18.1     , extra >=1.6.3@@ -203,12 +205,13 @@     , filepath     , hashtables >=1.2.3.1     , lucid-    , megaparsec >=7.0.0 && <9.7+    , megaparsec >=7.0.0 && <9.8     , microlens >=0.4     , microlens-th >=0.4     , mtl >=2.2.1     , parser-combinators >=0.4.0     , pretty-simple >4 && <5+    , process     , regex-tdfa     , safe >=0.3.20     , tabular >=0.2@@ -217,7 +220,6 @@     , template-haskell     , terminal-size >=0.3.3     , text >=1.2.4.1-    , text-ansi >=0.2.1     , time >=1.5     , timeit     , transformers >=0.2@@ -225,13 +227,8 @@     , unordered-containers >=0.2     , utf8-string >=0.3.5   default-language: Haskell2010-  if (!(os(windows)))-    build-depends:-        pager >=0.1.1.0-  if (flag(ghcdebug))-    cpp-options: -DGHCDEBUG-    build-depends:-        ghc-debug-stub >=0.6.0.0 && <0.7+  if (flag(debug))+    cpp-options: -DDEBUG   if impl(ghc >= 9.0) && impl(ghc < 9.2)     buildable: False @@ -249,8 +246,9 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.14 && <4.20-    , base-compat+    , base >=4.14 && <4.21+    , base-compat >=0.14.0+    , blaze-html     , blaze-markup >=0.5.1     , bytestring     , call-stack@@ -261,7 +259,7 @@     , containers >=0.5.9     , data-default >=0.5     , deepseq-    , directory+    , directory >=1.2.6.1     , doclayout >=0.3 && <0.6     , extra >=1.6.3     , file-embed >=0.0.10@@ -269,12 +267,13 @@     , hashtables >=1.2.3.1     , hledger-lib     , lucid-    , megaparsec >=7.0.0 && <9.7+    , megaparsec >=7.0.0 && <9.8     , microlens >=0.4     , microlens-th >=0.4     , mtl >=2.2.1     , parser-combinators >=0.4.0     , pretty-simple >4 && <5+    , process     , regex-tdfa     , safe >=0.3.20     , tabular >=0.2@@ -283,7 +282,6 @@     , template-haskell     , terminal-size >=0.3.3     , text >=1.2.4.1-    , text-ansi >=0.2.1     , time >=1.5     , timeit     , transformers >=0.2@@ -292,10 +290,5 @@     , utf8-string >=0.3.5   buildable: True   default-language: Haskell2010-  if (!(os(windows)))-    build-depends:-        pager >=0.1.1.0-  if (flag(ghcdebug))-    cpp-options: -DGHCDEBUG-    build-depends:-        ghc-debug-stub >=0.6.0.0 && <0.7+  if (flag(debug))+    cpp-options: -DDEBUG