hledger-lib 1.20.4 → 1.21
raw patch · 50 files changed
+1857/−11670 lines, 50 filesdep −splitPVP ok
version bump matches the API change (PVP)
Dependencies removed: split
API changes (from Hackage documentation)
- Hledger.Data.Amount: divideAmountAndPrice :: Quantity -> Amount -> Amount
- Hledger.Data.Amount: divideMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount
- Hledger.Data.Amount: instance GHC.Show.Show Hledger.Data.Amount.AmountDisplay
- Hledger.Data.Amount: multiplyAmountAndPrice :: Quantity -> Amount -> Amount
- Hledger.Data.Amount: multiplyMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount
- Hledger.Data.Amount: setAmountPrecision :: AmountPrecision -> Amount -> Amount
- Hledger.Data.Amount: setFullPrecision :: Amount -> Amount
- Hledger.Data.Amount: setMixedAmountPrecision :: AmountPrecision -> MixedAmount -> MixedAmount
- Hledger.Data.Amount: showMixed :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
- Hledger.Data.Amount: showMixedAmountWithPrecision :: AmountPrecision -> MixedAmount -> String
- Hledger.Data.Amount: showMixedOneLine :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
- Hledger.Data.Amount: showMixedOneLineUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
- Hledger.Data.Amount: showMixedUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
- Hledger.Data.Journal: mapJournalPostings :: (Posting -> Posting) -> Journal -> Journal
- Hledger.Data.Journal: mapJournalTransactions :: (Transaction -> Transaction) -> Journal -> Journal
- Hledger.Data.Journal: mapTransactionPostings :: (Posting -> Posting) -> Transaction -> Transaction
- Hledger.Data.StringFormat: overlineWidth :: StringFormat -> Maybe Int
- Hledger.Data.Transaction: showTransactionUnelided :: Transaction -> String
- Hledger.Data.Transaction: showTransactionUnelidedOneLineAmounts :: Transaction -> String
- Hledger.Data.Valuation: AtCost :: Maybe CommoditySymbol -> ValuationType
- Hledger.Data.Valuation: AtDefault :: Maybe CommoditySymbol -> ValuationType
- Hledger.Data.Valuation: unsupportedValueThenError :: String
- Hledger.Reports.MultiBalanceReport: calculateColSpans :: ReportOpts -> DateSpan -> [Day] -> [DateSpan]
- Hledger.Reports.MultiBalanceReport: calculateReportSpan :: ReportSpec -> Journal -> DateSpan
- Hledger.Reports.ReportOptions: AtCost :: Maybe CommoditySymbol -> ValuationType
- Hledger.Reports.ReportOptions: AtDefault :: Maybe CommoditySymbol -> ValuationType
- Hledger.Reports.ReportOptions: changingValuation :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: valuationTypeIsCost :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: valuationTypeIsDefaultValue :: ReportOpts -> Bool
- Text.Tabular.AsciiWide: alignCell :: Align -> String -> Cell
- Text.Tabular.AsciiWide: instance GHC.Show.Show Text.Tabular.AsciiWide.Cell
+ Hledger.Data.Amount: AmountDisplayOpts :: Bool -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> AmountDisplayOpts
+ Hledger.Data.Amount: [displayColour] :: AmountDisplayOpts -> Bool
+ Hledger.Data.Amount: [displayMaxWidth] :: AmountDisplayOpts -> Maybe Int
+ Hledger.Data.Amount: [displayMinWidth] :: AmountDisplayOpts -> Maybe Int
+ Hledger.Data.Amount: [displayNormalised] :: AmountDisplayOpts -> Bool
+ Hledger.Data.Amount: [displayOneLine] :: AmountDisplayOpts -> Bool
+ Hledger.Data.Amount: [displayPrice] :: AmountDisplayOpts -> Bool
+ Hledger.Data.Amount: [displayZeroCommodity] :: AmountDisplayOpts -> Bool
+ Hledger.Data.Amount: amountSetFullPrecision :: Amount -> Amount
+ Hledger.Data.Amount: amountSetPrecision :: AmountPrecision -> Amount -> Amount
+ Hledger.Data.Amount: data AmountDisplayOpts
+ Hledger.Data.Amount: instance Data.Default.Class.Default Hledger.Data.Amount.AmountDisplayOpts
+ Hledger.Data.Amount: instance GHC.Show.Show Hledger.Data.Amount.AmountDisplayOpts
+ Hledger.Data.Amount: mixedAmountSetFullPrecision :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: mixedAmountSetPrecision :: AmountPrecision -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: noColour :: AmountDisplayOpts
+ Hledger.Data.Amount: noPrice :: AmountDisplayOpts
+ Hledger.Data.Amount: oneLine :: AmountDisplayOpts
+ Hledger.Data.Amount: showAmountB :: AmountDisplayOpts -> Amount -> WideBuilder
+ Hledger.Data.Amount: showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder
+ Hledger.Data.Amount: showMixedAmountLinesB :: AmountDisplayOpts -> MixedAmount -> [WideBuilder]
+ Hledger.Data.Amount: wbToText :: WideBuilder -> Text
+ Hledger.Data.Amount: wbUnpack :: WideBuilder -> String
+ Hledger.Data.Journal: journalDateSpanBothDates :: Journal -> DateSpan
+ Hledger.Data.Journal: journalMapPostingAmounts :: (Amount -> Amount) -> Journal -> Journal
+ Hledger.Data.Journal: journalMapPostings :: (Posting -> Posting) -> Journal -> Journal
+ Hledger.Data.Journal: journalMapTransactions :: (Transaction -> Transaction) -> Journal -> Journal
+ Hledger.Data.Journal: journalPayeesDeclared :: Journal -> [Payee]
+ Hledger.Data.Journal: journalPayeesDeclaredOrUsed :: Journal -> [Payee]
+ Hledger.Data.Journal: journalPayeesUsed :: Journal -> [Payee]
+ Hledger.Data.Journal: journalTransactionsSimilarTo :: Journal -> Query -> Text -> Int -> [(Double, Transaction)]
+ Hledger.Data.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.PayeeDeclarationInfo
+ Hledger.Data.Posting: postingApplyCostValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Costing -> Maybe ValuationType -> Posting -> Posting
+ Hledger.Data.Transaction: transactionApplyCostValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Costing -> Maybe ValuationType -> Transaction -> Transaction
+ Hledger.Data.Transaction: transactionFile :: Transaction -> FilePath
+ Hledger.Data.Transaction: transactionMapPostingAmounts :: (Amount -> Amount) -> Transaction -> Transaction
+ Hledger.Data.Transaction: transactionMapPostings :: (Posting -> Posting) -> Transaction -> Transaction
+ Hledger.Data.Types: PayeeDeclarationInfo :: Text -> [Tag] -> PayeeDeclarationInfo
+ Hledger.Data.Types: [jdeclaredpayees] :: Journal -> [(Payee, PayeeDeclarationInfo)]
+ Hledger.Data.Types: [pdicomment] :: PayeeDeclarationInfo -> Text
+ Hledger.Data.Types: [pditags] :: PayeeDeclarationInfo -> [Tag]
+ Hledger.Data.Types: data PayeeDeclarationInfo
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.PayeeDeclarationInfo
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.PayeeDeclarationInfo
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.PayeeDeclarationInfo
+ Hledger.Data.Types: nullpayeedeclarationinfo :: PayeeDeclarationInfo
+ Hledger.Data.Types: type Payee = Text
+ Hledger.Data.Valuation: Cost :: Costing
+ Hledger.Data.Valuation: NoCost :: Costing
+ Hledger.Data.Valuation: data Costing
+ Hledger.Data.Valuation: instance GHC.Classes.Eq Hledger.Data.Valuation.Costing
+ Hledger.Data.Valuation: instance GHC.Show.Show Hledger.Data.Valuation.Costing
+ Hledger.Data.Valuation: mixedAmountApplyCostValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Day -> Costing -> Maybe ValuationType -> MixedAmount -> MixedAmount
+ Hledger.Query: matchesDescription :: Query -> Text -> Bool
+ Hledger.Query: matchesPayeeWIP :: Query -> Payee -> Bool
+ Hledger.Read.Common: journalCheckAccountsDeclared :: Journal -> Either String ()
+ Hledger.Read.Common: journalCheckCommoditiesDeclared :: Journal -> Either String ()
+ Hledger.Read.Common: journalCheckPayeesDeclared :: Journal -> Either String ()
+ Hledger.Reports.ReportOptions: BudgetReport :: ReportType
+ Hledger.Reports.ReportOptions: ChangeReport :: ReportType
+ Hledger.Reports.ReportOptions: ValueChangeReport :: ReportType
+ Hledger.Reports.ReportOptions: [cost_] :: ReportOpts -> Costing
+ Hledger.Reports.ReportOptions: [reporttype_] :: ReportOpts -> ReportType
+ Hledger.Reports.ReportOptions: balanceTypeOverride :: RawOpts -> Maybe BalanceType
+ Hledger.Reports.ReportOptions: data ReportType
+ Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.ReportType
+ Hledger.Reports.ReportOptions: instance GHC.Classes.Eq Hledger.Reports.ReportOptions.ReportType
+ Hledger.Reports.ReportOptions: instance GHC.Show.Show Hledger.Reports.ReportOptions.ReportType
+ Hledger.Reports.ReportOptions: reportSpanBothDates :: Journal -> ReportSpec -> DateSpan
+ Hledger.Utils.Color: bgColorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder
+ Hledger.Utils.Color: colorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder
+ Hledger.Utils.Regex: regexMatchText :: Regexp -> Text -> Bool
+ Hledger.Utils.String: chomp1 :: String -> String
+ Hledger.Utils.Text: WideBuilder :: !Builder -> !Int -> WideBuilder
+ Hledger.Utils.Text: [wbBuilder] :: WideBuilder -> !Builder
+ Hledger.Utils.Text: [wbWidth] :: WideBuilder -> !Int
+ Hledger.Utils.Text: data WideBuilder
+ Hledger.Utils.Text: formatText :: Bool -> Maybe Int -> Maybe Int -> Text -> Text
+ Hledger.Utils.Text: linesPrepend :: Text -> Text -> Text
+ Hledger.Utils.Text: linesPrepend2 :: Text -> Text -> Text -> Text
+ Hledger.Utils.Text: textChomp :: Text -> Text
+ Hledger.Utils.Text: textConcatBottomPadded :: [Text] -> Text
+ Hledger.Utils.Text: unlinesB :: [Builder] -> Builder
+ Hledger.Utils.Text: wbToText :: WideBuilder -> Text
+ Hledger.Utils.Text: wbUnpack :: WideBuilder -> String
+ Hledger.Utils.Text: wrap :: Text -> Text -> Text -> Text
+ Text.Tabular.AsciiWide: renderRowB :: TableOpts -> Header Cell -> Builder
+ Text.Tabular.AsciiWide: renderTableB :: TableOpts -> (rh -> Cell) -> (ch -> Cell) -> (a -> Cell) -> Table rh ch a -> Builder
+ Text.Tabular.AsciiWide: textCell :: Align -> Text -> Cell
- Hledger.Data.AccountName: escapeName :: AccountName -> String
+ Hledger.Data.AccountName: escapeName :: AccountName -> Text
- Hledger.Data.Dates: fixSmartDateStr :: Day -> Text -> String
+ Hledger.Data.Dates: fixSmartDateStr :: Day -> Text -> Text
- Hledger.Data.Dates: fixSmartDateStrEither :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) String
+ Hledger.Data.Dates: fixSmartDateStrEither :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) Text
- Hledger.Data.Dates: showDate :: Day -> String
+ Hledger.Data.Dates: showDate :: Day -> Text
- Hledger.Data.Dates: showDateSpan :: DateSpan -> String
+ Hledger.Data.Dates: showDateSpan :: DateSpan -> Text
- Hledger.Data.Dates: showDateSpanMonthAbbrev :: DateSpan -> String
+ Hledger.Data.Dates: showDateSpanMonthAbbrev :: DateSpan -> Text
- Hledger.Data.Period: showPeriod :: Period -> String
+ Hledger.Data.Period: showPeriod :: Period -> Text
- Hledger.Data.Period: showPeriodMonthAbbrev :: Period -> String
+ Hledger.Data.Period: showPeriodMonthAbbrev :: Period -> Text
- Hledger.Data.Posting: postingApplyValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> Posting -> ValuationType -> Posting
+ Hledger.Data.Posting: postingApplyValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> ValuationType -> Posting -> Posting
- Hledger.Data.Posting: showComment :: Text -> String
+ Hledger.Data.Posting: showComment :: Text -> Text
- Hledger.Data.StringFormat: BottomAligned :: Maybe Int -> [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: BottomAligned :: [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: FormatLiteral :: String -> StringFormatComponent
+ Hledger.Data.StringFormat: FormatLiteral :: Text -> StringFormatComponent
- Hledger.Data.StringFormat: OneLine :: Maybe Int -> [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: OneLine :: [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: TopAligned :: Maybe Int -> [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: TopAligned :: [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: defaultStringFormatStyle :: Maybe Int -> [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: defaultStringFormatStyle :: [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: parseStringFormat :: String -> Either String StringFormat
+ Hledger.Data.StringFormat: parseStringFormat :: Text -> Either String StringFormat
- Hledger.Data.Transaction: showAccountName :: Maybe Int -> PostingType -> AccountName -> String
+ Hledger.Data.Transaction: showAccountName :: Maybe Int -> PostingType -> AccountName -> Text
- Hledger.Data.Transaction: showPostingLines :: Posting -> [String]
+ Hledger.Data.Transaction: showPostingLines :: Posting -> [Text]
- Hledger.Data.Transaction: showTransaction :: Transaction -> String
+ Hledger.Data.Transaction: showTransaction :: Transaction -> Text
- Hledger.Data.Transaction: showTransactionOneLineAmounts :: Transaction -> String
+ Hledger.Data.Transaction: showTransactionOneLineAmounts :: Transaction -> Text
- Hledger.Data.Transaction: transactionApplyValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> Transaction -> ValuationType -> Transaction
+ Hledger.Data.Transaction: transactionApplyValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> ValuationType -> Transaction -> Transaction
- Hledger.Data.Types: Amount :: CommoditySymbol -> Quantity -> Bool -> AmountStyle -> Maybe AmountPrice -> Amount
+ Hledger.Data.Types: Amount :: !CommoditySymbol -> !Quantity -> !Bool -> !AmountStyle -> !Maybe AmountPrice -> Amount
- Hledger.Data.Types: AmountStyle :: Side -> Bool -> !AmountPrecision -> Maybe Char -> Maybe DigitGroupStyle -> AmountStyle
+ Hledger.Data.Types: AmountStyle :: !Side -> !Bool -> !AmountPrecision -> !Maybe Char -> !Maybe DigitGroupStyle -> AmountStyle
- Hledger.Data.Types: DigitGroups :: Char -> [Word8] -> DigitGroupStyle
+ Hledger.Data.Types: DigitGroups :: !Char -> ![Word8] -> DigitGroupStyle
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
- Hledger.Data.Types: TotalPrice :: Amount -> AmountPrice
+ Hledger.Data.Types: TotalPrice :: !Amount -> AmountPrice
- Hledger.Data.Types: UnitPrice :: Amount -> AmountPrice
+ Hledger.Data.Types: UnitPrice :: !Amount -> AmountPrice
- Hledger.Data.Types: [acommodity] :: Amount -> CommoditySymbol
+ Hledger.Data.Types: [acommodity] :: Amount -> !CommoditySymbol
- Hledger.Data.Types: [aismultiplier] :: Amount -> Bool
+ Hledger.Data.Types: [aismultiplier] :: Amount -> !Bool
- Hledger.Data.Types: [aprice] :: Amount -> Maybe AmountPrice
+ Hledger.Data.Types: [aprice] :: Amount -> !Maybe AmountPrice
- Hledger.Data.Types: [aquantity] :: Amount -> Quantity
+ Hledger.Data.Types: [aquantity] :: Amount -> !Quantity
- Hledger.Data.Types: [ascommodityside] :: AmountStyle -> Side
+ Hledger.Data.Types: [ascommodityside] :: AmountStyle -> !Side
- Hledger.Data.Types: [ascommodityspaced] :: AmountStyle -> Bool
+ Hledger.Data.Types: [ascommodityspaced] :: AmountStyle -> !Bool
- Hledger.Data.Types: [asdecimalpoint] :: AmountStyle -> Maybe Char
+ Hledger.Data.Types: [asdecimalpoint] :: AmountStyle -> !Maybe Char
- Hledger.Data.Types: [asdigitgroups] :: AmountStyle -> Maybe DigitGroupStyle
+ Hledger.Data.Types: [asdigitgroups] :: AmountStyle -> !Maybe DigitGroupStyle
- Hledger.Data.Types: [astyle] :: Amount -> AmountStyle
+ Hledger.Data.Types: [astyle] :: Amount -> !AmountStyle
- Hledger.Data.Valuation: mixedAmountApplyValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> ValuationType -> MixedAmount -> MixedAmount
+ Hledger.Data.Valuation: mixedAmountApplyValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Day -> ValuationType -> MixedAmount -> MixedAmount
- Hledger.Query: noteTag :: Maybe String -> Either RegexError Query
+ Hledger.Query: noteTag :: Maybe Text -> Either RegexError Query
- Hledger.Query: payeeTag :: Maybe String -> Either RegexError Query
+ Hledger.Query: payeeTag :: Maybe Text -> Either RegexError Query
- Hledger.Read.Common: priceamountp :: JournalParser m AmountPrice
+ Hledger.Read.Common: priceamountp :: Amount -> JournalParser m AmountPrice
- Hledger.Read.CsvReader: printCSV :: CSV -> String
+ Hledger.Read.CsvReader: printCSV :: CSV -> Text
- Hledger.Read.CsvReader: type CsvValue = String
+ Hledger.Read.CsvReader: type CsvValue = Text
- Hledger.Reports.AccountTransactionsReport: type AccountTransactionsReport = (String, [AccountTransactionsReportItem])
+ Hledger.Reports.AccountTransactionsReport: type AccountTransactionsReport = [AccountTransactionsReportItem]
- Hledger.Reports.AccountTransactionsReport: type AccountTransactionsReportItem = (Transaction, Transaction, Bool, String, MixedAmount, MixedAmount)
+ Hledger.Reports.AccountTransactionsReport: type AccountTransactionsReportItem = (Transaction, Transaction, Bool, Text, MixedAmount, MixedAmount)
- Hledger.Reports.BudgetReport: budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount)
+ Hledger.Reports.BudgetReport: budgetReportAsTable :: ReportOpts -> BudgetReport -> Table Text Text (Maybe MixedAmount, Maybe MixedAmount)
- Hledger.Reports.BudgetReport: budgetReportAsText :: ReportOpts -> BudgetReport -> String
+ Hledger.Reports.BudgetReport: budgetReportAsText :: ReportOpts -> BudgetReport -> Text
- Hledger.Reports.BudgetReport: reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> String
+ Hledger.Reports.BudgetReport: reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> Text
- Hledger.Reports.MultiBalanceReport: generateMultiBalanceReport :: ReportSpec -> Journal -> (Day -> MixedAmount -> MixedAmount) -> [DateSpan] -> Map DateSpan [Posting] -> HashMap AccountName Account -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReport: generateMultiBalanceReport :: ReportSpec -> Journal -> PriceOracle -> Map DateSpan [Posting] -> HashMap AccountName Account -> MultiBalanceReport
- Hledger.Reports.MultiBalanceReport: startingBalances :: ReportSpec -> Journal -> DateSpan -> HashMap AccountName Account
+ Hledger.Reports.MultiBalanceReport: startingBalances :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> HashMap AccountName Account
- Hledger.Reports.PostingsReport: type PostingsReport = (String, [PostingsReportItem])
+ Hledger.Reports.PostingsReport: type PostingsReport = [PostingsReportItem]
- Hledger.Reports.PostingsReport: type PostingsReportItem = (Maybe Day, Maybe Day, Maybe String, Posting, MixedAmount)
+ Hledger.Reports.PostingsReport: type PostingsReportItem = (Maybe Day, Maybe Day, Maybe Text, Posting, MixedAmount)
- Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> [Text] -> Bool -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Maybe DateSpan -> Bool -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Costing -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> [Text] -> Bool -> Bool -> Bool -> ReportType -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Maybe DateSpan -> Bool -> ReportOpts
- Hledger.Reports.ReportTypes: CBCSubreportSpec :: String -> (Journal -> Query) -> (ReportOpts -> ReportOpts) -> (PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount) -> Bool -> CBCSubreportSpec a
+ Hledger.Reports.ReportTypes: CBCSubreportSpec :: Text -> (Journal -> Query) -> (ReportOpts -> ReportOpts) -> (PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount) -> Bool -> CBCSubreportSpec a
- Hledger.Reports.ReportTypes: CompoundPeriodicReport :: String -> [DateSpan] -> [(String, PeriodicReport a b, Bool)] -> PeriodicReportRow () b -> CompoundPeriodicReport a b
+ Hledger.Reports.ReportTypes: CompoundPeriodicReport :: Text -> [DateSpan] -> [(Text, PeriodicReport a b, Bool)] -> PeriodicReportRow () b -> CompoundPeriodicReport a b
- Hledger.Reports.ReportTypes: [cbcsubreporttitle] :: CBCSubreportSpec a -> String
+ Hledger.Reports.ReportTypes: [cbcsubreporttitle] :: CBCSubreportSpec a -> Text
- Hledger.Reports.ReportTypes: [cbrSubreports] :: CompoundPeriodicReport a b -> [(String, PeriodicReport a b, Bool)]
+ Hledger.Reports.ReportTypes: [cbrSubreports] :: CompoundPeriodicReport a b -> [(Text, PeriodicReport a b, Bool)]
- Hledger.Reports.ReportTypes: [cbrTitle] :: CompoundPeriodicReport a b -> String
+ Hledger.Reports.ReportTypes: [cbrTitle] :: CompoundPeriodicReport a b -> Text
- Hledger.Reports.TransactionsReport: type TransactionsReport = (String, [TransactionsReportItem])
+ Hledger.Reports.TransactionsReport: type TransactionsReport = [TransactionsReportItem]
- Hledger.Reports.TransactionsReport: type TransactionsReportItem = (Transaction, Transaction, Bool, String, MixedAmount, MixedAmount)
+ Hledger.Reports.TransactionsReport: type TransactionsReportItem = (Transaction, Transaction, Bool, Text, MixedAmount, MixedAmount)
- Hledger.Utils.Debug: traceAtWith :: (a -> String) -> a -> a
+ Hledger.Utils.Debug: traceAtWith :: Int -> (a -> String) -> a -> a
- Hledger.Utils.Regex: toRegex :: String -> Either RegexError Regexp
+ Hledger.Utils.Regex: toRegex :: Text -> Either RegexError Regexp
- Hledger.Utils.Regex: toRegex' :: String -> Regexp
+ Hledger.Utils.Regex: toRegex' :: Text -> Regexp
- Hledger.Utils.Regex: toRegexCI :: String -> Either RegexError Regexp
+ Hledger.Utils.Regex: toRegexCI :: Text -> Either RegexError Regexp
- Hledger.Utils.Regex: toRegexCI' :: String -> Regexp
+ Hledger.Utils.Regex: toRegexCI' :: Text -> Regexp
- Text.Tabular.AsciiWide: Cell :: Align -> [(String, Int)] -> Cell
+ Text.Tabular.AsciiWide: Cell :: Align -> [WideBuilder] -> Cell
- Text.Tabular.AsciiWide: boxchar :: VPos -> HPos -> Properties -> Properties -> Bool -> String
+ Text.Tabular.AsciiWide: boxchar :: VPos -> HPos -> Properties -> Properties -> Bool -> Builder
- Text.Tabular.AsciiWide: doubleMidBar :: Bool -> Bool -> String
+ Text.Tabular.AsciiWide: doubleMidBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: leftBar :: Bool -> Bool -> String
+ Text.Tabular.AsciiWide: leftBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: lineart :: Properties -> Properties -> Properties -> Properties -> Bool -> String
+ Text.Tabular.AsciiWide: lineart :: Properties -> Properties -> Properties -> Properties -> Bool -> Builder
- Text.Tabular.AsciiWide: midBar :: Bool -> Bool -> String
+ Text.Tabular.AsciiWide: midBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: pick :: String -> String -> Bool -> String
+ Text.Tabular.AsciiWide: pick :: Text -> Text -> Bool -> Builder
- Text.Tabular.AsciiWide: render :: Bool -> (rh -> String) -> (ch -> String) -> (a -> String) -> Table rh ch a -> String
+ Text.Tabular.AsciiWide: render :: Bool -> (rh -> Text) -> (ch -> Text) -> (a -> Text) -> Table rh ch a -> Text
- Text.Tabular.AsciiWide: renderColumns :: TableOpts -> [Int] -> Header Cell -> String
+ Text.Tabular.AsciiWide: renderColumns :: TableOpts -> [Int] -> Header Cell -> Builder
- Text.Tabular.AsciiWide: renderHLine :: VPos -> Bool -> Bool -> [Int] -> Header a -> Properties -> [String]
+ Text.Tabular.AsciiWide: renderHLine :: VPos -> Bool -> Bool -> [Int] -> Header a -> Properties -> [Builder]
- Text.Tabular.AsciiWide: renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> String
+ Text.Tabular.AsciiWide: renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> Builder
- Text.Tabular.AsciiWide: renderRow :: TableOpts -> Header Cell -> String
+ Text.Tabular.AsciiWide: renderRow :: TableOpts -> Header Cell -> Text
- Text.Tabular.AsciiWide: renderTable :: TableOpts -> (rh -> Cell) -> (ch -> Cell) -> (a -> Cell) -> Table rh ch a -> String
+ Text.Tabular.AsciiWide: renderTable :: TableOpts -> (rh -> Cell) -> (ch -> Cell) -> (a -> Cell) -> Table rh ch a -> Text
- Text.Tabular.AsciiWide: rightBar :: Bool -> Bool -> String
+ Text.Tabular.AsciiWide: rightBar :: Bool -> Bool -> Builder
Files
- CHANGES.md +91/−0
- Hledger/Data/Account.hs +4/−4
- Hledger/Data/AccountName.hs +7/−7
- Hledger/Data/Amount.hs +283/−231
- Hledger/Data/Dates.hs +8/−8
- Hledger/Data/Journal.hs +220/−100
- Hledger/Data/Json.hs +3/−2
- Hledger/Data/Period.hs +15/−9
- Hledger/Data/PeriodicTransaction.hs +3/−2
- Hledger/Data/Posting.hs +27/−35
- Hledger/Data/StringFormat.hs +43/−48
- Hledger/Data/Timeclock.hs +13/−8
- Hledger/Data/Transaction.hs +135/−98
- Hledger/Data/TransactionModifier.hs +6/−5
- Hledger/Data/Types.hs +31/−13
- Hledger/Data/Valuation.hs +51/−42
- Hledger/Query.hs +55/−26
- Hledger/Read.hs +17/−13
- Hledger/Read/Common.hs +52/−18
- Hledger/Read/CsvReader.hs +175/−159
- Hledger/Read/JournalReader.hs +29/−3
- Hledger/Read/TimedotReader.hs +1/−1
- Hledger/Reports/AccountTransactionsReport.hs +11/−20
- Hledger/Reports/BudgetReport.hs +47/−44
- Hledger/Reports/EntriesReport.hs +2/−6
- Hledger/Reports/MultiBalanceReport.hs +96/−120
- Hledger/Reports/PostingsReport.hs +26/−57
- Hledger/Reports/ReportOptions.hs +117/−66
- Hledger/Reports/ReportTypes.hs +6/−5
- Hledger/Reports/TransactionsReport.hs +9/−15
- Hledger/Utils/Color.hs +18/−0
- Hledger/Utils/Debug.hs +20/−3
- Hledger/Utils/Regex.hs +27/−13
- Hledger/Utils/String.hs +15/−11
- Hledger/Utils/Text.hs +60/−46
- Text/Tabular/AsciiWide.hs +96/−78
- Text/WideString.hs +36/−1
- hledger-lib.cabal +2/−17
- hledger_csv.5 +0/−1300
- hledger_csv.info +0/−1352
- hledger_csv.txt +0/−962
- hledger_journal.5 +0/−2153
- hledger_journal.info +0/−2217
- hledger_journal.txt +0/−1597
- hledger_timeclock.5 +0/−90
- hledger_timeclock.info +0/−67
- hledger_timeclock.txt +0/−80
- hledger_timedot.5 +0/−199
- hledger_timedot.info +0/−156
- hledger_timedot.txt +0/−163
CHANGES.md view
@@ -1,6 +1,97 @@ Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. +# 1.21 2021-03-10++- Building Hledger.Data.Journal no longer fails if the monad-extras+ package is installed.++- Many parts of the hledger-lib and hledger APIs have become more+ Text-ified, expecting or returning Text instead of String, reducing+ hledger's time and resident memory requirements by roughly 10%.+ Some functions now use WideBuilder (a text "builder" which keeps track+ of width), to concatenate text more efficiently. There are some+ helpers for converting to and from WideBuilder (wbUnpack, wbToText..)+ showAmountB/showMixedAmountB are new amount-displaying functions+ taking an AmountDisplayOpts. These will probably replace the old+ show(Mixed)Amount* functions. (#1427, Stephen Morgan)++- AtThen valuation is now implemented for all report types.+ amountApplyValuation now takes the posting date as an argument.+ (transaction/posting)ApplyValuation's valuation type and+ transaction/posting arguments have been reordered like+ amountApplyValuation's. (Stephen Morgan)++- Amount, AmountPrice, AmountStyle, DigitGroupStyle fields are now+ strict. (Stephen Morgan)++- Amount prices are now stored with their sign, so negative prices can+ be represented. (They seem to have always worked, but now the+ internal representation is more accurate.) (Stephen Morgan)+ +- normaliseMixedAmount now combines Amounts with TotalPrices in the+ same commodity. (Stephen Morgan)++- normaliseMixedAmount now uses a strict Map for combining amounts+ internally, closing a big space leak. (Stephen Morgan)++- (multiply|divide)(Mixed)?Amount now also multiply or divide the+ TotalPrice if it is present, and the old+ (multiply|divide)(Mixed)?AmountAndPrice functions are removed. (Stephen Morgan)++- (amount|mixedAmount)(Looks|Is)Zero functions now check whether both+ the quantity and the cost are zero. This is usually what you want,+ but if you do only want to check whether the quantity is zero, you+ can run mixedAmountStripPrices (or similar) before this. (Stephen Morgan)++- commodityStylesFromAmounts now consumes the list immediately,+ reducing the maximum heap size per thread from ~850K to ~430K in a+ real-world register report. (Stephen Morgan)++- *ApplyValuation functions take two less arguments, and+ *ApplyCostValuation functions have been added, performing both+ costing and valuation. (Stephen Morgan)++- traceAtWith now has a level argument and works properly.++- API changes include:+ ```+ Hledger.Data.Amount:+ setAmountPrecision -> amountSetPrecision+ setFullPrecision -> amountSetFullPrecision+ setMixedAmountPrecision -> mixedAmountSetPrecision+ showMixed -> showMixedAmountB+ showMixedLines -> showMixedAmountLinesB+ -mixedAmountSetFullPrecision++ Hledger.Data.Journal:+ mapJournalTransactions -> journalMapTransactions+ mapJournalPostings -> journalMapPostings+ -mapTransactionPostings+ +journalPayeesUsed+ +journalPayeesDeclaredOrUsed++ Hledger.Data.Transaction:+ +transactionFile+ +transactionMapPostings++ Hledger.Data.Valuation:+ -valuationTypeIsCost+ -valuationTypeIsDefaultValue+ -ValuationType's AtDefault constructor++ Hledger.Query:+ +matchesDescription+ +matchesPayeeWIP++ Hledger.Utils.Text:+ +textConcatBottomPadded+ +wbToText+ +wbUnpack++ Text.Tabular.AsciiWide:+ alignCell -> textCell+ ``` # 1.20.4 2021-01-29 - See hledger.
Hledger/Data/Account.hs view
@@ -30,8 +30,8 @@ aname (if aboring then "y" else "n" :: String) anumpostings- (showMixedAmount aebalance)- (showMixedAmount aibalance)+ (wbUnpack $ showMixedAmountB noColour aebalance)+ (wbUnpack $ showMixedAmountB noColour aibalance) instance Eq Account where (==) a b = aname a == aname b -- quick equality test for speed@@ -265,6 +265,6 @@ showAccountDebug a = printf "%-25s %4s %4s %s" (aname a)- (showMixedAmount $ aebalance a)- (showMixedAmount $ aibalance a)+ (wbUnpack . showMixedAmountB noColour $ aebalance a)+ (wbUnpack . showMixedAmountB noColour $ aibalance a) (if aboring a then "b" else " " :: String)
Hledger/Data/AccountName.hs view
@@ -208,31 +208,31 @@ clipOrEllipsifyAccountName n = clipAccountName n -- | Escape an AccountName for use within a regular expression.--- >>> putStr $ escapeName "First?!#$*?$(*) !@^#*? %)*!@#"+-- >>> putStr . T.unpack $ escapeName "First?!#$*?$(*) !@^#*? %)*!@#" -- First\?!#\$\*\?\$\(\*\) !@\^#\*\? %\)\*!@#-escapeName :: AccountName -> String-escapeName = T.unpack . T.concatMap escapeChar+escapeName :: AccountName -> Text+escapeName = T.concatMap escapeChar where escapeChar c = if c `elem` escapedChars then T.snoc "\\" c else T.singleton c escapedChars = ['[', '?', '+', '|', '(', ')', '*', '$', '^', '\\'] -- | Convert an account name to a regular expression matching it and its subaccounts. accountNameToAccountRegex :: AccountName -> Regexp-accountNameToAccountRegex a = toRegex' $ '^' : escapeName a ++ "(:|$)" -- PARTIAL: Is this safe after escapeName?+accountNameToAccountRegex a = toRegex' $ "^" <> escapeName a <> "(:|$)" -- PARTIAL: Is this safe after escapeName? -- | Convert an account name to a regular expression matching it and its subaccounts, -- case insensitively. accountNameToAccountRegexCI :: AccountName -> Regexp-accountNameToAccountRegexCI a = toRegexCI' $ '^' : escapeName a ++ "(:|$)" -- PARTIAL: Is this safe after escapeName?+accountNameToAccountRegexCI a = toRegexCI' $ "^" <> escapeName a <> "(:|$)" -- PARTIAL: Is this safe after escapeName? -- | Convert an account name to a regular expression matching it but not its subaccounts. accountNameToAccountOnlyRegex :: AccountName -> Regexp-accountNameToAccountOnlyRegex a = toRegex' $ '^' : escapeName a ++ "$" -- PARTIAL: Is this safe after escapeName?+accountNameToAccountOnlyRegex a = toRegex' $ "^" <> escapeName a <> "$" -- PARTIAL: Is this safe after escapeName? -- | Convert an account name to a regular expression matching it but not its subaccounts, -- case insensitively. accountNameToAccountOnlyRegexCI :: AccountName -> Regexp-accountNameToAccountOnlyRegexCI a = toRegexCI' $ '^' : escapeName a ++ "$" -- PARTIAL: Is this safe after escapeName?+accountNameToAccountOnlyRegexCI a = toRegexCI' $ "^" <> escapeName a <> "$" -- PARTIAL: Is this safe after escapeName? -- -- | Does this string look like an exact account-matching regular expression ? --isAccountRegex :: String -> Bool
Hledger/Data/Amount.hs view
@@ -40,7 +40,11 @@ -} -{-# LANGUAGE StandaloneDeriving, RecordWildCards, OverloadedStrings #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE StandaloneDeriving #-} module Hledger.Data.Amount ( -- * Amount@@ -62,22 +66,25 @@ amountLooksZero, divideAmount, multiplyAmount,- divideAmountAndPrice,- multiplyAmountAndPrice, amountTotalPriceToUnitPrice, -- ** rendering+ AmountDisplayOpts(..),+ noColour,+ noPrice,+ oneLine, amountstyle, styleAmount, styleAmountExceptPrecision, amountUnstyled,+ showAmountB, showAmount, cshowAmount, showAmountWithZeroCommodity, showAmountDebug, showAmountWithoutPrice,- setAmountPrecision,+ amountSetPrecision, withPrecision,- setFullPrecision,+ amountSetFullPrecision, setAmountInternalPrecision, withInternalPrecision, setAmountDecimalPoint,@@ -99,8 +106,6 @@ mixedAmountCost, divideMixedAmount, multiplyMixedAmount,- divideMixedAmountAndPrice,- multiplyMixedAmountAndPrice, averageMixedAmounts, isNegativeAmount, isNegativeMixedAmount,@@ -117,12 +122,12 @@ showMixedAmountOneLineWithoutPrice, showMixedAmountElided, showMixedAmountWithZeroCommodity,- showMixedAmountWithPrecision,- showMixed,- showMixedUnnormalised,- showMixedOneLine,- showMixedOneLineUnnormalised,- setMixedAmountPrecision,+ showMixedAmountB,+ showMixedAmountLinesB,+ wbToText,+ wbUnpack,+ mixedAmountSetPrecision,+ mixedAmountSetFullPrecision, canonicaliseMixedAmount, -- * misc. ltraceamount,@@ -130,17 +135,20 @@ ) where import Control.Monad (foldM)-import Data.Char (isDigit)-import Data.Decimal (decimalPlaces, normalizeDecimal, roundTo)-import Data.Function (on)-import Data.List (genericSplitAt, groupBy, intercalate, mapAccumL,- partition, sortBy)-import qualified Data.Map as M-import Data.Map (findWithDefault)+import Data.Decimal (DecimalRaw(..), decimalPlaces, normalizeDecimal, roundTo)+import Data.Default (Default(..))+import Data.Foldable (toList)+import Data.List (intercalate, intersperse, mapAccumL, partition)+import Data.List.NonEmpty (NonEmpty(..), nonEmpty)+import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe)+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif import qualified Data.Text as T+import qualified Data.Text.Lazy.Builder as TB import Data.Word (Word8)-import Safe (lastDef, lastMay)+import Safe (headDef, lastDef, lastMay) import Text.Printf (printf) import Hledger.Data.Types@@ -150,13 +158,45 @@ deriving instance Show MarketPrice +-- | Options for the display of Amount and MixedAmount.+data AmountDisplayOpts = AmountDisplayOpts+ { displayPrice :: Bool -- ^ Whether to display the Price of an Amount.+ , displayZeroCommodity :: Bool -- ^ If the Amount rounds to 0, whether to display its commodity string.+ , displayColour :: Bool -- ^ Whether to colourise negative Amounts.+ , displayNormalised :: Bool -- ^ Whether to normalise MixedAmounts before displaying.+ , displayOneLine :: Bool -- ^ Whether to display on one line.+ , displayMinWidth :: Maybe Int -- ^ Minimum width to pad to+ , displayMaxWidth :: Maybe Int -- ^ Maximum width to clip to+ } deriving (Show)++-- | Display Amount and MixedAmount with no colour.+instance Default AmountDisplayOpts where def = noColour++-- | Display Amount and MixedAmount with no colour.+noColour :: AmountDisplayOpts+noColour = AmountDisplayOpts { displayPrice = True+ , displayColour = False+ , displayZeroCommodity = False+ , displayNormalised = True+ , displayOneLine = False+ , displayMinWidth = Nothing+ , displayMaxWidth = Nothing+ }++-- | Display Amount and MixedAmount with no prices.+noPrice :: AmountDisplayOpts+noPrice = def{displayPrice=False}++-- | Display Amount and MixedAmount on one line with no prices.+oneLine :: AmountDisplayOpts+oneLine = def{displayOneLine=True, displayPrice=False}+ ------------------------------------------------------------------------------- -- Amount styles -- | Default amount style amountstyle = AmountStyle L False (Precision 0) (Just '.') Nothing - ------------------------------------------------------------------------------- -- Amount @@ -164,7 +204,7 @@ abs a@Amount{aquantity=q} = a{aquantity=abs q} signum a@Amount{aquantity=q} = a{aquantity=signum q} fromInteger i = nullamt{aquantity=fromInteger i}- negate a@Amount{aquantity=q} = a{aquantity= -q}+ negate a = transformAmount negate a (+) = similarAmountsOp (+) (-) = similarAmountsOp (-) (*) = similarAmountsOp (*)@@ -197,8 +237,8 @@ -- Prices are ignored and discarded. -- Remember: the caller is responsible for ensuring both amounts have the same commodity. similarAmountsOp :: (Quantity -> Quantity -> Quantity) -> Amount -> Amount -> Amount-similarAmountsOp op Amount{acommodity=_, aquantity=q1, astyle=AmountStyle{asprecision=p1}}- Amount{acommodity=c2, aquantity=q2, astyle=s2@AmountStyle{asprecision=p2}} =+similarAmountsOp op !Amount{acommodity=_, aquantity=q1, astyle=AmountStyle{asprecision=p1}}+ !Amount{acommodity=c2, aquantity=q2, astyle=s2@AmountStyle{asprecision=p2}} = -- trace ("a1:"++showAmountDebug a1) $ trace ("a2:"++showAmountDebug a2) $ traceWith (("= :"++).showAmountDebug) amount{acommodity=c2, aquantity=q1 `op` q2, astyle=s2{asprecision=max p1 p2}} -- c1==c2 || q1==0 || q2==0 =@@ -215,14 +255,14 @@ -- - price amounts must be MixedAmounts with exactly one component Amount -- (or there will be a runtime error XXX) ----- - price amounts should be positive+-- - price amounts should be positive in the Journal -- (though this is currently not enforced) amountCost :: Amount -> Amount amountCost a@Amount{aquantity=q, aprice=mp} = case mp of Nothing -> a Just (UnitPrice p@Amount{aquantity=pq}) -> p{aquantity=pq * q}- Just (TotalPrice p@Amount{aquantity=pq}) -> p{aquantity=pq * signum q}+ Just (TotalPrice p@Amount{aquantity=pq}) -> p{aquantity=pq} -- | Replace an amount's TotalPrice, if it has one, with an equivalent UnitPrice. -- Has no effect on amounts without one.@@ -240,29 +280,20 @@ Precision p -> Precision $ if p == maxBound then maxBound else p + 1 amountTotalPriceToUnitPrice a = a --- | Divide an amount's quantity by a constant.-divideAmount :: Quantity -> Amount -> Amount-divideAmount n a@Amount{aquantity=q} = a{aquantity=q/n}---- | Multiply an amount's quantity by a constant.-multiplyAmount :: Quantity -> Amount -> Amount-multiplyAmount n a@Amount{aquantity=q} = a{aquantity=q*n}+-- | Apply a function to an amount's quantity (and its total price, if it has one).+transformAmount :: (Quantity -> Quantity) -> Amount -> Amount+transformAmount f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p}+ where+ f' (TotalPrice a@Amount{aquantity=pq}) = TotalPrice a{aquantity = f pq}+ f' p = p -- | Divide an amount's quantity (and its total price, if it has one) by a constant.--- The total price will be kept positive regardless of the multiplier's sign.-divideAmountAndPrice :: Quantity -> Amount -> Amount-divideAmountAndPrice n a@Amount{aquantity=q,aprice=p} = a{aquantity=q/n, aprice=f <$> p}- where- f (TotalPrice a) = TotalPrice $ abs $ n `divideAmount` a- f p = p+divideAmount :: Quantity -> Amount -> Amount+divideAmount n = transformAmount (/n) -- | Multiply an amount's quantity (and its total price, if it has one) by a constant.--- The total price will be kept positive regardless of the multiplier's sign.-multiplyAmountAndPrice :: Quantity -> Amount -> Amount-multiplyAmountAndPrice n a@Amount{aquantity=q,aprice=p} = a{aquantity=q*n, aprice=f <$> p}- where- f (TotalPrice a) = TotalPrice $ abs $ n `multiplyAmount` a- f p = p+multiplyAmount :: Quantity -> Amount -> Amount+multiplyAmount n = transformAmount (*n) -- | Is this amount negative ? The price is ignored. isNegativeAmount :: Amount -> Bool@@ -275,28 +306,34 @@ NaturalPrecision -> q Precision p' -> roundTo p' q --- | Does mixed amount appear to be zero when rendered with its+-- | Apply a test to both an Amount and its total price, if it has one.+testAmountAndTotalPrice :: (Amount -> Bool) -> Amount -> Bool+testAmountAndTotalPrice f amt = case aprice amt of+ Just (TotalPrice price) -> f amt && f price+ _ -> f amt++-- | Do this Amount and (and its total price, if it has one) appear to be zero when rendered with its -- display precision ? amountLooksZero :: Amount -> Bool-amountLooksZero = (0==) . amountRoundedQuantity+amountLooksZero = testAmountAndTotalPrice ((0==) . amountRoundedQuantity) --- | Is this amount exactly zero, ignoring its display precision ?+-- | Is this Amount (and its total price, if it has one) exactly zero, ignoring its display precision ? amountIsZero :: Amount -> Bool-amountIsZero Amount{aquantity=q} = q == 0+amountIsZero = testAmountAndTotalPrice ((0==) . aquantity) -- | Set an amount's display precision, flipped. withPrecision :: Amount -> AmountPrecision -> Amount-withPrecision = flip setAmountPrecision+withPrecision = flip amountSetPrecision -- | Set an amount's display precision.-setAmountPrecision :: AmountPrecision -> Amount -> Amount-setAmountPrecision p a@Amount{astyle=s} = a{astyle=s{asprecision=p}}+amountSetPrecision :: AmountPrecision -> Amount -> Amount+amountSetPrecision p a@Amount{astyle=s} = a{astyle=s{asprecision=p}} -- | Increase an amount's display precision, if needed, to enough decimal places -- to show it exactly (showing all significant decimal digits, excluding trailing -- zeros).-setFullPrecision :: Amount -> Amount-setFullPrecision a = setAmountPrecision p a+amountSetFullPrecision :: Amount -> Amount+amountSetFullPrecision a = amountSetPrecision p a where p = max displayprecision naturalprecision displayprecision = asprecision $ astyle a@@ -327,10 +364,12 @@ withDecimalPoint :: Amount -> Maybe Char -> Amount withDecimalPoint = flip setAmountDecimalPoint -showAmountPrice :: Maybe AmountPrice -> String-showAmountPrice Nothing = ""-showAmountPrice (Just (UnitPrice pa)) = " @ " ++ showAmount pa-showAmountPrice (Just (TotalPrice pa)) = " @@ " ++ showAmount pa+showAmountPrice :: Amount -> WideBuilder+showAmountPrice amt = case aprice amt of+ Nothing -> mempty+ Just (UnitPrice pa) -> WideBuilder (TB.fromString " @ ") 3 <> showAmountB noColour pa+ Just (TotalPrice pa) -> WideBuilder (TB.fromString " @@ ") 4 <> showAmountB noColour (sign pa)+ where sign = if aquantity amt < 0 then negate else id showAmountPriceDebug :: Maybe AmountPrice -> String showAmountPriceDebug Nothing = ""@@ -361,40 +400,49 @@ -- commodity's display settings. String representations equivalent to -- zero are converted to just \"0\". The special "missing" amount is -- displayed as the empty string.+--+-- > showAmount = wbUnpack . showAmountB noColour showAmount :: Amount -> String-showAmount = showAmountHelper False+showAmount = wbUnpack . showAmountB noColour +-- | General function to generate a WideBuilder for an Amount, according the+-- supplied AmountDisplayOpts. The special "missing" amount is displayed as+-- the empty string. This is the main function to use for showing+-- Amounts, constructing a builder; it can then be converted to a Text with+-- wbToText, or to a String with wbUnpack.+showAmountB :: AmountDisplayOpts -> Amount -> WideBuilder+showAmountB _ Amount{acommodity="AUTO"} = mempty+showAmountB opts a@Amount{astyle=style} =+ color $ case ascommodityside style of+ L -> c' <> space <> quantity' <> price+ R -> quantity' <> space <> c' <> price+ where+ quantity = showamountquantity a+ (quantity',c) | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1,"")+ | otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a)+ space = if not (T.null c) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty+ c' = WideBuilder (TB.fromText c) (textWidth c)+ price = if displayPrice opts then showAmountPrice a else mempty+ color = if displayColour opts && isNegativeAmount a then colorB Dull Red else id+ -- | Colour version. For a negative amount, adds ANSI codes to change the colour, -- currently to hard-coded red.+--+-- > cshowAmount = wbUnpack . showAmountB def{displayColour=True} cshowAmount :: Amount -> String-cshowAmount a = (if isNegativeAmount a then color Dull Red else id) $- showAmountHelper False a+cshowAmount = wbUnpack . showAmountB def{displayColour=True} -- | Get the string representation of an amount, without any \@ price.+--+-- > showAmountWithoutPrice = wbUnpack . showAmountB noPrice showAmountWithoutPrice :: Amount -> String-showAmountWithoutPrice a = showAmount a{aprice=Nothing}---- | Get the string representation of an amount, based on its commodity's--- display settings except using the specified precision.-showAmountWithPrecision :: AmountPrecision -> Amount -> String-showAmountWithPrecision p = showAmount . setAmountPrecision p--showAmountHelper :: Bool -> Amount -> String-showAmountHelper _ Amount{acommodity="AUTO"} = ""-showAmountHelper showzerocommodity a@Amount{acommodity=c, aprice=mp, astyle=AmountStyle{..}} =- case ascommodityside of- L -> printf "%s%s%s%s" (T.unpack c') space quantity' price- R -> printf "%s%s%s%s" quantity' space (T.unpack c') price- where- quantity = showamountquantity a- (quantity',c') | amountLooksZero a && not showzerocommodity = ("0","")- | otherwise = (quantity, quoteCommoditySymbolIfNeeded c)- space = if not (T.null c') && ascommodityspaced then " " else "" :: String- price = showAmountPrice mp+showAmountWithoutPrice = wbUnpack . showAmountB noPrice -- | Like showAmount, but show a zero amount's commodity if it has one.+--+-- > showAmountWithZeroCommodity = wbUnpack . showAmountB noColour{displayZeryCommodity=True} showAmountWithZeroCommodity :: Amount -> String-showAmountWithZeroCommodity = showAmountHelper True+showAmountWithZeroCommodity = wbUnpack . showAmountB noColour{displayZeroCommodity=True} -- | Get a string representation of an amount for debugging, -- appropriate to the current debug level. 9 shows maximum detail.@@ -402,42 +450,46 @@ showAmountDebug Amount{acommodity="AUTO"} = "(missing)" showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice=%s, astyle=%s}" (show acommodity) (show aquantity) (showAmountPriceDebug aprice) (show astyle) --- | Get the string representation of the number part of of an amount,--- using the display settings from its commodity.-showamountquantity :: Amount -> String+-- | Get a Text Builder for the string representation of the number part of of an amount,+-- using the display settings from its commodity. Also returns the width of the+-- number.+showamountquantity :: Amount -> WideBuilder showamountquantity amt@Amount{astyle=AmountStyle{asdecimalpoint=mdec, asdigitgroups=mgrps}} =- punctuatenumber (fromMaybe '.' mdec) mgrps . show $ amountRoundedQuantity amt+ signB <> intB <> fracB+ where+ Decimal e n = amountRoundedQuantity amt --- | Replace a number string's decimal mark with the specified--- character, and add the specified digit group marks. The last digit--- group will be repeated as needed.-punctuatenumber :: Char -> Maybe DigitGroupStyle -> String -> String-punctuatenumber dec mgrps s = sign ++ reverse (applyDigitGroupStyle mgrps (reverse int)) ++ frac''- where- (sign,num) = break isDigit s- (int,frac) = break (=='.') num- frac' = dropWhile (=='.') frac- frac'' | null frac' = ""- | otherwise = dec:frac'+ strN = T.pack . show $ abs n+ len = T.length strN+ intLen = max 1 $ len - fromIntegral e+ dec = fromMaybe '.' mdec+ padded = T.replicate (fromIntegral e + 1 - len) "0" <> strN+ (intPart, fracPart) = T.splitAt intLen padded -applyDigitGroupStyle :: Maybe DigitGroupStyle -> String -> String-applyDigitGroupStyle Nothing s = s-applyDigitGroupStyle (Just (DigitGroups c gs)) s = addseps (repeatLast gs) s+ intB = applyDigitGroupStyle mgrps intLen $ if e == 0 then strN else intPart+ signB = if n < 0 then WideBuilder (TB.singleton '-') 1 else mempty+ fracB = if e > 0 then WideBuilder (TB.singleton dec <> TB.fromText fracPart) (fromIntegral e + 1) else mempty++-- | Split a string representation into chunks according to DigitGroupStyle,+-- returning a Text builder and the number of separators used.+applyDigitGroupStyle :: Maybe DigitGroupStyle -> Int -> T.Text -> WideBuilder+applyDigitGroupStyle Nothing l s = WideBuilder (TB.fromText s) l+applyDigitGroupStyle (Just (DigitGroups _ [])) l s = WideBuilder (TB.fromText s) l+applyDigitGroupStyle (Just (DigitGroups c (g:gs))) l s = addseps (g:|gs) (toInteger l) s where- addseps [] s = s- addseps (g:gs) s- | toInteger (length s) <= toInteger g = s- | otherwise = let (part,rest) = genericSplitAt g s- in part ++ c : addseps gs rest- repeatLast [] = []- repeatLast gs = init gs ++ repeat (last gs)+ addseps (g:|gs) l s+ | l' > 0 = addseps gs' l' rest <> WideBuilder (TB.singleton c <> TB.fromText part) (fromIntegral g + 1)+ | otherwise = WideBuilder (TB.fromText s) (fromInteger l)+ where+ (rest, part) = T.splitAt (fromInteger l') s+ gs' = fromMaybe (g:|[]) $ nonEmpty gs+ l' = l - toInteger g -- like journalCanonicaliseAmounts -- | Canonicalise an amount's display style using the provided commodity style map. canonicaliseAmount :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount canonicaliseAmount styles a@Amount{acommodity=c, astyle=s} = a{astyle=s'}- where- s' = findWithDefault s c styles+ where s' = M.findWithDefault s c styles ------------------------------------------------------------------------------- -- MixedAmount@@ -479,24 +531,18 @@ normaliseHelper :: Bool -> MixedAmount -> MixedAmount normaliseHelper squashprices (Mixed as)- | missingamt `elem` as = missingmixedamt -- missingamt should always be alone, but detect it even if not- | null nonzeros = Mixed [newzero]- | otherwise = Mixed nonzeros+ | missingkey `M.member` amtMap = missingmixedamt -- missingamt should always be alone, but detect it even if not+ | M.null nonzeros= Mixed [newzero]+ | otherwise = Mixed $ toList nonzeros where- newzero = lastDef nullamt $ filter (not . T.null . acommodity) zeros- (zeros, nonzeros) = partition amountIsZero $- map sumSimilarAmountsUsingFirstPrice $- groupBy groupfn $- sortBy sortfn- as- sortfn | squashprices = compare `on` acommodity- | otherwise = compare `on` \a -> (acommodity a, aprice a)- groupfn | squashprices = (==) `on` acommodity- | otherwise = \a1 a2 -> acommodity a1 == acommodity a2 && combinableprices a1 a2-- combinableprices Amount{aprice=Nothing} Amount{aprice=Nothing} = True- combinableprices Amount{aprice=Just (UnitPrice p1)} Amount{aprice=Just (UnitPrice p2)} = p1 == p2- combinableprices _ _ = False+ newzero = maybe nullamt snd . M.lookupMin $ M.filter (not . T.null . acommodity) zeros+ (zeros, nonzeros) = M.partition amountIsZero amtMap+ amtMap = foldr (\a -> M.insertWith sumSimilarAmountsUsingFirstPrice (key a) a) mempty as+ key Amount{acommodity=c,aprice=p} = (c, if squashprices then Nothing else priceKey <$> p)+ where+ priceKey (UnitPrice x) = (acommodity x, Just $ aquantity x)+ priceKey (TotalPrice x) = (acommodity x, Nothing)+ missingkey = key missingamt -- | Like normaliseMixedAmount, but combine each commodity's amounts -- into just one by throwing away all prices except the first. This is@@ -520,9 +566,13 @@ -- | Sum same-commodity amounts in a lossy way, applying the first -- price to the result and discarding any other prices. Only used as a -- rendering helper.-sumSimilarAmountsUsingFirstPrice :: [Amount] -> Amount-sumSimilarAmountsUsingFirstPrice [] = nullamt-sumSimilarAmountsUsingFirstPrice as = (sumStrict as){aprice=aprice $ head as}+sumSimilarAmountsUsingFirstPrice :: Amount -> Amount -> Amount+sumSimilarAmountsUsingFirstPrice a b = (a + b){aprice=p}+ where+ p = case (aprice a, aprice b) of+ (Just (TotalPrice ap), Just (TotalPrice bp))+ -> Just . TotalPrice $ ap{aquantity = aquantity ap + aquantity bp }+ _ -> aprice a -- -- | Sum same-commodity amounts. If there were different prices, set -- -- the price to a special marker indicating "various". Only used as a@@ -557,26 +607,16 @@ -- | Convert all component amounts to cost/selling price where -- possible (see amountCost). mixedAmountCost :: MixedAmount -> MixedAmount-mixedAmountCost (Mixed as) = Mixed $ map amountCost as+mixedAmountCost = mapMixedAmount amountCost --- | Divide a mixed amount's quantities by a constant.+-- | Divide a mixed amount's quantities (and total prices, if any) by a constant. divideMixedAmount :: Quantity -> MixedAmount -> MixedAmount divideMixedAmount n = mapMixedAmount (divideAmount n) --- | Multiply a mixed amount's quantities by a constant.+-- | Multiply a mixed amount's quantities (and total prices, if any) by a constant. multiplyMixedAmount :: Quantity -> MixedAmount -> MixedAmount multiplyMixedAmount n = mapMixedAmount (multiplyAmount n) --- | Divide a mixed amount's quantities (and total prices, if any) by a constant.--- The total prices will be kept positive regardless of the multiplier's sign.-divideMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount-divideMixedAmountAndPrice n = mapMixedAmount (divideAmountAndPrice n)---- | Multiply a mixed amount's quantities (and total prices, if any) by a constant.--- The total prices will be kept positive regardless of the multiplier's sign.-multiplyMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount-multiplyMixedAmountAndPrice n = mapMixedAmount (multiplyAmountAndPrice n)- -- | Calculate the average of some mixed amounts. averageMixedAmounts :: [MixedAmount] -> MixedAmount averageMixedAmounts [] = 0@@ -593,12 +633,15 @@ as | not (any isNegativeAmount as) -> Just False _ -> Nothing -- multiple amounts with different signs --- | Does this mixed amount appear to be zero when rendered with its--- display precision ?+-- | Does this mixed amount appear to be zero when rendered with its display precision?+-- i.e. does it have zero quantity with no price, zero quantity with a total price (which is also zero),+-- and zero quantity for each unit price? mixedAmountLooksZero :: MixedAmount -> Bool mixedAmountLooksZero = all amountLooksZero . amounts . normaliseMixedAmountSquashPricesForDisplay --- | Is this mixed amount exactly zero, ignoring display precisions ?+-- | Is this mixed amount exactly to be zero, ignoring its display precision?+-- i.e. does it have zero quantity with no price, zero quantity with a total price (which is also zero),+-- and zero quantity for each unit price? mixedAmountIsZero :: MixedAmount -> Bool mixedAmountIsZero = all amountIsZero . amounts . normaliseMixedAmountSquashPricesForDisplay @@ -613,7 +656,7 @@ -- | Given a map of standard commodity display styles, apply the -- appropriate one to each individual amount. styleMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount-styleMixedAmount styles (Mixed as) = Mixed $ map (styleAmount styles) as+styleMixedAmount styles = mapMixedAmount (styleAmount styles) -- | Reset each individual amount's display style to the default. mixedAmountUnstyled :: MixedAmount -> MixedAmount@@ -622,40 +665,46 @@ -- | Get the string representation of a mixed amount, after -- normalising it to one amount per commodity. Assumes amounts have -- no or similar prices, otherwise this can show misleading prices.+--+-- > showMixedAmount = wbUnpack . showMixedAmountB noColour showMixedAmount :: MixedAmount -> String-showMixedAmount = fst . showMixed showAmount Nothing Nothing False+showMixedAmount = wbUnpack . showMixedAmountB noColour -- | Get the one-line string representation of a mixed amount.+--+-- > showMixedAmountOneLine = wbUnpack . showMixedAmountB oneLine showMixedAmountOneLine :: MixedAmount -> String-showMixedAmountOneLine = fst . showMixedOneLine showAmountWithoutPrice Nothing Nothing False+showMixedAmountOneLine = wbUnpack . showMixedAmountB oneLine -- | Like showMixedAmount, but zero amounts are shown with their -- commodity if they have one.+--+-- > showMixedAmountWithZeroCommodity = wbUnpack . showMixedAmountB noColour{displayZeroCommodity=True} showMixedAmountWithZeroCommodity :: MixedAmount -> String-showMixedAmountWithZeroCommodity = fst . showMixed showAmountWithZeroCommodity Nothing Nothing False---- | Get the string representation of a mixed amount, showing each of its--- component amounts with the specified precision, ignoring their--- commoditys' display precision settings.-showMixedAmountWithPrecision :: AmountPrecision -> MixedAmount -> String-showMixedAmountWithPrecision p = fst . showMixed (showAmountWithPrecision p) Nothing Nothing False+showMixedAmountWithZeroCommodity = wbUnpack . showMixedAmountB noColour{displayZeroCommodity=True} -- | Get the string representation of a mixed amount, without showing any transaction prices. -- With a True argument, adds ANSI codes to show negative amounts in red.+--+-- > showMixedAmountWithoutPrice c = wbUnpack . showMixedAmountB noPrice{displayColour=c} showMixedAmountWithoutPrice :: Bool -> MixedAmount -> String-showMixedAmountWithoutPrice c = fst . showMixed showAmountWithoutPrice Nothing Nothing c+showMixedAmountWithoutPrice c = wbUnpack . showMixedAmountB noPrice{displayColour=c} -- | Get the one-line string representation of a mixed amount, but without -- any \@ prices. -- With a True argument, adds ANSI codes to show negative amounts in red.+--+-- > showMixedAmountOneLineWithoutPrice c = wbUnpack . showMixedAmountB oneLine{displayColour=c} showMixedAmountOneLineWithoutPrice :: Bool -> MixedAmount -> String-showMixedAmountOneLineWithoutPrice c = fst . showMixedOneLine showAmountWithoutPrice Nothing Nothing c+showMixedAmountOneLineWithoutPrice c = wbUnpack . showMixedAmountB oneLine{displayColour=c} -- | Like showMixedAmountOneLineWithoutPrice, but show at most the given width, -- with an elision indicator if there are more. -- With a True argument, adds ANSI codes to show negative amounts in red.+--+-- > showMixedAmountElided w c = wbUnpack . showMixedAmountB oneLine{displayColour=c, displayMaxWidth=Just w} showMixedAmountElided :: Int -> Bool -> MixedAmount -> String-showMixedAmountElided w c = fst . showMixedOneLine showAmountWithoutPrice Nothing (Just w) c+showMixedAmountElided w c = wbUnpack . showMixedAmountB oneLine{displayColour=c, displayMaxWidth=Just w} -- | Get an unambiguous string representation of a mixed amount for debugging. showMixedAmountDebug :: MixedAmount -> String@@ -663,60 +712,67 @@ | otherwise = printf "Mixed [%s]" as where as = intercalate "\n " $ map showAmountDebug $ amounts m --- | General function to display a MixedAmount, one Amount on each line.--- It takes a function to display each Amount, an optional minimum width--- to pad to, an optional maximum width to display, and a Bool to determine--- whether to colourise negative numbers. Amounts longer than the maximum--- width (if given) will be elided. The function also returns the actual--- width of the output string.-showMixed :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)-showMixed showamt mmin mmax c =- showMixedUnnormalised showamt mmin mmax c . normaliseMixedAmountSquashPricesForDisplay+-- | General function to generate a WideBuilder for a MixedAmount, according the+-- supplied AmountDisplayOpts. This is the main function to use for showing+-- MixedAmounts, constructing a builder; it can then be converted to a Text with+-- wbToText, or to a String with wbUnpack.+--+-- If a maximum width is given then:+-- - If displayed on one line, it will display as many Amounts as can+-- fit in the given width, and further Amounts will be elided.+-- - If displayed on multiple lines, any Amounts longer than the+-- maximum width will be elided.+showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder+showMixedAmountB opts ma+ | displayOneLine opts = showMixedAmountOneLineB opts ma'+ | otherwise = WideBuilder (wbBuilder . mconcat $ intersperse sep lines) width+ where+ ma' = if displayPrice opts then ma else mixedAmountStripPrices ma+ lines = showMixedAmountLinesB opts ma'+ width = headDef 0 $ map wbWidth lines+ sep = WideBuilder (TB.singleton '\n') 0 --- | Like showMixed, but does not normalise the MixedAmount before displaying.-showMixedUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)-showMixedUnnormalised showamt mmin mmax c (Mixed as) =- (intercalate "\n" $ map finalise elided, width)+-- | Helper for showMixedAmountB to show a MixedAmount on multiple lines. This returns+-- the list of WideBuilders: one for each Amount in the MixedAmount (possibly+-- normalised), and padded/elided to the appropriate width. This does not+-- honour displayOneLine: all amounts will be displayed as if displayOneLine+-- were False.+showMixedAmountLinesB :: AmountDisplayOpts -> MixedAmount -> [WideBuilder]+showMixedAmountLinesB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWidth=mmin} ma =+ map (adBuilder . pad) elided where- width = maximum $ fromMaybe 0 mmin : map adLength elided- astrs = amtDisplayList sepwidth showamt as- sepwidth = 0 -- "\n" has width 0+ Mixed amts = if displayNormalised opts then normaliseMixedAmountSquashPricesForDisplay ma else ma - finalise = adString . pad . if c then colourise else id- pad amt = amt{ adString = applyN (width - adLength amt) (' ':) $ adString amt- , adLength = width- }+ astrs = amtDisplayList (wbWidth sep) (showAmountB opts) amts+ sep = WideBuilder (TB.singleton '\n') 0+ width = maximum $ fromMaybe 0 mmin : map (wbWidth . adBuilder) elided + pad amt = amt{ adBuilder = WideBuilder (TB.fromText $ T.replicate w " ") w <> adBuilder amt }+ where w = width - wbWidth (adBuilder amt)+ elided = maybe id elideTo mmax astrs elideTo m xs = maybeAppend elisionStr short where- elisionStr = elisionDisplay (Just m) sepwidth (length long) $ lastDef nullAmountDisplay short- (short, long) = partition ((m>=) . adLength) xs---- | General function to display a MixedAmount on a single line. It--- takes a function to display each Amount, an optional minimum width to--- pad to, an optional maximum width to display, and a Bool to determine--- whether to colourise negative numbers. It will display as many Amounts--- as it can in the maximum width (if given), and further Amounts will be--- elided. The function also returns the actual width of the output string.-showMixedOneLine :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)-showMixedOneLine showamt mmin mmax c =- showMixedOneLineUnnormalised showamt mmin mmax c . normaliseMixedAmountSquashPricesForDisplay+ elisionStr = elisionDisplay (Just m) (wbWidth sep) (length long) $ lastDef nullAmountDisplay short+ (short, long) = partition ((m>=) . wbWidth . adBuilder) xs --- | Like showMixedOneLine, but does not normalise the MixedAmount before--- displaying.-showMixedOneLineUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)-showMixedOneLineUnnormalised showamt mmin mmax c (Mixed as) =- (pad . intercalate ", " $ map finalise elided, max width $ fromMaybe 0 mmin)+-- | Helper for showMixedAmountB to deal with single line displays. This does not+-- honour displayOneLine: all amounts will be displayed as if displayOneLine+-- were True.+showMixedAmountOneLineB :: AmountDisplayOpts -> MixedAmount -> WideBuilder+showMixedAmountOneLineB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWidth=mmin} ma =+ WideBuilder (wbBuilder . pad . mconcat . intersperse sep $ map adBuilder elided) . max width $ fromMaybe 0 mmin where- width = maybe 0 adTotal $ lastMay elided- astrs = amtDisplayList sepwidth showamt as- sepwidth = 2 -- ", " has width 2- n = length as+ Mixed amts = if displayNormalised opts then normaliseMixedAmountSquashPricesForDisplay ma else ma - finalise = adString . if c then colourise else id- pad = applyN (fromMaybe 0 mmin - width) (' ':)+ width = maybe 0 adTotal $ lastMay elided+ astrs = amtDisplayList (wbWidth sep) (showAmountB opts) amts+ sep = WideBuilder (TB.fromString ", ") 2+ n = length amts + pad = (WideBuilder (TB.fromText $ T.replicate w " ") w <>)+ where w = fromMaybe 0 mmin - width+ elided = maybe id elideTo mmax astrs elideTo m = addElide . takeFitting m . withElided -- Add the last elision string to the end of the display list@@ -728,39 +784,36 @@ dropWhileRev p = foldr (\x xs -> if null xs && p x then [] else x:xs) [] -- Add the elision strings (if any) to each amount- withElided = zipWith (\num amt -> (amt, elisionDisplay Nothing sepwidth num amt)) [n-1,n-2..0]+ withElided = zipWith (\num amt -> (amt, elisionDisplay Nothing (wbWidth sep) num amt)) [n-1,n-2..0] data AmountDisplay = AmountDisplay- { adAmount :: !Amount -- ^ Amount displayed- , adString :: !String -- ^ String representation of the Amount- , adLength :: !Int -- ^ Length of the string representation- , adTotal :: !Int -- ^ Cumulative length of MixedAmount this Amount is part of,- -- including separators- } deriving (Show)+ { adBuilder :: !WideBuilder -- ^ String representation of the Amount+ , adTotal :: !Int -- ^ Cumulative length of MixedAmount this Amount is part of,+ -- including separators+ } nullAmountDisplay :: AmountDisplay-nullAmountDisplay = AmountDisplay nullamt "" 0 0+nullAmountDisplay = AmountDisplay mempty 0 -amtDisplayList :: Int -> (Amount -> String) -> [Amount] -> [AmountDisplay]+amtDisplayList :: Int -> (Amount -> WideBuilder) -> [Amount] -> [AmountDisplay] amtDisplayList sep showamt = snd . mapAccumL display (-sep) where- display tot amt = (tot', AmountDisplay amt str width tot')+ display tot amt = (tot', AmountDisplay str tot') where str = showamt amt- width = strWidth str- tot' = tot + width + sep+ tot' = tot + (wbWidth str) + sep -- The string "m more", added to the previous running total elisionDisplay :: Maybe Int -> Int -> Int -> AmountDisplay -> Maybe AmountDisplay elisionDisplay mmax sep n lastAmt- | n > 0 = Just $ AmountDisplay 0 str len (adTotal lastAmt + len)+ | n > 0 = Just $ AmountDisplay (WideBuilder (TB.fromText str) len) (adTotal lastAmt + len) | otherwise = Nothing where- fullString = show n ++ " more.."+ fullString = T.pack $ show n ++ " more.." -- sep from the separator, 7 from " more..", 1 + floor (logBase 10 n) from number fullLength = sep + 8 + floor (logBase 10 $ fromIntegral n) - str | Just m <- mmax, fullLength > m = take (m - 2) fullString ++ ".."+ str | Just m <- mmax, fullLength > m = T.take (m - 2) fullString <> ".." | otherwise = fullString len = case mmax of Nothing -> fullLength Just m -> max 2 $ min m fullLength@@ -769,30 +822,31 @@ maybeAppend Nothing = id maybeAppend (Just a) = (++[a]) -colourise :: AmountDisplay -> AmountDisplay-colourise amt = amt{adString=markColour $ adString amt}- where markColour = if isNegativeAmount (adAmount amt) then color Dull Red else id- -- | Compact labelled trace of a mixed amount, for debugging. ltraceamount :: String -> MixedAmount -> MixedAmount ltraceamount s = traceWith (((s ++ ": ") ++).showMixedAmount) -- | Set the display precision in the amount's commodities.-setMixedAmountPrecision :: AmountPrecision -> MixedAmount -> MixedAmount-setMixedAmountPrecision p (Mixed as) = Mixed $ map (setAmountPrecision p) as+mixedAmountSetPrecision :: AmountPrecision -> MixedAmount -> MixedAmount+mixedAmountSetPrecision p = mapMixedAmount (amountSetPrecision p) +-- | In each component amount, increase the display precision sufficiently+-- to render it exactly (showing all significant decimal digits).+mixedAmountSetFullPrecision :: MixedAmount -> MixedAmount+mixedAmountSetFullPrecision = mapMixedAmount amountSetFullPrecision+ mixedAmountStripPrices :: MixedAmount -> MixedAmount-mixedAmountStripPrices (Mixed as) = Mixed $ map (\a -> a{aprice=Nothing}) as+mixedAmountStripPrices = mapMixedAmount (\a -> a{aprice=Nothing}) -- | Canonicalise a mixed amount's display styles using the provided commodity style map. canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount-canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as+canonicaliseMixedAmount styles = mapMixedAmount (canonicaliseAmount styles) -- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice. -- Has no effect on amounts without one. -- Does Decimal division, might be some rounding/irrational number issues. mixedAmountTotalPriceToUnitPrice :: MixedAmount -> MixedAmount-mixedAmountTotalPriceToUnitPrice (Mixed as) = Mixed $ map amountTotalPriceToUnitPrice as+mixedAmountTotalPriceToUnitPrice = mapMixedAmount amountTotalPriceToUnitPrice -------------------------------------------------------------------------------@@ -805,7 +859,7 @@ amountCost (eur 1) @?= eur 1 amountCost (eur 2){aprice=Just $ UnitPrice $ usd 2} @?= usd 4 amountCost (eur 1){aprice=Just $ TotalPrice $ usd 2} @?= usd 2- amountCost (eur (-1)){aprice=Just $ TotalPrice $ usd 2} @?= usd (-2)+ amountCost (eur (-1)){aprice=Just $ TotalPrice $ usd (-2)} @?= usd (-2) ,test "amountLooksZero" $ do assertBool "" $ amountLooksZero amount@@ -846,9 +900,7 @@ [usd 1 @@ eur 1 ,usd (-2) @@ eur 1 ])- @?= Mixed [usd 1 @@ eur 1- ,usd (-2) @@ eur 1- ]+ @?= Mixed [usd (-1) @@ eur 2 ] ,test "showMixedAmount" $ do showMixedAmount (Mixed [usd 1]) @?= "$1.00"@@ -871,8 +923,8 @@ normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) @?= Mixed [usd 2 `at` eur 1] ,test "amounts with different unit prices are not combined" $ normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) @?= Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]- ,test "amounts with total prices are not combined" $- normaliseMixedAmount (Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]) @?= Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]+ ,test "amounts with total prices are combined" $+ normaliseMixedAmount (Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]) @?= Mixed [usd 2 @@ eur 2] ] ,test "normaliseMixedAmountSquashPricesForDisplay" $ do
Hledger/Data/Dates.hs view
@@ -110,19 +110,19 @@ -- Help ppShow parse and line-wrap DateSpans better in debug output. instance Show DateSpan where- show s = "DateSpan " ++ showDateSpan s+ show s = "DateSpan " ++ T.unpack (showDateSpan s) -showDate :: Day -> String-showDate = show+showDate :: Day -> Text+showDate = T.pack . show -- | Render a datespan as a display string, abbreviating into a -- compact form if possible.-showDateSpan :: DateSpan -> String+showDateSpan :: DateSpan -> Text showDateSpan = showPeriod . dateSpanAsPeriod -- | Like showDateSpan, but show month spans as just the abbreviated month name -- in the current locale.-showDateSpanMonthAbbrev :: DateSpan -> String+showDateSpanMonthAbbrev :: DateSpan -> Text showDateSpanMonthAbbrev = showPeriodMonthAbbrev . dateSpanAsPeriod -- | Get the current local date.@@ -388,13 +388,13 @@ -- | Convert a smart date string to an explicit yyyy\/mm\/dd string using -- the provided reference date, or raise an error.-fixSmartDateStr :: Day -> Text -> String+fixSmartDateStr :: Day -> Text -> Text fixSmartDateStr d s = either (error' . printf "could not parse date %s %s" (show s) . show) id $ -- PARTIAL:- (fixSmartDateStrEither d s :: Either (ParseErrorBundle Text CustomErr) String)+ (fixSmartDateStrEither d s :: Either (ParseErrorBundle Text CustomErr) Text) -- | A safe version of fixSmartDateStr.-fixSmartDateStrEither :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) String+fixSmartDateStrEither :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) Text fixSmartDateStrEither d = fmap showDate . fixSmartDateStrEither' d fixSmartDateStrEither'
Hledger/Data/Journal.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE PackageImports #-} {-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-}@@ -37,9 +38,9 @@ filterTransactionPostings, filterPostingAmount, -- * Mapping- mapJournalTransactions,- mapJournalPostings,- mapTransactionPostings,+ journalMapTransactions,+ journalMapPostings,+ journalMapPostingAmounts, -- * Querying journalAccountNamesUsed, journalAccountNamesImplied,@@ -52,8 +53,12 @@ -- overJournalAmounts, -- traverseJournalAmounts, -- journalCanonicalCommodities,+ journalPayeesDeclared,+ journalPayeesUsed,+ journalPayeesDeclaredOrUsed, journalCommoditiesDeclared, journalDateSpan,+ journalDateSpanBothDates, journalStartDate, journalEndDate, journalDescriptions,@@ -63,6 +68,7 @@ journalNextTransaction, journalPrevTransaction, journalPostings,+ journalTransactionsSimilarTo, -- journalPrices, -- * Standard account types journalBalanceSheetAccountQuery,@@ -86,31 +92,33 @@ tests_Journal, ) where-import Control.Monad-import Control.Monad.Except-import Control.Monad.Extra++import Control.Applicative ((<|>))+import Control.Monad.Except (ExceptT(..), runExceptT, throwError)+import "extra" Control.Monad.Extra (whenM) import Control.Monad.Reader as R-import Control.Monad.ST-import Data.Array.ST+import Control.Monad.ST (ST, runST)+import Data.Array.ST (STArray, getElems, newListArray, writeArray)+import Data.Char (toUpper, isDigit) import Data.Default (Default(..)) import Data.Function ((&)) import qualified Data.HashTable.Class as H (toList) import qualified Data.HashTable.ST.Cuckoo as H-import Data.List-import Data.List.Extra (groupSort, nubSort)-import qualified Data.Map as M-import Data.Maybe+import Data.List (find, foldl', sortOn)+import Data.List.Extra (nubSort)+import qualified Data.Map.Strict as M+import Data.Maybe (catMaybes, fromJust, fromMaybe, isJust, mapMaybe, maybeToList) #if !(MIN_VERSION_base(4,11,0)) import Data.Semigroup (Semigroup(..)) #endif import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T-import Safe (headMay, headDef)-import Data.Time.Calendar-import Data.Tree+import Safe (headMay, headDef, maximumMay, minimumMay)+import Data.Time.Calendar (Day, addDays, fromGregorian)+import Data.Tree (Tree, flatten) import System.Time (ClockTime(TOD))-import Text.Printf+import Text.Printf (printf) import Hledger.Utils import Hledger.Data.Types@@ -121,6 +129,7 @@ import Hledger.Data.TransactionModifier import Hledger.Data.Posting import Hledger.Query+import Data.List (sortBy) -- try to make Journal ppShow-compatible@@ -183,6 +192,7 @@ -- ,jparsetransactioncount = jparsetransactioncount j1 + jparsetransactioncount j2 ,jparsetimeclockentries = jparsetimeclockentries j1 <> jparsetimeclockentries j2 ,jincludefilestack = jincludefilestack j2+ ,jdeclaredpayees = jdeclaredpayees j1 <> jdeclaredpayees j2 ,jdeclaredaccounts = jdeclaredaccounts j1 <> jdeclaredaccounts j2 ,jdeclaredaccounttypes = jdeclaredaccounttypes j1 <> jdeclaredaccounttypes j2 ,jglobalcommoditystyles = jglobalcommoditystyles j1 <> jglobalcommoditystyles j2@@ -211,6 +221,7 @@ -- ,jparsetransactioncount = 0 ,jparsetimeclockentries = [] ,jincludefilestack = []+ ,jdeclaredpayees = [] ,jdeclaredaccounts = [] ,jdeclaredaccounttypes = M.empty ,jglobalcommoditystyles = M.empty@@ -261,10 +272,6 @@ journalPrevTransaction :: Journal -> Transaction -> Maybe Transaction journalPrevTransaction j t = journalTransactionAt j (tindex t - 1) --- | Unique transaction descriptions used in this journal.-journalDescriptions :: Journal -> [Text]-journalDescriptions = nubSort . map tdescription . jtxns- -- | All postings from this journal's transactions, in order. journalPostings :: Journal -> [Posting] journalPostings = concatMap tpostings . jtxns@@ -273,6 +280,22 @@ journalCommoditiesDeclared :: Journal -> [AccountName] journalCommoditiesDeclared = nubSort . M.keys . jcommodities +-- | Unique transaction descriptions used in this journal.+journalDescriptions :: Journal -> [Text]+journalDescriptions = nubSort . map tdescription . jtxns++-- | Sorted unique payees declared by payee directives in this journal.+journalPayeesDeclared :: Journal -> [Payee]+journalPayeesDeclared = nubSort . map fst . jdeclaredpayees++-- | Sorted unique payees used by transactions in this journal.+journalPayeesUsed :: Journal -> [Payee]+journalPayeesUsed = nubSort . map transactionPayee . jtxns++-- | Sorted unique payees used in transactions or declared by payee directives in this journal.+journalPayeesDeclaredOrUsed :: Journal -> [Payee]+journalPayeesDeclaredOrUsed j = nubSort $ journalPayeesDeclared j ++ journalPayeesUsed j+ -- | Sorted unique account names posted to by this journal's transactions. journalAccountNamesUsed :: Journal -> [AccountName] journalAccountNamesUsed = accountNamesFromPostings . journalPostings@@ -303,26 +326,134 @@ journalAccountNameTree :: Journal -> Tree AccountName journalAccountNameTree = accountNameTreeFrom . journalAccountNames +-- | Find up to N most similar and most recent transactions matching+-- the given transaction description and query. Transactions are+-- listed with their description's similarity score (see+-- compareDescriptions), sorted by highest score and then by date.+-- Only transactions with a similarity score greater than a minimum+-- threshold (currently 0) are returned.+journalTransactionsSimilarTo :: Journal -> Query -> Text -> Int -> [(Double,Transaction)]+journalTransactionsSimilarTo Journal{jtxns} q desc n =+ take n $+ sortBy (\(s1,t1) (s2,t2) -> compare (s2,tdate t2) (s1,tdate t1)) $+ filter ((> threshold).fst)+ [(compareDescriptions desc $ tdescription t, t) | t <- jtxns, q `matchesTransaction` t]+ where+ threshold = 0++-- | Return a similarity score from 0 to 1.5 for two transaction descriptions. +-- This is based on compareStrings, with the following modifications:+--+-- - numbers are stripped out before measuring similarity+--+-- - if the (unstripped) first description appears in its entirety within the second,+-- the score is boosted by 0.5.+--+compareDescriptions :: Text -> Text -> Double+compareDescriptions a b =+ (if a `T.isInfixOf` b then (0.5+) else id) $+ compareStrings (simplify a) (simplify b)+ where+ simplify = T.unpack . T.filter (not.isDigit)++-- | Return a similarity score from 0 to 1 for two strings. This+-- was based on Simon White's string similarity algorithm+-- (http://www.catalysoft.com/articles/StrikeAMatch.html), later found+-- to be https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient,+-- and modified to handle short strings better.+-- Todo: check out http://nlp.fi.muni.cz/raslan/2008/raslan08.pdf#page=14 .+compareStrings :: String -> String -> Double+compareStrings "" "" = 1+compareStrings [_] "" = 0+compareStrings "" [_] = 0+compareStrings [a] [b] = if toUpper a == toUpper b then 1 else 0+compareStrings s1 s2 = 2 * commonpairs / totalpairs+ where+ pairs1 = S.fromList $ wordLetterPairs $ uppercase s1+ pairs2 = S.fromList $ wordLetterPairs $ uppercase s2+ commonpairs = fromIntegral $ S.size $ S.intersection pairs1 pairs2+ totalpairs = fromIntegral $ S.size pairs1 + S.size pairs2++wordLetterPairs :: String -> [String]+wordLetterPairs = concatMap letterPairs . words++letterPairs :: String -> [String]+letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest)+letterPairs _ = []+ -- queries for standard account types +-- | Get a query for accounts of the specified types in this journal. +-- Account types include Asset, Liability, Equity, Revenue, Expense, Cash.+-- For each type, if no accounts were declared with this type, the query +-- will instead match accounts with names matched by the case-insensitive +-- regular expression provided as a fallback.+-- The query will match all accounts which were declared as one of+-- these types (by account directives with the type: tag), plus all their +-- subaccounts which have not been declared as some other type.+journalAccountTypeQuery :: [AccountType] -> Regexp -> Journal -> Query+journalAccountTypeQuery atypes fallbackregex Journal{jdeclaredaccounttypes} =+ let+ declaredacctsoftype :: [AccountName] =+ concat $ mapMaybe (`M.lookup` jdeclaredaccounttypes) atypes+ in case declaredacctsoftype of+ [] -> Acct fallbackregex+ as -> And $ [ Or acctnameRegexes ]+ ++ if null differentlyTypedRegexes then [] else [ Not $ Or differentlyTypedRegexes ]+ where+ -- XXX Query isn't able to match account type since that requires extra info from the journal.+ -- So we do a hacky search by name instead.+ acctnameRegexes = map (Acct . accountNameToAccountRegex) as+ differentlyTypedRegexes = map (Acct . accountNameToAccountRegex) differentlytypedsubs++ differentlytypedsubs = concat+ [subs | (t,bs) <- M.toList jdeclaredaccounttypes+ , not $ t `elem` atypes+ , let subs = [b | b <- bs, any (`isAccountNamePrefixOf` b) as]+ ]+ -- | A query for accounts in this journal which have been -- declared as Asset (or Cash, a subtype of Asset) by account directives, -- or otherwise for accounts with names matched by the case-insensitive -- regular expression @^assets?(:|$)@. journalAssetAccountQuery :: Journal -> Query-journalAssetAccountQuery = journalAccountTypeQuery [Asset,Cash] (toRegexCI' "^assets?(:|$)")+journalAssetAccountQuery j = + Or [+ journalAccountTypeQuery [Asset] (toRegexCI' "^assets?(:|$)") j+ ,journalCashAccountOnlyQuery j+ ] --- | A query for "Cash" (liquid asset) accounts in this journal, ie accounts--- declared as Cash by account directives, or otherwise with names matched by the --- case-insensitive regular expression @^assets?(:|$)@. and not including--- the case-insensitive regular expression @(investment|receivable|:A/R|:fixed)@.+-- | A query for accounts in this journal which have been+-- declared as Asset (and not Cash) by account directives, +-- or otherwise for accounts with names matched by the case-insensitive +-- regular expression @^assets?(:|$)@.+journalAssetNonCashAccountQuery :: Journal -> Query+journalAssetNonCashAccountQuery j = + journalAccountTypeQuery [Asset] (toRegexCI' "^assets?(:|$)") j++-- | A query for Cash (liquid asset) accounts in this journal, ie accounts+-- declared as Cash by account directives, or otherwise Asset accounts whose +-- names do not include the case-insensitive regular expression +-- @(investment|receivable|:A/R|:fixed)@. journalCashAccountQuery :: Journal -> Query journalCashAccountQuery j = case M.lookup Cash (jdeclaredaccounttypes j) of- Nothing -> And [ journalAssetAccountQuery j, Not . Acct $ toRegexCI' "(investment|receivable|:A/R|:fixed)" ]- Just _ -> journalAccountTypeQuery [Cash] notused j- where notused = error' "journalCashAccountQuery: this should not have happened!" -- PARTIAL:+ Just _ -> journalCashAccountOnlyQuery j+ Nothing ->+ -- no Cash accounts are declared; query for Asset accounts and exclude some of them+ And [ journalAssetNonCashAccountQuery j, Not . Acct $ toRegexCI' "(investment|receivable|:A/R|:fixed)" ] +-- | A query for accounts in this journal specifically declared as Cash by +-- account directives, or otherwise the None query.+journalCashAccountOnlyQuery :: Journal -> Query+journalCashAccountOnlyQuery j = + case M.lookup Cash (jdeclaredaccounttypes j) of+ Just _ -> + -- Cash accounts are declared; get a query for them (the fallback regex won't be used)+ journalAccountTypeQuery [Cash] notused j+ where notused = error' "journalCashAccountOnlyQuery: this should not have happened!" -- PARTIAL:+ Nothing -> None+ -- | A query for accounts in this journal which have been -- declared as Liability by account directives, or otherwise for -- accounts with names matched by the case-insensitive regular expression@@ -366,33 +497,6 @@ ,journalExpenseAccountQuery j ] --- | Get a query for accounts of the specified types (Asset, Liability..) in this journal.--- The query will match all accounts which were declared as one of--- these types by account directives, plus all their subaccounts which--- have not been declared as some other type.--- Or if no accounts were declared with these types, the query will--- instead match accounts with names matched by the provided--- case-insensitive regular expression.-journalAccountTypeQuery :: [AccountType] -> Regexp -> Journal -> Query-journalAccountTypeQuery atypes fallbackregex Journal{jdeclaredaccounttypes} =- let- declaredacctsoftype :: [AccountName] =- concat $ mapMaybe (`M.lookup` jdeclaredaccounttypes) atypes- in case declaredacctsoftype of- [] -> Acct fallbackregex- as -> And [ Or acctnameRegexes, Not $ Or differentlyTypedRegexes ]- where- -- XXX Query isn't able to match account type since that requires extra info from the journal.- -- So we do a hacky search by name instead.- acctnameRegexes = map (Acct . accountNameToAccountRegex) as- differentlyTypedRegexes = map (Acct . accountNameToAccountRegex) differentlytypedsubs-- differentlytypedsubs = concat- [subs | (t,bs) <- M.toList jdeclaredaccounttypes- , not $ t `elem` atypes- , let subs = [b | b <- bs, any (`isAccountNamePrefixOf` b) as]- ]- -- Various kinds of filtering on journals. We do it differently depending -- on the command. @@ -426,16 +530,16 @@ filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps} -- | Apply a transformation to a journal's transactions.-mapJournalTransactions :: (Transaction -> Transaction) -> Journal -> Journal-mapJournalTransactions f j@Journal{jtxns=ts} = j{jtxns=map f ts}+journalMapTransactions :: (Transaction -> Transaction) -> Journal -> Journal+journalMapTransactions f j@Journal{jtxns=ts} = j{jtxns=map f ts} -- | Apply a transformation to a journal's postings.-mapJournalPostings :: (Posting -> Posting) -> Journal -> Journal-mapJournalPostings f j@Journal{jtxns=ts} = j{jtxns=map (mapTransactionPostings f) ts}+journalMapPostings :: (Posting -> Posting) -> Journal -> Journal+journalMapPostings f j@Journal{jtxns=ts} = j{jtxns=map (transactionMapPostings f) ts} --- | Apply a transformation to a transaction's postings.-mapTransactionPostings :: (Posting -> Posting) -> Transaction -> Transaction-mapTransactionPostings f t@Transaction{tpostings=ps} = t{tpostings=map f ps}+-- | Apply a transformation to a journal's posting amounts.+journalMapPostingAmounts :: (Amount -> Amount) -> Journal -> Journal+journalMapPostingAmounts f = journalMapPostings (postingTransformAmount (mapMixedAmount f)) {- -------------------------------------------------------------------------------@@ -888,7 +992,7 @@ Nothing -> "?" -- shouldn't happen Just t -> printf "%s\ntransaction:\n%s" (showGenericSourcePos pos)- (chomp $ showTransaction t)+ (textChomp $ showTransaction t) :: String where pos = baposition $ fromJust $ pbalanceassertion p@@ -919,11 +1023,11 @@ checkBalanceAssignmentPostingDateB :: Posting -> Balancing s () checkBalanceAssignmentPostingDateB p = when (hasBalanceAssignment p && isJust (pdate p)) $- throwError $ unlines $+ throwError . T.unpack $ T.unlines ["postings which are balance assignments may not have a custom date." ,"Please write the posting amount explicitly, or remove the posting date:" ,""- ,maybe (unlines $ showPostingLines p) showTransaction $ ptransaction p+ ,maybe (T.unlines $ showPostingLines p) showTransaction $ ptransaction p ] -- | Throw an error if this posting is trying to do a balance assignment and@@ -933,16 +1037,16 @@ checkBalanceAssignmentUnassignableAccountB p = do unassignable <- R.asks bsUnassignable when (hasBalanceAssignment p && paccount p `S.member` unassignable) $- throwError $ unlines $+ throwError . T.unpack $ T.unlines ["balance assignments cannot be used with accounts which are" ,"posted to by transaction modifier rules (auto postings)." ,"Please write the posting amount explicitly, or remove the rule." ,""- ,"account: "++T.unpack (paccount p)+ ,"account: " <> paccount p ,"" ,"transaction:" ,""- ,maybe (unlines $ showPostingLines p) showTransaction $ ptransaction p+ ,maybe (T.unlines $ showPostingLines p) showTransaction $ ptransaction p ] --@@ -963,7 +1067,8 @@ fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps} fixposting p = p{pamount=styleMixedAmount styles $ pamount p ,pbalanceassertion=fixbalanceassertion <$> pbalanceassertion p}- fixbalanceassertion ba = ba{baamount=styleAmount styles $ baamount ba}+ -- balance assertion amounts are always displayed (by print) at full precision, per docs+ fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba} fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a} -- | Get the canonical amount styles for this journal, whether (in order of precedence):@@ -1006,42 +1111,40 @@ -- and this function never reports an error. -- commodityStylesFromAmounts :: [Amount] -> Either String (M.Map CommoditySymbol AmountStyle)-commodityStylesFromAmounts amts =- Right $ M.fromList commstyles- where- commamts = groupSort [(acommodity as, as) | as <- amts]- commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts]+commodityStylesFromAmounts =+ Right . foldr (\a -> M.insertWith canonicalStyle (acommodity a) (astyle a)) mempty --- TODO: should probably detect and report inconsistencies here.--- Though, we don't have the info for a good error message, so maybe elsewhere. -- | Given a list of amount styles (assumed to be from parsed amounts -- in a single commodity), in parse order, choose a canonical style.+canonicalStyleFrom :: [AmountStyle] -> AmountStyle+-- canonicalStyleFrom [] = amountstyle+canonicalStyleFrom ss = foldl' canonicalStyle amountstyle ss++-- TODO: should probably detect and report inconsistencies here.+-- Though, we don't have the info for a good error message, so maybe elsewhere.+-- | Given a pair of AmountStyles, choose a canonical style. -- This is:--- the general style of the first amount, +-- the general style of the first amount, -- with the first digit group style seen, -- with the maximum precision of all.----canonicalStyleFrom :: [AmountStyle] -> AmountStyle-canonicalStyleFrom [] = amountstyle-canonicalStyleFrom ss@(s:_) =- s{asprecision=prec, asdecimalpoint=Just decmark, asdigitgroups=mgrps}+canonicalStyle :: AmountStyle -> AmountStyle -> AmountStyle+canonicalStyle a b = a{asprecision=prec, asdecimalpoint=decmark, asdigitgroups=mgrps} where -- precision is maximum of all precisions- prec = maximumStrict $ map asprecision ss+ prec = max (asprecision a) (asprecision b) -- identify the digit group mark (& group sizes)- mgrps = headMay $ mapMaybe asdigitgroups ss+ mgrps = asdigitgroups a <|> asdigitgroups b -- if a digit group mark was identified above, we can rely on that; -- make sure the decimal mark is different. If not, default to period.- defdecmark =- case mgrps of+ defdecmark = case mgrps of Just (DigitGroups '.' _) -> ',' _ -> '.' -- identify the decimal mark: the first one used, or the above default, -- but never the same character as the digit group mark. -- urgh.. refactor.. decmark = case mgrps of- Just _ -> defdecmark- _ -> headDef defdecmark $ mapMaybe asdecimalpoint ss+ Just _ -> Just defdecmark+ Nothing -> asdecimalpoint a <|> asdecimalpoint b <|> Just defdecmark -- -- | Apply this journal's historical price records to unpriced amounts where possible. -- journalApplyPriceDirectives :: Journal -> Journal@@ -1185,17 +1288,34 @@ -- of all this journal's transactions and postings, or DateSpan Nothing Nothing -- if there are none. journalDateSpan :: Bool -> Journal -> DateSpan-journalDateSpan secondary j- | null ts = DateSpan Nothing Nothing- | otherwise = DateSpan (Just earliest) (Just $ addDays 1 latest)- where- earliest = minimumStrict dates- latest = maximumStrict dates- dates = pdates ++ tdates- tdates = map (if secondary then transactionDate2 else tdate) ts- pdates = concatMap (mapMaybe (if secondary then (Just . postingDate2) else pdate) . tpostings) ts- ts = jtxns j+journalDateSpan False = journalDateSpanHelper $ Just PrimaryDate+journalDateSpan True = journalDateSpanHelper $ Just SecondaryDate +-- | The fully specified date span enclosing the dates (primary and secondary)+-- of all this journal's transactions and postings, or DateSpan Nothing Nothing+-- if there are none.+journalDateSpanBothDates :: Journal -> DateSpan+journalDateSpanBothDates = journalDateSpanHelper Nothing++-- | A helper for journalDateSpan which takes Maybe WhichDate directly. Nothing+-- uses both primary and secondary dates.+journalDateSpanHelper :: Maybe WhichDate -> Journal -> DateSpan+journalDateSpanHelper whichdate j =+ DateSpan (minimumMay dates) (addDays 1 <$> maximumMay dates)+ where+ dates = pdates ++ tdates+ tdates = concatMap gettdate ts+ pdates = concatMap getpdate $ concatMap tpostings ts+ ts = jtxns j+ gettdate t = case whichdate of+ Just PrimaryDate -> [tdate t]+ Just SecondaryDate -> [fromMaybe (tdate t) $ tdate2 t]+ Nothing -> tdate t : maybeToList (tdate2 t)+ getpdate p = case whichdate of+ Just PrimaryDate -> maybeToList $ pdate p+ Just SecondaryDate -> maybeToList $ pdate2 p <|> pdate p+ Nothing -> catMaybes [pdate p, pdate2 p]+ -- | The earliest of this journal's transaction and posting dates, or -- Nothing if there are none. journalStartDate :: Bool -> Journal -> Maybe Day@@ -1241,7 +1361,7 @@ case mapM (transactionApplyAliases aliases) $ jtxns j of Right ts -> Right j{jtxns = ts} Left err -> Left err- + -- -- | Build a database of market prices in effect on the given date, -- -- from the journal's price directives. -- journalPrices :: Day -> Journal -> Prices@@ -1253,7 +1373,7 @@ -- [ "P" -- , showDate (pddate pd) -- , T.unpack (pdcommodity pd)--- , (showAmount . setAmountPrecision maxprecision) (pdamount pd+-- , (showAmount . amountSetPrecision maxprecision) (pdamount pd -- ) -- ]
Hledger/Data/Json.hs view
@@ -44,7 +44,7 @@ import Data.Maybe import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.IO as TL-import Data.Text.Lazy.Builder (toLazyText)+import qualified Data.Text.Lazy.Builder as TB import GHC.Generics (Generic) import System.Time (ClockTime) @@ -126,6 +126,7 @@ instance ToJSON AccountType instance ToJSONKey AccountType instance ToJSON AccountDeclarationInfo+instance ToJSON PayeeDeclarationInfo instance ToJSON Commodity instance ToJSON TimeclockCode instance ToJSON TimeclockEntry@@ -231,7 +232,7 @@ -- | Show a JSON-convertible haskell value as pretty-printed JSON text. toJsonText :: ToJSON a => a -> TL.Text-toJsonText = (<>"\n") . toLazyText . encodePrettyToTextBuilder+toJsonText = TB.toLazyText . (<> TB.fromText "\n") . encodePrettyToTextBuilder -- | Write a JSON-convertible haskell value to a pretty-printed JSON file. -- Eg: writeJsonFile "a.json" nulltransaction
Hledger/Data/Period.hs view
@@ -5,6 +5,8 @@ -} +{-# LANGUAGE OverloadedStrings #-}+ module Hledger.Data.Period ( periodAsDateSpan ,dateSpanAsPeriod@@ -30,6 +32,8 @@ ) where +import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar import Data.Time.Calendar.MonthDay import Data.Time.Calendar.OrdinalDate@@ -155,21 +159,23 @@ -- -- >>> showPeriod (WeekPeriod (fromGregorian 2016 7 25)) -- "2016-07-25W30"-showPeriod (DayPeriod b) = formatTime defaultTimeLocale "%F" b -- DATE-showPeriod (WeekPeriod b) = formatTime defaultTimeLocale "%FW%V" b -- STARTDATEWYEARWEEK-showPeriod (MonthPeriod y m) = printf "%04d-%02d" y m -- YYYY-MM-showPeriod (QuarterPeriod y q) = printf "%04dQ%d" y q -- YYYYQN-showPeriod (YearPeriod y) = printf "%04d" y -- YYYY-showPeriod (PeriodBetween b e) = formatTime defaultTimeLocale "%F" b+showPeriod :: Period -> Text+showPeriod (DayPeriod b) = T.pack $ formatTime defaultTimeLocale "%F" b -- DATE+showPeriod (WeekPeriod b) = T.pack $ formatTime defaultTimeLocale "%FW%V" b -- STARTDATEWYEARWEEK+showPeriod (MonthPeriod y m) = T.pack $ printf "%04d-%02d" y m -- YYYY-MM+showPeriod (QuarterPeriod y q) = T.pack $ printf "%04dQ%d" y q -- YYYYQN+showPeriod (YearPeriod y) = T.pack $ printf "%04d" y -- YYYY+showPeriod (PeriodBetween b e) = T.pack $ formatTime defaultTimeLocale "%F" b ++ formatTime defaultTimeLocale "..%F" (addDays (-1) e) -- STARTDATE..INCLUSIVEENDDATE-showPeriod (PeriodFrom b) = formatTime defaultTimeLocale "%F.." b -- STARTDATE..-showPeriod (PeriodTo e) = formatTime defaultTimeLocale "..%F" (addDays (-1) e) -- ..INCLUSIVEENDDATE+showPeriod (PeriodFrom b) = T.pack $ formatTime defaultTimeLocale "%F.." b -- STARTDATE..+showPeriod (PeriodTo e) = T.pack $ formatTime defaultTimeLocale "..%F" (addDays (-1) e) -- ..INCLUSIVEENDDATE showPeriod PeriodAll = ".." -- | Like showPeriod, but if it's a month period show just -- the 3 letter month name abbreviation for the current locale.+showPeriodMonthAbbrev :: Period -> Text showPeriodMonthAbbrev (MonthPeriod _ m) -- Jan- | m > 0 && m <= length monthnames = snd $ monthnames !! (m-1)+ | m > 0 && m <= length monthnames = T.pack . snd $ monthnames !! (m-1) where monthnames = months defaultTimeLocale showPeriodMonthAbbrev p = showPeriod p
Hledger/Data/PeriodicTransaction.hs view
@@ -16,6 +16,7 @@ import Data.Semigroup ((<>)) #endif import qualified Data.Text as T+import qualified Data.Text.IO as T import Text.Printf import Hledger.Data.Types@@ -40,7 +41,7 @@ case checkPeriodicTransactionStartDate i s t of Just e -> error' e -- PARTIAL: Nothing ->- mapM_ (putStr . showTransaction) $+ mapM_ (T.putStr . showTransaction) $ runPeriodicTransaction nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } nulldatespan@@ -52,7 +53,7 @@ case checkPeriodicTransactionStartDate i s t of Just e -> error' e -- PARTIAL: Nothing ->- mapM_ (putStr . showTransaction) $+ mapM_ (T.putStr . showTransaction) $ runPeriodicTransaction nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } span
Hledger/Data/Posting.hs view
@@ -64,6 +64,7 @@ -- * misc. showComment, postingTransformAmount,+ postingApplyCostValuation, postingApplyValuation, postingToCost, tests_Posting@@ -161,20 +162,20 @@ -- XXX once rendered user output, but just for debugging now; clean up showPosting :: Posting -> String showPosting p@Posting{paccount=a,pamount=amt,ptype=t} =- unlines $ [concatTopPadded [show (postingDate p) ++ " ", showaccountname a ++ " ", showamount amt, showComment (pcomment p)]]+ unlines $ [concatTopPadded [show (postingDate p) ++ " ", showaccountname a ++ " ", showamount amt, T.unpack . showComment $ pcomment p]] where ledger3ishlayout = False acctnamewidth = if ledger3ishlayout then 25 else 22- showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . T.unpack . elideAccountName width+ showaccountname = T.unpack . fitText (Just acctnamewidth) Nothing False False . bracket . elideAccountName width (bracket,width) = case t of- BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)- VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)- _ -> (id,acctnamewidth)- showamount = fst . showMixed showAmount (Just 12) Nothing False+ BalancedVirtualPosting -> (wrap "[" "]", acctnamewidth-2)+ VirtualPosting -> (wrap "(" ")", acctnamewidth-2)+ _ -> (id,acctnamewidth)+ showamount = wbUnpack . showMixedAmountB noColour{displayMinWidth=Just 12} -showComment :: Text -> String-showComment t = if T.null t then "" else " ;" ++ T.unpack t+showComment :: Text -> Text+showComment t = if T.null t then "" else " ;" <> t isReal :: Posting -> Bool isReal p = ptype p == RegularPosting@@ -274,9 +275,9 @@ RegularPosting -> a accountNameWithPostingType :: PostingType -> AccountName -> AccountName-accountNameWithPostingType BalancedVirtualPosting a = "["<>accountNameWithoutPostingType a<>"]"-accountNameWithPostingType VirtualPosting a = "("<>accountNameWithoutPostingType a<>")"-accountNameWithPostingType RegularPosting a = accountNameWithoutPostingType a+accountNameWithPostingType BalancedVirtualPosting = wrap "[" "]" . accountNameWithoutPostingType+accountNameWithPostingType VirtualPosting = wrap "(" ")" . accountNameWithoutPostingType+accountNameWithPostingType RegularPosting = accountNameWithoutPostingType -- | Prefix one account name to another, preserving posting type -- indicators like concatAccountNames.@@ -330,33 +331,24 @@ aliasReplace (RegexAlias re repl) a = fmap T.pack . regexReplace re repl $ T.unpack a -- XXX +-- | Apply a specified costing and valuation to this posting's amount,+-- using the provided price oracle, commodity styles, and reference dates.+-- Costing is done first if requested, and after that any valuation.+-- See amountApplyValuation and amountCost.+postingApplyCostValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Costing -> Maybe ValuationType -> Posting -> Posting+postingApplyCostValuation priceoracle styles periodlast today cost v p =+ postingTransformAmount (mixedAmountApplyCostValuation priceoracle styles periodlast today (postingDate p) cost v) p+ -- | Apply a specified valuation to this posting's amount, using the--- provided price oracle, commodity styles, reference dates, and--- whether this is for a multiperiod report or not. See--- amountApplyValuation.-postingApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> Posting -> ValuationType -> Posting-postingApplyValuation priceoracle styles periodlast mreportlast today ismultiperiod p v =- case v of- AtCost Nothing -> postingToCost styles p- AtCost mc -> postingValueAtDate priceoracle styles mc periodlast $ postingToCost styles p- AtThen mc -> postingValueAtDate priceoracle styles mc (postingDate p) p- AtEnd mc -> postingValueAtDate priceoracle styles mc periodlast p- AtNow mc -> postingValueAtDate priceoracle styles mc today p- AtDefault mc | ismultiperiod -> postingValueAtDate priceoracle styles mc periodlast p- AtDefault mc -> postingValueAtDate priceoracle styles mc (fromMaybe today mreportlast) p- AtDate d mc -> postingValueAtDate priceoracle styles mc d p+-- provided price oracle, commodity styles, and reference dates.+-- See amountApplyValuation.+postingApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> ValuationType -> Posting -> Posting+postingApplyValuation priceoracle styles periodlast today v p =+ postingTransformAmount (mixedAmountApplyValuation priceoracle styles periodlast today (postingDate p) v) p -- | Convert this posting's amount to cost, and apply the appropriate amount styles. postingToCost :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting-postingToCost styles p@Posting{pamount=a} = p{pamount=styleMixedAmount styles $ mixedAmountCost a}---- | Convert this posting's amount to market value in the given commodity,--- or the default valuation commodity, at the given valuation date,--- using the given market price oracle.--- When market prices available on that date are not sufficient to--- calculate the value, amounts are left unchanged.-postingValueAtDate :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Maybe CommoditySymbol -> Day -> Posting -> Posting-postingValueAtDate priceoracle styles mc d p = postingTransformAmount (mixedAmountValueAtDate priceoracle styles mc d) p+postingToCost styles = postingTransformAmount (styleMixedAmount styles . mixedAmountCost) -- | Apply a transform function to this posting's amount. postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting@@ -385,7 +377,7 @@ -- A space is inserted following the colon, before the value. commentAddTagNextLine :: Text -> Tag -> Text commentAddTagNextLine cmt (t,v) =- cmt <> if "\n" `T.isSuffixOf` cmt then "" else "\n" <> t <> ": " <> v + cmt <> (if "\n" `T.isSuffixOf` cmt then "" else "\n") <> t <> ": " <> v -- tests
Hledger/Data/StringFormat.hs view
@@ -2,7 +2,10 @@ -- hledger's report item fields. The formats are used by -- report-specific renderers like renderBalanceReportItem. -{-# LANGUAGE FlexibleContexts, OverloadedStrings, TypeFamilies, PackageImports #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TypeFamilies #-} module Hledger.Data.StringFormat ( parseStringFormat@@ -10,7 +13,6 @@ , StringFormat(..) , StringFormatComponent(..) , ReportItemField(..)- , overlineWidth , defaultBalanceLineFormat , tests_StringFormat ) where@@ -21,22 +23,20 @@ import Data.Char (isPrint) import Data.Default (Default(..)) import Data.Maybe (isJust)--- import qualified Data.Text as T+import Data.Text (Text)+import qualified Data.Text as T import Text.Megaparsec import Text.Megaparsec.Char (char, digitChar, string) -import Hledger.Utils.Parse (SimpleStringParser)-import Hledger.Utils.String (formatString)+import Hledger.Utils.Parse (SimpleTextParser)+import Hledger.Utils.Text (formatText) import Hledger.Utils.Test -- | A format specification/template to use when rendering a report line item as text. ----- A format is an optional width, along with a sequence of components;--- each is either a literal string, or a hledger report item field with--- specified width and justification whose value will be interpolated--- at render time. The optional width determines the length of the--- overline to draw above the totals row; if it is Nothing, then the--- maximum width of all lines is used.+-- A format is a sequence of components; each is either a literal+-- string, or a hledger report item field with specified width and+-- justification whose value will be interpolated at render time. -- -- A component's value may be a multi-line string (or a -- multi-commodity amount), in which case the final string will be@@ -47,13 +47,13 @@ -- mode, which provides a limited StringFormat renderer. -- data StringFormat =- OneLine (Maybe Int) [StringFormatComponent] -- ^ multi-line values will be rendered on one line, comma-separated- | TopAligned (Maybe Int) [StringFormatComponent] -- ^ values will be top-aligned (and bottom-padded to the same height)- | BottomAligned (Maybe Int) [StringFormatComponent] -- ^ values will be bottom-aligned (and top-padded)+ OneLine [StringFormatComponent] -- ^ multi-line values will be rendered on one line, comma-separated+ | TopAligned [StringFormatComponent] -- ^ values will be top-aligned (and bottom-padded to the same height)+ | BottomAligned [StringFormatComponent] -- ^ values will be bottom-aligned (and top-padded) deriving (Show, Eq) data StringFormatComponent =- FormatLiteral String -- ^ Literal text to be rendered as-is+ FormatLiteral Text -- ^ Literal text to be rendered as-is | FormatField Bool (Maybe Int) (Maybe Int)@@ -81,14 +81,9 @@ instance Default StringFormat where def = defaultBalanceLineFormat -overlineWidth :: StringFormat -> Maybe Int-overlineWidth (OneLine w _) = w-overlineWidth (TopAligned w _) = w-overlineWidth (BottomAligned w _) = w- -- | Default line format for balance report: "%20(total) %2(depth_spacer)%-(account)" defaultBalanceLineFormat :: StringFormat-defaultBalanceLineFormat = BottomAligned (Just 20) [+defaultBalanceLineFormat = BottomAligned [ FormatField False (Just 20) Nothing TotalField , FormatLiteral " " , FormatField True (Just 2) Nothing DepthSpacerField@@ -102,37 +97,37 @@ ---------------------------------------------------------------------- -- | Parse a string format specification, or return a parse error.-parseStringFormat :: String -> Either String StringFormat+parseStringFormat :: Text -> Either String StringFormat parseStringFormat input = case (runParser (stringformatp <* eof) "(unknown)") input of Left y -> Left $ show y Right x -> Right x defaultStringFormatStyle = BottomAligned -stringformatp :: SimpleStringParser StringFormat+stringformatp :: SimpleTextParser StringFormat stringformatp = do alignspec <- optional (try $ char '%' >> oneOf ("^_,"::String)) let constructor = case alignspec of- Just '^' -> TopAligned Nothing- Just '_' -> BottomAligned Nothing- Just ',' -> OneLine Nothing- _ -> defaultStringFormatStyle Nothing+ Just '^' -> TopAligned+ Just '_' -> BottomAligned+ Just ',' -> OneLine+ _ -> defaultStringFormatStyle constructor <$> many componentp -componentp :: SimpleStringParser StringFormatComponent+componentp :: SimpleTextParser StringFormatComponent componentp = formatliteralp <|> formatfieldp -formatliteralp :: SimpleStringParser StringFormatComponent+formatliteralp :: SimpleTextParser StringFormatComponent formatliteralp = do- s <- some c+ s <- T.pack <$> some c return $ FormatLiteral s where isPrintableButNotPercentage x = isPrint x && x /= '%' c = (satisfy isPrintableButNotPercentage <?> "printable character") <|> try (string "%%" >> return '%') -formatfieldp :: SimpleStringParser StringFormatComponent+formatfieldp :: SimpleTextParser StringFormatComponent formatfieldp = do char '%' leftJustified <- optional (char '-')@@ -147,7 +142,7 @@ Just text -> Just m where ((m,_):_) = readDec text _ -> Nothing -fieldp :: SimpleStringParser ReportItemField+fieldp :: SimpleTextParser ReportItemField fieldp = do try (string "account" >> return AccountField) <|> try (string "depth_spacer" >> return DepthSpacerField)@@ -161,8 +156,8 @@ formatStringTester fs value expected = actual @?= expected where actual = case fs of- FormatLiteral l -> formatString False Nothing Nothing l- FormatField leftJustify min max _ -> formatString leftJustify min max value+ FormatLiteral l -> formatText False Nothing Nothing l+ FormatField leftJustify min max _ -> formatText leftJustify min max value tests_StringFormat = tests "StringFormat" [ @@ -176,25 +171,25 @@ formatStringTester (FormatField True (Just 20) (Just 20) DescriptionField) "description" "description " formatStringTester (FormatField True Nothing (Just 3) DescriptionField) "description" "des" - ,let s `gives` expected = test s $ parseStringFormat s @?= Right expected+ ,let s `gives` expected = test s $ parseStringFormat (T.pack s) @?= Right expected in tests "parseStringFormat" [- "" `gives` (defaultStringFormatStyle Nothing [])- , "D" `gives` (defaultStringFormatStyle Nothing [FormatLiteral "D"])- , "%(date)" `gives` (defaultStringFormatStyle Nothing [FormatField False Nothing Nothing DescriptionField])- , "%(total)" `gives` (defaultStringFormatStyle Nothing [FormatField False Nothing Nothing TotalField])+ "" `gives` (defaultStringFormatStyle [])+ , "D" `gives` (defaultStringFormatStyle [FormatLiteral "D"])+ , "%(date)" `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing DescriptionField])+ , "%(total)" `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing TotalField]) -- TODO -- , "^%(total)" `gives` (TopAligned [FormatField False Nothing Nothing TotalField]) -- , "_%(total)" `gives` (BottomAligned [FormatField False Nothing Nothing TotalField]) -- , ",%(total)" `gives` (OneLine [FormatField False Nothing Nothing TotalField])- , "Hello %(date)!" `gives` (defaultStringFormatStyle Nothing [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])- , "%-(date)" `gives` (defaultStringFormatStyle Nothing [FormatField True Nothing Nothing DescriptionField])- , "%20(date)" `gives` (defaultStringFormatStyle Nothing [FormatField False (Just 20) Nothing DescriptionField])- , "%.10(date)" `gives` (defaultStringFormatStyle Nothing [FormatField False Nothing (Just 10) DescriptionField])- , "%20.10(date)" `gives` (defaultStringFormatStyle Nothing [FormatField False (Just 20) (Just 10) DescriptionField])- , "%20(account) %.10(total)" `gives` (defaultStringFormatStyle Nothing [FormatField False (Just 20) Nothing AccountField- ,FormatLiteral " "- ,FormatField False Nothing (Just 10) TotalField- ])+ , "Hello %(date)!" `gives` (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])+ , "%-(date)" `gives` (defaultStringFormatStyle [FormatField True Nothing Nothing DescriptionField])+ , "%20(date)" `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing DescriptionField])+ , "%.10(date)" `gives` (defaultStringFormatStyle [FormatField False Nothing (Just 10) DescriptionField])+ , "%20.10(date)" `gives` (defaultStringFormatStyle [FormatField False (Just 20) (Just 10) DescriptionField])+ , "%20(account) %.10(total)" `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing AccountField+ ,FormatLiteral " "+ ,FormatField False Nothing (Just 10) TotalField+ ]) , test "newline not parsed" $ assertLeft $ parseStringFormat "\n" ] ]
Hledger/Data/Timeclock.hs view
@@ -6,6 +6,7 @@ -} +{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Hledger.Data.Timeclock (@@ -14,14 +15,18 @@ ) where -import Data.Maybe+import Data.Maybe (fromMaybe)+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif -- import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar-import Data.Time.Clock-import Data.Time.Format-import Data.Time.LocalTime-import Text.Printf+import Data.Time.Calendar (addDays)+import Data.Time.Clock (addUTCTime, getCurrentTime)+import Data.Time.Format (defaultTimeLocale, formatTime, parseTimeM)+import Data.Time.LocalTime (LocalTime(..), TimeOfDay(..), getCurrentTimeZone,+ localTimeToUTC, midnight, utc, utcToLocalTime)+import Text.Printf (printf) import Hledger.Utils import Hledger.Data.Types@@ -90,8 +95,8 @@ entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction entryFromTimeclockInOut i o | otime >= itime = t- | otherwise =- error' $ "clock-out time less than clock-in time in:\n" ++ showTransaction t -- PARTIAL:+ | otherwise = error' . T.unpack $+ "clock-out time less than clock-in time in:\n" <> showTransaction t -- PARTIAL: where t = Transaction { tindex = 0,
Hledger/Data/Transaction.hs view
@@ -7,12 +7,14 @@ -} -{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE Rank2Types #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NamedFieldPuns #-} module Hledger.Data.Transaction ( -- * Transaction nulltransaction,@@ -31,9 +33,12 @@ balanceTransaction, balanceTransactionHelper, transactionTransformPostings,+ transactionApplyCostValuation, transactionApplyValuation, transactionToCost, transactionApplyAliases,+ transactionMapPostings,+ transactionMapPostingAmounts, -- nonzerobalanceerror, -- * date operations transactionDate2,@@ -44,8 +49,6 @@ -- * rendering showTransaction, showTransactionOneLineAmounts,- showTransactionUnelided,- showTransactionUnelidedOneLineAmounts, -- showPostingLine, showPostingLines, -- * GenericSourcePos@@ -53,17 +56,24 @@ sourceFirstLine, showGenericSourcePos, annotateErrorWithTransaction,+ transactionFile, -- * tests tests_Transaction ) where-import Data.List++import Data.Default (def)+import Data.List (intercalate, partition) import Data.List.Extra (nubSort)-import Data.Maybe+import Data.Maybe (fromMaybe, mapMaybe)+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar-import Text.Printf+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TB+import Data.Time.Calendar (Day, fromGregorian) import qualified Data.Map as M import Hledger.Utils@@ -72,6 +82,8 @@ import Hledger.Data.Posting import Hledger.Data.Amount import Hledger.Data.Valuation+import Text.Tabular+import Text.Tabular.AsciiWide sourceFilePath :: GenericSourcePos -> FilePath sourceFilePath = \case@@ -148,53 +160,46 @@ are displayed as multiple similar postings, one per commodity. (Normally does not happen with this function). -}-showTransaction :: Transaction -> String-showTransaction = showTransactionHelper False---- | Deprecated alias for 'showTransaction'-showTransactionUnelided :: Transaction -> String-showTransactionUnelided = showTransaction -- TODO: drop it+showTransaction :: Transaction -> Text+showTransaction = TL.toStrict . TB.toLazyText . showTransactionHelper False -- | Like showTransaction, but explicit multi-commodity amounts -- are shown on one line, comma-separated. In this case the output will -- not be parseable journal syntax.-showTransactionOneLineAmounts :: Transaction -> String-showTransactionOneLineAmounts = showTransactionHelper True---- | Deprecated alias for 'showTransactionOneLineAmounts'-showTransactionUnelidedOneLineAmounts :: Transaction -> String-showTransactionUnelidedOneLineAmounts = showTransactionOneLineAmounts -- TODO: drop it+showTransactionOneLineAmounts :: Transaction -> Text+showTransactionOneLineAmounts = TL.toStrict . TB.toLazyText . showTransactionHelper True -- | Helper for showTransaction*.-showTransactionHelper :: Bool -> Transaction -> String+showTransactionHelper :: Bool -> Transaction -> TB.Builder showTransactionHelper onelineamounts t =- unlines $ [descriptionline]- ++ newlinecomments- ++ (postingsAsLines onelineamounts (tpostings t))- ++ [""]- where- descriptionline = rstrip $ concat [date, status, code, desc, samelinecomment]- date = showDate (tdate t) ++ maybe "" (("="++) . showDate) (tdate2 t)- status | tstatus t == Cleared = " *"- | tstatus t == Pending = " !"- | otherwise = ""- code = if T.length (tcode t) > 0 then printf " (%s)" $ T.unpack $ tcode t else ""- desc = if null d then "" else " " ++ d where d = T.unpack $ tdescription t- (samelinecomment, newlinecomments) =- case renderCommentLines (tcomment t) of [] -> ("",[])- c:cs -> (c,cs)+ TB.fromText descriptionline <> newline+ <> foldMap ((<> newline) . TB.fromText) newlinecomments+ <> foldMap ((<> newline) . TB.fromText) (postingsAsLines onelineamounts $ tpostings t)+ <> newline+ where+ descriptionline = T.stripEnd $ T.concat [date, status, code, desc, samelinecomment]+ date = showDate (tdate t) <> maybe "" (("="<>) . showDate) (tdate2 t)+ status | tstatus t == Cleared = " *"+ | tstatus t == Pending = " !"+ | otherwise = ""+ code = if T.null (tcode t) then "" else wrap " (" ")" $ tcode t+ desc = if T.null d then "" else " " <> d where d = tdescription t+ (samelinecomment, newlinecomments) =+ case renderCommentLines (tcomment t) of [] -> ("",[])+ c:cs -> (c,cs)+ newline = TB.singleton '\n' -- | 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 -> [String]+renderCommentLines :: Text -> [Text] renderCommentLines t =- case lines $ T.unpack t of+ case T.lines t of [] -> []- [l] -> [(commentSpace . comment) l] -- single-line comment+ [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+ (l:ls) -> commentSpace (comment l) : map (lineIndent . comment) ls where- comment = ("; "++)+ comment = ("; "<>) -- | Given a transaction and its postings, render the postings, suitable -- for `print` output. Normally this output will be valid journal syntax which@@ -214,7 +219,7 @@ -- Posting amounts will be aligned with each other, starting about 4 columns -- beyond the widest account name (see postingAsLines for details). ---postingsAsLines :: Bool -> [Posting] -> [String]+postingsAsLines :: Bool -> [Posting] -> [Text] postingsAsLines onelineamounts ps = concatMap (postingAsLines False onelineamounts ps) ps -- | Render one posting, on one or more lines, suitable for `print` output.@@ -236,41 +241,55 @@ -- increased if needed to match the posting with the longest account name. -- This is used to align the amounts of a transaction's postings. ---postingAsLines :: Bool -> Bool -> [Posting] -> Posting -> [String]-postingAsLines elideamount onelineamounts pstoalignwith p = concat [- postingblock- ++ newlinecomments- | postingblock <- postingblocks]+postingAsLines :: Bool -> Bool -> [Posting] -> Posting -> [Text]+postingAsLines elideamount onelineamounts pstoalignwith p =+ concatMap (++ newlinecomments) postingblocks where- postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amt, assertion, samelinecomment] | amt <- shownAmounts]- assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p- statusandaccount = lineIndent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p- where- -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned- minwidth = maximum $ map ((2+) . textWidth . T.pack . pacctstr) pstoalignwith- pstatusandacct p' = pstatusprefix p' ++ pacctstr p'- pstatusprefix p' | null s = ""- | otherwise = s ++ " "- where s = show $ pstatus p'- pacctstr p' = showAccountName Nothing (ptype p') (paccount p')+ -- 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 [amt]+ , Cell BottomLeft [assertion]+ , textCell BottomLeft samelinecomment+ ]+ | amt <- shownAmounts]+ render = renderRow def{tableBorders=False, borderSpaces=False} . Group NoLine . map Header+ assertion = maybe mempty ((WideBuilder (TB.singleton ' ') 1 <>).showBalanceAssertion) $ pbalanceassertion p+ statusandaccount = lineIndent . fitText (Just $ minwidth) Nothing False True $ pstatusandacct p+ where+ -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned+ minwidth = maximum $ map ((2+) . textWidth . pacctstr) pstoalignwith+ pstatusandacct p' = pstatusprefix p' <> pacctstr p'+ pstatusprefix p' = case pstatus p' of+ Unmarked -> ""+ s -> T.pack (show s) <> " "+ pacctstr p' = showAccountName Nothing (ptype p') (paccount p') -- currently prices are considered part of the amount string when right-aligning amounts shownAmounts- | elideamount = [""]- | onelineamounts = [fst . showMixedOneLineUnnormalised showAmount (Just amtwidth) Nothing False $ pamount p]- | null (amounts $ pamount p) = [""]- | otherwise = lines . fst . showMixedUnnormalised showAmount (Just amtwidth) Nothing False $ pamount p+ | elideamount || null (amounts $ pamount p) = [mempty]+ | otherwise = showMixedAmountLinesB displayopts $ pamount p where- amtwidth = maximum $ 12 : map (snd . showMixedUnnormalised showAmount Nothing Nothing False . pamount) pstoalignwith -- min. 12 for backwards compatibility+ displayopts = noColour{displayOneLine=onelineamounts, displayMinWidth = Just amtwidth, displayNormalised=False}+ amtwidth = maximum $ 12 : map (wbWidth . showMixedAmountB displayopts{displayMinWidth=Nothing} . pamount) pstoalignwith -- min. 12 for backwards compatibility (samelinecomment, newlinecomments) = case renderCommentLines (pcomment p) of [] -> ("",[]) c:cs -> (c,cs) -- | Render a balance assertion, as the =[=][*] symbol and expected amount.-showBalanceAssertion :: BalanceAssertion -> [Char]+showBalanceAssertion :: BalanceAssertion -> WideBuilder showBalanceAssertion BalanceAssertion{..} =- "=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount+ singleton '=' <> eq <> ast <> singleton ' ' <> showAmountB def{displayZeroCommodity=True} baamount+ where+ eq = if batotal then singleton '=' else mempty+ ast = if bainclusive then singleton '*' else mempty+ singleton c = WideBuilder (TB.singleton c) 1 -- | Render a posting, simply. Used in balance assertion errors. -- showPostingLine p =@@ -286,33 +305,27 @@ -- | Render a posting, at the appropriate width for aligning with -- its siblings if any. Used by the rewrite command.-showPostingLines :: Posting -> [String]+showPostingLines :: Posting -> [Text] showPostingLines p = postingAsLines False False ps p where ps | Just t <- ptransaction p = tpostings t | otherwise = [p] -- | Prepend a suitable indent for a posting (or transaction/posting comment) line.-lineIndent :: String -> String-lineIndent = (" "++)+lineIndent :: Text -> Text+lineIndent = (" "<>) -- | Prepend the space required before a same-line comment.-commentSpace :: String -> String-commentSpace = (" "++)+commentSpace :: Text -> Text+commentSpace = (" "<>) -- | Show an account name, clipped to the given width if any, and -- appropriately bracketed/parenthesised for the given posting type.-showAccountName :: Maybe Int -> PostingType -> AccountName -> String+showAccountName :: Maybe Int -> PostingType -> AccountName -> Text showAccountName w = fmt where- fmt RegularPosting = maybe id take w . T.unpack- fmt VirtualPosting = parenthesise . maybe id (takeEnd . subtract 2) w . T.unpack- fmt BalancedVirtualPosting = bracket . maybe id (takeEnd . subtract 2) w . T.unpack--parenthesise :: String -> String-parenthesise s = "("++s++")"--bracket :: String -> String-bracket s = "["++s++"]"+ fmt RegularPosting = maybe id T.take w+ fmt VirtualPosting = wrap "(" ")" . maybe id (T.takeEnd . subtract 2) w+ fmt BalancedVirtualPosting = wrap "[" "]" . maybe id (T.takeEnd . subtract 2) w hasRealPostings :: Transaction -> Bool hasRealPostings = not . null . realPostings@@ -356,7 +369,7 @@ -- check for mixed signs, detecting nonzeros at display precision canonicalise = maybe id canonicaliseMixedAmount mstyles- signsOk ps = + signsOk ps = case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of nonzeros | length nonzeros >= 2 -> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1@@ -427,7 +440,9 @@ annotateErrorWithTransaction :: Transaction -> String -> String annotateErrorWithTransaction t s =- unlines [showGenericSourcePos $ tsourcepos t, s, rstrip $ showTransaction t]+ unlines [ showGenericSourcePos $ tsourcepos t, s+ , T.unpack . T.stripEnd $ showTransaction t+ ] -- | Infer up to one missing amount for this transactions's real postings, and -- likewise for its balanced virtual postings, if needed; or return an error@@ -540,8 +555,9 @@ = p{pamount=Mixed [a{aprice=Just conversionprice}], poriginal=Just $ originalPosting p} where fromcommodity = head $ filter (`elem` sumcommodities) pcommodities -- these heads are ugly but should be safe+ totalpricesign = if aquantity a < 0 then negate else id conversionprice- | fromcount==1 = TotalPrice $ abs toamount `withPrecision` NaturalPrecision+ | fromcount==1 = TotalPrice $ totalpricesign (abs toamount) `withPrecision` NaturalPrecision | otherwise = UnitPrice $ abs unitprice `withPrecision` unitprecision where fromcount = length $ filter ((==fromcommodity).acommodity) pamounts@@ -580,13 +596,19 @@ transactionTransformPostings :: (Posting -> Posting) -> Transaction -> Transaction transactionTransformPostings f t@Transaction{tpostings=ps} = t{tpostings=map f ps} +-- | Apply a specified costing and valuation to this transaction's amounts,+-- using the provided price oracle, commodity styles, and reference dates.+-- See amountApplyValuation and amountCost.+transactionApplyCostValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Costing -> Maybe ValuationType -> Transaction -> Transaction+transactionApplyCostValuation priceoracle styles periodlast today cost v =+ transactionTransformPostings (postingApplyCostValuation priceoracle styles periodlast today cost v)+ -- | Apply a specified valuation to this transaction's amounts, using--- the provided price oracle, commodity styles, reference dates, and--- whether this is for a multiperiod report or not. See--- amountApplyValuation.-transactionApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> Transaction -> ValuationType -> Transaction-transactionApplyValuation priceoracle styles periodlast mreportlast today ismultiperiod t v =- transactionTransformPostings (\p -> postingApplyValuation priceoracle styles periodlast mreportlast today ismultiperiod p v) t+-- the provided price oracle, commodity styles, and reference dates.+-- See amountApplyValuation.+transactionApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> ValuationType -> Transaction -> Transaction+transactionApplyValuation priceoracle styles periodlast today v =+ transactionTransformPostings (postingApplyValuation priceoracle styles periodlast today v) -- | Convert this transaction's amounts to cost, and apply the appropriate amount styles. transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction@@ -600,6 +622,21 @@ Right ps -> Right $ txnTieKnot $ t{tpostings=ps} Left err -> Left err +-- | Apply a transformation to a transaction's postings.+transactionMapPostings :: (Posting -> Posting) -> Transaction -> Transaction+transactionMapPostings f t@Transaction{tpostings=ps} = t{tpostings=map f ps}++-- | Apply a transformation to a transaction's posting amounts.+transactionMapPostingAmounts :: (Amount -> Amount) -> Transaction -> Transaction+transactionMapPostingAmounts f = transactionMapPostings (postingTransformAmount (mapMixedAmount f))++-- | The file path from which this transaction was parsed.+transactionFile :: Transaction -> FilePath+transactionFile Transaction{tsourcepos} =+ case tsourcepos of+ GenericSourcePos f _ _ -> f+ JournalSourcePos f _ -> f+ -- tests tests_Transaction :: TestTree@@ -678,7 +715,7 @@ Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` usd 5]} (fst <$> inferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` missingamt]}) @?= Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` usd 1]}- + , tests "showTransaction" [ test "null transaction" $ showTransaction nulltransaction @?= "0000-01-01\n\n" , test "non-null transaction" $ showTransaction@@ -701,7 +738,7 @@ } ] } @?=- unlines+ T.unlines [ "2012-05-14=2012-05-15 (code) desc ; tcomment1" , " ; tcomment2" , " * a $1.00"@@ -727,7 +764,7 @@ , posting {paccount = "assets:checking", pamount = Mixed [usd (-47.18)], ptransaction = Just t} ] in showTransaction t) @?=- (unlines+ (T.unlines [ "2007-01-28 coopportunity" , " expenses:food:groceries $47.18" , " assets:checking $-47.18"@@ -750,7 +787,7 @@ [ posting {paccount = "expenses:food:groceries", pamount = Mixed [usd 47.18]} , posting {paccount = "assets:checking", pamount = Mixed [usd (-47.19)]} ])) @?=- (unlines+ (T.unlines [ "2007-01-28 coopportunity" , " expenses:food:groceries $47.18" , " assets:checking $-47.19"@@ -771,7 +808,7 @@ "" [] [posting {paccount = "expenses:food:groceries", pamount = missingmixedamt}])) @?=- (unlines ["2007-01-28 coopportunity", " expenses:food:groceries", ""])+ (T.unlines ["2007-01-28 coopportunity", " expenses:food:groceries", ""]) , test "show a transaction with a priced commodityless amount" $ (showTransaction (txnTieKnot $@@ -789,7 +826,7 @@ [ posting {paccount = "a", pamount = Mixed [num 1 `at` (usd 2 `withPrecision` Precision 0)]} , posting {paccount = "b", pamount = missingmixedamt} ])) @?=- (unlines ["2010-01-01 x", " a 1 @ $2", " b", ""])+ (T.unlines ["2010-01-01 x", " a 1 @ $2", " b", ""]) ] , tests "balanceTransaction" [ test "detect unbalanced entry, sign error" $@@ -896,7 +933,7 @@ "" [] [ posting {paccount = "a", pamount = Mixed [usd 1 @@ eur 1]}- , posting {paccount = "a", pamount = Mixed [usd (-2) @@ eur 1]}+ , posting {paccount = "a", pamount = Mixed [usd (-2) @@ eur (-1)]} ]) ] , tests "isTransactionBalanced" [
Hledger/Data/TransactionModifier.hs view
@@ -26,7 +26,7 @@ import Hledger.Data.Transaction import Hledger.Query import Hledger.Data.Posting (commentJoin, commentAddTag)-import Hledger.Utils.Debug+import Hledger.Utils -- $setup -- >>> :set -XOverloadedStrings@@ -62,7 +62,8 @@ -- postings when certain other postings are present. -- -- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]}--- >>> test = either putStr (putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction nulldate+-- >>> import qualified Data.Text.IO as T+-- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction nulldate -- >>> test $ TransactionModifier "" ["pong" `post` usd 2] -- 0000-01-01 -- ping $1.00@@ -119,7 +120,7 @@ -- Approach 1: convert to a unit price and increase the display precision slightly -- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount -- Approach 2: multiply the total price (keeping it positive) as well as the quantity- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount+ Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` matchedamount in case acommodity pramount of "" -> Mixed as@@ -137,7 +138,7 @@ renderPostingCommentDates :: Posting -> Posting renderPostingCommentDates p = p { pcomment = comment' } where- dates = T.concat $ catMaybes [T.pack . showDate <$> pdate p, ("=" <>) . T.pack . showDate <$> pdate2 p]+ dates = T.concat $ catMaybes [showDate <$> pdate p, ("=" <>) . showDate <$> pdate2 p] comment' | T.null dates = pcomment p- | otherwise = ("[" <> dates <> "]") `commentJoin` pcomment p+ | otherwise = (wrap "[" "]" dates) `commentJoin` pcomment p
Hledger/Data/Types.hs view
@@ -132,6 +132,8 @@ instance Default Interval where def = NoInterval +type Payee = Text+ type AccountName = Text data AccountType =@@ -176,16 +178,16 @@ -- | An amount's per-unit or total cost/selling price in another -- commodity, as recorded in the journal entry eg with @ or @@. -- Docs call this "transaction price". The amount is always positive.-data AmountPrice = UnitPrice Amount | TotalPrice Amount+data AmountPrice = UnitPrice !Amount | TotalPrice !Amount deriving (Eq,Ord,Generic,Show) -- | Display style for an amount. data AmountStyle = AmountStyle {- ascommodityside :: Side, -- ^ does the symbol appear on the left or the right ?- ascommodityspaced :: Bool, -- ^ space between symbol and quantity ?- asprecision :: !AmountPrecision, -- ^ number of digits displayed after the decimal point- asdecimalpoint :: Maybe Char, -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"- asdigitgroups :: Maybe DigitGroupStyle -- ^ style for displaying digit groups, if any+ ascommodityside :: !Side, -- ^ does the symbol appear on the left or the right ?+ ascommodityspaced :: !Bool, -- ^ space between symbol and quantity ?+ asprecision :: !AmountPrecision, -- ^ number of digits displayed after the decimal point+ asdecimalpoint :: !(Maybe Char), -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"+ asdigitgroups :: !(Maybe DigitGroupStyle) -- ^ style for displaying digit groups, if any } deriving (Eq,Ord,Read,Generic) instance Show AmountStyle where@@ -197,6 +199,10 @@ (show asdecimalpoint) (show asdigitgroups) +-- | The "display precision" for a hledger amount, by which we mean+-- the number of decimal digits to display to the right of the decimal mark.+-- This can be from 0 to 255 digits (the maximum supported by the Decimal library),+-- or NaturalPrecision meaning "show all significant decimal digits". data AmountPrecision = Precision !Word8 | NaturalPrecision deriving (Eq,Ord,Read,Show,Generic) -- | A style for displaying digit groups in the integer part of a@@ -205,7 +211,7 @@ -- point), and the size of each group, starting with the one nearest -- the decimal point. The last group size is assumed to repeat. Eg, -- comma between thousands is DigitGroups ',' [3].-data DigitGroupStyle = DigitGroups Char [Word8]+data DigitGroupStyle = DigitGroups !Char ![Word8] deriving (Eq,Ord,Read,Show,Generic) type CommoditySymbol = Text@@ -216,12 +222,12 @@ } deriving (Show,Eq,Generic) --,Ord) data Amount = Amount {- acommodity :: CommoditySymbol, -- commodity symbol, or special value "AUTO"- aquantity :: Quantity, -- numeric quantity, or zero in case of "AUTO"- aismultiplier :: Bool, -- ^ kludge: a flag marking this amount and posting as a multiplier- -- in a TMPostingRule. In a regular Posting, should always be false.- astyle :: AmountStyle,- aprice :: Maybe AmountPrice -- ^ the (fixed, transaction-specific) price for this amount, if any+ acommodity :: !CommoditySymbol, -- commodity symbol, or special value "AUTO"+ aquantity :: !Quantity, -- numeric quantity, or zero in case of "AUTO"+ aismultiplier :: !Bool, -- ^ kludge: a flag marking this amount and posting as a multiplier+ -- in a TMPostingRule. In a regular Posting, should always be false.+ astyle :: !AmountStyle,+ aprice :: !(Maybe AmountPrice) -- ^ the (fixed, transaction-specific) price for this amount, if any } deriving (Eq,Ord,Generic,Show) newtype MixedAmount = Mixed [Amount] deriving (Eq,Ord,Generic,Show)@@ -453,6 +459,7 @@ ,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) ,jdeclaredaccounts :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation) ,jdeclaredaccounttypes :: M.Map AccountType [AccountName] -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts) ,jglobalcommoditystyles :: M.Map CommoditySymbol AmountStyle -- ^ per-commodity display styles declared globally, eg by command line option or import command@@ -481,6 +488,17 @@ -- | The id of a data format understood by hledger, eg @journal@ or @csv@. -- The --output-format option selects one of these for output. type StorageFormat = String++-- | Extra information found in a payee directive.+data PayeeDeclarationInfo = PayeeDeclarationInfo {+ pdicomment :: Text -- ^ any comment lines following the payee directive+ ,pditags :: [Tag] -- ^ tags extracted from the comment, if any+} deriving (Eq,Show,Generic)++nullpayeedeclarationinfo = PayeeDeclarationInfo {+ pdicomment = ""+ ,pditags = []+} -- | Extra information about an account that can be derived from -- its account directive (and the other account directives).
Hledger/Data/Valuation.hs view
@@ -13,12 +13,13 @@ {-# LANGUAGE DeriveGeneric #-} module Hledger.Data.Valuation (- ValuationType(..)+ Costing(..)+ ,ValuationType(..) ,PriceOracle ,journalPriceOracle- ,unsupportedValueThenError -- ,amountApplyValuation -- ,amountValueAtDate+ ,mixedAmountApplyCostValuation ,mixedAmountApplyValuation ,mixedAmountValueAtDate ,marketPriceReverse@@ -34,7 +35,6 @@ import Data.List.Extra (nubSortBy) import qualified Data.Map as M import qualified Data.Set as S-import Data.Maybe ( fromMaybe ) import qualified Data.Text as T import Data.Time.Calendar (Day, fromGregorian) import Data.MemoUgly (memo)@@ -46,20 +46,24 @@ import Hledger.Data.Amount import Hledger.Data.Dates (nulldate) import Hledger.Data.Commodity (showCommoditySymbol)+import Data.Maybe (fromMaybe)+import Text.Printf (printf) ------------------------------------------------------------------------------ -- Types +-- | Whether to convert amounts to cost.+data Costing = Cost | NoCost+ deriving (Show,Eq)+ -- | What kind of value conversion should be done on amounts ?--- CLI: --value=cost|then|end|now|DATE[,COMM]+-- CLI: --value=then|end|now|DATE[,COMM] data ValuationType =- AtCost (Maybe CommoditySymbol) -- ^ convert to cost commodity using transaction prices, then optionally to given commodity using market prices at posting date- | AtThen (Maybe CommoditySymbol) -- ^ convert to default or given valuation commodity, using market prices at each posting's date+ AtThen (Maybe CommoditySymbol) -- ^ convert to default or given valuation commodity, using market prices at each posting's date | AtEnd (Maybe CommoditySymbol) -- ^ convert to default or given valuation commodity, using market prices at period end(s) | AtNow (Maybe CommoditySymbol) -- ^ convert to default or given valuation commodity, using current market prices | AtDate Day (Maybe CommoditySymbol) -- ^ convert to default or given valuation commodity, using market prices on some date- | AtDefault (Maybe CommoditySymbol) -- ^ works like AtNow in single period reports, like AtEnd in multiperiod reports deriving (Show,Eq) -- | A price oracle is a magic memoising function that efficiently@@ -95,13 +99,25 @@ ------------------------------------------------------------------------------ -- Converting things to value +-- | Apply a specified costing and valuation to this mixed amount,+-- using the provided price oracle, commodity styles, and reference dates.+-- Costing is done first if requested, and after that any valuation.+-- See amountApplyValuation and amountCost.+mixedAmountApplyCostValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Day -> Costing -> Maybe ValuationType -> MixedAmount -> MixedAmount+mixedAmountApplyCostValuation priceoracle styles periodlast today postingdate cost v =+ valuation . costing+ where+ valuation = maybe id (mixedAmountApplyValuation priceoracle styles periodlast today postingdate) v+ costing = case cost of+ Cost -> styleMixedAmount styles . mixedAmountCost+ NoCost -> id+ -- | Apply a specified valuation to this mixed amount, using the--- provided price oracle, commodity styles, reference dates, and--- whether this is for a multiperiod report or not.+-- provided price oracle, commodity styles, and reference dates. -- See amountApplyValuation.-mixedAmountApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> ValuationType -> MixedAmount -> MixedAmount-mixedAmountApplyValuation priceoracle styles periodlast mreportlast today ismultiperiod v (Mixed as) =- Mixed $ map (amountApplyValuation priceoracle styles periodlast mreportlast today ismultiperiod v) as+mixedAmountApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Day -> ValuationType -> MixedAmount -> MixedAmount+mixedAmountApplyValuation priceoracle styles periodlast today postingdate v =+ mapMixedAmount (amountApplyValuation priceoracle styles periodlast today postingdate v) -- | Apply a specified valuation to this amount, using the provided -- price oracle, reference dates, and whether this is for a@@ -115,7 +131,7 @@ -- -- - a fixed date specified by the ValuationType itself -- (--value=DATE).--- +-- -- - the provided "period end" date - this is typically the last day -- of a subperiod (--value=end with a multi-period report), or of -- the specified report period or the journal (--value=end with a@@ -127,29 +143,17 @@ -- - the provided "today" date - (--value=now, or -V/X with no report -- end date). -- --- Note --value=then is not supported by this function, and will cause an error;--- use postingApplyValuation for that.--- -- This is all a bit complicated. See the reference doc at -- https://hledger.org/hledger.html#effect-of-valuation-on-reports -- (hledger_options.m4.md "Effect of valuation on reports"), and #1083. ---amountApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> ValuationType -> Amount -> Amount-amountApplyValuation priceoracle styles periodlast mreportlast today ismultiperiod v a =+amountApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Day -> ValuationType -> Amount -> Amount+amountApplyValuation priceoracle styles periodlast today postingdate v a = case v of- AtCost Nothing -> styleAmount styles $ amountCost a- AtCost mc -> amountValueAtDate priceoracle styles mc periodlast $ styleAmount styles $ amountCost a- AtThen _mc -> error' unsupportedValueThenError -- PARTIAL:- -- amountValueAtDate priceoracle styles mc periodlast a -- posting date unknown, handle like AtEnd- AtEnd mc -> amountValueAtDate priceoracle styles mc periodlast a- AtNow mc -> amountValueAtDate priceoracle styles mc today a- AtDefault mc | ismultiperiod -> amountValueAtDate priceoracle styles mc periodlast a- AtDefault mc -> amountValueAtDate priceoracle styles mc (fromMaybe today mreportlast) a- AtDate d mc -> amountValueAtDate priceoracle styles mc d a---- | Standard error message for a report not supporting --value=then.-unsupportedValueThenError :: String-unsupportedValueThenError = "Sorry, --value=then is not yet supported for this kind of report."+ AtThen mc -> amountValueAtDate priceoracle styles mc postingdate a+ AtEnd mc -> amountValueAtDate priceoracle styles mc periodlast a+ AtNow mc -> amountValueAtDate priceoracle styles mc today a+ AtDate d mc -> amountValueAtDate priceoracle styles mc d a -- | Find the market value of each component amount in the given -- commodity, or its default valuation commodity, at the given@@ -157,7 +161,7 @@ -- When market prices available on that date are not sufficient to -- calculate the value, amounts are left unchanged. mixedAmountValueAtDate :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Maybe CommoditySymbol -> Day -> MixedAmount -> MixedAmount-mixedAmountValueAtDate priceoracle styles mc d (Mixed as) = Mixed $ map (amountValueAtDate priceoracle styles mc d) as+mixedAmountValueAtDate priceoracle styles mc d = mapMixedAmount (amountValueAtDate priceoracle styles mc d) -- | Find the market value of this amount in the given valuation -- commodity if any, otherwise the default valuation commodity, at the@@ -220,7 +224,7 @@ Just to -> -- We have a commodity to convert to. Find the most direct price available, -- according to the rules described in makePriceGraph.- let msg = "seeking " ++ pshowedge' "" from to ++ " price"+ let msg = printf "seeking %s to %s price" (showCommoditySymbol from) (showCommoditySymbol to) in case (traceAt 2 (msg++" using forward prices") $ pricesShortestPath from to forwardprices)@@ -285,20 +289,25 @@ -- USD->EUR price and one EUR->USD price. pricesShortestPath :: CommoditySymbol -> CommoditySymbol -> [Edge] -> Maybe Path pricesShortestPath start end edges =- dbg2 ("shortest "++pshowedge' "" start end++" price path") $+ -- 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 ""))) $+ find [([],edges)]+ where -- Find the first and shortest complete path using a breadth-first search. find :: [(Path,[Edge])] -> Maybe Path find paths = case concatMap extend paths of [] -> Nothing - _ | iteration > maxiterations -> - trace ("gave up searching for a price chain after "++show maxiterations++" iterations, please report a bug")+ _ | pathlength > maxpathlength -> + trace ("gave up searching for a price chain at length "++show maxpathlength++", please report a bug") Nothing where - iteration = 1 + maybe 0 (length . fst) (headMay paths)- maxiterations = 1000+ pathlength = 2 + maybe 0 (length . fst) (headMay paths)+ maxpathlength = 1000 paths' -> case completepaths of p:_ -> Just p -- the left-most complete path at this length@@ -329,7 +338,7 @@ p@(e:_) -> prefix label $ pshownode (mpfrom e) ++ ">" ++ intercalate ">" (map (pshownode . mpto) p) -- pshowedges label = prefix label . intercalate ", " . map (pshowedge "") -- pshowedge label MarketPrice{..} = pshowedge' label mpfrom mpto-pshowedge' label from to = prefix label $ pshownode from ++ ">" ++ pshownode to+-- pshowedge' label from to = prefix label $ pshownode from ++ ">" ++ pshownode to pshownode = T.unpack . showCommoditySymbol prefix l = if null l then (""++) else ((l++": ")++) @@ -356,7 +365,7 @@ -- -- 1. A *declared market price* or *inferred market price*: -- A's latest market price in B on or before the valuation date--- as declared by a P directive, or (with the `--infer-value` flag)+-- as declared by a P directive, or (with the `--infer-market-price` flag) -- inferred from transaction prices. -- -- 2. A *reverse market price*:@@ -381,7 +390,7 @@ -- prices before the valuation date.) -- -- 3. If there are no P directives at all (any commodity or date), and--- the `--infer-value` flag is used, then the price commodity from+-- the `--infer-market-price` flag is used, then the price commodity from -- the latest transaction price for A on or before valuation date." -- makePriceGraph :: [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph@@ -420,7 +429,7 @@ where ps | not $ null visibledeclaredprices = visibledeclaredprices | not $ null alldeclaredprices = alldeclaredprices- | otherwise = visibleinferredprices -- will be null without --infer-value+ | otherwise = visibleinferredprices -- will be null without --infer-market-price -- | Given a list of P-declared market prices in parse order and a -- list of transaction-inferred market prices in parse order, select
Hledger/Query.hs view
@@ -44,6 +44,8 @@ inAccountQuery, -- * matching matchesTransaction,+ matchesDescription,+ matchesPayeeWIP, matchesPosting, matchesAccount, matchesMixedAmount,@@ -66,6 +68,7 @@ #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>)) #endif+import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar (Day, fromGregorian ) import Safe (readDef, readMay, maximumByMay, maximumMay, minimumMay)@@ -107,11 +110,11 @@ instance Default Query where def = Any -- | Construct a payee tag-payeeTag :: Maybe String -> Either RegexError Query+payeeTag :: Maybe Text -> Either RegexError Query payeeTag = fmap (Tag (toRegexCI' "payee")) . maybe (pure Nothing) (fmap Just . toRegexCI) -- | Construct a note tag-noteTag :: Maybe String -> Either RegexError Query+noteTag :: Maybe Text -> Either RegexError Query noteTag = fmap (Tag (toRegexCI' "note")) . maybe (pure Nothing) (fmap Just . toRegexCI) -- | Construct a generated-transaction tag@@ -262,11 +265,11 @@ Right (Left m) -> Right $ Left $ Not m Right (Right _) -> Right $ Left Any -- not:somequeryoption will be ignored Left err -> Left err-parseQueryTerm _ (T.stripPrefix "code:" -> Just s) = Left . Code <$> toRegexCI (T.unpack s)-parseQueryTerm _ (T.stripPrefix "desc:" -> Just s) = Left . Desc <$> toRegexCI (T.unpack s)-parseQueryTerm _ (T.stripPrefix "payee:" -> Just s) = Left <$> payeeTag (Just $ T.unpack s)-parseQueryTerm _ (T.stripPrefix "note:" -> Just s) = Left <$> noteTag (Just $ T.unpack s)-parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left . Acct <$> toRegexCI (T.unpack s)+parseQueryTerm _ (T.stripPrefix "code:" -> Just s) = Left . Code <$> toRegexCI s+parseQueryTerm _ (T.stripPrefix "desc:" -> Just s) = Left . Desc <$> toRegexCI s+parseQueryTerm _ (T.stripPrefix "payee:" -> Just s) = Left <$> payeeTag (Just s)+parseQueryTerm _ (T.stripPrefix "note:" -> Just s) = Left <$> noteTag (Just s)+parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left . Acct <$> toRegexCI s parseQueryTerm d (T.stripPrefix "date2:" -> Just s) = case parsePeriodExpr d s of Left e -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e Right (_,span) -> Right $ Left $ Date2 span@@ -283,7 +286,7 @@ | otherwise = Left "depth: should have a positive number" where n = readDef 0 (T.unpack s) -parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Left . Sym <$> toRegexCI ('^' : T.unpack s ++ "$") -- support cur: as an alias+parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Left . Sym <$> toRegexCI ("^" <> s <> "$") -- support cur: as an alias parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = Left <$> parseTag s parseQueryTerm _ "" = Right $ Left $ Any parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s@@ -322,20 +325,19 @@ (parse ">" -> Just q) -> Right (AbsGt ,q) (parse "=" -> Just q) -> Right (AbsEq ,q) (parse "" -> Just q) -> Right (AbsEq ,q)- _ -> Left $- "could not parse as a comparison operator followed by an optionally-signed number: "- ++ T.unpack amtarg+ _ -> Left . T.unpack $+ "could not parse as a comparison operator followed by an optionally-signed number: " <> amtarg where -- Strip outer whitespace from the text, require and remove the -- specified prefix, remove all whitespace from the remainder, and -- read it as a simple integer or decimal if possible. parse :: T.Text -> T.Text -> Maybe Quantity- parse p s = (T.stripPrefix p . T.strip) s >>= readMay . filter (not.(==' ')) . T.unpack+ parse p s = (T.stripPrefix p . T.strip) s >>= readMay . T.unpack . T.filter (/=' ') parseTag :: T.Text -> Either RegexError Query parseTag s = do- tag <- toRegexCI . T.unpack $ if T.null v then s else n- body <- if T.null v then pure Nothing else Just <$> toRegexCI (tail $ T.unpack v)+ tag <- toRegexCI $ if T.null v then s else n+ body <- if T.null v then pure Nothing else Just <$> toRegexCI (T.tail v) return $ Tag tag body where (n,v) = T.break (=='=') s @@ -554,7 +556,7 @@ matchesAccount (Not m) a = not $ matchesAccount m a matchesAccount (Or ms) a = any (`matchesAccount` a) ms matchesAccount (And ms) a = all (`matchesAccount` a) ms-matchesAccount (Acct r) a = regexMatch r $ T.unpack a -- XXX pack+matchesAccount (Acct r) a = regexMatchText r a matchesAccount (Depth d) a = accountNameLevel a <= d matchesAccount (Tag _ _) _ = False matchesAccount _ _ = True@@ -564,7 +566,7 @@ matchesMixedAmount q (Mixed as) = any (q `matchesAmount`) as matchesCommodity :: Query -> CommoditySymbol -> Bool-matchesCommodity (Sym r) = regexMatch r . T.unpack+matchesCommodity (Sym r) = regexMatchText r matchesCommodity _ = const True -- | Does the match expression match this (simple) amount ?@@ -603,10 +605,10 @@ matchesPosting (None) _ = False matchesPosting (Or qs) p = any (`matchesPosting` p) qs matchesPosting (And qs) p = all (`matchesPosting` p) qs-matchesPosting (Code r) p = regexMatch r $ maybe "" (T.unpack . tcode) $ ptransaction p-matchesPosting (Desc r) p = regexMatch r $ maybe "" (T.unpack . tdescription) $ ptransaction p+matchesPosting (Code r) p = maybe False (regexMatchText r . tcode) $ ptransaction p+matchesPosting (Desc r) p = maybe False (regexMatchText r . tdescription) $ ptransaction p matchesPosting (Acct r) p = matches p || matches (originalPosting p)- where matches p = regexMatch r . T.unpack $ paccount p -- XXX pack+ where matches = regexMatchText r . paccount matchesPosting (Date span) p = span `spanContainsDate` postingDate p matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p matchesPosting (StatusQ s) p = postingStatus p == s@@ -615,8 +617,8 @@ matchesPosting q@(Amt _ _) Posting{pamount=amt} = q `matchesMixedAmount` amt matchesPosting (Sym r) Posting{pamount=Mixed as} = any (matchesCommodity (Sym r)) $ map acommodity as matchesPosting (Tag n v) p = case (reString n, v) of- ("payee", Just v) -> maybe False (regexMatch v . T.unpack . transactionPayee) $ ptransaction p- ("note", Just v) -> maybe False (regexMatch v . T.unpack . transactionNote) $ ptransaction p+ ("payee", Just v) -> maybe False (regexMatchText v . transactionPayee) $ ptransaction p+ ("note", Just v) -> maybe False (regexMatchText v . transactionNote) $ ptransaction p (_, v) -> matchesTags n v $ postingAllTags p -- | Does the match expression match this transaction ?@@ -626,8 +628,8 @@ matchesTransaction (None) _ = False matchesTransaction (Or qs) t = any (`matchesTransaction` t) qs matchesTransaction (And qs) t = all (`matchesTransaction` t) qs-matchesTransaction (Code r) t = regexMatch r $ T.unpack $ tcode t-matchesTransaction (Desc r) t = regexMatch r $ T.unpack $ tdescription t+matchesTransaction (Code r) t = regexMatchText r $ tcode t+matchesTransaction (Desc r) t = regexMatchText r $ tdescription t matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction (Date span) t = spanContainsDate span $ tdate t matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t@@ -637,15 +639,42 @@ matchesTransaction (Depth d) t = any (Depth d `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) -> regexMatch v . T.unpack . transactionPayee $ t- ("note", Just v) -> regexMatch v . T.unpack . transactionNote $ t+ ("payee", Just v) -> regexMatchText v $ transactionPayee t+ ("note", Just v) -> regexMatchText v $ transactionNote t (_, v) -> matchesTags n v $ transactionAllTags t +-- | Does the query match this transaction description ?+-- Tests desc: terms, any other terms are ignored.+matchesDescription :: Query -> Text -> Bool+matchesDescription (Not q) d = not $ q `matchesDescription` d+matchesDescription (Any) _ = True+matchesDescription (None) _ = False+matchesDescription (Or qs) d = any (`matchesDescription` d) $ filter queryIsDesc qs+matchesDescription (And qs) d = all (`matchesDescription` d) $ filter queryIsDesc qs+matchesDescription (Code _) _ = False+matchesDescription (Desc r) d = regexMatchText r d+matchesDescription (Acct _) _ = False+matchesDescription (Date _) _ = False+matchesDescription (Date2 _) _ = False+matchesDescription (StatusQ _) _ = False+matchesDescription (Real _) _ = False+matchesDescription (Amt _ _) _ = False+matchesDescription (Depth _) _ = False+matchesDescription (Sym _) _ = False+matchesDescription (Tag _ _) _ = False++-- | Does the query match this transaction payee ?+-- Tests desc: (and payee: ?) terms, any other terms are ignored.+-- XXX Currently an alias for matchDescription. I'm not sure if more is needed,+-- There's some shenanigan with payee: and "payeeTag" to figure out.+matchesPayeeWIP :: Query -> Payee -> Bool+matchesPayeeWIP q p = matchesDescription q p+ -- | Does the query match the name and optionally the value of any of these tags ? matchesTags :: Regexp -> Maybe Regexp -> [Tag] -> Bool matchesTags namepat valuepat = not . null . filter (matches namepat valuepat) where- matches npat vpat (n,v) = regexMatch npat (T.unpack n) && maybe (const True) regexMatch vpat (T.unpack v)+ matches npat vpat (n,v) = regexMatchText npat n && maybe (const True) regexMatchText vpat v -- | Does the query match this market price ? matchesPriceDirective :: Query -> PriceDirective -> Bool
Hledger/Read.hs view
@@ -11,8 +11,9 @@ -} --- ** language-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PackageImports #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-} {-# LANGUAGE ScopedTypeVariables #-} --- ** exports@@ -53,9 +54,13 @@ import Data.List.NonEmpty (nonEmpty) import Data.Maybe (fromMaybe) import Data.Ord (comparing)+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif import Data.Semigroup (sconcat) import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.IO as T import Data.Time (Day) import Safe (headDef) import System.Directory (doesFileExist, getHomeDirectory)@@ -63,8 +68,7 @@ import System.Exit (exitFailure) import System.FilePath ((<.>), (</>), splitDirectories, splitFileName) import System.Info (os)-import System.IO (stderr, writeFile)-import Text.Printf (hPrintf, printf)+import System.IO (hPutStr, stderr) import Hledger.Data.Dates (getCurrentDay, parsedateM, showDate) import Hledger.Data.Types@@ -191,9 +195,9 @@ requireJournalFileExists f = do exists <- doesFileExist f when (not exists) $ do -- XXX might not be a journal file- hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f- hPrintf stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"- hPrintf stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n"+ hPutStr stderr $ "The hledger journal file \"" <> show f <> "\" was not found.\n"+ hPutStr stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"+ hPutStr stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n" exitFailure -- | Ensure there is a journal file at the given path, creating an empty one if needed.@@ -202,14 +206,14 @@ ensureJournalFileExists :: FilePath -> IO () ensureJournalFileExists f = do when (os/="mingw32" && isWindowsUnsafeDotPath f) $ do- hPrintf stderr "Part of file path %s\n ends with a dot, which is unsafe on Windows; please use a different path.\n" (show f)+ hPutStr stderr $ "Part of file path \"" <> show f <> "\"\n ends with a dot, which is unsafe on Windows; please use a different path.\n" exitFailure exists <- doesFileExist f when (not exists) $ do- hPrintf stderr "Creating hledger journal file %s.\n" f+ hPutStr stderr $ "Creating hledger journal file " <> show f <> ".\n" -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, -- we currently require unix line endings on all platforms.- newJournalContent >>= writeFile f+ newJournalContent >>= T.writeFile f -- | Does any part of this path contain non-. characters and end with a . ? -- Such paths are not safe to use on Windows (cf #1056).@@ -221,10 +225,10 @@ splitDirectories -- | Give the content for a new auto-created journal file.-newJournalContent :: IO String+newJournalContent :: IO Text newJournalContent = do d <- getCurrentDay- return $ printf "; journal created %s by hledger\n" (show d)+ return $ "; journal created " <> T.pack (show d) <> " by hledger\n" -- A "LatestDates" is zero or more copies of the same date, -- representing the latest transaction date read from a file,@@ -240,7 +244,7 @@ -- | Remember that these transaction dates were the latest seen when -- reading this journal file. saveLatestDates :: LatestDates -> FilePath -> IO ()-saveLatestDates dates f = writeFile (latestDatesFileFor f) $ unlines $ map showDate dates+saveLatestDates dates f = T.writeFile (latestDatesFileFor f) $ T.unlines $ map showDate dates -- | What were the latest transaction dates seen the last time this -- journal file was read ? If there were multiple transactions on the
Hledger/Read/Common.hs view
@@ -45,6 +45,9 @@ parseAndFinaliseJournal, parseAndFinaliseJournal', journalFinalise,+ journalCheckAccountsDeclared,+ journalCheckCommoditiesDeclared,+ journalCheckPayeesDeclared, setYear, getYear, setDefaultCommodityAndStyle,@@ -149,6 +152,7 @@ import Hledger.Data import Hledger.Utils import Safe (headMay)+import Text.Printf (printf) --- ** doctest setup -- $setup@@ -368,6 +372,22 @@ ) & fmap journalInferMarketPricesFromTransactions -- infer market prices from commodity-exchanging transactions +-- | Check that all the journal's transactions have payees declared with+-- payee directives, returning an error message otherwise.+journalCheckPayeesDeclared :: Journal -> Either String ()+journalCheckPayeesDeclared j = sequence_ $ map checkpayee $ jtxns j+ where+ checkpayee t+ | p `elem` ps = Right ()+ | otherwise = Left $+ printf "undeclared payee \"%s\"\nat: %s\n\n%s"+ (T.unpack p)+ (showGenericSourcePos $ tsourcepos t)+ (linesPrepend2 "> " " " . (<>"\n") . textChomp $ showTransaction t)+ where+ p = transactionPayee t+ ps = journalPayeesDeclared j+ -- | Check that all the journal's postings are to accounts declared with -- account directives, returning an error message otherwise. journalCheckAccountsDeclared :: Journal -> Either String ()@@ -375,11 +395,13 @@ where checkacct Posting{paccount,ptransaction} | paccount `elem` as = Right ()- | otherwise = - Left $ "\nstrict mode: undeclared account \""++T.unpack paccount++"\""- ++ case ptransaction of- Just Transaction{tsourcepos} -> "\nin transaction at: "++showGenericSourcePos tsourcepos- Nothing -> ""+ | otherwise = Left $+ (printf "undeclared account \"%s\"\n" (T.unpack paccount))+ ++ case ptransaction of+ Nothing -> ""+ Just t -> printf "in transaction at: %s\n\n%s"+ (showGenericSourcePos $ tsourcepos t)+ (linesPrepend " " . (<>"\n") . textChomp $ showTransaction t) where as = journalAccountNamesDeclared j @@ -392,18 +414,21 @@ checkcommodities Posting{..} = case mfirstundeclaredcomm of Nothing -> Right ()- Just c -> Left $ - "\nstrict mode: undeclared commodity \""++T.unpack c++"\""+ Just c -> Left $+ (printf "undeclared commodity \"%s\"\n" (T.unpack c)) ++ case ptransaction of- Just Transaction{tsourcepos} -> "\nin transaction at: "++showGenericSourcePos tsourcepos- Nothing -> "" + Nothing -> ""+ Just t -> printf "in transaction at: %s\n\n%s"+ (showGenericSourcePos $ tsourcepos t)+ (linesPrepend " " . (<>"\n") . textChomp $ showTransaction t) where- mfirstundeclaredcomm = + mfirstundeclaredcomm = headMay $ filter (not . (`elem` cs)) $ catMaybes $ (acommodity . baamount <$> pbalanceassertion) : (map (Just . acommodity) . filter (/= missingamt) $ amounts pamount) cs = journalCommoditiesDeclared j + setYear :: Year -> JournalParser m () setYear y = modify' (\j -> j{jparsedefaultyear=Just y}) @@ -732,7 +757,7 @@ spaces = lift $ skipNonNewlineSpaces amount <- amountwithoutpricep <* spaces (mprice, _elotprice, _elotdate) <- runPermutation $- (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp <* spaces)+ (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amount <* spaces) <*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces) <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces) pure $ amount { aprice = mprice }@@ -742,7 +767,7 @@ let spaces = lift $ skipNonNewlineSpaces amount <- amountwithoutpricep spaces- mprice <- optional $ priceamountp <* spaces+ mprice <- optional $ priceamountp amount <* spaces pure $ amount { aprice = mprice } amountwithoutpricep :: JournalParser m Amount@@ -757,6 +782,7 @@ c <- lift commoditysymbolp mdecmarkStyle <- getDecimalMarkStyle mcommodityStyle <- getAmountStyle c+ -- XXX amounts of this commodity in periodic transaction rules and auto posting rules ? #1461 let suggestedStyle = mdecmarkStyle <|> mcommodityStyle commodityspaced <- lift skipNonNewlineSpaces' sign2 <- lift $ signp@@ -782,6 +808,7 @@ Just (commodityspaced, c) -> do mdecmarkStyle <- getDecimalMarkStyle mcommodityStyle <- getAmountStyle c+ -- XXX amounts of this commodity in periodic transaction rules and auto posting rules ? #1461 let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}@@ -793,6 +820,7 @@ mdecmarkStyle <- getDecimalMarkStyle -- a decimal-mark CSV rule mcommodityStyle <- getAmountStyle "" -- a commodity directive for the no-symbol commodity mdefaultStyle <- getDefaultAmountStyle -- a D default commodity directive+ -- XXX no-symbol amounts in periodic transaction rules and auto posting rules ? #1461 let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle <|> mdefaultStyle (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent -- if a default commodity has been set, apply it and its style to this amount@@ -849,19 +877,25 @@ simplecommoditysymbolp :: TextParser m CommoditySymbol simplecommoditysymbolp = takeWhile1P Nothing (not . isNonsimpleCommodityChar) -priceamountp :: JournalParser m AmountPrice-priceamountp = label "transaction price" $ do+priceamountp :: Amount -> JournalParser m AmountPrice+priceamountp baseAmt = label "transaction price" $ do -- https://www.ledger-cli.org/3.0/doc/ledger3.html#Virtual-posting-costs parenthesised <- option False $ char '(' >> pure True char '@'- priceConstructor <- char '@' *> pure TotalPrice <|> pure UnitPrice+ totalPrice <- char '@' *> pure True <|> pure False when parenthesised $ void $ char ')' lift skipNonNewlineSpaces priceAmount <- amountwithoutpricep -- <?> "unpriced amount (specifying a price)" - pure $ priceConstructor priceAmount+ let amtsign' = signum $ aquantity baseAmt+ amtsign = if amtsign' == 0 then 1 else amtsign' + pure $ if totalPrice+ then TotalPrice priceAmount{aquantity=amtsign * aquantity priceAmount}+ else UnitPrice priceAmount++ balanceassertionp :: JournalParser m BalanceAssertion balanceassertionp = do sourcepos <- genericSourcePos <$> lift getSourcePos@@ -1122,7 +1156,7 @@ digitgroupp = label "digits" $ makeGroup <$> takeWhile1P (Just "digit") isDigit where- makeGroup = uncurry DigitGrp . foldl' step (0, 0) . T.unpack+ makeGroup = uncurry DigitGrp . T.foldl' step (0, 0) step (!l, !a) c = (l+1, a*10 + fromIntegral (digitToInt c)) --- *** comments@@ -1461,7 +1495,7 @@ char '=' skipNonNewlineSpaces repl <- anySingle `manyTill` eolof- case toRegexCI re of+ case toRegexCI $ T.pack re of Right r -> return $! RegexAlias r repl Left e -> customFailure $! parseErrorAtRegion off1 off2 e
Hledger/Read/CsvReader.hs view
@@ -11,17 +11,17 @@ -- stack haddock hledger-lib --fast --no-haddock-deps --haddock-arguments='--ignore-all-exports' --open --- ** language-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE ViewPatterns #-} --- ** exports module Hledger.Read.CsvReader (@@ -52,7 +52,6 @@ import Data.Char (toLower, isDigit, isSpace, isAlphaNum, isAscii, ord) import Data.Bifunctor (first) import "base-compat-batteries" Data.List.Compat-import qualified Data.List.Split as LS (splitOn) import Data.Maybe (catMaybes, fromMaybe, isJust) import Data.MemoUgly (memo) import Data.Ord (comparing)@@ -61,6 +60,8 @@ import qualified Data.Text as T import qualified Data.Text.Encoding 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.Calendar (Day) import Data.Time.Format (parseTimeM, defaultTimeLocale) import Safe (atMay, headMay, lastMay, readDef, readMay)@@ -88,7 +89,7 @@ type CSV = [CsvRecord] type CsvRecord = [CsvValue]-type CsvValue = String+type CsvValue = Text --- ** reader @@ -164,7 +165,7 @@ ," account2 assets:bank:savings\n" ] -addDirective :: (DirectiveName, String) -> CsvRulesParsed -> CsvRulesParsed+addDirective :: (DirectiveName, Text) -> CsvRulesParsed -> CsvRulesParsed addDirective d r = r{rdirectives=d:rdirectives r} addAssignment :: (HledgerFieldName, FieldTemplate) -> CsvRulesParsed -> CsvRulesParsed@@ -181,7 +182,7 @@ where maybeAddAssignment rules f = (maybe id addAssignmentFromIndex $ elemIndex f fs) rules where- addAssignmentFromIndex i = addAssignment (f, "%"++show (i+1))+ addAssignmentFromIndex i = addAssignment (f, T.pack $ '%':show (i+1)) addConditionalBlock :: ConditionalBlock -> CsvRulesParsed -> CsvRulesParsed addConditionalBlock b r = r{rconditionalblocks=b:rconditionalblocks r}@@ -205,7 +206,7 @@ case line of (T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< T.readFile f' where- f' = dir </> dropWhile isSpace (T.unpack f)+ f' = dir </> T.unpack (T.dropWhile isSpace f) dir' = takeDirectory f' _ -> return line @@ -240,7 +241,7 @@ -- | A set of data definitions and account-matching patterns sufficient to -- convert a particular CSV data file into meaningful journal transactions. data CsvRules' a = CsvRules' {- rdirectives :: [(DirectiveName,String)],+ rdirectives :: [(DirectiveName,Text)], -- ^ top-level rules, as (keyword, value) pairs rcsvfieldindexes :: [(CsvFieldName, CsvFieldIndex)], -- ^ csv field names and their column number, if declared by a fields list@@ -260,7 +261,7 @@ -- | Type used after parsing is done. Directives, assignments and conditional blocks -- are in the same order as they were in the unput file and rblocksassigning is functional. -- Ready to be used for CSV record processing-type CsvRules = CsvRules' (String -> [ConditionalBlock])+type CsvRules = CsvRules' (Text -> [ConditionalBlock]) instance Eq CsvRules where r1 == r2 = (rdirectives r1, rcsvfieldindexes r1, rassignments r1) ==@@ -277,27 +278,27 @@ type CsvRulesParser a = StateT CsvRulesParsed SimpleTextParser a -- | The keyword of a CSV rule - "fields", "skip", "if", etc.-type DirectiveName = String+type DirectiveName = Text -- | CSV field name.-type CsvFieldName = String+type CsvFieldName = Text -- | 1-based CSV column number. type CsvFieldIndex = Int -- | Percent symbol followed by a CSV field name or column number. Eg: %date, %1.-type CsvFieldReference = String+type CsvFieldReference = Text -- | One of the standard hledger fields or pseudo-fields that can be assigned to. -- Eg date, account1, amount, amount1-in, date-format.-type HledgerFieldName = String+type HledgerFieldName = Text -- | A text value to be assigned to a hledger field, possibly -- containing csv field references to be interpolated.-type FieldTemplate = String+type FieldTemplate = Text -- | A strptime date parsing pattern, as supported by Data.Time.Format.-type DateFormat = String+type DateFormat = Text -- | A prefix for a matcher test, either & or none (implicit or). data MatcherPrefix = And | None@@ -453,16 +454,16 @@ commentcharp :: CsvRulesParser Char commentcharp = oneOf (";#*" :: [Char]) -directivep :: CsvRulesParser (DirectiveName, String)+directivep :: CsvRulesParser (DirectiveName, Text) directivep = (do lift $ dbgparse 8 "trying directive"- d <- fmap T.unpack $ choiceInState $ map (lift . string . T.pack) directives+ d <- choiceInState $ map (lift . string) directives v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp) <|> (optional (char ':') >> lift skipNonNewlineSpaces >> lift eolof >> return "") return (d, v) ) <?> "directive" -directives :: [String]+directives :: [Text] directives = ["date-format" ,"decimal-mark"@@ -474,8 +475,8 @@ , "balance-type" ] -directivevalp :: CsvRulesParser String-directivevalp = anySingle `manyTill` lift eolof+directivevalp :: CsvRulesParser Text+directivevalp = T.pack <$> anySingle `manyTill` lift eolof fieldnamelistp :: CsvRulesParser [CsvFieldName] fieldnamelistp = (do@@ -487,21 +488,18 @@ f <- fromMaybe "" <$> optional fieldnamep fs <- some $ (separator >> fromMaybe "" <$> optional fieldnamep) lift restofline- return $ map (map toLower) $ f:fs+ return . map T.toLower $ f:fs ) <?> "field name list" -fieldnamep :: CsvRulesParser String+fieldnamep :: CsvRulesParser Text fieldnamep = quotedfieldnamep <|> barefieldnamep -quotedfieldnamep :: CsvRulesParser String-quotedfieldnamep = do- char '"'- f <- some $ noneOf ("\"\n:;#~" :: [Char])- char '"'- return f+quotedfieldnamep :: CsvRulesParser Text+quotedfieldnamep =+ char '"' *> takeWhile1P Nothing (`notElem` ("\"\n:;#~" :: [Char])) <* char '"' -barefieldnamep :: CsvRulesParser String-barefieldnamep = some $ noneOf (" \t\n,;#~" :: [Char])+barefieldnamep :: CsvRulesParser Text+barefieldnamep = takeWhile1P Nothing (`notElem` (" \t\n,;#~" :: [Char])) fieldassignmentp :: CsvRulesParser (HledgerFieldName, FieldTemplate) fieldassignmentp = do@@ -513,10 +511,10 @@ return (f,v) <?> "field assignment" -journalfieldnamep :: CsvRulesParser String+journalfieldnamep :: CsvRulesParser Text journalfieldnamep = do lift (dbgparse 8 "trying journalfieldnamep")- T.unpack <$> choiceInState (map (lift . string . T.pack) journalfieldnames)+ choiceInState $ map (lift . string) journalfieldnames maxpostings = 99 @@ -524,14 +522,14 @@ -- Names must precede any other name they contain, for the parser -- (amount-in before amount; date2 before date). TODO: fix journalfieldnames =- concat [[ "account" ++ i- ,"amount" ++ i ++ "-in"- ,"amount" ++ i ++ "-out"- ,"amount" ++ i- ,"balance" ++ i- ,"comment" ++ i- ,"currency" ++ i- ] | x <- [maxpostings, (maxpostings-1)..1], let i = show x]+ concat [[ "account" <> i+ ,"amount" <> i <> "-in"+ ,"amount" <> i <> "-out"+ ,"amount" <> i+ ,"balance" <> i+ ,"comment" <> i+ ,"currency" <> i+ ] | x <- [maxpostings, (maxpostings-1)..1], let i = T.pack $ show x] ++ ["amount-in" ,"amount-out"@@ -556,10 +554,10 @@ ] return () -fieldvalp :: CsvRulesParser String+fieldvalp :: CsvRulesParser Text fieldvalp = do lift $ dbgparse 8 "trying fieldvalp"- anySingle `manyTill` lift eolof+ T.pack <$> anySingle `manyTill` lift eolof -- A conditional block: one or more matchers, one per line, followed by one or more indented rules. conditionalblockp :: CsvRulesParser ConditionalBlock@@ -587,14 +585,14 @@ conditionaltablep = do lift $ dbgparse 8 "trying conditionaltablep" start <- getOffset- string "if" + string "if" sep <- lift $ satisfy (\c -> not (isAlphaNum c || isSpace c)) fields <- journalfieldnamep `sepBy1` (char sep) newline body <- flip manyTill (lift eolof) $ do off <- getOffset m <- matcherp' (char sep >> return ())- vs <- LS.splitOn [sep] <$> lift restofline+ vs <- T.split (==sep) . T.pack <$> lift restofline if (length vs /= length fields) then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d\n" (length fields) (length vs)) :: String) else return (m,vs)@@ -655,8 +653,7 @@ csvfieldreferencep = do lift $ dbgparse 8 "trying csvfieldreferencep" char '%'- f <- fieldnamep- return $ '%' : quoteIfNeeded f+ T.cons '%' . textQuoteIfNeeded <$> fieldnamep -- A single regular expression regexp :: CsvRulesParser () -> CsvRulesParser Regexp@@ -665,7 +662,7 @@ -- notFollowedBy matchoperatorp c <- lift nonspace cs <- anySingle `manyTill` end- case toRegexCI . strip $ c:cs of+ case toRegexCI . T.strip . T.pack $ c:cs of Left x -> Fail.fail $ "CSV parser: " ++ x Right x -> return x @@ -721,7 +718,7 @@ let skiplines = case getDirective "skip" rules of Nothing -> 0 Just "" -> 1- Just s -> readDef (throwerr $ "could not parse skip value: " ++ show s) s+ Just s -> readDef (throwerr $ "could not parse skip value: " ++ show s) $ T.unpack s -- parse csv let@@ -779,18 +776,17 @@ when (not rulesfileexists) $ do dbg1IO "creating conversion rules file" rulesfile- writeFile rulesfile $ T.unpack rulestext+ T.writeFile rulesfile rulestext return $ Right nulljournal{jtxns=txns''} -- | Parse special separator names TAB and SPACE, or return the first -- character. Return Nothing on empty string-parseSeparator :: String -> Maybe Char-parseSeparator = specials . map toLower+parseSeparator :: Text -> Maybe Char+parseSeparator = specials . T.toLower where specials "space" = Just ' ' specials "tab" = Just '\t'- specials (x:_) = Just x- specials [] = Nothing+ specials xs = fst <$> T.uncons xs parseCsv :: Char -> FilePath -> Text -> IO (Either String CSV) parseCsv separator filePath csvdata =@@ -813,15 +809,12 @@ parseResultToCsv = toListList . unpackFields where toListList = toList . fmap toList- unpackFields = (fmap . fmap) (T.unpack . T.decodeUtf8)+ unpackFields = (fmap . fmap) T.decodeUtf8 -printCSV :: CSV -> String-printCSV records = unlined (printRecord `map` records)- where printRecord = concat . intersperse "," . map printField- printField f = "\"" ++ concatMap escape f ++ "\""- escape '"' = "\"\""- escape x = [x]- unlined = concat . intersperse "\n"+printCSV :: CSV -> TL.Text+printCSV = TB.toLazyText . unlinesB . map printRecord+ where printRecord = mconcat . map TB.fromText . intersperse "," . map printField+ printField = wrap "\"" "\"" . T.replace "\"" "\\\"\\\"" -- | Return the cleaned up and validated CSV data (can be empty), or an error. validateCsv :: CsvRules -> Int -> Either String CSV -> Either String [CsvRecord]@@ -834,7 +827,7 @@ (Nothing, Nothing) -> Nothing (Just _, _) -> Just maxBound (Nothing, Just "") -> Just 1- (Nothing, Just x) -> Just (read x)+ (Nothing, Just x) -> Just (read $ T.unpack x) applyConditionalSkips [] = [] applyConditionalSkips (r:rest) = case skipCount r of@@ -866,7 +859,7 @@ --- ** converting csv records to transactions showRules rules record =- unlines $ catMaybes [ (("the "++fld++" rule is: ")++) <$> getEffectiveAssignment rules record fld | fld <- journalfieldnames]+ T.unlines $ catMaybes [ (("the "<>fld<>" rule is: ")<>) <$> getEffectiveAssignment rules record fld | fld <- journalfieldnames] -- | Look up the value (template) of a csv rule by rule keyword. csvRule :: CsvRules -> DirectiveName -> Maybe FieldTemplate@@ -880,7 +873,7 @@ -- | Look up the final value assigned to a hledger field, with csv field -- references interpolated.-hledgerFieldValue :: CsvRules -> CsvRecord -> HledgerFieldName -> Maybe String+hledgerFieldValue :: CsvRules -> CsvRecord -> HledgerFieldName -> Maybe Text hledgerFieldValue rules record = fmap (renderTemplate rules record) . hledgerField rules record transactionFromCsvRecord :: SourcePos -> CsvRules -> CsvRecord -> Transaction@@ -892,18 +885,18 @@ rule = csvRule rules :: DirectiveName -> Maybe FieldTemplate -- ruleval = csvRuleValue rules record :: DirectiveName -> Maybe String field = hledgerField rules record :: HledgerFieldName -> Maybe FieldTemplate- fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe String+ fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text parsedate = parseDateWithCustomOrDefaultFormats (rule "date-format")- mkdateerror datefield datevalue mdateformat = unlines- ["error: could not parse \""++datevalue++"\" as a date using date format "- ++maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" show mdateformat+ mkdateerror datefield datevalue mdateformat = T.unpack $ T.unlines+ ["error: could not parse \""<>datevalue<>"\" as a date using date format "+ <>maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" (T.pack . show) mdateformat ,showRecord record- ,"the "++datefield++" rule is: "++(fromMaybe "required, but missing" $ field datefield)- ,"the date-format is: "++fromMaybe "unspecified" mdateformat+ ,"the "<>datefield<>" rule is: "<>(fromMaybe "required, but missing" $ field datefield)+ ,"the date-format is: "<>fromMaybe "unspecified" mdateformat ,"you may need to "- ++"change your "++datefield++" rule, "- ++maybe "add a" (const "change your") mdateformat++" date-format rule, "- ++"or "++maybe "add a" (const "change your") mskip++" skip rule"+ <>"change your "<>datefield<>" rule, "+ <>maybe "add a" (const "change your") mdateformat<>" date-format rule, "+ <>"or "<>maybe "add a" (const "change your") mskip<>" skip rule" ,"for m/d/y or d/m/y dates, use date-format %-m/%-d/%Y or date-format %-d/%-m/%Y" ] where@@ -923,25 +916,27 @@ status = case fieldval "status" of Nothing -> Unmarked- Just s -> either statuserror id $ runParser (statusp <* eof) "" $ T.pack s+ Just s -> either statuserror id $ runParser (statusp <* eof) "" s where- statuserror err = error' $ unlines- ["error: could not parse \""++s++"\" as a cleared status (should be *, ! or empty)"- ,"the parse error is: "++customErrorBundlePretty err+ statuserror err = error' . T.unpack $ T.unlines+ ["error: could not parse \""<>s<>"\" as a cleared status (should be *, ! or empty)"+ ,"the parse error is: "<>T.pack (customErrorBundlePretty err) ] code = maybe "" singleline $ fieldval "code" description = maybe "" singleline $ fieldval "description" comment = maybe "" singleline $ fieldval "comment" precomment = maybe "" singleline $ fieldval "precomment" + singleline = T.unwords . filter (not . T.null) . map T.strip . T.lines+ ---------------------------------------------------------------------- -- 3. Generate the postings for which an account has been assigned -- (possibly indirectly due to an amount or balance assignment) - p1IsVirtual = (accountNamePostingType . T.pack <$> fieldval "account1") == Just VirtualPosting+ p1IsVirtual = (accountNamePostingType <$> fieldval "account1") == Just VirtualPosting ps = [p | n <- [1..maxpostings]- ,let comment = T.pack $ fromMaybe "" $ fieldval ("comment"++show n)- ,let currency = fromMaybe "" (fieldval ("currency"++show n) <|> fieldval "currency")+ ,let comment = fromMaybe "" $ fieldval ("comment"<> T.pack (show n))+ ,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 ,Just (acct,isfinal) <- [getAccount rules record mamount mbalance n] -- skips Nothings@@ -965,10 +960,10 @@ ,tdate = date' ,tdate2 = mdate2' ,tstatus = status- ,tcode = T.pack code- ,tdescription = T.pack description- ,tcomment = T.pack comment- ,tprecedingcomment = T.pack precomment+ ,tcode = code+ ,tdescription = description+ ,tcomment = comment+ ,tprecedingcomment = precomment ,tpostings = ps } @@ -979,7 +974,7 @@ -- For postings 1 or 2 it also looks at "amount", "amount-in", "amount-out". -- If more than one of these has a value, it looks for one that is non-zero. -- If there's multiple non-zeros, or no non-zeros but multiple zeros, it throws an error.-getAmount :: CsvRules -> CsvRecord -> String -> Bool -> Int -> Maybe MixedAmount+getAmount :: CsvRules -> CsvRecord -> Text -> Bool -> Int -> Maybe MixedAmount getAmount rules record currency p1IsVirtual n = -- Warning, many tricky corner cases here. -- docs: hledger_csv.m4.md #### amount@@ -988,14 +983,15 @@ unnumberedfieldnames = ["amount","amount-in","amount-out"] -- amount field names which can affect this posting- fieldnames = map (("amount"++show n)++) ["","-in","-out"]+ fieldnames = map (("amount"<> T.pack(show n))<>) ["","-in","-out"] -- For posting 1, also recognise the old amount/amount-in/amount-out names. -- For posting 2, the same but only if posting 1 needs balancing. ++ if n==1 || n==2 && not p1IsVirtual then unnumberedfieldnames else [] -- assignments to any of these field names with non-empty values assignments = [(f,a') | f <- fieldnames- , Just v@(_:_) <- [strip . renderTemplate rules record <$> hledgerField rules record f]+ , Just v <- [T.strip . renderTemplate rules record <$> hledgerField rules record f]+ , not $ T.null v , let a = parseAmount rules record currency v -- With amount/amount-in/amount-out, in posting 2, -- flip the sign and convert to cost, as they did before 1.17@@ -1006,7 +1002,7 @@ assignments' | any isnumbered assignments = filter isnumbered assignments | otherwise = assignments where- isnumbered (f,_) = any (flip elem ['0'..'9']) f+ isnumbered (f,_) = T.any (flip elem ['0'..'9']) f -- if there's more than one value and only some are zeros, discard the zeros assignments''@@ -1017,24 +1013,24 @@ in case -- dbg0 ("amounts for posting "++show n) assignments'' of [] -> Nothing- [(f,a)] | "-out" `isSuffixOf` f -> Just (-a) -- for -out fields, flip the sign+ [(f,a)] | "-out" `T.isSuffixOf` f -> Just (-a) -- for -out fields, flip the sign [(_,a)] -> Just a- fs -> error' $ unlines $ [ -- PARTIAL:+ fs -> error' . T.unpack . T.unlines $ [ -- PARTIAL: "multiple non-zero amounts or multiple zero amounts assigned," ,"please ensure just one. (https://hledger.org/csv.html#amount)"- ," " ++ showRecord record- ," for posting: " ++ show n+ ," " <> showRecord record+ ," for posting: " <> T.pack (show n) ]- ++ [" assignment: " ++ f ++ " " ++- fromMaybe "" (hledgerField rules record f) ++- "\t=> value: " ++ showMixedAmount a -- XXX not sure this is showing all the right info+ ++ [" assignment: " <> f <> " " <>+ fromMaybe "" (hledgerField rules record f) <>+ "\t=> value: " <> wbToText (showMixedAmountB noColour a) -- XXX not sure this is showing all the right info | (f,a) <- fs] -- | Figure out the expected balance (assertion or assignment) specified for posting N, -- if any (and its parse position).-getBalance :: CsvRules -> CsvRecord -> String -> Int -> Maybe (Amount, GenericSourcePos)+getBalance :: CsvRules -> CsvRecord -> Text -> Int -> Maybe (Amount, GenericSourcePos) getBalance rules record currency n = do- v <- (fieldval ("balance"++show n)+ v <- (fieldval ("balance"<> T.pack (show n)) -- for posting 1, also recognise the old field name <|> if n==1 then fieldval "balance" else Nothing) case v of@@ -1043,30 +1039,29 @@ parseBalanceAmount rules record currency n s ,nullsourcepos -- parse position to show when assertion fails, ) -- XXX the csv record's line number would be good- where- fieldval = fmap strip . hledgerFieldValue rules record :: HledgerFieldName -> Maybe String+ fieldval = fmap T.strip . hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text -- | Given a non-empty amount string (from CSV) to parse, along with a -- possibly non-empty currency symbol to prepend, -- parse as a hledger MixedAmount (as in journal format), or raise an error. -- The whole CSV record is provided for the error message.-parseAmount :: CsvRules -> CsvRecord -> String -> String -> MixedAmount+parseAmount :: CsvRules -> CsvRecord -> Text -> Text -> MixedAmount parseAmount rules record currency s =- either mkerror (Mixed . (:[])) $ -- PARTIAL:- runParser (evalStateT (amountp <* eof) journalparsestate) "" $- T.pack $ (currency++) $ simplifySign s+ either mkerror (Mixed . (:[])) $ -- PARTIAL:+ runParser (evalStateT (amountp <* eof) journalparsestate) "" $+ currency <> simplifySign s where journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules}- mkerror e = error' $ unlines- ["error: could not parse \""++s++"\" as an amount"+ mkerror e = error' . T.unpack $ T.unlines+ ["error: could not parse \"" <> s <> "\" as an amount" ,showRecord record ,showRules rules record -- ,"the default-currency is: "++fromMaybe "unspecified" (getDirective "default-currency" rules)- ,"the parse error is: "++customErrorBundlePretty e- ,"you may need to "- ++"change your amount*, balance*, or currency* rules, "- ++"or add or change your skip rule"+ ,"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" ] -- XXX unify these ^v@@ -1076,30 +1071,30 @@ -- possibly non-empty currency symbol to prepend, -- parse as a hledger Amount (as in journal format), or raise an error. -- The CSV record and the field's numeric suffix are provided for the error message.-parseBalanceAmount :: CsvRules -> CsvRecord -> String -> Int -> String -> Amount+parseBalanceAmount :: CsvRules -> CsvRecord -> Text -> Int -> Text -> Amount parseBalanceAmount rules record currency n s = either (mkerror n s) id $ runParser (evalStateT (amountp <* eof) journalparsestate) "" $- T.pack $ (currency++) $ simplifySign s+ currency <> simplifySign s -- the csv record's line number would be good where journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules}- mkerror n s e = error' $ unlines- ["error: could not parse \""++s++"\" as balance"++show n++" amount"+ mkerror n s e = error' . T.unpack $ T.unlines+ ["error: could not parse \"" <> s <> "\" as balance"<> T.pack (show n) <> " amount" ,showRecord record ,showRules rules record -- ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency- ,"the parse error is: "++customErrorBundlePretty e+ ,"the parse error is: "<> T.pack (customErrorBundlePretty e) ] -- Read a valid decimal mark from the decimal-mark rule, if any. -- If the rule is present with an invalid argument, raise an error. parseDecimalMark :: CsvRules -> Maybe DecimalMark-parseDecimalMark rules =- case rules `csvRule` "decimal-mark" of- Nothing -> Nothing- Just [c] | isDecimalMark c -> Just c- Just s -> error' $ "decimal-mark's argument should be \".\" or \",\" (not \""++s++"\")"+parseDecimalMark rules = do+ s <- rules `csvRule` "decimal-mark"+ case T.uncons s of+ Just (c, rest) | T.null rest && isDecimalMark c -> return c+ _ -> error' . T.unpack $ "decimal-mark's argument should be \".\" or \",\" (not \""<>s<>"\")" -- | Make a balance assertion for the given amount, with the given parse -- position (to be shown in assertion failures), with the assertion type@@ -1116,8 +1111,8 @@ Just "==" -> nullassertion{batotal=True} Just "=*" -> nullassertion{bainclusive=True} Just "==*" -> nullassertion{batotal=True, bainclusive=True}- Just x -> error' $ unlines -- PARTIAL:- [ "balance-type \"" ++ x ++"\" is invalid. Use =, ==, =* or ==*."+ Just x -> error' . T.unpack $ T.unlines -- PARTIAL:+ [ "balance-type \"" <> x <>"\" is invalid. Use =, ==, =* or ==*." , showRecord record , showRules rules record ]@@ -1128,8 +1123,8 @@ getAccount :: CsvRules -> CsvRecord -> Maybe MixedAmount -> Maybe (Amount, GenericSourcePos) -> Int -> Maybe (AccountName, Bool) getAccount rules record mamount mbalance n = let- fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe String- maccount = T.pack <$> fieldval ("account"++show n)+ fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text+ maccount = fieldval ("account"<> T.pack (show n)) in case maccount of -- accountN is set to the empty string - no posting will be generated Just "" -> Nothing@@ -1150,14 +1145,21 @@ unknownExpenseAccount = "expenses:unknown" unknownIncomeAccount = "income:unknown" -type CsvAmountString = String+type CsvAmountString = Text -- | Canonicalise the sign in a CSV amount string.--- Such strings can have a minus sign, negating parentheses,--- or any two of these (which cancels out).+-- Such strings can have a minus sign, parentheses (equivalent to minus),+-- or any two of these (which cancel out),+-- or a plus sign (which is removed),+-- or any sign by itself with no following number (which is removed).+-- See hledger > CSV FORMAT > Tips > Setting amounts. --+-- These are supported (note, not every possibile combination):+-- -- >>> simplifySign "1" -- "1"+-- >>> simplifySign "+1"+-- "1" -- >>> simplifySign "-1" -- "-1" -- >>> simplifySign "(1)"@@ -1166,23 +1168,36 @@ -- "1" -- >>> simplifySign "-(1)" -- "1"+-- >>> simplifySign "-+1"+-- "-1" -- >>> simplifySign "(-1)" -- "1" -- >>> simplifySign "((1))" -- "1"+-- >>> simplifySign "-"+-- ""+-- >>> simplifySign "()"+-- ""+-- >>> simplifySign "+"+-- "" simplifySign :: CsvAmountString -> CsvAmountString-simplifySign ('(':s) | lastMay s == Just ')' = simplifySign $ negateStr $ init s-simplifySign ('-':'(':s) | lastMay s == Just ')' = simplifySign $ init s-simplifySign ('-':'-':s) = s-simplifySign s = s+simplifySign amtstr+ | Just ('(',t) <- T.uncons amtstr, Just (amt,')') <- T.unsnoc t = simplifySign $ negateStr amt+ | Just ('-',b) <- T.uncons amtstr, Just ('(',t) <- T.uncons b, Just (amt,')') <- T.unsnoc t = simplifySign amt+ | Just ('-',m) <- T.uncons amtstr, Just ('-',amt) <- T.uncons m = amt+ | Just ('-',m) <- T.uncons amtstr, Just ('+',amt) <- T.uncons m = negateStr amt+ | amtstr `elem` ["-","+","()"] = ""+ | Just ('+',amt) <- T.uncons amtstr = simplifySign amt+ | otherwise = amtstr -negateStr :: String -> String-negateStr ('-':s) = s-negateStr s = '-':s+negateStr :: Text -> Text+negateStr amtstr = case T.uncons amtstr of+ Just ('-',s) -> s+ _ -> T.cons '-' amtstr -- | Show a (approximate) recreation of the original CSV record.-showRecord :: CsvRecord -> String-showRecord r = "record values: "++intercalate "," (map show r)+showRecord :: CsvRecord -> Text+showRecord r = "record values: "<>T.intercalate "," (map (wrap "\"" "\"") r) -- | Given the conversion rules, a CSV record and a hledger field name, find -- the value template ultimately assigned to this field, if any, by a field@@ -1208,7 +1223,7 @@ where -- does this individual matcher match the current csv record ? matcherMatches :: Matcher -> Bool- matcherMatches (RecordMatcher _ pat) = regexMatch pat' wholecsvline+ matcherMatches (RecordMatcher _ pat) = regexMatchText pat' wholecsvline where pat' = dbg7 "regex" pat -- A synthetic whole CSV record to match against. Note, this can be@@ -1217,47 +1232,48 @@ -- - 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" $ intercalate "," record- matcherMatches (FieldMatcher _ csvfieldref pat) = regexMatch pat csvfieldvalue+ wholecsvline = dbg7 "wholecsvline" $ T.intercalate "," record+ matcherMatches (FieldMatcher _ csvfieldref pat) = regexMatchText pat csvfieldvalue where -- the value of the referenced CSV field to match against. csvfieldvalue = dbg7 "csvfieldvalue" $ replaceCsvFieldReference rules record csvfieldref -- | Render a field assignment's template, possibly interpolating referenced -- CSV field values. Outer whitespace is removed from interpolated values.-renderTemplate :: CsvRules -> CsvRecord -> FieldTemplate -> String-renderTemplate rules record t = maybe t concat $ parseMaybe+renderTemplate :: CsvRules -> CsvRecord -> FieldTemplate -> Text+renderTemplate rules record t = maybe t mconcat $ parseMaybe (many $ takeWhile1P Nothing (/='%') <|> replaceCsvFieldReference rules record <$> referencep) t where- referencep = liftA2 (:) (char '%') (takeWhile1P (Just "reference") isDescriptorChar) :: Parsec CustomErr String String+ referencep = liftA2 T.cons (char '%') (takeWhile1P (Just "reference") isDescriptorChar) :: Parsec CustomErr Text Text isDescriptorChar c = isAscii c && (isAlphaNum c || c == '_' || c == '-') -- | Replace something that looks like a reference to a csv field ("%date" or "%1) -- with that field's value. If it doesn't look like a field reference, or if we -- can't find such a field, leave it unchanged.-replaceCsvFieldReference :: CsvRules -> CsvRecord -> CsvFieldReference -> String-replaceCsvFieldReference rules record s@('%':fieldname) = fromMaybe s $ csvFieldValue rules record fieldname-replaceCsvFieldReference _ _ s = s+replaceCsvFieldReference :: CsvRules -> CsvRecord -> CsvFieldReference -> Text+replaceCsvFieldReference rules record s = case T.uncons s of+ Just ('%', fieldname) -> fromMaybe s $ csvFieldValue rules record fieldname+ _ -> s -- | 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.-csvFieldValue :: CsvRules -> CsvRecord -> CsvFieldName -> Maybe String+csvFieldValue :: CsvRules -> CsvRecord -> CsvFieldName -> Maybe Text csvFieldValue rules record fieldname = do- fieldindex <- if | all isDigit fieldname -> readMay fieldname- | otherwise -> lookup (map toLower fieldname) $ rcsvfieldindexes rules- fieldvalue <- strip <$> atMay record (fieldindex-1)+ fieldindex <- if | T.all isDigit fieldname -> readMay $ T.unpack fieldname+ | otherwise -> lookup (T.toLower fieldname) $ rcsvfieldindexes rules+ fieldvalue <- T.strip <$> atMay record (fieldindex-1) return fieldvalue -- | Parse the date string using the specified date-format, or if unspecified -- the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, leading -- zeroes optional).-parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> String -> Maybe Day+parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> Text -> Maybe Day parseDateWithCustomOrDefaultFormats mformat s = asum $ map parsewith formats where- parsewith = flip (parseTimeM True defaultTimeLocale) s- formats = maybe+ parsewith = flip (parseTimeM True defaultTimeLocale) (T.unpack s)+ formats = map T.unpack $ maybe ["%Y/%-m/%-d" ,"%Y-%-m-%-d" ,"%Y.%-m.%-d"
Hledger/Read/JournalReader.hs view
@@ -42,7 +42,7 @@ -- * Reader-finding utils findReader, splitReaderPrefix,- + -- * Reader reader, @@ -226,6 +226,7 @@ ,applyaccountdirectivep ,commoditydirectivep ,endapplyaccountdirectivep+ ,payeedirectivep ,tagdirectivep ,endtagdirectivep ,defaultyeardirectivep@@ -379,8 +380,8 @@ "c" -> Right Cash _ -> Left err where- err = "invalid account type code "++T.unpack s++", should be one of " ++- (intercalate ", " $ ["A","L","E","R","X","C","Asset","Liability","Equity","Revenue","Expense","Cash"])+ err = T.unpack $ "invalid account type code "<>s<>", should be one of " <>+ T.intercalate ", " ["A","L","E","R","X","C","Asset","Liability","Equity","Revenue","Expense","Cash"] -- Add an account declaration to the journal, auto-numbering it. addAccountDeclaration :: (AccountName,Text,[Tag]) -> JournalParser m ()@@ -396,6 +397,17 @@ in j{jdeclaredaccounts = d:decls}) +-- Add a payee declaration to the journal.+addPayeeDeclaration :: (Payee,Text,[Tag]) -> JournalParser m ()+addPayeeDeclaration (p, cmt, tags) =+ modify' (\j@Journal{jdeclaredpayees} -> j{jdeclaredpayees=d:jdeclaredpayees})+ where+ d = (p+ ,nullpayeedeclarationinfo{+ pdicomment = cmt+ ,pditags = tags+ })+ indentedlinep :: JournalParser m String indentedlinep = lift skipNonNewlineSpaces1 >> (rstrip <$> lift restofline) @@ -519,6 +531,15 @@ lift restofline return () +payeedirectivep :: JournalParser m ()+payeedirectivep = do+ string "payee" <?> "payee directive"+ lift skipNonNewlineSpaces1+ payee <- lift descriptionp -- all text until ; or \n+ (comment, tags) <- lift transactioncommentp+ addPayeeDeclaration (payee, comment, tags)+ return ()+ defaultyeardirectivep :: JournalParser m () defaultyeardirectivep = do char 'Y' <?> "default year"@@ -984,6 +1005,11 @@ pdcommodity = "BTC", pdamount = usd 922.83 }++ ,tests "payeedirectivep" [+ test "simple" $ assertParse payeedirectivep "payee foo\n"+ ,test "with-comment" $ assertParse payeedirectivep "payee foo ; comment\n"+ ] ,test "tagdirectivep" $ do assertParse tagdirectivep "tag foo \n"
Hledger/Read/TimedotReader.hs view
@@ -182,7 +182,7 @@ tstatus = Cleared, tpostings = [ nullposting{paccount=a- ,pamount=Mixed [setAmountPrecision (Precision 2) $ num hours] -- don't assume hours; do set precision to 2+ ,pamount=Mixed [amountSetPrecision (Precision 2) $ num hours] -- don't assume hours; do set precision to 2 ,ptype=VirtualPosting ,ptransaction=Just t }
Hledger/Reports/AccountTransactionsReport.hs view
@@ -18,6 +18,7 @@ import Data.List import Data.Ord import Data.Maybe+import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar @@ -64,26 +65,20 @@ -- posts to the current account), most recent first. -- Reporting intervals are currently ignored. ---type AccountTransactionsReport =- (String -- label for the balance column, eg "balance" or "total"- ,[AccountTransactionsReportItem] -- line items, one per transaction- )+type AccountTransactionsReport = [AccountTransactionsReportItem] -- line items, one per transaction type AccountTransactionsReportItem = ( Transaction -- the transaction, unmodified ,Transaction -- the transaction, as seen from the current account ,Bool -- is this a split (more than one posting to other accounts) ?- ,String -- a display string describing the other account(s), if any+ ,Text -- a display string describing the other account(s), if any ,MixedAmount -- the amount posted to the current account(s) (or total amount posted) ,MixedAmount -- the register's running total or the current account(s)'s historical balance, after this transaction ) -totallabel = "Period Total"-balancelabel = "Historical Total"- accountTransactionsReport :: ReportSpec -> Journal -> Query -> Query -> AccountTransactionsReport-accountTransactionsReport rspec@ReportSpec{rsOpts=ropts} j reportq thisacctq = (label, items)+accountTransactionsReport rspec@ReportSpec{rsOpts=ropts} j reportq thisacctq = items where -- a depth limit should not affect the account transactions report -- seems unnecessary for some reason XXX@@ -116,14 +111,10 @@ periodlast = fromMaybe (error' "journalApplyValuation: expected a non-empty journal") $ -- XXX shouldn't happen reportPeriodOrJournalLastDay rspec j- mreportlast = reportPeriodLastDay rspec- multiperiod = interval_ ropts /= NoInterval- tval = case value_ ropts of- Just v -> \t -> transactionApplyValuation prices styles periodlast mreportlast (rsToday rspec) multiperiod t v- Nothing -> id+ tval = transactionApplyCostValuation prices styles periodlast (rsToday rspec) (cost_ ropts) $ value_ ropts ts4 = ptraceAtWith 5 (("ts4:\n"++).pshowTransactions) $- map tval ts3 + map tval ts3 -- sort by the transaction's register date, for accurate starting balance -- these are not yet filtered by tdate, we want to search them all for priorps@@ -131,9 +122,9 @@ ptraceAtWith 5 (("ts5:\n"++).pshowTransactions) $ sortBy (comparing (transactionRegisterDate reportq' thisacctq)) ts4 - (startbal,label)- | balancetype_ ropts == HistoricalBalance = (sumPostings priorps, balancelabel)- | otherwise = (nullmixedamt, totallabel)+ startbal+ | balancetype_ ropts == HistoricalBalance = sumPostings priorps+ | otherwise = nullmixedamt where priorps = dbg5 "priorps" $ filter (matchesPosting@@ -218,9 +209,9 @@ -- | Generate a simplified summary of some postings' accounts. -- To reduce noise, if there are both real and virtual postings, show only the real ones.-summarisePostingAccounts :: [Posting] -> String+summarisePostingAccounts :: [Posting] -> Text summarisePostingAccounts ps =- (intercalate ", " . map (T.unpack . accountSummarisedName) . nub . map paccount) displayps -- XXX pack+ T.intercalate ", " . map accountSummarisedName . nub $ map paccount displayps where realps = filter isReal ps displayps | null realps = ps
Hledger/Reports/BudgetReport.hs view
@@ -27,27 +27,27 @@ ) where -import Data.Decimal+import Data.Decimal (roundTo) import Data.Default (def) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM-import Data.List+import Data.List (nub, partition, transpose) import Data.List.Extra (nubSort)-import Data.Maybe+import Data.Maybe (fromMaybe) #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>)) #endif-import Safe+import Safe (headDef) --import Data.List --import Data.Maybe import qualified Data.Map as Map import Data.Map (Map)+import Data.Text (Text) import qualified Data.Text as T---import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TB --import System.Console.CmdArgs.Explicit as C --import Lucid as L--import Text.Printf (printf) import Text.Tabular as T import Text.Tabular.AsciiWide as T @@ -68,7 +68,7 @@ type BudgetReportRow = PeriodicReportRow DisplayName BudgetCell type BudgetReport = PeriodicReport DisplayName BudgetCell -type BudgetDisplayCell = ((String, Int), Maybe ((String, Int), Maybe (String, Int)))+type BudgetDisplayCell = ((Text, Int), Maybe ((Text, Int), Maybe (Text, Int))) -- | Calculate per-account, per-period budget (balance change) goals -- from all periodic transactions, calculate actual balance changes @@ -219,27 +219,25 @@ totActualByPeriod = Map.fromList $ zip actualperiods actualtots :: Map DateSpan Change -- | Render a budget report as plain text suitable for console output.-budgetReportAsText :: ReportOpts -> BudgetReport -> String-budgetReportAsText ropts@ReportOpts{..} budgetr =- title ++ "\n\n" ++- renderTable def{tableBorders=False,prettyTable=pretty_tables_}- (alignCell TopLeft) (alignCell TopRight) (uncurry showcell) displayTableWithWidths+budgetReportAsText :: ReportOpts -> BudgetReport -> TL.Text+budgetReportAsText ropts@ReportOpts{..} budgetr = TB.toLazyText $+ TB.fromText title <> TB.fromText "\n\n" <>+ renderTableB def{tableBorders=False,prettyTable=pretty_tables_}+ (textCell TopLeft) (textCell TopRight) (uncurry showcell) displayTableWithWidths where- multiperiod = interval_ /= NoInterval- title = printf "Budget performance in %s%s:"- (showDateSpan $ periodicReportSpan budgetr)- (case value_ of- Just (AtCost _mc) -> ", valued at cost"- Just (AtThen _mc) -> error' unsupportedValueThenError -- PARTIAL:- Just (AtEnd _mc) -> ", valued at period ends"- Just (AtNow _mc) -> ", current value"- -- XXX duplicates the above- Just (AtDefault _mc) | multiperiod -> ", valued at period ends"- Just (AtDefault _mc) -> ", current value"- Just (AtDate d _mc) -> ", valued at "++showDate d- Nothing -> "")+ title = "Budget performance in " <> showDateSpan (periodicReportSpan budgetr)+ <> (case cost_ of+ Cost -> ", converted to cost"+ NoCost -> "")+ <> (case value_ of+ Just (AtThen _mc) -> ", valued at posting date"+ Just (AtEnd _mc) -> ", valued at period ends"+ Just (AtNow _mc) -> ", current value"+ Just (AtDate d _mc) -> ", valued at " <> showDate d+ Nothing -> "")+ <> ":" - displayTableWithWidths :: Table String String ((Int, Int, Int), BudgetDisplayCell)+ displayTableWithWidths :: Table Text Text ((Int, Int, Int), BudgetDisplayCell) displayTableWithWidths = Table rh ch $ map (zipWith (,) widths) displaycells Table rh ch displaycells = case budgetReportAsTable ropts budgetr of Table rh' ch' vals -> maybetranspose . Table rh' ch' $ map (map displayCell) vals@@ -248,8 +246,8 @@ where actual' = fromMaybe 0 actual budgetAndPerc b = (showamt b, showper <$> percentage actual' b)- showamt = showMixedOneLine showAmountWithoutPrice Nothing (Just 32) color_- showper p = let str = show (roundTo 0 p) in (str, length str)+ showamt = (\(WideBuilder b w) -> (TL.toStrict $ TB.toLazyText b, w)) . showMixedAmountB oneLine{displayColour=color_, displayMaxWidth=Just 32}+ showper p = let str = T.pack (show $ roundTo 0 p) in (str, T.length str) cellWidth ((_,wa), Nothing) = (wa, 0, 0) cellWidth ((_,wa), Just ((_,wb), Nothing)) = (wa, wb, 0) cellWidth ((_,wa), Just ((_,wb), Just (_,wp))) = (wa, wb, wp)@@ -263,14 +261,17 @@ -- XXX lay out actual, percentage and/or goal in the single table cell for now, should probably use separate cells showcell :: (Int, Int, Int) -> BudgetDisplayCell -> Cell showcell (actualwidth, budgetwidth, percentwidth) ((actual,wa), mbudget) =- Cell TopRight [(replicate (actualwidth - wa) ' ' ++ actual ++ budgetstr, actualwidth + totalbudgetwidth)]+ Cell TopRight [WideBuilder ( TB.fromText (T.replicate (actualwidth - wa) " ")+ <> TB.fromText actual+ <> budgetstr+ ) (actualwidth + totalbudgetwidth)] where totalpercentwidth = if percentwidth == 0 then 0 else percentwidth + 5 totalbudgetwidth = if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth + 3- budgetstr = case mbudget of- Nothing -> replicate totalbudgetwidth ' '- Just ((budget, wb), Nothing) -> " [" ++ replicate totalpercentwidth ' ' ++ replicate (budgetwidth - wb) ' ' ++ budget ++ "]"- Just ((budget, wb), Just (pct, wp)) -> " [" ++ replicate (percentwidth - wp) ' ' ++ pct ++ "% of " ++ replicate (budgetwidth - wb) ' ' ++ budget ++ "]"+ budgetstr = TB.fromText $ case mbudget of+ Nothing -> T.replicate totalbudgetwidth " "+ Just ((budget, wb), Nothing) -> " [" <> T.replicate totalpercentwidth " " <> T.replicate (budgetwidth - wb) " " <> budget <> "]"+ Just ((budget, wb), Just (pct, wp)) -> " [" <> T.replicate (percentwidth - wp) " " <> pct <> "% of " <> T.replicate (budgetwidth - wb) " " <> budget <> "]" -- | Calculate the percentage of actual change to budget goal to show, if any. -- If valuing at cost, both amounts are converted to cost before comparing.@@ -285,13 +286,15 @@ _ -> -- trace (pshow $ (maybecost actual, maybecost budget)) -- debug missing percentage Nothing where- maybecost = if valuationTypeIsCost ropts then mixedAmountCost else id+ maybecost = case cost_ of+ Cost -> mixedAmountCost+ NoCost -> id maybetranspose | transpose_ = \(Table rh ch vals) -> Table ch rh (transpose vals) | otherwise = id -- | Build a 'Table' from a multi-column balance report.-budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount)+budgetReportAsTable :: ReportOpts -> BudgetReport -> Table Text Text (Maybe MixedAmount, Maybe MixedAmount) budgetReportAsTable ropts@ReportOpts{balancetype_} (PeriodicReport spans rows (PeriodicReportRow _ coltots grandtot grandavg)) =@@ -310,8 +313,8 @@ -- budgetReport sets accountlistmode to ALTree. Find a principled way to do -- this. renderacct row = case accountlistmode_ ropts of- ALTree -> replicate ((prrDepth row - 1)*2) ' ' ++ T.unpack (prrDisplayName row)- ALFlat -> T.unpack . accountNameDrop (drop_ ropts) $ prrFullName row+ ALTree -> T.replicate ((prrDepth row - 1)*2) " " <> prrDisplayName row+ ALFlat -> accountNameDrop (drop_ ropts) $ prrFullName row rowvals (PeriodicReportRow _ as rowtot rowavg) = as ++ [rowtot | row_total_ ropts] ++ [rowavg | average_ ropts] addtotalrow@@ -334,7 +337,7 @@ -- - all other balance change reports: a description of the datespan, -- abbreviated to compact form if possible (see showDateSpan). ---reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> String+reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> T.Text reportPeriodName balancetype spans = case balancetype of PeriodChange -> if multiyear then showDateSpan else showDateSpanMonthAbbrev@@ -346,20 +349,20 @@ -- | Render a budget report as CSV. Like multiBalanceReportAsCsv, -- but includes alternating actual and budget amount columns. budgetReportAsCsv :: ReportOpts -> BudgetReport -> CSV-budgetReportAsCsv +budgetReportAsCsv ReportOpts{average_, row_total_, no_total_, transpose_} (PeriodicReport colspans items (PeriodicReportRow _ abtotals (magrandtot,mbgrandtot) (magrandavg,mbgrandavg))) = (if transpose_ then transpose else id) $ -- heading row- ("Account" : + ("Account" : concatMap (\span -> [showDateSpan span, "budget"]) colspans ++ concat [["Total" ,"budget"] | row_total_] ++ concat [["Average","budget"] | average_] ) : -- account rows- [T.unpack (displayFull a) :+ [displayFull a : map showmamt (flattentuples abamts) ++ concat [[showmamt mactualrowtot, showmamt mbudgetrowtot] | row_total_] ++ concat [[showmamt mactualrowavg, showmamt mbudgetrowavg] | average_]@@ -371,7 +374,7 @@ [ "Total:" : map showmamt (flattentuples abtotals)- ++ concat [[showmamt magrandtot,showmamt mbgrandtot] | row_total_] + ++ concat [[showmamt magrandtot,showmamt mbgrandtot] | row_total_] ++ concat [[showmamt magrandavg,showmamt mbgrandavg] | average_] ] | not no_total_@@ -379,7 +382,7 @@ where flattentuples abs = concat [[a,b] | (a,b) <- abs]- showmamt = maybe "" (showMixedAmountOneLineWithoutPrice False)+ showmamt = maybe "" (wbToText . showMixedAmountB oneLine) -- tests
Hledger/Reports/EntriesReport.hs view
@@ -40,12 +40,8 @@ -- We may be converting posting amounts to value, per hledger_options.m4.md "Effect of --value on reports". tvalue t@Transaction{..} = t{tpostings=map pvalue tpostings} where- pvalue p = maybe p- (postingApplyValuation (journalPriceOracle infer_value_ j) (journalCommodityStyles j) periodlast mreportlast (rsToday rspec) False p)- value_- where- periodlast = fromMaybe (rsToday rspec) $ reportPeriodOrJournalLastDay rspec j- mreportlast = reportPeriodLastDay rspec+ pvalue = postingApplyCostValuation (journalPriceOracle infer_value_ j) (journalCommodityStyles j) periodlast (rsToday rspec) cost_ value_+ where periodlast = fromMaybe (rsToday rspec) $ reportPeriodOrJournalLastDay rspec j tests_EntriesReport = tests "EntriesReport" [ tests "entriesReport" [
Hledger/Reports/MultiBalanceReport.hs view
@@ -23,11 +23,9 @@ sortRowsLike, -- * Helper functions- calculateReportSpan, makeReportQuery, getPostingsByColumn, getPostings,- calculateColSpans, startingBalances, generateMultiBalanceReport, @@ -51,7 +49,7 @@ #endif import Data.Semigroup (sconcat) import Data.Time.Calendar (Day, addDays, fromGregorian)-import Safe (headMay, lastDef, lastMay)+import Safe (lastDef, minimumMay) import Hledger.Data import Hledger.Query@@ -113,22 +111,19 @@ multiBalanceReportWith rspec' j priceoracle = report where -- Queries, report/column dates.- reportspan = dbg3 "reportspan" $ calculateReportSpan rspec' j+ reportspan = dbg3 "reportspan" $ reportSpan j rspec' rspec = dbg3 "reportopts" $ makeReportQuery rspec' reportspan- valuation = makeValuation rspec' j priceoracle -- Must use rspec' instead of rspec,- -- so the reportspan isn't used for valuation -- Group postings into their columns. colps = dbg5 "colps" $ getPostingsByColumn rspec j reportspan- colspans = dbg3 "colspans" $ M.keys colps -- The matched accounts with a starting balance. All of these should appear -- in the report, even if they have no postings during the report period.- startbals = dbg5 "startbals" $ startingBalances rspec j reportspan+ startbals = dbg5 "startbals" $ startingBalances rspec j priceoracle reportspan -- Generate and postprocess the report, negating balances and taking percentages if needed report = dbg4 "multiBalanceReportWith" $- generateMultiBalanceReport rspec j valuation colspans colps startbals+ generateMultiBalanceReport rspec j priceoracle colps startbals -- | Generate a compound balance report from a list of CBCSubreportSpec. This -- shares postings between the subreports.@@ -144,18 +139,15 @@ compoundBalanceReportWith rspec' j priceoracle subreportspecs = cbr where -- Queries, report/column dates.- reportspan = dbg3 "reportspan" $ calculateReportSpan rspec' j+ reportspan = dbg3 "reportspan" $ reportSpan j rspec' rspec = dbg3 "reportopts" $ makeReportQuery rspec' reportspan- valuation = makeValuation rspec' j priceoracle -- Must use ropts' instead of ropts,- -- so the reportspan isn't used for valuation -- Group postings into their columns.- colps = dbg5 "colps" $ getPostingsByColumn rspec{rsOpts=(rsOpts rspec){empty_=True}} j reportspan- colspans = dbg3 "colspans" $ M.keys colps+ colps = dbg5 "colps" $ getPostingsByColumn rspec j reportspan -- The matched accounts with a starting balance. All of these should appear -- in the report, even if they have no postings during the report period.- startbals = dbg5 "startbals" $ startingBalances rspec j reportspan+ startbals = dbg5 "startbals" $ startingBalances rspec j priceoracle reportspan subreports = map generateSubreport subreportspecs where@@ -163,7 +155,7 @@ ( cbcsubreporttitle -- Postprocess the report, negating balances and taking percentages if needed , cbcsubreporttransform $- generateMultiBalanceReport rspec{rsOpts=ropts} j valuation colspans colps' startbals'+ generateMultiBalanceReport rspec{rsOpts=ropts} j priceoracle colps' startbals' , cbcsubreportincreasestotal ) where@@ -184,7 +176,7 @@ subreportTotal (_, sr, increasestotal) = (if increasestotal then id else fmap negate) $ prTotals sr - cbr = CompoundPeriodicReport "" colspans subreports overalltotals+ cbr = CompoundPeriodicReport "" (M.keys colps) subreports overalltotals -- | Calculate starting balances, if needed for -H@@ -194,14 +186,20 @@ -- TODO: Do we want to check whether to bother calculating these? isHistorical -- and startDate is not nothing, otherwise mempty? This currently gives a -- failure with some totals which are supposed to be 0 being blank.-startingBalances :: ReportSpec -> Journal -> DateSpan -> HashMap AccountName Account-startingBalances rspec@ReportSpec{rsQuery=query,rsOpts=ropts} j reportspan =- acctChangesFromPostings rspec' . map fst $ getPostings rspec' j+startingBalances :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> HashMap AccountName Account+startingBalances rspec@ReportSpec{rsQuery=query,rsOpts=ropts} j priceoracle reportspan =+ fmap (M.findWithDefault nullacct precedingspan) acctmap where+ acctmap = calculateReportMatrix rspec' j priceoracle mempty+ . M.singleton precedingspan . map fst $ getPostings rspec' j+ rspec' = rspec{rsQuery=startbalq,rsOpts=ropts'}- ropts' = case accountlistmode_ ropts of- ALTree -> ropts{period_=precedingperiod, no_elide_=True}- ALFlat -> ropts{period_=precedingperiod}+ -- If we're re-valuing every period, we need to have the unvalued start+ -- balance, so we can do it ourselves later.+ ropts' = case value_ ropts of+ Just (AtEnd _) -> ropts''{value_=Nothing}+ _ -> ropts''+ where ropts'' = ropts{period_=precedingperiod, no_elide_=accountlistmode_ ropts == ALTree} -- q projected back before the report start date. -- When there's no report start date, in case there are future txns (the hledger-ui case above),@@ -216,26 +214,6 @@ DateSpan Nothing Nothing -> emptydatespan a -> a --- | Calculate the span of the report to be generated.-calculateReportSpan :: ReportSpec -> Journal -> DateSpan-calculateReportSpan ReportSpec{rsQuery=query,rsOpts=ropts} j = reportspan- where- -- The date span specified by -b/-e/-p options and query args if any.- requestedspan = dbg3 "requestedspan" $ queryDateSpan (date2_ ropts) query- -- If the requested span is open-ended, close it using the journal's end dates.- -- This can still be the null (open) span if the journal is empty.- requestedspan' = dbg3 "requestedspan'" $- requestedspan `spanDefaultsFrom` journalDateSpan (date2_ ropts) j- -- The list of interval spans enclosing the requested span.- -- This list can be empty if the journal was empty,- -- or if hledger-ui has added its special date:-tomorrow to the query- -- and all txns are in the future.- intervalspans = dbg3 "intervalspans" $ splitSpan (interval_ ropts) requestedspan'- -- The requested span enlarged to enclose a whole number of intervals.- -- This can be the null span if there were no intervals.- reportspan = DateSpan (spanStart =<< headMay intervalspans)- (spanEnd =<< lastMay intervalspans)- -- | Remove any date queries and insert queries from the report span. -- The user's query expanded to the report span -- if there is one (otherwise any date queries are left as-is, which@@ -250,27 +228,15 @@ dateless = dbg3 "dateless" . filterQuery (not . queryIsDateOrDate2) dateqcons = if date2_ (rsOpts rspec) then Date2 else Date --- | Make a valuation function for valuating MixedAmounts and a given Day-makeValuation :: ReportSpec -> Journal -> PriceOracle -> (Day -> MixedAmount -> MixedAmount)-makeValuation rspec j priceoracle day = case value_ (rsOpts rspec) of- Nothing -> id- Just v -> mixedAmountApplyValuation priceoracle styles day mreportlast (rsToday rspec) multiperiod v- where- -- Some things needed if doing valuation.- styles = journalCommodityStyles j- mreportlast = reportPeriodLastDay rspec- multiperiod = interval_ (rsOpts rspec) /= NoInterval- -- | Group postings, grouped by their column getPostingsByColumn :: ReportSpec -> Journal -> DateSpan -> Map DateSpan [Posting] getPostingsByColumn rspec j reportspan = columns where -- Postings matching the query within the report period. ps :: [(Posting, Day)] = dbg5 "getPostingsByColumn" $ getPostings rspec j- days = map snd ps -- The date spans to be included as report columns.- colspans = calculateColSpans (rsOpts rspec) reportspan days+ colspans = dbg3 "colspans" $ splitSpan (interval_ $ rsOpts rspec) reportspan addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d emptyMap = M.fromList . zip colspans $ repeat [] @@ -296,33 +262,7 @@ PrimaryDate -> postingDate SecondaryDate -> postingDate2 --- | Calculate the DateSpans to be used for the columns of the report.-calculateColSpans :: ReportOpts -> DateSpan -> [Day] -> [DateSpan]-calculateColSpans ropts reportspan days =- splitSpan (interval_ ropts) displayspan- where- displayspan- | empty_ ropts = dbg3 "displayspan (-E)" reportspan -- all the requested intervals- | otherwise = dbg3 "displayspan" $ reportspan `spanIntersect` matchedspan -- exclude leading/trailing empty intervals- matchedspan = dbg3 "matchedspan" $ daysSpan days ---- | Gather the account balance changes into a regular matrix--- including the accounts from all columns.-calculateAccountChanges :: ReportSpec -> [DateSpan] -> Map DateSpan [Posting]- -> HashMap ClippedAccountName (Map DateSpan Account)-calculateAccountChanges rspec colspans colps- | queryDepth (rsQuery rspec) == Just 0 = acctchanges <> elided- | otherwise = acctchanges- where- -- Transpose to get each account's balance changes across all columns.- acctchanges = transposeMap colacctchanges-- colacctchanges :: Map DateSpan (HashMap ClippedAccountName Account) =- dbg5 "colacctchanges" $ fmap (acctChangesFromPostings rspec) colps-- elided = HM.singleton "..." $ M.fromList [(span, nullacct) | span <- colspans]- -- | Given a set of postings, eg for a single report column, gather -- the accounts that have postings and calculate the change amount for -- each. Accounts and amounts will be depth-clipped appropriately if@@ -338,70 +278,68 @@ filter ((0<) . anumpostings) depthq = dbg3 "depthq" $ filterQuery queryIsDepth query --- | Accumulate and value amounts, as specified by the report options.+-- | Gather the account balance changes into a regular matrix, then+-- accumulate and value amounts, as specified by the report options. -- -- Makes sure all report columns have an entry.-accumValueAmounts :: ReportOpts -> (Day -> MixedAmount -> MixedAmount) -> [DateSpan]- -> HashMap ClippedAccountName Account- -> HashMap ClippedAccountName (Map DateSpan Account)- -> HashMap ClippedAccountName (Map DateSpan Account)-accumValueAmounts ropts valuation colspans startbals acctchanges = -- PARTIAL:+calculateReportMatrix :: ReportSpec -> Journal -> PriceOracle+ -> HashMap ClippedAccountName Account+ -> Map DateSpan [Posting]+ -> HashMap ClippedAccountName (Map DateSpan Account)+calculateReportMatrix rspec@ReportSpec{rsOpts=ropts} j priceoracle startbals colps = -- PARTIAL: -- Ensure all columns have entries, including those with starting balances- HM.mapWithKey rowbals $ ((<>zeros) <$> acctchanges) <> (zeros <$ startbals)+ HM.mapWithKey rowbals allchanges where -- The valued row amounts to be displayed: per-period changes, -- zero-based cumulative totals, or -- starting-balance-based historical balances. rowbals name changes = dbg5 "rowbals" $ case balancetype_ ropts of PeriodChange -> changeamts- CumulativeChange -> cumulative+ CumulativeChange -> cumulativeSum avalue nullacct changeamts HistoricalBalance -> historical where- historical = cumulativeSum startingBalance- cumulative = cumulativeSum nullacct- changeamts = M.mapWithKey valueAcct changes-- cumulativeSum start = snd $ M.mapAccumWithKey accumValued start changes- where accumValued startAmt date newAmt = (s, valueAcct date s)- where s = sumAcct startAmt newAmt-+ -- changes to report on: usually just the changes itself, but use the+ -- differences in the historical amount for ValueChangeReports.+ changeamts = case reporttype_ ropts of+ ChangeReport -> M.mapWithKey avalue changes+ BudgetReport -> M.mapWithKey avalue changes+ ValueChangeReport -> periodChanges valuedStart historical+ historical = cumulativeSum avalue startingBalance changes startingBalance = HM.lookupDefault nullacct name startbals-- -- Add the values of two accounts. Should be right-biased, since it's used- -- in scanl, so other properties (such as anumpostings) stay in the right place- sumAcct Account{aibalance=i1,aebalance=e1} a@Account{aibalance=i2,aebalance=e2} =- a{aibalance = i1 + i2, aebalance = e1 + e2}+ valuedStart = avalue (DateSpan Nothing historicalDate) startingBalance - -- We may be converting amounts to value, per hledger_options.m4.md "Effect of --value on reports".- valueAcct (DateSpan _ (Just end)) acct =- acct{aibalance = value (aibalance acct), aebalance = value (aebalance acct)}- where value = valuation (addDays (-1) end)- valueAcct _ _ = error "multiBalanceReport: expected all spans to have an end date" -- XXX should not happen+ -- Transpose to get each account's balance changes across all columns, then+ -- pad with zeros+ allchanges = ((<>zeros) <$> acctchanges) <> (zeros <$ startbals)+ acctchanges = dbg5 "acctchanges" . addElided $ transposeMap colacctchanges+ colacctchanges = dbg5 "colacctchanges" $ fmap (acctChangesFromPostings rspec) valuedps+ valuedps = M.mapWithKey (\colspan -> map (pvalue colspan)) colps + (pvalue, avalue) = postingAndAccountValuations rspec j priceoracle+ addElided = if queryDepth (rsQuery rspec) == Just 0 then HM.insert "..." zeros else id+ historicalDate = minimumMay $ mapMaybe spanStart colspans zeros = M.fromList [(span, nullacct) | span <- colspans]+ colspans = M.keys colps + -- | Lay out a set of postings grouped by date span into a regular matrix with rows -- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport -- from the columns.-generateMultiBalanceReport :: ReportSpec -> Journal -> (Day -> MixedAmount -> MixedAmount) -> [DateSpan]+generateMultiBalanceReport :: ReportSpec -> Journal -> PriceOracle -> Map DateSpan [Posting] -> HashMap AccountName Account -> MultiBalanceReport-generateMultiBalanceReport rspec@ReportSpec{rsOpts=ropts} j valuation colspans colps startbals =+generateMultiBalanceReport rspec@ReportSpec{rsOpts=ropts} j priceoracle colps startbals = report where- -- Each account's balance changes across all columns.- acctchanges = dbg5 "acctchanges" $ calculateAccountChanges rspec colspans colps- -- Process changes into normal, cumulative, or historical amounts, plus value them- accumvalued = accumValueAmounts ropts valuation colspans startbals acctchanges+ matrix = calculateReportMatrix rspec j priceoracle startbals colps -- All account names that will be displayed, possibly depth-clipped.- displaynames = dbg5 "displaynames" $ displayedAccounts rspec accumvalued+ displaynames = dbg5 "displaynames" $ displayedAccounts rspec matrix -- All the rows of the report.- rows = dbg5 "rows"- . (if invert_ ropts then map (fmap negate) else id) -- Negate amounts if applicable- $ buildReportRows ropts displaynames accumvalued+ rows = dbg5 "rows" . (if invert_ ropts then map (fmap negate) else id) -- Negate amounts if applicable+ $ buildReportRows ropts displaynames matrix -- Calculate column totals totalsrow = dbg5 "totalsrow" $ calculateTotalsRow ropts rows@@ -410,7 +348,7 @@ sortedrows = dbg5 "sortedrows" $ sortRows ropts j rows -- Take percentages if needed- report = reportPercent ropts $ PeriodicReport colspans sortedrows totalsrow+ report = reportPercent ropts $ PeriodicReport (M.keys colps) sortedrows totalsrow -- | Build the report rows. -- One row per account, with account name info, row amounts, row total and row average.@@ -560,7 +498,7 @@ -- Makes sure that all DateSpans are present in all rows. transposeMap :: Map DateSpan (HashMap AccountName a) -> HashMap AccountName (Map DateSpan a)-transposeMap xs = M.foldrWithKey addSpan mempty xs+transposeMap = M.foldrWithKey addSpan mempty where addSpan span acctmap seen = HM.foldrWithKey (addAcctSpan span) seen acctmap @@ -592,6 +530,44 @@ guard $ amountIsZero a' || amountIsZero b' || acommodity a' == acommodity b' return $ mixed [per $ if aquantity b' == 0 then 0 else aquantity a' / abs (aquantity b') * 100] where errmsg = "Cannot calculate percentages if accounts have different commodities (Hint: Try --cost, -V or similar flags.)"++-- Add the values of two accounts. Should be right-biased, since it's used+-- in scanl, so other properties (such as anumpostings) stay in the right place+sumAcct :: Account -> Account -> Account+sumAcct Account{aibalance=i1,aebalance=e1} a@Account{aibalance=i2,aebalance=e2} =+ a{aibalance = i1 + i2, aebalance = e1 + e2}++-- Subtract the values in one account from another. Should be left-biased.+subtractAcct :: Account -> Account -> Account+subtractAcct a@Account{aibalance=i1,aebalance=e1} Account{aibalance=i2,aebalance=e2} =+ a{aibalance = i1 - i2, aebalance = e1 - e2}++-- | Extract period changes from a cumulative list+periodChanges :: Account -> Map k Account -> Map k Account+periodChanges start amtmap =+ M.fromDistinctAscList . zip dates $ zipWith subtractAcct amts (start:amts)+ where (dates, amts) = unzip $ M.toAscList amtmap++-- | Calculate a cumulative sum from a list of period changes and a valuation+-- function.+cumulativeSum :: (DateSpan -> Account -> Account) -> Account -> Map DateSpan Account -> Map DateSpan Account+cumulativeSum value start = snd . M.mapAccumWithKey accumValued start+ where accumValued startAmt date newAmt = let s = sumAcct startAmt newAmt in (s, value date s)++-- | Calculate the Posting and Account valuation functions required by this+-- MultiBalanceReport.+postingAndAccountValuations :: ReportSpec -> Journal -> PriceOracle+ -> (DateSpan -> Posting -> Posting, DateSpan -> Account -> Account)+postingAndAccountValuations ReportSpec{rsToday=today, rsOpts=ropts} j priceoracle = case value_ ropts of+ Just (AtEnd _) -> (const id, avalue' (cost_ ropts) (value_ ropts))+ _ -> (pvalue' (cost_ ropts) (value_ ropts), const id)+ where+ avalue' c v span a = a{aibalance = value (aibalance a), aebalance = value (aebalance a)}+ where value = mixedAmountApplyCostValuation priceoracle styles (end span) today (error "multiBalanceReport: did not expect amount valuation to be called ") c v -- PARTIAL: should not happen+ pvalue' c v span = postingApplyCostValuation priceoracle styles (end span) today c v+ end = fromMaybe (error "multiBalanceReport: expected all spans to have an end date") -- XXX should not happen+ . fmap (addDays (-1)) . spanEnd+ styles = journalCommodityStyles j -- tests
Hledger/Reports/PostingsReport.hs view
@@ -24,8 +24,7 @@ import Data.List import Data.List.Extra (nubSort) import Data.Maybe--- import Data.Text (Text)-import qualified Data.Text as T+import Data.Text (Text) import Data.Time.Calendar import Safe (headMay, lastMay) @@ -35,12 +34,10 @@ import Hledger.Reports.ReportOptions --- | A postings report is a list of postings with a running total, a label--- for the total field, and a little extra transaction info to help with rendering.+-- | A postings report is a list of postings with a running total, and a little extra+-- transaction info to help with rendering. -- This is used eg for the register command.-type PostingsReport = (String -- label for the running balance column XXX remove- ,[PostingsReportItem] -- line items, one per posting- )+type PostingsReport = [PostingsReportItem] -- line items, one per posting type PostingsReportItem = (Maybe Day -- The posting date, if this is the first posting in a -- transaction or if it's different from the previous -- posting's date. Or if this a summary posting, the@@ -49,7 +46,7 @@ ,Maybe Day -- If this is a summary posting, the report interval's -- end date if this is the first summary posting in -- the interval.- ,Maybe String -- The posting's transaction's description, if this is the first posting in the transaction.+ ,Maybe Text -- The posting's transaction's description, if this is the first posting in the transaction. ,Posting -- The posting, possibly with the account name depth-clipped. ,MixedAmount -- The running total after this posting, or with --average, -- the running average posting amount. With --historical,@@ -66,10 +63,9 @@ -- | Select postings from the journal and add running balance and other -- information to make a postings report. Used by eg hledger's register command. postingsReport :: ReportSpec -> Journal -> PostingsReport-postingsReport rspec@ReportSpec{rsOpts=ropts@ReportOpts{..}} j =- (totallabel, items)+postingsReport rspec@ReportSpec{rsOpts=ropts@ReportOpts{..}} j = items where- reportspan = adjustReportDates rspec j+ reportspan = reportSpanBothDates j rspec whichdate = whichDateFromOpts ropts mdepth = queryDepth $ rsQuery rspec styles = journalCommodityStyles j@@ -79,19 +75,18 @@ -- postings to be included in the report, and similarly-matched postings before the report start date (precedingps, reportps) = matchedPostingsBeforeAndDuring rspec j reportspan + -- We may be converting posting amounts to value, per hledger_options.m4.md "Effect of --value on reports".+ pvalue periodlast = postingApplyCostValuation priceoracle styles periodlast (rsToday rspec) cost_ value_+ -- Postings, or summary postings with their subperiod's end date, to be displayed. displayps :: [(Posting, Maybe Day)]- | multiperiod && changingValuation ropts = [(pvalue lastday p, Just periodend) | (p, periodend) <- summariseps reportps, let lastday = addDays (-1) periodend]- | multiperiod = [(p, Just periodend) | (p, periodend) <- summariseps valuedps]- | otherwise = [(p, Nothing) | p <- valuedps]+ | multiperiod, Just (AtEnd _) <- value_ = [(pvalue lastday p, Just periodend) | (p, periodend) <- summariseps reportps, let lastday = addDays (-1) periodend]+ | multiperiod = [(p, Just periodend) | (p, periodend) <- summariseps valuedps]+ | otherwise = [(p, Nothing) | p <- valuedps] where summariseps = summarisePostingsByInterval interval_ whichdate mdepth showempty reportspan valuedps = map (pvalue reportorjournallast) reportps showempty = empty_ || average_- -- We may be converting posting amounts to value, per hledger_options.m4.md "Effect of --value on reports".- pvalue periodlast p = maybe p (postingApplyValuation priceoracle styles periodlast mreportlast (rsToday rspec) multiperiod p) value_- where- mreportlast = reportPeriodLastDay rspec reportorjournallast = fromMaybe (error' "postingsReport: expected a non-empty journal") $ -- PARTIAL: shouldn't happen reportPeriodOrJournalLastDay rspec j@@ -106,19 +101,16 @@ -- of --value on reports". -- XXX balance report doesn't value starting balance.. should this ? historical = balancetype_ == HistoricalBalance- startbal | average_ = if historical then bvalue precedingavg else 0- | otherwise = if historical then bvalue precedingsum else 0+ startbal | average_ = if historical then precedingavg else 0+ | otherwise = if historical then precedingsum else 0 where- precedingsum = sumPostings precedingps+ precedingsum = sumPostings $ map (pvalue daybeforereportstart) precedingps precedingavg | null precedingps = 0 | otherwise = divideMixedAmount (fromIntegral $ length precedingps) precedingsum- bvalue = maybe id (mixedAmountApplyValuation priceoracle styles daybeforereportstart Nothing (rsToday rspec) multiperiod) value_- -- XXX constrain valuation type to AtDate daybeforereportstart here ?- where- daybeforereportstart =- maybe (error' "postingsReport: expected a non-empty journal") -- PARTIAL: shouldn't happen- (addDays (-1))- $ reportPeriodOrJournalStart rspec j+ daybeforereportstart =+ maybe (error' "postingsReport: expected a non-empty journal") -- PARTIAL: shouldn't happen+ (addDays (-1))+ $ reportPeriodOrJournalStart rspec j runningcalc = registerRunningCalculationFn ropts startnum = if historical then length precedingps + 1 else 1@@ -132,28 +124,6 @@ | average_ ropts = \i avg amt -> avg + divideMixedAmount (fromIntegral i) (amt - avg) | otherwise = \_ bal amt -> bal + amt -totallabel = "Total"---- | Adjust report start/end dates to more useful ones based on--- journal data and report intervals. Ie:--- 1. If the start date is unspecified, use the earliest date in the journal (if any)--- 2. If the end date is unspecified, use the latest date in the journal (if any)--- 3. If a report interval is specified, enlarge the dates to enclose whole intervals-adjustReportDates :: ReportSpec -> Journal -> DateSpan-adjustReportDates rspec@ReportSpec{rsOpts=ropts} j = reportspan- where- -- see also multiBalanceReport- requestedspan = dbg3 "requestedspan" $ queryDateSpan' $ rsQuery rspec -- span specified by -b/-e/-p options and query args- journalspan = dbg3 "journalspan" $ dates `spanUnion` date2s -- earliest and latest dates (or date2s) in the journal- where- dates = journalDateSpan False j- date2s = journalDateSpan True j- requestedspanclosed = dbg3 "requestedspanclosed" $ requestedspan `spanDefaultsFrom` journalspan -- if open-ended, close it using the journal's dates (if any)- intervalspans = dbg3 "intervalspans" $ splitSpan (interval_ ropts) requestedspanclosed -- get the whole intervals enclosing that- mreportstart = dbg3 "reportstart" $ maybe Nothing spanStart $ headMay intervalspans -- start of the first interval, or open ended- mreportend = dbg3 "reportend" $ maybe Nothing spanEnd $ lastMay intervalspans -- end of the last interval, or open ended- reportspan = dbg3 "reportspan" $ DateSpan mreportstart mreportend -- the requested span enlarged to whole intervals if possible- -- | Find postings matching a given query, within a given date span, -- and also any similarly-matched postings before that date span. -- Date restrictions and depth restrictions in the query are ignored.@@ -208,14 +178,13 @@ mkpostingsReportItem showdate showdesc wd menddate p b = (if showdate then Just date else Nothing ,menddate- ,if showdesc then Just desc else Nothing+ ,if showdesc then tdescription <$> ptransaction p else Nothing ,p ,b ) where date = case wd of PrimaryDate -> postingDate p SecondaryDate -> postingDate2 p- desc = T.unpack $ maybe "" tdescription $ ptransaction p -- | Convert a list of postings into summary postings, one per interval, -- aggregated to the specified depth if any.@@ -269,7 +238,7 @@ tests_PostingsReport = tests "PostingsReport" [ test "postingsReport" $ do- let (query, journal) `gives` n = (length $ snd $ postingsReport defreportspec{rsQuery=query} journal) @?= n+ let (query, journal) `gives` n = (length $ postingsReport defreportspec{rsQuery=query} journal) @?= n -- with the query specified explicitly (Any, nulljournal) `gives` 0 (Any, samplejournal) `gives` 13@@ -278,10 +247,10 @@ (And [Depth 1, StatusQ Cleared, Acct (toRegex' "expenses")], samplejournal) `gives` 2 (And [And [Depth 1, StatusQ Cleared], Acct (toRegex' "expenses")], samplejournal) `gives` 2 -- with query and/or command-line options- (length $ snd $ postingsReport defreportspec samplejournal) @?= 13- (length $ snd $ postingsReport defreportspec{rsOpts=defreportopts{interval_=Months 1}} samplejournal) @?= 11- (length $ snd $ postingsReport defreportspec{rsOpts=defreportopts{interval_=Months 1, empty_=True}} samplejournal) @?= 20- (length $ snd $ postingsReport defreportspec{rsQuery=Acct $ toRegex' "assets:bank:checking"} samplejournal) @?= 5+ (length $ postingsReport defreportspec samplejournal) @?= 13+ (length $ postingsReport defreportspec{rsOpts=defreportopts{interval_=Months 1}} samplejournal) @?= 11+ (length $ postingsReport defreportspec{rsOpts=defreportopts{interval_=Months 1, empty_=True}} samplejournal) @?= 20+ (length $ postingsReport defreportspec{rsQuery=Acct $ toRegex' "assets:bank:checking"} samplejournal) @?= 5 -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0 -- [(Just (fromGregorian 2008 01 01,"income"),assets:bank:checking $1,$1)
Hledger/Reports/ReportOptions.hs view
@@ -11,6 +11,7 @@ module Hledger.Reports.ReportOptions ( ReportOpts(..), ReportSpec(..),+ ReportType(..), BalanceType(..), AccountListMode(..), ValuationType(..),@@ -21,9 +22,9 @@ updateReportSpec, updateReportSpecWith, rawOptsToReportSpec,+ balanceTypeOverride, flat_, tree_,- changingValuation, reportOptsToggleStatus, simplifyStatuses, whichDateFromOpts,@@ -34,24 +35,23 @@ transactionDateFn, postingDateFn, reportSpan,+ reportSpanBothDates, reportStartDate, reportEndDate, reportPeriodStart, reportPeriodOrJournalStart, reportPeriodLastDay, reportPeriodOrJournalLastDay,- valuationTypeIsCost,- valuationTypeIsDefaultValue, ) where import Control.Applicative ((<|>)) import Data.List.Extra (nubSort)-import Data.Maybe (fromMaybe, isJust)+import Data.Maybe (fromMaybe, isJust, mapMaybe) import qualified Data.Text as T import Data.Time.Calendar (Day, addDays) import Data.Default (Default(..))-import Safe (lastDef, lastMay)+import Safe (headMay, lastDef, lastMay, maximumMay) import System.Console.ANSI (hSupportsANSIColor) import System.Environment (lookupEnv)@@ -63,8 +63,16 @@ import Hledger.Utils --- | Which "balance" is being shown in a balance report.-data BalanceType = PeriodChange -- ^ The change of balance in each period.+-- | What is calculated and shown in each cell in a balance report.+data ReportType = ChangeReport -- ^ The sum of posting amounts.+ | BudgetReport -- ^ The sum of posting amounts and the goal.+ | ValueChangeReport -- ^ The change of value of period-end historical values.+ deriving (Eq, Show)++instance Default ReportType where def = ChangeReport++-- | Which "accumulation method" is being shown in a balance report.+data BalanceType = PeriodChange -- ^ The accumulate change over a single period. | CumulativeChange -- ^ The accumulated change across multiple periods. | HistoricalBalance -- ^ The historical ending balance, including the effect of -- all postings before the report period. Unless altered by,@@ -87,6 +95,7 @@ period_ :: Period ,interval_ :: Interval ,statuses_ :: [Status] -- ^ Zero, one, or two statuses to be matched+ ,cost_ :: Costing -- ^ Should we convert amounts to cost, when present? ,value_ :: Maybe ValuationType -- ^ What value should amounts be converted to ? ,infer_value_ :: Bool -- ^ Infer market prices from transactions ? ,depth_ :: Maybe Int@@ -103,6 +112,7 @@ -- for account transactions reports (aregister) ,txn_dates_ :: Bool -- for balance reports (bal, bs, cf, is)+ ,reporttype_ :: ReportType ,balancetype_ :: BalanceType ,accountlistmode_ :: AccountListMode ,drop_ :: Int@@ -136,6 +146,7 @@ { period_ = PeriodAll , interval_ = NoInterval , statuses_ = []+ , cost_ = NoCost , value_ = Nothing , infer_value_ = False , depth_ = Nothing@@ -148,6 +159,7 @@ , average_ = False , related_ = False , txn_dates_ = False+ , reporttype_ = def , balancetype_ = def , accountlistmode_ = ALFlat , drop_ = 0@@ -170,20 +182,22 @@ supports_color <- hSupportsANSIColor stdout let colorflag = stringopt "color" rawopts- formatstring = maybestringopt "format" rawopts+ formatstring = T.pack <$> maybestringopt "format" rawopts querystring = map T.pack $ listofstringopt "args" rawopts -- doesn't handle an arg like "" right+ (costing, valuation) = valuationTypeFromRawOpts rawopts format <- case parseStringFormat <$> formatstring of Nothing -> return defaultBalanceLineFormat Just (Right x) -> return x Just (Left err) -> fail $ "could not parse format option: " ++ err - let reportopts = defreportopts+ return defreportopts {period_ = periodFromRawOpts d rawopts ,interval_ = intervalFromRawOpts rawopts ,statuses_ = statusesFromRawOpts rawopts- ,value_ = valuationTypeFromRawOpts rawopts- ,infer_value_ = boolopt "infer-value" rawopts+ ,cost_ = costing+ ,value_ = valuation+ ,infer_value_ = boolopt "infer-market-price" rawopts ,depth_ = maybeposintopt "depth" rawopts ,date2_ = boolopt "date2" rawopts ,empty_ = boolopt "empty" rawopts@@ -194,6 +208,7 @@ ,average_ = boolopt "average" rawopts ,related_ = boolopt "related" rawopts ,txn_dates_ = boolopt "txn-dates" rawopts+ ,reporttype_ = reporttypeopt rawopts ,balancetype_ = balancetypeopt rawopts ,accountlistmode_ = accountlistmodeopt rawopts ,drop_ = posintopt "drop" rawopts@@ -210,7 +225,6 @@ ,forecast_ = forecastPeriodFromRawOpts d rawopts ,transpose_ = boolopt "transpose" rawopts }- return reportopts -- | The result of successfully parsing a ReportOpts on a particular -- Day. Any ambiguous dates are completed and Queries are parsed,@@ -273,13 +287,29 @@ "flat" -> Just ALFlat _ -> Nothing +reporttypeopt :: RawOpts -> ReportType+reporttypeopt =+ fromMaybe ChangeReport . choiceopt parse where+ parse = \case+ "sum" -> Just ChangeReport+ "valuechange" -> Just ValueChangeReport+ "budget" -> Just BudgetReport+ _ -> Nothing+ balancetypeopt :: RawOpts -> BalanceType-balancetypeopt =- fromMaybe PeriodChange . choiceopt parse where+balancetypeopt = fromMaybe PeriodChange . balanceTypeOverride++balanceTypeOverride :: RawOpts -> Maybe BalanceType+balanceTypeOverride rawopts = choiceopt parse rawopts <|> reportbal+ where parse = \case "historical" -> Just HistoricalBalance "cumulative" -> Just CumulativeChange+ "change" -> Just PeriodChange _ -> Nothing+ reportbal = case reporttypeopt rawopts of+ ValueChangeReport -> Just PeriodChange+ _ -> Nothing -- Get the period specified by any -b/--begin, -e/--end and/or -p/--period -- options appearing in the command line.@@ -402,27 +432,37 @@ | s `elem` ss = ropts{statuses_=filter (/= s) ss} | otherwise = ropts{statuses_=simplifyStatuses (s:ss)} --- | Parse the type of valuation to be performed, if any, specified by--- -B/--cost, -V, -X/--exchange, or --value flags. If there's more--- than one of these, the rightmost flag wins.-valuationTypeFromRawOpts :: RawOpts -> Maybe ValuationType-valuationTypeFromRawOpts = lastMay . collectopts valuationfromrawopt+-- | Parse the type of valuation and costing to be performed, if any,+-- specified by -B/--cost, -V, -X/--exchange, or --value flags. It is+-- allowed to combine -B/--cost with any other valuation type. If+-- there's more than one valuation type, the rightmost flag wins.+valuationTypeFromRawOpts :: RawOpts -> (Costing, Maybe ValuationType)+valuationTypeFromRawOpts rawopts = (costing, valuation) where+ costing = if (any ((Cost==) . fst) valuationopts) then Cost else NoCost+ valuation = case reporttypeopt rawopts of+ ValueChangeReport -> case directval of+ Nothing -> Just $ AtEnd Nothing -- If no valuation requested for valuechange, use AtEnd+ Just (AtEnd _) -> directval -- If AtEnd valuation requested, use it+ Just _ -> usageError "--valuechange only produces sensible results with --value=end"+ _ -> directval -- Otherwise, use requested valuation+ where directval = lastMay $ mapMaybe snd valuationopts++ valuationopts = collectopts valuationfromrawopt rawopts valuationfromrawopt (n,v) -- option name, value- | n == "B" = Just $ AtCost Nothing- | n == "V" = Just $ AtDefault Nothing- | n == "X" = Just $ AtDefault (Just $ T.pack v)- | n == "value" = Just $ valuation v+ | n == "B" = Just (Cost, Nothing) -- keep supporting --value=cost for now+ | n == "V" = Just (NoCost, Just $ AtEnd Nothing)+ | n == "X" = Just (NoCost, Just $ AtEnd (Just $ T.pack v))+ | n == "value" = Just $ valueopt v | otherwise = Nothing- valuation v- | t `elem` ["cost","c"] = AtCost mc- | t `elem` ["then" ,"t"] = AtThen mc- | t `elem` ["end" ,"e"] = AtEnd mc- | t `elem` ["now" ,"n"] = AtNow mc- | otherwise =- case parsedateM t of- Just d -> AtDate d mc- Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: cost|then|end|now|c|t|e|n|YYYY-MM-DD"+ valueopt v+ | t `elem` ["cost","c"] = (Cost, AtEnd . Just <$> mc) -- keep supporting --value=cost,COMM for now+ | t `elem` ["then" ,"t"] = (NoCost, Just $ AtThen mc)+ | t `elem` ["end" ,"e"] = (NoCost, Just $ AtEnd mc)+ | t `elem` ["now" ,"n"] = (NoCost, Just $ AtNow mc)+ | otherwise = case parsedateM t of+ Just d -> (NoCost, Just $ AtDate d mc)+ Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: then|end|now|t|e|n|YYYY-MM-DD" where -- parse --value's value: TYPE[,COMM] (t,c') = break (==',') v@@ -430,18 +470,6 @@ "" -> Nothing c -> Just $ T.pack c -valuationTypeIsCost :: ReportOpts -> Bool-valuationTypeIsCost ropts =- case value_ ropts of- Just (AtCost _) -> True- _ -> False--valuationTypeIsDefaultValue :: ReportOpts -> Bool-valuationTypeIsDefaultValue ropts =- case value_ ropts of- Just (AtDefault _) -> True- _ -> False- -- | Select the Transaction date accessor based on --date2. transactionDateFn :: ReportOpts -> (Transaction -> Day) transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate@@ -466,13 +494,12 @@ -- depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts) -- | Convert this journal's postings' amounts to cost using their--- transaction prices, if specified by options (-B/--value=cost).+-- transaction prices, if specified by options (-B/--cost). -- Maybe soon superseded by newer valuation code. journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal-journalSelectingAmountFromOpts opts =- case value_ opts of- Just (AtCost _) -> journalToCost- _ -> id+journalSelectingAmountFromOpts opts = case cost_ opts of+ Cost -> journalToCost+ NoCost -> id -- | Convert report options to a query, ignoring any non-flag command line arguments. queryFromFlags :: ReportOpts -> Query@@ -486,28 +513,47 @@ consIf f b = if b then (f True:) else id consJust f = maybe id ((:) . f) --- | Whether the market price for postings might change when reported in--- different report periods.-changingValuation :: ReportOpts -> Bool-changingValuation ropts = case value_ ropts of- Just (AtCost (Just _)) -> True- Just (AtEnd _) -> True- Just (AtDefault _) -> interval_ ropts /= NoInterval- _ -> False- -- Report dates. -- | The effective report span is the start and end dates specified by -- options or queries, or otherwise the earliest and latest transaction or -- posting dates in the journal. If no dates are specified by options/queries -- and the journal is empty, returns the null date span.+-- The boolean argument flags whether primary and secondary dates are considered+-- equivalently. reportSpan :: Journal -> ReportSpec -> DateSpan-reportSpan j ReportSpec{rsQuery=query} = dbg3 "reportspan" $ DateSpan mstartdate menddate+reportSpan = reportSpanHelper False++-- | Like reportSpan, but uses both primary and secondary dates when calculating+-- the span.+reportSpanBothDates :: Journal -> ReportSpec -> DateSpan+reportSpanBothDates = reportSpanHelper True++-- | A helper for reportSpan, which takes a Bool indicating whether to use both+-- primary and secondary dates.+reportSpanHelper :: Bool -> Journal -> ReportSpec -> DateSpan+reportSpanHelper bothdates j ReportSpec{rsQuery=query, rsOpts=ropts} = reportspan where- DateSpan mjournalstartdate mjournalenddate =- dbg3 "journalspan" $ journalDateSpan False j -- ignore secondary dates- mstartdate = queryStartDate False query <|> mjournalstartdate- menddate = queryEndDate False query <|> mjournalenddate+ -- The date span specified by -b/-e/-p options and query args if any.+ requestedspan = dbg3 "requestedspan" $ if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query+ -- If we are requesting period-end valuation, the journal date span should+ -- include price directives after the last transaction+ journalspan = dbg3 "journalspan" $ if bothdates then journalDateSpanBothDates j else journalDateSpan (date2_ ropts) j+ pricespan = dbg3 "pricespan" . DateSpan Nothing $ case value_ ropts of+ Just (AtEnd _) -> fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j+ _ -> Nothing+ -- If the requested span is open-ended, close it using the journal's start and end dates.+ -- This can still be the null (open) span if the journal is empty.+ requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanUnion` pricespan)+ -- The list of interval spans enclosing the requested span.+ -- This list can be empty if the journal was empty,+ -- or if hledger-ui has added its special date:-tomorrow to the query+ -- and all txns are in the future.+ intervalspans = dbg3 "intervalspans" $ splitSpan (interval_ ropts) requestedspan'+ -- The requested span enlarged to enclose a whole number of intervals.+ -- This can be the null span if there were no intervals.+ reportspan = dbg3 "reportspan" $ DateSpan (spanStart =<< headMay intervalspans)+ (spanEnd =<< lastMay intervalspans) reportStartDate :: Journal -> ReportSpec -> Maybe Day reportStartDate j = spanStart . reportSpan j@@ -538,8 +584,13 @@ -- Get the last day of the overall report period, or if no report -- period is specified, the last day of the journal (ie the latest--- posting date). If there's no report period and nothing in the+-- posting date). If we're doing period-end valuation, include price+-- directive dates. If there's no report period and nothing in the -- journal, will be Nothing. reportPeriodOrJournalLastDay :: ReportSpec -> Journal -> Maybe Day-reportPeriodOrJournalLastDay rspec j =- reportPeriodLastDay rspec <|> journalEndDate False j+reportPeriodOrJournalLastDay rspec j = reportPeriodLastDay rspec <|> journalOrPriceEnd+ where+ journalOrPriceEnd = case value_ $ rsOpts rspec of+ Just (AtEnd _) -> max (journalEndDate False j) lastPriceDirective+ _ -> journalEndDate False j+ lastPriceDirective = fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j
Hledger/Reports/ReportTypes.hs view
@@ -32,9 +32,10 @@ , prrDepth ) where -import Data.Aeson-import Data.Decimal+import Data.Aeson (ToJSON(..))+import Data.Decimal (Decimal) import Data.Maybe (mapMaybe)+import Data.Text (Text) #if !(MIN_VERSION_base(4,11,0)) import Data.Semigroup (Semigroup(..)) #endif@@ -144,16 +145,16 @@ -- It is used in compound balance report commands like balancesheet, -- cashflow and incomestatement. data CompoundPeriodicReport a b = CompoundPeriodicReport- { cbrTitle :: String+ { cbrTitle :: Text , cbrDates :: [DateSpan]- , cbrSubreports :: [(String, PeriodicReport a b, Bool)]+ , cbrSubreports :: [(Text, PeriodicReport a b, Bool)] , cbrTotals :: PeriodicReportRow () b } deriving (Show, Functor, Generic, ToJSON) -- | Description of one subreport within a compound balance report. -- Part of a "CompoundBalanceCommandSpec", but also used in hledger-lib. data CBCSubreportSpec a = CBCSubreportSpec- { cbcsubreporttitle :: String -- ^ The title to use for the subreport+ { cbcsubreporttitle :: Text -- ^ The title to use for the subreport , cbcsubreportquery :: Journal -> Query -- ^ The Query to use for the subreport , cbcsubreportoptions :: ReportOpts -> ReportOpts -- ^ A function to transform the ReportOpts used to produce the subreport , cbcsubreporttransform :: PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount -- ^ A function to transform the result of the subreport
Hledger/Reports/TransactionsReport.hs view
@@ -23,6 +23,7 @@ import Data.List import Data.List.Extra (nubSort)+import Data.Text (Text) import Data.Ord import Hledger.Data@@ -34,18 +35,14 @@ -- | A transactions report includes a list of transactions touching multiple accounts -- (posting-filtered and unfiltered variants), a running balance, and some--- other information helpful for rendering a register view (a flag--- indicating multiple other accounts and a display string describing--- them) with or without a notion of current account(s).--- Two kinds of report use this data structure, see transactionsReport+-- other information helpful for rendering a register view with or without a notion+-- of current account(s). Two kinds of report use this data structure, see transactionsReport -- and accountTransactionsReport below for details.-type TransactionsReport = (String -- label for the balance column, eg "balance" or "total"- ,[TransactionsReportItem] -- line items, one per transaction- )+type TransactionsReport = [TransactionsReportItem] -- line items, one per transaction type TransactionsReportItem = (Transaction -- the original journal transaction, unmodified ,Transaction -- the transaction as seen from a particular account, with postings maybe filtered ,Bool -- is this a split, ie more than one other account posting- ,String -- a display string describing the other account(s), if any+ ,Text -- a display string describing the other account(s), if any ,MixedAmount -- the amount posted to the current account(s) by the filtered postings (or total amount posted) ,MixedAmount -- the running total of item amounts, starting from zero; -- or with --historical, the running total including items@@ -59,14 +56,12 @@ triCommodityAmount c = filterMixedAmountByCommodity c . triAmount triCommodityBalance c = filterMixedAmountByCommodity c . triBalance -totallabel = "Period Total"- -- | Select transactions from the whole journal. This is similar to a -- "postingsReport" except with transaction-based report items which -- are ordered most recent first. XXX Or an EntriesReport - use that instead ? -- This is used by hledger-web's journal view. transactionsReport :: ReportOpts -> Journal -> Query -> TransactionsReport-transactionsReport opts j q = (totallabel, items)+transactionsReport opts j q = items where -- XXX items' first element should be the full transaction with all postings items = reverse $ accountTransactionsReportItems q None nullmixedamt id ts@@ -79,15 +74,14 @@ transactionsReportByCommodity tr = [(c, filterTransactionsReportByCommodity c tr) | c <- transactionsReportCommodities tr] where- transactionsReportCommodities (_,items) =- nubSort . map acommodity $ concatMap (amounts . triAmount) items+ transactionsReportCommodities = nubSort . map acommodity . concatMap (amounts . triAmount) -- Remove transaction report items and item amount (and running -- balance amount) components that don't involve the specified -- commodity. Other item fields such as the transaction are left unchanged. filterTransactionsReportByCommodity :: CommoditySymbol -> TransactionsReport -> TransactionsReport-filterTransactionsReportByCommodity c (label,items) =- (label, fixTransactionsReportItemBalances $ concat [filterTransactionsReportItemByCommodity c i | i <- items])+filterTransactionsReportByCommodity c =+ fixTransactionsReportItemBalances . concatMap (filterTransactionsReportItemByCommodity c) where filterTransactionsReportItemByCommodity c (t,t2,s,o,a,bal) | c `elem` cs = [item']
Hledger/Utils/Color.hs view
@@ -1,17 +1,25 @@ -- | Basic color helpers for prettifying console output. +{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Hledger.Utils.Color ( color, bgColor,+ colorB,+ bgColorB, Color(..), ColorIntensity(..) ) where +#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif+import qualified Data.Text.Lazy.Builder as TB import System.Console.ANSI+import Hledger.Utils.Text (WideBuilder(..)) -- | Wrap a string in ANSI codes to set and reset foreground colour.@@ -21,3 +29,13 @@ -- | Wrap a string in ANSI codes to set and reset background colour. bgColor :: ColorIntensity -> Color -> String -> String bgColor int col s = setSGRCode [SetColor Background int col] ++ s ++ setSGRCode []++-- | Wrap a WideBuilder in ANSI codes to set and reset foreground colour.+colorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder+colorB int col (WideBuilder s w) =+ WideBuilder (TB.fromString (setSGRCode [SetColor Foreground int col]) <> s <> TB.fromString (setSGRCode [])) w++-- | Wrap a WideBuilder in ANSI codes to set and reset background colour.+bgColorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder+bgColorB int col (WideBuilder s w) =+ WideBuilder (TB.fromString (setSGRCode [SetColor Background int col]) <> s <> TB.fromString (setSGRCode [])) w
Hledger/Utils/Debug.hs view
@@ -15,6 +15,21 @@ save a dummy change in Debug.hs and do a :reload. (Sometimes it's more convenient to temporarily add dbg0's and :reload.) +In hledger, debug levels are used as follows:++Debug level: What to show:+------------ ---------------------------------------------------------+0 normal command output only (no warnings, eg)+1 (--debug) useful warnings, most common troubleshooting info, eg valuation+2 common troubleshooting info, more detail+3 report options selection+4 report generation+5 report generation, more detail+6 input file reading+7 input file reading, more detail+8 command line parsing+9 any other rarely needed / more in-depth info+ -} -- more:@@ -153,9 +168,11 @@ | level > 0 && debugLevel < level = flip const | otherwise = trace --- | Trace (print to stderr) a showable value using a custom show function.-traceAtWith :: (a -> String) -> a -> a-traceAtWith f a = trace (f a) a+-- | Trace (print to stderr) a showable value using a custom show function,+-- if the global debug level is at or above the specified level.+-- At level 0, always prints. Otherwise, uses unsafePerformIO.+traceAtWith :: Int -> (a -> String) -> a -> a+traceAtWith level f a = traceAt level (f a) a -- | Pretty-print a label and a showable value to the console -- if the global debug level is at or above the specified level.
Hledger/Utils/Regex.hs view
@@ -1,5 +1,7 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} {-|@@ -54,6 +56,7 @@ ,RegexError -- * total regex operations ,regexMatch+ ,regexMatchText ,regexReplace ,regexReplaceUnmemo ,regexReplaceAllBy@@ -66,6 +69,10 @@ import Data.Char (isDigit) import Data.List (foldl') import Data.MemoUgly (memo)+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif+import Data.Text (Text) import qualified Data.Text as T import Text.Regex.TDFA ( Regex, CompOption(..), defaultCompOpt, defaultExecOpt,@@ -78,8 +85,8 @@ -- | Regular expression. Extended regular expression-ish syntax ? But does not support eg (?i) syntax. data Regexp- = Regexp { reString :: String, reCompiled :: Regex }- | RegexpCI { reString :: String, reCompiled :: Regex }+ = Regexp { reString :: Text, reCompiled :: Regex }+ | RegexpCI { reString :: Text, reCompiled :: Regex } instance Eq Regexp where Regexp s1 _ == Regexp s2 _ = s1 == s2@@ -93,7 +100,7 @@ RegexpCI _ _ `compare` Regexp _ _ = GT instance Show Regexp where- showsPrec d r = showParen (d > app_prec) $ reCons . showsPrec (app_prec+1) (reString r)+ showsPrec d r = showParen (d > app_prec) $ reCons . showsPrec (app_prec+1) (T.unpack $ reString r) where app_prec = 10 reCons = case r of Regexp _ _ -> showString "Regexp " RegexpCI _ _ -> showString "RegexpCI "@@ -108,8 +115,8 @@ where app_prec = 10 instance ToJSON Regexp where- toJSON (Regexp s _) = String . T.pack $ "Regexp " ++ s- toJSON (RegexpCI s _) = String . T.pack $ "RegexpCI " ++ s+ toJSON (Regexp s _) = String $ "Regexp " <> s+ toJSON (RegexpCI s _) = String $ "RegexpCI " <> s instance RegexLike Regexp String where matchOnce = matchOnce . reCompiled@@ -124,24 +131,24 @@ matchM = matchM . reCompiled -- Convert a Regexp string to a compiled Regex, or return an error message.-toRegex :: String -> Either RegexError Regexp-toRegex = memo $ \s -> mkRegexErr s (Regexp s <$> makeRegexM s)+toRegex :: Text -> Either RegexError Regexp+toRegex = memo $ \s -> mkRegexErr s (Regexp s <$> makeRegexM (T.unpack s)) -- Have to unpack here because Text instance in regex-tdfa only appears in 1.3.1 -- Like toRegex, but make a case-insensitive Regex.-toRegexCI :: String -> Either RegexError Regexp-toRegexCI = memo $ \s -> mkRegexErr s (RegexpCI s <$> makeRegexOptsM defaultCompOpt{caseSensitive=False} defaultExecOpt s)+toRegexCI :: Text -> Either RegexError Regexp+toRegexCI = memo $ \s -> mkRegexErr s (RegexpCI s <$> makeRegexOptsM defaultCompOpt{caseSensitive=False} defaultExecOpt (T.unpack s)) -- Have to unpack here because Text instance in regex-tdfa only appears in 1.3.1 -- | Make a nice error message for a regexp error.-mkRegexErr :: String -> Maybe a -> Either RegexError a+mkRegexErr :: Text -> Maybe a -> Either RegexError a mkRegexErr s = maybe (Left errmsg) Right- where errmsg = "this regular expression could not be compiled: " ++ s+ where errmsg = T.unpack $ "this regular expression could not be compiled: " <> s -- Convert a Regexp string to a compiled Regex, throw an error-toRegex' :: String -> Regexp+toRegex' :: Text -> Regexp toRegex' = either error' id . toRegex -- Like toRegex', but make a case-insensitive Regex.-toRegexCI' :: String -> Regexp+toRegexCI' :: Text -> Regexp toRegexCI' = either error' id . toRegexCI -- | A replacement pattern. May include numeric backreferences (\N).@@ -158,6 +165,13 @@ -- naming. regexMatch :: Regexp -> String -> Bool regexMatch = matchTest++-- | 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.+regexMatchText :: Regexp -> Text -> Bool+regexMatchText r = matchTest r . T.unpack -------------------------------------------------------------------------------- -- new total functions
Hledger/Utils/String.hs view
@@ -21,6 +21,7 @@ lstrip, rstrip, chomp,+ chomp1, singleline, elideLeft, elideRight,@@ -52,6 +53,8 @@ import Data.Char (isSpace, toLower, toUpper) import Data.Default (def) import Data.List (intercalate)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL import Text.Megaparsec ((<|>), between, many, noneOf, sepBy) import Text.Megaparsec.Char (char) import Text.Printf (printf)@@ -59,8 +62,8 @@ import Hledger.Utils.Parse import Hledger.Utils.Regex (toRegex', regexReplace) import Text.Tabular (Header(..), Properties(..))-import Text.Tabular.AsciiWide (Align(..), Cell(..), TableOpts(..), renderRow)-import Text.WideString (strWidth, charWidth)+import Text.Tabular.AsciiWide (Align(..), TableOpts(..), textCell, renderRow)+import Text.WideString (charWidth, strWidth) -- | Take elements from the end of a list.@@ -86,10 +89,14 @@ rstrip :: String -> String rstrip = reverse . lstrip . reverse --- | Remove trailing newlines/carriage returns.+-- | Remove all trailing newlines/carriage returns. chomp :: String -> String chomp = reverse . dropWhile (`elem` "\r\n") . reverse +-- | Remove all trailing newline/carriage returns, leaving just one trailing newline.+chomp1 :: String -> String+chomp1 = (++"\n") . chomp+ -- | Remove consecutive line breaks, replacing them with single space singleline :: String -> String singleline = unwords . filter (/="") . (map strip) . lines@@ -177,17 +184,14 @@ -- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded. -- Treats wide characters as double width. concatTopPadded :: [String] -> String-concatTopPadded = renderRow def{tableBorders=False, borderSpaces=False}- . Group NoLine . map (Header . cell)- where cell = Cell BottomLeft . map (\x -> (x, strWidth x)) . lines+concatTopPadded = TL.unpack . renderRow def{tableBorders=False, borderSpaces=False}+ . Group NoLine . map (Header . textCell BottomLeft . T.pack) -- | Join several multi-line strings as side-by-side rectangular strings of the same height, bottom-padded. -- Treats wide characters as double width. concatBottomPadded :: [String] -> String-concatBottomPadded = renderRow def{tableBorders=False, borderSpaces=False}- . Group NoLine . map (Header . cell)- where cell = Cell TopLeft . map (\x -> (x, strWidth x)) . lines-+concatBottomPadded = TL.unpack . renderRow def{tableBorders=False, borderSpaces=False}+ . Group NoLine . map (Header . textCell TopLeft . T.pack) -- | Join multi-line strings horizontally, after compressing each of -- them to a single line with a comma and space between each original line.@@ -342,4 +346,4 @@ stripAnsi s = either err id $ regexReplace ansire "" s where err = error "stripAnsi: invalid replacement pattern" -- PARTIAL, shouldn't happen- ansire = toRegex' "\ESC\\[([0-9]+;)*([0-9]+)?[ABCDHJKfmsu]" -- PARTIAL, should succeed+ ansire = toRegex' $ T.pack "\ESC\\[([0-9]+;)*([0-9]+)?[ABCDHJKfmsu]" -- PARTIAL, should succeed
Hledger/Utils/Text.hs view
@@ -12,6 +12,8 @@ -- underline, -- stripbrackets, textUnbracket,+ wrap,+ textChomp, -- -- quoting quoteIfSpaced, textQuoteIfNeeded,@@ -29,10 +31,10 @@ -- -- * single-line layout -- elideLeft, textElideRight,- -- formatString,+ formatText, -- -- * multi-line layout textConcatTopPadded,- -- concatBottomPadded,+ textConcatBottomPadded, -- concatOneLine, -- vConcatLeftAligned, -- vConcatRightAligned,@@ -43,7 +45,13 @@ -- cliptopleft, -- fitto, fitText,+ linesPrepend,+ linesPrepend2,+ unlinesB, -- -- * wide-character-aware layout+ WideBuilder(..),+ wbToText,+ wbUnpack, textWidth, textTakeWidth, -- fitString,@@ -58,20 +66,21 @@ where import Data.Char (digitToInt)-import Data.List+import Data.Default (def) #if !(MIN_VERSION_base(4,11,0))-import Data.Monoid+import Data.Semigroup ((<>)) #endif import Data.Text (Text) import qualified Data.Text as T--- import Text.Parsec--- import Text.Printf (printf)+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TB --- import Hledger.Utils.Parse--- import Hledger.Utils.Regex-import Hledger.Utils.Test-import Text.WideString (charWidth, textWidth)+import Hledger.Utils.Test ((@?=), test, tests)+import Text.Tabular (Header(..), Properties(..))+import Text.Tabular.AsciiWide (Align(..), TableOpts(..), textCell, renderRow)+import Text.WideString (WideBuilder(..), wbToText, wbUnpack, charWidth, textWidth) + -- lowercase, uppercase :: String -> String -- lowercase = map toLower -- uppercase = map toUpper@@ -87,16 +96,24 @@ textElideRight width t = if T.length t > width then T.take (width - 2) t <> ".." else t --- -- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it.--- -- Works on multi-line strings too (but will rewrite non-unix line endings).--- formatString :: Bool -> Maybe Int -> Maybe Int -> String -> String--- formatString leftJustified minwidth maxwidth s = intercalate "\n" $ map (printf fmt) $ lines s--- where--- justify = if leftJustified then "-" else ""--- minwidth' = maybe "" show minwidth--- maxwidth' = maybe "" (("."++).show) maxwidth--- fmt = "%" ++ justify ++ minwidth' ++ maxwidth' ++ "s"+-- | Wrap a Text with the surrounding Text.+wrap :: Text -> Text -> Text -> Text+wrap start end x = start <> x <> end +-- | Remove trailing newlines/carriage returns.+textChomp :: Text -> Text+textChomp = T.dropWhileEnd (`elem` ['\r', '\n'])++-- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it.+-- Works on multi-line strings too (but will rewrite non-unix line endings).+formatText :: Bool -> Maybe Int -> Maybe Int -> Text -> Text+formatText leftJustified minwidth maxwidth t =+ T.intercalate "\n" . map (pad . clip) $ if T.null t then [""] else T.lines t+ where+ pad = maybe id justify minwidth+ clip = maybe id T.take maxwidth+ justify n = if leftJustified then T.justifyLeft n ' ' else T.justifyRight n ' '+ -- underline :: String -> String -- underline s = s' ++ replicate (length s) '-' ++ "\n" -- where s'@@ -108,7 +125,7 @@ -- double-quoted. quoteIfSpaced :: T.Text -> T.Text quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s- | not $ any (`elem` (T.unpack s)) whitespacechars = s+ | not $ any (\c -> T.any (==c) s) whitespacechars = s | otherwise = textQuoteIfNeeded s -- -- | Wrap a string in double quotes, and \-prefix any embedded single@@ -122,7 +139,7 @@ -- -- | Double-quote this string if it contains whitespace, single quotes -- -- or double-quotes, escaping the quotes as needed. textQuoteIfNeeded :: T.Text -> T.Text-textQuoteIfNeeded s | any (`elem` T.unpack s) (quotechars++whitespacechars) = "\"" <> escapeDoubleQuotes s <> "\""+textQuoteIfNeeded s | any (\c -> T.any (==c) s) (quotechars++whitespacechars) = "\"" <> escapeDoubleQuotes s <> "\"" | otherwise = s -- -- | Single-quote this string if it contains whitespace or double-quotes.@@ -181,28 +198,14 @@ -- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded. -- Treats wide characters as double width. textConcatTopPadded :: [Text] -> Text-textConcatTopPadded ts = T.intercalate "\n" $ map T.concat $ transpose padded- where- lss = map T.lines ts :: [[Text]]- h = maximum $ map length lss- ypad ls = replicate (difforzero h (length ls)) "" ++ ls- xpad ls = map (textPadLeftWide w) ls- where w | null ls = 0- | otherwise = maximum $ map textWidth ls- padded = map (xpad . ypad) lss :: [[Text]]---- -- | Join several multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.--- -- Treats wide characters as double width.--- concatBottomPadded :: [String] -> String--- concatBottomPadded strs = intercalate "\n" $ map concat $ transpose padded--- where--- lss = map lines strs--- h = maximum $ map length lss--- ypad ls = ls ++ replicate (difforzero h (length ls)) ""--- xpad ls = map (padRightWide w) ls where w | null ls = 0--- | otherwise = maximum $ map strWidth ls--- padded = map (xpad . ypad) lss+textConcatTopPadded = TL.toStrict . renderRow def{tableBorders=False, borderSpaces=False}+ . Group NoLine . map (Header . textCell BottomLeft) +-- | Join several multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.+-- Treats wide characters as double width.+textConcatBottomPadded :: [Text] -> Text+textConcatBottomPadded = TL.toStrict . renderRow def{tableBorders=False, borderSpaces=False}+ . Group NoLine . map (Header . textCell TopLeft) -- -- | Join multi-line strings horizontally, after compressing each of -- -- them to a single line with a comma and space between each original line.@@ -245,9 +248,6 @@ -- ypadded = ls ++ replicate (difforzero h sh) "" -- xpadded = map (padleft sw) ypadded -difforzero :: (Num a, Ord a) => a -> a -> a-difforzero a b = maximum [(a - b), 0]- -- -- | Convert a multi-line string to a rectangular string left-padded to the specified width. -- -- Treats wide characters as double width. -- padleft :: Int -> String -> String@@ -344,11 +344,25 @@ = T.cons c $ textTakeWidth (w-cw) (T.tail t) | otherwise = "" +-- | Add a prefix to each line of a string.+linesPrepend :: Text -> Text -> Text+linesPrepend prefix = T.unlines . map (prefix<>) . T.lines +-- | Add a prefix to the first line of a string, +-- and a different prefix to the remaining lines.+linesPrepend2 :: Text -> Text -> Text -> Text+linesPrepend2 prefix1 prefix2 s = T.unlines $ case T.lines s of+ [] -> []+ l:ls -> (prefix1<>l) : map (prefix2<>) ls++-- | Join a list of Text Builders with a newline after each item.+unlinesB :: [TB.Builder] -> TB.Builder+unlinesB = foldMap (<> TB.singleton '\n')+ -- | Read a decimal number from a Text. Assumes the input consists only of digit -- characters. readDecimal :: Text -> Integer-readDecimal = foldl' step 0 . T.unpack+readDecimal = T.foldl' step 0 where step a c = a * 10 + toInteger (digitToInt c)
Text/Tabular/AsciiWide.hs view
@@ -1,14 +1,25 @@ -- | Text.Tabular.AsciiArt from tabular-0.2.2.7, modified to treat -- wide characters as double width. +{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+ module Text.Tabular.AsciiWide where import Data.Maybe (fromMaybe) import Data.Default (Default(..)) import Data.List (intersperse, transpose)+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup ((<>))+#endif+import Data.Semigroup (stimesMonoid)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Text.Lazy.Builder (Builder, fromString, fromText, singleton, toLazyText) import Safe (maximumMay) import Text.Tabular-import Text.WideString (strWidth)+import Text.WideString (WideBuilder(..), textWidth) -- | The options to use for rendering a table.@@ -25,8 +36,7 @@ } -- | Cell contents along an alignment-data Cell = Cell Align [(String, Int)]- deriving (Show)+data Cell = Cell Align [WideBuilder] -- | How to align text in a cell data Align = TopRight | BottomRight | BottomLeft | TopLeft@@ -36,31 +46,40 @@ emptyCell = Cell TopRight [] -- | Create a single-line cell from the given contents with its natural width.-alignCell :: Align -> String -> Cell-alignCell a x = Cell a [(x, strWidth x)]+textCell :: Align -> Text -> Cell+textCell a x = Cell a . map (\x -> WideBuilder (fromText x) (textWidth x)) $ if T.null x then [""] else T.lines x -- | Return the width of a Cell. cellWidth :: Cell -> Int-cellWidth (Cell _ xs) = fromMaybe 0 . maximumMay $ map snd xs+cellWidth (Cell _ xs) = fromMaybe 0 . maximumMay $ map wbWidth xs -- | Render a table according to common options, for backwards compatibility-render :: Bool -> (rh -> String) -> (ch -> String) -> (a -> String) -> Table rh ch a -> String+render :: Bool -> (rh -> Text) -> (ch -> Text) -> (a -> Text) -> Table rh ch a -> TL.Text render pretty fr fc f = renderTable def{prettyTable=pretty} (cell . fr) (cell . fc) (cell . f)- where cell = alignCell TopRight+ where cell = textCell TopRight --- | Render a table according to various cell specifications-renderTable :: TableOpts -- ^ Options controlling Table rendering+-- | Render a table according to various cell specifications>+renderTable :: TableOpts -- ^ Options controlling Table rendering -> (rh -> Cell) -- ^ Rendering function for row headers -> (ch -> Cell) -- ^ Rendering function for column headers -> (a -> Cell) -- ^ Function determining the string and width of a cell -> Table rh ch a- -> String-renderTable topts@TableOpts{prettyTable=pretty, tableBorders=borders} fr fc f (Table rh ch cells) =- unlines . addBorders $- renderColumns topts sizes ch2- : bar VM DoubleLine -- +======================================+- : renderRs (fmap renderR $ zipHeader [] cellContents rowHeaders)+ -> TL.Text+renderTable topts fr fc f = toLazyText . renderTableB topts fr fc f++-- | A version of renderTable which returns the underlying Builder.+renderTableB :: TableOpts -- ^ Options controlling Table rendering+ -> (rh -> Cell) -- ^ Rendering function for row headers+ -> (ch -> Cell) -- ^ Rendering function for column headers+ -> (a -> Cell) -- ^ Function determining the string and width of a cell+ -> Table rh ch a+ -> Builder+renderTableB topts@TableOpts{prettyTable=pretty, tableBorders=borders} fr fc f (Table rh ch cells) =+ unlinesB . addBorders $+ renderColumns topts sizes ch2+ : bar VM DoubleLine -- +======================================++ : renderRs (fmap renderR $ zipHeader [] cellContents rowHeaders) where renderR (cs,h) = renderColumns topts sizes $ Group DoubleLine [ Header h@@ -83,63 +102,68 @@ -- borders and bars addBorders xs = if borders then bar VT SingleLine : xs ++ [bar VB SingleLine] else xs- bar vpos prop = concat $ renderHLine vpos borders pretty sizes ch2 prop+ bar vpos prop = mconcat $ renderHLine vpos borders pretty sizes ch2 prop+ unlinesB = foldMap (<> singleton '\n') -- | Render a single row according to cell specifications.-renderRow :: TableOpts -> Header Cell -> String-renderRow topts h = renderColumns topts is h- where is = map (\(Cell _ xs) -> fromMaybe 0 . maximumMay $ map snd xs) $ headerContents h+renderRow :: TableOpts -> Header Cell -> TL.Text+renderRow topts = toLazyText . renderRowB topts +-- | A version of renderRow which returns the underlying Builder.+renderRowB:: TableOpts -> Header Cell -> Builder+renderRowB topts h = renderColumns topts is h+ where is = map cellWidth $ headerContents h + verticalBar :: Bool -> Char verticalBar pretty = if pretty then '│' else '|' -leftBar :: Bool -> Bool -> String-leftBar pretty True = verticalBar pretty : " "-leftBar pretty False = [verticalBar pretty]+leftBar :: Bool -> Bool -> Builder+leftBar pretty True = fromString $ verticalBar pretty : " "+leftBar pretty False = singleton $ verticalBar pretty -rightBar :: Bool -> Bool -> String-rightBar pretty True = ' ' : [verticalBar pretty]-rightBar pretty False = [verticalBar pretty]+rightBar :: Bool -> Bool -> Builder+rightBar pretty True = fromString $ ' ' : [verticalBar pretty]+rightBar pretty False = singleton $ verticalBar pretty -midBar :: Bool -> Bool -> String-midBar pretty True = ' ' : verticalBar pretty : " "-midBar pretty False = [verticalBar pretty]+midBar :: Bool -> Bool -> Builder+midBar pretty True = fromString $ ' ' : verticalBar pretty : " "+midBar pretty False = singleton $ verticalBar pretty -doubleMidBar :: Bool -> Bool -> String-doubleMidBar pretty True = if pretty then " ║ " else " || "-doubleMidBar pretty False = if pretty then "║" else "||"+doubleMidBar :: Bool -> Bool -> Builder+doubleMidBar pretty True = fromText $ if pretty then " ║ " else " || "+doubleMidBar pretty False = fromText $ if pretty then "║" else "||" -- | We stop rendering on the shortest list! renderColumns :: TableOpts -- ^ rendering options for the table -> [Int] -- ^ max width for each column -> Header Cell- -> String+ -> Builder renderColumns TableOpts{prettyTable=pretty, tableBorders=borders, borderSpaces=spaces} is h =- concat . intersperse "\n" -- Put each line on its own line- . map (addBorders . concat) . transpose -- Change to a list of lines and add borders+ mconcat . intersperse "\n" -- Put each line on its own line+ . map (addBorders . mconcat) . transpose -- Change to a list of lines and add borders . map (either hsep padCell) . flattenHeader -- We now have a matrix of strings . zipHeader 0 is $ padRow <$> h -- Pad cell height and add width marker where -- Pad each cell to have the appropriate width- padCell (w, Cell TopLeft ls) = map (\(x,xw) -> x ++ replicate (w - xw) ' ') ls- padCell (w, Cell BottomLeft ls) = map (\(x,xw) -> x ++ replicate (w - xw) ' ') ls- padCell (w, Cell TopRight ls) = map (\(x,xw) -> replicate (w - xw) ' ' ++ x) ls- padCell (w, Cell BottomRight ls) = map (\(x,xw) -> replicate (w - xw) ' ' ++ x) ls+ padCell (w, Cell TopLeft ls) = map (\x -> wbBuilder x <> fromText (T.replicate (w - wbWidth x) " ")) ls+ padCell (w, Cell BottomLeft ls) = map (\x -> wbBuilder x <> fromText (T.replicate (w - wbWidth x) " ")) ls+ padCell (w, Cell TopRight ls) = map (\x -> fromText (T.replicate (w - wbWidth x) " ") <> wbBuilder x) ls+ padCell (w, Cell BottomRight ls) = map (\x -> fromText (T.replicate (w - wbWidth x) " ") <> wbBuilder x) ls -- Pad each cell to have the same number of lines- padRow (Cell TopLeft ls) = Cell TopLeft $ ls ++ replicate (nLines - length ls) ("",0)- padRow (Cell TopRight ls) = Cell TopRight $ ls ++ replicate (nLines - length ls) ("",0)- padRow (Cell BottomLeft ls) = Cell BottomLeft $ replicate (nLines - length ls) ("",0) ++ ls- padRow (Cell BottomRight ls) = Cell BottomRight $ replicate (nLines - length ls) ("",0) ++ ls+ padRow (Cell TopLeft ls) = Cell TopLeft $ ls ++ replicate (nLines - length ls) mempty+ padRow (Cell TopRight ls) = Cell TopRight $ ls ++ replicate (nLines - length ls) mempty+ padRow (Cell BottomLeft ls) = Cell BottomLeft $ replicate (nLines - length ls) mempty ++ ls+ padRow (Cell BottomRight ls) = Cell BottomRight $ replicate (nLines - length ls) mempty ++ ls - hsep :: Properties -> [String]+ hsep :: Properties -> [Builder] hsep NoLine = replicate nLines $ if spaces then " " else "" hsep SingleLine = replicate nLines $ midBar pretty spaces hsep DoubleLine = replicate nLines $ doubleMidBar pretty spaces - addBorders xs | borders = leftBar pretty spaces ++ xs ++ rightBar pretty spaces- | spaces = ' ' : xs ++ " "+ addBorders xs | borders = leftBar pretty spaces <> xs <> rightBar pretty spaces+ | spaces = fromText " " <> xs <> fromText " " | otherwise = xs nLines = fromMaybe 0 . maximumMay . map (\(Cell _ ls) -> length ls) $ headerContents h@@ -150,52 +174,48 @@ -> [Int] -- ^ width specifications -> Header a -> Properties- -> [String]+ -> [Builder] renderHLine _ _ _ _ _ NoLine = [] renderHLine vpos borders pretty w h prop = [renderHLine' vpos borders pretty prop w h] -renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> String-renderHLine' vpos borders pretty prop is h = addBorders $ sep ++ coreLine ++ sep+renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> Builder+renderHLine' vpos borders pretty prop is h = addBorders $ sep <> coreLine <> sep where- addBorders xs = if borders then edge HL ++ xs ++ edge HR else xs+ addBorders xs = if borders then edge HL <> xs <> edge HR else xs edge hpos = boxchar vpos hpos SingleLine prop pretty- coreLine = concatMap helper $ flattenHeader $ zipHeader 0 is h+ coreLine = foldMap helper $ flattenHeader $ zipHeader 0 is h helper = either vsep dashes- dashes (i,_) = concat (replicate i sep)+ dashes (i,_) = stimesMonoid i sep sep = boxchar vpos HM NoLine prop pretty vsep v = case v of- NoLine -> sep ++ sep- _ -> sep ++ cross v prop ++ sep+ NoLine -> sep <> sep+ _ -> sep <> cross v prop <> sep cross v h = boxchar vpos HM v h pretty data VPos = VT | VM | VB -- top middle bottom data HPos = HL | HM | HR -- left middle right -boxchar :: VPos -> HPos -> Properties -> Properties -> Bool -> String+boxchar :: VPos -> HPos -> Properties -> Properties -> Bool -> Builder boxchar vpos hpos vert horiz = lineart u d l r where- u =- case vpos of- VT -> NoLine- _ -> vert- d =- case vpos of- VB -> NoLine- _ -> vert- l =- case hpos of- HL -> NoLine- _ -> horiz- r =- case hpos of- HR -> NoLine- _ -> horiz+ u = case vpos of+ VT -> NoLine+ _ -> vert+ d = case vpos of+ VB -> NoLine+ _ -> vert+ l = case hpos of+ HL -> NoLine+ _ -> horiz+ r = case hpos of+ HR -> NoLine+ _ -> horiz -pick :: String -> String -> Bool -> String-pick x _ True = x-pick _ x False = x+pick :: Text -> Text -> Bool -> Builder+pick x _ True = fromText x+pick _ x False = fromText x -lineart :: Properties -> Properties -> Properties -> Properties -> Bool -> String+lineart :: Properties -> Properties -> Properties -> Properties -> Bool -> Builder -- up down left right lineart SingleLine SingleLine SingleLine SingleLine = pick "┼" "+" lineart SingleLine SingleLine SingleLine NoLine = pick "┤" "+"@@ -244,6 +264,4 @@ lineart SingleLine SingleLine DoubleLine DoubleLine = pick "╪" "+" lineart DoubleLine DoubleLine SingleLine SingleLine = pick "╫" "++" -lineart _ _ _ _ = const ""---- +lineart _ _ _ _ = const mempty
Text/WideString.hs view
@@ -1,14 +1,49 @@ -- | Calculate the width of String and Text, being aware of wide characters. +{-# LANGUAGE CPP #-}+ module Text.WideString ( -- * wide-character-aware layout strWidth, textWidth,- charWidth+ charWidth,+ -- * Text Builders which keep track of length+ WideBuilder(..),+ wbUnpack,+ wbToText ) where +#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup (Semigroup(..))+#endif 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+++-- | Helper for constructing Builders while keeping track of text width.+data WideBuilder = WideBuilder+ { wbBuilder :: !TB.Builder+ , wbWidth :: !Int+ }++instance Semigroup WideBuilder where+ WideBuilder x i <> WideBuilder y j = WideBuilder (x <> y) (i + j)++instance Monoid WideBuilder where+ mempty = WideBuilder mempty 0+#if !(MIN_VERSION_base(4,11,0))+ mappend = (<>)+#endif++-- | Convert a WideBuilder to a strict Text.+wbToText :: WideBuilder -> Text+wbToText = TL.toStrict . TB.toLazyText . wbBuilder++-- | Convert a WideBuilder to a String.+wbUnpack :: WideBuilder -> String+wbUnpack = TL.unpack . TB.toLazyText . wbBuilder -- | Calculate the render width of a string, considering
hledger-lib.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 3f8656e682d0ff102bad0022b06b881b2de2ca72cf342fd31090a33f7d87b692+-- hash: 2724f18a071add9d644b3060aba58a3f4f6c71b66d88af1e8ca3f526043d461f name: hledger-lib-version: 1.20.4+version: 1.21 synopsis: A reusable library providing the core functionality of hledger description: A reusable library containing hledger's core functionality. This is used by most hledger* packages so that they support the same@@ -34,18 +34,6 @@ README.md test/unittest.hs test/doctests.hs- hledger_csv.5- hledger_csv.txt- hledger_csv.info- hledger_journal.5- hledger_journal.txt- hledger_journal.info- hledger_timedot.5- hledger_timedot.txt- hledger_timedot.info- hledger_timeclock.5- hledger_timeclock.txt- hledger_timeclock.info source-repository head type: git@@ -137,7 +125,6 @@ , pretty-simple >4 && <5 , regex-tdfa , safe >=0.2- , split >=0.1 , tabular >=0.2 , tasty >=1.2.3 , tasty-hunit >=0.10.0.2@@ -188,7 +175,6 @@ , pretty-simple >4 && <5 , regex-tdfa , safe >=0.2- , split >=0.1 , tabular >=0.2 , tasty >=1.2.3 , tasty-hunit >=0.10.0.2@@ -241,7 +227,6 @@ , pretty-simple >4 && <5 , regex-tdfa , safe >=0.2- , split >=0.1 , tabular >=0.2 , tasty >=1.2.3 , tasty-hunit >=0.10.0.2
− hledger_csv.5
@@ -1,1300 +0,0 @@-.\"t--.TH "HLEDGER_CSV" "5" "December 2020" "hledger-lib-1.20.4 " "hledger User Manuals"----.SH NAME-.PP-How hledger reads CSV data, and the CSV rules file format.-.SH DESCRIPTION-.PP-hledger can read CSV files (Character Separated Value - usually comma,-semicolon, or tab) containing dated records as if they were journal-files, automatically converting each CSV record into a transaction.-.PP-(To learn about \f[I]writing\f[R] CSV, see CSV output.)-.PP-We describe each CSV file\[aq]s format with a corresponding \f[I]rules-file\f[R].-By default this is named like the CSV file with a \f[C].rules\f[R]-extension added.-Eg when reading \f[C]FILE.csv\f[R], hledger also looks for-\f[C]FILE.csv.rules\f[R] in the same directory as \f[C]FILE.csv\f[R].-You can specify a different rules file with the \f[C]--rules-file\f[R]-option.-If a rules file is not found, hledger will create a sample rules file,-which you\[aq]ll need to adjust.-.PP-This file contains rules describing the CSV data (header line, fields-layout, date format etc.), and how to construct hledger journal entries-(transactions) from it.-Often there will also be a list of conditional rules for categorising-transactions based on their descriptions.-Here\[aq]s an overview of the CSV rules; these are described more fully-below, after the examples:-.PP-.TS-tab(@);-lw(30.1n) lw(39.9n).-T{-\f[B]\f[CB]skip\f[B]\f[R]-T}@T{-skip one or more header lines or matched CSV records-T}-T{-\f[B]\f[CB]fields\f[B]\f[R]-T}@T{-name CSV fields, assign them to hledger fields-T}-T{-\f[B]field assignment\f[R]-T}@T{-assign a value to one hledger field, with interpolation-T}-T{-\f[B]\f[CB]separator\f[B]\f[R]-T}@T{-a custom field separator-T}-T{-\f[B]\f[CB]if\f[B] block\f[R]-T}@T{-apply some rules to CSV records matched by patterns-T}-T{-\f[B]\f[CB]if\f[B] table\f[R]-T}@T{-apply some rules to CSV records matched by patterns, alternate syntax-T}-T{-\f[B]\f[CB]end\f[B]\f[R]-T}@T{-skip the remaining CSV records-T}-T{-\f[B]\f[CB]date-format\f[B]\f[R]-T}@T{-how to parse dates in CSV records-T}-T{-\f[B]\f[CB]decimal-mark\f[B]\f[R]-T}@T{-the decimal mark used in CSV amounts, if ambiguous-T}-T{-\f[B]\f[CB]newest-first\f[B]\f[R]-T}@T{-disambiguate record order when there\[aq]s only one date-T}-T{-\f[B]\f[CB]include\f[B]\f[R]-T}@T{-inline another CSV rules file-T}-T{-\f[B]\f[CB]balance-type\f[B]\f[R]-T}@T{-choose which type of balance assignments to use-T}-.TE-.PP-Note, for best error messages when reading CSV files, use a-\f[C].csv\f[R], \f[C].tsv\f[R] or \f[C].ssv\f[R] file extension or file-prefix - see File Extension below.-.PP-There\[aq]s an introductory Convert CSV files tutorial on hledger.org.-.SH EXAMPLES-.PP-Here are some sample hledger CSV rules files.-See also the full collection at:-.PD 0-.P-.PD-https://github.com/simonmichael/hledger/tree/master/examples/csv-.SS Basic-.PP-At minimum, the rules file must identify the date and amount fields, and-often it also specifies the date format and how many header lines there-are.-Here\[aq]s a simple CSV file and a rules file for it:-.IP-.nf-\f[C]-Date, Description, Id, Amount-12/11/2019, Foo, 123, 10.23-\f[R]-.fi-.IP-.nf-\f[C]-# basic.csv.rules-skip 1-fields date, description, _, amount-date-format %d/%m/%Y-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger print -f basic.csv-2019-11-12 Foo- expenses:unknown 10.23- income:unknown -10.23-\f[R]-.fi-.PP-Default account names are chosen, since we didn\[aq]t set them.-.SS Bank of Ireland-.PP-Here\[aq]s a CSV with two amount fields (Debit and Credit), and a-balance field, which we can use to add balance assertions, which is not-necessary but provides extra error checking:-.IP-.nf-\f[C]-Date,Details,Debit,Credit,Balance-07/12/2012,LODGMENT 529898,,10.0,131.21-07/12/2012,PAYMENT,5,,126-\f[R]-.fi-.IP-.nf-\f[C]-# bankofireland-checking.csv.rules--# skip the header line-skip--# name the csv fields, and assign some of them as journal entry fields-fields date, description, amount-out, amount-in, balance--# We generate balance assertions by assigning to \[dq]balance\[dq]-# above, but you may sometimes need to remove these because:-#-# - the CSV balance differs from the true balance,-# by up to 0.0000000000005 in my experience-#-# - it is sometimes calculated based on non-chronological ordering,-# eg when multiple transactions clear on the same day--# date is in UK/Ireland format-date-format %d/%m/%Y--# set the currency-currency EUR--# set the base account for all txns-account1 assets:bank:boi:checking-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f bankofireland-checking.csv print-2012-12-07 LODGMENT 529898- assets:bank:boi:checking EUR10.0 = EUR131.2- income:unknown EUR-10.0--2012-12-07 PAYMENT- assets:bank:boi:checking EUR-5.0 = EUR126.0- expenses:unknown EUR5.0-\f[R]-.fi-.PP-The balance assertions don\[aq]t raise an error above, because we\[aq]re-reading directly from CSV, but they will be checked if these entries are-imported into a journal file.-.SS Amazon-.PP-Here we convert amazon.com order history, and use an if block to-generate a third posting if there\[aq]s a fee.-(In practice you\[aq]d probably get this data from your bank instead,-but it\[aq]s an example.)-.IP-.nf-\f[C]-\[dq]Date\[dq],\[dq]Type\[dq],\[dq]To/From\[dq],\[dq]Name\[dq],\[dq]Status\[dq],\[dq]Amount\[dq],\[dq]Fees\[dq],\[dq]Transaction ID\[dq]-\[dq]Jul 29, 2012\[dq],\[dq]Payment\[dq],\[dq]To\[dq],\[dq]Foo.\[dq],\[dq]Completed\[dq],\[dq]$20.00\[dq],\[dq]$0.00\[dq],\[dq]16000000000000DGLNJPI1P9B8DKPVHL\[dq]-\[dq]Jul 30, 2012\[dq],\[dq]Payment\[dq],\[dq]To\[dq],\[dq]Adapteva, Inc.\[dq],\[dq]Completed\[dq],\[dq]$25.00\[dq],\[dq]$1.00\[dq],\[dq]17LA58JSKRD4HDGLNJPI1P9B8DKPVHL\[dq]-\f[R]-.fi-.IP-.nf-\f[C]-# amazon-orders.csv.rules--# skip one header line-skip 1--# name the csv fields, and assign the transaction\[aq]s date, amount and code.-# Avoided the \[dq]status\[dq] and \[dq]amount\[dq] hledger field names to prevent confusion.-fields date, _, toorfrom, name, amzstatus, amzamount, fees, code--# how to parse the date-date-format %b %-d, %Y--# combine two fields to make the description-description %toorfrom %name--# save the status as a tag-comment status:%amzstatus--# set the base account for all transactions-account1 assets:amazon-# leave amount1 blank so it can balance the other(s).-# I\[aq]m assuming amzamount excludes the fees, don\[aq]t remember--# set a generic account2-account2 expenses:misc-amount2 %amzamount-# and maybe refine it further:-#include categorisation.rules--# add a third posting for fees, but only if they are non-zero.-if %fees [1-9]- account3 expenses:fees- amount3 %fees-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f amazon-orders.csv print-2012-07-29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo. ; status:Completed- assets:amazon- expenses:misc $20.00--2012-07-30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc. ; status:Completed- assets:amazon- expenses:misc $25.00- expenses:fees $1.00-\f[R]-.fi-.SS Paypal-.PP-Here\[aq]s a real-world rules file for (customised) Paypal CSV, with-some Paypal-specific rules, and a second rules file included:-.IP-.nf-\f[C]-\[dq]Date\[dq],\[dq]Time\[dq],\[dq]TimeZone\[dq],\[dq]Name\[dq],\[dq]Type\[dq],\[dq]Status\[dq],\[dq]Currency\[dq],\[dq]Gross\[dq],\[dq]Fee\[dq],\[dq]Net\[dq],\[dq]From Email Address\[dq],\[dq]To Email Address\[dq],\[dq]Transaction ID\[dq],\[dq]Item Title\[dq],\[dq]Item ID\[dq],\[dq]Reference Txn ID\[dq],\[dq]Receipt ID\[dq],\[dq]Balance\[dq],\[dq]Note\[dq]-\[dq]10/01/2019\[dq],\[dq]03:46:20\[dq],\[dq]PDT\[dq],\[dq]Calm Radio\[dq],\[dq]Subscription Payment\[dq],\[dq]Completed\[dq],\[dq]USD\[dq],\[dq]-6.99\[dq],\[dq]0.00\[dq],\[dq]-6.99\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]memberships\[at]calmradio.com\[dq],\[dq]60P57143A8206782E\[dq],\[dq]MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month\[dq],\[dq]\[dq],\[dq]I-R8YLY094FJYR\[dq],\[dq]\[dq],\[dq]-6.99\[dq],\[dq]\[dq]-\[dq]10/01/2019\[dq],\[dq]03:46:20\[dq],\[dq]PDT\[dq],\[dq]\[dq],\[dq]Bank Deposit to PP Account \[dq],\[dq]Pending\[dq],\[dq]USD\[dq],\[dq]6.99\[dq],\[dq]0.00\[dq],\[dq]6.99\[dq],\[dq]\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]0TU1544T080463733\[dq],\[dq]\[dq],\[dq]\[dq],\[dq]60P57143A8206782E\[dq],\[dq]\[dq],\[dq]0.00\[dq],\[dq]\[dq]-\[dq]10/01/2019\[dq],\[dq]08:57:01\[dq],\[dq]PDT\[dq],\[dq]Patreon\[dq],\[dq]PreApproved Payment Bill User Payment\[dq],\[dq]Completed\[dq],\[dq]USD\[dq],\[dq]-7.00\[dq],\[dq]0.00\[dq],\[dq]-7.00\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]support\[at]patreon.com\[dq],\[dq]2722394R5F586712G\[dq],\[dq]Patreon* Membership\[dq],\[dq]\[dq],\[dq]B-0PG93074E7M86381M\[dq],\[dq]\[dq],\[dq]-7.00\[dq],\[dq]\[dq]-\[dq]10/01/2019\[dq],\[dq]08:57:01\[dq],\[dq]PDT\[dq],\[dq]\[dq],\[dq]Bank Deposit to PP Account \[dq],\[dq]Pending\[dq],\[dq]USD\[dq],\[dq]7.00\[dq],\[dq]0.00\[dq],\[dq]7.00\[dq],\[dq]\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]71854087RG994194F\[dq],\[dq]Patreon* Membership\[dq],\[dq]\[dq],\[dq]2722394R5F586712G\[dq],\[dq]\[dq],\[dq]0.00\[dq],\[dq]\[dq]-\[dq]10/19/2019\[dq],\[dq]03:02:12\[dq],\[dq]PDT\[dq],\[dq]Wikimedia Foundation, Inc.\[dq],\[dq]Subscription Payment\[dq],\[dq]Completed\[dq],\[dq]USD\[dq],\[dq]-2.00\[dq],\[dq]0.00\[dq],\[dq]-2.00\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]tle\[at]wikimedia.org\[dq],\[dq]K9U43044RY432050M\[dq],\[dq]Monthly donation to the Wikimedia Foundation\[dq],\[dq]\[dq],\[dq]I-R5C3YUS3285L\[dq],\[dq]\[dq],\[dq]-2.00\[dq],\[dq]\[dq]-\[dq]10/19/2019\[dq],\[dq]03:02:12\[dq],\[dq]PDT\[dq],\[dq]\[dq],\[dq]Bank Deposit to PP Account \[dq],\[dq]Pending\[dq],\[dq]USD\[dq],\[dq]2.00\[dq],\[dq]0.00\[dq],\[dq]2.00\[dq],\[dq]\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]3XJ107139A851061F\[dq],\[dq]\[dq],\[dq]\[dq],\[dq]K9U43044RY432050M\[dq],\[dq]\[dq],\[dq]0.00\[dq],\[dq]\[dq]-\[dq]10/22/2019\[dq],\[dq]05:07:06\[dq],\[dq]PDT\[dq],\[dq]Noble Benefactor\[dq],\[dq]Subscription Payment\[dq],\[dq]Completed\[dq],\[dq]USD\[dq],\[dq]10.00\[dq],\[dq]-0.59\[dq],\[dq]9.41\[dq],\[dq]noble\[at]bene.fac.tor\[dq],\[dq]simon\[at]joyful.com\[dq],\[dq]6L8L1662YP1334033\[dq],\[dq]Joyful Systems\[dq],\[dq]\[dq],\[dq]I-KC9VBGY2GWDB\[dq],\[dq]\[dq],\[dq]9.41\[dq],\[dq]\[dq]-\f[R]-.fi-.IP-.nf-\f[C]-# paypal-custom.csv.rules--# Tips:-# Export from Activity -> Statements -> Custom -> Activity download-# Suggested transaction type: \[dq]Balance affecting\[dq]-# Paypal\[aq]s default fields in 2018 were:-# \[dq]Date\[dq],\[dq]Time\[dq],\[dq]TimeZone\[dq],\[dq]Name\[dq],\[dq]Type\[dq],\[dq]Status\[dq],\[dq]Currency\[dq],\[dq]Gross\[dq],\[dq]Fee\[dq],\[dq]Net\[dq],\[dq]From Email Address\[dq],\[dq]To Email Address\[dq],\[dq]Transaction ID\[dq],\[dq]Shipping Address\[dq],\[dq]Address Status\[dq],\[dq]Item Title\[dq],\[dq]Item ID\[dq],\[dq]Shipping and Handling Amount\[dq],\[dq]Insurance Amount\[dq],\[dq]Sales Tax\[dq],\[dq]Option 1 Name\[dq],\[dq]Option 1 Value\[dq],\[dq]Option 2 Name\[dq],\[dq]Option 2 Value\[dq],\[dq]Reference Txn ID\[dq],\[dq]Invoice Number\[dq],\[dq]Custom Number\[dq],\[dq]Quantity\[dq],\[dq]Receipt ID\[dq],\[dq]Balance\[dq],\[dq]Address Line 1\[dq],\[dq]Address Line 2/District/Neighborhood\[dq],\[dq]Town/City\[dq],\[dq]State/Province/Region/County/Territory/Prefecture/Republic\[dq],\[dq]Zip/Postal Code\[dq],\[dq]Country\[dq],\[dq]Contact Phone Number\[dq],\[dq]Subject\[dq],\[dq]Note\[dq],\[dq]Country Code\[dq],\[dq]Balance Impact\[dq]-# This rules file assumes the following more detailed fields, configured in \[dq]Customize report fields\[dq]:-# \[dq]Date\[dq],\[dq]Time\[dq],\[dq]TimeZone\[dq],\[dq]Name\[dq],\[dq]Type\[dq],\[dq]Status\[dq],\[dq]Currency\[dq],\[dq]Gross\[dq],\[dq]Fee\[dq],\[dq]Net\[dq],\[dq]From Email Address\[dq],\[dq]To Email Address\[dq],\[dq]Transaction ID\[dq],\[dq]Item Title\[dq],\[dq]Item ID\[dq],\[dq]Reference Txn ID\[dq],\[dq]Receipt ID\[dq],\[dq]Balance\[dq],\[dq]Note\[dq]--fields date, time, timezone, description_, type, status_, currency, grossamount, feeamount, netamount, fromemail, toemail, code, itemtitle, itemid, referencetxnid, receiptid, balance, note--skip 1--date-format %-m/%-d/%Y--# ignore some paypal events-if-In Progress-Temporary Hold-Update to- skip--# add more fields to the description-description %description_ %itemtitle--# save some other fields as tags-comment itemid:%itemid, fromemail:%fromemail, toemail:%toemail, time:%time, type:%type, status:%status_--# convert to short currency symbols-if %currency USD- currency $-if %currency EUR- currency E-if %currency GBP- currency P--# generate postings--# the first posting will be the money leaving/entering my paypal account-# (negative means leaving my account, in all amount fields)-account1 assets:online:paypal-amount1 %netamount--# the second posting will be money sent to/received from other party-# (account2 is set below)-amount2 -%grossamount--# if there\[aq]s a fee, add a third posting for the money taken by paypal.-if %feeamount [1-9]- account3 expenses:banking:paypal- amount3 -%feeamount- comment3 business:--# choose an account for the second posting--# override the default account names:-# if the amount is positive, it\[aq]s income (a debit)-if %grossamount \[ha][\[ha]-]- account2 income:unknown-# if negative, it\[aq]s an expense (a credit)-if %grossamount \[ha]-- account2 expenses:unknown--# apply common rules for setting account2 & other tweaks-include common.rules--# apply some overrides specific to this csv--# Transfers from/to bank. These are usually marked Pending,-# which can be disregarded in this case.-if-Bank Account-Bank Deposit to PP Account- description %type for %referencetxnid %itemtitle- account2 assets:bank:wf:pchecking- account1 assets:online:paypal--# Currency conversions-if Currency Conversion- account2 equity:currency conversion-\f[R]-.fi-.IP-.nf-\f[C]-# common.rules--if-darcs-noble benefactor- account2 revenues:foss donations:darcshub- comment2 business:--if-Calm Radio- account2 expenses:online:apps--if-electronic frontier foundation-Patreon-wikimedia-Advent of Code- account2 expenses:dues--if Google- account2 expenses:online:apps- description google | music-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f paypal-custom.csv print-2019-10-01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month ; itemid:, fromemail:simon\[at]joyful.com, toemail:memberships\[at]calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed- assets:online:paypal $-6.99 = $-6.99- expenses:online:apps $6.99--2019-10-01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $6.99 = $0.00- assets:bank:wf:pchecking $-6.99--2019-10-01 (2722394R5F586712G) Patreon Patreon* Membership ; itemid:, fromemail:simon\[at]joyful.com, toemail:support\[at]patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed- assets:online:paypal $-7.00 = $-7.00- expenses:dues $7.00--2019-10-01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $7.00 = $0.00- assets:bank:wf:pchecking $-7.00--2019-10-19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation ; itemid:, fromemail:simon\[at]joyful.com, toemail:tle\[at]wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed- assets:online:paypal $-2.00 = $-2.00- expenses:dues $2.00- expenses:banking:paypal ; business:--2019-10-19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $2.00 = $0.00- assets:bank:wf:pchecking $-2.00--2019-10-22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems ; itemid:, fromemail:noble\[at]bene.fac.tor, toemail:simon\[at]joyful.com, time:05:07:06, type:Subscription Payment, status:Completed- assets:online:paypal $9.41 = $9.41- revenues:foss donations:darcshub $-10.00 ; business:- expenses:banking:paypal $0.59 ; business:-\f[R]-.fi-.SH CSV RULES-.PP-The following kinds of rule can appear in the rules file, in any order.-Blank lines and lines beginning with \f[C]#\f[R] or \f[C];\f[R] are-ignored.-.SS \f[C]skip\f[R]-.IP-.nf-\f[C]-skip N-\f[R]-.fi-.PP-The word \[dq]skip\[dq] followed by a number (or no number, meaning 1)-tells hledger to ignore this many non-empty lines preceding the CSV-data.-(Empty/blank lines are skipped automatically.) You\[aq]ll need this-whenever your CSV data contains header lines.-.PP-It also has a second purpose: it can be used inside if blocks to ignore-certain CSV records (described below).-.SS \f[C]fields\f[R]-.IP-.nf-\f[C]-fields FIELDNAME1, FIELDNAME2, ...-\f[R]-.fi-.PP-A fields list (the word \[dq]fields\[dq] followed by comma-separated-field names) is the quick way to assign CSV field values to hledger-fields.-It does two things:-.IP "1." 3-it names the CSV fields.-This is optional, but can be convenient later for interpolating them.-.IP "2." 3-when you use a standard hledger field name, it assigns the CSV value to-that part of the hledger transaction.-.PP-Here\[aq]s an example that says \[dq]use the 1st, 2nd and 4th fields as-the transaction\[aq]s date, description and amount; name the last two-fields for later reference; and ignore the others\[dq]:-.IP-.nf-\f[C]-fields date, description, , amount, , , somefield, anotherfield-\f[R]-.fi-.PP-Field names may not contain whitespace.-Fields you don\[aq]t care about can be left unnamed.-Currently there must be least two items (there must be at least one-comma).-.PP-Note, always use comma in the fields list, even if your CSV uses another-separator character.-.PP-Here are the standard hledger field/pseudo-field names.-For more about the transaction parts they refer to, see the manual for-hledger\[aq]s journal format.-.SS Transaction field names-.PP-\f[C]date\f[R], \f[C]date2\f[R], \f[C]status\f[R], \f[C]code\f[R],-\f[C]description\f[R], \f[C]comment\f[R] can be used to form the-transaction\[aq]s first line.-.SS Posting field names-.SS account-.PP-\f[C]accountN\f[R], where N is 1 to 99, causes a posting to be-generated, with that account name.-.PP-Most often there are two postings, so you\[aq]ll want to set-\f[C]account1\f[R] and \f[C]account2\f[R].-Typically \f[C]account1\f[R] is associated with the CSV file, and is set-once with a top-level assignment, while \f[C]account2\f[R] is set based-on each transaction\[aq]s description, and in conditional blocks.-.PP-If a posting\[aq]s account name is left unset but its amount is set (see-below), a default account name will be chosen (like-\[dq]expenses:unknown\[dq] or \[dq]income:unknown\[dq]).-.SS amount-.PP-\f[C]amountN\f[R] sets posting N\[aq]s amount.-If the CSV uses separate fields for inflows and outflows, you can use-\f[C]amountN-in\f[R] and \f[C]amountN-out\f[R] instead.-By assigning to \f[C]amount1\f[R], \f[C]amount2\f[R], ...-etc.-you can generate anywhere from 0 to 99 postings.-.PP-There is also an older, unnumbered form of these names, suitable for-2-posting transactions, which sets both posting 1\[aq]s and (negated)-posting 2\[aq]s amount: \f[C]amount\f[R], or \f[C]amount-in\f[R] and-\f[C]amount-out\f[R].-This is still supported because it keeps pre-hledger-1.17 csv rules-files working, and because it can be more succinct, and because it-converts posting 2\[aq]s amount to cost if there\[aq]s a transaction-price, which can be useful.-.PP-If you have an existing rules file using the unnumbered form, you might-want to use the numbered form in certain conditional blocks, without-having to update and retest all the old rules.-To facilitate this, posting 1 ignores-\f[C]amount\f[R]/\f[C]amount-in\f[R]/\f[C]amount-out\f[R] if any of-\f[C]amount1\f[R]/\f[C]amount1-in\f[R]/\f[C]amount1-out\f[R] are-assigned, and posting 2 ignores them if any of-\f[C]amount2\f[R]/\f[C]amount2-in\f[R]/\f[C]amount2-out\f[R] are-assigned, avoiding conflicts.-.SS currency-.PP-If the CSV has the currency symbol in a separate field (ie, not part of-the amount field), you can use \f[C]currencyN\f[R] to prepend it to-posting N\[aq]s amount.-Or, \f[C]currency\f[R] with no number affects all postings.-.SS balance-.PP-\f[C]balanceN\f[R] sets a balance assertion amount (or if the posting-amount is left empty, a balance assignment) on posting N.-.PP-Also, for compatibility with hledger <1.17: \f[C]balance\f[R] with no-number is equivalent to \f[C]balance1\f[R].-.PP-You can adjust the type of assertion/assignment with the-\f[C]balance-type\f[R] rule (see below).-.SS comment-.PP-Finally, \f[C]commentN\f[R] sets a comment on the Nth posting.-Comments can also contain tags, as usual.-.PP-See TIPS below for more about setting amounts and currency.-.SS field assignment-.IP-.nf-\f[C]-HLEDGERFIELDNAME FIELDVALUE-\f[R]-.fi-.PP-Instead of or in addition to a fields list, you can use a \[dq]field-assignment\[dq] rule to set the value of a single hledger field, by-writing its name (any of the standard hledger field names above)-followed by a text value.-The value may contain interpolated CSV fields, referenced by their-1-based position in the CSV record (\f[C]%N\f[R]), or by the name they-were given in the fields list (\f[C]%CSVFIELDNAME\f[R]).-Some examples:-.IP-.nf-\f[C]-# set the amount to the 4th CSV field, with \[dq] USD\[dq] appended-amount %4 USD--# combine three fields to make a comment, containing note: and date: tags-comment note: %somefield - %anotherfield, date: %1-\f[R]-.fi-.PP-Interpolation strips outer whitespace (so a CSV value like-\f[C]\[dq] 1 \[dq]\f[R] becomes \f[C]1\f[R] when interpolated) (#1051).-See TIPS below for more about referencing other fields.-.SS \f[C]separator\f[R]-.PP-You can use the \f[C]separator\f[R] rule to read other kinds of-character-separated data.-The argument is any single separator character, or the words-\f[C]tab\f[R] or \f[C]space\f[R] (case insensitive).-Eg, for comma-separated values (CSV):-.IP-.nf-\f[C]-separator ,-\f[R]-.fi-.PP-or for semicolon-separated values (SSV):-.IP-.nf-\f[C]-separator ;-\f[R]-.fi-.PP-or for tab-separated values (TSV):-.IP-.nf-\f[C]-separator TAB-\f[R]-.fi-.PP-If the input file has a \f[C].csv\f[R], \f[C].ssv\f[R] or \f[C].tsv\f[R]-file extension (or a \f[C]csv:\f[R], \f[C]ssv:\f[R], \f[C]tsv:\f[R]-prefix), the appropriate separator will be inferred automatically, and-you won\[aq]t need this rule.-.SS \f[C]if\f[R] block-.IP-.nf-\f[C]-if MATCHER- RULE--if-MATCHER-MATCHER-MATCHER- RULE- RULE-\f[R]-.fi-.PP-Conditional blocks (\[dq]if blocks\[dq]) are a block of rules that are-applied only to CSV records which match certain patterns.-They are often used for customising account names based on transaction-descriptions.-.SS Matching the whole record-.PP-Each MATCHER can be a record matcher, which looks like this:-.IP-.nf-\f[C]-REGEX-\f[R]-.fi-.PP-REGEX is a case-insensitive regular expression which tries to match-anywhere within the CSV record.-It is a POSIX ERE (extended regular expression) that also supports GNU-word boundaries (\f[C]\[rs]b\f[R], \f[C]\[rs]B\f[R], \f[C]\[rs]<\f[R],-\f[C]\[rs]>\f[R]), and nothing else.-If you have trouble, be sure to check our-https://hledger.org/hledger.html#regular-expressions doc.-.PP-Important note: the record that is matched is not the original record,-but a synthetic one, with any enclosing double quotes (but not enclosing-whitespace) removed, and always comma-separated (which means that a-field containing a comma will appear like two fields).-Eg, if the original record is-\f[C]2020-01-01; \[dq]Acme, Inc.\[dq]; 1,000\f[R], the REGEX will-actually see \f[C]2020-01-01,Acme, Inc., 1,000\f[R]).-.SS Matching individual fields-.PP-Or, MATCHER can be a field matcher, like this:-.IP-.nf-\f[C]-%CSVFIELD REGEX-\f[R]-.fi-.PP-which matches just the content of a particular CSV field.-CSVFIELD is a percent sign followed by the field\[aq]s name or column-number, like \f[C]%date\f[R] or \f[C]%1\f[R].-.SS Combining matchers-.PP-A single matcher can be written on the same line as the \[dq]if\[dq]; or-multiple matchers can be written on the following lines, non-indented.-Multiple matchers are OR\[aq]d (any one of them can match), unless one-begins with an \f[C]&\f[R] symbol, in which case it is AND\[aq]ed with-the previous matcher.-.IP-.nf-\f[C]-if-MATCHER-& MATCHER- RULE-\f[R]-.fi-.SS Rules applied on successful match-.PP-After the patterns there should be one or more rules to apply, all-indented by at least one space.-Three kinds of rule are allowed in conditional blocks:-.IP \[bu] 2-field assignments (to set a hledger field)-.IP \[bu] 2-skip (to skip the matched CSV record)-.IP \[bu] 2-end (to skip all remaining CSV records).-.PP-Examples:-.IP-.nf-\f[C]-# if the CSV record contains \[dq]groceries\[dq], set account2 to \[dq]expenses:groceries\[dq]-if groceries- account2 expenses:groceries-\f[R]-.fi-.IP-.nf-\f[C]-# if the CSV record contains any of these patterns, set account2 and comment as shown-if-monthly service fee-atm transaction fee-banking thru software- account2 expenses:business:banking- comment XXX deductible ? check it-\f[R]-.fi-.SS \f[C]if\f[R] table-.IP-.nf-\f[C]-if,CSVFIELDNAME1,CSVFIELDNAME2,...,CSVFIELDNAMEn-MATCHER1,VALUE11,VALUE12,...,VALUE1n-MATCHER2,VALUE21,VALUE22,...,VALUE2n-MATCHER3,VALUE31,VALUE32,...,VALUE3n-<empty line>-\f[R]-.fi-.PP-Conditional tables (\[dq]if tables\[dq]) are a different syntax to-specify field assignments that will be applied only to CSV records which-match certain patterns.-.PP-MATCHER could be either field or record matcher, as described above.-When MATCHER matches, values from that row would be assigned to the CSV-fields named on the \f[C]if\f[R] line, in the same order.-.PP-Therefore \f[C]if\f[R] table is exactly equivalent to a sequence of of-\f[C]if\f[R] blocks:-.IP-.nf-\f[C]-if MATCHER1- CSVFIELDNAME1 VALUE11- CSVFIELDNAME2 VALUE12- ...- CSVFIELDNAMEn VALUE1n--if MATCHER2- CSVFIELDNAME1 VALUE21- CSVFIELDNAME2 VALUE22- ...- CSVFIELDNAMEn VALUE2n--if MATCHER3- CSVFIELDNAME1 VALUE31- CSVFIELDNAME2 VALUE32- ...- CSVFIELDNAMEn VALUE3n-\f[R]-.fi-.PP-Each line starting with MATCHER should contain enough (possibly empty)-values for all the listed fields.-.PP-Rules would be checked and applied in the order they are listed in the-table and, like with \f[C]if\f[R] blocks, later rules (in the same or-another table) or \f[C]if\f[R] blocks could override the effect of any-rule.-.PP-Instead of \[aq],\[aq] you can use a variety of other non-alphanumeric-characters as a separator.-First character after \f[C]if\f[R] is taken to be the separator for the-rest of the table.-It is the responsibility of the user to ensure that separator does not-occur inside MATCHERs and values - there is no way to escape separator.-.PP-Example:-.IP-.nf-\f[C]-if,account2,comment-atm transaction fee,expenses:business:banking,deductible? check it-%description groceries,expenses:groceries,-2020/01/12.*Plumbing LLC,expenses:house:upkeep,emergency plumbing call-out-\f[R]-.fi-.SS \f[C]end\f[R]-.PP-This rule can be used inside if blocks (only), to make hledger stop-reading this CSV file and move on to the next input file, or to command-execution.-Eg:-.IP-.nf-\f[C]-# ignore everything following the first empty record-if ,,,,- end-\f[R]-.fi-.SS \f[C]date-format\f[R]-.IP-.nf-\f[C]-date-format DATEFMT-\f[R]-.fi-.PP-This is a helper for the \f[C]date\f[R] (and \f[C]date2\f[R]) fields.-If your CSV dates are not formatted like \f[C]YYYY-MM-DD\f[R],-\f[C]YYYY/MM/DD\f[R] or \f[C]YYYY.MM.DD\f[R], you\[aq]ll need to add a-date-format rule describing them with a strptime date parsing pattern,-which must parse the CSV date value completely.-Some examples:-.IP-.nf-\f[C]-# MM/DD/YY-date-format %m/%d/%y-\f[R]-.fi-.IP-.nf-\f[C]-# D/M/YYYY-# The - makes leading zeros optional.-date-format %-d/%-m/%Y-\f[R]-.fi-.IP-.nf-\f[C]-# YYYY-Mmm-DD-date-format %Y-%h-%d-\f[R]-.fi-.IP-.nf-\f[C]-# M/D/YYYY HH:MM AM some other junk-# Note the time and junk must be fully parsed, though only the date is used.-date-format %-m/%-d/%Y %l:%M %p some other junk-\f[R]-.fi-.PP-For the supported strptime syntax, see:-.PD 0-.P-.PD-https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime-.SS \f[C]decimal-mark\f[R]-.IP-.nf-\f[C]-decimal-mark .-\f[R]-.fi-.PP-or:-.IP-.nf-\f[C]-decimal-mark ,-\f[R]-.fi-.PP-hledger automatically accepts either period or comma as a decimal mark-when parsing numbers (cf Amounts).-However if any numbers in the CSV contain digit group marks, such as-thousand-separating commas, you should declare the decimal mark-explicitly with this rule, to avoid misparsed numbers.-.SS \f[C]newest-first\f[R]-.PP-hledger always sorts the generated transactions by date.-Transactions on the same date should appear in the same order as their-CSV records, as hledger can usually auto-detect whether the CSV\[aq]s-normal order is oldest first or newest first.-But if all of the following are true:-.IP \[bu] 2-the CSV might sometimes contain just one day of data (all records having-the same date)-.IP \[bu] 2-the CSV records are normally in reverse chronological order (newest at-the top)-.IP \[bu] 2-and you care about preserving the order of same-day transactions-.PP-then, you should add the \f[C]newest-first\f[R] rule as a hint.-Eg:-.IP-.nf-\f[C]-# tell hledger explicitly that the CSV is normally newest first-newest-first-\f[R]-.fi-.SS \f[C]include\f[R]-.IP-.nf-\f[C]-include RULESFILE-\f[R]-.fi-.PP-This includes the contents of another CSV rules file at this point.-\f[C]RULESFILE\f[R] is an absolute file path or a path relative to the-current file\[aq]s directory.-This can be useful for sharing common rules between several rules files,-eg:-.IP-.nf-\f[C]-# someaccount.csv.rules--## someaccount-specific rules-fields date,description,amount-account1 assets:someaccount-account2 expenses:misc--## common rules-include categorisation.rules-\f[R]-.fi-.SS \f[C]balance-type\f[R]-.PP-Balance assertions generated by assigning to balanceN are of the simple-\f[C]=\f[R] type by default, which is a single-commodity,-subaccount-excluding assertion.-You may find the subaccount-including variants more useful, eg if you-have created some virtual subaccounts of checking to help with-budgeting.-You can select a different type of assertion with the-\f[C]balance-type\f[R] rule:-.IP-.nf-\f[C]-# balance assertions will consider all commodities and all subaccounts-balance-type ==*-\f[R]-.fi-.PP-Here are the balance assertion types for quick reference:-.IP-.nf-\f[C]-= single commodity, exclude subaccounts-=* single commodity, include subaccounts-== multi commodity, exclude subaccounts-==* multi commodity, include subaccounts-\f[R]-.fi-.SH TIPS-.SS Rapid feedback-.PP-It\[aq]s a good idea to get rapid feedback while-creating/troubleshooting CSV rules.-Here\[aq]s a good way, using entr from http://eradman.com/entrproject :-.IP-.nf-\f[C]-$ ls foo.csv* | entr bash -c \[aq]echo ----; hledger -f foo.csv print desc:SOMEDESC\[aq]-\f[R]-.fi-.PP-A desc: query (eg) is used to select just one, or a few, transactions of-interest.-\[dq]bash -c\[dq] is used to run multiple commands, so we can echo a-separator each time the command re-runs, making it easier to read the-output.-.SS Valid CSV-.PP-hledger accepts CSV conforming to RFC 4180.-When CSV values are enclosed in quotes, note:-.IP \[bu] 2-they must be double quotes (not single quotes)-.IP \[bu] 2-spaces outside the quotes are not allowed-.SS File Extension-.PP-To help hledger identify the format and show the right error messages,-CSV/SSV/TSV files should normally be named with a \f[C].csv\f[R],-\f[C].ssv\f[R] or \f[C].tsv\f[R] filename extension.-Or, the file path should be prefixed with \f[C]csv:\f[R], \f[C]ssv:\f[R]-or \f[C]tsv:\f[R].-Eg:-.IP-.nf-\f[C]-$ hledger -f foo.ssv print-\f[R]-.fi-.PP-or:-.IP-.nf-\f[C]-$ cat foo | hledger -f ssv:- foo-\f[R]-.fi-.PP-You can override the file extension with a separator rule if needed.-See also: Input files in the hledger manual.-.SS Reading multiple CSV files-.PP-If you use multiple \f[C]-f\f[R] options to read multiple CSV files at-once, hledger will look for a correspondingly-named rules file for each-CSV file.-But if you use the \f[C]--rules-file\f[R] option, that rules file will-be used for all the CSV files.-.SS Valid transactions-.PP-After reading a CSV file, hledger post-processes and validates the-generated journal entries as it would for a journal file - balancing-them, applying balance assignments, and canonicalising amount styles.-Any errors at this stage will be reported in the usual way, displaying-the problem entry.-.PP-There is one exception: balance assertions, if you have generated them,-will not be checked, since normally these will work only when the CSV-data is part of the main journal.-If you do need to check balance assertions generated from CSV right-away, pipe into another hledger:-.IP-.nf-\f[C]-$ hledger -f file.csv print | hledger -f- print-\f[R]-.fi-.SS Deduplicating, importing-.PP-When you download a CSV file periodically, eg to get your latest bank-transactions, the new file may overlap with the old one, containing some-of the same records.-.PP-The import command will (a) detect the new transactions, and (b) append-just those transactions to your main journal.-It is idempotent, so you don\[aq]t have to remember how many times you-ran it or with which version of the CSV.-(It keeps state in a hidden \f[C].latest.FILE.csv\f[R] file.) This is-the easiest way to import CSV data.-Eg:-.IP-.nf-\f[C]-# download the latest CSV files, then run this command.-# Note, no -f flags needed here.-$ hledger import *.csv [--dry]-\f[R]-.fi-.PP-This method works for most CSV files.-(Where records have a stable chronological order, and new records appear-only at the new end.)-.PP-A number of other tools and workflows, hledger-specific and otherwise,-exist for converting, deduplicating, classifying and managing CSV data.-See:-.IP \[bu] 2-https://hledger.org -> sidebar -> real world setups-.IP \[bu] 2-https://plaintextaccounting.org -> data import/conversion-.SS Setting amounts-.PP-A posting amount can be set in one of these ways:-.IP \[bu] 2-by assigning (with a fields list or field assignment) to-\f[C]amountN\f[R] (posting N\[aq]s amount) or \f[C]amount\f[R] (posting-1\[aq]s amount)-.IP \[bu] 2-by assigning to \f[C]amountN-in\f[R] and \f[C]amountN-out\f[R] (or-\f[C]amount-in\f[R] and \f[C]amount-out\f[R]).-For each CSV record, whichever of these has a non-zero value will be-used, with appropriate sign.-If both contain a non-zero value, this may not work.-.IP \[bu] 2-by assigning to \f[C]balanceN\f[R] (or \f[C]balance\f[R]) instead of the-above, setting the amount indirectly via a balance assignment.-If you do this the default account name may be wrong, so you should set-that explicitly.-.PP-There is some special handling for an amount\[aq]s sign:-.IP \[bu] 2-If an amount value is parenthesised, it will be de-parenthesised and-sign-flipped.-.IP \[bu] 2-If an amount value begins with a double minus sign, those cancel out and-are removed.-.IP \[bu] 2-If an amount value begins with a plus sign, that will be removed-.SS Setting currency/commodity-.PP-If the currency/commodity symbol is included in the CSV\[aq]s amount-field(s):-.IP-.nf-\f[C]-2020-01-01,foo,$123.00-\f[R]-.fi-.PP-you don\[aq]t have to do anything special for the commodity symbol, it-will be assigned as part of the amount.-Eg:-.IP-.nf-\f[C]-fields date,description,amount-\f[R]-.fi-.IP-.nf-\f[C]-2020-01-01 foo- expenses:unknown $123.00- income:unknown $-123.00-\f[R]-.fi-.PP-If the currency is provided as a separate CSV field:-.IP-.nf-\f[C]-2020-01-01,foo,USD,123.00-\f[R]-.fi-.PP-You can assign that to the \f[C]currency\f[R] pseudo-field, which has-the special effect of prepending itself to every amount in the-transaction (on the left, with no separating space):-.IP-.nf-\f[C]-fields date,description,currency,amount-\f[R]-.fi-.IP-.nf-\f[C]-2020-01-01 foo- expenses:unknown USD123.00- income:unknown USD-123.00-\f[R]-.fi-.PP-Or, you can use a field assignment to construct the amount yourself,-with more control.-Eg to put the symbol on the right, and separated by a space:-.IP-.nf-\f[C]-fields date,description,cur,amt-amount %amt %cur-\f[R]-.fi-.IP-.nf-\f[C]-2020-01-01 foo- expenses:unknown 123.00 USD- income:unknown -123.00 USD-\f[R]-.fi-.PP-Note we used a temporary field name (\f[C]cur\f[R]) that is not-\f[C]currency\f[R] - that would trigger the prepending effect, which we-don\[aq]t want here.-.SS Referencing other fields-.PP-In field assignments, you can interpolate only CSV fields, not hledger-fields.-In the example below, there\[aq]s both a CSV field and a hledger field-named amount1, but %amount1 always means the CSV field, not the hledger-field:-.IP-.nf-\f[C]-# Name the third CSV field \[dq]amount1\[dq]-fields date,description,amount1--# Set hledger\[aq]s amount1 to the CSV amount1 field followed by USD-amount1 %amount1 USD--# Set comment to the CSV amount1 (not the amount1 assigned above)-comment %amount1-\f[R]-.fi-.PP-Here, since there\[aq]s no CSV amount1 field, %amount1 will produce a-literal \[dq]amount1\[dq]:-.IP-.nf-\f[C]-fields date,description,csvamount-amount1 %csvamount USD-# Can\[aq]t interpolate amount1 here-comment %amount1-\f[R]-.fi-.PP-When there are multiple field assignments to the same hledger field,-only the last one takes effect.-Here, comment\[aq]s value will be be B, or C if \[dq]something\[dq] is-matched, but never A:-.IP-.nf-\f[C]-comment A-comment B-if something- comment C-\f[R]-.fi-.SS How CSV rules are evaluated-.PP-Here\[aq]s how to think of CSV rules being evaluated (if you really need-to).-First,-.IP \[bu] 2-\f[C]include\f[R] - all includes are inlined, from top to bottom, depth-first.-(At each include point the file is inlined and scanned for further-includes, recursively, before proceeding.)-.PP-Then \[dq]global\[dq] rules are evaluated, top to bottom.-If a rule is repeated, the last one wins:-.IP \[bu] 2-\f[C]skip\f[R] (at top level)-.IP \[bu] 2-\f[C]date-format\f[R]-.IP \[bu] 2-\f[C]newest-first\f[R]-.IP \[bu] 2-\f[C]fields\f[R] - names the CSV fields, optionally sets up initial-assignments to hledger fields-.PP-Then for each CSV record in turn:-.IP \[bu] 2-test all \f[C]if\f[R] blocks.-If any of them contain a \f[C]end\f[R] rule, skip all remaining CSV-records.-Otherwise if any of them contain a \f[C]skip\f[R] rule, skip that many-CSV records.-If there are multiple matched \f[C]skip\f[R] rules, the first one wins.-.IP \[bu] 2-collect all field assignments at top level and in matched \f[C]if\f[R]-blocks.-When there are multiple assignments for a field, keep only the last one.-.IP \[bu] 2-compute a value for each hledger field - either the one that was-assigned to it (and interpolate the %CSVFIELDNAME references), or a-default-.IP \[bu] 2-generate a synthetic hledger transaction from these values.-.PP-This is all part of the CSV reader, one of several readers hledger can-use to parse input files.-When all files have been read successfully, the transactions are passed-as input to whichever hledger command the user specified.---.SH "REPORTING BUGS"-Report bugs at http://bugs.hledger.org-(or on the #hledger IRC channel or hledger mail list)--.SH AUTHORS-Simon Michael <simon@joyful.com> and contributors--.SH COPYRIGHT--Copyright (C) 2007-2020 Simon Michael.-.br-Released under GNU GPL v3 or later.--.SH SEE ALSO-hledger(1), hledger\-ui(1), hledger\-web(1), ledger(1)--hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_timedot(5)
− hledger_csv.info
@@ -1,1352 +0,0 @@-This is hledger-lib/hledger_csv.info, produced by makeinfo version 4.8-from stdin.---File: hledger_csv.info, Node: Top, Up: (dir)--hledger_csv(5)-**************--How hledger reads CSV data, and the CSV rules file format.-- hledger can read CSV files (Character Separated Value - usually-comma, semicolon, or tab) containing dated records as if they were-journal files, automatically converting each CSV record into a-transaction.-- (To learn about _writing_ CSV, see CSV output.)-- We describe each CSV file's format with a corresponding _rules-file_. By default this is named like the CSV file with a `.rules'-extension added. Eg when reading `FILE.csv', hledger also looks for-`FILE.csv.rules' in the same directory as `FILE.csv'. You can specify a-different rules file with the `--rules-file' option. If a rules file is-not found, hledger will create a sample rules file, which you'll need-to adjust.-- This file contains rules describing the CSV data (header line, fields-layout, date format etc.), and how to construct hledger journal entries-(transactions) from it. Often there will also be a list of conditional-rules for categorising transactions based on their descriptions. Here's-an overview of the CSV rules; these are described more fully below,-after the examples:--*`skip'* skip one or more header lines or matched- CSV records-*`fields'* name CSV fields, assign them to hledger- fields-*field assignment* assign a value to one hledger field,- with interpolation-*`separator'* a custom field separator-*`if' block* apply some rules to CSV records matched- by patterns-*`if' table* apply some rules to CSV records matched- by patterns, alternate syntax-*`end'* skip the remaining CSV records-*`date-format'* how to parse dates in CSV records-*`decimal-mark'* the decimal mark used in CSV amounts, if- ambiguous-*`newest-first'* disambiguate record order when there's- only one date-*`include'* inline another CSV rules file-*`balance-type'* choose which type of balance assignments- to use-- Note, for best error messages when reading CSV files, use a `.csv',-`.tsv' or `.ssv' file extension or file prefix - see File Extension-below.-- There's an introductory Convert CSV files tutorial on hledger.org.--* Menu:--* EXAMPLES::-* CSV RULES::-* TIPS::---File: hledger_csv.info, Node: EXAMPLES, Next: CSV RULES, Prev: Top, Up: Top--1 EXAMPLES-**********--Here are some sample hledger CSV rules files. See also the full-collection at:-https://github.com/simonmichael/hledger/tree/master/examples/csv--* Menu:--* Basic::-* Bank of Ireland::-* Amazon::-* Paypal::---File: hledger_csv.info, Node: Basic, Next: Bank of Ireland, Up: EXAMPLES--1.1 Basic-=========--At minimum, the rules file must identify the date and amount fields, and-often it also specifies the date format and how many header lines there-are. Here's a simple CSV file and a rules file for it:---Date, Description, Id, Amount-12/11/2019, Foo, 123, 10.23---# basic.csv.rules-skip 1-fields date, description, _, amount-date-format %d/%m/%Y---$ hledger print -f basic.csv-2019-11-12 Foo- expenses:unknown 10.23- income:unknown -10.23-- Default account names are chosen, since we didn't set them.---File: hledger_csv.info, Node: Bank of Ireland, Next: Amazon, Prev: Basic, Up: EXAMPLES--1.2 Bank of Ireland-===================--Here's a CSV with two amount fields (Debit and Credit), and a balance-field, which we can use to add balance assertions, which is not-necessary but provides extra error checking:---Date,Details,Debit,Credit,Balance-07/12/2012,LODGMENT 529898,,10.0,131.21-07/12/2012,PAYMENT,5,,126---# bankofireland-checking.csv.rules--# skip the header line-skip--# name the csv fields, and assign some of them as journal entry fields-fields date, description, amount-out, amount-in, balance--# We generate balance assertions by assigning to "balance"-# above, but you may sometimes need to remove these because:-#-# - the CSV balance differs from the true balance,-# by up to 0.0000000000005 in my experience-#-# - it is sometimes calculated based on non-chronological ordering,-# eg when multiple transactions clear on the same day--# date is in UK/Ireland format-date-format %d/%m/%Y--# set the currency-currency EUR--# set the base account for all txns-account1 assets:bank:boi:checking---$ hledger -f bankofireland-checking.csv print-2012-12-07 LODGMENT 529898- assets:bank:boi:checking EUR10.0 = EUR131.2- income:unknown EUR-10.0--2012-12-07 PAYMENT- assets:bank:boi:checking EUR-5.0 = EUR126.0- expenses:unknown EUR5.0-- The balance assertions don't raise an error above, because we're-reading directly from CSV, but they will be checked if these entries are-imported into a journal file.---File: hledger_csv.info, Node: Amazon, Next: Paypal, Prev: Bank of Ireland, Up: EXAMPLES--1.3 Amazon-==========--Here we convert amazon.com order history, and use an if block to-generate a third posting if there's a fee. (In practice you'd probably-get this data from your bank instead, but it's an example.)---"Date","Type","To/From","Name","Status","Amount","Fees","Transaction ID"-"Jul 29, 2012","Payment","To","Foo.","Completed","$20.00","$0.00","16000000000000DGLNJPI1P9B8DKPVHL"-"Jul 30, 2012","Payment","To","Adapteva, Inc.","Completed","$25.00","$1.00","17LA58JSKRD4HDGLNJPI1P9B8DKPVHL"---# amazon-orders.csv.rules--# skip one header line-skip 1--# name the csv fields, and assign the transaction's date, amount and code.-# Avoided the "status" and "amount" hledger field names to prevent confusion.-fields date, _, toorfrom, name, amzstatus, amzamount, fees, code--# how to parse the date-date-format %b %-d, %Y--# combine two fields to make the description-description %toorfrom %name--# save the status as a tag-comment status:%amzstatus--# set the base account for all transactions-account1 assets:amazon-# leave amount1 blank so it can balance the other(s).-# I'm assuming amzamount excludes the fees, don't remember--# set a generic account2-account2 expenses:misc-amount2 %amzamount-# and maybe refine it further:-#include categorisation.rules--# add a third posting for fees, but only if they are non-zero.-if %fees [1-9]- account3 expenses:fees- amount3 %fees---$ hledger -f amazon-orders.csv print-2012-07-29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo. ; status:Completed- assets:amazon- expenses:misc $20.00--2012-07-30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc. ; status:Completed- assets:amazon- expenses:misc $25.00- expenses:fees $1.00---File: hledger_csv.info, Node: Paypal, Prev: Amazon, Up: EXAMPLES--1.4 Paypal-==========--Here's a real-world rules file for (customised) Paypal CSV, with some-Paypal-specific rules, and a second rules file included:---"Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"-"10/01/2019","03:46:20","PDT","Calm Radio","Subscription Payment","Completed","USD","-6.99","0.00","-6.99","simon@joyful.com","memberships@calmradio.com","60P57143A8206782E","MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month","","I-R8YLY094FJYR","","-6.99",""-"10/01/2019","03:46:20","PDT","","Bank Deposit to PP Account ","Pending","USD","6.99","0.00","6.99","","simon@joyful.com","0TU1544T080463733","","","60P57143A8206782E","","0.00",""-"10/01/2019","08:57:01","PDT","Patreon","PreApproved Payment Bill User Payment","Completed","USD","-7.00","0.00","-7.00","simon@joyful.com","support@patreon.com","2722394R5F586712G","Patreon* Membership","","B-0PG93074E7M86381M","","-7.00",""-"10/01/2019","08:57:01","PDT","","Bank Deposit to PP Account ","Pending","USD","7.00","0.00","7.00","","simon@joyful.com","71854087RG994194F","Patreon* Membership","","2722394R5F586712G","","0.00",""-"10/19/2019","03:02:12","PDT","Wikimedia Foundation, Inc.","Subscription Payment","Completed","USD","-2.00","0.00","-2.00","simon@joyful.com","tle@wikimedia.org","K9U43044RY432050M","Monthly donation to the Wikimedia Foundation","","I-R5C3YUS3285L","","-2.00",""-"10/19/2019","03:02:12","PDT","","Bank Deposit to PP Account ","Pending","USD","2.00","0.00","2.00","","simon@joyful.com","3XJ107139A851061F","","","K9U43044RY432050M","","0.00",""-"10/22/2019","05:07:06","PDT","Noble Benefactor","Subscription Payment","Completed","USD","10.00","-0.59","9.41","noble@bene.fac.tor","simon@joyful.com","6L8L1662YP1334033","Joyful Systems","","I-KC9VBGY2GWDB","","9.41",""---# paypal-custom.csv.rules--# Tips:-# Export from Activity -> Statements -> Custom -> Activity download-# Suggested transaction type: "Balance affecting"-# Paypal's default fields in 2018 were:-# "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Shipping Address","Address Status","Item Title","Item ID","Shipping and Handling Amount","Insurance Amount","Sales Tax","Option 1 Name","Option 1 Value","Option 2 Name","Option 2 Value","Reference Txn ID","Invoice Number","Custom Number","Quantity","Receipt ID","Balance","Address Line 1","Address Line 2/District/Neighborhood","Town/City","State/Province/Region/County/Territory/Prefecture/Republic","Zip/Postal Code","Country","Contact Phone Number","Subject","Note","Country Code","Balance Impact"-# This rules file assumes the following more detailed fields, configured in "Customize report fields":-# "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"--fields date, time, timezone, description_, type, status_, currency, grossamount, feeamount, netamount, fromemail, toemail, code, itemtitle, itemid, referencetxnid, receiptid, balance, note--skip 1--date-format %-m/%-d/%Y--# ignore some paypal events-if-In Progress-Temporary Hold-Update to- skip--# add more fields to the description-description %description_ %itemtitle--# save some other fields as tags-comment itemid:%itemid, fromemail:%fromemail, toemail:%toemail, time:%time, type:%type, status:%status_--# convert to short currency symbols-if %currency USD- currency $-if %currency EUR- currency E-if %currency GBP- currency P--# generate postings--# the first posting will be the money leaving/entering my paypal account-# (negative means leaving my account, in all amount fields)-account1 assets:online:paypal-amount1 %netamount--# the second posting will be money sent to/received from other party-# (account2 is set below)-amount2 -%grossamount--# if there's a fee, add a third posting for the money taken by paypal.-if %feeamount [1-9]- account3 expenses:banking:paypal- amount3 -%feeamount- comment3 business:--# choose an account for the second posting--# override the default account names:-# if the amount is positive, it's income (a debit)-if %grossamount ^[^-]- account2 income:unknown-# if negative, it's an expense (a credit)-if %grossamount ^-- account2 expenses:unknown--# apply common rules for setting account2 & other tweaks-include common.rules--# apply some overrides specific to this csv--# Transfers from/to bank. These are usually marked Pending,-# which can be disregarded in this case.-if-Bank Account-Bank Deposit to PP Account- description %type for %referencetxnid %itemtitle- account2 assets:bank:wf:pchecking- account1 assets:online:paypal--# Currency conversions-if Currency Conversion- account2 equity:currency conversion---# common.rules--if-darcs-noble benefactor- account2 revenues:foss donations:darcshub- comment2 business:--if-Calm Radio- account2 expenses:online:apps--if-electronic frontier foundation-Patreon-wikimedia-Advent of Code- account2 expenses:dues--if Google- account2 expenses:online:apps- description google | music---$ hledger -f paypal-custom.csv print-2019-10-01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month ; itemid:, fromemail:simon@joyful.com, toemail:memberships@calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed- assets:online:paypal $-6.99 = $-6.99- expenses:online:apps $6.99--2019-10-01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $6.99 = $0.00- assets:bank:wf:pchecking $-6.99--2019-10-01 (2722394R5F586712G) Patreon Patreon* Membership ; itemid:, fromemail:simon@joyful.com, toemail:support@patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed- assets:online:paypal $-7.00 = $-7.00- expenses:dues $7.00--2019-10-01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership ; itemid:, fromemail:, toemail:simon@joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $7.00 = $0.00- assets:bank:wf:pchecking $-7.00--2019-10-19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation ; itemid:, fromemail:simon@joyful.com, toemail:tle@wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed- assets:online:paypal $-2.00 = $-2.00- expenses:dues $2.00- expenses:banking:paypal ; business:--2019-10-19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $2.00 = $0.00- assets:bank:wf:pchecking $-2.00--2019-10-22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems ; itemid:, fromemail:noble@bene.fac.tor, toemail:simon@joyful.com, time:05:07:06, type:Subscription Payment, status:Completed- assets:online:paypal $9.41 = $9.41- revenues:foss donations:darcshub $-10.00 ; business:- expenses:banking:paypal $0.59 ; business:---File: hledger_csv.info, Node: CSV RULES, Next: TIPS, Prev: EXAMPLES, Up: Top--2 CSV RULES-***********--The following kinds of rule can appear in the rules file, in any order.-Blank lines and lines beginning with `#' or `;' are ignored.--* Menu:--* skip::-* fields::-* field assignment::-* separator::-* if block::-* if table::-* end::-* date-format::-* decimal-mark::-* newest-first::-* include::-* balance-type::---File: hledger_csv.info, Node: skip, Next: fields, Up: CSV RULES--2.1 `skip'-==========---skip N--The word "skip" followed by a number (or no number, meaning 1) tells-hledger to ignore this many non-empty lines preceding the CSV data.-(Empty/blank lines are skipped automatically.) You'll need this whenever-your CSV data contains header lines.-- It also has a second purpose: it can be used inside if blocks to-ignore certain CSV records (described below).---File: hledger_csv.info, Node: fields, Next: field assignment, Prev: skip, Up: CSV RULES--2.2 `fields'-============---fields FIELDNAME1, FIELDNAME2, ...--A fields list (the word "fields" followed by comma-separated field-names) is the quick way to assign CSV field values to hledger fields. It-does two things:-- 1. it names the CSV fields. This is optional, but can be convenient- later for interpolating them.-- 2. when you use a standard hledger field name, it assigns the CSV- value to that part of the hledger transaction.--- Here's an example that says "use the 1st, 2nd and 4th fields as the-transaction's date, description and amount; name the last two fields for-later reference; and ignore the others":---fields date, description, , amount, , , somefield, anotherfield-- Field names may not contain whitespace. Fields you don't care about-can be left unnamed. Currently there must be least two items (there-must be at least one comma).-- Note, always use comma in the fields list, even if your CSV uses-another separator character.-- Here are the standard hledger field/pseudo-field names. For more-about the transaction parts they refer to, see the manual for hledger's-journal format.--* Menu:--* Transaction field names::-* Posting field names::---File: hledger_csv.info, Node: Transaction field names, Next: Posting field names, Up: fields--2.2.1 Transaction field names--------------------------------`date', `date2', `status', `code', `description', `comment' can be used-to form the transaction's first line.---File: hledger_csv.info, Node: Posting field names, Prev: Transaction field names, Up: fields--2.2.2 Posting field names----------------------------* Menu:--* account::-* amount::-* currency::-* balance::-* comment::---File: hledger_csv.info, Node: account, Next: amount, Up: Posting field names--2.2.2.1 account-...............--`accountN', where N is 1 to 99, causes a posting to be generated, with-that account name.-- Most often there are two postings, so you'll want to set `account1'-and `account2'. Typically `account1' is associated with the CSV file,-and is set once with a top-level assignment, while `account2' is set-based on each transaction's description, and in conditional blocks.-- If a posting's account name is left unset but its amount is set (see-below), a default account name will be chosen (like "expenses:unknown"-or "income:unknown").---File: hledger_csv.info, Node: amount, Next: currency, Prev: account, Up: Posting field names--2.2.2.2 amount-..............--`amountN' sets posting N's amount. If the CSV uses separate fields for-inflows and outflows, you can use `amountN-in' and `amountN-out'-instead. By assigning to `amount1', `amount2', ... etc. you can-generate anywhere from 0 to 99 postings.-- There is also an older, unnumbered form of these names, suitable for-2-posting transactions, which sets both posting 1's and (negated)-posting 2's amount: `amount', or `amount-in' and `amount-out'. This is-still supported because it keeps pre-hledger-1.17 csv rules files-working, and because it can be more succinct, and because it converts-posting 2's amount to cost if there's a transaction price, which can be-useful.-- If you have an existing rules file using the unnumbered form, you-might want to use the numbered form in certain conditional blocks,-without having to update and retest all the old rules. To facilitate-this, posting 1 ignores `amount'/`amount-in'/`amount-out' if any of-`amount1'/`amount1-in'/`amount1-out' are assigned, and posting 2-ignores them if any of `amount2'/`amount2-in'/`amount2-out' are-assigned, avoiding conflicts.---File: hledger_csv.info, Node: currency, Next: balance, Prev: amount, Up: Posting field names--2.2.2.3 currency-................--If the CSV has the currency symbol in a separate field (ie, not part of-the amount field), you can use `currencyN' to prepend it to posting N's-amount. Or, `currency' with no number affects all postings.---File: hledger_csv.info, Node: balance, Next: comment, Prev: currency, Up: Posting field names--2.2.2.4 balance-...............--`balanceN' sets a balance assertion amount (or if the posting amount is-left empty, a balance assignment) on posting N.-- Also, for compatibility with hledger <1.17: `balance' with no number-is equivalent to `balance1'.-- You can adjust the type of assertion/assignment with the-`balance-type' rule (see below).---File: hledger_csv.info, Node: comment, Prev: balance, Up: Posting field names--2.2.2.5 comment-...............--Finally, `commentN' sets a comment on the Nth posting. Comments can-also contain tags, as usual.-- See TIPS below for more about setting amounts and currency.---File: hledger_csv.info, Node: field assignment, Next: separator, Prev: fields, Up: CSV RULES--2.3 field assignment-====================---HLEDGERFIELDNAME FIELDVALUE--Instead of or in addition to a fields list, you can use a "field-assignment" rule to set the value of a single hledger field, by writing-its name (any of the standard hledger field names above) followed by a-text value. The value may contain interpolated CSV fields, referenced by-their 1-based position in the CSV record (`%N'), or by the name they-were given in the fields list (`%CSVFIELDNAME'). Some examples:---# set the amount to the 4th CSV field, with " USD" appended-amount %4 USD--# combine three fields to make a comment, containing note: and date: tags-comment note: %somefield - %anotherfield, date: %1-- Interpolation strips outer whitespace (so a CSV value like `" 1 "'-becomes `1' when interpolated) (#1051). See TIPS below for more about-referencing other fields.---File: hledger_csv.info, Node: separator, Next: if block, Prev: field assignment, Up: CSV RULES--2.4 `separator'-===============--You can use the `separator' rule to read other kinds of-character-separated data. The argument is any single separator-character, or the words `tab' or `space' (case insensitive). Eg, for-comma-separated values (CSV):---separator ,-- or for semicolon-separated values (SSV):---separator ;-- or for tab-separated values (TSV):---separator TAB-- If the input file has a `.csv', `.ssv' or `.tsv' file extension (or-a `csv:', `ssv:', `tsv:' prefix), the appropriate separator will be-inferred automatically, and you won't need this rule.---File: hledger_csv.info, Node: if block, Next: if table, Prev: separator, Up: CSV RULES--2.5 `if' block-==============---if MATCHER- RULE--if-MATCHER-MATCHER-MATCHER- RULE- RULE--Conditional blocks ("if blocks") are a block of rules that are applied-only to CSV records which match certain patterns. They are often used-for customising account names based on transaction descriptions.--* Menu:--* Matching the whole record::-* Matching individual fields::-* Combining matchers::-* Rules applied on successful match::---File: hledger_csv.info, Node: Matching the whole record, Next: Matching individual fields, Up: if block--2.5.1 Matching the whole record----------------------------------Each MATCHER can be a record matcher, which looks like this:---REGEX-- REGEX is a case-insensitive regular expression which tries to match-anywhere within the CSV record. It is a POSIX ERE (extended regular-expression) that also supports GNU word boundaries (`\b', `\B', `\<',-`\>'), and nothing else. If you have trouble, be sure to check our-https://hledger.org/hledger.html#regular-expressions doc.-- Important note: the record that is matched is not the original-record, but a synthetic one, with any enclosing double quotes (but not-enclosing whitespace) removed, and always comma-separated (which means-that a field containing a comma will appear like two fields). Eg, if the-original record is `2020-01-01; "Acme, Inc."; 1,000', the REGEX will-actually see `2020-01-01,Acme, Inc., 1,000').---File: hledger_csv.info, Node: Matching individual fields, Next: Combining matchers, Prev: Matching the whole record, Up: if block--2.5.2 Matching individual fields-----------------------------------Or, MATCHER can be a field matcher, like this:---%CSVFIELD REGEX-- which matches just the content of a particular CSV field. CSVFIELD-is a percent sign followed by the field's name or column number, like-`%date' or `%1'.---File: hledger_csv.info, Node: Combining matchers, Next: Rules applied on successful match, Prev: Matching individual fields, Up: if block--2.5.3 Combining matchers---------------------------A single matcher can be written on the same line as the "if"; or-multiple matchers can be written on the following lines, non-indented.-Multiple matchers are OR'd (any one of them can match), unless one-begins with an `&' symbol, in which case it is AND'ed with the previous-matcher.---if-MATCHER-& MATCHER- RULE---File: hledger_csv.info, Node: Rules applied on successful match, Prev: Combining matchers, Up: if block--2.5.4 Rules applied on successful match------------------------------------------After the patterns there should be one or more rules to apply, all-indented by at least one space. Three kinds of rule are allowed in-conditional blocks:-- * field assignments (to set a hledger field)-- * skip (to skip the matched CSV record)-- * end (to skip all remaining CSV records).-- Examples:---# if the CSV record contains "groceries", set account2 to "expenses:groceries"-if groceries- account2 expenses:groceries---# if the CSV record contains any of these patterns, set account2 and comment as shown-if-monthly service fee-atm transaction fee-banking thru software- account2 expenses:business:banking- comment XXX deductible ? check it---File: hledger_csv.info, Node: if table, Next: end, Prev: if block, Up: CSV RULES--2.6 `if' table-==============---if,CSVFIELDNAME1,CSVFIELDNAME2,...,CSVFIELDNAMEn-MATCHER1,VALUE11,VALUE12,...,VALUE1n-MATCHER2,VALUE21,VALUE22,...,VALUE2n-MATCHER3,VALUE31,VALUE32,...,VALUE3n-<empty line>--Conditional tables ("if tables") are a different syntax to specify field-assignments that will be applied only to CSV records which match certain-patterns.-- MATCHER could be either field or record matcher, as described above.-When MATCHER matches, values from that row would be assigned to the CSV-fields named on the `if' line, in the same order.-- Therefore `if' table is exactly equivalent to a sequence of of `if'-blocks:---if MATCHER1- CSVFIELDNAME1 VALUE11- CSVFIELDNAME2 VALUE12- ...- CSVFIELDNAMEn VALUE1n--if MATCHER2- CSVFIELDNAME1 VALUE21- CSVFIELDNAME2 VALUE22- ...- CSVFIELDNAMEn VALUE2n--if MATCHER3- CSVFIELDNAME1 VALUE31- CSVFIELDNAME2 VALUE32- ...- CSVFIELDNAMEn VALUE3n-- Each line starting with MATCHER should contain enough (possibly-empty) values for all the listed fields.-- Rules would be checked and applied in the order they are listed in-the table and, like with `if' blocks, later rules (in the same or-another table) or `if' blocks could override the effect of any rule.-- Instead of ',' you can use a variety of other non-alphanumeric-characters as a separator. First character after `if' is taken to be-the separator for the rest of the table. It is the responsibility of-the user to ensure that separator does not occur inside MATCHERs and-values - there is no way to escape separator.-- Example:---if,account2,comment-atm transaction fee,expenses:business:banking,deductible? check it-%description groceries,expenses:groceries,-2020/01/12.*Plumbing LLC,expenses:house:upkeep,emergency plumbing call-out---File: hledger_csv.info, Node: end, Next: date-format, Prev: if table, Up: CSV RULES--2.7 `end'-=========--This rule can be used inside if blocks (only), to make hledger stop-reading this CSV file and move on to the next input file, or to command-execution. Eg:---# ignore everything following the first empty record-if ,,,,- end---File: hledger_csv.info, Node: date-format, Next: decimal-mark, Prev: end, Up: CSV RULES--2.8 `date-format'-=================---date-format DATEFMT--This is a helper for the `date' (and `date2') fields. If your CSV dates-are not formatted like `YYYY-MM-DD', `YYYY/MM/DD' or `YYYY.MM.DD',-you'll need to add a date-format rule describing them with a strptime-date parsing pattern, which must parse the CSV date value completely.-Some examples:---# MM/DD/YY-date-format %m/%d/%y---# D/M/YYYY-# The - makes leading zeros optional.-date-format %-d/%-m/%Y---# YYYY-Mmm-DD-date-format %Y-%h-%d---# M/D/YYYY HH:MM AM some other junk-# Note the time and junk must be fully parsed, though only the date is used.-date-format %-m/%-d/%Y %l:%M %p some other junk-- For the supported strptime syntax, see:-https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime---File: hledger_csv.info, Node: decimal-mark, Next: newest-first, Prev: date-format, Up: CSV RULES--2.9 `decimal-mark'-==================---decimal-mark .--or:---decimal-mark ,-- hledger automatically accepts either period or comma as a decimal-mark when parsing numbers (cf Amounts). However if any numbers in the-CSV contain digit group marks, such as thousand-separating commas, you-should declare the decimal mark explicitly with this rule, to avoid-misparsed numbers.---File: hledger_csv.info, Node: newest-first, Next: include, Prev: decimal-mark, Up: CSV RULES--2.10 `newest-first'-===================--hledger always sorts the generated transactions by date. Transactions on-the same date should appear in the same order as their CSV records, as-hledger can usually auto-detect whether the CSV's normal order is oldest-first or newest first. But if all of the following are true:-- * the CSV might sometimes contain just one day of data (all records- having the same date)-- * the CSV records are normally in reverse chronological order- (newest at the top)-- * and you care about preserving the order of same-day transactions-- then, you should add the `newest-first' rule as a hint. Eg:---# tell hledger explicitly that the CSV is normally newest first-newest-first---File: hledger_csv.info, Node: include, Next: balance-type, Prev: newest-first, Up: CSV RULES--2.11 `include'-==============---include RULESFILE--This includes the contents of another CSV rules file at this point.-`RULESFILE' is an absolute file path or a path relative to the current-file's directory. This can be useful for sharing common rules between-several rules files, eg:---# someaccount.csv.rules--## someaccount-specific rules-fields date,description,amount-account1 assets:someaccount-account2 expenses:misc--## common rules-include categorisation.rules---File: hledger_csv.info, Node: balance-type, Prev: include, Up: CSV RULES--2.12 `balance-type'-===================--Balance assertions generated by assigning to balanceN are of the simple-`=' type by default, which is a single-commodity, subaccount-excluding-assertion. You may find the subaccount-including variants more useful,-eg if you have created some virtual subaccounts of checking to help-with budgeting. You can select a different type of assertion with the-`balance-type' rule:---# balance assertions will consider all commodities and all subaccounts-balance-type ==*-- Here are the balance assertion types for quick reference:---= single commodity, exclude subaccounts-=* single commodity, include subaccounts-== multi commodity, exclude subaccounts-==* multi commodity, include subaccounts---File: hledger_csv.info, Node: TIPS, Prev: CSV RULES, Up: Top--3 TIPS-******--* Menu:--* Rapid feedback::-* Valid CSV::-* File Extension::-* Reading multiple CSV files::-* Valid transactions::-* Deduplicating importing::-* Setting amounts::-* Setting currency/commodity::-* Referencing other fields::-* How CSV rules are evaluated::---File: hledger_csv.info, Node: Rapid feedback, Next: Valid CSV, Up: TIPS--3.1 Rapid feedback-==================--It's a good idea to get rapid feedback while creating/troubleshooting-CSV rules. Here's a good way, using entr from-http://eradman.com/entrproject :---$ ls foo.csv* | entr bash -c 'echo ----; hledger -f foo.csv print desc:SOMEDESC'-- A desc: query (eg) is used to select just one, or a few,-transactions of interest. "bash -c" is used to run multiple commands,-so we can echo a separator each time the command re-runs, making it-easier to read the output.---File: hledger_csv.info, Node: Valid CSV, Next: File Extension, Prev: Rapid feedback, Up: TIPS--3.2 Valid CSV-=============--hledger accepts CSV conforming to RFC 4180. When CSV values are enclosed-in quotes, note:-- * they must be double quotes (not single quotes)-- * spaces outside the quotes are not allowed---File: hledger_csv.info, Node: File Extension, Next: Reading multiple CSV files, Prev: Valid CSV, Up: TIPS--3.3 File Extension-==================--To help hledger identify the format and show the right error messages,-CSV/SSV/TSV files should normally be named with a `.csv', `.ssv' or-`.tsv' filename extension. Or, the file path should be prefixed with-`csv:', `ssv:' or `tsv:'. Eg:---$ hledger -f foo.ssv print-- or:---$ cat foo | hledger -f ssv:- foo-- You can override the file extension with a separator rule if needed.-See also: Input files in the hledger manual.---File: hledger_csv.info, Node: Reading multiple CSV files, Next: Valid transactions, Prev: File Extension, Up: TIPS--3.4 Reading multiple CSV files-==============================--If you use multiple `-f' options to read multiple CSV files at once,-hledger will look for a correspondingly-named rules file for each CSV-file. But if you use the `--rules-file' option, that rules file will be-used for all the CSV files.---File: hledger_csv.info, Node: Valid transactions, Next: Deduplicating importing, Prev: Reading multiple CSV files, Up: TIPS--3.5 Valid transactions-======================--After reading a CSV file, hledger post-processes and validates the-generated journal entries as it would for a journal file - balancing-them, applying balance assignments, and canonicalising amount styles.-Any errors at this stage will be reported in the usual way, displaying-the problem entry.-- There is one exception: balance assertions, if you have generated-them, will not be checked, since normally these will work only when the-CSV data is part of the main journal. If you do need to check balance-assertions generated from CSV right away, pipe into another hledger:---$ hledger -f file.csv print | hledger -f- print---File: hledger_csv.info, Node: Deduplicating importing, Next: Setting amounts, Prev: Valid transactions, Up: TIPS--3.6 Deduplicating, importing-============================--When you download a CSV file periodically, eg to get your latest bank-transactions, the new file may overlap with the old one, containing some-of the same records.-- The import command will (a) detect the new transactions, and (b)-append just those transactions to your main journal. It is idempotent,-so you don't have to remember how many times you ran it or with which-version of the CSV. (It keeps state in a hidden `.latest.FILE.csv'-file.) This is the easiest way to import CSV data. Eg:---# download the latest CSV files, then run this command.-# Note, no -f flags needed here.-$ hledger import *.csv [--dry]-- This method works for most CSV files. (Where records have a stable-chronological order, and new records appear only at the new end.)-- A number of other tools and workflows, hledger-specific and-otherwise, exist for converting, deduplicating, classifying and-managing CSV data. See:-- * https://hledger.org -> sidebar -> real world setups-- * https://plaintextaccounting.org -> data import/conversion---File: hledger_csv.info, Node: Setting amounts, Next: Setting currency/commodity, Prev: Deduplicating importing, Up: TIPS--3.7 Setting amounts-===================--A posting amount can be set in one of these ways:-- * by assigning (with a fields list or field assignment) to `amountN'- (posting N's amount) or `amount' (posting 1's amount)-- * by assigning to `amountN-in' and `amountN-out' (or `amount-in' and- `amount-out'). For each CSV record, whichever of these has a- non-zero value will be used, with appropriate sign. If both- contain a non-zero value, this may not work.-- * by assigning to `balanceN' (or `balance') instead of the above,- setting the amount indirectly via a balance assignment. If you do- this the default account name may be wrong, so you should set that- explicitly.--- There is some special handling for an amount's sign:-- * If an amount value is parenthesised, it will be de-parenthesised- and sign-flipped.-- * If an amount value begins with a double minus sign, those cancel- out and are removed.-- * If an amount value begins with a plus sign, that will be removed---File: hledger_csv.info, Node: Setting currency/commodity, Next: Referencing other fields, Prev: Setting amounts, Up: TIPS--3.8 Setting currency/commodity-==============================--If the currency/commodity symbol is included in the CSV's amount-field(s):---2020-01-01,foo,$123.00-- you don't have to do anything special for the commodity symbol, it-will be assigned as part of the amount. Eg:---fields date,description,amount---2020-01-01 foo- expenses:unknown $123.00- income:unknown $-123.00-- If the currency is provided as a separate CSV field:---2020-01-01,foo,USD,123.00-- You can assign that to the `currency' pseudo-field, which has the-special effect of prepending itself to every amount in the transaction-(on the left, with no separating space):---fields date,description,currency,amount---2020-01-01 foo- expenses:unknown USD123.00- income:unknown USD-123.00-- Or, you can use a field assignment to construct the amount yourself,-with more control. Eg to put the symbol on the right, and separated by a-space:---fields date,description,cur,amt-amount %amt %cur---2020-01-01 foo- expenses:unknown 123.00 USD- income:unknown -123.00 USD-- Note we used a temporary field name (`cur') that is not `currency' --that would trigger the prepending effect, which we don't want here.---File: hledger_csv.info, Node: Referencing other fields, Next: How CSV rules are evaluated, Prev: Setting currency/commodity, Up: TIPS--3.9 Referencing other fields-============================--In field assignments, you can interpolate only CSV fields, not hledger-fields. In the example below, there's both a CSV field and a hledger-field named amount1, but %amount1 always means the CSV field, not the-hledger field:---# Name the third CSV field "amount1"-fields date,description,amount1--# Set hledger's amount1 to the CSV amount1 field followed by USD-amount1 %amount1 USD--# Set comment to the CSV amount1 (not the amount1 assigned above)-comment %amount1-- Here, since there's no CSV amount1 field, %amount1 will produce a-literal "amount1":---fields date,description,csvamount-amount1 %csvamount USD-# Can't interpolate amount1 here-comment %amount1-- When there are multiple field assignments to the same hledger field,-only the last one takes effect. Here, comment's value will be be B, or C-if "something" is matched, but never A:---comment A-comment B-if something- comment C---File: hledger_csv.info, Node: How CSV rules are evaluated, Prev: Referencing other fields, Up: TIPS--3.10 How CSV rules are evaluated-================================--Here's how to think of CSV rules being evaluated (if you really need-to). First,-- * `include' - all includes are inlined, from top to bottom, depth- first. (At each include point the file is inlined and scanned for- further includes, recursively, before proceeding.)-- Then "global" rules are evaluated, top to bottom. If a rule is-repeated, the last one wins:-- * `skip' (at top level)-- * `date-format'-- * `newest-first'-- * `fields' - names the CSV fields, optionally sets up initial- assignments to hledger fields-- Then for each CSV record in turn:-- * test all `if' blocks. If any of them contain a `end' rule, skip- all remaining CSV records. Otherwise if any of them contain a- `skip' rule, skip that many CSV records. If there are multiple- matched `skip' rules, the first one wins.-- * collect all field assignments at top level and in matched `if'- blocks. When there are multiple assignments for a field, keep only- the last one.-- * compute a value for each hledger field - either the one that was- assigned to it (and interpolate the %CSVFIELDNAME references), or a- default-- * generate a synthetic hledger transaction from these values.-- This is all part of the CSV reader, one of several readers hledger-can use to parse input files. When all files have been read-successfully, the transactions are passed as input to whichever hledger-command the user specified.----Tag Table:-Node: Top84-Node: EXAMPLES2746-Ref: #examples2852-Node: Basic3059-Ref: #basic3159-Node: Bank of Ireland3703-Ref: #bank-of-ireland3838-Node: Amazon5303-Ref: #amazon5421-Node: Paypal7142-Ref: #paypal7236-Node: CSV RULES14884-Ref: #csv-rules14993-Node: skip15305-Ref: #skip15398-Node: fields15770-Ref: #fields15892-Node: Transaction field names17053-Ref: #transaction-field-names17213-Node: Posting field names17324-Ref: #posting-field-names17476-Node: account17546-Ref: #account17662-Node: amount18198-Ref: #amount18329-Node: currency19430-Ref: #currency19565-Node: balance19770-Ref: #balance19904-Node: comment20221-Ref: #comment20338-Node: field assignment20500-Ref: #field-assignment20643-Node: separator21457-Ref: #separator21592-Node: if block22134-Ref: #if-block22259-Node: Matching the whole record22657-Ref: #matching-the-whole-record22832-Node: Matching individual fields23636-Ref: #matching-individual-fields23840-Node: Combining matchers24064-Ref: #combining-matchers24260-Node: Rules applied on successful match24574-Ref: #rules-applied-on-successful-match24765-Node: if table25422-Ref: #if-table25541-Node: end27277-Ref: #end27389-Node: date-format27613-Ref: #date-format27745-Node: decimal-mark28495-Ref: #decimal-mark28638-Node: newest-first28975-Ref: #newest-first29116-Node: include29799-Ref: #include29930-Node: balance-type30372-Ref: #balance-type30492-Node: TIPS31192-Ref: #tips31274-Node: Rapid feedback31530-Ref: #rapid-feedback31647-Node: Valid CSV32106-Ref: #valid-csv32236-Node: File Extension32428-Ref: #file-extension32580-Node: Reading multiple CSV files33009-Ref: #reading-multiple-csv-files33194-Node: Valid transactions33434-Ref: #valid-transactions33612-Node: Deduplicating importing34240-Ref: #deduplicating-importing34419-Node: Setting amounts35451-Ref: #setting-amounts35620-Node: Setting currency/commodity36607-Ref: #setting-currencycommodity36799-Node: Referencing other fields37979-Ref: #referencing-other-fields38179-Node: How CSV rules are evaluated39077-Ref: #how-csv-rules-are-evaluated39250--End Tag Table
− hledger_csv.txt
@@ -1,962 +0,0 @@--HLEDGER_CSV(5) hledger User Manuals HLEDGER_CSV(5)----NAME- How hledger reads CSV data, and the CSV rules file format.--DESCRIPTION- hledger can read CSV files (Character Separated Value - usually comma,- semicolon, or tab) containing dated records as if they were journal- files, automatically converting each CSV record into a transaction.-- (To learn about writing CSV, see CSV output.)-- We describe each CSV file's format with a corresponding rules file. By- default this is named like the CSV file with a .rules extension added.- Eg when reading FILE.csv, hledger also looks for FILE.csv.rules in the- same directory as FILE.csv. You can specify a different rules file- with the --rules-file option. If a rules file is not found, hledger- will create a sample rules file, which you'll need to adjust.-- This file contains rules describing the CSV data (header line, fields- layout, date format etc.), and how to construct hledger journal entries- (transactions) from it. Often there will also be a list of conditional- rules for categorising transactions based on their descriptions.- Here's an overview of the CSV rules; these are described more fully- below, after the examples:--- skip skip one or more header lines or matched- CSV records- fields name CSV fields, assign them to hledger- fields- field assignment assign a value to one hledger field,- with interpolation- separator a custom field separator- if block apply some rules to CSV records matched- by patterns- if table apply some rules to CSV records matched- by patterns, alternate syntax- end skip the remaining CSV records- date-format how to parse dates in CSV records- decimal-mark the decimal mark used in CSV amounts, if- ambiguous- newest-first disambiguate record order when there's- only one date- include inline another CSV rules file- balance-type choose which type of balance assignments- to use-- Note, for best error messages when reading CSV files, use a .csv, .tsv- or .ssv file extension or file prefix - see File Extension below.-- There's an introductory Convert CSV files tutorial on hledger.org.--EXAMPLES- Here are some sample hledger CSV rules files. See also the full col-- lection at:- https://github.com/simonmichael/hledger/tree/master/examples/csv-- Basic- At minimum, the rules file must identify the date and amount fields,- and often it also specifies the date format and how many header lines- there are. Here's a simple CSV file and a rules file for it:-- Date, Description, Id, Amount- 12/11/2019, Foo, 123, 10.23-- # basic.csv.rules- skip 1- fields date, description, _, amount- date-format %d/%m/%Y-- $ hledger print -f basic.csv- 2019-11-12 Foo- expenses:unknown 10.23- income:unknown -10.23-- Default account names are chosen, since we didn't set them.-- Bank of Ireland- Here's a CSV with two amount fields (Debit and Credit), and a balance- field, which we can use to add balance assertions, which is not neces-- sary but provides extra error checking:-- Date,Details,Debit,Credit,Balance- 07/12/2012,LODGMENT 529898,,10.0,131.21- 07/12/2012,PAYMENT,5,,126-- # bankofireland-checking.csv.rules-- # skip the header line- skip-- # name the csv fields, and assign some of them as journal entry fields- fields date, description, amount-out, amount-in, balance-- # We generate balance assertions by assigning to "balance"- # above, but you may sometimes need to remove these because:- #- # - the CSV balance differs from the true balance,- # by up to 0.0000000000005 in my experience- #- # - it is sometimes calculated based on non-chronological ordering,- # eg when multiple transactions clear on the same day-- # date is in UK/Ireland format- date-format %d/%m/%Y-- # set the currency- currency EUR-- # set the base account for all txns- account1 assets:bank:boi:checking-- $ hledger -f bankofireland-checking.csv print- 2012-12-07 LODGMENT 529898- assets:bank:boi:checking EUR10.0 = EUR131.2- income:unknown EUR-10.0-- 2012-12-07 PAYMENT- assets:bank:boi:checking EUR-5.0 = EUR126.0- expenses:unknown EUR5.0-- The balance assertions don't raise an error above, because we're read-- ing directly from CSV, but they will be checked if these entries are- imported into a journal file.-- Amazon- Here we convert amazon.com order history, and use an if block to gener-- ate a third posting if there's a fee. (In practice you'd probably get- this data from your bank instead, but it's an example.)-- "Date","Type","To/From","Name","Status","Amount","Fees","Transaction ID"- "Jul 29, 2012","Payment","To","Foo.","Completed","$20.00","$0.00","16000000000000DGLNJPI1P9B8DKPVHL"- "Jul 30, 2012","Payment","To","Adapteva, Inc.","Completed","$25.00","$1.00","17LA58JSKRD4HDGLNJPI1P9B8DKPVHL"-- # amazon-orders.csv.rules-- # skip one header line- skip 1-- # name the csv fields, and assign the transaction's date, amount and code.- # Avoided the "status" and "amount" hledger field names to prevent confusion.- fields date, _, toorfrom, name, amzstatus, amzamount, fees, code-- # how to parse the date- date-format %b %-d, %Y-- # combine two fields to make the description- description %toorfrom %name-- # save the status as a tag- comment status:%amzstatus-- # set the base account for all transactions- account1 assets:amazon- # leave amount1 blank so it can balance the other(s).- # I'm assuming amzamount excludes the fees, don't remember-- # set a generic account2- account2 expenses:misc- amount2 %amzamount- # and maybe refine it further:- #include categorisation.rules-- # add a third posting for fees, but only if they are non-zero.- if %fees [1-9]- account3 expenses:fees- amount3 %fees-- $ hledger -f amazon-orders.csv print- 2012-07-29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo. ; status:Completed- assets:amazon- expenses:misc $20.00-- 2012-07-30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc. ; status:Completed- assets:amazon- expenses:misc $25.00- expenses:fees $1.00-- Paypal- Here's a real-world rules file for (customised) Paypal CSV, with some- Paypal-specific rules, and a second rules file included:-- "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"- "10/01/2019","03:46:20","PDT","Calm Radio","Subscription Payment","Completed","USD","-6.99","0.00","-6.99","simon@joyful.com","memberships@calmradio.com","60P57143A8206782E","MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month","","I-R8YLY094FJYR","","-6.99",""- "10/01/2019","03:46:20","PDT","","Bank Deposit to PP Account ","Pending","USD","6.99","0.00","6.99","","simon@joyful.com","0TU1544T080463733","","","60P57143A8206782E","","0.00",""- "10/01/2019","08:57:01","PDT","Patreon","PreApproved Payment Bill User Payment","Completed","USD","-7.00","0.00","-7.00","simon@joyful.com","support@patreon.com","2722394R5F586712G","Patreon* Membership","","B-0PG93074E7M86381M","","-7.00",""- "10/01/2019","08:57:01","PDT","","Bank Deposit to PP Account ","Pending","USD","7.00","0.00","7.00","","simon@joyful.com","71854087RG994194F","Patreon* Membership","","2722394R5F586712G","","0.00",""- "10/19/2019","03:02:12","PDT","Wikimedia Foundation, Inc.","Subscription Payment","Completed","USD","-2.00","0.00","-2.00","simon@joyful.com","tle@wikimedia.org","K9U43044RY432050M","Monthly donation to the Wikimedia Foundation","","I-R5C3YUS3285L","","-2.00",""- "10/19/2019","03:02:12","PDT","","Bank Deposit to PP Account ","Pending","USD","2.00","0.00","2.00","","simon@joyful.com","3XJ107139A851061F","","","K9U43044RY432050M","","0.00",""- "10/22/2019","05:07:06","PDT","Noble Benefactor","Subscription Payment","Completed","USD","10.00","-0.59","9.41","noble@bene.fac.tor","simon@joyful.com","6L8L1662YP1334033","Joyful Systems","","I-KC9VBGY2GWDB","","9.41",""-- # paypal-custom.csv.rules-- # Tips:- # Export from Activity -> Statements -> Custom -> Activity download- # Suggested transaction type: "Balance affecting"- # Paypal's default fields in 2018 were:- # "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Shipping Address","Address Status","Item Title","Item ID","Shipping and Handling Amount","Insurance Amount","Sales Tax","Option 1 Name","Option 1 Value","Option 2 Name","Option 2 Value","Reference Txn ID","Invoice Number","Custom Number","Quantity","Receipt ID","Balance","Address Line 1","Address Line 2/District/Neighborhood","Town/City","State/Province/Region/County/Territory/Prefecture/Republic","Zip/Postal Code","Country","Contact Phone Number","Subject","Note","Country Code","Balance Impact"- # This rules file assumes the following more detailed fields, configured in "Customize report fields":- # "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"-- fields date, time, timezone, description_, type, status_, currency, grossamount, feeamount, netamount, fromemail, toemail, code, itemtitle, itemid, referencetxnid, receiptid, balance, note-- skip 1-- date-format %-m/%-d/%Y-- # ignore some paypal events- if- In Progress- Temporary Hold- Update to- skip-- # add more fields to the description- description %description_ %itemtitle-- # save some other fields as tags- comment itemid:%itemid, fromemail:%fromemail, toemail:%toemail, time:%time, type:%type, status:%status_-- # convert to short currency symbols- if %currency USD- currency $- if %currency EUR- currency E- if %currency GBP- currency P-- # generate postings-- # the first posting will be the money leaving/entering my paypal account- # (negative means leaving my account, in all amount fields)- account1 assets:online:paypal- amount1 %netamount-- # the second posting will be money sent to/received from other party- # (account2 is set below)- amount2 -%grossamount-- # if there's a fee, add a third posting for the money taken by paypal.- if %feeamount [1-9]- account3 expenses:banking:paypal- amount3 -%feeamount- comment3 business:-- # choose an account for the second posting-- # override the default account names:- # if the amount is positive, it's income (a debit)- if %grossamount ^[^-]- account2 income:unknown- # if negative, it's an expense (a credit)- if %grossamount ^-- account2 expenses:unknown-- # apply common rules for setting account2 & other tweaks- include common.rules-- # apply some overrides specific to this csv-- # Transfers from/to bank. These are usually marked Pending,- # which can be disregarded in this case.- if- Bank Account- Bank Deposit to PP Account- description %type for %referencetxnid %itemtitle- account2 assets:bank:wf:pchecking- account1 assets:online:paypal-- # Currency conversions- if Currency Conversion- account2 equity:currency conversion-- # common.rules-- if- darcs- noble benefactor- account2 revenues:foss donations:darcshub- comment2 business:-- if- Calm Radio- account2 expenses:online:apps-- if- electronic frontier foundation- Patreon- wikimedia- Advent of Code- account2 expenses:dues-- if Google- account2 expenses:online:apps- description google | music-- $ hledger -f paypal-custom.csv print- 2019-10-01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month ; itemid:, fromemail:simon@joyful.com, toemail:memberships@calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed- assets:online:paypal $-6.99 = $-6.99- expenses:online:apps $6.99-- 2019-10-01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $6.99 = $0.00- assets:bank:wf:pchecking $-6.99-- 2019-10-01 (2722394R5F586712G) Patreon Patreon* Membership ; itemid:, fromemail:simon@joyful.com, toemail:support@patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed- assets:online:paypal $-7.00 = $-7.00- expenses:dues $7.00-- 2019-10-01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership ; itemid:, fromemail:, toemail:simon@joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $7.00 = $0.00- assets:bank:wf:pchecking $-7.00-- 2019-10-19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation ; itemid:, fromemail:simon@joyful.com, toemail:tle@wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed- assets:online:paypal $-2.00 = $-2.00- expenses:dues $2.00- expenses:banking:paypal ; business:-- 2019-10-19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending- assets:online:paypal $2.00 = $0.00- assets:bank:wf:pchecking $-2.00-- 2019-10-22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems ; itemid:, fromemail:noble@bene.fac.tor, toemail:simon@joyful.com, time:05:07:06, type:Subscription Payment, status:Completed- assets:online:paypal $9.41 = $9.41- revenues:foss donations:darcshub $-10.00 ; business:- expenses:banking:paypal $0.59 ; business:--CSV RULES- The following kinds of rule can appear in the rules file, in any order.- Blank lines and lines beginning with # or ; are ignored.-- skip- skip N-- The word "skip" followed by a number (or no number, meaning 1) tells- hledger to ignore this many non-empty lines preceding the CSV data.- (Empty/blank lines are skipped automatically.) You'll need this when-- ever your CSV data contains header lines.-- It also has a second purpose: it can be used inside if blocks to ignore- certain CSV records (described below).-- fields- fields FIELDNAME1, FIELDNAME2, ...-- A fields list (the word "fields" followed by comma-separated field- names) is the quick way to assign CSV field values to hledger fields.- It does two things:-- 1. it names the CSV fields. This is optional, but can be convenient- later for interpolating them.-- 2. when you use a standard hledger field name, it assigns the CSV value- to that part of the hledger transaction.-- Here's an example that says "use the 1st, 2nd and 4th fields as the- transaction's date, description and amount; name the last two fields- for later reference; and ignore the others":-- fields date, description, , amount, , , somefield, anotherfield-- Field names may not contain whitespace. Fields you don't care about- can be left unnamed. Currently there must be least two items (there- must be at least one comma).-- Note, always use comma in the fields list, even if your CSV uses- another separator character.-- Here are the standard hledger field/pseudo-field names. For more about- the transaction parts they refer to, see the manual for hledger's jour-- nal format.-- Transaction field names- date, date2, status, code, description, comment can be used to form the- transaction's first line.-- Posting field names- account- accountN, where N is 1 to 99, causes a posting to be generated, with- that account name.-- Most often there are two postings, so you'll want to set account1 and- account2. Typically account1 is associated with the CSV file, and is- set once with a top-level assignment, while account2 is set based on- each transaction's description, and in conditional blocks.-- If a posting's account name is left unset but its amount is set (see- below), a default account name will be chosen (like "expenses:unknown"- or "income:unknown").-- amount- amountN sets posting N's amount. If the CSV uses separate fields for- inflows and outflows, you can use amountN-in and amountN-out instead.- By assigning to amount1, amount2, ... etc. you can generate anywhere- from 0 to 99 postings.-- There is also an older, unnumbered form of these names, suitable for- 2-posting transactions, which sets both posting 1's and (negated) post-- ing 2's amount: amount, or amount-in and amount-out. This is still- supported because it keeps pre-hledger-1.17 csv rules files working,- and because it can be more succinct, and because it converts posting- 2's amount to cost if there's a transaction price, which can be useful.-- If you have an existing rules file using the unnumbered form, you might- want to use the numbered form in certain conditional blocks, without- having to update and retest all the old rules. To facilitate this,- posting 1 ignores amount/amount-in/amount-out if any of- amount1/amount1-in/amount1-out are assigned, and posting 2 ignores them- if any of amount2/amount2-in/amount2-out are assigned, avoiding con-- flicts.-- currency- If the CSV has the currency symbol in a separate field (ie, not part of- the amount field), you can use currencyN to prepend it to posting N's- amount. Or, currency with no number affects all postings.-- balance- balanceN sets a balance assertion amount (or if the posting amount is- left empty, a balance assignment) on posting N.-- Also, for compatibility with hledger <1.17: balance with no number is- equivalent to balance1.-- You can adjust the type of assertion/assignment with the balance-type- rule (see below).-- comment- Finally, commentN sets a comment on the Nth posting. Comments can also- contain tags, as usual.-- See TIPS below for more about setting amounts and currency.-- field assignment- HLEDGERFIELDNAME FIELDVALUE-- Instead of or in addition to a fields list, you can use a "field- assignment" rule to set the value of a single hledger field, by writing- its name (any of the standard hledger field names above) followed by a- text value. The value may contain interpolated CSV fields, referenced- by their 1-based position in the CSV record (%N), or by the name they- were given in the fields list (%CSVFIELDNAME). Some examples:-- # set the amount to the 4th CSV field, with " USD" appended- amount %4 USD-- # combine three fields to make a comment, containing note: and date: tags- comment note: %somefield - %anotherfield, date: %1-- Interpolation strips outer whitespace (so a CSV value like " 1 "- becomes 1 when interpolated) (#1051). See TIPS below for more about- referencing other fields.-- separator- You can use the separator rule to read other kinds of character-sepa-- rated data. The argument is any single separator character, or the- words tab or space (case insensitive). Eg, for comma-separated values- (CSV):-- separator ,-- or for semicolon-separated values (SSV):-- separator ;-- or for tab-separated values (TSV):-- separator TAB-- If the input file has a .csv, .ssv or .tsv file extension (or a csv:,- ssv:, tsv: prefix), the appropriate separator will be inferred automat-- ically, and you won't need this rule.-- if block- if MATCHER- RULE-- if- MATCHER- MATCHER- MATCHER- RULE- RULE-- Conditional blocks ("if blocks") are a block of rules that are applied- only to CSV records which match certain patterns. They are often used- for customising account names based on transaction descriptions.-- Matching the whole record- Each MATCHER can be a record matcher, which looks like this:-- REGEX-- REGEX is a case-insensitive regular expression which tries to match- anywhere within the CSV record. It is a POSIX ERE (extended regular- expression) that also supports GNU word boundaries (\b, \B, \<, \>),- and nothing else. If you have trouble, be sure to check our- https://hledger.org/hledger.html#regular-expressions doc.-- Important note: the record that is matched is not the original record,- but a synthetic one, with any enclosing double quotes (but not enclos-- ing whitespace) removed, and always comma-separated (which means that a- field containing a comma will appear like two fields). Eg, if the- original record is 2020-01-01; "Acme, Inc."; 1,000, the REGEX will- actually see 2020-01-01,Acme, Inc., 1,000).-- Matching individual fields- Or, MATCHER can be a field matcher, like this:-- %CSVFIELD REGEX-- which matches just the content of a particular CSV field. CSVFIELD is- a percent sign followed by the field's name or column number, like- %date or %1.-- Combining matchers- A single matcher can be written on the same line as the "if"; or multi-- ple matchers can be written on the following lines, non-indented. Mul-- tiple matchers are OR'd (any one of them can match), unless one begins- with an & symbol, in which case it is AND'ed with the previous matcher.-- if- MATCHER- & MATCHER- RULE-- Rules applied on successful match- After the patterns there should be one or more rules to apply, all- indented by at least one space. Three kinds of rule are allowed in- conditional blocks:-- o field assignments (to set a hledger field)-- o skip (to skip the matched CSV record)-- o end (to skip all remaining CSV records).-- Examples:-- # if the CSV record contains "groceries", set account2 to "expenses:groceries"- if groceries- account2 expenses:groceries-- # if the CSV record contains any of these patterns, set account2 and comment as shown- if- monthly service fee- atm transaction fee- banking thru software- account2 expenses:business:banking- comment XXX deductible ? check it-- if table- if,CSVFIELDNAME1,CSVFIELDNAME2,...,CSVFIELDNAMEn- MATCHER1,VALUE11,VALUE12,...,VALUE1n- MATCHER2,VALUE21,VALUE22,...,VALUE2n- MATCHER3,VALUE31,VALUE32,...,VALUE3n- <empty line>-- Conditional tables ("if tables") are a different syntax to specify- field assignments that will be applied only to CSV records which match- certain patterns.-- MATCHER could be either field or record matcher, as described above.- When MATCHER matches, values from that row would be assigned to the CSV- fields named on the if line, in the same order.-- Therefore if table is exactly equivalent to a sequence of of if blocks:-- if MATCHER1- CSVFIELDNAME1 VALUE11- CSVFIELDNAME2 VALUE12- ...- CSVFIELDNAMEn VALUE1n-- if MATCHER2- CSVFIELDNAME1 VALUE21- CSVFIELDNAME2 VALUE22- ...- CSVFIELDNAMEn VALUE2n-- if MATCHER3- CSVFIELDNAME1 VALUE31- CSVFIELDNAME2 VALUE32- ...- CSVFIELDNAMEn VALUE3n-- Each line starting with MATCHER should contain enough (possibly empty)- values for all the listed fields.-- Rules would be checked and applied in the order they are listed in the- table and, like with if blocks, later rules (in the same or another ta-- ble) or if blocks could override the effect of any rule.-- Instead of ',' you can use a variety of other non-alphanumeric charac-- ters as a separator. First character after if is taken to be the sepa-- rator for the rest of the table. It is the responsibility of the user- to ensure that separator does not occur inside MATCHERs and values -- there is no way to escape separator.-- Example:-- if,account2,comment- atm transaction fee,expenses:business:banking,deductible? check it- %description groceries,expenses:groceries,- 2020/01/12.*Plumbing LLC,expenses:house:upkeep,emergency plumbing call-out-- end- This rule can be used inside if blocks (only), to make hledger stop- reading this CSV file and move on to the next input file, or to command- execution. Eg:-- # ignore everything following the first empty record- if ,,,,- end-- date-format- date-format DATEFMT-- This is a helper for the date (and date2) fields. If your CSV dates- are not formatted like YYYY-MM-DD, YYYY/MM/DD or YYYY.MM.DD, you'll- need to add a date-format rule describing them with a strptime date- parsing pattern, which must parse the CSV date value completely. Some- examples:-- # MM/DD/YY- date-format %m/%d/%y-- # D/M/YYYY- # The - makes leading zeros optional.- date-format %-d/%-m/%Y-- # YYYY-Mmm-DD- date-format %Y-%h-%d-- # M/D/YYYY HH:MM AM some other junk- # Note the time and junk must be fully parsed, though only the date is used.- date-format %-m/%-d/%Y %l:%M %p some other junk-- For the supported strptime syntax, see:- https://hackage.haskell.org/package/time/docs/Data-Time-For-- mat.html#v:formatTime-- decimal-mark- decimal-mark .-- or:-- decimal-mark ,-- hledger automatically accepts either period or comma as a decimal mark- when parsing numbers (cf Amounts). However if any numbers in the CSV- contain digit group marks, such as thousand-separating commas, you- should declare the decimal mark explicitly with this rule, to avoid- misparsed numbers.-- newest-first- hledger always sorts the generated transactions by date. Transactions- on the same date should appear in the same order as their CSV records,- as hledger can usually auto-detect whether the CSV's normal order is- oldest first or newest first. But if all of the following are true:-- o the CSV might sometimes contain just one day of data (all records- having the same date)-- o the CSV records are normally in reverse chronological order (newest- at the top)-- o and you care about preserving the order of same-day transactions-- then, you should add the newest-first rule as a hint. Eg:-- # tell hledger explicitly that the CSV is normally newest first- newest-first-- include- include RULESFILE-- This includes the contents of another CSV rules file at this point.- RULESFILE is an absolute file path or a path relative to the current- file's directory. This can be useful for sharing common rules between- several rules files, eg:-- # someaccount.csv.rules-- ## someaccount-specific rules- fields date,description,amount- account1 assets:someaccount- account2 expenses:misc-- ## common rules- include categorisation.rules-- balance-type- Balance assertions generated by assigning to balanceN are of the simple- = type by default, which is a single-commodity, subaccount-excluding- assertion. You may find the subaccount-including variants more useful,- eg if you have created some virtual subaccounts of checking to help- with budgeting. You can select a different type of assertion with the- balance-type rule:-- # balance assertions will consider all commodities and all subaccounts- balance-type ==*-- Here are the balance assertion types for quick reference:-- = single commodity, exclude subaccounts- =* single commodity, include subaccounts- == multi commodity, exclude subaccounts- ==* multi commodity, include subaccounts--TIPS- Rapid feedback- It's a good idea to get rapid feedback while creating/troubleshooting- CSV rules. Here's a good way, using entr from http://eradman.com/entr-- project :-- $ ls foo.csv* | entr bash -c 'echo ----; hledger -f foo.csv print desc:SOMEDESC'-- A desc: query (eg) is used to select just one, or a few, transactions- of interest. "bash -c" is used to run multiple commands, so we can- echo a separator each time the command re-runs, making it easier to- read the output.-- Valid CSV- hledger accepts CSV conforming to RFC 4180. When CSV values are- enclosed in quotes, note:-- o they must be double quotes (not single quotes)-- o spaces outside the quotes are not allowed-- File Extension- To help hledger identify the format and show the right error messages,- CSV/SSV/TSV files should normally be named with a .csv, .ssv or .tsv- filename extension. Or, the file path should be prefixed with csv:,- ssv: or tsv:. Eg:-- $ hledger -f foo.ssv print-- or:-- $ cat foo | hledger -f ssv:- foo-- You can override the file extension with a separator rule if needed.- See also: Input files in the hledger manual.-- Reading multiple CSV files- If you use multiple -f options to read multiple CSV files at once,- hledger will look for a correspondingly-named rules file for each CSV- file. But if you use the --rules-file option, that rules file will be- used for all the CSV files.-- Valid transactions- After reading a CSV file, hledger post-processes and validates the gen-- erated journal entries as it would for a journal file - balancing them,- applying balance assignments, and canonicalising amount styles. Any- errors at this stage will be reported in the usual way, displaying the- problem entry.-- There is one exception: balance assertions, if you have generated them,- will not be checked, since normally these will work only when the CSV- data is part of the main journal. If you do need to check balance- assertions generated from CSV right away, pipe into another hledger:-- $ hledger -f file.csv print | hledger -f- print-- Deduplicating, importing- When you download a CSV file periodically, eg to get your latest bank- transactions, the new file may overlap with the old one, containing- some of the same records.-- The import command will (a) detect the new transactions, and (b) append- just those transactions to your main journal. It is idempotent, so you- don't have to remember how many times you ran it or with which version- of the CSV. (It keeps state in a hidden .latest.FILE.csv file.) This- is the easiest way to import CSV data. Eg:-- # download the latest CSV files, then run this command.- # Note, no -f flags needed here.- $ hledger import *.csv [--dry]-- This method works for most CSV files. (Where records have a stable- chronological order, and new records appear only at the new end.)-- A number of other tools and workflows, hledger-specific and otherwise,- exist for converting, deduplicating, classifying and managing CSV data.- See:-- o https://hledger.org -> sidebar -> real world setups-- o https://plaintextaccounting.org -> data import/conversion-- Setting amounts- A posting amount can be set in one of these ways:-- o by assigning (with a fields list or field assignment) to amountN- (posting N's amount) or amount (posting 1's amount)-- o by assigning to amountN-in and amountN-out (or amount-in and amount-- out). For each CSV record, whichever of these has a non-zero value- will be used, with appropriate sign. If both contain a non-zero- value, this may not work.-- o by assigning to balanceN (or balance) instead of the above, setting- the amount indirectly via a balance assignment. If you do this the- default account name may be wrong, so you should set that explicitly.-- There is some special handling for an amount's sign:-- o If an amount value is parenthesised, it will be de-parenthesised and- sign-flipped.-- o If an amount value begins with a double minus sign, those cancel out- and are removed.-- o If an amount value begins with a plus sign, that will be removed-- Setting currency/commodity- If the currency/commodity symbol is included in the CSV's amount- field(s):-- 2020-01-01,foo,$123.00-- you don't have to do anything special for the commodity symbol, it will- be assigned as part of the amount. Eg:-- fields date,description,amount-- 2020-01-01 foo- expenses:unknown $123.00- income:unknown $-123.00-- If the currency is provided as a separate CSV field:-- 2020-01-01,foo,USD,123.00-- You can assign that to the currency pseudo-field, which has the special- effect of prepending itself to every amount in the transaction (on the- left, with no separating space):-- fields date,description,currency,amount-- 2020-01-01 foo- expenses:unknown USD123.00- income:unknown USD-123.00-- Or, you can use a field assignment to construct the amount yourself,- with more control. Eg to put the symbol on the right, and separated by- a space:-- fields date,description,cur,amt- amount %amt %cur-- 2020-01-01 foo- expenses:unknown 123.00 USD- income:unknown -123.00 USD-- Note we used a temporary field name (cur) that is not currency - that- would trigger the prepending effect, which we don't want here.-- Referencing other fields- In field assignments, you can interpolate only CSV fields, not hledger- fields. In the example below, there's both a CSV field and a hledger- field named amount1, but %amount1 always means the CSV field, not the- hledger field:-- # Name the third CSV field "amount1"- fields date,description,amount1-- # Set hledger's amount1 to the CSV amount1 field followed by USD- amount1 %amount1 USD-- # Set comment to the CSV amount1 (not the amount1 assigned above)- comment %amount1-- Here, since there's no CSV amount1 field, %amount1 will produce a lit-- eral "amount1":-- fields date,description,csvamount- amount1 %csvamount USD- # Can't interpolate amount1 here- comment %amount1-- When there are multiple field assignments to the same hledger field,- only the last one takes effect. Here, comment's value will be be B, or- C if "something" is matched, but never A:-- comment A- comment B- if something- comment C-- How CSV rules are evaluated- Here's how to think of CSV rules being evaluated (if you really need- to). First,-- o include - all includes are inlined, from top to bottom, depth first.- (At each include point the file is inlined and scanned for further- includes, recursively, before proceeding.)-- Then "global" rules are evaluated, top to bottom. If a rule is- repeated, the last one wins:-- o skip (at top level)-- o date-format-- o newest-first-- o fields - names the CSV fields, optionally sets up initial assignments- to hledger fields-- Then for each CSV record in turn:-- o test all if blocks. If any of them contain a end rule, skip all- remaining CSV records. Otherwise if any of them contain a skip rule,- skip that many CSV records. If there are multiple matched skip- rules, the first one wins.-- o collect all field assignments at top level and in matched if blocks.- When there are multiple assignments for a field, keep only the last- one.-- o compute a value for each hledger field - either the one that was- assigned to it (and interpolate the %CSVFIELDNAME references), or a- default-- o generate a synthetic hledger transaction from these values.-- This is all part of the CSV reader, one of several readers hledger can- use to parse input files. When all files have been read successfully,- the transactions are passed as input to whichever hledger command the- user specified.----REPORTING BUGS- Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel- or hledger mail list)---AUTHORS- Simon Michael <simon@joyful.com> and contributors---COPYRIGHT- Copyright (C) 2007-2020 Simon Michael.- Released under GNU GPL v3 or later.---SEE ALSO- hledger(1), hledger-ui(1), hledger-web(1), ledger(1)-- hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_time-- dot(5)----hledger-lib-1.20.4 December 2020 HLEDGER_CSV(5)
− hledger_journal.5
@@ -1,2153 +0,0 @@-.\"t--.TH "HLEDGER_JOURNAL" "5" "December 2020" "hledger-lib-1.20.4 " "hledger User Manuals"----.SH NAME-.PP-hledger\[aq]s default file format, representing a General Journal.-.SH DESCRIPTION-.PP-hledger\[aq]s usual data source is a plain text file containing journal-entries in hledger journal format.-This file represents a standard accounting general journal.-I use file names ending in \f[C].journal\f[R], but that\[aq]s not-required.-The journal file contains a number of transaction entries, each-describing a transfer of money (or any commodity) between two or more-named accounts, in a simple format readable by both hledger and humans.-.PP-hledger\[aq]s journal format is a compatible subset, mostly, of-ledger\[aq]s journal format, so hledger can work with compatible ledger-journal files as well.-It\[aq]s safe, and encouraged, to run both hledger and ledger on the-same journal file, eg to validate the results you\[aq]re getting.-.PP-You can use hledger without learning any more about this file; just use-the add or web or import commands to create and update it.-.PP-Many users, though, edit the journal file with a text editor, and track-changes with a version control system such as git.-Editor addons such as ledger-mode or hledger-mode for Emacs, vim-ledger-for Vim, and hledger-vscode for Visual Studio Code, make this easier,-adding colour, formatting, tab completion, and useful commands.-See Editor configuration at hledger.org for the full list.-.PP-Here\[aq]s a description of each part of the file format (and-hledger\[aq]s data model).-These are mostly in the order you\[aq]ll use them, but in some cases-related concepts have been grouped together for easy reference, or-linked before they are introduced, so feel free to skip over anything-that looks unnecessary right now.-.SH TRANSACTIONS-.PP-Transactions are the main unit of information in a journal file.-They represent events, typically a movement of some quantity of-commodities between two or more named accounts.-.PP-Each transaction is recorded as a journal entry, beginning with a simple-date in column 0.-This can be followed by any of the following optional fields, separated-by spaces:-.IP \[bu] 2-a status character (empty, \f[C]!\f[R], or \f[C]*\f[R])-.IP \[bu] 2-a code (any short number or text, enclosed in parentheses)-.IP \[bu] 2-a description (any remaining text until end of line or a semicolon)-.IP \[bu] 2-a comment (any remaining text following a semicolon until end of line,-and any following indented lines beginning with a semicolon)-.IP \[bu] 2-0 or more indented \f[I]posting\f[R] lines, describing what was-transferred and the accounts involved (indented comment lines are also-allowed, but not blank lines or non-indented lines).-.PP-Here\[aq]s a simple journal file containing one transaction:-.IP-.nf-\f[C]-2008/01/01 income- assets:bank:checking $1- income:salary $-1-\f[R]-.fi-.SH DATES-.SS Simple dates-.PP-Dates in the journal file use \f[I]simple dates\f[R] format:-\f[C]YYYY-MM-DD\f[R] or \f[C]YYYY/MM/DD\f[R] or \f[C]YYYY.MM.DD\f[R],-with leading zeros optional.-The year may be omitted, in which case it will be inferred from the-context: the current transaction, the default year set with a default-year directive, or the current date when the command is run.-Some examples: \f[C]2010-01-31\f[R], \f[C]2010/01/31\f[R],-\f[C]2010.1.31\f[R], \f[C]1/31\f[R].-.PP-(The UI also accepts simple dates, as well as the more flexible smart-dates documented in the hledger manual.)-.SS Secondary dates-.PP-Real-life transactions sometimes involve more than one date - eg the-date you write a cheque, and the date it clears in your bank.-When you want to model this, for more accurate daily balances, you can-specify individual posting dates.-.PP-Or, you can use the older \f[I]secondary date\f[R] feature (Ledger calls-it auxiliary date or effective date).-Note: we support this for compatibility, but I usually recommend-avoiding this feature; posting dates are almost always clearer and-simpler.-.PP-A secondary date is written after the primary date, following an equals-sign.-If the year is omitted, the primary date\[aq]s year is assumed.-When running reports, the primary (left) date is used by default, but-with the \f[C]--date2\f[R] flag (or \f[C]--aux-date\f[R] or-\f[C]--effective\f[R]), the secondary (right) date will be used instead.-.PP-The meaning of secondary dates is up to you, but it\[aq]s best to follow-a consistent rule.-Eg \[dq]primary = the bank\[aq]s clearing date, secondary = date the-transaction was initiated, if different\[dq], as shown here:-.IP-.nf-\f[C]-2010/2/23=2/19 movie ticket- expenses:cinema $10- assets:checking-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger register checking-2010-02-23 movie ticket assets:checking $-10 $-10-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger register checking --date2-2010-02-19 movie ticket assets:checking $-10 $-10-\f[R]-.fi-.SS Posting dates-.PP-You can give individual postings a different date from their parent-transaction, by adding a posting comment containing a tag (see below)-like \f[C]date:DATE\f[R].-This is probably the best way to control posting dates precisely.-Eg in this example the expense should appear in May reports, and the-deduction from checking should be reported on 6/1 for easy bank-reconciliation:-.IP-.nf-\f[C]-2015/5/30- expenses:food $10 ; food purchased on saturday 5/30- assets:checking ; bank cleared it on monday, date:6/1-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f t.j register food-2015-05-30 expenses:food $10 $10-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f t.j register checking-2015-06-01 assets:checking $-10 $-10-\f[R]-.fi-.PP-DATE should be a simple date; if the year is not specified it will use-the year of the transaction\[aq]s date.-You can set the secondary date similarly, with \f[C]date2:DATE2\f[R].-The \f[C]date:\f[R] or \f[C]date2:\f[R] tags must have a valid simple-date value if they are present, eg a \f[C]date:\f[R] tag with no value-is not allowed.-.PP-Ledger\[aq]s earlier, more compact bracketed date syntax is also-supported: \f[C][DATE]\f[R], \f[C][DATE=DATE2]\f[R] or-\f[C][=DATE2]\f[R].-hledger will attempt to parse any square-bracketed sequence of the-\f[C]0123456789/-.=\f[R] characters in this way.-With this syntax, DATE infers its year from the transaction and DATE2-infers its year from DATE.-.SH STATUS-.PP-Transactions, or individual postings within a transaction, can have a-status mark, which is a single character before the transaction-description or posting account name, separated from it by a space,-indicating one of three statuses:-.PP-.TS-tab(@);-l l.-T{-mark \ -T}@T{-status-T}-_-T{-\ -T}@T{-unmarked-T}-T{-\f[C]!\f[R]-T}@T{-pending-T}-T{-\f[C]*\f[R]-T}@T{-cleared-T}-.TE-.PP-When reporting, you can filter by status with the-\f[C]-U/--unmarked\f[R], \f[C]-P/--pending\f[R], and-\f[C]-C/--cleared\f[R] flags; or the \f[C]status:\f[R],-\f[C]status:!\f[R], and \f[C]status:*\f[R] queries; or the U, P, C keys-in hledger-ui.-.PP-Note, in Ledger and in older versions of hledger, the \[dq]unmarked\[dq]-state is called \[dq]uncleared\[dq].-As of hledger 1.3 we have renamed it to unmarked for clarity.-.PP-To replicate Ledger and old hledger\[aq]s behaviour of also matching-pending, combine -U and -P.-.PP-Status marks are optional, but can be helpful eg for reconciling with-real-world accounts.-Some editor modes provide highlighting and shortcuts for working with-status.-Eg in Emacs ledger-mode, you can toggle transaction status with C-c C-e,-or posting status with C-c C-c.-.PP-What \[dq]uncleared\[dq], \[dq]pending\[dq], and \[dq]cleared\[dq]-actually mean is up to you.-Here\[aq]s one suggestion:-.PP-.TS-tab(@);-lw(9.7n) lw(60.3n).-T{-status-T}@T{-meaning-T}-_-T{-uncleared-T}@T{-recorded but not yet reconciled; needs review-T}-T{-pending-T}@T{-tentatively reconciled (if needed, eg during a big reconciliation)-T}-T{-cleared-T}@T{-complete, reconciled as far as possible, and considered correct-T}-.TE-.PP-With this scheme, you would use \f[C]-PC\f[R] to see the current balance-at your bank, \f[C]-U\f[R] to see things which will probably hit your-bank soon (like uncashed checks), and no flags to see the most-up-to-date state of your finances.-.SH DESCRIPTION-.PP-A transaction\[aq]s description is the rest of the line following the-date and status mark (or until a comment begins).-Sometimes called the \[dq]narration\[dq] in traditional bookkeeping, it-can be used for whatever you wish, or left blank.-Transaction descriptions can be queried, unlike comments.-.SS Payee and note-.PP-You can optionally include a \f[C]|\f[R] (pipe) character in-descriptions to subdivide the description into separate fields for-payee/payer name on the left (up to the first \f[C]|\f[R]) and an-additional note field on the right (after the first \f[C]|\f[R]).-This may be worthwhile if you need to do more precise querying and-pivoting by payee or by note.-.SH COMMENTS-.PP-Lines in the journal beginning with a semicolon (\f[C];\f[R]) or hash-(\f[C]#\f[R]) or star (\f[C]*\f[R]) are comments, and will be ignored.-(Star comments cause org-mode nodes to be ignored, allowing emacs users-to fold and navigate their journals with org-mode or orgstruct-mode.)-.PP-You can attach comments to a transaction by writing them after the-description and/or indented on the following lines (before the-postings).-Similarly, you can attach comments to an individual posting by writing-them after the amount and/or indented on the following lines.-Transaction and posting comments must begin with a semicolon-(\f[C];\f[R]).-.PP-Some examples:-.IP-.nf-\f[C]-# a file comment-; another file comment-* also a file comment, useful in org/orgstruct mode--comment-A multiline file comment, which continues-until a line containing just \[dq]end comment\[dq]-(or end of file).-end comment--2012/5/14 something ; a transaction comment- ; the transaction comment, continued- posting1 1 ; a comment for posting 1- posting2- ; a comment for posting 2- ; another comment line for posting 2-; a file comment (because not indented)-\f[R]-.fi-.PP-You can also comment larger regions of a file using \f[C]comment\f[R]-and \f[C]end comment\f[R] directives.-.SH TAGS-.PP-Tags are a way to add extra labels or labelled data to postings and-transactions, which you can then search or pivot on.-.PP-A simple tag is a word (which may contain hyphens) followed by a full-colon, written inside a transaction or posting comment line:-.IP-.nf-\f[C]-2017/1/16 bought groceries ; sometag:-\f[R]-.fi-.PP-Tags can have a value, which is the text after the colon, up to the next-comma or end of line, with leading/trailing whitespace removed:-.IP-.nf-\f[C]- expenses:food $10 ; a-posting-tag: the tag value-\f[R]-.fi-.PP-Note this means hledger\[aq]s tag values can not contain commas or-newlines.-Ending at commas means you can write multiple short tags on one line,-comma separated:-.IP-.nf-\f[C]- assets:checking ; a comment containing tag1:, tag2: some value ...-\f[R]-.fi-.PP-Here,-.IP \[bu] 2-\[dq]\f[C]a comment containing\f[R]\[dq] is just comment text, not a tag-.IP \[bu] 2-\[dq]\f[C]tag1\f[R]\[dq] is a tag with no value-.IP \[bu] 2-\[dq]\f[C]tag2\f[R]\[dq] is another tag, whose value is-\[dq]\f[C]some value ...\f[R]\[dq]-.PP-Tags in a transaction comment affect the transaction and all of its-postings, while tags in a posting comment affect only that posting.-For example, the following transaction has three tags (\f[C]A\f[R],-\f[C]TAG2\f[R], \f[C]third-tag\f[R]) and the posting has four (those-plus \f[C]posting-tag\f[R]):-.IP-.nf-\f[C]-1/1 a transaction ; A:, TAG2:- ; third-tag: a third transaction tag, <- with a value- (a) $1 ; posting-tag:-\f[R]-.fi-.PP-Tags are like Ledger\[aq]s metadata feature, except hledger\[aq]s tag-values are simple strings.-.SH POSTINGS-.PP-A posting is an addition of some amount to, or removal of some amount-from, an account.-Each posting line begins with at least one space or tab (2 or 4 spaces-is common), followed by:-.IP \[bu] 2-(optional) a status character (empty, \f[C]!\f[R], or \f[C]*\f[R]),-followed by a space-.IP \[bu] 2-(required) an account name (any text, optionally containing \f[B]single-spaces\f[R], until end of line or a double space)-.IP \[bu] 2-(optional) \f[B]two or more spaces\f[R] or tabs followed by an amount.-.PP-Positive amounts are being added to the account, negative amounts are-being removed.-.PP-The amounts within a transaction must always sum up to zero.-As a convenience, one amount may be left blank; it will be inferred so-as to balance the transaction.-.PP-Be sure to note the unusual two-space delimiter between account name and-amount.-This makes it easy to write account names containing spaces.-But if you accidentally leave only one space (or tab) before the amount,-the amount will be considered part of the account name.-.SS Virtual postings-.PP-A posting with a parenthesised account name is called a \f[I]virtual-posting\f[R] or \f[I]unbalanced posting\f[R], which means it is exempt-from the usual rule that a transaction\[aq]s postings must balance add-up to zero.-.PP-This is not part of double entry accounting, so you might choose to-avoid this feature.-Or you can use it sparingly for certain special cases where it can be-convenient.-Eg, you could set opening balances without using a balancing equity-account:-.IP-.nf-\f[C]-1/1 opening balances- (assets:checking) $1000- (assets:savings) $2000-\f[R]-.fi-.PP-A posting with a bracketed account name is called a \f[I]balanced-virtual posting\f[R].-The balanced virtual postings in a transaction must add up to zero-(separately from other postings).-Eg:-.IP-.nf-\f[C]-1/1 buy food with cash, update budget envelope subaccounts, & something else- assets:cash $-10 ; <- these balance- expenses:food $7 ; <-- expenses:food $3 ; <-- [assets:checking:budget:food] $-10 ; <- and these balance- [assets:checking:available] $10 ; <-- (something:else) $5 ; <- not required to balance-\f[R]-.fi-.PP-Ordinary non-parenthesised, non-bracketed postings are called \f[I]real-postings\f[R].-You can exclude virtual postings from reports with the-\f[C]-R/--real\f[R] flag or \f[C]real:1\f[R] query.-.SH ACCOUNT NAMES-.PP-Account names typically have several parts separated by a full colon,-from which hledger derives a hierarchical chart of accounts.-They can be anything you like, but in finance there are traditionally-five top-level accounts: \f[C]assets\f[R], \f[C]liabilities\f[R],-\f[C]income\f[R], \f[C]expenses\f[R], and \f[C]equity\f[R].-.PP-Account names may contain single spaces, eg:-\f[C]assets:accounts receivable\f[R].-Because of this, they must always be followed by \f[B]two or more-spaces\f[R] (or newline).-.PP-Account names can be aliased.-.SH AMOUNTS-.PP-After the account name, there is usually an amount.-(Important: between account name and amount, there must be \f[B]two or-more spaces\f[R].)-.PP-hledger\[aq]s amount format is flexible, supporting several-international formats.-Here are some examples.-Amounts have a number (the \[dq]quantity\[dq]):-.IP-.nf-\f[C]-1-\f[R]-.fi-.PP-\&..and usually a currency or commodity name (the \[dq]commodity\[dq]).-This is a symbol, word, or phrase, to the left or right of the quantity,-with or without a separating space:-.IP-.nf-\f[C]-$1-4000 AAPL-\f[R]-.fi-.PP-If the commodity name contains spaces, numbers, or punctuation, it must-be enclosed in double quotes:-.IP-.nf-\f[C]-3 \[dq]no. 42 green apples\[dq]-\f[R]-.fi-.PP-Amounts can be preceded by a minus sign (or a plus sign, though plus is-the default), The sign can be written before or after a left-side-commodity symbol:-.IP-.nf-\f[C]--$1-$-1-\f[R]-.fi-.PP-One or more spaces between the sign and the number are acceptable when-parsing (but they won\[aq]t be displayed in output):-.IP-.nf-\f[C]-+ $1-$- 1-\f[R]-.fi-.PP-Scientific E notation is allowed:-.IP-.nf-\f[C]-1E-6-EUR 1E3-\f[R]-.fi-.PP-A decimal mark can be written as a period or a comma:-.IP-.nf-\f[C]-1.23-1,23456780000009-\f[R]-.fi-.SS Digit group marks-.PP-In the integer part of the quantity (left of the decimal mark), groups-of digits can optionally be separated by a \[dq]digit group mark\[dq] --a space, comma, or period (different from the decimal mark):-.IP-.nf-\f[C]- $1,000,000.00- EUR 2.000.000,00-INR 9,99,99,999.00- 1 000 000.9455-\f[R]-.fi-.PP-Note, a number containing a single group mark and no decimal mark is-ambiguous.-Are these group marks or decimal marks ?-.IP-.nf-\f[C]-1,000-1.000-\f[R]-.fi-.PP-hledger will treat them both as decimal marks by default (cf #793).-If you use digit group marks, to prevent confusion and undetected typos-we recommend you write commodity directives at the top of the file to-explicitly declare the decimal mark (and optionally a digit group mark).-Note, these formats (\[dq]amount styles\[dq]) are specific to each-commodity, so if your data uses multiple formats, hledger can handle it:-.IP-.nf-\f[C]-commodity $1,000.00-commodity EUR 1.000,00-commodity INR 9,99,99,999.00-commodity 1 000 000.9455-\f[R]-.fi-.PP-.SS Commodity display style-.PP-For each commodity, hledger chooses a consistent style to use when-displaying amounts.-(Except price amounts, which are always displayed as written).-The display style is chosen as follows:-.IP \[bu] 2-If there is a commodity directive (or default commodity directive) for-the commodity, its style is used (see examples above).-.IP \[bu] 2-Otherwise the style is inferred from the amounts in that commodity seen-in the journal.-.IP \[bu] 2-Or if there are no such amounts in the journal, a default style is used-(like \f[C]$1000.00\f[R]).-.PP-A style is inferred from the journal amounts in a commodity as follows:-.IP \[bu] 2-Use the general style (decimal mark, symbol placement) of the first-amount-.IP \[bu] 2-Use the first-seen digit group style (digit group mark, digit group-sizes), if any-.IP \[bu] 2-Use the maximum number of decimal places of all.-.PP-Transaction price amounts don\[aq]t affect the commodity display style-directly, but occasionally they can do so indirectly (eg when a-posting\[aq]s amount is inferred using a transaction price).-If you find this causing problems, use a commodity directive to fix the-display style.-.PP-In summary, each commodity\[aq]s amounts will be normalised to-.IP \[bu] 2-the style declared by a \f[C]commodity\f[R] directive-.IP \[bu] 2-or, the style of the first posting amount in the journal, with the-first-seen digit group style and the maximum-seen number of decimal-places.-.PP-If reports are showing amounts in a way you don\[aq]t like (eg, with too-many decimal places), use a commodity directive to set your preferred-style.-.SS Rounding-.PP-Amounts are stored internally as decimal numbers with up to 255 decimal-places, and displayed with the number of decimal places specified by the-commodity display style.-Note, hledger uses banker\[aq]s rounding: it rounds to the nearest even-number, eg 0.5 displayed with zero decimal places is \[dq]0\[dq]).-(Guaranteed since hledger 1.17.1; in older versions this could vary if-hledger was built with Decimal < 0.5.1.)-.SH TRANSACTION PRICES-.PP-Within a transaction, you can note an amount\[aq]s price in another-commodity.-This can be used to document the cost (in a purchase) or selling price-(in a sale).-For example, transaction prices are useful to record purchases of a-foreign currency.-Note transaction prices are fixed at the time of the transaction, and do-not change over time.-See also market prices, which represent prevailing exchange rates on a-certain date.-.PP-There are several ways to record a transaction price:-.IP "1." 3-Write the price per unit, as \f[C]\[at] UNITPRICE\f[R] after the amount:-.RS 4-.IP-.nf-\f[C]-2009/1/1- assets:euros \[Eu]100 \[at] $1.35 ; one hundred euros purchased at $1.35 each- assets:dollars ; balancing amount is -$135.00-\f[R]-.fi-.RE-.IP "2." 3-Write the total price, as \f[C]\[at]\[at] TOTALPRICE\f[R] after the-amount:-.RS 4-.IP-.nf-\f[C]-2009/1/1- assets:euros \[Eu]100 \[at]\[at] $135 ; one hundred euros purchased at $135 for the lot- assets:dollars-\f[R]-.fi-.RE-.IP "3." 3-Specify amounts for all postings, using exactly two commodities, and let-hledger infer the price that balances the transaction:-.RS 4-.IP-.nf-\f[C]-2009/1/1- assets:euros \[Eu]100 ; one hundred euros purchased- assets:dollars $-135 ; for $135-\f[R]-.fi-.RE-.IP "4." 3-Like 1, but the \f[C]\[at]\f[R] is parenthesised, i.e.-\f[C](\[at])\f[R]; this is for compatibility with Ledger journals-(Virtual posting costs), and is equivalent to 1 in hledger.-.IP "5." 3-Like 2, but as in 4 the \f[C]\[at]\[at]\f[R] is parenthesised, i.e.-\f[C](\[at]\[at])\f[R]; in hledger, this is equivalent to 2.-.PP-Use the \f[C]-B/--cost\f[R] flag to convert amounts to their transaction-price\[aq]s commodity, if any.-(mnemonic: \[dq]B\[dq] is from \[dq]cost Basis\[dq], as in Ledger).-Eg here is how -B affects the balance report for the example above:-.IP-.nf-\f[C]-$ hledger bal -N --flat- $-135 assets:dollars- \[Eu]100 assets:euros-$ hledger bal -N --flat -B- $-135 assets:dollars- $135 assets:euros # <- the euros\[aq] cost-\f[R]-.fi-.PP-Note -B is sensitive to the order of postings when a transaction price-is inferred: the inferred price will be in the commodity of the last-amount.-So if example 3\[aq]s postings are reversed, while the transaction is-equivalent, -B shows something different:-.IP-.nf-\f[C]-2009/1/1- assets:dollars $-135 ; 135 dollars sold- assets:euros \[Eu]100 ; for 100 euros-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger bal -N --flat -B- \[Eu]-100 assets:dollars # <- the dollars\[aq] selling price- \[Eu]100 assets:euros-\f[R]-.fi-.SH LOT PRICES, LOT DATES-.PP-Ledger allows another kind of price, lot price (four variants:-\f[C]{UNITPRICE}\f[R], \f[C]{{TOTALPRICE}}\f[R],-\f[C]{=FIXEDUNITPRICE}\f[R], \f[C]{{=FIXEDTOTALPRICE}}\f[R]), and/or a-lot date (\f[C][DATE]\f[R]) to be specified.-These are normally used to select a lot when selling investments.-hledger will parse these, for compatibility with Ledger journals, but-currently ignores them.-A transaction price, lot price and/or lot date may appear in any order,-after the posting amount and before the balance assertion if any.-.SH BALANCE ASSERTIONS-.PP-hledger supports Ledger-style balance assertions in journal files.-These look like, for example, \f[C]= EXPECTEDBALANCE\f[R] following a-posting\[aq]s amount.-Eg here we assert the expected dollar balance in accounts a and b after-each posting:-.IP-.nf-\f[C]-2013/1/1- a $1 =$1- b =$-1--2013/1/2- a $1 =$2- b $-1 =$-2-\f[R]-.fi-.PP-After reading a journal file, hledger will check all balance assertions-and report an error if any of them fail.-Balance assertions can protect you from, eg, inadvertently disrupting-reconciled balances while cleaning up old entries.-You can disable them temporarily with the-\f[C]-I/--ignore-assertions\f[R] flag, which can be useful for-troubleshooting or for reading Ledger files.-(Note: this flag currently does not disable balance assignments, below).-.SS Assertions and ordering-.PP-hledger sorts an account\[aq]s postings and assertions first by date and-then (for postings on the same day) by parse order.-Note this is different from Ledger, which sorts assertions only by parse-order.-(Also, Ledger assertions do not see the accumulated effect of repeated-postings to the same account within a transaction.)-.PP-So, hledger balance assertions keep working if you reorder-differently-dated transactions within the journal.-But if you reorder same-dated transactions or postings, assertions might-break and require updating.-This order dependence does bring an advantage: precise control over the-order of postings and assertions within a day, so you can assert-intra-day balances.-.SS Assertions and included files-.PP-With included files, things are a little more complicated.-Including preserves the ordering of postings and assertions.-If you have multiple postings to an account on the same day, split-across different files, and you also want to assert the account\[aq]s-balance on the same day, you\[aq]ll have to put the assertion in the-right file.-.SS Assertions and multiple -f options-.PP-Balance assertions don\[aq]t work well across files specified with-multiple -f options.-Use include or concatenate the files instead.-.SS Assertions and commodities-.PP-The asserted balance must be a simple single-commodity amount, and in-fact the assertion checks only this commodity\[aq]s balance within the-(possibly multi-commodity) account balance.-This is how assertions work in Ledger also.-We could call this a \[dq]partial\[dq] balance assertion.-.PP-To assert the balance of more than one commodity in an account, you can-write multiple postings, each asserting one commodity\[aq]s balance.-.PP-You can make a stronger \[dq]total\[dq] balance assertion by writing a-double equals sign (\f[C]== EXPECTEDBALANCE\f[R]).-This asserts that there are no other unasserted commodities in the-account (or, that their balance is 0).-.IP-.nf-\f[C]-2013/1/1- a $1- a 1\[Eu]- b $-1- c -1\[Eu]--2013/1/2 ; These assertions succeed- a 0 = $1- a 0 = 1\[Eu]- b 0 == $-1- c 0 == -1\[Eu]--2013/1/3 ; This assertion fails as \[aq]a\[aq] also contains 1\[Eu]- a 0 == $1-\f[R]-.fi-.PP-It\[aq]s not yet possible to make a complete assertion about a balance-that has multiple commodities.-One workaround is to isolate each commodity into its own subaccount:-.IP-.nf-\f[C]-2013/1/1- a:usd $1- a:euro 1\[Eu]- b--2013/1/2- a 0 == 0- a:usd 0 == $1- a:euro 0 == 1\[Eu]-\f[R]-.fi-.SS Assertions and prices-.PP-Balance assertions ignore transaction prices, and should normally be-written without one:-.IP-.nf-\f[C]-2019/1/1- (a) $1 \[at] \[Eu]1 = $1-\f[R]-.fi-.PP-We do allow prices to be written there, however, and print shows them,-even though they don\[aq]t affect whether the assertion passes or fails.-This is for backward compatibility (hledger\[aq]s close command used to-generate balance assertions with prices), and because balance-\f[I]assignments\f[R] do use them (see below).-.SS Assertions and subaccounts-.PP-The balance assertions above (\f[C]=\f[R] and \f[C]==\f[R]) do not count-the balance from subaccounts; they check the account\[aq]s exclusive-balance only.-You can assert the balance including subaccounts by writing \f[C]=*\f[R]-or \f[C]==*\f[R], eg:-.IP-.nf-\f[C]-2019/1/1- equity:opening balances- checking:a 5- checking:b 5- checking 1 ==* 11-\f[R]-.fi-.SS Assertions and virtual postings-.PP-Balance assertions are checked against all postings, both real and-virtual.-They are not affected by the \f[C]--real/-R\f[R] flag or \f[C]real:\f[R]-query.-.SS Assertions and precision-.PP-Balance assertions compare the exactly calculated amounts, which are not-always what is shown by reports.-Eg a commodity directive may limit the display precision, but this will-not affect balance assertions.-Balance assertion failure messages show exact amounts.-.SH BALANCE ASSIGNMENTS-.PP-Ledger-style balance assignments are also supported.-These are like balance assertions, but with no posting amount on the-left side of the equals sign; instead it is calculated automatically so-as to satisfy the assertion.-This can be a convenience during data entry, eg when setting opening-balances:-.IP-.nf-\f[C]-; starting a new journal, set asset account balances-2016/1/1 opening balances- assets:checking = $409.32- assets:savings = $735.24- assets:cash = $42- equity:opening balances-\f[R]-.fi-.PP-or when adjusting a balance to reality:-.IP-.nf-\f[C]-; no cash left; update balance, record any untracked spending as a generic expense-2016/1/15- assets:cash = $0- expenses:misc-\f[R]-.fi-.PP-The calculated amount depends on the account\[aq]s balance in the-commodity at that point (which depends on the previously-dated postings-of the commodity to that account since the last balance assertion or-assignment).-Note that using balance assignments makes your journal a little less-explicit; to know the exact amount posted, you have to run hledger or do-the calculations yourself, instead of just reading it.-.SS Balance assignments and prices-.PP-A transaction price in a balance assignment will cause the calculated-amount to have that price attached:-.IP-.nf-\f[C]-2019/1/1- (a) = $1 \[at] \[Eu]2-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger print --explicit-2019-01-01- (a) $1 \[at] \[Eu]2 = $1 \[at] \[Eu]2-\f[R]-.fi-.SH DIRECTIVES-.PP-A directive is a line in the journal beginning with a special keyword,-that influences how the journal is processed.-hledger\[aq]s directives are based on a subset of Ledger\[aq]s, but-there are many differences (and also some differences between hledger-versions).-.PP-Directives\[aq] behaviour and interactions can get a little bit complex,-so here is a table summarising the directives and their effects, with-links to more detailed docs.-Note part of this table is hidden when viewed in a web browser - scroll-it sideways to see more.-.PP-.TS-tab(@);-lw(7.8n) lw(8.6n) lw(7.0n) lw(27.8n) lw(18.8n).-T{-directive-T}@T{-end directive-T}@T{-subdirectives-T}@T{-purpose-T}@T{-can affect (as of 2018/06)-T}-_-T{-\f[C]account\f[R]-T}@T{-T}@T{-any text-T}@T{-document account names, declare account types & display order-T}@T{-all entries in all files, before or after-T}-T{-\f[C]alias\f[R]-T}@T{-\f[C]end aliases\f[R]-T}@T{-T}@T{-rewrite account names-T}@T{-following entries until end of current file or end directive-T}-T{-\f[C]apply account\f[R]-T}@T{-\f[C]end apply account\f[R]-T}@T{-T}@T{-prepend a common parent to account names-T}@T{-following entries until end of current file or end directive-T}-T{-\f[C]comment\f[R]-T}@T{-\f[C]end comment\f[R]-T}@T{-T}@T{-ignore part of journal-T}@T{-following entries until end of current file or end directive-T}-T{-\f[C]commodity\f[R]-T}@T{-T}@T{-\f[C]format\f[R]-T}@T{-declare a commodity and its number notation & display style-T}@T{-number notation: following entries in that commodity in all files ;-display style: amounts of that commodity in reports-T}-T{-\f[C]D\f[R]-T}@T{-T}@T{-T}@T{-declare a commodity to be used for commodityless amounts, and its number-notation & display style-T}@T{-default commodity: following commodityless entries until end of current-file; number notation: following entries in that commodity until end of-current file; display style: amounts of that commodity in reports-T}-T{-\f[C]include\f[R]-T}@T{-T}@T{-T}@T{-include entries/directives from another file-T}@T{-what the included directives affect-T}-T{-\f[C]P\f[R]-T}@T{-T}@T{-T}@T{-declare a market price for a commodity-T}@T{-amounts of that commodity in reports, when -V is used-T}-T{-\f[C]Y\f[R]-T}@T{-T}@T{-T}@T{-declare a year for yearless dates-T}@T{-following entries until end of current file-T}-T{-\f[C]=\f[R]-T}@T{-T}@T{-T}@T{-declare an auto posting rule, adding postings to other transactions-T}@T{-all entries in parent/current/child files (but not sibling files, see-#1212)-T}-.TE-.PP-And some definitions:-.PP-.TS-tab(@);-lw(6.0n) lw(64.0n).-T{-subdirective-T}@T{-optional indented directive line immediately following a parent-directive-T}-T{-number notation-T}@T{-how to interpret numbers when parsing journal entries (the identity of-the decimal separator character).-(Currently each commodity can have its own notation, even in the same-file.)-T}-T{-display style-T}@T{-how to display amounts of a commodity in reports (symbol side and-spacing, digit groups, decimal separator, decimal places)-T}-T{-directive scope-T}@T{-which entries and (when there are multiple files) which files are-affected by a directive-T}-.TE-.PP-As you can see, directives vary in which journal entries and files they-affect, and whether they are focussed on input (parsing) or output-(reports).-Some directives have multiple effects.-.SS Directives and multiple files-.PP-If you use multiple \f[C]-f\f[R]/\f[C]--file\f[R] options, or the-\f[C]include\f[R] directive, hledger will process multiple input files.-But note that directives which affect input (see above) typically last-only until the end of the file in which they occur.-.PP-This may seem inconvenient, but it\[aq]s intentional; it makes reports-stable and deterministic, independent of the order of input.-Otherwise you could see different numbers if you happened to write -f-options in a different order, or if you moved includes around while-cleaning up your files.-.PP-It can be surprising though; for example, it means that \f[C]alias\f[R]-directives do not affect parent or sibling files (see below).-.SS Comment blocks-.PP-A line containing just \f[C]comment\f[R] starts a commented region of-the file, and a line containing just \f[C]end comment\f[R] (or the end-of the current file) ends it.-See also comments.-.SS Including other files-.PP-You can pull in the content of additional files by writing an include-directive, like this:-.IP-.nf-\f[C]-include FILEPATH-\f[R]-.fi-.PP-Only journal files can include, and only journal, timeclock or timedot-files can be included (not CSV files, currently).-.PP-If the file path does not begin with a slash, it is relative to the-current file\[aq]s folder.-.PP-A tilde means home directory, eg: \f[C]include \[ti]/main.journal\f[R].-.PP-The path may contain glob patterns to match multiple files, eg:-\f[C]include *.journal\f[R].-.PP-There is limited support for recursive wildcards: \f[C]**/\f[R] (the-slash is required) matches 0 or more subdirectories.-It\[aq]s not super convenient since you have to avoid include cycles and-including directories, but this can be done, eg:-\f[C]include */**/*.journal\f[R].-.PP-The path may also be prefixed to force a specific file format,-overriding the file extension (as described in hledger.1 -> Input-files): \f[C]include timedot:\[ti]/notes/2020*.md\f[R].-.SS Default year-.PP-You can set a default year to be used for subsequent dates which-don\[aq]t specify a year.-This is a line beginning with \f[C]Y\f[R] followed by the year.-Eg:-.IP-.nf-\f[C]-Y2009 ; set default year to 2009--12/15 ; equivalent to 2009/12/15- expenses 1- assets--Y2010 ; change default year to 2010--2009/1/30 ; specifies the year, not affected- expenses 1- assets--1/31 ; equivalent to 2010/1/31- expenses 1- assets-\f[R]-.fi-.SS Declaring commodities-.PP-The \f[C]commodity\f[R] directive has several functions:-.IP "1." 3-It declares commodities which may be used in the journal.-This is currently not enforced, but can serve as documentation.-.IP "2." 3-It declares what decimal mark character (period or comma) to expect when-parsing input - useful to disambiguate international number formats in-your data.-(Without this, hledger will parse both \f[C]1,000\f[R] and-\f[C]1.000\f[R] as 1).-.IP "3." 3-It declares a commodity\[aq]s display style in output - decimal and-digit group marks, number of decimal places, symbol placement etc.-.PP-You are likely to run into one of the problems solved by commodity-directives, sooner or later, so it\[aq]s a good idea to just always use-them to declare your commodities.-.PP-A commodity directive is just the word \f[C]commodity\f[R] followed by-an amount.-It may be written on a single line, like this:-.IP-.nf-\f[C]-; commodity EXAMPLEAMOUNT--; display AAAA amounts with the symbol on the right, space-separated,-; using period as decimal point, with four decimal places, and-; separating thousands with comma.-commodity 1,000.0000 AAAA-\f[R]-.fi-.PP-or on multiple lines, using the \[dq]format\[dq] subdirective.-(In this case the commodity symbol appears twice and should be the same-in both places.):-.IP-.nf-\f[C]-; commodity SYMBOL-; format EXAMPLEAMOUNT--; display indian rupees with currency name on the left,-; thousands, lakhs and crores comma-separated,-; period as decimal point, and two decimal places.-commodity INR- format INR 1,00,00,000.00-\f[R]-.fi-.PP-The quantity of the amount does not matter; only the format is-significant.-The number must include a decimal mark: either a period or a comma,-followed by 0 or more decimal digits.-.PP-Note hledger normally uses banker\[aq]s rounding, so 0.5 displayed with-zero decimal digits is \[dq]0\[dq].-(More at Commodity display style.)-.SS Commodity error checking-.PP-In strict mode, enabled with the \f[C]-s\f[R]/\f[C]--strict\f[R] flag,-hledger will report an error if a commodity symbol is used that has not-been declared by a \f[C]commodity\f[R] directive.-This works similarly to account error checking, see the notes there for-more details.-.SS Default commodity-.PP-The \f[C]D\f[R] directive sets a default commodity, to be used for-amounts without a commodity symbol (ie, plain numbers).-This commodity will be applied to all subsequent commodity-less amounts,-or until the next \f[C]D\f[R] directive.-(Note, this is different from Ledger\[aq]s \f[C]D\f[R].)-.PP-For compatibility/historical reasons, \f[C]D\f[R] also acts like a-\f[C]commodity\f[R] directive, setting the commodity\[aq]s display style-(for output) and decimal mark (for parsing input).-As with \f[C]commodity\f[R], the amount must always be written with a-decimal mark (period or comma).-If both directives are used, \f[C]commodity\f[R]\[aq]s style takes-precedence.-.PP-The syntax is \f[C]D AMOUNT\f[R].-Eg:-.IP-.nf-\f[C]-; commodity-less amounts should be treated as dollars-; (and displayed with the dollar sign on the left, thousands separators and two decimal places)-D $1,000.00--1/1- a 5 ; <- commodity-less amount, parsed as $5 and displayed as $5.00- b-\f[R]-.fi-.SS Declaring market prices-.PP-The \f[C]P\f[R] directive declares a market price, which is an exchange-rate between two commodities on a certain date.-(In Ledger, they are called \[dq]historical prices\[dq].) These are-often obtained from a stock exchange, cryptocurrency exchange, or the-foreign exchange market.-.PP-Here is the format:-.IP-.nf-\f[C]-P DATE COMMODITYA COMMODITYBAMOUNT-\f[R]-.fi-.IP \[bu] 2-DATE is a simple date-.IP \[bu] 2-COMMODITYA is the symbol of the commodity being priced-.IP \[bu] 2-COMMODITYBAMOUNT is an amount (symbol and quantity) in a second-commodity, giving the price in commodity B of one unit of commodity A.-.PP-These two market price directives say that one euro was worth 1.35 US-dollars during 2009, and $1.40 from 2010 onward:-.IP-.nf-\f[C]-P 2009/1/1 \[Eu] $1.35-P 2010/1/1 \[Eu] $1.40-\f[R]-.fi-.PP-The \f[C]-V\f[R], \f[C]-X\f[R] and \f[C]--value\f[R] flags use these-market prices to show amount values in another commodity.-See Valuation.-.SS Declaring accounts-.PP-\f[C]account\f[R] directives can be used to declare accounts (ie, the-places that amounts are transferred from and to).-Though not required, these declarations can provide several benefits:-.IP \[bu] 2-They can document your intended chart of accounts, providing a-reference.-.IP \[bu] 2-They can help hledger know your accounts\[aq] types (asset, liability,-equity, revenue, expense), useful for reports like balancesheet and-incomestatement.-.IP \[bu] 2-They control account display order in reports, allowing non-alphabetic-sorting (eg Revenues to appear above Expenses).-.IP \[bu] 2-They can store extra information about accounts (account numbers, notes,-etc.)-.IP \[bu] 2-They help with account name completion in the add command, hledger-iadd,-hledger-web, ledger-mode etc.-.IP \[bu] 2-In strict mode, they restrict which accounts may be posted to by-transactions, which helps detect typos.-.PP-The simplest form is just the word \f[C]account\f[R] followed by a-hledger-style account name, eg this account directive declares the-\f[C]assets:bank:checking\f[R] account:-.IP-.nf-\f[C]-account assets:bank:checking-\f[R]-.fi-.SS Account error checking-.PP-By default, accounts come into existence when a transaction references-them by name.-This is convenient, but it means hledger can\[aq]t warn you when you-mis-spell an account name in the journal.-Usually you\[aq]ll find the error later, as an extra account in balance-reports, or an incorrect balance when reconciling.-.PP-In strict mode, enabled with the \f[C]-s\f[R]/\f[C]--strict\f[R] flag,-hledger will report an error if any transaction uses an account name-that has not been declared by an account directive.-Some notes:-.IP \[bu] 2-The declaration is case-sensitive; transactions must use the correct-account name capitalisation.-.IP \[bu] 2-The account directive\[aq]s scope is \[dq]whole file and below\[dq] (see-directives).-This means it affects all of the current file, and any files it-includes, but not parent or sibling files.-The position of account directives within the file does not matter,-though it\[aq]s usual to put them at the top.-.IP \[bu] 2-Accounts can only be declared in \f[C]journal\f[R] files (but will-affect included files in other formats).-.IP \[bu] 2-It\[aq]s currently not possible to declare \[dq]all possible-subaccounts\[dq] with a wildcard; every account posted to must be-declared.-.SS Account comments-.PP-Comments, beginning with a semicolon, can be added:-.IP \[bu] 2-on the same line, \f[B]after two or more spaces\f[R] (because ; is-allowed in account names)-.IP \[bu] 2-on the next lines, indented-.PP-An example of both:-.IP-.nf-\f[C]-account assets:bank:checking ; same-line comment, note 2+ spaces before ;- ; next-line comment- ; another with tag, acctno:12345 (not used yet)-\f[R]-.fi-.PP-Same-line comments are not supported by Ledger, or hledger <1.13.-.SS Account subdirectives-.PP-We also allow (and ignore) Ledger-style indented subdirectives, just for-compatibility.:-.IP-.nf-\f[C]-account assets:bank:checking- format blah blah ; <- subdirective, ignored-\f[R]-.fi-.PP-Here is the full syntax of account directives:-.IP-.nf-\f[C]-account ACCTNAME [ACCTTYPE] [;COMMENT]- [;COMMENTS]- [LEDGER-STYLE SUBDIRECTIVES, IGNORED]-\f[R]-.fi-.SS Account types-.PP-hledger recognises five main types of account, corresponding to the-account classes in the accounting equation:-.PP-\f[C]Asset\f[R], \f[C]Liability\f[R], \f[C]Equity\f[R],-\f[C]Revenue\f[R], \f[C]Expense\f[R].-.PP-These account types are important for controlling which accounts appear-in the balancesheet, balancesheetequity, incomestatement reports (and-probably for other things in future).-.PP-Additionally, we recognise the \f[C]Cash\f[R] type, which is also an-\f[C]Asset\f[R], and which causes accounts to appear in the cashflow-report.-(\[dq]Cash\[dq] here means liquid assets, eg bank balances but typically-not investments or receivables.)-.SS Declaring account types-.PP-Generally, to make these reports work you should declare your top-level-accounts and their types, using account directives with \f[C]type:\f[R]-tags.-.PP-The tag\[aq]s value should be one of: \f[C]Asset\f[R],-\f[C]Liability\f[R], \f[C]Equity\f[R], \f[C]Revenue\f[R],-\f[C]Expense\f[R], \f[C]Cash\f[R], \f[C]A\f[R], \f[C]L\f[R],-\f[C]E\f[R], \f[C]R\f[R], \f[C]X\f[R], \f[C]C\f[R] (all case-insensitive).-The type is inherited by all subaccounts except where they override it.-Here\[aq]s a complete example:-.IP-.nf-\f[C]-account assets ; type: Asset-account assets:bank ; type: Cash-account assets:cash ; type: Cash-account liabilities ; type: Liability-account equity ; type: Equity-account revenues ; type: Revenue-account expenses ; type: Expense-\f[R]-.fi-.SS Auto-detected account types-.PP-If you happen to use common english top-level account names, you may not-need to declare account types, as they will be detected automatically-using the following rules:-.PP-.TS-tab(@);-l l.-T{-If name matches regular expression:-T}@T{-account type is:-T}-_-T{-\f[C]\[ha]assets?(:|$)\f[R]-T}@T{-\f[C]Asset\f[R]-T}-T{-\f[C]\[ha](debts?|liabilit(y|ies))(:|$)\f[R]-T}@T{-\f[C]Liability\f[R]-T}-T{-\f[C]\[ha]equity(:|$)\f[R]-T}@T{-\f[C]Equity\f[R]-T}-T{-\f[C]\[ha](income|revenue)s?(:|$)\f[R]-T}@T{-\f[C]Revenue\f[R]-T}-T{-\f[C]\[ha]expenses?(:|$)\f[R]-T}@T{-\f[C]Expense\f[R]-T}-.TE-.PP-.TS-tab(@);-lw(56.9n) lw(13.1n).-T{-If account type is \f[C]Asset\f[R] and name does not contain regular-expression:-T}@T{-account type is:-T}-_-T{-\f[C](investment|receivable|:A/R|:fixed)\f[R]-T}@T{-\f[C]Cash\f[R]-T}-.TE-.PP-Even so, explicit declarations may be a good idea, for clarity and-predictability.-.SS Interference from auto-detected account types-.PP-If you assign any account type, it\[aq]s a good idea to assign all of-them, to prevent any confusion from mixing declared and auto-detected-types.-Although it\[aq]s unlikely to happen in real life, here\[aq]s an-example: with the following journal, \f[C]balancesheetequity\f[R] shows-\[dq]liabilities\[dq] in both Liabilities and Equity sections.-Declaring another account as \f[C]type:Liability\f[R] would fix it:-.IP-.nf-\f[C]-account liabilities ; type:Equity--2020-01-01- assets 1- liabilities 1- equity -2-\f[R]-.fi-.SS Old account type syntax-.PP-In some hledger journals you might instead see this old syntax (the-letters ALERX, separated from the account name by two or more spaces);-this is deprecated and may be removed soon:-.IP-.nf-\f[C]-account assets A-account liabilities L-account equity E-account revenues R-account expenses X-\f[R]-.fi-.SS Account display order-.PP-Account directives also set the order in which accounts are displayed,-eg in reports, the hledger-ui accounts screen, and the hledger-web-sidebar.-By default accounts are listed in alphabetical order.-But if you have these account directives in the journal:-.IP-.nf-\f[C]-account assets-account liabilities-account equity-account revenues-account expenses-\f[R]-.fi-.PP-you\[aq]ll see those accounts displayed in declaration order, not-alphabetically:-.IP-.nf-\f[C]-$ hledger accounts -1-assets-liabilities-equity-revenues-expenses-\f[R]-.fi-.PP-Undeclared accounts, if any, are displayed last, in alphabetical order.-.PP-Note that sorting is done at each level of the account tree (within each-group of sibling accounts under the same parent).-And currently, this directive:-.IP-.nf-\f[C]-account other:zoo-\f[R]-.fi-.PP-would influence the position of \f[C]zoo\f[R] among-\f[C]other\f[R]\[aq]s subaccounts, but not the position of-\f[C]other\f[R] among the top-level accounts.-This means:-.IP \[bu] 2-you will sometimes declare parent accounts (eg \f[C]account other\f[R]-above) that you don\[aq]t intend to post to, just to customize their-display order-.IP \[bu] 2-sibling accounts stay together (you couldn\[aq]t display \f[C]x:y\f[R]-in between \f[C]a:b\f[R] and \f[C]a:c\f[R]).-.SS Rewriting accounts-.PP-You can define account alias rules which rewrite your account names, or-parts of them, before generating reports.-This can be useful for:-.IP \[bu] 2-expanding shorthand account names to their full form, allowing easier-data entry and a less verbose journal-.IP \[bu] 2-adapting old journals to your current chart of accounts-.IP \[bu] 2-experimenting with new account organisations, like a new hierarchy or-combining two accounts into one-.IP \[bu] 2-customising reports-.PP-Account aliases also rewrite account names in account directives.-They do not affect account names being entered via hledger add or-hledger-web.-.PP-See also Rewrite account names.-.SS Basic aliases-.PP-To set an account alias, use the \f[C]alias\f[R] directive in your-journal file.-This affects all subsequent journal entries in the current file or its-included files.-The spaces around the = are optional:-.IP-.nf-\f[C]-alias OLD = NEW-\f[R]-.fi-.PP-Or, you can use the \f[C]--alias \[aq]OLD=NEW\[aq]\f[R] option on the-command line.-This affects all entries.-It\[aq]s useful for trying out aliases interactively.-.PP-OLD and NEW are case sensitive full account names.-hledger will replace any occurrence of the old account name with the new-one.-Subaccounts are also affected.-Eg:-.IP-.nf-\f[C]-alias checking = assets:bank:wells fargo:checking-; rewrites \[dq]checking\[dq] to \[dq]assets:bank:wells fargo:checking\[dq], or \[dq]checking:a\[dq] to \[dq]assets:bank:wells fargo:checking:a\[dq]-\f[R]-.fi-.SS Regex aliases-.PP-There is also a more powerful variant that uses a regular expression,-indicated by the forward slashes:-.IP-.nf-\f[C]-alias /REGEX/ = REPLACEMENT-\f[R]-.fi-.PP-or \f[C]--alias \[aq]/REGEX/=REPLACEMENT\[aq]\f[R].-.PP-REGEX is a case-insensitive regular expression.-Anywhere it matches inside an account name, the matched part will be-replaced by REPLACEMENT.-If REGEX contains parenthesised match groups, these can be referenced by-the usual numeric backreferences in REPLACEMENT.-Eg:-.IP-.nf-\f[C]-alias /\[ha](.+):bank:([\[ha]:]+):(.*)/ = \[rs]1:\[rs]2 \[rs]3-; rewrites \[dq]assets:bank:wells fargo:checking\[dq] to \[dq]assets:wells fargo checking\[dq]-\f[R]-.fi-.PP-Also note that REPLACEMENT continues to the end of line (or on command-line, to end of option argument), so it can contain trailing whitespace.-.SS Combining aliases-.PP-You can define as many aliases as you like, using journal directives-and/or command line options.-.PP-Recursive aliases - where an account name is rewritten by one alias,-then by another alias, and so on - are allowed.-Each alias sees the effect of previously applied aliases.-.PP-In such cases it can be important to understand which aliases will be-applied and in which order.-For (each account name in) each journal entry, we apply:-.IP "1." 3-\f[C]alias\f[R] directives preceding the journal entry, most recently-parsed first (ie, reading upward from the journal entry, bottom to top)-.IP "2." 3-\f[C]--alias\f[R] options, in the order they appeared on the command-line (left to right).-.PP-In other words, for (an account name in) a given journal entry:-.IP \[bu] 2-the nearest alias declaration before/above the entry is applied first-.IP \[bu] 2-the next alias before/above that will be be applied next, and so on-.IP \[bu] 2-aliases defined after/below the entry do not affect it.-.PP-This gives nearby aliases precedence over distant ones, and helps-provide semantic stability - aliases will keep working the same way-independent of which files are being read and in which order.-.PP-In case of trouble, adding \f[C]--debug=6\f[R] to the command line will-show which aliases are being applied when.-.SS Aliases and multiple files-.PP-As explained at Directives and multiple files, \f[C]alias\f[R]-directives do not affect parent or sibling files.-Eg in this command,-.IP-.nf-\f[C]-hledger -f a.aliases -f b.journal-\f[R]-.fi-.PP-account aliases defined in a.aliases will not affect b.journal.-Including the aliases doesn\[aq]t work either:-.IP-.nf-\f[C]-include a.aliases--2020-01-01 ; not affected by a.aliases- foo 1- bar-\f[R]-.fi-.PP-This means that account aliases should usually be declared at the start-of your top-most file, like this:-.IP-.nf-\f[C]-alias foo=Foo-alias bar=Bar--2020-01-01 ; affected by aliases above- foo 1- bar--include c.journal ; also affected-\f[R]-.fi-.SS \f[C]end aliases\f[R]-.PP-You can clear (forget) all currently defined aliases with the-\f[C]end aliases\f[R] directive:-.IP-.nf-\f[C]-end aliases-\f[R]-.fi-.SS Default parent account-.PP-You can specify a parent account which will be prepended to all accounts-within a section of the journal.-Use the \f[C]apply account\f[R] and \f[C]end apply account\f[R]-directives like so:-.IP-.nf-\f[C]-apply account home--2010/1/1- food $10- cash--end apply account-\f[R]-.fi-.PP-which is equivalent to:-.IP-.nf-\f[C]-2010/01/01- home:food $10- home:cash $-10-\f[R]-.fi-.PP-If \f[C]end apply account\f[R] is omitted, the effect lasts to the end-of the file.-Included files are also affected, eg:-.IP-.nf-\f[C]-apply account business-include biz.journal-end apply account-apply account personal-include personal.journal-\f[R]-.fi-.PP-Prior to hledger 1.0, legacy \f[C]account\f[R] and \f[C]end\f[R]-spellings were also supported.-.PP-A default parent account also affects account directives.-It does not affect account names being entered via hledger add or-hledger-web.-If account aliases are present, they are applied after the default-parent account.-.SH PERIODIC TRANSACTIONS-.PP-Periodic transaction rules describe transactions that recur.-They allow hledger to generate temporary future transactions to help-with forecasting, so you don\[aq]t have to write out each one in the-journal, and it\[aq]s easy to try out different forecasts.-.PP-Periodic transactions can be a little tricky, so before you use them,-read this whole section - or at least these tips:-.IP "1." 3-Two spaces accidentally added or omitted will cause you trouble - read-about this below.-.IP "2." 3-For troubleshooting, show the generated transactions with-\f[C]hledger print --forecast tag:generated\f[R] or-\f[C]hledger register --forecast tag:generated\f[R].-.IP "3." 3-Forecasted transactions will begin only after the last non-forecasted-transaction\[aq]s date.-.IP "4." 3-Forecasted transactions will end 6 months from today, by default.-See below for the exact start/end rules.-.IP "5." 3-period expressions can be tricky.-Their documentation needs improvement, but is worth studying.-.IP "6." 3-Some period expressions with a repeating interval must begin on a-natural boundary of that interval.-Eg in \f[C]weekly from DATE\f[R], DATE must be a monday.-\f[C]\[ti] weekly from 2019/10/1\f[R] (a tuesday) will give an error.-.IP "7." 3-Other period expressions with an interval are automatically expanded to-cover a whole number of that interval.-(This is done to improve reports, but it also affects periodic-transactions.-Yes, it\[aq]s a bit inconsistent with the above.) Eg:-\f[C]\[ti] every 10th day of month from 2020/01\f[R], which is-equivalent to \f[C]\[ti] every 10th day of month from 2020/01/01\f[R],-will be adjusted to start on 2019/12/10.-.PP-Periodic transaction rules also have a second meaning: they are used to-define budget goals, shown in budget reports.-.SS Periodic rule syntax-.PP-A periodic transaction rule looks like a normal journal entry, with the-date replaced by a tilde (\f[C]\[ti]\f[R]) followed by a period-expression (mnemonic: \f[C]\[ti]\f[R] looks like a recurring sine-wave.):-.IP-.nf-\f[C]-\[ti] monthly- expenses:rent $2000- assets:bank:checking-\f[R]-.fi-.PP-There is an additional constraint on the period expression: the start-date must fall on a natural boundary of the interval.-Eg \f[C]monthly from 2018/1/1\f[R] is valid, but-\f[C]monthly from 2018/1/15\f[R] is not.-.PP-Partial or relative dates (M/D, D, tomorrow, last week) in the period-expression can work (useful or not).-They will be relative to today\[aq]s date, unless a Y default year-directive is in effect, in which case they will be relative to Y/1/1.-.SS Two spaces between period expression and description!-.PP-If the period expression is followed by a transaction description, these-must be separated by \f[B]two or more spaces\f[R].-This helps hledger know where the period expression ends, so that-descriptions can not accidentally alter their meaning, as in this-example:-.IP-.nf-\f[C]-; 2 or more spaces needed here, so the period is not understood as \[dq]every 2 months in 2020\[dq]-; ||-; vv-\[ti] every 2 months in 2020, we will review- assets:bank:checking $1500- income:acme inc-\f[R]-.fi-.PP-So,-.IP \[bu] 2-Do write two spaces between your period expression and your transaction-description, if any.-.IP \[bu] 2-Don\[aq]t accidentally write two spaces in the middle of your period-expression.-.SS Forecasting with periodic transactions-.PP-The \f[C]--forecast\f[R] flag activates any periodic transaction rules-in the journal.-They will generate temporary recurring transactions, which are not saved-in the journal, but will appear in all reports (eg print).-This can be useful for estimating balances into the future, or-experimenting with different scenarios.-Or, it can be used as a data entry aid: describe recurring transactions,-and every so often copy the output of \f[C]print --forecast\f[R] into-the journal.-.PP-These transactions will have an extra tag indicating which periodic rule-generated them: \f[C]generated-transaction:\[ti] PERIODICEXPR\f[R].-And a similar, hidden tag (beginning with an underscore) which, because-it\[aq]s never displayed by print, can be used to match transactions-generated \[dq]just now\[dq]:-\f[C]_generated-transaction:\[ti] PERIODICEXPR\f[R].-.PP-Periodic transactions are generated within some forecast period.-By default, this-.IP \[bu] 2-begins on the later of-.RS 2-.IP \[bu] 2-the report start date if specified with -b/-p/date:-.IP \[bu] 2-the day after the latest normal (non-periodic) transaction in the-journal, or today if there are no normal transactions.-.RE-.IP \[bu] 2-ends on the report end date if specified with -e/-p/date:, or 6 months-(180 days) from today.-.PP-This means that periodic transactions will begin only after the latest-recorded transaction.-And a recorded transaction dated in the future can prevent generation of-periodic transactions.-(You can avoid that by writing the future transaction as a one-time-periodic rule instead - put tilde before the date, eg-\f[C]\[ti] YYYY-MM-DD ...\f[R]).-.PP-Or, you can set your own arbitrary \[dq]forecast period\[dq], which can-overlap recorded transactions, and need not be in the future, by-providing an option argument, like \f[C]--forecast=PERIODEXPR\f[R].-Note the equals sign is required, a space won\[aq]t work.-PERIODEXPR is a period expression, which can specify the start date, end-date, or both, like in a \f[C]date:\f[R] query.-(See also hledger.1 -> Report start & end date).-Some examples: \f[C]--forecast=202001-202004\f[R],-\f[C]--forecast=jan-\f[R], \f[C]--forecast=2020\f[R].-.SS Budgeting with periodic transactions-.PP-With the \f[C]--budget\f[R] flag, currently supported by the balance-command, each periodic transaction rule declares recurring budget goals-for the specified accounts.-Eg the first example above declares a goal of spending $2000 on rent-(and also, a goal of depositing $2000 into checking) every month.-Goals and actual performance can then be compared in budget reports.-.PP-See also: Budgeting and Forecasting.-.PP-.SH AUTO POSTINGS-.PP-\[dq]Automated postings\[dq] or \[dq]auto postings\[dq] are extra-postings which get added automatically to transactions which match-certain queries, defined by \[dq]auto posting rules\[dq], when you use-the \f[C]--auto\f[R] flag.-.PP-An auto posting rule looks a bit like a transaction:-.IP-.nf-\f[C]-= QUERY- ACCOUNT AMOUNT- ...- ACCOUNT [AMOUNT]-\f[R]-.fi-.PP-except the first line is an equals sign (mnemonic: \f[C]=\f[R] suggests-matching), followed by a query (which matches existing postings), and-each \[dq]posting\[dq] line describes a posting to be generated, and the-posting amounts can be:-.IP \[bu] 2-a normal amount with a commodity symbol, eg \f[C]$2\f[R].-This will be used as-is.-.IP \[bu] 2-a number, eg \f[C]2\f[R].-The commodity symbol (if any) from the matched posting will be added to-this.-.IP \[bu] 2-a numeric multiplier, eg \f[C]*2\f[R] (a star followed by a number N).-The matched posting\[aq]s amount (and total price, if any) will be-multiplied by N.-.IP \[bu] 2-a multiplier with a commodity symbol, eg \f[C]*$2\f[R] (a star, number-N, and symbol S).-The matched posting\[aq]s amount will be multiplied by N, and its-commodity symbol will be replaced with S.-.PP-Any query term containing spaces must be enclosed in single or double-quotes, as on the command line.-Eg, note the quotes around the second query term below:-.IP-.nf-\f[C]-= expenses:groceries \[aq]expenses:dining out\[aq]- (budget:funds:dining out) *-1-\f[R]-.fi-.PP-Some examples:-.IP-.nf-\f[C]-; every time I buy food, schedule a dollar donation-= expenses:food- (liabilities:charity) $-1--; when I buy a gift, also deduct that amount from a budget envelope subaccount-= expenses:gifts- assets:checking:gifts *-1- assets:checking *1--2017/12/1- expenses:food $10- assets:checking--2017/12/14- expenses:gifts $20- assets:checking-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger print --auto-2017-12-01- expenses:food $10- assets:checking- (liabilities:charity) $-1--2017-12-14- expenses:gifts $20- assets:checking- assets:checking:gifts -$20- assets:checking $20-\f[R]-.fi-.SS Auto postings and multiple files-.PP-An auto posting rule can affect any transaction in the current file, or-in any parent file or child file.-Note, currently it will not affect sibling files (when multiple-\f[C]-f\f[R]/\f[C]--file\f[R] are used - see #1212).-.SS Auto postings and dates-.PP-A posting date (or secondary date) in the matched posting, or (taking-precedence) a posting date in the auto posting rule itself, will also be-used in the generated posting.-.SS Auto postings and transaction balancing / inferred amounts / balance assertions-.PP-Currently, auto postings are added:-.IP \[bu] 2-after missing amounts are inferred, and transactions are checked for-balancedness,-.IP \[bu] 2-but before balance assertions are checked.-.PP-Note this means that journal entries must be balanced both before and-after auto postings are added.-This changed in hledger 1.12+; see #893 for background.-.SS Auto posting tags-.PP-Automated postings will have some extra tags:-.IP \[bu] 2-\f[C]generated-posting:= QUERY\f[R] - shows this was generated by an-auto posting rule, and the query-.IP \[bu] 2-\f[C]_generated-posting:= QUERY\f[R] - a hidden tag, which does not-appear in hledger\[aq]s output.-This can be used to match postings generated \[dq]just now\[dq], rather-than generated in the past and saved to the journal.-.PP-Also, any transaction that has been changed by auto posting rules will-have these tags added:-.IP \[bu] 2-\f[C]modified:\f[R] - this transaction was modified-.IP \[bu] 2-\f[C]_modified:\f[R] - a hidden tag not appearing in the comment; this-transaction was modified \[dq]just now\[dq].---.SH "REPORTING BUGS"-Report bugs at http://bugs.hledger.org-(or on the #hledger IRC channel or hledger mail list)--.SH AUTHORS-Simon Michael <simon@joyful.com> and contributors--.SH COPYRIGHT--Copyright (C) 2007-2020 Simon Michael.-.br-Released under GNU GPL v3 or later.--.SH SEE ALSO-hledger(1), hledger\-ui(1), hledger\-web(1), ledger(1)--hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_timedot(5)
− hledger_journal.info
@@ -1,2217 +0,0 @@-This is hledger-lib/hledger_journal.info, produced by makeinfo version-4.8 from stdin.---File: hledger_journal.info, Node: Top, Up: (dir)--hledger_journal(5)-******************--hledger's default file format, representing a General Journal.-- hledger's usual data source is a plain text file containing journal-entries in hledger journal format. This file represents a standard-accounting general journal. I use file names ending in `.journal', but-that's not required. The journal file contains a number of transaction-entries, each describing a transfer of money (or any commodity) between-two or more named accounts, in a simple format readable by both hledger-and humans.-- hledger's journal format is a compatible subset, mostly, of ledger's-journal format, so hledger can work with compatible ledger journal files-as well. It's safe, and encouraged, to run both hledger and ledger on-the same journal file, eg to validate the results you're getting.-- You can use hledger without learning any more about this file; just-use the add or web or import commands to create and update it.-- Many users, though, edit the journal file with a text editor, and-track changes with a version control system such as git. Editor addons-such as ledger-mode or hledger-mode for Emacs, vim-ledger for Vim, and-hledger-vscode for Visual Studio Code, make this easier, adding colour,-formatting, tab completion, and useful commands. See Editor-configuration at hledger.org for the full list.-- Here's a description of each part of the file format (and hledger's-data model). These are mostly in the order you'll use them, but in some-cases related concepts have been grouped together for easy reference, or-linked before they are introduced, so feel free to skip over anything-that looks unnecessary right now.--* Menu:--* TRANSACTIONS::-* DATES::-* STATUS::-* DESCRIPTION::-* COMMENTS::-* TAGS::-* POSTINGS::-* ACCOUNT NAMES::-* AMOUNTS::-* TRANSACTION PRICES::-* LOT PRICES LOT DATES::-* BALANCE ASSERTIONS::-* BALANCE ASSIGNMENTS::-* DIRECTIVES::-* PERIODIC TRANSACTIONS::-* AUTO POSTINGS::---File: hledger_journal.info, Node: TRANSACTIONS, Next: DATES, Prev: Top, Up: Top--1 TRANSACTIONS-**************--Transactions are the main unit of information in a journal file. They-represent events, typically a movement of some quantity of commodities-between two or more named accounts.-- Each transaction is recorded as a journal entry, beginning with a-simple date in column 0. This can be followed by any of the following-optional fields, separated by spaces:-- * a status character (empty, `!', or `*')-- * a code (any short number or text, enclosed in parentheses)-- * a description (any remaining text until end of line or a semicolon)-- * a comment (any remaining text following a semicolon until end of- line, and any following indented lines beginning with a semicolon)-- * 0 or more indented _posting_ lines, describing what was transferred- and the accounts involved (indented comment lines are also- allowed, but not blank lines or non-indented lines).-- Here's a simple journal file containing one transaction:---2008/01/01 income- assets:bank:checking $1- income:salary $-1---File: hledger_journal.info, Node: DATES, Next: STATUS, Prev: TRANSACTIONS, Up: Top--2 DATES-*******--* Menu:--* Simple dates::-* Secondary dates::-* Posting dates::---File: hledger_journal.info, Node: Simple dates, Next: Secondary dates, Up: DATES--2.1 Simple dates-================--Dates in the journal file use _simple dates_ format: `YYYY-MM-DD' or-`YYYY/MM/DD' or `YYYY.MM.DD', with leading zeros optional. The year may-be omitted, in which case it will be inferred from the context: the-current transaction, the default year set with a default year-directive, or the current date when the command is run. Some examples:-`2010-01-31', `2010/01/31', `2010.1.31', `1/31'.-- (The UI also accepts simple dates, as well as the more flexible smart-dates documented in the hledger manual.)---File: hledger_journal.info, Node: Secondary dates, Next: Posting dates, Prev: Simple dates, Up: DATES--2.2 Secondary dates-===================--Real-life transactions sometimes involve more than one date - eg the-date you write a cheque, and the date it clears in your bank. When you-want to model this, for more accurate daily balances, you can specify-individual posting dates.-- Or, you can use the older _secondary date_ feature (Ledger calls it-auxiliary date or effective date). Note: we support this for-compatibility, but I usually recommend avoiding this feature; posting-dates are almost always clearer and simpler.-- A secondary date is written after the primary date, following an-equals sign. If the year is omitted, the primary date's year is-assumed. When running reports, the primary (left) date is used by-default, but with the `--date2' flag (or `--aux-date' or `--effective'),-the secondary (right) date will be used instead.-- The meaning of secondary dates is up to you, but it's best to follow-a consistent rule. Eg "primary = the bank's clearing date, secondary =-date the transaction was initiated, if different", as shown here:---2010/2/23=2/19 movie ticket- expenses:cinema $10- assets:checking---$ hledger register checking-2010-02-23 movie ticket assets:checking $-10 $-10---$ hledger register checking --date2-2010-02-19 movie ticket assets:checking $-10 $-10---File: hledger_journal.info, Node: Posting dates, Prev: Secondary dates, Up: DATES--2.3 Posting dates-=================--You can give individual postings a different date from their parent-transaction, by adding a posting comment containing a tag (see below)-like `date:DATE'. This is probably the best way to control posting-dates precisely. Eg in this example the expense should appear in May-reports, and the deduction from checking should be reported on 6/1 for-easy bank reconciliation:---2015/5/30- expenses:food $10 ; food purchased on saturday 5/30- assets:checking ; bank cleared it on monday, date:6/1---$ hledger -f t.j register food-2015-05-30 expenses:food $10 $10---$ hledger -f t.j register checking-2015-06-01 assets:checking $-10 $-10-- DATE should be a simple date; if the year is not specified it will-use the year of the transaction's date. You can set the secondary date-similarly, with `date2:DATE2'. The `date:' or `date2:' tags must have a-valid simple date value if they are present, eg a `date:' tag with no-value is not allowed.-- Ledger's earlier, more compact bracketed date syntax is also-supported: `[DATE]', `[DATE=DATE2]' or `[=DATE2]'. hledger will attempt-to parse any square-bracketed sequence of the `0123456789/-.='-characters in this way. With this syntax, DATE infers its year from the-transaction and DATE2 infers its year from DATE.---File: hledger_journal.info, Node: STATUS, Next: DESCRIPTION, Prev: DATES, Up: Top--3 STATUS-********--Transactions, or individual postings within a transaction, can have a-status mark, which is a single character before the transaction-description or posting account name, separated from it by a space,-indicating one of three statuses:--mark status------------------ - unmarked-`!' pending-`*' cleared-- When reporting, you can filter by status with the `-U/--unmarked',-`-P/--pending', and `-C/--cleared' flags; or the `status:', `status:!',-and `status:*' queries; or the U, P, C keys in hledger-ui.-- Note, in Ledger and in older versions of hledger, the "unmarked"-state is called "uncleared". As of hledger 1.3 we have renamed it to-unmarked for clarity.-- To replicate Ledger and old hledger's behaviour of also matching-pending, combine -U and -P.-- Status marks are optional, but can be helpful eg for reconciling with-real-world accounts. Some editor modes provide highlighting and-shortcuts for working with status. Eg in Emacs ledger-mode, you can-toggle transaction status with C-c C-e, or posting status with C-c C-c.-- What "uncleared", "pending", and "cleared" actually mean is up to-you. Here's one suggestion:--status meaning--------------------------------------------------------------------------- -uncleared recorded but not yet reconciled; needs review-pending tentatively reconciled (if needed, eg during a big- reconciliation)-cleared complete, reconciled as far as possible, and considered- correct-- With this scheme, you would use `-PC' to see the current balance at-your bank, `-U' to see things which will probably hit your bank soon-(like uncashed checks), and no flags to see the most up-to-date state-of your finances.---File: hledger_journal.info, Node: DESCRIPTION, Next: COMMENTS, Prev: STATUS, Up: Top--4 DESCRIPTION-*************--A transaction's description is the rest of the line following the date-and status mark (or until a comment begins). Sometimes called the-"narration" in traditional bookkeeping, it can be used for whatever you-wish, or left blank. Transaction descriptions can be queried, unlike-comments.--* Menu:--* Payee and note::---File: hledger_journal.info, Node: Payee and note, Up: DESCRIPTION--4.1 Payee and note-==================--You can optionally include a `|' (pipe) character in descriptions to-subdivide the description into separate fields for payee/payer name on-the left (up to the first `|') and an additional note field on the-right (after the first `|'). This may be worthwhile if you need to do-more precise querying and pivoting by payee or by note.---File: hledger_journal.info, Node: COMMENTS, Next: TAGS, Prev: DESCRIPTION, Up: Top--5 COMMENTS-**********--Lines in the journal beginning with a semicolon (`;') or hash (`#') or-star (`*') are comments, and will be ignored. (Star comments cause-org-mode nodes to be ignored, allowing emacs users to fold and navigate-their journals with org-mode or orgstruct-mode.)-- You can attach comments to a transaction by writing them after the-description and/or indented on the following lines (before the-postings). Similarly, you can attach comments to an individual posting-by writing them after the amount and/or indented on the following lines.-Transaction and posting comments must begin with a semicolon (`;').-- Some examples:---# a file comment-; another file comment-* also a file comment, useful in org/orgstruct mode--comment-A multiline file comment, which continues-until a line containing just "end comment"-(or end of file).-end comment--2012/5/14 something ; a transaction comment- ; the transaction comment, continued- posting1 1 ; a comment for posting 1- posting2- ; a comment for posting 2- ; another comment line for posting 2-; a file comment (because not indented)-- You can also comment larger regions of a file using `comment' and-`end comment' directives.---File: hledger_journal.info, Node: TAGS, Next: POSTINGS, Prev: COMMENTS, Up: Top--6 TAGS-******--Tags are a way to add extra labels or labelled data to postings and-transactions, which you can then search or pivot on.-- A simple tag is a word (which may contain hyphens) followed by a full-colon, written inside a transaction or posting comment line:---2017/1/16 bought groceries ; sometag:-- Tags can have a value, which is the text after the colon, up to the-next comma or end of line, with leading/trailing whitespace removed:--- expenses:food $10 ; a-posting-tag: the tag value-- Note this means hledger's tag values can not contain commas or-newlines. Ending at commas means you can write multiple short tags on-one line, comma separated:--- assets:checking ; a comment containing tag1:, tag2: some value ...-- Here,-- * "`a comment containing'" is just comment text, not a tag-- * "`tag1'" is a tag with no value-- * "`tag2'" is another tag, whose value is "`some value ...'"-- Tags in a transaction comment affect the transaction and all of its-postings, while tags in a posting comment affect only that posting. For-example, the following transaction has three tags (`A', `TAG2',-`third-tag') and the posting has four (those plus `posting-tag'):---1/1 a transaction ; A:, TAG2:- ; third-tag: a third transaction tag, <- with a value- (a) $1 ; posting-tag:-- Tags are like Ledger's metadata feature, except hledger's tag values-are simple strings.---File: hledger_journal.info, Node: POSTINGS, Next: ACCOUNT NAMES, Prev: TAGS, Up: Top--7 POSTINGS-**********--A posting is an addition of some amount to, or removal of some amount-from, an account. Each posting line begins with at least one space or-tab (2 or 4 spaces is common), followed by:-- * (optional) a status character (empty, `!', or `*'), followed by a- space-- * (required) an account name (any text, optionally containing- *single spaces*, until end of line or a double space)-- * (optional) *two or more spaces* or tabs followed by an amount.-- Positive amounts are being added to the account, negative amounts are-being removed.-- The amounts within a transaction must always sum up to zero. As a-convenience, one amount may be left blank; it will be inferred so as to-balance the transaction.-- Be sure to note the unusual two-space delimiter between account name-and amount. This makes it easy to write account names containing-spaces. But if you accidentally leave only one space (or tab) before-the amount, the amount will be considered part of the account name.--* Menu:--* Virtual postings::---File: hledger_journal.info, Node: Virtual postings, Up: POSTINGS--7.1 Virtual postings-====================--A posting with a parenthesised account name is called a _virtual-posting_ or _unbalanced posting_, which means it is exempt from the-usual rule that a transaction's postings must balance add up to zero.-- This is not part of double entry accounting, so you might choose to-avoid this feature. Or you can use it sparingly for certain special-cases where it can be convenient. Eg, you could set opening balances-without using a balancing equity account:---1/1 opening balances- (assets:checking) $1000- (assets:savings) $2000-- A posting with a bracketed account name is called a _balanced-virtual posting_. The balanced virtual postings in a transaction must-add up to zero (separately from other postings). Eg:---1/1 buy food with cash, update budget envelope subaccounts, & something else- assets:cash $-10 ; <- these balance- expenses:food $7 ; <-- expenses:food $3 ; <-- [assets:checking:budget:food] $-10 ; <- and these balance- [assets:checking:available] $10 ; <-- (something:else) $5 ; <- not required to balance-- Ordinary non-parenthesised, non-bracketed postings are called _real-postings_. You can exclude virtual postings from reports with the-`-R/--real' flag or `real:1' query.---File: hledger_journal.info, Node: ACCOUNT NAMES, Next: AMOUNTS, Prev: POSTINGS, Up: Top--8 ACCOUNT NAMES-***************--Account names typically have several parts separated by a full colon,-from which hledger derives a hierarchical chart of accounts. They can be-anything you like, but in finance there are traditionally five top-level-accounts: `assets', `liabilities', `income', `expenses', and `equity'.-- Account names may contain single spaces, eg: `assets:accounts-receivable'. Because of this, they must always be followed by *two or-more spaces* (or newline).-- Account names can be aliased.---File: hledger_journal.info, Node: AMOUNTS, Next: TRANSACTION PRICES, Prev: ACCOUNT NAMES, Up: Top--9 AMOUNTS-*********--After the account name, there is usually an amount. (Important: between-account name and amount, there must be *two or more spaces*.)-- hledger's amount format is flexible, supporting several international-formats. Here are some examples. Amounts have a number (the "quantity"):---1-- ..and usually a currency or commodity name (the "commodity"). This-is a symbol, word, or phrase, to the left or right of the quantity,-with or without a separating space:---$1-4000 AAPL-- If the commodity name contains spaces, numbers, or punctuation, it-must be enclosed in double quotes:---3 "no. 42 green apples"-- Amounts can be preceded by a minus sign (or a plus sign, though plus-is the default), The sign can be written before or after a left-side-commodity symbol:----$1-$-1-- One or more spaces between the sign and the number are acceptable-when parsing (but they won't be displayed in output):---+ $1-$- 1-- Scientific E notation is allowed:---1E-6-EUR 1E3-- A decimal mark can be written as a period or a comma:---1.23-1,23456780000009--* Menu:--* Digit group marks::-* Commodity display style::-* Rounding::---File: hledger_journal.info, Node: Digit group marks, Next: Commodity display style, Up: AMOUNTS--9.1 Digit group marks-=====================--In the integer part of the quantity (left of the decimal mark), groups-of digits can optionally be separated by a "digit group mark" - a space,-comma, or period (different from the decimal mark):--- $1,000,000.00- EUR 2.000.000,00-INR 9,99,99,999.00- 1 000 000.9455-- Note, a number containing a single group mark and no decimal mark is-ambiguous. Are these group marks or decimal marks ?---1,000-1.000-- hledger will treat them both as decimal marks by default (cf #793).-If you use digit group marks, to prevent confusion and undetected typos-we recommend you write commodity directives at the top of the file to-explicitly declare the decimal mark (and optionally a digit group mark).-Note, these formats ("amount styles") are specific to each commodity, so-if your data uses multiple formats, hledger can handle it:---commodity $1,000.00-commodity EUR 1.000,00-commodity INR 9,99,99,999.00-commodity 1 000 000.9455---File: hledger_journal.info, Node: Commodity display style, Next: Rounding, Prev: Digit group marks, Up: AMOUNTS--9.2 Commodity display style-===========================--For each commodity, hledger chooses a consistent style to use when-displaying amounts. (Except price amounts, which are always displayed as-written). The display style is chosen as follows:-- * If there is a commodity directive (or default commodity directive)- for the commodity, its style is used (see examples above).-- * Otherwise the style is inferred from the amounts in that commodity- seen in the journal.-- * Or if there are no such amounts in the journal, a default style is- used (like `$1000.00').--- A style is inferred from the journal amounts in a commodity as-follows:-- * Use the general style (decimal mark, symbol placement) of the first- amount-- * Use the first-seen digit group style (digit group mark, digit group- sizes), if any-- * Use the maximum number of decimal places of all.-- Transaction price amounts don't affect the commodity display style-directly, but occasionally they can do so indirectly (eg when a-posting's amount is inferred using a transaction price). If you find-this causing problems, use a commodity directive to fix the display-style.-- In summary, each commodity's amounts will be normalised to-- * the style declared by a `commodity' directive-- * or, the style of the first posting amount in the journal, with the- first-seen digit group style and the maximum-seen number of decimal- places.-- If reports are showing amounts in a way you don't like (eg, with too-many decimal places), use a commodity directive to set your preferred-style.---File: hledger_journal.info, Node: Rounding, Prev: Commodity display style, Up: AMOUNTS--9.3 Rounding-============--Amounts are stored internally as decimal numbers with up to 255 decimal-places, and displayed with the number of decimal places specified by the-commodity display style. Note, hledger uses banker's rounding: it rounds-to the nearest even number, eg 0.5 displayed with zero decimal places is-"0"). (Guaranteed since hledger 1.17.1; in older versions this could-vary if hledger was built with Decimal < 0.5.1.)---File: hledger_journal.info, Node: TRANSACTION PRICES, Next: LOT PRICES LOT DATES, Prev: AMOUNTS, Up: Top--10 TRANSACTION PRICES-*********************--Within a transaction, you can note an amount's price in another-commodity. This can be used to document the cost (in a purchase) or-selling price (in a sale). For example, transaction prices are useful to-record purchases of a foreign currency. Note transaction prices are-fixed at the time of the transaction, and do not change over time. See-also market prices, which represent prevailing exchange rates on a-certain date.-- There are several ways to record a transaction price:-- 1. Write the price per unit, as `@ UNITPRICE' after the amount:--- 2009/1/1- assets:euros €100 @ $1.35 ; one hundred euros purchased at $1.35 each- assets:dollars ; balancing amount is -$135.00-- 2. Write the total price, as `@@ TOTALPRICE' after the amount:--- 2009/1/1- assets:euros €100 @@ $135 ; one hundred euros purchased at $135 for the lot- assets:dollars-- 3. Specify amounts for all postings, using exactly two commodities,- and let hledger infer the price that balances the transaction:--- 2009/1/1- assets:euros €100 ; one hundred euros purchased- assets:dollars $-135 ; for $135-- 4. Like 1, but the `@' is parenthesised, i.e. `(@)'; this is for- compatibility with Ledger journals (Virtual posting costs), and is- equivalent to 1 in hledger.-- 5. Like 2, but as in 4 the `@@' is parenthesised, i.e. `(@@)'; in- hledger, this is equivalent to 2.--- Use the `-B/--cost' flag to convert amounts to their transaction-price's commodity, if any. (mnemonic: "B" is from "cost Basis", as in-Ledger). Eg here is how -B affects the balance report for the example-above:---$ hledger bal -N --flat- $-135 assets:dollars- €100 assets:euros-$ hledger bal -N --flat -B- $-135 assets:dollars- $135 assets:euros # <- the euros' cost-- Note -B is sensitive to the order of postings when a transaction-price is inferred: the inferred price will be in the commodity of the-last amount. So if example 3's postings are reversed, while the-transaction is equivalent, -B shows something different:---2009/1/1- assets:dollars $-135 ; 135 dollars sold- assets:euros €100 ; for 100 euros---$ hledger bal -N --flat -B- €-100 assets:dollars # <- the dollars' selling price- €100 assets:euros---File: hledger_journal.info, Node: LOT PRICES LOT DATES, Next: BALANCE ASSERTIONS, Prev: TRANSACTION PRICES, Up: Top--11 LOT PRICES, LOT DATES-************************--Ledger allows another kind of price, lot price (four variants:-`{UNITPRICE}', `{{TOTALPRICE}}', `{=FIXEDUNITPRICE}',-`{{=FIXEDTOTALPRICE}}'), and/or a lot date (`[DATE]') to be specified.-These are normally used to select a lot when selling investments.-hledger will parse these, for compatibility with Ledger journals, but-currently ignores them. A transaction price, lot price and/or lot date-may appear in any order, after the posting amount and before the-balance assertion if any.---File: hledger_journal.info, Node: BALANCE ASSERTIONS, Next: BALANCE ASSIGNMENTS, Prev: LOT PRICES LOT DATES, Up: Top--12 BALANCE ASSERTIONS-*********************--hledger supports Ledger-style balance assertions in journal files. These-look like, for example, `= EXPECTEDBALANCE' following a posting's-amount. Eg here we assert the expected dollar balance in accounts a and-b after each posting:---2013/1/1- a $1 =$1- b =$-1--2013/1/2- a $1 =$2- b $-1 =$-2-- After reading a journal file, hledger will check all balance-assertions and report an error if any of them fail. Balance assertions-can protect you from, eg, inadvertently disrupting reconciled balances-while cleaning up old entries. You can disable them temporarily with the-`-I/--ignore-assertions' flag, which can be useful for troubleshooting-or for reading Ledger files. (Note: this flag currently does not-disable balance assignments, below).--* Menu:--* Assertions and ordering::-* Assertions and included files::-* Assertions and multiple -f options::-* Assertions and commodities::-* Assertions and prices::-* Assertions and subaccounts::-* Assertions and virtual postings::-* Assertions and precision::---File: hledger_journal.info, Node: Assertions and ordering, Next: Assertions and included files, Up: BALANCE ASSERTIONS--12.1 Assertions and ordering-============================--hledger sorts an account's postings and assertions first by date and-then (for postings on the same day) by parse order. Note this is-different from Ledger, which sorts assertions only by parse order.-(Also, Ledger assertions do not see the accumulated effect of repeated-postings to the same account within a transaction.)-- So, hledger balance assertions keep working if you reorder-differently-dated transactions within the journal. But if you reorder-same-dated transactions or postings, assertions might break and require-updating. This order dependence does bring an advantage: precise control-over the order of postings and assertions within a day, so you can-assert intra-day balances.---File: hledger_journal.info, Node: Assertions and included files, Next: Assertions and multiple -f options, Prev: Assertions and ordering, Up: BALANCE ASSERTIONS--12.2 Assertions and included files-==================================--With included files, things are a little more complicated. Including-preserves the ordering of postings and assertions. If you have multiple-postings to an account on the same day, split across different files,-and you also want to assert the account's balance on the same day,-you'll have to put the assertion in the right file.---File: hledger_journal.info, Node: Assertions and multiple -f options, Next: Assertions and commodities, Prev: Assertions and included files, Up: BALANCE ASSERTIONS--12.3 Assertions and multiple -f options-=======================================--Balance assertions don't work well across files specified with multiple--f options. Use include or concatenate the files instead.---File: hledger_journal.info, Node: Assertions and commodities, Next: Assertions and prices, Prev: Assertions and multiple -f options, Up: BALANCE ASSERTIONS--12.4 Assertions and commodities-===============================--The asserted balance must be a simple single-commodity amount, and in-fact the assertion checks only this commodity's balance within the-(possibly multi-commodity) account balance. This is how assertions work-in Ledger also. We could call this a "partial" balance assertion.-- To assert the balance of more than one commodity in an account, you-can write multiple postings, each asserting one commodity's balance.-- You can make a stronger "total" balance assertion by writing a double-equals sign (`== EXPECTEDBALANCE'). This asserts that there are no-other unasserted commodities in the account (or, that their balance is-0).---2013/1/1- a $1- a 1€- b $-1- c -1€--2013/1/2 ; These assertions succeed- a 0 = $1- a 0 = 1€- b 0 == $-1- c 0 == -1€--2013/1/3 ; This assertion fails as 'a' also contains 1€- a 0 == $1-- It's not yet possible to make a complete assertion about a balance-that has multiple commodities. One workaround is to isolate each-commodity into its own subaccount:---2013/1/1- a:usd $1- a:euro 1€- b--2013/1/2- a 0 == 0- a:usd 0 == $1- a:euro 0 == 1€---File: hledger_journal.info, Node: Assertions and prices, Next: Assertions and subaccounts, Prev: Assertions and commodities, Up: BALANCE ASSERTIONS--12.5 Assertions and prices-==========================--Balance assertions ignore transaction prices, and should normally be-written without one:---2019/1/1- (a) $1 @ €1 = $1-- We do allow prices to be written there, however, and print shows-them, even though they don't affect whether the assertion passes or-fails. This is for backward compatibility (hledger's close command-used to generate balance assertions with prices), and because balance-_assignments_ do use them (see below).---File: hledger_journal.info, Node: Assertions and subaccounts, Next: Assertions and virtual postings, Prev: Assertions and prices, Up: BALANCE ASSERTIONS--12.6 Assertions and subaccounts-===============================--The balance assertions above (`=' and `==') do not count the balance-from subaccounts; they check the account's exclusive balance only. You-can assert the balance including subaccounts by writing `=*' or `==*',-eg:---2019/1/1- equity:opening balances- checking:a 5- checking:b 5- checking 1 ==* 11---File: hledger_journal.info, Node: Assertions and virtual postings, Next: Assertions and precision, Prev: Assertions and subaccounts, Up: BALANCE ASSERTIONS--12.7 Assertions and virtual postings-====================================--Balance assertions are checked against all postings, both real and-virtual. They are not affected by the `--real/-R' flag or `real:' query.---File: hledger_journal.info, Node: Assertions and precision, Prev: Assertions and virtual postings, Up: BALANCE ASSERTIONS--12.8 Assertions and precision-=============================--Balance assertions compare the exactly calculated amounts, which are not-always what is shown by reports. Eg a commodity directive may limit the-display precision, but this will not affect balance assertions. Balance-assertion failure messages show exact amounts.---File: hledger_journal.info, Node: BALANCE ASSIGNMENTS, Next: DIRECTIVES, Prev: BALANCE ASSERTIONS, Up: Top--13 BALANCE ASSIGNMENTS-**********************--Ledger-style balance assignments are also supported. These are like-balance assertions, but with no posting amount on the left side of the-equals sign; instead it is calculated automatically so as to satisfy the-assertion. This can be a convenience during data entry, eg when setting-opening balances:---; starting a new journal, set asset account balances-2016/1/1 opening balances- assets:checking = $409.32- assets:savings = $735.24- assets:cash = $42- equity:opening balances-- or when adjusting a balance to reality:---; no cash left; update balance, record any untracked spending as a generic expense-2016/1/15- assets:cash = $0- expenses:misc-- The calculated amount depends on the account's balance in the-commodity at that point (which depends on the previously-dated postings-of the commodity to that account since the last balance assertion or-assignment). Note that using balance assignments makes your journal a-little less explicit; to know the exact amount posted, you have to run-hledger or do the calculations yourself, instead of just reading it.--* Menu:--* Balance assignments and prices::---File: hledger_journal.info, Node: Balance assignments and prices, Up: BALANCE ASSIGNMENTS--13.1 Balance assignments and prices-===================================--A transaction price in a balance assignment will cause the calculated-amount to have that price attached:---2019/1/1- (a) = $1 @ €2---$ hledger print --explicit-2019-01-01- (a) $1 @ €2 = $1 @ €2---File: hledger_journal.info, Node: DIRECTIVES, Next: PERIODIC TRANSACTIONS, Prev: BALANCE ASSIGNMENTS, Up: Top--14 DIRECTIVES-*************--A directive is a line in the journal beginning with a special keyword,-that influences how the journal is processed. hledger's directives are-based on a subset of Ledger's, but there are many differences (and also-some differences between hledger versions).-- Directives' behaviour and interactions can get a little bit complex,-so here is a table summarising the directives and their effects, with-links to more detailed docs. Note part of this table is hidden when-viewed in a web browser - scroll it sideways to see more.--directiveend subdirectivespurpose can affect (as of- directive 2018/06)------------------------------------------------------------------------------ -`account' any document account names, all entries in all- text declare account types & files, before or- display order after-`alias' `end rewrite account names following entries- aliases' until end of- current file or- end directive-`apply `end prepend a common parent to following entries-account' apply account names until end of- account' current file or- end directive-`comment'`end ignore part of journal following entries- comment' until end of- current file or- end directive-`commodity' `format'declare a commodity and its number notation:- number notation & display following entries- style in that commodity- in all files ;- display style:- amounts of that- commodity in- reports-`D' declare a commodity to be default commodity:- used for commodityless following- amounts, and its number commodityless- notation & display style entries until end- of current file;- number notation:- following entries- in that commodity- until end of- current file;- display style:- amounts of that- commodity in- reports-`include' include entries/directives what the included- from another file directives affect-`P' declare a market price for a amounts of that- commodity commodity in- reports, when -V- is used-`Y' declare a year for yearless following entries- dates until end of- current file-`=' declare an auto posting all entries in- rule, adding postings to parent/current/child- other transactions files (but not- sibling files, see- #1212)-- And some definitions:--subdirectiveoptional indented directive line immediately following a parent- directive-number how to interpret numbers when parsing journal entries (the-notationidentity of the decimal separator character). (Currently each- commodity can have its own notation, even in the same file.)-displayhow to display amounts of a commodity in reports (symbol side and-style spacing, digit groups, decimal separator, decimal places)-directivewhich entries and (when there are multiple files) which files are-scope affected by a directive-- As you can see, directives vary in which journal entries and files-they affect, and whether they are focussed on input (parsing) or output-(reports). Some directives have multiple effects.--* Menu:--* Directives and multiple files::-* Comment blocks::-* Including other files::-* Default year::-* Declaring commodities::-* Default commodity::-* Declaring market prices::-* Declaring accounts::-* Rewriting accounts::-* Default parent account::---File: hledger_journal.info, Node: Directives and multiple files, Next: Comment blocks, Up: DIRECTIVES--14.1 Directives and multiple files-==================================--If you use multiple `-f'/`--file' options, or the `include' directive,-hledger will process multiple input files. But note that directives-which affect input (see above) typically last only until the end of the-file in which they occur.-- This may seem inconvenient, but it's intentional; it makes reports-stable and deterministic, independent of the order of input. Otherwise-you could see different numbers if you happened to write -f options in a-different order, or if you moved includes around while cleaning up your-files.-- It can be surprising though; for example, it means that `alias'-directives do not affect parent or sibling files (see below).---File: hledger_journal.info, Node: Comment blocks, Next: Including other files, Prev: Directives and multiple files, Up: DIRECTIVES--14.2 Comment blocks-===================--A line containing just `comment' starts a commented region of the file,-and a line containing just `end comment' (or the end of the current-file) ends it. See also comments.---File: hledger_journal.info, Node: Including other files, Next: Default year, Prev: Comment blocks, Up: DIRECTIVES--14.3 Including other files-==========================--You can pull in the content of additional files by writing an include-directive, like this:---include FILEPATH-- Only journal files can include, and only journal, timeclock or-timedot files can be included (not CSV files, currently).-- If the file path does not begin with a slash, it is relative to the-current file's folder.-- A tilde means home directory, eg: `include ~/main.journal'.-- The path may contain glob patterns to match multiple files, eg:-`include *.journal'.-- There is limited support for recursive wildcards: `**/' (the slash-is required) matches 0 or more subdirectories. It's not super convenient-since you have to avoid include cycles and including directories, but-this can be done, eg: `include */**/*.journal'.-- The path may also be prefixed to force a specific file format,-overriding the file extension (as described in hledger.1 -> Input-files): `include timedot:~/notes/2020*.md'.---File: hledger_journal.info, Node: Default year, Next: Declaring commodities, Prev: Including other files, Up: DIRECTIVES--14.4 Default year-=================--You can set a default year to be used for subsequent dates which don't-specify a year. This is a line beginning with `Y' followed by the year.-Eg:---Y2009 ; set default year to 2009--12/15 ; equivalent to 2009/12/15- expenses 1- assets--Y2010 ; change default year to 2010--2009/1/30 ; specifies the year, not affected- expenses 1- assets--1/31 ; equivalent to 2010/1/31- expenses 1- assets---File: hledger_journal.info, Node: Declaring commodities, Next: Default commodity, Prev: Default year, Up: DIRECTIVES--14.5 Declaring commodities-==========================--The `commodity' directive has several functions:-- 1. It declares commodities which may be used in the journal. This is- currently not enforced, but can serve as documentation.-- 2. It declares what decimal mark character (period or comma) to- expect when parsing input - useful to disambiguate international- number formats in your data. (Without this, hledger will parse- both `1,000' and `1.000' as 1).-- 3. It declares a commodity's display style in output - decimal and- digit group marks, number of decimal places, symbol placement etc.--- You are likely to run into one of the problems solved by commodity-directives, sooner or later, so it's a good idea to just always use them-to declare your commodities.-- A commodity directive is just the word `commodity' followed by an-amount. It may be written on a single line, like this:---; commodity EXAMPLEAMOUNT--; display AAAA amounts with the symbol on the right, space-separated,-; using period as decimal point, with four decimal places, and-; separating thousands with comma.-commodity 1,000.0000 AAAA-- or on multiple lines, using the "format" subdirective. (In this case-the commodity symbol appears twice and should be the same in both-places.):---; commodity SYMBOL-; format EXAMPLEAMOUNT--; display indian rupees with currency name on the left,-; thousands, lakhs and crores comma-separated,-; period as decimal point, and two decimal places.-commodity INR- format INR 1,00,00,000.00-- The quantity of the amount does not matter; only the format is-significant. The number must include a decimal mark: either a period or-a comma, followed by 0 or more decimal digits.-- Note hledger normally uses banker's rounding, so 0.5 displayed with-zero decimal digits is "0". (More at Commodity display style.)--* Menu:--* Commodity error checking::---File: hledger_journal.info, Node: Commodity error checking, Up: Declaring commodities--14.5.1 Commodity error checking----------------------------------In strict mode, enabled with the `-s'/`--strict' flag, hledger will-report an error if a commodity symbol is used that has not been-declared by a `commodity' directive. This works similarly to account-error checking, see the notes there for more details.---File: hledger_journal.info, Node: Default commodity, Next: Declaring market prices, Prev: Declaring commodities, Up: DIRECTIVES--14.6 Default commodity-======================--The `D' directive sets a default commodity, to be used for amounts-without a commodity symbol (ie, plain numbers). This commodity will be-applied to all subsequent commodity-less amounts, or until the next `D'-directive. (Note, this is different from Ledger's `D'.)-- For compatibility/historical reasons, `D' also acts like a-`commodity' directive, setting the commodity's display style (for-output) and decimal mark (for parsing input). As with `commodity', the-amount must always be written with a decimal mark (period or comma).-If both directives are used, `commodity''s style takes precedence.-- The syntax is `D AMOUNT'. Eg:---; commodity-less amounts should be treated as dollars-; (and displayed with the dollar sign on the left, thousands separators and two decimal places)-D $1,000.00--1/1- a 5 ; <- commodity-less amount, parsed as $5 and displayed as $5.00- b---File: hledger_journal.info, Node: Declaring market prices, Next: Declaring accounts, Prev: Default commodity, Up: DIRECTIVES--14.7 Declaring market prices-============================--The `P' directive declares a market price, which is an exchange rate-between two commodities on a certain date. (In Ledger, they are called-"historical prices".) These are often obtained from a stock exchange,-cryptocurrency exchange, or the foreign exchange market.-- Here is the format:---P DATE COMMODITYA COMMODITYBAMOUNT-- * DATE is a simple date-- * COMMODITYA is the symbol of the commodity being priced-- * COMMODITYBAMOUNT is an amount (symbol and quantity) in a second- commodity, giving the price in commodity B of one unit of- commodity A.-- These two market price directives say that one euro was worth 1.35 US-dollars during 2009, and $1.40 from 2010 onward:---P 2009/1/1 € $1.35-P 2010/1/1 € $1.40-- The `-V', `-X' and `--value' flags use these market prices to show-amount values in another commodity. See Valuation.---File: hledger_journal.info, Node: Declaring accounts, Next: Rewriting accounts, Prev: Declaring market prices, Up: DIRECTIVES--14.8 Declaring accounts-=======================--`account' directives can be used to declare accounts (ie, the places-that amounts are transferred from and to). Though not required, these-declarations can provide several benefits:-- * They can document your intended chart of accounts, providing a- reference.-- * They can help hledger know your accounts' types (asset, liability,- equity, revenue, expense), useful for reports like balancesheet and- incomestatement.-- * They control account display order in reports, allowing- non-alphabetic sorting (eg Revenues to appear above Expenses).-- * They can store extra information about accounts (account numbers,- notes, etc.)-- * They help with account name completion in the add command,- hledger-iadd, hledger-web, ledger-mode etc.-- * In strict mode, they restrict which accounts may be posted to by- transactions, which helps detect typos.-- The simplest form is just the word `account' followed by a-hledger-style account name, eg this account directive declares the-`assets:bank:checking' account:---account assets:bank:checking--* Menu:--* Account error checking::-* Account comments::-* Account subdirectives::-* Account types::-* Account display order::---File: hledger_journal.info, Node: Account error checking, Next: Account comments, Up: Declaring accounts--14.8.1 Account error checking--------------------------------By default, accounts come into existence when a transaction references-them by name. This is convenient, but it means hledger can't warn you-when you mis-spell an account name in the journal. Usually you'll find-the error later, as an extra account in balance reports, or an incorrect-balance when reconciling.-- In strict mode, enabled with the `-s'/`--strict' flag, hledger will-report an error if any transaction uses an account name that has not-been declared by an account directive. Some notes:-- * The declaration is case-sensitive; transactions must use the- correct account name capitalisation.-- * The account directive's scope is "whole file and below" (see- directives). This means it affects all of the current file, and any- files it includes, but not parent or sibling files. The position of- account directives within the file does not matter, though it's- usual to put them at the top.-- * Accounts can only be declared in `journal' files (but will affect- included files in other formats).-- * It's currently not possible to declare "all possible subaccounts"- with a wildcard; every account posted to must be declared.---File: hledger_journal.info, Node: Account comments, Next: Account subdirectives, Prev: Account error checking, Up: Declaring accounts--14.8.2 Account comments--------------------------Comments, beginning with a semicolon, can be added:-- * on the same line, *after two or more spaces* (because ; is allowed- in account names)-- * on the next lines, indented-- An example of both:---account assets:bank:checking ; same-line comment, note 2+ spaces before ;- ; next-line comment- ; another with tag, acctno:12345 (not used yet)-- Same-line comments are not supported by Ledger, or hledger <1.13.---File: hledger_journal.info, Node: Account subdirectives, Next: Account types, Prev: Account comments, Up: Declaring accounts--14.8.3 Account subdirectives-------------------------------We also allow (and ignore) Ledger-style indented subdirectives, just for-compatibility.:---account assets:bank:checking- format blah blah ; <- subdirective, ignored-- Here is the full syntax of account directives:---account ACCTNAME [ACCTTYPE] [;COMMENT]- [;COMMENTS]- [LEDGER-STYLE SUBDIRECTIVES, IGNORED]---File: hledger_journal.info, Node: Account types, Next: Account display order, Prev: Account subdirectives, Up: Declaring accounts--14.8.4 Account types-----------------------hledger recognises five main types of account, corresponding to the-account classes in the accounting equation:-- `Asset', `Liability', `Equity', `Revenue', `Expense'.-- These account types are important for controlling which accounts-appear in the balancesheet, balancesheetequity, incomestatement reports-(and probably for other things in future).-- Additionally, we recognise the `Cash' type, which is also an-`Asset', and which causes accounts to appear in the cashflow report.-("Cash" here means liquid assets, eg bank balances but typically not-investments or receivables.)--* Menu:--* Declaring account types::-* Auto-detected account types::-* Interference from auto-detected account types::-* Old account type syntax::---File: hledger_journal.info, Node: Declaring account types, Next: Auto-detected account types, Up: Account types--14.8.4.1 Declaring account types-................................--Generally, to make these reports work you should declare your top-level-accounts and their types, using account directives with `type:' tags.-- The tag's value should be one of: `Asset', `Liability', `Equity',-`Revenue', `Expense', `Cash', `A', `L', `E', `R', `X', `C' (all case-insensitive). The type is inherited by all subaccounts except where-they override it. Here's a complete example:---account assets ; type: Asset-account assets:bank ; type: Cash-account assets:cash ; type: Cash-account liabilities ; type: Liability-account equity ; type: Equity-account revenues ; type: Revenue-account expenses ; type: Expense---File: hledger_journal.info, Node: Auto-detected account types, Next: Interference from auto-detected account types, Prev: Declaring account types, Up: Account types--14.8.4.2 Auto-detected account types-....................................--If you happen to use common english top-level account names, you may not-need to declare account types, as they will be detected automatically-using the following rules:--If name matches regular account type-expression: is:-------------------------------------------------- -`^assets?(:|$)' `Asset'-`^(debts?|liabilit(y|ies))(:|$)' `Liability'-`^equity(:|$)' `Equity'-`^(income|revenue)s?(:|$)' `Revenue'-`^expenses?(:|$)' `Expense'--If account type is `Asset' and name does not contain account type-regular expression: is:--------------------------------------------------------------------------- -`(investment|receivable|:A/R|:fixed)' `Cash'-- Even so, explicit declarations may be a good idea, for clarity and-predictability.---File: hledger_journal.info, Node: Interference from auto-detected account types, Next: Old account type syntax, Prev: Auto-detected account types, Up: Account types--14.8.4.3 Interference from auto-detected account types-......................................................--If you assign any account type, it's a good idea to assign all of them,-to prevent any confusion from mixing declared and auto-detected types.-Although it's unlikely to happen in real life, here's an example: with-the following journal, `balancesheetequity' shows "liabilities" in both-Liabilities and Equity sections. Declaring another account as-`type:Liability' would fix it:---account liabilities ; type:Equity--2020-01-01- assets 1- liabilities 1- equity -2---File: hledger_journal.info, Node: Old account type syntax, Prev: Interference from auto-detected account types, Up: Account types--14.8.4.4 Old account type syntax-................................--In some hledger journals you might instead see this old syntax (the-letters ALERX, separated from the account name by two or more spaces);-this is deprecated and may be removed soon:---account assets A-account liabilities L-account equity E-account revenues R-account expenses X---File: hledger_journal.info, Node: Account display order, Prev: Account types, Up: Declaring accounts--14.8.5 Account display order-------------------------------Account directives also set the order in which accounts are displayed,-eg in reports, the hledger-ui accounts screen, and the hledger-web-sidebar. By default accounts are listed in alphabetical order. But if-you have these account directives in the journal:---account assets-account liabilities-account equity-account revenues-account expenses-- you'll see those accounts displayed in declaration order, not-alphabetically:---$ hledger accounts -1-assets-liabilities-equity-revenues-expenses-- Undeclared accounts, if any, are displayed last, in alphabetical-order.-- Note that sorting is done at each level of the account tree (within-each group of sibling accounts under the same parent). And currently,-this directive:---account other:zoo-- would influence the position of `zoo' among `other''s subaccounts,-but not the position of `other' among the top-level accounts. This-means:-- * you will sometimes declare parent accounts (eg `account other'- above) that you don't intend to post to, just to customize their- display order-- * sibling accounts stay together (you couldn't display `x:y' in- between `a:b' and `a:c').---File: hledger_journal.info, Node: Rewriting accounts, Next: Default parent account, Prev: Declaring accounts, Up: DIRECTIVES--14.9 Rewriting accounts-=======================--You can define account alias rules which rewrite your account names, or-parts of them, before generating reports. This can be useful for:-- * expanding shorthand account names to their full form, allowing- easier data entry and a less verbose journal-- * adapting old journals to your current chart of accounts-- * experimenting with new account organisations, like a new hierarchy- or combining two accounts into one-- * customising reports-- Account aliases also rewrite account names in account directives.-They do not affect account names being entered via hledger add or-hledger-web.-- See also Rewrite account names.--* Menu:--* Basic aliases::-* Regex aliases::-* Combining aliases::-* Aliases and multiple files::-* end aliases::---File: hledger_journal.info, Node: Basic aliases, Next: Regex aliases, Up: Rewriting accounts--14.9.1 Basic aliases-----------------------To set an account alias, use the `alias' directive in your journal-file. This affects all subsequent journal entries in the current file or-its included files. The spaces around the = are optional:---alias OLD = NEW-- Or, you can use the `--alias 'OLD=NEW'' option on the command line.-This affects all entries. It's useful for trying out aliases-interactively.-- OLD and NEW are case sensitive full account names. hledger will-replace any occurrence of the old account name with the new one.-Subaccounts are also affected. Eg:---alias checking = assets:bank:wells fargo:checking-; rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"---File: hledger_journal.info, Node: Regex aliases, Next: Combining aliases, Prev: Basic aliases, Up: Rewriting accounts--14.9.2 Regex aliases-----------------------There is also a more powerful variant that uses a regular expression,-indicated by the forward slashes:---alias /REGEX/ = REPLACEMENT-- or `--alias '/REGEX/=REPLACEMENT''.-- REGEX is a case-insensitive regular expression. Anywhere it matches-inside an account name, the matched part will be replaced by-REPLACEMENT. If REGEX contains parenthesised match groups, these can be-referenced by the usual numeric backreferences in REPLACEMENT. Eg:---alias /^(.+):bank:([^:]+):(.*)/ = \1:\2 \3-; rewrites "assets:bank:wells fargo:checking" to "assets:wells fargo checking"-- Also note that REPLACEMENT continues to the end of line (or on-command line, to end of option argument), so it can contain trailing-whitespace.---File: hledger_journal.info, Node: Combining aliases, Next: Aliases and multiple files, Prev: Regex aliases, Up: Rewriting accounts--14.9.3 Combining aliases---------------------------You can define as many aliases as you like, using journal directives-and/or command line options.-- Recursive aliases - where an account name is rewritten by one alias,-then by another alias, and so on - are allowed. Each alias sees the-effect of previously applied aliases.-- In such cases it can be important to understand which aliases will be-applied and in which order. For (each account name in) each journal-entry, we apply:-- 1. `alias' directives preceding the journal entry, most recently- parsed first (ie, reading upward from the journal entry, bottom to- top)-- 2. `--alias' options, in the order they appeared on the command line- (left to right).-- In other words, for (an account name in) a given journal entry:-- * the nearest alias declaration before/above the entry is applied- first-- * the next alias before/above that will be be applied next, and so on-- * aliases defined after/below the entry do not affect it.-- This gives nearby aliases precedence over distant ones, and helps-provide semantic stability - aliases will keep working the same way-independent of which files are being read and in which order.-- In case of trouble, adding `--debug=6' to the command line will show-which aliases are being applied when.---File: hledger_journal.info, Node: Aliases and multiple files, Next: end aliases, Prev: Combining aliases, Up: Rewriting accounts--14.9.4 Aliases and multiple files------------------------------------As explained at Directives and multiple files, `alias' directives do-not affect parent or sibling files. Eg in this command,---hledger -f a.aliases -f b.journal-- account aliases defined in a.aliases will not affect b.journal.-Including the aliases doesn't work either:---include a.aliases--2020-01-01 ; not affected by a.aliases- foo 1- bar-- This means that account aliases should usually be declared at the-start of your top-most file, like this:---alias foo=Foo-alias bar=Bar--2020-01-01 ; affected by aliases above- foo 1- bar--include c.journal ; also affected---File: hledger_journal.info, Node: end aliases, Prev: Aliases and multiple files, Up: Rewriting accounts--14.9.5 `end aliases'-----------------------You can clear (forget) all currently defined aliases with the `end-aliases' directive:---end aliases---File: hledger_journal.info, Node: Default parent account, Prev: Rewriting accounts, Up: DIRECTIVES--14.10 Default parent account-============================--You can specify a parent account which will be prepended to all accounts-within a section of the journal. Use the `apply account' and `end apply-account' directives like so:---apply account home--2010/1/1- food $10- cash--end apply account-- which is equivalent to:---2010/01/01- home:food $10- home:cash $-10-- If `end apply account' is omitted, the effect lasts to the end of-the file. Included files are also affected, eg:---apply account business-include biz.journal-end apply account-apply account personal-include personal.journal-- Prior to hledger 1.0, legacy `account' and `end' spellings were also-supported.-- A default parent account also affects account directives. It does not-affect account names being entered via hledger add or hledger-web. If-account aliases are present, they are applied after the default parent-account.---File: hledger_journal.info, Node: PERIODIC TRANSACTIONS, Next: AUTO POSTINGS, Prev: DIRECTIVES, Up: Top--15 PERIODIC TRANSACTIONS-************************--Periodic transaction rules describe transactions that recur. They allow-hledger to generate temporary future transactions to help with-forecasting, so you don't have to write out each one in the journal, and-it's easy to try out different forecasts.-- Periodic transactions can be a little tricky, so before you use them,-read this whole section - or at least these tips:-- 1. Two spaces accidentally added or omitted will cause you trouble -- read about this below.-- 2. For troubleshooting, show the generated transactions with `hledger- print --forecast tag:generated' or `hledger register --forecast- tag:generated'.-- 3. Forecasted transactions will begin only after the last- non-forecasted transaction's date.-- 4. Forecasted transactions will end 6 months from today, by default.- See below for the exact start/end rules.-- 5. period expressions can be tricky. Their documentation needs- improvement, but is worth studying.-- 6. Some period expressions with a repeating interval must begin on a- natural boundary of that interval. Eg in `weekly from DATE', DATE- must be a monday. `~ weekly from 2019/10/1' (a tuesday) will give- an error.-- 7. Other period expressions with an interval are automatically- expanded to cover a whole number of that interval. (This is done- to improve reports, but it also affects periodic transactions.- Yes, it's a bit inconsistent with the above.) Eg: `~ every 10th- day of month from 2020/01', which is equivalent to `~ every 10th- day of month from 2020/01/01', will be adjusted to start on- 2019/12/10.-- Periodic transaction rules also have a second meaning: they are used-to define budget goals, shown in budget reports.--* Menu:--* Periodic rule syntax::-* Two spaces between period expression and description!::-* Forecasting with periodic transactions::-* Budgeting with periodic transactions::---File: hledger_journal.info, Node: Periodic rule syntax, Next: Two spaces between period expression and description!, Up: PERIODIC TRANSACTIONS--15.1 Periodic rule syntax-=========================--A periodic transaction rule looks like a normal journal entry, with the-date replaced by a tilde (`~') followed by a period expression-(mnemonic: `~' looks like a recurring sine wave.):---~ monthly- expenses:rent $2000- assets:bank:checking-- There is an additional constraint on the period expression: the start-date must fall on a natural boundary of the interval. Eg `monthly from-2018/1/1' is valid, but `monthly from 2018/1/15' is not.-- Partial or relative dates (M/D, D, tomorrow, last week) in the period-expression can work (useful or not). They will be relative to today's-date, unless a Y default year directive is in effect, in which case they-will be relative to Y/1/1.---File: hledger_journal.info, Node: Two spaces between period expression and description!, Next: Forecasting with periodic transactions, Prev: Periodic rule syntax, Up: PERIODIC TRANSACTIONS--15.2 Two spaces between period expression and description!-==========================================================--If the period expression is followed by a transaction description, these-must be separated by *two or more spaces*. This helps hledger know-where the period expression ends, so that descriptions can not-accidentally alter their meaning, as in this example:---; 2 or more spaces needed here, so the period is not understood as "every 2 months in 2020"-; ||-; vv-~ every 2 months in 2020, we will review- assets:bank:checking $1500- income:acme inc-- So,-- * Do write two spaces between your period expression and your- transaction description, if any.-- * Don't accidentally write two spaces in the middle of your period- expression.---File: hledger_journal.info, Node: Forecasting with periodic transactions, Next: Budgeting with periodic transactions, Prev: Two spaces between period expression and description!, Up: PERIODIC TRANSACTIONS--15.3 Forecasting with periodic transactions-===========================================--The `--forecast' flag activates any periodic transaction rules in the-journal. They will generate temporary recurring transactions, which are-not saved in the journal, but will appear in all reports (eg print).-This can be useful for estimating balances into the future, or-experimenting with different scenarios. Or, it can be used as a data-entry aid: describe recurring transactions, and every so often copy the-output of `print --forecast' into the journal.-- These transactions will have an extra tag indicating which periodic-rule generated them: `generated-transaction:~ PERIODICEXPR'. And a-similar, hidden tag (beginning with an underscore) which, because it's-never displayed by print, can be used to match transactions generated-"just now": `_generated-transaction:~ PERIODICEXPR'.-- Periodic transactions are generated within some forecast period. By-default, this-- * begins on the later of- * the report start date if specified with -b/-p/date:-- * the day after the latest normal (non-periodic) transaction in- the journal, or today if there are no normal transactions.-- * ends on the report end date if specified with -e/-p/date:, or 6- months (180 days) from today.-- This means that periodic transactions will begin only after the-latest recorded transaction. And a recorded transaction dated in the-future can prevent generation of periodic transactions. (You can avoid-that by writing the future transaction as a one-time periodic rule-instead - put tilde before the date, eg `~ YYYY-MM-DD ...').-- Or, you can set your own arbitrary "forecast period", which can-overlap recorded transactions, and need not be in the future, by-providing an option argument, like `--forecast=PERIODEXPR'. Note the-equals sign is required, a space won't work. PERIODEXPR is a period-expression, which can specify the start date, end date, or both, like-in a `date:' query. (See also hledger.1 -> Report start & end date).-Some examples: `--forecast=202001-202004', `--forecast=jan-',-`--forecast=2020'.---File: hledger_journal.info, Node: Budgeting with periodic transactions, Prev: Forecasting with periodic transactions, Up: PERIODIC TRANSACTIONS--15.4 Budgeting with periodic transactions-=========================================--With the `--budget' flag, currently supported by the balance command,-each periodic transaction rule declares recurring budget goals for the-specified accounts. Eg the first example above declares a goal of-spending $2000 on rent (and also, a goal of depositing $2000 into-checking) every month. Goals and actual performance can then be compared-in budget reports.-- See also: Budgeting and Forecasting.---File: hledger_journal.info, Node: AUTO POSTINGS, Prev: PERIODIC TRANSACTIONS, Up: Top--16 AUTO POSTINGS-****************--"Automated postings" or "auto postings" are extra postings which get-added automatically to transactions which match certain queries, defined-by "auto posting rules", when you use the `--auto' flag.-- An auto posting rule looks a bit like a transaction:---= QUERY- ACCOUNT AMOUNT- ...- ACCOUNT [AMOUNT]-- except the first line is an equals sign (mnemonic: `=' suggests-matching), followed by a query (which matches existing postings), and-each "posting" line describes a posting to be generated, and the posting-amounts can be:-- * a normal amount with a commodity symbol, eg `$2'. This will be used- as-is.-- * a number, eg `2'. The commodity symbol (if any) from the matched- posting will be added to this.-- * a numeric multiplier, eg `*2' (a star followed by a number N). The- matched posting's amount (and total price, if any) will be- multiplied by N.-- * a multiplier with a commodity symbol, eg `*$2' (a star, number N,- and symbol S). The matched posting's amount will be multiplied by- N, and its commodity symbol will be replaced with S.-- Any query term containing spaces must be enclosed in single or double-quotes, as on the command line. Eg, note the quotes around the second-query term below:---= expenses:groceries 'expenses:dining out'- (budget:funds:dining out) *-1-- Some examples:---; every time I buy food, schedule a dollar donation-= expenses:food- (liabilities:charity) $-1--; when I buy a gift, also deduct that amount from a budget envelope subaccount-= expenses:gifts- assets:checking:gifts *-1- assets:checking *1--2017/12/1- expenses:food $10- assets:checking--2017/12/14- expenses:gifts $20- assets:checking---$ hledger print --auto-2017-12-01- expenses:food $10- assets:checking- (liabilities:charity) $-1--2017-12-14- expenses:gifts $20- assets:checking- assets:checking:gifts -$20- assets:checking $20--* Menu:--* Auto postings and multiple files::-* Auto postings and dates::-* Auto postings and transaction balancing / inferred amounts / balance assertions::-* Auto posting tags::---File: hledger_journal.info, Node: Auto postings and multiple files, Next: Auto postings and dates, Up: AUTO POSTINGS--16.1 Auto postings and multiple files-=====================================--An auto posting rule can affect any transaction in the current file, or-in any parent file or child file. Note, currently it will not affect-sibling files (when multiple `-f'/`--file' are used - see #1212).---File: hledger_journal.info, Node: Auto postings and dates, Next: Auto postings and transaction balancing / inferred amounts / balance assertions, Prev: Auto postings and multiple files, Up: AUTO POSTINGS--16.2 Auto postings and dates-============================--A posting date (or secondary date) in the matched posting, or (taking-precedence) a posting date in the auto posting rule itself, will also be-used in the generated posting.---File: hledger_journal.info, Node: Auto postings and transaction balancing / inferred amounts / balance assertions, Next: Auto posting tags, Prev: Auto postings and dates, Up: AUTO POSTINGS--16.3 Auto postings and transaction balancing / inferred amounts /-=================================================================--balance assertions-- Currently, auto postings are added:-- * after missing amounts are inferred, and transactions are checked- for balancedness,-- * but before balance assertions are checked.-- Note this means that journal entries must be balanced both before and-after auto postings are added. This changed in hledger 1.12+; see #893-for background.---File: hledger_journal.info, Node: Auto posting tags, Prev: Auto postings and transaction balancing / inferred amounts / balance assertions, Up: AUTO POSTINGS--16.4 Auto posting tags-======================--Automated postings will have some extra tags:-- * `generated-posting:= QUERY' - shows this was generated by an auto- posting rule, and the query-- * `_generated-posting:= QUERY' - a hidden tag, which does not appear- in hledger's output. This can be used to match postings generated- "just now", rather than generated in the past and saved to the- journal.-- Also, any transaction that has been changed by auto posting rules-will have these tags added:-- * `modified:' - this transaction was modified-- * `_modified:' - a hidden tag not appearing in the comment; this- transaction was modified "just now".----Tag Table:-Node: Top88-Node: TRANSACTIONS2095-Ref: #transactions2213-Node: DATES3230-Ref: #dates3337-Node: Simple dates3402-Ref: #simple-dates3524-Node: Secondary dates4031-Ref: #secondary-dates4181-Node: Posting dates5515-Ref: #posting-dates5640-Node: STATUS7009-Ref: #status7117-Node: DESCRIPTION8822-Ref: #description8943-Node: Payee and note9261-Ref: #payee-and-note9371-Node: COMMENTS9705-Ref: #comments9818-Node: TAGS11011-Ref: #tags11113-Node: POSTINGS12511-Ref: #postings12626-Node: Virtual postings13650-Ref: #virtual-postings13763-Node: ACCOUNT NAMES15065-Ref: #account-names15193-Node: AMOUNTS15678-Ref: #amounts15804-Node: Digit group marks16931-Ref: #digit-group-marks17078-Node: Commodity display style18018-Ref: #commodity-display-style18194-Node: Rounding19738-Ref: #rounding19858-Node: TRANSACTION PRICES20268-Ref: #transaction-prices20425-Node: LOT PRICES LOT DATES22855-Ref: #lot-prices-lot-dates23029-Node: BALANCE ASSERTIONS23516-Ref: #balance-assertions23685-Node: Assertions and ordering24715-Ref: #assertions-and-ordering24899-Node: Assertions and included files25596-Ref: #assertions-and-included-files25835-Node: Assertions and multiple -f options26166-Ref: #assertions-and-multiple--f-options26418-Node: Assertions and commodities26549-Ref: #assertions-and-commodities26777-Node: Assertions and prices27932-Ref: #assertions-and-prices28142-Node: Assertions and subaccounts28583-Ref: #assertions-and-subaccounts28808-Node: Assertions and virtual postings29132-Ref: #assertions-and-virtual-postings29370-Node: Assertions and precision29511-Ref: #assertions-and-precision29700-Node: BALANCE ASSIGNMENTS29965-Ref: #balance-assignments30126-Node: Balance assignments and prices31289-Ref: #balance-assignments-and-prices31457-Node: DIRECTIVES31683-Ref: #directives31829-Node: Directives and multiple files37274-Ref: #directives-and-multiple-files37453-Node: Comment blocks38115-Ref: #comment-blocks38294-Node: Including other files38469-Ref: #including-other-files38645-Node: Default year39569-Ref: #default-year39734-Node: Declaring commodities40141-Ref: #declaring-commodities40320-Node: Commodity error checking42161-Ref: #commodity-error-checking42317-Node: Default commodity42573-Ref: #default-commodity42755-Node: Declaring market prices43640-Ref: #declaring-market-prices43831-Node: Declaring accounts44689-Ref: #declaring-accounts44871-Node: Account error checking46078-Ref: #account-error-checking46250-Node: Account comments47427-Ref: #account-comments47617-Node: Account subdirectives48043-Ref: #account-subdirectives48234-Node: Account types48549-Ref: #account-types48729-Node: Declaring account types49464-Ref: #declaring-account-types49649-Node: Auto-detected account types50300-Ref: #auto-detected-account-types50547-Node: Interference from auto-detected account types51446-Ref: #interference-from-auto-detected-account-types51729-Node: Old account type syntax52212-Ref: #old-account-type-syntax52415-Node: Account display order52716-Ref: #account-display-order52882-Node: Rewriting accounts54033-Ref: #rewriting-accounts54214-Node: Basic aliases54973-Ref: #basic-aliases55115-Node: Regex aliases55817-Ref: #regex-aliases55985-Node: Combining aliases56705-Ref: #combining-aliases56894-Node: Aliases and multiple files58171-Ref: #aliases-and-multiple-files58376-Node: end aliases58957-Ref: #end-aliases59110-Node: Default parent account59212-Ref: #default-parent-account59376-Node: PERIODIC TRANSACTIONS60260-Ref: #periodic-transactions60422-Node: Periodic rule syntax62339-Ref: #periodic-rule-syntax62541-Node: Two spaces between period expression and description!63244-Ref: #two-spaces-between-period-expression-and-description63559-Node: Forecasting with periodic transactions64244-Ref: #forecasting-with-periodic-transactions64545-Node: Budgeting with periodic transactions66591-Ref: #budgeting-with-periodic-transactions66826-Node: AUTO POSTINGS67233-Ref: #auto-postings67360-Node: Auto postings and multiple files69543-Ref: #auto-postings-and-multiple-files69743-Node: Auto postings and dates69951-Ref: #auto-postings-and-dates70221-Node: Auto postings and transaction balancing / inferred amounts / balance assertions70396-Ref: #auto-postings-and-transaction-balancing-inferred-amounts-balance-assertions70744-Node: Auto posting tags71089-Ref: #auto-posting-tags71300--End Tag Table
− hledger_journal.txt
@@ -1,1597 +0,0 @@--HLEDGER_JOURNAL(5) hledger User Manuals HLEDGER_JOURNAL(5)----NAME- hledger's default file format, representing a General Journal.--DESCRIPTION- hledger's usual data source is a plain text file containing journal- entries in hledger journal format. This file represents a standard- accounting general journal. I use file names ending in .journal, but- that's not required. The journal file contains a number of transaction- entries, each describing a transfer of money (or any commodity) between- two or more named accounts, in a simple format readable by both hledger- and humans.-- hledger's journal format is a compatible subset, mostly, of ledger's- journal format, so hledger can work with compatible ledger journal- files as well. It's safe, and encouraged, to run both hledger and- ledger on the same journal file, eg to validate the results you're get-- ting.-- You can use hledger without learning any more about this file; just use- the add or web or import commands to create and update it.-- Many users, though, edit the journal file with a text editor, and track- changes with a version control system such as git. Editor addons such- as ledger-mode or hledger-mode for Emacs, vim-ledger for Vim, and- hledger-vscode for Visual Studio Code, make this easier, adding colour,- formatting, tab completion, and useful commands. See Editor configura-- tion at hledger.org for the full list.-- Here's a description of each part of the file format (and hledger's- data model). These are mostly in the order you'll use them, but in- some cases related concepts have been grouped together for easy refer-- ence, or linked before they are introduced, so feel free to skip over- anything that looks unnecessary right now.--TRANSACTIONS- Transactions are the main unit of information in a journal file. They- represent events, typically a movement of some quantity of commodities- between two or more named accounts.-- Each transaction is recorded as a journal entry, beginning with a sim-- ple date in column 0. This can be followed by any of the following- optional fields, separated by spaces:-- o a status character (empty, !, or *)-- o a code (any short number or text, enclosed in parentheses)-- o a description (any remaining text until end of line or a semicolon)-- o a comment (any remaining text following a semicolon until end of- line, and any following indented lines beginning with a semicolon)-- o 0 or more indented posting lines, describing what was transferred and- the accounts involved (indented comment lines are also allowed, but- not blank lines or non-indented lines).-- Here's a simple journal file containing one transaction:-- 2008/01/01 income- assets:bank:checking $1- income:salary $-1--DATES- Simple dates- Dates in the journal file use simple dates format: YYYY-MM-DD or- YYYY/MM/DD or YYYY.MM.DD, with leading zeros optional. The year may be- omitted, in which case it will be inferred from the context: the cur-- rent transaction, the default year set with a default year directive,- or the current date when the command is run. Some examples:- 2010-01-31, 2010/01/31, 2010.1.31, 1/31.-- (The UI also accepts simple dates, as well as the more flexible smart- dates documented in the hledger manual.)-- Secondary dates- Real-life transactions sometimes involve more than one date - eg the- date you write a cheque, and the date it clears in your bank. When you- want to model this, for more accurate daily balances, you can specify- individual posting dates.-- Or, you can use the older secondary date feature (Ledger calls it aux-- iliary date or effective date). Note: we support this for compatibil-- ity, but I usually recommend avoiding this feature; posting dates are- almost always clearer and simpler.-- A secondary date is written after the primary date, following an equals- sign. If the year is omitted, the primary date's year is assumed.- When running reports, the primary (left) date is used by default, but- with the --date2 flag (or --aux-date or --effective), the secondary- (right) date will be used instead.-- The meaning of secondary dates is up to you, but it's best to follow a- consistent rule. Eg "primary = the bank's clearing date, secondary =- date the transaction was initiated, if different", as shown here:-- 2010/2/23=2/19 movie ticket- expenses:cinema $10- assets:checking-- $ hledger register checking- 2010-02-23 movie ticket assets:checking $-10 $-10-- $ hledger register checking --date2- 2010-02-19 movie ticket assets:checking $-10 $-10-- Posting dates- You can give individual postings a different date from their parent- transaction, by adding a posting comment containing a tag (see below)- like date:DATE. This is probably the best way to control posting dates- precisely. Eg in this example the expense should appear in May- reports, and the deduction from checking should be reported on 6/1 for- easy bank reconciliation:-- 2015/5/30- expenses:food $10 ; food purchased on saturday 5/30- assets:checking ; bank cleared it on monday, date:6/1-- $ hledger -f t.j register food- 2015-05-30 expenses:food $10 $10-- $ hledger -f t.j register checking- 2015-06-01 assets:checking $-10 $-10-- DATE should be a simple date; if the year is not specified it will use- the year of the transaction's date. You can set the secondary date- similarly, with date2:DATE2. The date: or date2: tags must have a- valid simple date value if they are present, eg a date: tag with no- value is not allowed.-- Ledger's earlier, more compact bracketed date syntax is also supported:- [DATE], [DATE=DATE2] or [=DATE2]. hledger will attempt to parse any- square-bracketed sequence of the 0123456789/-.= characters in this way.- With this syntax, DATE infers its year from the transaction and DATE2- infers its year from DATE.--STATUS- Transactions, or individual postings within a transaction, can have a- status mark, which is a single character before the transaction- description or posting account name, separated from it by a space,- indicating one of three statuses:--- mark status- ------------------- unmarked- ! pending- * cleared-- When reporting, you can filter by status with the -U/--unmarked,- -P/--pending, and -C/--cleared flags; or the status:, status:!, and- status:* queries; or the U, P, C keys in hledger-ui.-- Note, in Ledger and in older versions of hledger, the "unmarked" state- is called "uncleared". As of hledger 1.3 we have renamed it to- unmarked for clarity.-- To replicate Ledger and old hledger's behaviour of also matching pend-- ing, combine -U and -P.-- Status marks are optional, but can be helpful eg for reconciling with- real-world accounts. Some editor modes provide highlighting and short-- cuts for working with status. Eg in Emacs ledger-mode, you can toggle- transaction status with C-c C-e, or posting status with C-c C-c.-- What "uncleared", "pending", and "cleared" actually mean is up to you.- Here's one suggestion:--- status meaning- --------------------------------------------------------------------------- uncleared recorded but not yet reconciled; needs review- pending tentatively reconciled (if needed, eg during a big reconcil-- iation)- cleared complete, reconciled as far as possible, and considered cor-- rect-- With this scheme, you would use -PC to see the current balance at your- bank, -U to see things which will probably hit your bank soon (like- uncashed checks), and no flags to see the most up-to-date state of your- finances.--DESCRIPTION- A transaction's description is the rest of the line following the date- and status mark (or until a comment begins). Sometimes called the- "narration" in traditional bookkeeping, it can be used for whatever you- wish, or left blank. Transaction descriptions can be queried, unlike- comments.-- Payee and note- You can optionally include a | (pipe) character in descriptions to sub-- divide the description into separate fields for payee/payer name on the- left (up to the first |) and an additional note field on the right- (after the first |). This may be worthwhile if you need to do more- precise querying and pivoting by payee or by note.--COMMENTS- Lines in the journal beginning with a semicolon (;) or hash (#) or star- (*) are comments, and will be ignored. (Star comments cause org-mode- nodes to be ignored, allowing emacs users to fold and navigate their- journals with org-mode or orgstruct-mode.)-- You can attach comments to a transaction by writing them after the- description and/or indented on the following lines (before the post-- ings). Similarly, you can attach comments to an individual posting by- writing them after the amount and/or indented on the following lines.- Transaction and posting comments must begin with a semicolon (;).-- Some examples:-- # a file comment- ; another file comment- * also a file comment, useful in org/orgstruct mode-- comment- A multiline file comment, which continues- until a line containing just "end comment"- (or end of file).- end comment-- 2012/5/14 something ; a transaction comment- ; the transaction comment, continued- posting1 1 ; a comment for posting 1- posting2- ; a comment for posting 2- ; another comment line for posting 2- ; a file comment (because not indented)-- You can also comment larger regions of a file using comment and end- comment directives.--TAGS- Tags are a way to add extra labels or labelled data to postings and- transactions, which you can then search or pivot on.-- A simple tag is a word (which may contain hyphens) followed by a full- colon, written inside a transaction or posting comment line:-- 2017/1/16 bought groceries ; sometag:-- Tags can have a value, which is the text after the colon, up to the- next comma or end of line, with leading/trailing whitespace removed:-- expenses:food $10 ; a-posting-tag: the tag value-- Note this means hledger's tag values can not contain commas or new-- lines. Ending at commas means you can write multiple short tags on one- line, comma separated:-- assets:checking ; a comment containing tag1:, tag2: some value ...-- Here,-- o "a comment containing" is just comment text, not a tag-- o "tag1" is a tag with no value-- o "tag2" is another tag, whose value is "some value ..."-- Tags in a transaction comment affect the transaction and all of its- postings, while tags in a posting comment affect only that posting.- For example, the following transaction has three tags (A, TAG2, third-- tag) and the posting has four (those plus posting-tag):-- 1/1 a transaction ; A:, TAG2:- ; third-tag: a third transaction tag, <- with a value- (a) $1 ; posting-tag:-- Tags are like Ledger's metadata feature, except hledger's tag values- are simple strings.--POSTINGS- A posting is an addition of some amount to, or removal of some amount- from, an account. Each posting line begins with at least one space or- tab (2 or 4 spaces is common), followed by:-- o (optional) a status character (empty, !, or *), followed by a space-- o (required) an account name (any text, optionally containing single- spaces, until end of line or a double space)-- o (optional) two or more spaces or tabs followed by an amount.-- Positive amounts are being added to the account, negative amounts are- being removed.-- The amounts within a transaction must always sum up to zero. As a con-- venience, one amount may be left blank; it will be inferred so as to- balance the transaction.-- Be sure to note the unusual two-space delimiter between account name- and amount. This makes it easy to write account names containing spa-- ces. But if you accidentally leave only one space (or tab) before the- amount, the amount will be considered part of the account name.-- Virtual postings- A posting with a parenthesised account name is called a virtual posting- or unbalanced posting, which means it is exempt from the usual rule- that a transaction's postings must balance add up to zero.-- This is not part of double entry accounting, so you might choose to- avoid this feature. Or you can use it sparingly for certain special- cases where it can be convenient. Eg, you could set opening balances- without using a balancing equity account:-- 1/1 opening balances- (assets:checking) $1000- (assets:savings) $2000-- A posting with a bracketed account name is called a balanced virtual- posting. The balanced virtual postings in a transaction must add up to- zero (separately from other postings). Eg:-- 1/1 buy food with cash, update budget envelope subaccounts, & something else- assets:cash $-10 ; <- these balance- expenses:food $7 ; <-- expenses:food $3 ; <-- [assets:checking:budget:food] $-10 ; <- and these balance- [assets:checking:available] $10 ; <-- (something:else) $5 ; <- not required to balance-- Ordinary non-parenthesised, non-bracketed postings are called real- postings. You can exclude virtual postings from reports with the- -R/--real flag or real:1 query.--ACCOUNT NAMES- Account names typically have several parts separated by a full colon,- from which hledger derives a hierarchical chart of accounts. They can- be anything you like, but in finance there are traditionally five top-- level accounts: assets, liabilities, income, expenses, and equity.-- Account names may contain single spaces, eg: assets:accounts receiv-- able. Because of this, they must always be followed by two or more- spaces (or newline).-- Account names can be aliased.--AMOUNTS- After the account name, there is usually an amount. (Important:- between account name and amount, there must be two or more spaces.)-- hledger's amount format is flexible, supporting several international- formats. Here are some examples. Amounts have a number (the "quan-- tity"):-- 1-- ..and usually a currency or commodity name (the "commodity"). This is- a symbol, word, or phrase, to the left or right of the quantity, with- or without a separating space:-- $1- 4000 AAPL-- If the commodity name contains spaces, numbers, or punctuation, it must- be enclosed in double quotes:-- 3 "no. 42 green apples"-- Amounts can be preceded by a minus sign (or a plus sign, though plus is- the default), The sign can be written before or after a left-side com-- modity symbol:-- -$1- $-1-- One or more spaces between the sign and the number are acceptable when- parsing (but they won't be displayed in output):-- + $1- $- 1-- Scientific E notation is allowed:-- 1E-6- EUR 1E3-- A decimal mark can be written as a period or a comma:-- 1.23- 1,23456780000009-- Digit group marks- In the integer part of the quantity (left of the decimal mark), groups- of digits can optionally be separated by a "digit group mark" - a- space, comma, or period (different from the decimal mark):-- $1,000,000.00- EUR 2.000.000,00- INR 9,99,99,999.00- 1 000 000.9455-- Note, a number containing a single group mark and no decimal mark is- ambiguous. Are these group marks or decimal marks ?-- 1,000- 1.000-- hledger will treat them both as decimal marks by default (cf #793). If- you use digit group marks, to prevent confusion and undetected typos we- recommend you write commodity directives at the top of the file to- explicitly declare the decimal mark (and optionally a digit group- mark). Note, these formats ("amount styles") are specific to each com-- modity, so if your data uses multiple formats, hledger can handle it:-- commodity $1,000.00- commodity EUR 1.000,00- commodity INR 9,99,99,999.00- commodity 1 000 000.9455--- Commodity display style- For each commodity, hledger chooses a consistent style to use when dis-- playing amounts. (Except price amounts, which are always displayed as- written). The display style is chosen as follows:-- o If there is a commodity directive (or default commodity directive)- for the commodity, its style is used (see examples above).-- o Otherwise the style is inferred from the amounts in that commodity- seen in the journal.-- o Or if there are no such amounts in the journal, a default style is- used (like $1000.00).-- A style is inferred from the journal amounts in a commodity as follows:-- o Use the general style (decimal mark, symbol placement) of the first- amount-- o Use the first-seen digit group style (digit group mark, digit group- sizes), if any-- o Use the maximum number of decimal places of all.-- Transaction price amounts don't affect the commodity display style- directly, but occasionally they can do so indirectly (eg when a post-- ing's amount is inferred using a transaction price). If you find this- causing problems, use a commodity directive to fix the display style.-- In summary, each commodity's amounts will be normalised to-- o the style declared by a commodity directive-- o or, the style of the first posting amount in the journal, with the- first-seen digit group style and the maximum-seen number of decimal- places.-- If reports are showing amounts in a way you don't like (eg, with too- many decimal places), use a commodity directive to set your preferred- style.-- Rounding- Amounts are stored internally as decimal numbers with up to 255 decimal- places, and displayed with the number of decimal places specified by- the commodity display style. Note, hledger uses banker's rounding: it- rounds to the nearest even number, eg 0.5 displayed with zero decimal- places is "0"). (Guaranteed since hledger 1.17.1; in older versions- this could vary if hledger was built with Decimal < 0.5.1.)--TRANSACTION PRICES- Within a transaction, you can note an amount's price in another commod-- ity. This can be used to document the cost (in a purchase) or selling- price (in a sale). For example, transaction prices are useful to- record purchases of a foreign currency. Note transaction prices are- fixed at the time of the transaction, and do not change over time. See- also market prices, which represent prevailing exchange rates on a cer-- tain date.-- There are several ways to record a transaction price:-- 1. Write the price per unit, as @ UNITPRICE after the amount:-- 2009/1/1- assets:euros EUR100 @ $1.35 ; one hundred euros purchased at $1.35 each- assets:dollars ; balancing amount is -$135.00-- 2. Write the total price, as @@ TOTALPRICE after the amount:-- 2009/1/1- assets:euros EUR100 @@ $135 ; one hundred euros purchased at $135 for the lot- assets:dollars-- 3. Specify amounts for all postings, using exactly two commodities, and- let hledger infer the price that balances the transaction:-- 2009/1/1- assets:euros EUR100 ; one hundred euros purchased- assets:dollars $-135 ; for $135-- 4. Like 1, but the @ is parenthesised, i.e. (@); this is for compati-- bility with Ledger journals (Virtual posting costs), and is equiva-- lent to 1 in hledger.-- 5. Like 2, but as in 4 the @@ is parenthesised, i.e. (@@); in hledger,- this is equivalent to 2.-- Use the -B/--cost flag to convert amounts to their transaction price's- commodity, if any. (mnemonic: "B" is from "cost Basis", as in Ledger).- Eg here is how -B affects the balance report for the example above:-- $ hledger bal -N --flat- $-135 assets:dollars- EUR100 assets:euros- $ hledger bal -N --flat -B- $-135 assets:dollars- $135 assets:euros # <- the euros' cost-- Note -B is sensitive to the order of postings when a transaction price- is inferred: the inferred price will be in the commodity of the last- amount. So if example 3's postings are reversed, while the transaction- is equivalent, -B shows something different:-- 2009/1/1- assets:dollars $-135 ; 135 dollars sold- assets:euros EUR100 ; for 100 euros-- $ hledger bal -N --flat -B- EUR-100 assets:dollars # <- the dollars' selling price- EUR100 assets:euros--LOT PRICES, LOT DATES- Ledger allows another kind of price, lot price (four variants: {UNIT-- PRICE}, {{TOTALPRICE}}, {=FIXEDUNITPRICE}, {{=FIXEDTOTALPRICE}}),- and/or a lot date ([DATE]) to be specified. These are normally used to- select a lot when selling investments. hledger will parse these, for- compatibility with Ledger journals, but currently ignores them. A- transaction price, lot price and/or lot date may appear in any order,- after the posting amount and before the balance assertion if any.--BALANCE ASSERTIONS- hledger supports Ledger-style balance assertions in journal files.- These look like, for example, = EXPECTEDBALANCE following a posting's- amount. Eg here we assert the expected dollar balance in accounts a- and b after each posting:-- 2013/1/1- a $1 =$1- b =$-1-- 2013/1/2- a $1 =$2- b $-1 =$-2-- After reading a journal file, hledger will check all balance assertions- and report an error if any of them fail. Balance assertions can pro-- tect you from, eg, inadvertently disrupting reconciled balances while- cleaning up old entries. You can disable them temporarily with the- -I/--ignore-assertions flag, which can be useful for troubleshooting or- for reading Ledger files. (Note: this flag currently does not disable- balance assignments, below).-- Assertions and ordering- hledger sorts an account's postings and assertions first by date and- then (for postings on the same day) by parse order. Note this is dif-- ferent from Ledger, which sorts assertions only by parse order. (Also,- Ledger assertions do not see the accumulated effect of repeated post-- ings to the same account within a transaction.)-- So, hledger balance assertions keep working if you reorder differently-- dated transactions within the journal. But if you reorder same-dated- transactions or postings, assertions might break and require updating.- This order dependence does bring an advantage: precise control over the- order of postings and assertions within a day, so you can assert intra-- day balances.-- Assertions and included files- With included files, things are a little more complicated. Including- preserves the ordering of postings and assertions. If you have multi-- ple postings to an account on the same day, split across different- files, and you also want to assert the account's balance on the same- day, you'll have to put the assertion in the right file.-- Assertions and multiple -f options- Balance assertions don't work well across files specified with multiple- -f options. Use include or concatenate the files instead.-- Assertions and commodities- The asserted balance must be a simple single-commodity amount, and in- fact the assertion checks only this commodity's balance within the- (possibly multi-commodity) account balance. This is how assertions- work in Ledger also. We could call this a "partial" balance assertion.-- To assert the balance of more than one commodity in an account, you can- write multiple postings, each asserting one commodity's balance.-- You can make a stronger "total" balance assertion by writing a double- equals sign (== EXPECTEDBALANCE). This asserts that there are no other- unasserted commodities in the account (or, that their balance is 0).-- 2013/1/1- a $1- a 1EUR- b $-1- c -1EUR-- 2013/1/2 ; These assertions succeed- a 0 = $1- a 0 = 1EUR- b 0 == $-1- c 0 == -1EUR-- 2013/1/3 ; This assertion fails as 'a' also contains 1EUR- a 0 == $1-- It's not yet possible to make a complete assertion about a balance that- has multiple commodities. One workaround is to isolate each commodity- into its own subaccount:-- 2013/1/1- a:usd $1- a:euro 1EUR- b-- 2013/1/2- a 0 == 0- a:usd 0 == $1- a:euro 0 == 1EUR-- Assertions and prices- Balance assertions ignore transaction prices, and should normally be- written without one:-- 2019/1/1- (a) $1 @ EUR1 = $1-- We do allow prices to be written there, however, and print shows them,- even though they don't affect whether the assertion passes or fails.- This is for backward compatibility (hledger's close command used to- generate balance assertions with prices), and because balance assign-- ments do use them (see below).-- Assertions and subaccounts- The balance assertions above (= and ==) do not count the balance from- subaccounts; they check the account's exclusive balance only. You can- assert the balance including subaccounts by writing =* or ==*, eg:-- 2019/1/1- equity:opening balances- checking:a 5- checking:b 5- checking 1 ==* 11-- Assertions and virtual postings- Balance assertions are checked against all postings, both real and vir-- tual. They are not affected by the --real/-R flag or real: query.-- Assertions and precision- Balance assertions compare the exactly calculated amounts, which are- not always what is shown by reports. Eg a commodity directive may- limit the display precision, but this will not affect balance asser-- tions. Balance assertion failure messages show exact amounts.--BALANCE ASSIGNMENTS- Ledger-style balance assignments are also supported. These are like- balance assertions, but with no posting amount on the left side of the- equals sign; instead it is calculated automatically so as to satisfy- the assertion. This can be a convenience during data entry, eg when- setting opening balances:-- ; starting a new journal, set asset account balances- 2016/1/1 opening balances- assets:checking = $409.32- assets:savings = $735.24- assets:cash = $42- equity:opening balances-- or when adjusting a balance to reality:-- ; no cash left; update balance, record any untracked spending as a generic expense- 2016/1/15- assets:cash = $0- expenses:misc-- The calculated amount depends on the account's balance in the commodity- at that point (which depends on the previously-dated postings of the- commodity to that account since the last balance assertion or assign-- ment). Note that using balance assignments makes your journal a little- less explicit; to know the exact amount posted, you have to run hledger- or do the calculations yourself, instead of just reading it.-- Balance assignments and prices- A transaction price in a balance assignment will cause the calculated- amount to have that price attached:-- 2019/1/1- (a) = $1 @ EUR2-- $ hledger print --explicit- 2019-01-01- (a) $1 @ EUR2 = $1 @ EUR2--DIRECTIVES- A directive is a line in the journal beginning with a special keyword,- that influences how the journal is processed. hledger's directives are- based on a subset of Ledger's, but there are many differences (and also- some differences between hledger versions).-- Directives' behaviour and interactions can get a little bit complex, so- here is a table summarising the directives and their effects, with- links to more detailed docs. Note part of this table is hidden when- viewed in a web browser - scroll it sideways to see more.--- direc- end subdi- purpose can affect (as of- tive directive rec- 2018/06)- tives- ------------------------------------------------------------------------------------- account any document account names, all entries in all- text declare account types & dis- files, before or- play order after- alias end rewrite account names following entries- aliases until end of cur-- rent file or end- directive- apply end apply prepend a common parent to following entries- account account account names until end of cur-- rent file or end- directive- comment end com- ignore part of journal following entries- ment until end of cur-- rent file or end- directive- commod- format declare a commodity and its number notation:- ity number notation & display following entries- style in that commodity- in all files ; dis-- play style: amounts- of that commodity- in reports------------- D declare a commodity to be default commodity:- used for commodityless following commod-- amounts, and its number ityless entries- notation & display style until end of cur-- rent file; number- notation: following- entries in that- commodity until end- of current file;- display style:- amounts of that- commodity in- reports- include include entries/directives what the included- from another file directives affect- P declare a market price for a amounts of that- commodity commodity in- reports, when -V is- used- Y declare a year for yearless following entries- dates until end of cur-- rent file- = declare an auto posting all entries in par-- rule, adding postings to ent/current/child- other transactions files (but not sib-- ling files, see- #1212)-- And some definitions:--- subdi- optional indented directive line immediately following a parent- rec- directive- tive- number how to interpret numbers when parsing journal entries (the iden-- nota- tity of the decimal separator character). (Currently each com-- tion modity can have its own notation, even in the same file.)- dis- how to display amounts of a commodity in reports (symbol side- play and spacing, digit groups, decimal separator, decimal places)- style- direc- which entries and (when there are multiple files) which files- tive are affected by a directive- scope-- As you can see, directives vary in which journal entries and files they- affect, and whether they are focussed on input (parsing) or output- (reports). Some directives have multiple effects.-- Directives and multiple files- If you use multiple -f/--file options, or the include directive,- hledger will process multiple input files. But note that directives- which affect input (see above) typically last only until the end of the- file in which they occur.-- This may seem inconvenient, but it's intentional; it makes reports sta-- ble and deterministic, independent of the order of input. Otherwise- you could see different numbers if you happened to write -f options in- a different order, or if you moved includes around while cleaning up- your files.-- It can be surprising though; for example, it means that alias direc-- tives do not affect parent or sibling files (see below).-- Comment blocks- A line containing just comment starts a commented region of the file,- and a line containing just end comment (or the end of the current file)- ends it. See also comments.-- Including other files- You can pull in the content of additional files by writing an include- directive, like this:-- include FILEPATH-- Only journal files can include, and only journal, timeclock or timedot- files can be included (not CSV files, currently).-- If the file path does not begin with a slash, it is relative to the- current file's folder.-- A tilde means home directory, eg: include ~/main.journal.-- The path may contain glob patterns to match multiple files, eg: include- *.journal.-- There is limited support for recursive wildcards: **/ (the slash is- required) matches 0 or more subdirectories. It's not super convenient- since you have to avoid include cycles and including directories, but- this can be done, eg: include */**/*.journal.-- The path may also be prefixed to force a specific file format, overrid-- ing the file extension (as described in hledger.1 -> Input files):- include timedot:~/notes/2020*.md.-- Default year- You can set a default year to be used for subsequent dates which don't- specify a year. This is a line beginning with Y followed by the year.- Eg:-- Y2009 ; set default year to 2009-- 12/15 ; equivalent to 2009/12/15- expenses 1- assets-- Y2010 ; change default year to 2010-- 2009/1/30 ; specifies the year, not affected- expenses 1- assets-- 1/31 ; equivalent to 2010/1/31- expenses 1- assets-- Declaring commodities- The commodity directive has several functions:-- 1. It declares commodities which may be used in the journal. This is- currently not enforced, but can serve as documentation.-- 2. It declares what decimal mark character (period or comma) to expect- when parsing input - useful to disambiguate international number- formats in your data. (Without this, hledger will parse both 1,000- and 1.000 as 1).-- 3. It declares a commodity's display style in output - decimal and- digit group marks, number of decimal places, symbol placement etc.-- You are likely to run into one of the problems solved by commodity- directives, sooner or later, so it's a good idea to just always use- them to declare your commodities.-- A commodity directive is just the word commodity followed by an amount.- It may be written on a single line, like this:-- ; commodity EXAMPLEAMOUNT-- ; display AAAA amounts with the symbol on the right, space-separated,- ; using period as decimal point, with four decimal places, and- ; separating thousands with comma.- commodity 1,000.0000 AAAA-- or on multiple lines, using the "format" subdirective. (In this case- the commodity symbol appears twice and should be the same in both- places.):-- ; commodity SYMBOL- ; format EXAMPLEAMOUNT-- ; display indian rupees with currency name on the left,- ; thousands, lakhs and crores comma-separated,- ; period as decimal point, and two decimal places.- commodity INR- format INR 1,00,00,000.00-- The quantity of the amount does not matter; only the format is signifi-- cant. The number must include a decimal mark: either a period or a- comma, followed by 0 or more decimal digits.-- Note hledger normally uses banker's rounding, so 0.5 displayed with- zero decimal digits is "0". (More at Commodity display style.)-- Commodity error checking- In strict mode, enabled with the -s/--strict flag, hledger will report- an error if a commodity symbol is used that has not been declared by a- commodity directive. This works similarly to account error checking,- see the notes there for more details.-- Default commodity- The D directive sets a default commodity, to be used for amounts with-- out a commodity symbol (ie, plain numbers). This commodity will be- applied to all subsequent commodity-less amounts, or until the next D- directive. (Note, this is different from Ledger's D.)-- For compatibility/historical reasons, D also acts like a commodity- directive, setting the commodity's display style (for output) and deci-- mal mark (for parsing input). As with commodity, the amount must- always be written with a decimal mark (period or comma). If both- directives are used, commodity's style takes precedence.-- The syntax is D AMOUNT. Eg:-- ; commodity-less amounts should be treated as dollars- ; (and displayed with the dollar sign on the left, thousands separators and two decimal places)- D $1,000.00-- 1/1- a 5 ; <- commodity-less amount, parsed as $5 and displayed as $5.00- b-- Declaring market prices- The P directive declares a market price, which is an exchange rate- between two commodities on a certain date. (In Ledger, they are called- "historical prices".) These are often obtained from a stock exchange,- cryptocurrency exchange, or the foreign exchange market.-- Here is the format:-- P DATE COMMODITYA COMMODITYBAMOUNT-- o DATE is a simple date-- o COMMODITYA is the symbol of the commodity being priced-- o COMMODITYBAMOUNT is an amount (symbol and quantity) in a second com-- modity, giving the price in commodity B of one unit of commodity A.-- These two market price directives say that one euro was worth 1.35 US- dollars during 2009, and $1.40 from 2010 onward:-- P 2009/1/1 EUR $1.35- P 2010/1/1 EUR $1.40-- The -V, -X and --value flags use these market prices to show amount- values in another commodity. See Valuation.-- Declaring accounts- account directives can be used to declare accounts (ie, the places that- amounts are transferred from and to). Though not required, these dec-- larations can provide several benefits:-- o They can document your intended chart of accounts, providing a refer-- ence.-- o They can help hledger know your accounts' types (asset, liability,- equity, revenue, expense), useful for reports like balancesheet and- incomestatement.-- o They control account display order in reports, allowing non-alpha-- betic sorting (eg Revenues to appear above Expenses).-- o They can store extra information about accounts (account numbers,- notes, etc.)-- o They help with account name completion in the add command, hledger-- iadd, hledger-web, ledger-mode etc.-- o In strict mode, they restrict which accounts may be posted to by- transactions, which helps detect typos.-- The simplest form is just the word account followed by a hledger-style- account name, eg this account directive declares the assets:bank:check-- ing account:-- account assets:bank:checking-- Account error checking- By default, accounts come into existence when a transaction references- them by name. This is convenient, but it means hledger can't warn you- when you mis-spell an account name in the journal. Usually you'll find- the error later, as an extra account in balance reports, or an incor-- rect balance when reconciling.-- In strict mode, enabled with the -s/--strict flag, hledger will report- an error if any transaction uses an account name that has not been- declared by an account directive. Some notes:-- o The declaration is case-sensitive; transactions must use the correct- account name capitalisation.-- o The account directive's scope is "whole file and below" (see direc-- tives). This means it affects all of the current file, and any files- it includes, but not parent or sibling files. The position of- account directives within the file does not matter, though it's usual- to put them at the top.-- o Accounts can only be declared in journal files (but will affect- included files in other formats).-- o It's currently not possible to declare "all possible subaccounts"- with a wildcard; every account posted to must be declared.-- Account comments- Comments, beginning with a semicolon, can be added:-- o on the same line, after two or more spaces (because ; is allowed in- account names)-- o on the next lines, indented-- An example of both:-- account assets:bank:checking ; same-line comment, note 2+ spaces before ;- ; next-line comment- ; another with tag, acctno:12345 (not used yet)-- Same-line comments are not supported by Ledger, or hledger <1.13.-- Account subdirectives- We also allow (and ignore) Ledger-style indented subdirectives, just- for compatibility.:-- account assets:bank:checking- format blah blah ; <- subdirective, ignored-- Here is the full syntax of account directives:-- account ACCTNAME [ACCTTYPE] [;COMMENT]- [;COMMENTS]- [LEDGER-STYLE SUBDIRECTIVES, IGNORED]-- Account types- hledger recognises five main types of account, corresponding to the- account classes in the accounting equation:-- Asset, Liability, Equity, Revenue, Expense.-- These account types are important for controlling which accounts appear- in the balancesheet, balancesheetequity, incomestatement reports (and- probably for other things in future).-- Additionally, we recognise the Cash type, which is also an Asset, and- which causes accounts to appear in the cashflow report. ("Cash" here- means liquid assets, eg bank balances but typically not investments or- receivables.)-- Declaring account types- Generally, to make these reports work you should declare your top-level- accounts and their types, using account directives with type: tags.-- The tag's value should be one of: Asset, Liability, Equity, Revenue,- Expense, Cash, A, L, E, R, X, C (all case insensitive). The type is- inherited by all subaccounts except where they override it. Here's a- complete example:-- account assets ; type: Asset- account assets:bank ; type: Cash- account assets:cash ; type: Cash- account liabilities ; type: Liability- account equity ; type: Equity- account revenues ; type: Revenue- account expenses ; type: Expense-- Auto-detected account types- If you happen to use common english top-level account names, you may- not need to declare account types, as they will be detected automati-- cally using the following rules:--- If name matches regular account type is:- expression:- ----------------------------------------------- ^assets?(:|$) Asset- ^(debts?|lia- Liability- bilit(y|ies))(:|$)- ^equity(:|$) Equity- ^(income|revenue)s?(:|$) Revenue- ^expenses?(:|$) Expense--- If account type is Asset and name does not contain regu- account type- lar expression: is:- --------------------------------------------------------------------------- (investment|receivable|:A/R|:fixed) Cash-- Even so, explicit declarations may be a good idea, for clarity and pre-- dictability.-- Interference from auto-detected account types- If you assign any account type, it's a good idea to assign all of them,- to prevent any confusion from mixing declared and auto-detected types.- Although it's unlikely to happen in real life, here's an example: with- the following journal, balancesheetequity shows "liabilities" in both- Liabilities and Equity sections. Declaring another account as- type:Liability would fix it:-- account liabilities ; type:Equity-- 2020-01-01- assets 1- liabilities 1- equity -2-- Old account type syntax- In some hledger journals you might instead see this old syntax (the- letters ALERX, separated from the account name by two or more spaces);- this is deprecated and may be removed soon:-- account assets A- account liabilities L- account equity E- account revenues R- account expenses X-- Account display order- Account directives also set the order in which accounts are displayed,- eg in reports, the hledger-ui accounts screen, and the hledger-web- sidebar. By default accounts are listed in alphabetical order. But if- you have these account directives in the journal:-- account assets- account liabilities- account equity- account revenues- account expenses-- you'll see those accounts displayed in declaration order, not alphabet-- ically:-- $ hledger accounts -1- assets- liabilities- equity- revenues- expenses-- Undeclared accounts, if any, are displayed last, in alphabetical order.-- Note that sorting is done at each level of the account tree (within- each group of sibling accounts under the same parent). And currently,- this directive:-- account other:zoo-- would influence the position of zoo among other's subaccounts, but not- the position of other among the top-level accounts. This means:-- o you will sometimes declare parent accounts (eg account other above)- that you don't intend to post to, just to customize their display- order-- o sibling accounts stay together (you couldn't display x:y in between- a:b and a:c).-- Rewriting accounts- You can define account alias rules which rewrite your account names, or- parts of them, before generating reports. This can be useful for:-- o expanding shorthand account names to their full form, allowing easier- data entry and a less verbose journal-- o adapting old journals to your current chart of accounts-- o experimenting with new account organisations, like a new hierarchy or- combining two accounts into one-- o customising reports-- Account aliases also rewrite account names in account directives. They- do not affect account names being entered via hledger add or hledger-- web.-- See also Rewrite account names.-- Basic aliases- To set an account alias, use the alias directive in your journal file.- This affects all subsequent journal entries in the current file or its- included files. The spaces around the = are optional:-- alias OLD = NEW-- Or, you can use the --alias 'OLD=NEW' option on the command line. This- affects all entries. It's useful for trying out aliases interactively.-- OLD and NEW are case sensitive full account names. hledger will- replace any occurrence of the old account name with the new one. Sub-- accounts are also affected. Eg:-- alias checking = assets:bank:wells fargo:checking- ; rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"-- Regex aliases- There is also a more powerful variant that uses a regular expression,- indicated by the forward slashes:-- alias /REGEX/ = REPLACEMENT-- or --alias '/REGEX/=REPLACEMENT'.-- REGEX is a case-insensitive regular expression. Anywhere it matches- inside an account name, the matched part will be replaced by REPLACE-- MENT. If REGEX contains parenthesised match groups, these can be ref-- erenced by the usual numeric backreferences in REPLACEMENT. Eg:-- alias /^(.+):bank:([^:]+):(.*)/ = \1:\2 \3- ; rewrites "assets:bank:wells fargo:checking" to "assets:wells fargo checking"-- Also note that REPLACEMENT continues to the end of line (or on command- line, to end of option argument), so it can contain trailing white-- space.-- Combining aliases- You can define as many aliases as you like, using journal directives- and/or command line options.-- Recursive aliases - where an account name is rewritten by one alias,- then by another alias, and so on - are allowed. Each alias sees the- effect of previously applied aliases.-- In such cases it can be important to understand which aliases will be- applied and in which order. For (each account name in) each journal- entry, we apply:-- 1. alias directives preceding the journal entry, most recently parsed- first (ie, reading upward from the journal entry, bottom to top)-- 2. --alias options, in the order they appeared on the command line- (left to right).-- In other words, for (an account name in) a given journal entry:-- o the nearest alias declaration before/above the entry is applied first-- o the next alias before/above that will be be applied next, and so on-- o aliases defined after/below the entry do not affect it.-- This gives nearby aliases precedence over distant ones, and helps pro-- vide semantic stability - aliases will keep working the same way inde-- pendent of which files are being read and in which order.-- In case of trouble, adding --debug=6 to the command line will show- which aliases are being applied when.-- Aliases and multiple files- As explained at Directives and multiple files, alias directives do not- affect parent or sibling files. Eg in this command,-- hledger -f a.aliases -f b.journal-- account aliases defined in a.aliases will not affect b.journal.- Including the aliases doesn't work either:-- include a.aliases-- 2020-01-01 ; not affected by a.aliases- foo 1- bar-- This means that account aliases should usually be declared at the start- of your top-most file, like this:-- alias foo=Foo- alias bar=Bar-- 2020-01-01 ; affected by aliases above- foo 1- bar-- include c.journal ; also affected-- end aliases- You can clear (forget) all currently defined aliases with the end- aliases directive:-- end aliases-- Default parent account- You can specify a parent account which will be prepended to all- accounts within a section of the journal. Use the apply account and- end apply account directives like so:-- apply account home-- 2010/1/1- food $10- cash-- end apply account-- which is equivalent to:-- 2010/01/01- home:food $10- home:cash $-10-- If end apply account is omitted, the effect lasts to the end of the- file. Included files are also affected, eg:-- apply account business- include biz.journal- end apply account- apply account personal- include personal.journal-- Prior to hledger 1.0, legacy account and end spellings were also sup-- ported.-- A default parent account also affects account directives. It does not- affect account names being entered via hledger add or hledger-web. If- account aliases are present, they are applied after the default parent- account.--PERIODIC TRANSACTIONS- Periodic transaction rules describe transactions that recur. They- allow hledger to generate temporary future transactions to help with- forecasting, so you don't have to write out each one in the journal,- and it's easy to try out different forecasts.-- Periodic transactions can be a little tricky, so before you use them,- read this whole section - or at least these tips:-- 1. Two spaces accidentally added or omitted will cause you trouble -- read about this below.-- 2. For troubleshooting, show the generated transactions with hledger- print --forecast tag:generated or hledger register --forecast- tag:generated.-- 3. Forecasted transactions will begin only after the last non-fore-- casted transaction's date.-- 4. Forecasted transactions will end 6 months from today, by default.- See below for the exact start/end rules.-- 5. period expressions can be tricky. Their documentation needs- improvement, but is worth studying.-- 6. Some period expressions with a repeating interval must begin on a- natural boundary of that interval. Eg in weekly from DATE, DATE- must be a monday. ~ weekly from 2019/10/1 (a tuesday) will give an- error.-- 7. Other period expressions with an interval are automatically expanded- to cover a whole number of that interval. (This is done to improve- reports, but it also affects periodic transactions. Yes, it's a bit- inconsistent with the above.) Eg: ~ every 10th day of month from- 2020/01, which is equivalent to ~ every 10th day of month from- 2020/01/01, will be adjusted to start on 2019/12/10.-- Periodic transaction rules also have a second meaning: they are used to- define budget goals, shown in budget reports.-- Periodic rule syntax- A periodic transaction rule looks like a normal journal entry, with the- date replaced by a tilde (~) followed by a period expression (mnemonic:- ~ looks like a recurring sine wave.):-- ~ monthly- expenses:rent $2000- assets:bank:checking-- There is an additional constraint on the period expression: the start- date must fall on a natural boundary of the interval. Eg monthly from- 2018/1/1 is valid, but monthly from 2018/1/15 is not.-- Partial or relative dates (M/D, D, tomorrow, last week) in the period- expression can work (useful or not). They will be relative to today's- date, unless a Y default year directive is in effect, in which case- they will be relative to Y/1/1.-- Two spaces between period expression and description!- If the period expression is followed by a transaction description,- these must be separated by two or more spaces. This helps hledger know- where the period expression ends, so that descriptions can not acciden-- tally alter their meaning, as in this example:-- ; 2 or more spaces needed here, so the period is not understood as "every 2 months in 2020"- ; ||- ; vv- ~ every 2 months in 2020, we will review- assets:bank:checking $1500- income:acme inc-- So,-- o Do write two spaces between your period expression and your transac-- tion description, if any.-- o Don't accidentally write two spaces in the middle of your period- expression.-- Forecasting with periodic transactions- The --forecast flag activates any periodic transaction rules in the- journal. They will generate temporary recurring transactions, which- are not saved in the journal, but will appear in all reports (eg- print). This can be useful for estimating balances into the future, or- experimenting with different scenarios. Or, it can be used as a data- entry aid: describe recurring transactions, and every so often copy the- output of print --forecast into the journal.-- These transactions will have an extra tag indicating which periodic- rule generated them: generated-transaction:~ PERIODICEXPR. And a simi-- lar, hidden tag (beginning with an underscore) which, because it's- never displayed by print, can be used to match transactions generated- "just now": _generated-transaction:~ PERIODICEXPR.-- Periodic transactions are generated within some forecast period. By- default, this-- o begins on the later of-- o the report start date if specified with -b/-p/date:-- o the day after the latest normal (non-periodic) transaction in the- journal, or today if there are no normal transactions.-- o ends on the report end date if specified with -e/-p/date:, or 6- months (180 days) from today.-- This means that periodic transactions will begin only after the latest- recorded transaction. And a recorded transaction dated in the future- can prevent generation of periodic transactions. (You can avoid that- by writing the future transaction as a one-time periodic rule instead -- put tilde before the date, eg ~ YYYY-MM-DD ...).-- Or, you can set your own arbitrary "forecast period", which can overlap- recorded transactions, and need not be in the future, by providing an- option argument, like --forecast=PERIODEXPR. Note the equals sign is- required, a space won't work. PERIODEXPR is a period expression, which- can specify the start date, end date, or both, like in a date: query.- (See also hledger.1 -> Report start & end date). Some examples:- --forecast=202001-202004, --forecast=jan-, --forecast=2020.-- Budgeting with periodic transactions- With the --budget flag, currently supported by the balance command,- each periodic transaction rule declares recurring budget goals for the- specified accounts. Eg the first example above declares a goal of- spending $2000 on rent (and also, a goal of depositing $2000 into- checking) every month. Goals and actual performance can then be com-- pared in budget reports.-- See also: Budgeting and Forecasting.---AUTO POSTINGS- "Automated postings" or "auto postings" are extra postings which get- added automatically to transactions which match certain queries,- defined by "auto posting rules", when you use the --auto flag.-- An auto posting rule looks a bit like a transaction:-- = QUERY- ACCOUNT AMOUNT- ...- ACCOUNT [AMOUNT]-- except the first line is an equals sign (mnemonic: = suggests match-- ing), followed by a query (which matches existing postings), and each- "posting" line describes a posting to be generated, and the posting- amounts can be:-- o a normal amount with a commodity symbol, eg $2. This will be used- as-is.-- o a number, eg 2. The commodity symbol (if any) from the matched post-- ing will be added to this.-- o a numeric multiplier, eg *2 (a star followed by a number N). The- matched posting's amount (and total price, if any) will be multiplied- by N.-- o a multiplier with a commodity symbol, eg *$2 (a star, number N, and- symbol S). The matched posting's amount will be multiplied by N, and- its commodity symbol will be replaced with S.-- Any query term containing spaces must be enclosed in single or double- quotes, as on the command line. Eg, note the quotes around the second- query term below:-- = expenses:groceries 'expenses:dining out'- (budget:funds:dining out) *-1-- Some examples:-- ; every time I buy food, schedule a dollar donation- = expenses:food- (liabilities:charity) $-1-- ; when I buy a gift, also deduct that amount from a budget envelope subaccount- = expenses:gifts- assets:checking:gifts *-1- assets:checking *1-- 2017/12/1- expenses:food $10- assets:checking-- 2017/12/14- expenses:gifts $20- assets:checking-- $ hledger print --auto- 2017-12-01- expenses:food $10- assets:checking- (liabilities:charity) $-1-- 2017-12-14- expenses:gifts $20- assets:checking- assets:checking:gifts -$20- assets:checking $20-- Auto postings and multiple files- An auto posting rule can affect any transaction in the current file, or- in any parent file or child file. Note, currently it will not affect- sibling files (when multiple -f/--file are used - see #1212).-- Auto postings and dates- A posting date (or secondary date) in the matched posting, or (taking- precedence) a posting date in the auto posting rule itself, will also- be used in the generated posting.-- Auto postings and transaction balancing / inferred amounts / balance asser-- tions- Currently, auto postings are added:-- o after missing amounts are inferred, and transactions are checked for- balancedness,-- o but before balance assertions are checked.-- Note this means that journal entries must be balanced both before and- after auto postings are added. This changed in hledger 1.12+; see #893- for background.-- Auto posting tags- Automated postings will have some extra tags:-- o generated-posting:= QUERY - shows this was generated by an auto post-- ing rule, and the query-- o _generated-posting:= QUERY - a hidden tag, which does not appear in- hledger's output. This can be used to match postings generated "just- now", rather than generated in the past and saved to the journal.-- Also, any transaction that has been changed by auto posting rules will- have these tags added:-- o modified: - this transaction was modified-- o _modified: - a hidden tag not appearing in the comment; this transac-- tion was modified "just now".----REPORTING BUGS- Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel- or hledger mail list)---AUTHORS- Simon Michael <simon@joyful.com> and contributors---COPYRIGHT- Copyright (C) 2007-2020 Simon Michael.- Released under GNU GPL v3 or later.---SEE ALSO- hledger(1), hledger-ui(1), hledger-web(1), ledger(1)-- hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_time-- dot(5)----hledger-lib-1.20.4 December 2020 HLEDGER_JOURNAL(5)
− hledger_timeclock.5
@@ -1,90 +0,0 @@--.TH "HLEDGER_TIMECLOCK" "5" "December 2020" "hledger-lib-1.20.4 " "hledger User Manuals"----.SH NAME-.PP-The time logging format of timeclock.el, as read by hledger.-.SH DESCRIPTION-.PP-hledger can read timeclock files.-As with Ledger, these are (a subset of) timeclock.el\[aq]s format,-containing clock-in and clock-out entries as in the example below.-The date is a simple date.-The time format is HH:MM[:SS][+-ZZZZ].-Seconds and timezone are optional.-The timezone, if present, must be four digits and is ignored (currently-the time is always interpreted as a local time).-.IP-.nf-\f[C]-i 2015/03/30 09:00:00 some:account name optional description after two spaces-o 2015/03/30 09:20:00-i 2015/03/31 22:21:45 another account-o 2015/04/01 02:00:34-\f[R]-.fi-.PP-hledger treats each clock-in/clock-out pair as a transaction posting-some number of hours to an account.-Or if the session spans more than one day, it is split into several-transactions, one for each day.-For the above time log, \f[C]hledger print\f[R] generates these journal-entries:-.IP-.nf-\f[C]-$ hledger -f t.timeclock print-2015-03-30 * optional description after two spaces- (some:account name) 0.33h--2015-03-31 * 22:21-23:59- (another account) 1.64h--2015-04-01 * 00:00-02:00- (another account) 2.01h-\f[R]-.fi-.PP-Here is a sample.timeclock to download and some queries to try:-.IP-.nf-\f[C]-$ hledger -f sample.timeclock balance # current time balances-$ hledger -f sample.timeclock register -p 2009/3 # sessions in march 2009-$ hledger -f sample.timeclock register -p weekly --depth 1 --empty # time summary by week-\f[R]-.fi-.PP-To generate time logs, ie to clock in and clock out, you could:-.IP \[bu] 2-use emacs and the built-in timeclock.el, or the extended timeclock-x.el-and perhaps the extras in ledgerutils.el-.IP \[bu] 2-at the command line, use these bash aliases:-\f[C]shell alias ti=\[dq]echo i \[ga]date \[aq]+%Y-%m-%d %H:%M:%S\[aq]\[ga] \[rs]$* >>$TIMELOG\[dq] alias to=\[dq]echo o \[ga]date \[aq]+%Y-%m-%d %H:%M:%S\[aq]\[ga] >>$TIMELOG\[dq]\f[R]-.IP \[bu] 2-or use the old \f[C]ti\f[R] and \f[C]to\f[R] scripts in the ledger 2.x-repository.-These rely on a \[dq]timeclock\[dq] executable which I think is just the-ledger 2 executable renamed.---.SH "REPORTING BUGS"-Report bugs at http://bugs.hledger.org-(or on the #hledger IRC channel or hledger mail list)--.SH AUTHORS-Simon Michael <simon@joyful.com> and contributors--.SH COPYRIGHT--Copyright (C) 2007-2020 Simon Michael.-.br-Released under GNU GPL v3 or later.--.SH SEE ALSO-hledger(1), hledger\-ui(1), hledger\-web(1), ledger(1)--hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_timedot(5)
− hledger_timeclock.info
@@ -1,67 +0,0 @@-This is hledger-lib/hledger_timeclock.info, produced by makeinfo-version 4.8 from stdin.---File: hledger_timeclock.info, Node: Top, Up: (dir)--hledger_timeclock(5)-********************--The time logging format of timeclock.el, as read by hledger.-- hledger can read timeclock files. As with Ledger, these are (a subset-of) timeclock.el's format, containing clock-in and clock-out entries as-in the example below. The date is a simple date. The time format is-HH:MM[:SS][+-ZZZZ]. Seconds and timezone are optional. The timezone, if-present, must be four digits and is ignored (currently the time is-always interpreted as a local time).---i 2015/03/30 09:00:00 some:account name optional description after two spaces-o 2015/03/30 09:20:00-i 2015/03/31 22:21:45 another account-o 2015/04/01 02:00:34-- hledger treats each clock-in/clock-out pair as a transaction posting-some number of hours to an account. Or if the session spans more than-one day, it is split into several transactions, one for each day. For-the above time log, `hledger print' generates these journal entries:---$ hledger -f t.timeclock print-2015-03-30 * optional description after two spaces- (some:account name) 0.33h--2015-03-31 * 22:21-23:59- (another account) 1.64h--2015-04-01 * 00:00-02:00- (another account) 2.01h-- Here is a sample.timeclock to download and some queries to try:---$ hledger -f sample.timeclock balance # current time balances-$ hledger -f sample.timeclock register -p 2009/3 # sessions in march 2009-$ hledger -f sample.timeclock register -p weekly --depth 1 --empty # time summary by week-- To generate time logs, ie to clock in and clock out, you could:-- * use emacs and the built-in timeclock.el, or the extended- timeclock-x.el and perhaps the extras in ledgerutils.el-- * at the command line, use these bash aliases: `shell alias- ti="echo i `date '+%Y-%m-%d %H:%M:%S'` \$* >>$TIMELOG" alias- to="echo o `date '+%Y-%m-%d %H:%M:%S'` >>$TIMELOG"'-- * or use the old `ti' and `to' scripts in the ledger 2.x repository.- These rely on a "timeclock" executable which I think is just the- ledger 2 executable renamed.-----Tag Table:-Node: Top90--End Tag Table
− hledger_timeclock.txt
@@ -1,80 +0,0 @@--HLEDGER_TIMECLOCK(5) hledger User Manuals HLEDGER_TIMECLOCK(5)----NAME- The time logging format of timeclock.el, as read by hledger.--DESCRIPTION- hledger can read timeclock files. As with Ledger, these are (a subset- of) timeclock.el's format, containing clock-in and clock-out entries as- in the example below. The date is a simple date. The time format is- HH:MM[:SS][+-ZZZZ]. Seconds and timezone are optional. The timezone,- if present, must be four digits and is ignored (currently the time is- always interpreted as a local time).-- i 2015/03/30 09:00:00 some:account name optional description after two spaces- o 2015/03/30 09:20:00- i 2015/03/31 22:21:45 another account- o 2015/04/01 02:00:34-- hledger treats each clock-in/clock-out pair as a transaction posting- some number of hours to an account. Or if the session spans more than- one day, it is split into several transactions, one for each day. For- the above time log, hledger print generates these journal entries:-- $ hledger -f t.timeclock print- 2015-03-30 * optional description after two spaces- (some:account name) 0.33h-- 2015-03-31 * 22:21-23:59- (another account) 1.64h-- 2015-04-01 * 00:00-02:00- (another account) 2.01h-- Here is a sample.timeclock to download and some queries to try:-- $ hledger -f sample.timeclock balance # current time balances- $ hledger -f sample.timeclock register -p 2009/3 # sessions in march 2009- $ hledger -f sample.timeclock register -p weekly --depth 1 --empty # time summary by week-- To generate time logs, ie to clock in and clock out, you could:-- o use emacs and the built-in timeclock.el, or the extended timeclock-- x.el and perhaps the extras in ledgerutils.el-- o at the command line, use these bash aliases: shell alias ti="echo- i `date '+%Y-%m-%d %H:%M:%S'` \$* >>$TIMELOG" alias to="echo o- `date '+%Y-%m-%d %H:%M:%S'` >>$TIMELOG"-- o or use the old ti and to scripts in the ledger 2.x repository. These- rely on a "timeclock" executable which I think is just the ledger 2- executable renamed.----REPORTING BUGS- Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel- or hledger mail list)---AUTHORS- Simon Michael <simon@joyful.com> and contributors---COPYRIGHT- Copyright (C) 2007-2020 Simon Michael.- Released under GNU GPL v3 or later.---SEE ALSO- hledger(1), hledger-ui(1), hledger-web(1), ledger(1)-- hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_time-- dot(5)----hledger-lib-1.20.4 December 2020 HLEDGER_TIMECLOCK(5)
− hledger_timedot.5
@@ -1,199 +0,0 @@--.TH "HLEDGER_TIMEDOT" "5" "December 2020" "hledger-lib-1.20.4 " "hledger User Manuals"----.SH NAME-.PP-hledger\[aq]s human-friendly time logging format.-.SH DESCRIPTION-.PP-Timedot is a plain text format for logging dated, categorised quantities-(of time, usually), supported by hledger.-It is convenient for approximate and retroactive time logging, eg when-the real-time clock-in/out required with a timeclock file is too precise-or too interruptive.-It can be formatted like a bar chart, making clear at a glance where-time was spent.-.PP-Though called \[dq]timedot\[dq], this format is read by hledger as-commodityless quantities, so it could be used to represent dated-quantities other than time.-In the docs below we\[aq]ll assume it\[aq]s time.-.PP-A timedot file contains a series of day entries.-A day entry begins with a non-indented hledger-style simple date (Y-M-D,-Y/M/D, Y.M.D..) Any additional text on the same line is used as a-transaction description for this day.-.PP-This is followed by optionally-indented timelog items for that day, one-per line.-Each timelog item is a note, usually a hledger:style:account:name-representing a time category, followed by two or more spaces, and a-quantity.-Each timelog item generates a hledger transaction.-.PP-Quantities can be written as:-.IP \[bu] 2-dots: a sequence of dots (.) representing quarter hours.-Spaces may optionally be used for grouping.-Eg: ....-\&..-.IP \[bu] 2-an integral or decimal number, representing hours.-Eg: 1.5-.IP \[bu] 2-an integral or decimal number immediately followed by a unit symbol-\f[C]s\f[R], \f[C]m\f[R], \f[C]h\f[R], \f[C]d\f[R], \f[C]w\f[R],-\f[C]mo\f[R], or \f[C]y\f[R], representing seconds, minutes, hours, days-weeks, months or years respectively.-Eg: 90m.-The following equivalencies are assumed, currently: 1m = 60s, 1h = 60m,-1d = 24h, 1w = 7d, 1mo = 30d, 1y=365d.-.PP-There is some flexibility allowing notes and todo lists to be kept right-in the time log, if needed:-.IP \[bu] 2-Blank lines and lines beginning with \f[C]#\f[R] or \f[C];\f[R] are-ignored.-.IP \[bu] 2-Lines not ending with a double-space and quantity are parsed as items-taking no time, which will not appear in balance reports by default.-(Add -E to see them.)-.IP \[bu] 2-Org mode headlines (lines beginning with one or more \f[C]*\f[R]-followed by a space) can be used as date lines or timelog items (the-stars are ignored).-Also all org headlines before the first date line are ignored.-This means org users can manage their timelog as an org outline (eg-using org-mode/orgstruct-mode in Emacs), for organisation, faster-navigation, controlling visibility etc.-.PP-Examples:-.IP-.nf-\f[C]-# on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.-2016/2/1-inc:client1 .... .... .... .... .... ....-fos:haskell .... ..-biz:research .--2016/2/2-inc:client1 .... ....-biz:research .-\f[R]-.fi-.IP-.nf-\f[C]-2016/2/3-inc:client1 4-fos:hledger 3-biz:research 1-\f[R]-.fi-.IP-.nf-\f[C]-* Time log-** 2020-01-01-*** adm:time .-*** adm:finance .-\f[R]-.fi-.IP-.nf-\f[C]-* 2020 Work Diary-** Q1-*** 2020-02-29-**** DONE-0700 yoga-**** UNPLANNED-**** BEGUN-hom:chores- cleaning ...- water plants- outdoor - one full watering can- indoor - light watering-**** TODO-adm:planning: trip-*** LATER-\f[R]-.fi-.PP-Reporting:-.IP-.nf-\f[C]-$ hledger -f t.timedot print date:2016/2/2-2016-02-02 *- (inc:client1) 2.00--2016-02-02 *- (biz:research) 0.25-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f t.timedot bal --daily --tree-Balance changes in 2016-02-01-2016-02-03:-- || 2016-02-01d 2016-02-02d 2016-02-03d -============++========================================- biz || 0.25 0.25 1.00 - research || 0.25 0.25 1.00 - fos || 1.50 0 3.00 - haskell || 1.50 0 0 - hledger || 0 0 3.00 - inc || 6.00 2.00 4.00 - client1 || 6.00 2.00 4.00 -------------++----------------------------------------- || 7.75 2.25 8.00 -\f[R]-.fi-.PP-I prefer to use period for separating account components.-We can make this work with an account alias:-.IP-.nf-\f[C]-2016/2/4-fos.hledger.timedot 4-fos.ledger ..-\f[R]-.fi-.IP-.nf-\f[C]-$ hledger -f t.timedot --alias /\[rs]\[rs]./=: bal date:2016/2/4 --tree- 4.50 fos- 4.00 hledger:timedot- 0.50 ledger---------------------- 4.50-\f[R]-.fi-.PP-Here is a sample.timedot.---.SH "REPORTING BUGS"-Report bugs at http://bugs.hledger.org-(or on the #hledger IRC channel or hledger mail list)--.SH AUTHORS-Simon Michael <simon@joyful.com> and contributors--.SH COPYRIGHT--Copyright (C) 2007-2020 Simon Michael.-.br-Released under GNU GPL v3 or later.--.SH SEE ALSO-hledger(1), hledger\-ui(1), hledger\-web(1), ledger(1)--hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_timedot(5)
− hledger_timedot.info
@@ -1,156 +0,0 @@-This is hledger-lib/hledger_timedot.info, produced by makeinfo version-4.8 from stdin.---File: hledger_timedot.info, Node: Top, Up: (dir)--hledger_timedot(5)-******************--hledger's human-friendly time logging format.-- Timedot is a plain text format for logging dated, categorised-quantities (of time, usually), supported by hledger. It is convenient-for approximate and retroactive time logging, eg when the real-time-clock-in/out required with a timeclock file is too precise or too-interruptive. It can be formatted like a bar chart, making clear at a-glance where time was spent.-- Though called "timedot", this format is read by hledger as-commodityless quantities, so it could be used to represent dated-quantities other than time. In the docs below we'll assume it's time.-- A timedot file contains a series of day entries. A day entry begins-with a non-indented hledger-style simple date (Y-M-D, Y/M/D, Y.M.D..)-Any additional text on the same line is used as a transaction-description for this day.-- This is followed by optionally-indented timelog items for that day,-one per line. Each timelog item is a note, usually a-hledger:style:account:name representing a time category, followed by two-or more spaces, and a quantity. Each timelog item generates a hledger-transaction.-- Quantities can be written as:-- * dots: a sequence of dots (.) representing quarter hours. Spaces may- optionally be used for grouping. Eg: .... ..-- * an integral or decimal number, representing hours. Eg: 1.5-- * an integral or decimal number immediately followed by a unit symbol- `s', `m', `h', `d', `w', `mo', or `y', representing seconds,- minutes, hours, days weeks, months or years respectively. Eg: 90m.- The following equivalencies are assumed, currently: 1m = 60s, 1h =- 60m, 1d = 24h, 1w = 7d, 1mo = 30d, 1y=365d.--- There is some flexibility allowing notes and todo lists to be kept-right in the time log, if needed:-- * Blank lines and lines beginning with `#' or `;' are ignored.-- * Lines not ending with a double-space and quantity are parsed as- items taking no time, which will not appear in balance reports by- default. (Add -E to see them.)-- * Org mode headlines (lines beginning with one or more `*' followed- by a space) can be used as date lines or timelog items (the stars- are ignored). Also all org headlines before the first date line- are ignored. This means org users can manage their timelog as an- org outline (eg using org-mode/orgstruct-mode in Emacs), for- organisation, faster navigation, controlling visibility etc.--- Examples:---# on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.-2016/2/1-inc:client1 .... .... .... .... .... ....-fos:haskell .... ..-biz:research .--2016/2/2-inc:client1 .... ....-biz:research .---2016/2/3-inc:client1 4-fos:hledger 3-biz:research 1---* Time log-** 2020-01-01-*** adm:time .-*** adm:finance .---* 2020 Work Diary-** Q1-*** 2020-02-29-**** DONE-0700 yoga-**** UNPLANNED-**** BEGUN-hom:chores- cleaning ...- water plants- outdoor - one full watering can- indoor - light watering-**** TODO-adm:planning: trip-*** LATER-- Reporting:---$ hledger -f t.timedot print date:2016/2/2-2016-02-02 *- (inc:client1) 2.00--2016-02-02 *- (biz:research) 0.25---$ hledger -f t.timedot bal --daily --tree-Balance changes in 2016-02-01-2016-02-03:-- || 2016-02-01d 2016-02-02d 2016-02-03d-============++========================================- biz || 0.25 0.25 1.00- research || 0.25 0.25 1.00- fos || 1.50 0 3.00- haskell || 1.50 0 0- hledger || 0 0 3.00- inc || 6.00 2.00 4.00- client1 || 6.00 2.00 4.00-------------++----------------------------------------- || 7.75 2.25 8.00-- I prefer to use period for separating account components. We can make-this work with an account alias:---2016/2/4-fos.hledger.timedot 4-fos.ledger ..---$ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4 --tree- 4.50 fos- 4.00 hledger:timedot- 0.50 ledger---------------------- 4.50-- Here is a sample.timedot.----Tag Table:-Node: Top88--End Tag Table
− hledger_timedot.txt
@@ -1,163 +0,0 @@--HLEDGER_TIMEDOT(5) hledger User Manuals HLEDGER_TIMEDOT(5)----NAME- hledger's human-friendly time logging format.--DESCRIPTION- Timedot is a plain text format for logging dated, categorised quanti-- ties (of time, usually), supported by hledger. It is convenient for- approximate and retroactive time logging, eg when the real-time clock-- in/out required with a timeclock file is too precise or too interrup-- tive. It can be formatted like a bar chart, making clear at a glance- where time was spent.-- Though called "timedot", this format is read by hledger as commodity-- less quantities, so it could be used to represent dated quantities- other than time. In the docs below we'll assume it's time.-- A timedot file contains a series of day entries. A day entry begins- with a non-indented hledger-style simple date (Y-M-D, Y/M/D, Y.M.D..)- Any additional text on the same line is used as a transaction descrip-- tion for this day.-- This is followed by optionally-indented timelog items for that day, one- per line. Each timelog item is a note, usually a- hledger:style:account:name representing a time category, followed by- two or more spaces, and a quantity. Each timelog item generates a- hledger transaction.-- Quantities can be written as:-- o dots: a sequence of dots (.) representing quarter hours. Spaces may- optionally be used for grouping. Eg: .... ..-- o an integral or decimal number, representing hours. Eg: 1.5-- o an integral or decimal number immediately followed by a unit symbol- s, m, h, d, w, mo, or y, representing seconds, minutes, hours, days- weeks, months or years respectively. Eg: 90m. The following equiva-- lencies are assumed, currently: 1m = 60s, 1h = 60m, 1d = 24h, 1w =- 7d, 1mo = 30d, 1y=365d.-- There is some flexibility allowing notes and todo lists to be kept- right in the time log, if needed:-- o Blank lines and lines beginning with # or ; are ignored.-- o Lines not ending with a double-space and quantity are parsed as items- taking no time, which will not appear in balance reports by default.- (Add -E to see them.)-- o Org mode headlines (lines beginning with one or more * followed by a- space) can be used as date lines or timelog items (the stars are- ignored). Also all org headlines before the first date line are- ignored. This means org users can manage their timelog as an org- outline (eg using org-mode/orgstruct-mode in Emacs), for organisa-- tion, faster navigation, controlling visibility etc.-- Examples:-- # on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.- 2016/2/1- inc:client1 .... .... .... .... .... ....- fos:haskell .... ..- biz:research .-- 2016/2/2- inc:client1 .... ....- biz:research .-- 2016/2/3- inc:client1 4- fos:hledger 3- biz:research 1-- * Time log- ** 2020-01-01- *** adm:time .- *** adm:finance .-- * 2020 Work Diary- ** Q1- *** 2020-02-29- **** DONE- 0700 yoga- **** UNPLANNED- **** BEGUN- hom:chores- cleaning ...- water plants- outdoor - one full watering can- indoor - light watering- **** TODO- adm:planning: trip- *** LATER-- Reporting:-- $ hledger -f t.timedot print date:2016/2/2- 2016-02-02 *- (inc:client1) 2.00-- 2016-02-02 *- (biz:research) 0.25-- $ hledger -f t.timedot bal --daily --tree- Balance changes in 2016-02-01-2016-02-03:-- || 2016-02-01d 2016-02-02d 2016-02-03d- ============++========================================- biz || 0.25 0.25 1.00- research || 0.25 0.25 1.00- fos || 1.50 0 3.00- haskell || 1.50 0 0- hledger || 0 0 3.00- inc || 6.00 2.00 4.00- client1 || 6.00 2.00 4.00- ------------++----------------------------------------- || 7.75 2.25 8.00-- I prefer to use period for separating account components. We can make- this work with an account alias:-- 2016/2/4- fos.hledger.timedot 4- fos.ledger ..-- $ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4 --tree- 4.50 fos- 4.00 hledger:timedot- 0.50 ledger- --------------------- 4.50-- Here is a sample.timedot.----REPORTING BUGS- Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel- or hledger mail list)---AUTHORS- Simon Michael <simon@joyful.com> and contributors---COPYRIGHT- Copyright (C) 2007-2020 Simon Michael.- Released under GNU GPL v3 or later.---SEE ALSO- hledger(1), hledger-ui(1), hledger-web(1), ledger(1)-- hledger_journal(5), hledger_csv(5), hledger_timeclock(5), hledger_time-- dot(5)----hledger-lib-1.20.4 December 2020 HLEDGER_TIMEDOT(5)