packages feed

hledger-lib 1.21 → 1.22

raw patch · 44 files changed

+1444/−1339 lines, 44 filesdep ~basedep ~containersdep ~megaparsecPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, containers, megaparsec

API changes (from Hackage documentation)

- Hledger.Data.Amount: [displayNormalised] :: AmountDisplayOpts -> Bool
- Hledger.Data.Json: instance Data.Aeson.Types.ToJSON.ToJSON Data.Decimal.Decimal
- Hledger.Data.Posting: postingApplyCostValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Costing -> Maybe ValuationType -> Posting -> Posting
- Hledger.Data.Posting: removePrices :: Posting -> Posting
- Hledger.Data.Transaction: transactionApplyCostValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Costing -> Maybe ValuationType -> Transaction -> Transaction
- Hledger.Data.Valuation: mixedAmountApplyCostValuation :: PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Day -> Day -> Costing -> Maybe ValuationType -> MixedAmount -> MixedAmount
- Hledger.Read.Common: [commoditystyles_] :: InputOpts -> Maybe (Map CommoditySymbol AmountStyle)
- Hledger.Read.Common: [ignore_assertions_] :: InputOpts -> Bool
- Hledger.Read.Common: singlespacedtextp :: TextParser m Text
- Hledger.Read.Common: singlespacedtextsatisfyingp :: (Char -> Bool) -> TextParser m Text
- Hledger.Reports.BudgetReport: reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> Text
- Hledger.Reports.ReportOptions: journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal
- Hledger.Reports.ReportTypes: instance GHC.Num.Num b => GHC.Base.Semigroup (Hledger.Reports.ReportTypes.PeriodicReportRow a b)
- Hledger.Utils.Debug: plog :: Show a => String -> a -> a
- Hledger.Utils.Debug: plogAt :: Show a => Int -> String -> a -> a
- Hledger.Utils.String: cliptopleft :: Int -> Int -> String -> String
- Hledger.Utils.String: concatBottomPadded :: [String] -> String
- Hledger.Utils.String: concatOneLine :: [String] -> String
- Hledger.Utils.String: concatTopPadded :: [String] -> String
- Hledger.Utils.String: fitString :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String
- Hledger.Utils.String: fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String
- Hledger.Utils.String: fitto :: Int -> Int -> String -> String
- Hledger.Utils.String: padLeftWide :: Int -> String -> String
- Hledger.Utils.String: padRightWide :: Int -> String -> String
- Hledger.Utils.String: padbottom :: Int -> String -> String
- Hledger.Utils.String: padleft :: Int -> String -> String
- Hledger.Utils.String: padright :: Int -> String -> String
- Hledger.Utils.String: padtop :: Int -> String -> String
- Hledger.Utils.String: unbracket :: String -> String
- Hledger.Utils.String: vConcatLeftAligned :: [String] -> String
- Hledger.Utils.String: vConcatRightAligned :: [String] -> String
- Hledger.Utils.Text: textPadLeftWide :: Int -> Text -> Text
- Hledger.Utils.Text: textPadRightWide :: Int -> Text -> Text
- Text.Tabular.AsciiWide: HL :: HPos
- Text.Tabular.AsciiWide: HM :: HPos
- Text.Tabular.AsciiWide: HR :: HPos
- Text.Tabular.AsciiWide: VB :: VPos
- Text.Tabular.AsciiWide: VM :: VPos
- Text.Tabular.AsciiWide: VT :: VPos
- Text.Tabular.AsciiWide: boxchar :: VPos -> HPos -> Properties -> Properties -> Bool -> Builder
- Text.Tabular.AsciiWide: data HPos
- Text.Tabular.AsciiWide: data VPos
- Text.Tabular.AsciiWide: doubleMidBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: leftBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: lineart :: Properties -> Properties -> Properties -> Properties -> Bool -> Builder
- Text.Tabular.AsciiWide: midBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: pick :: Text -> Text -> Bool -> Builder
- Text.Tabular.AsciiWide: renderColumns :: TableOpts -> [Int] -> Header Cell -> Builder
- Text.Tabular.AsciiWide: renderHLine :: VPos -> Bool -> Bool -> [Int] -> Header a -> Properties -> [Builder]
- Text.Tabular.AsciiWide: renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> Builder
- Text.Tabular.AsciiWide: rightBar :: Bool -> Bool -> Builder
- Text.Tabular.AsciiWide: verticalBar :: Bool -> Char
+ Hledger.Data: Account :: AccountName -> Maybe AccountDeclarationInfo -> [Account] -> Maybe Account -> Bool -> Int -> MixedAmount -> MixedAmount -> Account
+ Hledger.Data: AccountDeclarationInfo :: Text -> [Tag] -> Int -> AccountDeclarationInfo
+ Hledger.Data: Amount :: !CommoditySymbol -> !Quantity -> !Bool -> !AmountStyle -> !Maybe AmountPrice -> Amount
+ Hledger.Data: AmountStyle :: !Side -> !Bool -> !AmountPrecision -> !Maybe Char -> !Maybe DigitGroupStyle -> AmountStyle
+ Hledger.Data: Asset :: AccountType
+ Hledger.Data: BalanceAssertion :: Amount -> Bool -> Bool -> GenericSourcePos -> BalanceAssertion
+ Hledger.Data: BalancedVirtualPosting :: PostingType
+ Hledger.Data: BasicAlias :: AccountName -> AccountName -> AccountAlias
+ Hledger.Data: Cash :: AccountType
+ Hledger.Data: Cleared :: Status
+ Hledger.Data: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Commodity
+ Hledger.Data: DateSpan :: Maybe Day -> Maybe Day -> DateSpan
+ Hledger.Data: Day :: SmartInterval
+ Hledger.Data: DayOfMonth :: Int -> Interval
+ Hledger.Data: DayOfWeek :: Int -> Interval
+ Hledger.Data: DayOfYear :: Int -> Int -> Interval
+ Hledger.Data: DayPeriod :: Day -> Period
+ Hledger.Data: Days :: Int -> Interval
+ Hledger.Data: DigitGroups :: !Char -> ![Word8] -> DigitGroupStyle
+ Hledger.Data: Equity :: AccountType
+ Hledger.Data: Expense :: AccountType
+ Hledger.Data: FinalOut :: TimeclockCode
+ Hledger.Data: GenericSourcePos :: FilePath -> Int -> Int -> GenericSourcePos
+ Hledger.Data: In :: TimeclockCode
+ Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
+ Hledger.Data: JournalSourcePos :: FilePath -> (Int, Int) -> GenericSourcePos
+ Hledger.Data: L :: Side
+ Hledger.Data: Last :: SmartSequence
+ Hledger.Data: Ledger :: Journal -> [Account] -> Ledger
+ Hledger.Data: Liability :: AccountType
+ Hledger.Data: MarketPrice :: Day -> CommoditySymbol -> CommoditySymbol -> Quantity -> MarketPrice
+ Hledger.Data: Month :: SmartInterval
+ Hledger.Data: MonthPeriod :: Year -> Month -> Period
+ Hledger.Data: Months :: Int -> Interval
+ Hledger.Data: NaturalPrecision :: AmountPrecision
+ Hledger.Data: Next :: SmartSequence
+ Hledger.Data: NoInterval :: Interval
+ Hledger.Data: NormallyNegative :: NormalSign
+ Hledger.Data: NormallyPositive :: NormalSign
+ Hledger.Data: Out :: TimeclockCode
+ Hledger.Data: PayeeDeclarationInfo :: Text -> [Tag] -> PayeeDeclarationInfo
+ Hledger.Data: Pending :: Status
+ Hledger.Data: PeriodAll :: Period
+ Hledger.Data: PeriodBetween :: Day -> Day -> Period
+ Hledger.Data: PeriodFrom :: Day -> Period
+ Hledger.Data: PeriodTo :: Day -> Period
+ Hledger.Data: PeriodicTransaction :: Text -> Interval -> DateSpan -> Status -> Text -> Text -> Text -> [Tag] -> [Posting] -> PeriodicTransaction
+ Hledger.Data: Posting :: Maybe Day -> Maybe Day -> Status -> AccountName -> MixedAmount -> Text -> PostingType -> [Tag] -> Maybe BalanceAssertion -> Maybe Transaction -> Maybe Posting -> Posting
+ Hledger.Data: Precision :: !Word8 -> AmountPrecision
+ Hledger.Data: PriceDirective :: Day -> CommoditySymbol -> Amount -> PriceDirective
+ Hledger.Data: PrimaryDate :: WhichDate
+ Hledger.Data: Quarter :: SmartInterval
+ Hledger.Data: QuarterPeriod :: Year -> Quarter -> Period
+ Hledger.Data: Quarters :: Int -> Interval
+ Hledger.Data: R :: Side
+ Hledger.Data: RegexAlias :: Regexp -> Replacement -> AccountAlias
+ Hledger.Data: RegularPosting :: PostingType
+ Hledger.Data: Revenue :: AccountType
+ Hledger.Data: SecondaryDate :: WhichDate
+ Hledger.Data: SetBalance :: TimeclockCode
+ Hledger.Data: SetRequiredHours :: TimeclockCode
+ Hledger.Data: SmartAssumeStart :: Year -> Maybe (Month, Maybe MonthDay) -> SmartDate
+ Hledger.Data: SmartFromReference :: Maybe Month -> MonthDay -> SmartDate
+ Hledger.Data: SmartMonth :: Month -> SmartDate
+ Hledger.Data: SmartRelative :: SmartSequence -> SmartInterval -> SmartDate
+ Hledger.Data: This :: SmartSequence
+ Hledger.Data: TimeclockEntry :: GenericSourcePos -> TimeclockCode -> LocalTime -> AccountName -> Text -> TimeclockEntry
+ Hledger.Data: TotalPrice :: !Amount -> AmountPrice
+ Hledger.Data: Transaction :: Integer -> Text -> GenericSourcePos -> Day -> Maybe Day -> Status -> Text -> Text -> Text -> [Tag] -> [Posting] -> Transaction
+ Hledger.Data: TransactionModifier :: Text -> [TMPostingRule] -> TransactionModifier
+ Hledger.Data: UnitPrice :: !Amount -> AmountPrice
+ Hledger.Data: Unmarked :: Status
+ Hledger.Data: VirtualPosting :: PostingType
+ Hledger.Data: Week :: SmartInterval
+ Hledger.Data: WeekPeriod :: Day -> Period
+ Hledger.Data: WeekdayOfMonth :: Int -> Int -> Interval
+ Hledger.Data: Weeks :: Int -> Interval
+ Hledger.Data: Year :: SmartInterval
+ Hledger.Data: YearPeriod :: Year -> Period
+ Hledger.Data: Years :: Int -> Interval
+ Hledger.Data: [aboring] :: Account -> Bool
+ Hledger.Data: [acommodity] :: Amount -> !CommoditySymbol
+ Hledger.Data: [adeclarationinfo] :: Account -> Maybe AccountDeclarationInfo
+ Hledger.Data: [adicomment] :: AccountDeclarationInfo -> Text
+ Hledger.Data: [adideclarationorder] :: AccountDeclarationInfo -> Int
+ Hledger.Data: [aditags] :: AccountDeclarationInfo -> [Tag]
+ Hledger.Data: [aebalance] :: Account -> MixedAmount
+ Hledger.Data: [aibalance] :: Account -> MixedAmount
+ Hledger.Data: [aismultiplier] :: Amount -> !Bool
+ Hledger.Data: [aname] :: Account -> AccountName
+ Hledger.Data: [anumpostings] :: Account -> Int
+ Hledger.Data: [aparent] :: Account -> Maybe Account
+ Hledger.Data: [aprice] :: Amount -> !Maybe AmountPrice
+ Hledger.Data: [aquantity] :: Amount -> !Quantity
+ Hledger.Data: [ascommodityside] :: AmountStyle -> !Side
+ Hledger.Data: [ascommodityspaced] :: AmountStyle -> !Bool
+ Hledger.Data: [asdecimalpoint] :: AmountStyle -> !Maybe Char
+ Hledger.Data: [asdigitgroups] :: AmountStyle -> !Maybe DigitGroupStyle
+ Hledger.Data: [asprecision] :: AmountStyle -> !AmountPrecision
+ Hledger.Data: [astyle] :: Amount -> !AmountStyle
+ Hledger.Data: [asubs] :: Account -> [Account]
+ Hledger.Data: [baamount] :: BalanceAssertion -> Amount
+ Hledger.Data: [bainclusive] :: BalanceAssertion -> Bool
+ Hledger.Data: [baposition] :: BalanceAssertion -> GenericSourcePos
+ Hledger.Data: [batotal] :: BalanceAssertion -> Bool
+ Hledger.Data: [cformat] :: Commodity -> Maybe AmountStyle
+ Hledger.Data: [csymbol] :: Commodity -> CommoditySymbol
+ Hledger.Data: [jcommodities] :: Journal -> Map CommoditySymbol Commodity
+ Hledger.Data: [jdeclaredaccounts] :: Journal -> [(AccountName, AccountDeclarationInfo)]
+ Hledger.Data: [jdeclaredaccounttypes] :: Journal -> Map AccountType [AccountName]
+ Hledger.Data: [jdeclaredpayees] :: Journal -> [(Payee, PayeeDeclarationInfo)]
+ Hledger.Data: [jfiles] :: Journal -> [(FilePath, Text)]
+ Hledger.Data: [jfinalcommentlines] :: Journal -> Text
+ Hledger.Data: [jglobalcommoditystyles] :: Journal -> Map CommoditySymbol AmountStyle
+ Hledger.Data: [jincludefilestack] :: Journal -> [FilePath]
+ Hledger.Data: [jinferredcommodities] :: Journal -> Map CommoditySymbol AmountStyle
+ Hledger.Data: [jinferredmarketprices] :: Journal -> [MarketPrice]
+ Hledger.Data: [jlastreadtime] :: Journal -> ClockTime
+ Hledger.Data: [jparsealiases] :: Journal -> [AccountAlias]
+ Hledger.Data: [jparsedecimalmark] :: Journal -> Maybe DecimalMark
+ Hledger.Data: [jparsedefaultcommodity] :: Journal -> Maybe (CommoditySymbol, AmountStyle)
+ Hledger.Data: [jparsedefaultyear] :: Journal -> Maybe Year
+ Hledger.Data: [jparseparentaccounts] :: Journal -> [AccountName]
+ Hledger.Data: [jparsetimeclockentries] :: Journal -> [TimeclockEntry]
+ Hledger.Data: [jperiodictxns] :: Journal -> [PeriodicTransaction]
+ Hledger.Data: [jpricedirectives] :: Journal -> [PriceDirective]
+ Hledger.Data: [jtxnmodifiers] :: Journal -> [TransactionModifier]
+ Hledger.Data: [jtxns] :: Journal -> [Transaction]
+ Hledger.Data: [laccounts] :: Ledger -> [Account]
+ Hledger.Data: [ljournal] :: Ledger -> Journal
+ Hledger.Data: [mpdate] :: MarketPrice -> Day
+ Hledger.Data: [mpfrom] :: MarketPrice -> CommoditySymbol
+ Hledger.Data: [mprate] :: MarketPrice -> Quantity
+ Hledger.Data: [mpto] :: MarketPrice -> CommoditySymbol
+ Hledger.Data: [paccount] :: Posting -> AccountName
+ Hledger.Data: [pamount] :: Posting -> MixedAmount
+ Hledger.Data: [pbalanceassertion] :: Posting -> Maybe BalanceAssertion
+ Hledger.Data: [pcomment] :: Posting -> Text
+ Hledger.Data: [pdamount] :: PriceDirective -> Amount
+ Hledger.Data: [pdate2] :: Posting -> Maybe Day
+ Hledger.Data: [pdate] :: Posting -> Maybe Day
+ Hledger.Data: [pdcommodity] :: PriceDirective -> CommoditySymbol
+ Hledger.Data: [pddate] :: PriceDirective -> Day
+ Hledger.Data: [pdicomment] :: PayeeDeclarationInfo -> Text
+ Hledger.Data: [pditags] :: PayeeDeclarationInfo -> [Tag]
+ Hledger.Data: [poriginal] :: Posting -> Maybe Posting
+ Hledger.Data: [pstatus] :: Posting -> Status
+ Hledger.Data: [ptags] :: Posting -> [Tag]
+ Hledger.Data: [ptcode] :: PeriodicTransaction -> Text
+ Hledger.Data: [ptcomment] :: PeriodicTransaction -> Text
+ Hledger.Data: [ptdescription] :: PeriodicTransaction -> Text
+ Hledger.Data: [ptinterval] :: PeriodicTransaction -> Interval
+ Hledger.Data: [ptperiodexpr] :: PeriodicTransaction -> Text
+ Hledger.Data: [ptpostings] :: PeriodicTransaction -> [Posting]
+ Hledger.Data: [ptransaction] :: Posting -> Maybe Transaction
+ Hledger.Data: [ptspan] :: PeriodicTransaction -> DateSpan
+ Hledger.Data: [ptstatus] :: PeriodicTransaction -> Status
+ Hledger.Data: [pttags] :: PeriodicTransaction -> [Tag]
+ Hledger.Data: [ptype] :: Posting -> PostingType
+ Hledger.Data: [tcode] :: Transaction -> Text
+ Hledger.Data: [tcomment] :: Transaction -> Text
+ Hledger.Data: [tdate2] :: Transaction -> Maybe Day
+ Hledger.Data: [tdate] :: Transaction -> Day
+ Hledger.Data: [tdescription] :: Transaction -> Text
+ Hledger.Data: [tindex] :: Transaction -> Integer
+ Hledger.Data: [tlaccount] :: TimeclockEntry -> AccountName
+ Hledger.Data: [tlcode] :: TimeclockEntry -> TimeclockCode
+ Hledger.Data: [tldatetime] :: TimeclockEntry -> LocalTime
+ Hledger.Data: [tldescription] :: TimeclockEntry -> Text
+ Hledger.Data: [tlsourcepos] :: TimeclockEntry -> GenericSourcePos
+ Hledger.Data: [tmpostingrules] :: TransactionModifier -> [TMPostingRule]
+ Hledger.Data: [tmquerytxt] :: TransactionModifier -> Text
+ Hledger.Data: [tpostings] :: Transaction -> [Posting]
+ Hledger.Data: [tprecedingcomment] :: Transaction -> Text
+ Hledger.Data: [tsourcepos] :: Transaction -> GenericSourcePos
+ Hledger.Data: [tstatus] :: Transaction -> Status
+ Hledger.Data: [ttags] :: Transaction -> [Tag]
+ Hledger.Data: data Account
+ Hledger.Data: data AccountAlias
+ Hledger.Data: data AccountDeclarationInfo
+ Hledger.Data: data AccountType
+ Hledger.Data: data Amount
+ Hledger.Data: data AmountPrecision
+ Hledger.Data: data AmountPrice
+ Hledger.Data: data AmountStyle
+ Hledger.Data: data BalanceAssertion
+ Hledger.Data: data Commodity
+ Hledger.Data: data DateSpan
+ Hledger.Data: data DigitGroupStyle
+ Hledger.Data: data GenericSourcePos
+ Hledger.Data: data Interval
+ Hledger.Data: data Journal
+ Hledger.Data: data Ledger
+ Hledger.Data: data MarketPrice
+ Hledger.Data: data MixedAmount
+ Hledger.Data: data NormalSign
+ Hledger.Data: data PayeeDeclarationInfo
+ Hledger.Data: data Period
+ Hledger.Data: data PeriodicTransaction
+ Hledger.Data: data Posting
+ Hledger.Data: data PostingType
+ Hledger.Data: data PriceDirective
+ Hledger.Data: data Side
+ Hledger.Data: data SmartDate
+ Hledger.Data: data SmartInterval
+ Hledger.Data: data SmartSequence
+ Hledger.Data: data Status
+ Hledger.Data: data TimeclockCode
+ Hledger.Data: data TimeclockEntry
+ Hledger.Data: data Transaction
+ Hledger.Data: data TransactionModifier
+ Hledger.Data: data WhichDate
+ Hledger.Data: isDecimalMark :: Char -> Bool
+ Hledger.Data: nullaccountdeclarationinfo :: AccountDeclarationInfo
+ Hledger.Data: nullpayeedeclarationinfo :: PayeeDeclarationInfo
+ Hledger.Data: nullperiodictransaction :: PeriodicTransaction
+ Hledger.Data: nulltransactionmodifier :: TransactionModifier
+ Hledger.Data: pattern MixedAmountKeyTotalPrice :: () => !CommoditySymbol -> !CommoditySymbol -> MixedAmountKey
+ Hledger.Data: type AccountName = Text
+ Hledger.Data: type CommoditySymbol = Text
+ Hledger.Data: type DateTag = (TagName, Day)
+ Hledger.Data: type DecimalMark = Char
+ Hledger.Data: type Month = Int
+ Hledger.Data: type MonthDay = Int
+ Hledger.Data: type MonthWeek = Int
+ Hledger.Data: type ParsedJournal = Journal
+ Hledger.Data: type Payee = Text
+ Hledger.Data: type Quantity = Decimal
+ Hledger.Data: type Quarter = Int
+ Hledger.Data: type StorageFormat = String
+ Hledger.Data: type TMPostingRule = Posting
+ Hledger.Data: type Tag = (TagName, TagValue) " A tag name and (possibly empty) value."
+ Hledger.Data: type TagName = Text
+ Hledger.Data: type TagValue = Text
+ Hledger.Data: type WeekDay = Int
+ Hledger.Data: type Year = Integer
+ Hledger.Data: type YearDay = Int
+ Hledger.Data: type YearWeek = Int
+ Hledger.Data.Amount: amountStripPrices :: Amount -> Amount
+ Hledger.Data.Amount: amountsRaw :: MixedAmount -> [Amount]
+ Hledger.Data.Amount: instance GHC.Base.Monoid Hledger.Data.Types.MixedAmount
+ Hledger.Data.Amount: instance GHC.Base.Semigroup Hledger.Data.Types.MixedAmount
+ Hledger.Data.Amount: instance GHC.Show.Show Hledger.Data.Amount.AmountDisplay
+ Hledger.Data.Amount: maAddAmount :: MixedAmount -> Amount -> MixedAmount
+ Hledger.Data.Amount: maAddAmounts :: Foldable t => MixedAmount -> t Amount -> MixedAmount
+ Hledger.Data.Amount: maIsNonZero :: MixedAmount -> Bool
+ Hledger.Data.Amount: maIsZero :: MixedAmount -> Bool
+ Hledger.Data.Amount: maMinus :: MixedAmount -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: maNegate :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: maPlus :: MixedAmount -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: maSum :: Foldable t => t MixedAmount -> MixedAmount
+ Hledger.Data.Amount: mixedAmount :: Amount -> MixedAmount
+ Hledger.Data.Journal: journalCommodities :: Journal -> Set CommoditySymbol
+ Hledger.Data.Json: instance (GHC.Real.Integral a, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Decimal.DecimalRaw a)
+ Hledger.Data.Posting: postingStripPrices :: Posting -> Posting
+ Hledger.Data.Transaction: BalancingOpts :: Bool -> Bool -> Maybe (Map CommoditySymbol AmountStyle) -> BalancingOpts
+ Hledger.Data.Transaction: [commodity_styles_] :: BalancingOpts -> Maybe (Map CommoditySymbol AmountStyle)
+ Hledger.Data.Transaction: [ignore_assertions_] :: BalancingOpts -> Bool
+ Hledger.Data.Transaction: [infer_prices_] :: BalancingOpts -> Bool
+ Hledger.Data.Transaction: balancingOpts :: BalancingOpts
+ Hledger.Data.Transaction: data BalancingOpts
+ Hledger.Data.Transaction: instance Data.Default.Class.Default Hledger.Data.Transaction.BalancingOpts
+ Hledger.Data.Transaction: instance GHC.Show.Show Hledger.Data.Transaction.BalancingOpts
+ Hledger.Data.Types: MixedAmountKeyNoPrice :: !CommoditySymbol -> MixedAmountKey
+ Hledger.Data.Types: MixedAmountKeyTotalPrice :: !CommoditySymbol -> !CommoditySymbol -> MixedAmountKey
+ Hledger.Data.Types: MixedAmountKeyUnitPrice :: !CommoditySymbol -> !CommoditySymbol -> !Quantity -> MixedAmountKey
+ Hledger.Data.Types: data MixedAmountKey
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.MixedAmountKey
+ Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.MixedAmountKey
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.MixedAmountKey
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.MixedAmountKey
+ Hledger.Data.Valuation: mixedAmountToCost :: Costing -> Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
+ Hledger.Read.Common: [balancingopts_] :: InputOpts -> BalancingOpts
+ Hledger.Read.Common: isLineCommentStart :: Char -> Bool
+ Hledger.Read.Common: isSameLineCommentStart :: Char -> Bool
+ Hledger.Read.Common: noncommenttext1p :: TextParser m Text
+ Hledger.Read.Common: noncommenttextp :: TextParser m Text
+ Hledger.Read.Common: singlespacednoncommenttext1p :: TextParser m Text
+ Hledger.Read.Common: singlespacedtext1p :: TextParser m Text
+ Hledger.Read.Common: singlespacedtextsatisfying1p :: (Char -> Bool) -> TextParser m Text
+ Hledger.Reports.ReportOptions: [show_costs_] :: ReportOpts -> Bool
+ Hledger.Reports.ReportOptions: journalApplyValuationFromOpts :: ReportSpec -> Journal -> Journal
+ Hledger.Reports.ReportOptions: journalApplyValuationFromOptsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
+ Hledger.Reports.ReportOptions: mixedAmountApplyValuationAfterSumFromOptsWith :: ReportOpts -> Journal -> PriceOracle -> DateSpan -> MixedAmount -> MixedAmount
+ Hledger.Reports.ReportOptions: reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> Text
+ Hledger.Reports.ReportOptions: valuationAfterSum :: ReportOpts -> Maybe (Maybe CommoditySymbol)
+ Hledger.Reports.ReportTypes: instance GHC.Base.Semigroup b => GHC.Base.Semigroup (Hledger.Reports.ReportTypes.PeriodicReportRow a b)
+ Hledger.Utils.Debug: useColorOnStderr :: Bool
+ Hledger.Utils.Debug: useColorOnStdout :: Bool
+ Hledger.Utils.Parse: isNewline :: Char -> Bool
- Hledger.Data.Amount: AmountDisplayOpts :: Bool -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> AmountDisplayOpts
+ Hledger.Data.Amount: AmountDisplayOpts :: Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> AmountDisplayOpts
- Hledger.Data.Amount: mixed :: [Amount] -> MixedAmount
+ Hledger.Data.Amount: mixed :: Foldable t => t Amount -> MixedAmount
- Hledger.Data.Journal: journalBalanceTransactions :: Bool -> Journal -> Either String Journal
+ Hledger.Data.Journal: journalBalanceTransactions :: BalancingOpts -> Journal -> Either String Journal
- Hledger.Data.Journal: journalCommoditiesDeclared :: Journal -> [AccountName]
+ Hledger.Data.Journal: journalCommoditiesDeclared :: Journal -> [CommoditySymbol]
- Hledger.Data.Journal: journalMapPostingAmounts :: (Amount -> Amount) -> Journal -> Journal
+ Hledger.Data.Journal: journalMapPostingAmounts :: (MixedAmount -> MixedAmount) -> Journal -> Journal
- Hledger.Data.Transaction: balanceTransaction :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Either String Transaction
+ Hledger.Data.Transaction: balanceTransaction :: BalancingOpts -> Transaction -> Either String Transaction
- Hledger.Data.Transaction: balanceTransactionHelper :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Either String (Transaction, [(AccountName, MixedAmount)])
+ Hledger.Data.Transaction: balanceTransactionHelper :: BalancingOpts -> Transaction -> Either String (Transaction, [(AccountName, MixedAmount)])
- Hledger.Data.Transaction: isTransactionBalanced :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Bool
+ Hledger.Data.Transaction: isTransactionBalanced :: BalancingOpts -> Transaction -> Bool
- Hledger.Data.Transaction: transactionMapPostingAmounts :: (Amount -> Amount) -> Transaction -> Transaction
+ Hledger.Data.Transaction: transactionMapPostingAmounts :: (MixedAmount -> MixedAmount) -> Transaction -> Transaction
- Hledger.Data.Types: Mixed :: [Amount] -> MixedAmount
+ Hledger.Data.Types: Mixed :: Map MixedAmountKey Amount -> MixedAmount
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> Bool -> String -> Bool -> Maybe (Map CommoditySymbol AmountStyle) -> Bool -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Bool -> BalancingOpts -> Bool -> InputOpts
- Hledger.Reports.BudgetReport: budgetReport :: ReportSpec -> Bool -> DateSpan -> Journal -> BudgetReport
+ Hledger.Reports.BudgetReport: budgetReport :: ReportSpec -> BalancingOpts -> DateSpan -> Journal -> BudgetReport
- Hledger.Reports.MultiBalanceReport: getPostings :: ReportSpec -> Journal -> [(Posting, Day)]
+ Hledger.Reports.MultiBalanceReport: getPostings :: ReportSpec -> Journal -> PriceOracle -> [(Posting, Day)]
- Hledger.Reports.MultiBalanceReport: getPostingsByColumn :: ReportSpec -> Journal -> DateSpan -> Map DateSpan [Posting]
+ Hledger.Reports.MultiBalanceReport: getPostingsByColumn :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> Map DateSpan [Posting]
- 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.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 -> Bool -> Maybe NormalSign -> Bool -> Maybe DateSpan -> Bool -> ReportOpts
- Hledger.Reports.TransactionsReport: transactionsReport :: ReportOpts -> Journal -> Query -> TransactionsReport
+ Hledger.Reports.TransactionsReport: transactionsReport :: ReportSpec -> Journal -> Query -> TransactionsReport

Files

CHANGES.md view
@@ -1,5 +1,200 @@+<!--+ _ _ _     +| (_) |__  +| | | '_ \ +| | | |_) |+|_|_|_.__/ +           +--> Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog.++# 1.22 2021-07-03++- GHC 9.0 is now officially supported, and GHC 8.0, 8.2, 8.4 are not;+  building hledger now requires GHC 8.6 or greater.++- Added now-required lower bound on containers. (#1514)++- Added useColor, colorOption helpers usable in pure code, eg for debug output.++- Added a Show instance for AmountDisplayOpts and WideBuilder, for debug logging.++Many internal refactorings/improvements/optimisations by Stephen Morgan,+including:++- Don't infer a txn price with same-sign amounts. (#1551)++- Clean up valuation functions, and make clear which to use where. (#1560)++- Replace journalSelectingAmountFromOpts with journalApplyValuationFromOpts.+  This also has the effect of allowing valuation in more reports, for+  example the transactionReport.++- Refactor to eliminate use of printf.++- Remove unused String, Text utility functions.++- Replace concat(Top|Bottom)Padded with textConcat(Top|Bottom)Padded.++- Export Text.Tabular from Text.Tabular.AsciiWide, clean up import lists.++- When matching an account query against a posting, don't try to match+  against the same posting twice, in cases when poriginal is Nothing.++- Create mixedAmountApplyValuationAfterSumFromOptsWith for doing any+  valuation needed after summing amounts.++- Create journalApplyValuationFromOpts. This does costing and+  valuation on a journal, and is meant to replace most direct calls of+  costing and valuation. The exception is for reports which require+  amounts to be summed before valuation is applied, for example a+  historical balance report with --value=end.++- Remove unused (amount|mixedAmount|posting|transaction)ApplyCostValuation functions.++- Remove unnecessary normalisedMixedAmount.++- Remove `showAmounts*B` functions, replacing them entirely with+  `showMixedAmount*B` functions.++- Pull "show-costs" option used by the Close command up into ReporOpts.++- Add more efficient toEncoding for custom ToJSON declarations.++- Fix ledgerDateSpan, so that it considers both transaction and+  posting dates. (#772)++- Move reportPeriodName to Hledger.Reports.ReportOptions, use it for+  HTML and CSV output for compound balance reports.++- Simplify the JSON representation of AmountPrecision. It now uses the+  same JSON representation as Maybe Word8. This means that the JSON+  serialisation is now broadly compatible with that used before the+  commit f6fa76bba7530af3be825445a1097ae42498b1cd, differing only in+  how it handles numbers outside Word8 and that it can now produce+  null for NaturalPrecision.++- A number of AccountName and Journal functions which are supposed to+  produce unique sorted results now use Sets internally to be slightly+  more efficient. There is also a new function journalCommodities.++- More efficiently check whether Amounts are or appear to be zero.+  Comparing two Quantity (either with == or compare) does a lot of+  normalisation (calling roundMax) which is unnecessary if we're+  comparing to zero. Do things more directly to save work.+  For `reg -f examples/10000x10000x10.journal`, this results in++  - A 12% reduction in heap allocations, from 70GB to 62GB+  - A 14% reduction in (profiled) time, from 79s to 70s++  Results for bal -f examples/10000x10000x10.journal are of the same+  order of magnitude.++- In sorting account names, perform lookups on HashSets and HashMaps,+  rather than lists. This is probably not an enormous performance sink+  in real situations, but it takes a huge amount of time and memory in+  our benchmarks (specifically 10000x10000x10.journal). For +  `bal -f examples/10000x10000x10.journal`, this results in++  - A 23% reduction in heap allocation, from 27GiB to 21GiB+  - A 33% reduction in (profiled) time running, from 26.5s to 17.9s++- Minor refactor, using foldMap instead of asum . map . toList.++- Do not call showAmount twice for every posting. For print -f+  examples/10000x10000x10.journal, this results in a 7.7% reduction in+  heap allocations, from 7.6GB to 7.1GB.++- Some efficiency improvements in register reports.+  Use renderRow interface for Register report.++  For `reg -f examples/10000x10000x10.journal`, this results in:++  - Heap allocations decreasing by 55%, from 68.6GB to 31.2GB+  - Resident memory decreasing by 75%, from 254GB to 65GB+  - Total (profiled) time decreasing by 55%, from 37s to 20s++- Split showMixedAmountB into showMixedAmountB and showAmountsB, the+  former being a simple wrapper around the latter. This removes the+  need for the showNormalised option, as showMixedAmountB will always+  showNormalised and showAmountsB will never do so.++- Change internal representation of MixedAmount to use a strict Map+  instead of a list of Amounts. No longer export Mixed constructor, to+  keep API clean. (If you really need it, you can import it directly+  from Hledger.Data.Types). We also ensure the JSON representation of+  MixedAmount doesn't change: it is stored as a normalised list of+  Amounts.+  +  This commit improves performance. Here are some indicative results:++      hledger reg -f examples/10000x1000x10.journal+      - Maximum residency decreases from 65MB to 60MB (8% decrease)+      - Total memory in use decreases from 178MiB to 157MiB (12% decrease)++      hledger reg -f examples/10000x10000x10.journal+      - Maximum residency decreases from 69MB to 60MB (13% decrease)+      - Total memory in use decreases from 198MiB to 153MiB (23% decrease)++      hledger bal -f examples/10000x1000x10.journal+      - Total heap usage decreases from 6.4GB to 6.0GB (6% decrease)+      - Total memory in use decreases from 178MiB to 153MiB (14% decrease)++      hledger bal -f examples/10000x10000x10.journal+      - Total heap usage decreases from 7.3GB to 6.9GB (5% decrease)+      - Total memory in use decreases from 196MiB to 185MiB (5% decrease)++      hledger bal -M -f examples/10000x1000x10.journal+      - Total heap usage decreases from 16.8GB to 10.6GB (47% decrease)+      - Total time decreases from 14.3s to 12.0s (16% decrease)++      hledger bal -M -f examples/10000x10000x10.journal+      - Total heap usage decreases from 108GB to 48GB (56% decrease)+      - Total time decreases from 62s to 41s (33% decrease)++  If you never directly use the constructor Mixed or pattern match against+  it then you don't need to make any changes. If you do, then do the+  following:++  - If you really care about the individual Amounts and never normalise+    your MixedAmount (for example, just storing `Mixed amts` and then+    extracting `amts` as a pattern match, then use should switch to using+    [Amount]. This should just involve removing the `Mixed` constructor.+  - If you ever call `mixed`, `normaliseMixedAmount`, or do any sort of+    amount arithmetic (+), (-), then you should replace the constructor+    `Mixed` with the function `mixed`. To extract the list of Amounts, use+    the function `amounts`.+  - Any remaining calls to `normaliseMixedAmount` can be removed, as that+    is now the identity function.++- Create a new API for MixedAmount arithmetic. This should supplant+  the old interface, which relied on the Num typeclass. MixedAmount+  did not have a very good Num instance. The only functions which were+  defined were fromInteger, (+), and negate. Furthermore, it was not+  law-abiding, as 0 + a /= a in general. Replacements for used+  functions are:++      0 -> nullmixedamt / mempty+      (+) -> maPlus / (<>)+      (-) -> maMinus+      negate -> maNegate+      sum -> maSum+      sumStrict -> maSum++  Also creates some new constructors for MixedAmount:++      mixedAmount :: Amount -> MixedAmount+      maAddAmount :: MixedAmount -> Amount -> MixedAmount+      maAddAmounts :: MixedAmount -> [Amount] -> MixedAmount++  Add Semigroup and Monoid instances for MixedAmount.+  Ideally we would remove the Num instance entirely.++  The only change needed have nullmixedamt/mempty substitute for 0+  without problems was to not squash prices in+  mixedAmount(Looks|Is)Zero. This is correct behaviour in any case.+  # 1.21 2021-03-10 
Hledger/Data.hs view
@@ -47,7 +47,7 @@ import Hledger.Data.Timeclock import Hledger.Data.Transaction import Hledger.Data.TransactionModifier-import Hledger.Data.Types+import Hledger.Data.Types hiding (MixedAmountKey, Mixed) import Hledger.Data.Valuation import Hledger.Utils.Test 
Hledger/Data/Account.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE RecordWildCards, OverloadedStrings #-}+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-} {-|  @@ -9,12 +11,13 @@  module Hledger.Data.Account where+import qualified Data.HashSet as HS+import qualified Data.HashMap.Strict as HM import Data.List (find, sortOn)-import Data.List.Extra (groupSort, groupOn)-import Data.Maybe (fromMaybe)-import Data.Ord (Down(..))+import Data.List.Extra (groupOn) import qualified Data.Map as M-import Safe (headMay, lookupJustDef)+import Data.Ord (Down(..))+import Safe (headMay) import Text.Printf  import Hledger.Data.AccountName@@ -63,12 +66,12 @@ accountsFromPostings :: [Posting] -> [Account] accountsFromPostings ps =   let-    grouped = groupSort [(paccount p,pamount p) | p <- ps]-    counted = [(aname, length amts) | (aname, amts) <- grouped]-    summed =  [(aname, sumStrict amts) | (aname, amts) <- grouped]  -- always non-empty-    acctstree      = accountTree "root" $ map fst summed-    acctswithnumps = mapAccounts setnumps    acctstree      where setnumps    a = a{anumpostings=fromMaybe 0 $ lookup (aname a) counted}-    acctswithebals = mapAccounts setebalance acctswithnumps where setebalance a = a{aebalance=lookupJustDef nullmixedamt (aname a) summed}+    summed = foldr (\p -> HM.insertWith addAndIncrement (paccount p) (1, pamount p)) mempty ps+      where addAndIncrement (n, a) (m, b) = (n + m, a `maPlus` b)+    acctstree      = accountTree "root" $ HM.keys summed+    acctswithebals = mapAccounts setnumpsebalance acctstree+      where setnumpsebalance a = a{anumpostings=numps, aebalance=total}+              where (numps, total) = HM.lookupDefault (0, nullmixedamt) (aname a) summed     acctswithibals = sumAccounts acctswithebals     acctswithparents = tieAccountParents acctswithibals     acctsflattened = flattenAccounts acctswithparents@@ -122,7 +125,7 @@   | otherwise      = a{aibalance=ibal, asubs=subs}   where     subs = map sumAccounts $ asubs a-    ibal = sum $ aebalance a : map aibalance subs+    ibal = maSum $ aebalance a : map aibalance subs  -- | Remove all subaccounts below a certain depth. clipAccounts :: Int -> Account -> Account@@ -139,7 +142,7 @@ clipAccountsAndAggregate (Just d) as = combined     where       clipped  = [a{aname=clipOrEllipsifyAccountName (Just d) $ aname a} | a <- as]-      combined = [a{aebalance=sum $ map aebalance same}+      combined = [a{aebalance=maSum $ map aebalance same}                  | same@(a:_) <- groupOn aname clipped] {- test cases, assuming d=1:@@ -205,7 +208,7 @@     sortSubs = case normalsign of         NormallyPositive -> sortOn (\a -> (Down $ amt a, aname a))         NormallyNegative -> sortOn (\a -> (amt a, aname a))-    amt = normaliseMixedAmountSquashPricesForDisplay . aibalance+    amt = mixedAmountStripPrices . aibalance  -- | Add extra info for this account derived from the Journal's -- account directives, if any (comment, tags, declaration order..).@@ -224,14 +227,14 @@ -- sortAccountNamesByDeclaration :: Journal -> Bool -> [AccountName] -> [AccountName] sortAccountNamesByDeclaration j keepparents as =-  (if keepparents then id else filter (`elem` as)) $  -- maybe discard missing parents that were added-  map aname $                                         -- keep just the names-  drop 1 $                                            -- drop the root node that was added-  flattenAccounts $                                   -- convert to an account list-  sortAccountTreeByDeclaration $                      -- sort by declaration order (and name)-  mapAccounts (accountSetDeclarationInfo j) $         -- add declaration order info-  accountTree "root"                                  -- convert to an account tree-  as+    (if keepparents then id else filter (`HS.member` HS.fromList as)) $  -- maybe discard missing parents that were added+    map aname $                                         -- keep just the names+    drop 1 $                                            -- drop the root node that was added+    flattenAccounts $                                   -- convert to an account list+    sortAccountTreeByDeclaration $                      -- sort by declaration order (and name)+    mapAccounts (accountSetDeclarationInfo j) $         -- add declaration order info+    accountTree "root"                                  -- convert to an account tree+    as  -- | Sort each group of siblings in an account tree by declaration order, then account name. -- So each group will contain first the declared accounts,
Hledger/Data/AccountName.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-} {-|  'AccountName's are strings like @assets:cash:petty@, with multiple@@ -41,11 +40,9 @@ ) where -import Data.List.Extra (nubSort)+import Data.Foldable (toList) import qualified Data.List.NonEmpty as NE-#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup ((<>))-#endif+import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Tree (Tree(..))@@ -113,7 +110,7 @@ -- ie these plus all their parent accounts up to the root. -- Eg: ["a:b:c","d:e"] -> ["a","a:b","a:b:c","d","d:e"] expandAccountNames :: [AccountName] -> [AccountName]-expandAccountNames as = nubSort $ concatMap expandAccountName as+expandAccountNames = toList . foldMap (S.fromList . expandAccountName)  -- | "a:b:c" -> ["a","a:b","a:b:c"] expandAccountName :: AccountName -> [AccountName]@@ -121,7 +118,7 @@  -- | ["a:b:c","d:e"] -> ["a","d"] topAccountNames :: [AccountName] -> [AccountName]-topAccountNames as = [a | a <- expandAccountNames as, accountNameLevel a == 1]+topAccountNames = filter ((1==) . accountNameLevel) . expandAccountNames  parentAccountName :: AccountName -> AccountName parentAccountName = accountNameFromComponents . init . accountNameComponents
Hledger/Data/Amount.hs view
@@ -41,7 +41,6 @@ -}  {-# LANGUAGE BangPatterns       #-}-{-# LANGUAGE CPP                #-} {-# LANGUAGE OverloadedStrings  #-} {-# LANGUAGE RecordWildCards    #-} {-# LANGUAGE StandaloneDeriving #-}@@ -89,12 +88,17 @@   withInternalPrecision,   setAmountDecimalPoint,   withDecimalPoint,+  amountStripPrices,   canonicaliseAmount,   -- * MixedAmount   nullmixedamt,   missingmixedamt,   mixed,+  mixedAmount,+  maAddAmount,+  maAddAmounts,   amounts,+  amountsRaw,   filterMixedAmount,   filterMixedAmountByCommodity,   mapMixedAmount,@@ -104,12 +108,18 @@   mixedAmountStripPrices,   -- ** arithmetic   mixedAmountCost,+  maNegate,+  maPlus,+  maMinus,+  maSum,   divideMixedAmount,   multiplyMixedAmount,   averageMixedAmounts,   isNegativeAmount,   isNegativeMixedAmount,   mixedAmountIsZero,+  maIsZero,+  maIsNonZero,   mixedAmountLooksZero,   mixedAmountTotalPriceToUnitPrice,   -- ** rendering@@ -138,13 +148,11 @@ 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 (find, foldl', 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 Data.Maybe (fromMaybe, isNothing)+import Data.Semigroup (Semigroup(..)) import qualified Data.Text as T import qualified Data.Text.Lazy.Builder as TB import Data.Word (Word8)@@ -163,7 +171,6 @@   { 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@@ -177,7 +184,6 @@ noColour = AmountDisplayOpts { displayPrice         = True                              , displayColour        = False                              , displayZeroCommodity = False-                             , displayNormalised    = True                              , displayOneLine       = False                              , displayMinWidth      = Nothing                              , displayMaxWidth      = Nothing@@ -315,11 +321,15 @@ -- | 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 = testAmountAndTotalPrice ((0==) . amountRoundedQuantity)+amountLooksZero = testAmountAndTotalPrice looksZero+  where+    looksZero Amount{aquantity=Decimal e q, astyle=AmountStyle{asprecision=p}} = case p of+        Precision d      -> if e > d then abs q <= 5*10^(e-d-1) else q == 0+        NaturalPrecision -> q == 0  -- | Is this Amount (and its total price, if it has one) exactly zero, ignoring its display precision ? amountIsZero :: Amount -> Bool-amountIsZero = testAmountAndTotalPrice ((0==) . aquantity)+amountIsZero = testAmountAndTotalPrice (\Amount{aquantity=Decimal _ q} -> q == 0)  -- | Set an amount's display precision, flipped. withPrecision :: Amount -> AmountPrecision -> Amount@@ -344,7 +354,7 @@ -- Rounding is done with Data.Decimal's default roundTo function: -- "If the value ends in 5 then it is rounded to the nearest even value (Banker's Rounding)". -- Does not change the amount's display precision.--- Intended only for internal use, eg when comparing amounts in tests.+-- Intended mainly for internal use, eg when comparing amounts in tests. setAmountInternalPrecision :: Word8 -> Amount -> Amount setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{    astyle=s{asprecision=Precision p}@@ -352,7 +362,7 @@   }  -- | Set an amount's internal precision, flipped.--- Intended only for internal use, eg when comparing amounts in tests.+-- Intended mainly for internal use, eg when comparing amounts in tests. withInternalPrecision :: Amount -> Word8 -> Amount withInternalPrecision = flip setAmountInternalPrecision @@ -364,6 +374,10 @@ withDecimalPoint :: Amount -> Maybe Char -> Amount withDecimalPoint = flip setAmountDecimalPoint +-- | Strip all prices from an Amount+amountStripPrices :: Amount -> Amount+amountStripPrices a = a{aprice=Nothing}+ showAmountPrice :: Amount -> WideBuilder showAmountPrice amt = case aprice amt of     Nothing              -> mempty@@ -494,28 +508,123 @@ ------------------------------------------------------------------------------- -- MixedAmount +instance Semigroup MixedAmount where+  (<>) = maPlus+  sconcat = maSum+  stimes n = multiplyMixedAmount (fromIntegral n)++instance Monoid MixedAmount where+  mempty = nullmixedamt+  mconcat = maSum+ instance Num MixedAmount where-    fromInteger i = Mixed [fromInteger i]-    negate (Mixed as) = Mixed $ map negate as-    (+) (Mixed as) (Mixed bs) = normaliseMixedAmount $ Mixed $ as ++ bs+    fromInteger = mixedAmount . fromInteger+    negate = maNegate+    (+)    = maPlus     (*)    = error' "error, mixed amounts do not support multiplication" -- PARTIAL:     abs    = error' "error, mixed amounts do not support abs"     signum = error' "error, mixed amounts do not support signum" +-- | Calculate the key used to store an Amount within a MixedAmount.+amountKey :: Amount -> MixedAmountKey+amountKey amt@Amount{acommodity=c} = case aprice amt of+    Nothing             -> MixedAmountKeyNoPrice    c+    Just (TotalPrice p) -> MixedAmountKeyTotalPrice c (acommodity p)+    Just (UnitPrice  p) -> MixedAmountKeyUnitPrice  c (acommodity p) (aquantity p)+ -- | The empty mixed amount. nullmixedamt :: MixedAmount-nullmixedamt = Mixed []+nullmixedamt = Mixed mempty  -- | A temporary value for parsed transactions which had no amount specified. missingmixedamt :: MixedAmount-missingmixedamt = Mixed [missingamt]+missingmixedamt = mixedAmount missingamt --- | Convert amounts in various commodities into a normalised MixedAmount.-mixed :: [Amount] -> MixedAmount-mixed = normaliseMixedAmount . Mixed+-- | Convert amounts in various commodities into a mixed amount.+mixed :: Foldable t => t Amount -> MixedAmount+mixed = maAddAmounts nullmixedamt --- | Simplify a mixed amount's component amounts:+-- | Create a MixedAmount from a single Amount.+mixedAmount :: Amount -> MixedAmount+mixedAmount a = Mixed $ M.singleton (amountKey a) a++-- | Add an Amount to a MixedAmount, normalising the result.+maAddAmount :: MixedAmount -> Amount -> MixedAmount+maAddAmount (Mixed ma) a = Mixed $ M.insertWith sumSimilarAmountsUsingFirstPrice (amountKey a) a ma++-- | Add a collection of Amounts to a MixedAmount, normalising the result.+maAddAmounts :: Foldable t => MixedAmount -> t Amount -> MixedAmount+maAddAmounts = foldl' maAddAmount++-- | Negate mixed amount's quantities (and total prices, if any).+maNegate :: MixedAmount -> MixedAmount+maNegate = transformMixedAmount negate++-- | Sum two MixedAmount.+maPlus :: MixedAmount -> MixedAmount -> MixedAmount+maPlus (Mixed as) (Mixed bs) = Mixed $ M.unionWith sumSimilarAmountsUsingFirstPrice as bs++-- | Subtract a MixedAmount from another.+maMinus :: MixedAmount -> MixedAmount -> MixedAmount+maMinus a = maPlus a . maNegate++-- | Sum a collection of MixedAmounts.+maSum :: Foldable t => t MixedAmount -> MixedAmount+maSum = foldl' maPlus nullmixedamt++-- | Divide a mixed amount's quantities (and total prices, if any) by a constant.+divideMixedAmount :: Quantity -> MixedAmount -> MixedAmount+divideMixedAmount n = transformMixedAmount (/n)++-- | Multiply a mixed amount's quantities (and total prices, if any) by a constant.+multiplyMixedAmount :: Quantity -> MixedAmount -> MixedAmount+multiplyMixedAmount n = transformMixedAmount (*n)++-- | Apply a function to a mixed amount's quantities (and its total prices, if it has any).+transformMixedAmount :: (Quantity -> Quantity) -> MixedAmount -> MixedAmount+transformMixedAmount f = mapMixedAmountUnsafe (transformAmount f)++-- | Calculate the average of some mixed amounts.+averageMixedAmounts :: [MixedAmount] -> MixedAmount+averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` maSum as++-- | Is this mixed amount negative, if we can tell that unambiguously?+-- Ie when normalised, are all individual commodity amounts negative ?+isNegativeMixedAmount :: MixedAmount -> Maybe Bool+isNegativeMixedAmount m =+  case amounts $ normaliseMixedAmountSquashPricesForDisplay m of+    []  -> Just False+    [a] -> Just $ isNegativeAmount a+    as | all isNegativeAmount as -> Just True+    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?+-- 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 . normaliseMixedAmount++-- | Is this mixed amount exactly 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 . normaliseMixedAmount++-- | Is this mixed amount exactly zero, ignoring its display precision? --+-- A convenient alias for mixedAmountIsZero.+maIsZero :: MixedAmount -> Bool+maIsZero = mixedAmountIsZero++-- | Is this mixed amount non-zero, ignoring its display precision?+--+-- A convenient alias for not . mixedAmountIsZero.+maIsNonZero :: MixedAmount -> Bool+maIsNonZero = not . mixedAmountIsZero++-- | Get a mixed amount's component amounts.+-- -- * amounts in the same commodity are combined unless they have different prices or total prices -- -- * multiple zero amounts, all with the same non-null commodity, are replaced by just the last of them, preserving the commodity and amount style (all but the last zero amount are discarded)@@ -526,34 +635,35 @@ -- -- * the special "missing" mixed amount remains unchanged ---normaliseMixedAmount :: MixedAmount -> MixedAmount-normaliseMixedAmount = normaliseHelper False--normaliseHelper :: Bool -> MixedAmount -> MixedAmount-normaliseHelper squashprices (Mixed as)-  | 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+amounts :: MixedAmount -> [Amount]+amounts (Mixed ma)+  | missingkey `M.member` ma = [missingamt]  -- missingamt should always be alone, but detect it even if not+  | M.null nonzeros          = [newzero]+  | otherwise                = toList nonzeros   where-    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+    newzero = fromMaybe nullamt $ find (not . T.null . acommodity) zeros+    (zeros, nonzeros) = M.partition amountIsZero ma+    missingkey = amountKey missingamt --- | Like normaliseMixedAmount, but combine each commodity's amounts--- into just one by throwing away all prices except the first. This is--- only used as a rendering helper, and could show a misleading price.+-- | Get a mixed amount's component amounts without normalising zero and missing+-- amounts. This is used for JSON serialisation, so the order is important. In+-- particular, we want the Amounts given in the order of the MixedAmountKeys,+-- i.e. lexicographically first by commodity, then by price commodity, then by+-- unit price from most negative to most positive.+amountsRaw :: MixedAmount -> [Amount]+amountsRaw (Mixed ma) = toList ma++normaliseMixedAmount :: MixedAmount -> MixedAmount+normaliseMixedAmount = id  -- XXX Remove++-- | Strip prices from a MixedAmount. normaliseMixedAmountSquashPricesForDisplay :: MixedAmount -> MixedAmount-normaliseMixedAmountSquashPricesForDisplay = normaliseHelper True+normaliseMixedAmountSquashPricesForDisplay = mixedAmountStripPrices  -- XXX Remove  -- | Unify a MixedAmount to a single commodity value if possible.--- Like normaliseMixedAmount, this consolidates amounts of the same commodity--- and discards zero amounts; but this one insists on simplifying to--- a single commodity, and will return Nothing if this is not possible.+-- This consolidates amounts of the same commodity and discards zero+-- amounts; but this one insists on simplifying to a single commodity,+-- and will return Nothing if this is not possible. unifyMixedAmount :: MixedAmount -> Maybe Amount unifyMixedAmount = foldM combine 0 . amounts   where@@ -581,86 +691,51 @@ -- sumSimilarAmountsNotingPriceDifference [] = nullamt -- sumSimilarAmountsNotingPriceDifference as = undefined --- | Get a mixed amount's component amounts.-amounts :: MixedAmount -> [Amount]-amounts (Mixed as) = as- -- | Filter a mixed amount's component amounts by a predicate. filterMixedAmount :: (Amount -> Bool) -> MixedAmount -> MixedAmount-filterMixedAmount p (Mixed as) = Mixed $ filter p as+filterMixedAmount p (Mixed ma) = Mixed $ M.filter p ma  -- | Return an unnormalised MixedAmount containing exactly one Amount -- with the specified commodity and the quantity of that commodity -- found in the original. NB if Amount's quantity is zero it will be -- discarded next time the MixedAmount gets normalised. filterMixedAmountByCommodity :: CommoditySymbol -> MixedAmount -> MixedAmount-filterMixedAmountByCommodity c (Mixed as) = Mixed as'-  where-    as' = case filter ((==c) . acommodity) as of-            []   -> [nullamt{acommodity=c}]-            as'' -> [sum as'']+filterMixedAmountByCommodity c (Mixed ma)+  | M.null ma' = mixedAmount nullamt{acommodity=c}+  | otherwise  = Mixed ma'+  where ma' = M.filter ((c==) . acommodity) ma  -- | Apply a transform to a mixed amount's component 'Amount's. mapMixedAmount :: (Amount -> Amount) -> MixedAmount -> MixedAmount-mapMixedAmount f (Mixed as) = Mixed $ map f as+mapMixedAmount f (Mixed ma) = mixed . map f $ toList ma +-- | Apply a transform to a mixed amount's component 'Amount's, which does not+-- affect the key of the amount (i.e. doesn't change the commodity, price+-- commodity, or unit price amount). This condition is not checked.+mapMixedAmountUnsafe :: (Amount -> Amount) -> MixedAmount -> MixedAmount+mapMixedAmountUnsafe f (Mixed ma) = Mixed $ M.map f ma  -- Use M.map instead of fmap to maintain strictness+ -- | Convert all component amounts to cost/selling price where -- possible (see amountCost). mixedAmountCost :: MixedAmount -> MixedAmount mixedAmountCost = mapMixedAmount amountCost --- | 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 (and total prices, if any) by a constant.-multiplyMixedAmount :: Quantity -> MixedAmount -> MixedAmount-multiplyMixedAmount n = mapMixedAmount (multiplyAmount n)---- | Calculate the average of some mixed amounts.-averageMixedAmounts :: [MixedAmount] -> MixedAmount-averageMixedAmounts [] = 0-averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` sum as---- | Is this mixed amount negative, if we can tell that unambiguously?--- Ie when normalised, are all individual commodity amounts negative ?-isNegativeMixedAmount :: MixedAmount -> Maybe Bool-isNegativeMixedAmount m =-  case amounts $ normaliseMixedAmountSquashPricesForDisplay m of-    []  -> Just False-    [a] -> Just $ isNegativeAmount a-    as | all isNegativeAmount as -> Just True-    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?--- 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 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- -- -- | MixedAmount derived Eq instance in Types.hs doesn't know that we -- -- want $0 = EUR0 = 0. Yet we don't want to drag all this code over there. -- -- For now, use this when cross-commodity zero equality is important. -- mixedAmountEquals :: MixedAmount -> MixedAmount -> Bool -- mixedAmountEquals a b = amounts a' == amounts b' || (mixedAmountLooksZero a' && mixedAmountLooksZero b')---     where a' = normaliseMixedAmountSquashPricesForDisplay a---           b' = normaliseMixedAmountSquashPricesForDisplay b+--     where a' = mixedAmountStripPrices a+--           b' = mixedAmountStripPrices b  -- | 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 = mapMixedAmount (styleAmount styles)+styleMixedAmount styles = mapMixedAmountUnsafe (styleAmount styles)  -- | Reset each individual amount's display style to the default. mixedAmountUnstyled :: MixedAmount -> MixedAmount-mixedAmountUnstyled = mapMixedAmount amountUnstyled+mixedAmountUnstyled = mapMixedAmountUnsafe amountUnstyled  -- | Get the string representation of a mixed amount, after -- normalising it to one amount per commodity. Assumes amounts have@@ -712,38 +787,37 @@                        | otherwise       = printf "Mixed [%s]" as     where as = intercalate "\n       " $ map showAmountDebug $ amounts m --- | General function to generate a WideBuilder for a MixedAmount, according the+-- | General function to generate a WideBuilder for a MixedAmount, according to 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.+--   fit in the given width, and further Amounts will be elided. There+--   will always be at least one amount displayed, even if this will+--   exceed the requested maximum width. -- - 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'+    | 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'+    lines = showMixedAmountLinesB opts ma     width = headDef 0 $ map wbWidth lines     sep = WideBuilder (TB.singleton '\n') 0 --- | 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.+-- | Helper for showMixedAmountB to show a list of Amounts on multiple lines. This returns+-- the list of WideBuilders: one for each Amount, 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-    Mixed amts = if displayNormalised opts then normaliseMixedAmountSquashPricesForDisplay ma else ma--    astrs = amtDisplayList (wbWidth sep) (showAmountB opts) amts+    astrs = amtDisplayList (wbWidth sep) (showAmountB opts) . amounts $+              if displayPrice opts then ma else mixedAmountStripPrices ma     sep   = WideBuilder (TB.singleton '\n') 0     width = maximum $ fromMaybe 0 mmin : map (wbWidth . adBuilder) elided @@ -761,14 +835,14 @@ -- 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+    WideBuilder (wbBuilder . pad . mconcat . intersperse sep $ map adBuilder elided)+    . max width $ fromMaybe 0 mmin   where-    Mixed amts = if displayNormalised opts then normaliseMixedAmountSquashPricesForDisplay ma else ma-     width  = maybe 0 adTotal $ lastMay elided-    astrs  = amtDisplayList (wbWidth sep) (showAmountB opts) amts+    astrs  = amtDisplayList (wbWidth sep) (showAmountB opts) . amounts $+               if displayPrice opts then ma else mixedAmountStripPrices ma     sep    = WideBuilder (TB.fromString ", ") 2-    n      = length amts+    n      = length astrs      pad = (WideBuilder (TB.fromText $ T.replicate w " ") w <>)       where w = fromMaybe 0 mmin - width@@ -779,8 +853,10 @@     addElide [] = []     addElide xs = maybeAppend (snd $ last xs) $ map fst xs     -- Return the elements of the display list which fit within the maximum width-    -- (including their elision strings)-    takeFitting m = dropWhileRev (\(a,e) -> m < adTotal (fromMaybe a e))+    -- (including their elision strings). Always display at least one amount,+    -- regardless of width.+    takeFitting _ []     = []+    takeFitting m (x:xs) = x : dropWhileRev (\(a,e) -> m < adTotal (fromMaybe a e)) xs     dropWhileRev p = foldr (\x xs -> if null xs && p x then [] else x:xs) []      -- Add the elision strings (if any) to each amount@@ -790,7 +866,7 @@   { adBuilder :: !WideBuilder  -- ^ String representation of the Amount   , adTotal   :: !Int            -- ^ Cumulative length of MixedAmount this Amount is part of,                                 --   including separators-  }+  } deriving (Show)  nullAmountDisplay :: AmountDisplay nullAmountDisplay = AmountDisplay mempty 0@@ -828,19 +904,22 @@  -- | Set the display precision in the amount's commodities. mixedAmountSetPrecision :: AmountPrecision -> MixedAmount -> MixedAmount-mixedAmountSetPrecision p = mapMixedAmount (amountSetPrecision p)+mixedAmountSetPrecision p = mapMixedAmountUnsafe (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+mixedAmountSetFullPrecision = mapMixedAmountUnsafe amountSetFullPrecision +-- | Remove all prices from a MixedAmount. mixedAmountStripPrices :: MixedAmount -> MixedAmount-mixedAmountStripPrices = mapMixedAmount (\a -> a{aprice=Nothing})+mixedAmountStripPrices (Mixed ma) =+    foldl' (\m a -> maAddAmount m a{aprice=Nothing}) (Mixed noPrices) withPrices+  where (noPrices, withPrices) = M.partition (isNothing . aprice) ma  -- | Canonicalise a mixed amount's display styles using the provided commodity style map. canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount-canonicaliseMixedAmount styles = mapMixedAmount (canonicaliseAmount styles)+canonicaliseMixedAmount styles = mapMixedAmountUnsafe (canonicaliseAmount styles)  -- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice. -- Has no effect on amounts without one.@@ -888,52 +967,52 @@   ,tests "MixedAmount" [       test "adding mixed amounts to zero, the commodity and amount style are preserved" $-      sum (map (Mixed . (:[]))-               [usd 1.25-               ,usd (-1) `withPrecision` Precision 3-               ,usd (-0.25)-               ])-        @?= Mixed [usd 0 `withPrecision` Precision 3]+      maSum (map mixedAmount+        [usd 1.25+        ,usd (-1) `withPrecision` Precision 3+        ,usd (-0.25)+        ])+        @?= mixedAmount (usd 0 `withPrecision` Precision 3)      ,test "adding mixed amounts with total prices" $ do-      sum (map (Mixed . (:[]))-       [usd 1 @@ eur 1-       ,usd (-2) @@ eur 1-       ])-        @?= Mixed [usd (-1) @@ eur 2 ]+      maSum (map mixedAmount+        [usd 1 @@ eur 1+        ,usd (-2) @@ eur 1+        ])+        @?= mixedAmount (usd (-1) @@ eur 2)      ,test "showMixedAmount" $ do-       showMixedAmount (Mixed [usd 1]) @?= "$1.00"-       showMixedAmount (Mixed [usd 1 `at` eur 2]) @?= "$1.00 @ €2.00"-       showMixedAmount (Mixed [usd 0]) @?= "0"-       showMixedAmount (Mixed []) @?= "0"+       showMixedAmount (mixedAmount (usd 1)) @?= "$1.00"+       showMixedAmount (mixedAmount (usd 1 `at` eur 2)) @?= "$1.00 @ €2.00"+       showMixedAmount (mixedAmount (usd 0)) @?= "0"+       showMixedAmount nullmixedamt @?= "0"        showMixedAmount missingmixedamt @?= ""      ,test "showMixedAmountWithoutPrice" $ do       let a = usd 1 `at` eur 2-      showMixedAmountWithoutPrice False (Mixed [a]) @?= "$1.00"-      showMixedAmountWithoutPrice False (Mixed [a, -a]) @?= "0"+      showMixedAmountWithoutPrice False (mixedAmount (a)) @?= "$1.00"+      showMixedAmountWithoutPrice False (mixed [a, -a]) @?= "0" -    ,tests "normaliseMixedAmount" [+    ,tests "amounts" [        test "a missing amount overrides any other amounts" $-        normaliseMixedAmount (Mixed [usd 1, missingamt]) @?= missingmixedamt+        amounts (mixed [usd 1, missingamt]) @?= [missingamt]       ,test "unpriced same-commodity amounts are combined" $-        normaliseMixedAmount (Mixed [usd 0, usd 2]) @?= Mixed [usd 2]+        amounts (mixed [usd 0, usd 2]) @?= [usd 2]       ,test "amounts with same unit price are combined" $-        normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) @?= Mixed [usd 2 `at` eur 1]+        amounts (mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) @?= [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]+        amounts (mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) @?= [usd 1 `at` eur 1, usd 1 `at` eur 2]       ,test "amounts with total prices are combined" $-        normaliseMixedAmount (Mixed  [usd 1 @@ eur 1, usd 1 @@ eur 1]) @?= Mixed [usd 2 @@ eur 2]+        amounts (mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]) @?= [usd 2 @@ eur 2]     ] -    ,test "normaliseMixedAmountSquashPricesForDisplay" $ do-       normaliseMixedAmountSquashPricesForDisplay (Mixed []) @?= Mixed [nullamt]-       assertBool "" $ mixedAmountLooksZero $ normaliseMixedAmountSquashPricesForDisplay-        (Mixed [usd 10+    ,test "mixedAmountStripPrices" $ do+       amounts (mixedAmountStripPrices nullmixedamt) @?= [nullamt]+       assertBool "" $ mixedAmountLooksZero $ mixedAmountStripPrices+        (mixed [usd 10                ,usd 10 @@ eur 7                ,usd (-10)-               ,usd (-10) @@ eur 7+               ,usd (-10) @@ eur (-7)                ])    ]
Hledger/Data/Commodity.hs view
@@ -8,7 +8,6 @@ -}  {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-}  module Hledger.Data.Commodity where@@ -16,9 +15,6 @@ import Data.Char (isDigit) import Data.List import Data.Maybe (fromMaybe)-#if !(MIN_VERSION_base(4,11,0))-import Data.Monoid-#endif import qualified Data.Text as T -- import qualified Data.Map as M 
Hledger/Data/Journal.hs view
@@ -1,11 +1,9 @@-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE LambdaCase          #-}+{-# LANGUAGE NamedFieldPuns      #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE PackageImports      #-}+{-# LANGUAGE Rank2Types          #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-}  {-| @@ -57,6 +55,7 @@   journalPayeesUsed,   journalPayeesDeclaredOrUsed,   journalCommoditiesDeclared,+  journalCommodities,   journalDateSpan,   journalDateSpanBothDates,   journalStartDate,@@ -101,6 +100,7 @@ import Data.Array.ST (STArray, getElems, newListArray, writeArray) import Data.Char (toUpper, isDigit) import Data.Default (Default(..))+import Data.Foldable (toList) import Data.Function ((&)) import qualified Data.HashTable.Class as H (toList) import qualified Data.HashTable.ST.Cuckoo as H@@ -108,9 +108,6 @@ 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@@ -277,9 +274,13 @@ journalPostings = concatMap tpostings . jtxns  -- | Sorted unique commodity symbols declared by commodity directives in this journal.-journalCommoditiesDeclared :: Journal -> [AccountName]-journalCommoditiesDeclared = nubSort . M.keys . jcommodities+journalCommoditiesDeclared :: Journal -> [CommoditySymbol]+journalCommoditiesDeclared = M.keys . jcommodities +-- | Sorted unique commodity symbols declared or inferred from this journal.+journalCommodities :: Journal -> S.Set CommoditySymbol+journalCommodities j = M.keysSet (jcommodities j) <> M.keysSet (jinferredcommodities j)+ -- | Unique transaction descriptions used in this journal. journalDescriptions :: Journal -> [Text] journalDescriptions = nubSort . map tdescription . jtxns@@ -294,7 +295,8 @@  -- | Sorted unique payees used in transactions or declared by payee directives in this journal. journalPayeesDeclaredOrUsed :: Journal -> [Payee]-journalPayeesDeclaredOrUsed j = nubSort $ journalPayeesDeclared j ++ journalPayeesUsed j+journalPayeesDeclaredOrUsed j = toList $ foldMap S.fromList+    [journalPayeesDeclared j, journalPayeesUsed j]  -- | Sorted unique account names posted to by this journal's transactions. journalAccountNamesUsed :: Journal -> [AccountName]@@ -312,19 +314,21 @@ -- | Sorted unique account names declared by account directives or posted to -- by transactions in this journal. journalAccountNamesDeclaredOrUsed :: Journal -> [AccountName]-journalAccountNamesDeclaredOrUsed j = nubSort $ journalAccountNamesDeclared j ++ journalAccountNamesUsed j+journalAccountNamesDeclaredOrUsed j = toList $ foldMap S.fromList+    [journalAccountNamesDeclared j, journalAccountNamesUsed j]  -- | Sorted unique account names declared by account directives, or posted to -- or implied as parents by transactions in this journal. journalAccountNamesDeclaredOrImplied :: Journal -> [AccountName]-journalAccountNamesDeclaredOrImplied j = nubSort $ journalAccountNamesDeclared j ++ journalAccountNamesImplied j+journalAccountNamesDeclaredOrImplied j = toList $ foldMap S.fromList+    [journalAccountNamesDeclared j, expandAccountNames $ journalAccountNamesUsed j]  -- | Convenience/compatibility alias for journalAccountNamesDeclaredOrImplied. journalAccountNames :: Journal -> [AccountName] journalAccountNames = journalAccountNamesDeclaredOrImplied  journalAccountNameTree :: Journal -> Tree AccountName-journalAccountNameTree = accountNameTreeFrom . journalAccountNames+journalAccountNameTree = accountNameTreeFrom . journalAccountNamesDeclaredOrImplied  -- | Find up to N most similar and most recent transactions matching -- the given transaction description and query. Transactions are@@ -524,7 +528,7 @@  -- | Filter out all parts of this posting's amount which do not match the query. filterPostingAmount :: Query -> Posting -> Posting-filterPostingAmount q p@Posting{pamount=Mixed as} = p{pamount=Mixed $ filter (q `matchesAmount`) as}+filterPostingAmount q p@Posting{pamount=as} = p{pamount=filterMixedAmount (q `matchesAmount`) as}  filterTransactionPostings :: Query -> Transaction -> Transaction filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps}@@ -538,8 +542,8 @@ journalMapPostings f j@Journal{jtxns=ts} = j{jtxns=map (transactionMapPostings f) ts}  -- | Apply a transformation to a journal's posting amounts.-journalMapPostingAmounts :: (Amount -> Amount) -> Journal -> Journal-journalMapPostingAmounts f = journalMapPostings (postingTransformAmount (mapMixedAmount f))+journalMapPostingAmounts :: (MixedAmount -> MixedAmount) -> Journal -> Journal+journalMapPostingAmounts f = journalMapPostings (postingTransformAmount f)  {- -------------------------------------------------------------------------------@@ -713,7 +717,7 @@ -- | Check any balance assertions in the journal and return an error message -- if any of them fail (or if the transaction balancing they require fails). journalCheckBalanceAssertions :: Journal -> Maybe String-journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTransactions True+journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTransactions def  -- "Transaction balancing", including: inferring missing amounts, -- applying balance assignments, checking transaction balancedness,@@ -765,14 +769,14 @@ -- | Get this account's current exclusive running balance. getRunningBalanceB :: AccountName -> Balancing s MixedAmount getRunningBalanceB acc = withRunningBalance $ \BalancingState{bsBalances} -> do-  fromMaybe 0 <$> H.lookup bsBalances acc+  fromMaybe nullmixedamt <$> H.lookup bsBalances acc  -- | Add this amount to this account's exclusive running balance. -- Returns the new running balance. addToRunningBalanceB :: AccountName -> MixedAmount -> Balancing s MixedAmount addToRunningBalanceB acc amt = withRunningBalance $ \BalancingState{bsBalances} -> do-  old <- fromMaybe 0 <$> H.lookup bsBalances acc-  let new = old + amt+  old <- fromMaybe nullmixedamt <$> H.lookup bsBalances acc+  let new = maPlus old amt   H.insert bsBalances acc new   return new @@ -780,9 +784,9 @@ -- Returns the change in exclusive running balance. setRunningBalanceB :: AccountName -> MixedAmount -> Balancing s MixedAmount setRunningBalanceB acc amt = withRunningBalance $ \BalancingState{bsBalances} -> do-  old <- fromMaybe 0 <$> H.lookup bsBalances acc+  old <- fromMaybe nullmixedamt <$> H.lookup bsBalances acc   H.insert bsBalances acc amt-  return $ amt - old+  return $ maMinus amt old  -- | Set this account's exclusive running balance to whatever amount -- makes its *inclusive* running balance (the sum of exclusive running@@ -790,13 +794,13 @@ -- Returns the change in exclusive running balance. setInclusiveRunningBalanceB :: AccountName -> MixedAmount -> Balancing s MixedAmount setInclusiveRunningBalanceB acc newibal = withRunningBalance $ \BalancingState{bsBalances} -> do-  oldebal  <- fromMaybe 0 <$> H.lookup bsBalances acc+  oldebal  <- fromMaybe nullmixedamt <$> H.lookup bsBalances acc   allebals <- H.toList bsBalances   let subsibal =  -- sum of any subaccounts' running balances-        sum $ map snd $ filter ((acc `isAccountNamePrefixOf`).fst) allebals-  let newebal = newibal - subsibal+        maSum . map snd $ filter ((acc `isAccountNamePrefixOf`).fst) allebals+  let newebal = maMinus newibal subsibal   H.insert bsBalances acc newebal-  return $ newebal - oldebal+  return $ maMinus newebal oldebal  -- | Update (overwrite) this transaction in the balancing state. updateTransactionB :: Transaction -> Balancing s ()@@ -813,13 +817,14 @@ -- -- This does multiple things at once because amount inferring, balance -- assignments, balance assertions and posting dates are interdependent.-journalBalanceTransactions :: Bool -> Journal -> Either String Journal-journalBalanceTransactions assrt j' =+journalBalanceTransactions :: BalancingOpts -> Journal -> Either String Journal+journalBalanceTransactions bopts' j' =   let     -- ensure transactions are numbered, so we can store them by number     j@Journal{jtxns=ts} = journalNumberTransactions j'     -- display precisions used in balanced checking     styles = Just $ journalCommodityStyles j+    bopts = bopts'{commodity_styles_=styles}     -- balance assignments will not be allowed on these     txnmodifieraccts = S.fromList $ map paccount $ concatMap tmpostingrules $ jtxnmodifiers j   in@@ -836,7 +841,7 @@         -- and leaving the others for later. The balanced ones are split into their postings.         -- The postings and not-yet-balanced transactions remain in the same relative order.         psandts :: [Either Posting Transaction] <- fmap concat $ forM ts $ \case-          t | null $ assignmentPostings t -> case balanceTransaction styles t of+          t | null $ assignmentPostings t -> case balanceTransaction bopts t of               Left  e  -> throwError e               Right t' -> do                 lift $ writeArray balancedtxns (tindex t') t'@@ -846,7 +851,7 @@         -- 2. Sort these items by date, preserving the order of same-day items,         -- and step through them while keeping running account balances,         runningbals <- lift $ H.newSized (length $ journalAccountNamesUsed j)-        flip runReaderT (BalancingState styles txnmodifieraccts assrt runningbals balancedtxns) $ do+        flip runReaderT (BalancingState styles txnmodifieraccts (not $ ignore_assertions_ bopts) runningbals balancedtxns) $ do           -- performing balance assignments in, and balancing, the remaining transactions,           -- and checking balance assertions as each posting is processed.           void $ mapM' balanceTransactionAndCheckAssertionsB $ sortOn (either postingDate tdate) psandts@@ -866,16 +871,16 @@ balanceTransactionAndCheckAssertionsB :: Either Posting Transaction -> Balancing s () balanceTransactionAndCheckAssertionsB (Left p@Posting{}) =   -- update the account's running balance and check the balance assertion if any-  void $ addAmountAndCheckAssertionB $ removePrices p+  void . addAmountAndCheckAssertionB $ postingStripPrices p balanceTransactionAndCheckAssertionsB (Right t@Transaction{tpostings=ps}) = do   -- make sure we can handle the balance assignments   mapM_ checkIllegalBalanceAssignmentB ps   -- for each posting, infer its amount from the balance assignment if applicable,   -- update the account's running balance and check the balance assertion if any-  ps' <- forM ps $ \p -> pure (removePrices p) >>= addOrAssignAmountAndCheckAssertionB+  ps' <- mapM (addOrAssignAmountAndCheckAssertionB . postingStripPrices) ps   -- infer any remaining missing amounts, and make sure the transaction is now fully balanced   styles <- R.reader bsStyles-  case balanceTransactionHelper styles t{tpostings=ps'} of+  case balanceTransactionHelper balancingOpts{commodity_styles_=styles} t{tpostings=ps'} of     Left err -> throwError err     Right (t', inferredacctsandamts) -> do       -- for each amount just inferred, update the running balance@@ -897,21 +902,15 @@       return p    -- no explicit posting amount, but there is a balance assignment-  -- TODO this doesn't yet handle inclusive assignments right, #1207   | Just BalanceAssertion{baamount,batotal,bainclusive} <- mba = do-      (diff,newbal) <- case batotal of-        -- a total balance assignment (==, all commodities)-        True  -> do-          let newbal = Mixed [baamount]-          diff <- (if bainclusive then setInclusiveRunningBalanceB else setRunningBalanceB) acc newbal-          return (diff,newbal)-        -- a partial balance assignment (=, one commodity)-        False -> do-          oldbalothercommodities <- filterMixedAmount ((acommodity baamount /=) . acommodity) <$> getRunningBalanceB acc-          let assignedbalthiscommodity = Mixed [baamount]-              newbal = oldbalothercommodities + assignedbalthiscommodity-          diff <- (if bainclusive then setInclusiveRunningBalanceB else setRunningBalanceB) acc newbal-          return (diff,newbal)+      newbal <- if batotal+                   -- a total balance assignment (==, all commodities)+                   then return $ mixedAmount baamount+                   -- a partial balance assignment (=, one commodity)+                   else do+                     oldbalothercommodities <- filterMixedAmount ((acommodity baamount /=) . acommodity) <$> getRunningBalanceB acc+                     return $ maAddAmount oldbalothercommodities baamount+      diff <- (if bainclusive then setInclusiveRunningBalanceB else setRunningBalanceB) acc newbal       let p' = p{pamount=diff, poriginal=Just $ originalPosting p}       whenM (R.reader bsAssrt) $ checkBalanceAssertionB p' newbal       return p'@@ -926,7 +925,7 @@ -- need to see the balance as it stands after each individual posting. addAmountAndCheckAssertionB :: Posting -> Balancing s Posting addAmountAndCheckAssertionB p | hasAmount p = do-  newbal <- addToRunningBalanceB (paccount p) (pamount p)+  newbal <- addToRunningBalanceB (paccount p) $ pamount p   whenM (R.reader bsAssrt) $ checkBalanceAssertionB p newbal   return p addAmountAndCheckAssertionB p = return p@@ -937,13 +936,12 @@ -- are ignored; if it is total, they will cause the assertion to fail. checkBalanceAssertionB :: Posting -> MixedAmount -> Balancing s () checkBalanceAssertionB p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal =-  forM_ assertedamts $ \amt -> checkBalanceAssertionOneCommodityB p amt actualbal+    forM_ (baamount : otheramts) $ \amt -> checkBalanceAssertionOneCommodityB p amt actualbal   where-    assertedamts = baamount : otheramts-      where-        assertedcomm = acommodity baamount-        otheramts | batotal   = map (\a -> a{aquantity=0}) $ amounts $ filterMixedAmount ((/=assertedcomm).acommodity) actualbal-                  | otherwise = []+    assertedcomm = acommodity baamount+    otheramts | batotal   = map (\a -> a{aquantity=0}) . amountsRaw+                          $ filterMixedAmount ((/=assertedcomm).acommodity) actualbal+              | otherwise = [] checkBalanceAssertionB _ _ = return ()  -- | Does this (single commodity) expected balance match the amount of that@@ -961,14 +959,14 @@       -- sum the running balances of this account and any of its subaccounts seen so far       withRunningBalance $ \BalancingState{bsBalances} ->         H.foldM-          (\ibal (acc, amt) -> return $ ibal +-            if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc then amt else 0)-          0+          (\ibal (acc, amt) -> return $+            if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc then maPlus ibal amt else ibal)+          nullmixedamt           bsBalances     else return actualbal   let     assertedcomm    = acommodity assertedamt-    actualbalincomm = headDef 0 $ amounts $ filterMixedAmountByCommodity assertedcomm $ actualbal'+    actualbalincomm = headDef nullamt . amountsRaw . filterMixedAmountByCommodity assertedcomm $ actualbal'     pass =       aquantity         -- traceWith (("asserted:"++).showAmountDebug)@@ -1093,10 +1091,8 @@ -- "the format of the first amount, adjusted to the highest precision of all amounts". -- Can return an error message eg if inconsistent number formats are found. journalInferCommodityStyles :: Journal -> Either String Journal-journalInferCommodityStyles j = -  case-    commodityStylesFromAmounts $ journalStyleInfluencingAmounts j-  of+journalInferCommodityStyles j =+  case commodityStylesFromAmounts $ journalStyleInfluencingAmounts j of     Left e   -> Left e     Right cs -> Right j{jinferredcommodities = dbg7 "journalInferCommodityStyles" cs} @@ -1117,7 +1113,6 @@ -- | 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.@@ -1153,7 +1148,7 @@ --       fixtransaction t@Transaction{tdate=d, tpostings=ps} = t{tpostings=map fixposting ps} --        where --         fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a}---         fixmixedamount (Mixed as) = Mixed $ map fixamount as+--         fixmixedamount = mapMixedAmount fixamount --         fixamount = fixprice --         fixprice a@Amount{price=Just _} = a --         fixprice a@Amount{commodity=c} = a{price=maybe Nothing (Just . UnitPrice) $ journalPriceDirectiveFor j d c}@@ -1181,16 +1176,16 @@ -- first commodity amount is considered. postingInferredmarketPrice :: Posting -> Maybe MarketPrice postingInferredmarketPrice p@Posting{pamount} =-  -- convert any total prices to unit prices-  case mixedAmountTotalPriceToUnitPrice pamount of-    Mixed ( Amount{acommodity=fromcomm, aprice = Just (UnitPrice Amount{acommodity=tocomm, aquantity=rate})} : _) ->-      Just MarketPrice {-         mpdate = postingDate p-        ,mpfrom = fromcomm-        ,mpto   = tocomm-        ,mprate = rate-        }-    _ -> Nothing+    -- convert any total prices to unit prices+    case amountsRaw $ mixedAmountTotalPriceToUnitPrice pamount of+      Amount{acommodity=fromcomm, aprice = Just (UnitPrice Amount{acommodity=tocomm, aquantity=rate})}:_ ->+        Just MarketPrice {+           mpdate = postingDate p+          ,mpfrom = fromcomm+          ,mpto   = tocomm+          ,mprate = rate+          }+      _ -> Nothing  -- | Convert all this journal's amounts to cost using the transaction prices, if any. -- The journal's commodity styles are applied to the resulting amounts.@@ -1229,12 +1224,12 @@ -- Transaction price amounts (posting amounts' aprice field) are not included. -- journalStyleInfluencingAmounts :: Journal -> [Amount]-journalStyleInfluencingAmounts j = +journalStyleInfluencingAmounts j =   dbg7 "journalStyleInfluencingAmounts" $   catMaybes $ concat [    [mdefaultcommodityamt]   ,map (Just . pdamount) $ jpricedirectives j-  ,map Just $ concatMap amounts $ map pamount $ journalPostings j+  ,map Just . concatMap (amountsRaw . pamount) $ journalPostings j   ]   where     -- D's amount style isn't actually stored as an amount, make it into one@@ -1410,7 +1405,7 @@ --     liabilities:debts  $1 --     assets:bank:checking ---Right samplejournal = journalBalanceTransactions False $+Right samplejournal = journalBalanceTransactions def $          nulljournal          {jtxns = [            txnTieKnot $ Transaction {@@ -1553,7 +1548,7 @@   ,tests "journalBalanceTransactions" [       test "balance-assignment" $ do-      let ej = journalBalanceTransactions True $+      let ej = journalBalanceTransactions def $             --2019/01/01             --  (a)            = 1             nulljournal{ jtxns = [@@ -1561,10 +1556,10 @@             ]}       assertRight ej       let Right j = ej-      (jtxns j & head & tpostings & head & pamount) @?= Mixed [num 1]+      (jtxns j & head & tpostings & head & pamount & amountsRaw) @?= [num 1]      ,test "same-day-1" $ do-      assertRight $ journalBalanceTransactions True $+      assertRight $ journalBalanceTransactions def $             --2019/01/01             --  (a)            = 1             --2019/01/01@@ -1575,7 +1570,7 @@             ]}      ,test "same-day-2" $ do-      assertRight $ journalBalanceTransactions True $+      assertRight $ journalBalanceTransactions def $             --2019/01/01             --    (a)                  2 = 2             --2019/01/01@@ -1593,7 +1588,7 @@             ]}      ,test "out-of-order" $ do-      assertRight $ journalBalanceTransactions True $+      assertRight $ journalBalanceTransactions def $             --2019/1/2             --  (a)    1 = 2             --2019/1/1
Hledger/Data/Json.hs view
@@ -4,12 +4,12 @@  {-# OPTIONS_GHC -fno-warn-orphans #-} -{-# LANGUAGE CPP                 #-} --{-# LANGUAGE DataKinds           #-} --{-# LANGUAGE DeriveAnyClass      #-} {-# LANGUAGE DeriveGeneric       #-} --{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances   #-}+{-# LANGUAGE LambdaCase          #-} --{-# LANGUAGE NamedFieldPuns #-} --{-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE OverloadedStrings #-}@@ -33,15 +33,12 @@   ,readJsonFile ) where -#if !(MIN_VERSION_base(4,13,0))-import           Data.Semigroup ((<>))-#endif import           Data.Aeson import           Data.Aeson.Encode.Pretty (encodePrettyToTextBuilder) --import           Data.Aeson.TH import qualified Data.ByteString.Lazy as BL-import           Data.Decimal-import           Data.Maybe+import           Data.Decimal (DecimalRaw(..), roundTo)+import           Data.Maybe (fromMaybe) import qualified Data.Text.Lazy    as TL import qualified Data.Text.Lazy.IO as TL import qualified Data.Text.Lazy.Builder as TB@@ -49,6 +46,7 @@ import           System.Time (ClockTime)  import           Hledger.Data.Types+import           Hledger.Data.Amount (amountsRaw, mixed)  -- To JSON @@ -79,41 +77,61 @@ -- and that the overall number of significant digits in the floating point -- remains manageable in practice. (I'm not sure how to limit the number -- of significant digits in a Decimal right now.)-instance ToJSON Decimal where-  toJSON d = object-    ["decimalPlaces"   .= toJSON decimalPlaces-    ,"decimalMantissa" .= toJSON decimalMantissa-    ,"floatingPoint"   .= toJSON (fromRational $ toRational d' :: Double)+instance (Integral a, ToJSON a) => ToJSON (DecimalRaw a) where+  toJSON = object . decimalKV+  toEncoding = pairs . mconcat . decimalKV++decimalKV :: (KeyValue kv, Integral a, ToJSON a) => DecimalRaw a -> [kv]+decimalKV d = let d' = if decimalPlaces d <= 10 then d else roundTo 10 d in+    [ "decimalPlaces"   .= decimalPlaces d'+    , "decimalMantissa" .= decimalMantissa d'+    , "floatingPoint"   .= (realToFrac d' :: Double)     ]-    where d'@Decimal{..} = roundTo 10 d  instance ToJSON Amount instance ToJSON AmountStyle-instance ToJSON AmountPrecision++-- Use the same JSON serialisation as Maybe Word8+instance ToJSON AmountPrecision where+  toJSON = toJSON . \case+    Precision n      -> Just n+    NaturalPrecision -> Nothing+  toEncoding = toEncoding . \case+    Precision n      -> Just n+    NaturalPrecision -> Nothing+ instance ToJSON Side instance ToJSON DigitGroupStyle-instance ToJSON MixedAmount++instance ToJSON MixedAmount where+  toJSON = toJSON . amountsRaw+  toEncoding = toEncoding . amountsRaw+ instance ToJSON BalanceAssertion instance ToJSON AmountPrice instance ToJSON MarketPrice instance ToJSON PostingType  instance ToJSON Posting where-  toJSON Posting{..} = object-    ["pdate"             .= pdate-    ,"pdate2"            .= pdate2-    ,"pstatus"           .= pstatus-    ,"paccount"          .= paccount-    ,"pamount"           .= pamount-    ,"pcomment"          .= pcomment-    ,"ptype"             .= ptype-    ,"ptags"             .= ptags-    ,"pbalanceassertion" .= pbalanceassertion+  toJSON = object . postingKV+  toEncoding = pairs . mconcat . postingKV++postingKV :: KeyValue kv => Posting -> [kv]+postingKV Posting{..} =+    [ "pdate"             .= pdate+    , "pdate2"            .= pdate2+    , "pstatus"           .= pstatus+    , "paccount"          .= paccount+    , "pamount"           .= pamount+    , "pcomment"          .= pcomment+    , "ptype"             .= ptype+    , "ptags"             .= ptags+    , "pbalanceassertion" .= pbalanceassertion     -- To avoid a cycle, show just the parent transaction's index number     -- in a dummy field. When re-parsed, there will be no parent.-    ,"ptransaction_"     .= maybe "" (show.tindex) ptransaction+    , "ptransaction_"     .= maybe "" (show.tindex) ptransaction     -- This is probably not wanted in json, we discard it.-    ,"poriginal"         .= (Nothing :: Maybe Posting)+    , "poriginal"         .= (Nothing :: Maybe Posting)     ]  instance ToJSON Transaction@@ -134,21 +152,25 @@ instance ToJSON Journal  instance ToJSON Account where-  toJSON a = object-    ["aname"        .= aname a-    ,"aebalance"    .= aebalance a-    ,"aibalance"    .= aibalance a-    ,"anumpostings" .= anumpostings a-    ,"aboring"      .= aboring a+  toJSON = object . accountKV+  toEncoding = pairs . mconcat . accountKV++accountKV :: KeyValue kv => Account -> [kv]+accountKV a =+    [ "aname"        .= aname a+    , "aebalance"    .= aebalance a+    , "aibalance"    .= aibalance a+    , "anumpostings" .= anumpostings a+    , "aboring"      .= aboring a     -- To avoid a cycle, show just the parent account's name     -- in a dummy field. When re-parsed, there will be no parent.-    ,"aparent_"     .= maybe "" aname (aparent a)+    , "aparent_"     .= maybe "" aname (aparent a)     -- Just the names of subaccounts, as a dummy field, ignored when parsed.-    ,"asubs_"       .= map aname (asubs a)+    , "asubs_"       .= map aname (asubs a)     -- The actual subaccounts (and their subs..), making a (probably highly redundant) tree     -- ,"asubs"        .= asubs a     -- Omit the actual subaccounts-    ,"asubs"        .= ([]::[Account])+    , "asubs"        .= ([]::[Account])     ]  deriving instance Generic (Ledger)@@ -160,10 +182,17 @@ instance FromJSON GenericSourcePos instance FromJSON Amount instance FromJSON AmountStyle-instance FromJSON AmountPrecision++-- Use the same JSON serialisation as Maybe Word8+instance FromJSON AmountPrecision where+  parseJSON = fmap (maybe NaturalPrecision Precision) . parseJSON+ instance FromJSON Side instance FromJSON DigitGroupStyle-instance FromJSON MixedAmount++instance FromJSON MixedAmount where+  parseJSON = fmap (mixed :: [Amount] -> MixedAmount) . parseJSON+ instance FromJSON BalanceAssertion instance FromJSON AmountPrice instance FromJSON MarketPrice
Hledger/Data/Ledger.hs view
@@ -31,9 +31,7 @@ import Hledger.Utils.Test import Hledger.Data.Types import Hledger.Data.Account-import Hledger.Data.Dates (daysSpan) import Hledger.Data.Journal-import Hledger.Data.Posting (postingDate) import Hledger.Query  @@ -94,7 +92,7 @@ -- | The (fully specified) date span containing all the ledger's (filtered) transactions, -- or DateSpan Nothing Nothing if there are none. ledgerDateSpan :: Ledger -> DateSpan-ledgerDateSpan = daysSpan . map postingDate . ledgerPostings+ledgerDateSpan = journalDateSpanBothDates . ljournal  -- | All commodities used in this ledger. ledgerCommodities :: Ledger -> [CommoditySymbol]
Hledger/Data/PeriodicTransaction.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-|@@ -12,9 +11,6 @@ ) where -#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup ((<>))-#endif import qualified Data.Text as T import qualified Data.Text.IO as T import Text.Printf
Hledger/Data/Posting.hs view
@@ -9,7 +9,6 @@ -}  {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-}  module Hledger.Data.Posting (   -- * Posting@@ -37,7 +36,7 @@   postingAllTags,   transactionAllTags,   relatedPostings,-  removePrices,+  postingStripPrices,   postingApplyAliases,   -- * date operations   postingDate,@@ -64,7 +63,6 @@   -- * misc.   showComment,   postingTransformAmount,-  postingApplyCostValuation,   postingApplyValuation,   postingToCost,   tests_Posting@@ -75,21 +73,19 @@ import Data.Foldable (asum) import Data.List.Extra (nubSort) import qualified Data.Map as M-import Data.Maybe+import Data.Maybe (fromMaybe, isJust) import Data.MemoUgly (memo)-#if !(MIN_VERSION_base(4,11,0))-import Data.Monoid-#endif+import Data.List (foldl') import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar-import Safe+import Data.Time.Calendar (Day)+import Safe (headDef)  import Hledger.Utils import Hledger.Data.Types import Hledger.Data.Amount import Hledger.Data.AccountName-import Hledger.Data.Dates (nulldate, spanContainsDate)+import Hledger.Data.Dates (nulldate, showDate, spanContainsDate) import Hledger.Data.Valuation  @@ -114,7 +110,7 @@  -- | Make a posting to an account. post :: AccountName -> Amount -> Posting-post acc amt = posting {paccount=acc, pamount=Mixed [amt]}+post acc amt = posting {paccount=acc, pamount=mixedAmount amt}  -- | Make a virtual (unbalanced) posting to an account. vpost :: AccountName -> Amount -> Posting@@ -122,7 +118,7 @@  -- | Make a posting to an account, maybe with a balance assertion. post' :: AccountName -> Amount -> Maybe BalanceAssertion -> Posting-post' acc amt ass = posting {paccount=acc, pamount=Mixed [amt], pbalanceassertion=ass}+post' acc amt ass = posting {paccount=acc, pamount=mixedAmount amt, pbalanceassertion=ass}  -- | Make a virtual (unbalanced) posting to an account, maybe with a balance assertion. vpost' :: AccountName -> Amount -> Maybe BalanceAssertion -> Posting@@ -162,16 +158,16 @@ -- 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, T.unpack . showComment $ pcomment p]]-    where-      ledger3ishlayout = False-      acctnamewidth = if ledger3ishlayout then 25 else 22-      showaccountname = T.unpack . fitText (Just acctnamewidth) Nothing False False . bracket . elideAccountName width-      (bracket,width) = case t of-                          BalancedVirtualPosting -> (wrap "[" "]", acctnamewidth-2)-                          VirtualPosting         -> (wrap "(" ")", acctnamewidth-2)-                          _                      -> (id,acctnamewidth)-      showamount = wbUnpack . showMixedAmountB noColour{displayMinWidth=Just 12}+    T.unpack $ textConcatTopPadded [showDate (postingDate p) <> " ", showaccountname a <> " ", showamt, showComment $ pcomment p]+  where+    ledger3ishlayout = False+    acctnamewidth = if ledger3ishlayout then 25 else 22+    showaccountname = fitText (Just acctnamewidth) Nothing False False . bracket . elideAccountName width+    (bracket,width) = case t of+                        BalancedVirtualPosting -> (wrap "[" "]", acctnamewidth-2)+                        VirtualPosting         -> (wrap "(" ")", acctnamewidth-2)+                        _                      -> (id,acctnamewidth)+    showamt = wbToText $ showMixedAmountB noColour{displayMinWidth=Just 12} amt   showComment :: Text -> Text@@ -196,13 +192,13 @@ accountNamesFromPostings :: [Posting] -> [AccountName] accountNamesFromPostings = nubSort . map paccount +-- | Sum all amounts from a list of postings. sumPostings :: [Posting] -> MixedAmount-sumPostings = sumStrict . map pamount+sumPostings = foldl' (\amt p -> maPlus amt $ pamount p) nullmixedamt --- | Remove all prices of a posting-removePrices :: Posting -> Posting-removePrices p = p{ pamount = Mixed $ remove <$> amounts (pamount p) }-  where remove a = a { aprice = Nothing }+-- | Strip all prices from a Posting.+postingStripPrices :: Posting -> Posting+postingStripPrices = postingTransformAmount mixedAmountStripPrices  -- | Get a posting's (primary) date - it's own primary date if specified, -- otherwise the parent transaction's primary date, or the null date if@@ -330,14 +326,6 @@   | otherwise = Right a 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, and reference dates.
Hledger/Data/Timeclock.hs view
@@ -6,7 +6,6 @@  -} -{-# LANGUAGE CPP               #-} {-# LANGUAGE OverloadedStrings #-}  module Hledger.Data.Timeclock (@@ -16,10 +15,6 @@ where  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 (addDays) import Data.Time.Clock (addUTCTime, getCurrentTime)@@ -121,7 +116,11 @@       showtime = take 5 . show       hours    = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc       acctname = tlaccount i-      amount   = Mixed [hrs hours]+      -- Generate an hours amount. Unusually, we also round the internal Decimal value,+      -- since otherwise it will often have large recurring decimal parts which (since 1.21)+      -- print would display all 255 digits of. timeclock amounts have one second resolution,+      -- so two decimal places is precise enough (#1527).+      amount   = mixedAmount $ setAmountInternalPrecision 2 $ hrs hours       ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]  
Hledger/Data/Transaction.hs view
@@ -7,7 +7,6 @@  -} -{-# LANGUAGE CPP               #-} {-# LANGUAGE FlexibleContexts  #-} {-# LANGUAGE LambdaCase        #-} {-# LANGUAGE OverloadedStrings #-}@@ -29,11 +28,12 @@   virtualPostings,   balancedVirtualPostings,   transactionsPostings,+  BalancingOpts(..),+  balancingOpts,   isTransactionBalanced,   balanceTransaction,   balanceTransactionHelper,   transactionTransformPostings,-  transactionApplyCostValuation,   transactionApplyValuation,   transactionToCost,   transactionApplyAliases,@@ -62,19 +62,18 @@ ) where -import Data.Default (def)+import Data.Default (Default(..))+import Data.Foldable (asum) import Data.List (intercalate, partition) import Data.List.Extra (nubSort)-import Data.Maybe (fromMaybe, mapMaybe)-#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup ((<>))-#endif+import Data.Maybe (fromMaybe, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB import Data.Time.Calendar (Day, fromGregorian) import qualified Data.Map as M+import Safe (maximumDef)  import Hledger.Utils import Hledger.Data.Types@@ -82,7 +81,6 @@ import Hledger.Data.Posting import Hledger.Data.Amount import Hledger.Data.Valuation-import Text.Tabular import Text.Tabular.AsciiWide  sourceFilePath :: GenericSourcePos -> FilePath@@ -218,9 +216,12 @@ -- -- Posting amounts will be aligned with each other, starting about 4 columns -- beyond the widest account name (see postingAsLines for details).--- postingsAsLines :: Bool -> [Posting] -> [Text]-postingsAsLines onelineamounts ps = concatMap (postingAsLines False onelineamounts ps) ps+postingsAsLines onelineamounts ps = concatMap first3 linesWithWidths+  where+    linesWithWidths = map (postingAsLines False onelineamounts maxacctwidth maxamtwidth) ps+    maxacctwidth = maximumDef 0 $ map second3 linesWithWidths+    maxamtwidth  = maximumDef 0 $ map third3 linesWithWidths  -- | Render one posting, on one or more lines, suitable for `print` output. -- There will be an indented account name, plus one or more of status flag,@@ -241,9 +242,10 @@ -- 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 -> [Text]-postingAsLines elideamount onelineamounts pstoalignwith p =-    concatMap (++ newlinecomments) postingblocks+-- Also returns the account width and amount width used.+postingAsLines :: Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)+postingAsLines elideamount onelineamounts acctwidth amtwidth p =+    (concatMap (++ newlinecomments) postingblocks, thisacctwidth, thisamtwidth)   where     -- This needs to be converted to strict Text in order to strip trailing     -- spaces. This adds a small amount of inefficiency, and the only difference@@ -253,30 +255,34 @@     postingblocks = [map T.stripEnd . T.lines . TL.toStrict $                        render [ textCell BottomLeft statusandaccount                               , textCell BottomLeft "  "-                              , Cell BottomLeft [amt]+                              , Cell BottomLeft [pad amt]                               , Cell BottomLeft [assertion]                               , textCell BottomLeft samelinecomment                               ]                     | amt <- shownAmounts]     render = renderRow def{tableBorders=False, borderSpaces=False} . Group NoLine . map Header+    pad amt = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt+      where w = max 12 amtwidth - wbWidth amt  -- min. 12 for backwards compatibility+     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')+    -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned+    statusandaccount = lineIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p+    thisacctwidth = textWidth $ pacctstr p +    pacctstr p' = showAccountName Nothing (ptype p') (paccount p')+    pstatusandacct p' = pstatusprefix p' <> pacctstr p'+    pstatusprefix p' = case pstatus p' of+        Unmarked -> ""+        s        -> T.pack (show s) <> " "+     -- currently prices are considered part of the amount string when right-aligning amounts+    -- Since we will usually be calling this function with the knot tied between+    -- amtwidth and thisamtwidth, make sure thisamtwidth does not depend on+    -- amtwidth at all.     shownAmounts-      | elideamount || null (amounts $ pamount p) = [mempty]-      | otherwise = showMixedAmountLinesB displayopts $ pamount p-      where-        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+      | elideamount = [mempty]+      | otherwise   = showMixedAmountLinesB noColour{displayOneLine=onelineamounts} $ pamount p+    thisamtwidth = maximumDef 0 $ map wbWidth shownAmounts      (samelinecomment, newlinecomments) =       case renderCommentLines (pcomment p) of []   -> ("",[])@@ -306,9 +312,11 @@ -- | Render a posting, at the appropriate width for aligning with -- its siblings if any. Used by the rewrite command. showPostingLines :: Posting -> [Text]-showPostingLines p = postingAsLines False False ps p where-    ps | Just t <- ptransaction p = tpostings t-       | otherwise = [p]+showPostingLines p = first3 $ postingAsLines False False maxacctwidth maxamtwidth p+  where+    linesWithWidths = map (postingAsLines False False maxacctwidth maxamtwidth) . maybe [p] tpostings $ ptransaction p+    maxacctwidth = maximumDef 0 $ map second3 linesWithWidths+    maxamtwidth  = maximumDef 0 $ map third3 linesWithWidths  -- | Prepend a suitable indent for a posting (or transaction/posting comment) line. lineIndent :: Text -> Text@@ -345,6 +353,21 @@ transactionsPostings :: [Transaction] -> [Posting] transactionsPostings = concatMap tpostings +data BalancingOpts = BalancingOpts+  { ignore_assertions_ :: Bool  -- ^ Ignore balance assertions+  , infer_prices_      :: Bool  -- ^ Infer prices in unbalanced multicommodity amounts+  , commodity_styles_  :: Maybe (M.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles+  } deriving (Show)++instance Default BalancingOpts where def = balancingOpts++balancingOpts :: BalancingOpts+balancingOpts = BalancingOpts+  { ignore_assertions_ = False+  , infer_prices_      = True+  , commodity_styles_  = Nothing+  }+ -- | Check that this transaction would appear balanced to a human when displayed. -- On success, returns the empty list, otherwise one or more error messages. --@@ -362,13 +385,13 @@ -- 3. Does the amounts' sum appear non-zero when displayed ? --    (using the given display styles if provided) ---transactionCheckBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> [String]-transactionCheckBalanced mstyles t = errs+transactionCheckBalanced :: BalancingOpts -> Transaction -> [String]+transactionCheckBalanced BalancingOpts{commodity_styles_} t = errs   where     (rps, bvps) = (realPostings t, balancedVirtualPostings t)      -- check for mixed signs, detecting nonzeros at display precision-    canonicalise = maybe id canonicaliseMixedAmount mstyles+    canonicalise = maybe id canonicaliseMixedAmount commodity_styles_     signsOk ps =       case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of         nonzeros | length nonzeros >= 2@@ -395,8 +418,8 @@           | otherwise     = ""  -- | Legacy form of transactionCheckBalanced.-isTransactionBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Bool-isTransactionBalanced mstyles = null . transactionCheckBalanced mstyles+isTransactionBalanced :: BalancingOpts -> Transaction -> Bool+isTransactionBalanced bopts = null . transactionCheckBalanced bopts  -- | Balance this transaction, ensuring that its postings -- (and its balanced virtual postings) sum to 0,@@ -412,22 +435,22 @@ -- if provided, so that the result agrees with the numbers users can see. -- balanceTransaction ::-     Maybe (M.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles+     BalancingOpts   -> Transaction   -> Either String Transaction-balanceTransaction mstyles = fmap fst . balanceTransactionHelper mstyles+balanceTransaction bopts = fmap fst . balanceTransactionHelper bopts  -- | Helper used by balanceTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB; -- use one of those instead. It also returns a list of accounts -- and amounts that were inferred. balanceTransactionHelper ::-     Maybe (M.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles+     BalancingOpts   -> Transaction   -> Either String (Transaction, [(AccountName, MixedAmount)])-balanceTransactionHelper mstyles t = do-  (t', inferredamtsandaccts) <--    inferBalancingAmount (fromMaybe M.empty mstyles) $ inferBalancingPrices t-  case transactionCheckBalanced mstyles t' of+balanceTransactionHelper bopts t = do+  (t', inferredamtsandaccts) <- inferBalancingAmount (fromMaybe M.empty $ commodity_styles_ bopts) $+    if infer_prices_ bopts then inferBalancingPrices t else t+  case transactionCheckBalanced bopts t' of     []   -> Right (txnTieKnot t', inferredamtsandaccts)     errs -> Left $ transactionBalanceError t' errs @@ -471,9 +494,9 @@         in Right (t{tpostings=map fst psandinferredamts}, inferredacctsandamts)   where     (amountfulrealps, amountlessrealps) = partition hasAmount (realPostings t)-    realsum = sumStrict $ map pamount amountfulrealps+    realsum = sumPostings amountfulrealps     (amountfulbvps, amountlessbvps) = partition hasAmount (balancedVirtualPostings t)-    bvsum = sumStrict $ map pamount amountfulbvps+    bvsum = sumPostings amountfulbvps      inferamount :: Posting -> (Posting, Maybe MixedAmount)     inferamount p =@@ -490,7 +513,7 @@               -- Inferred amounts are converted to cost.               -- Also ensure the new amount has the standard style for its commodity               -- (since the main amount styling pass happened before this balancing pass);-              a' = styleMixedAmount styles $ normaliseMixedAmount $ mixedAmountCost (-a)+              a' = styleMixedAmount styles . mixedAmountCost $ maNegate a  -- | Infer prices for this transaction's posting amounts, if needed to make -- the postings balance, and if possible. This is done once for the real@@ -537,42 +560,50 @@  -- | Generate a posting update function which assigns a suitable balancing -- price to the posting, if and as appropriate for the given transaction and--- posting type (real or balanced virtual).+-- posting type (real or balanced virtual). If we cannot or should not infer+-- prices, just act as the identity on postings. priceInferrerFor :: Transaction -> PostingType -> (Posting -> Posting)-priceInferrerFor t pt = inferprice+priceInferrerFor t pt = maybe id inferprice inferFromAndTo   where-    postings       = filter ((==pt).ptype) $ tpostings t-    pmixedamounts  = map pamount postings-    pamounts       = concatMap amounts pmixedamounts-    pcommodities   = map acommodity pamounts-    sumamounts     = amounts $ sumStrict pmixedamounts -- sum normalises to one amount per commodity & price-    sumcommodities = map acommodity sumamounts-    sumprices      = filter (/=Nothing) $ map aprice sumamounts-    caninferprices = length sumcommodities == 2 && null sumprices+    postings     = filter ((==pt).ptype) $ tpostings t+    pcommodities = map acommodity $ concatMap (amounts . pamount) postings+    sumamounts   = amounts $ sumPostings postings  -- amounts normalises to one amount per commodity & price -    inferprice p@Posting{pamount=Mixed [a]}-      | caninferprices && ptype p == pt && acommodity a == fromcommodity-        = p{pamount=Mixed [a{aprice=Just conversionprice}], poriginal=Just $ originalPosting p}+    -- We can infer prices if there are no prices given, exactly two commodities in the normalised+    -- sum of postings in this transaction, and these two have opposite signs. The amount we are+    -- converting from is the first commodity to appear in the ordered list of postings, and the+    -- commodity we are converting to is the other. If we cannot infer prices, return Nothing.+    inferFromAndTo = case sumamounts of+      [a,b] | noprices, oppositesigns -> asum $ map orderIfMatches pcommodities+        where+          noprices      = all (isNothing . aprice) sumamounts+          oppositesigns = signum (aquantity a) /= signum (aquantity b)+          orderIfMatches x | x == acommodity a = Just (a,b)+                           | x == acommodity b = Just (b,a)+                           | otherwise         = Nothing+      _ -> Nothing++    -- For each posting, if the posting type matches, there is only a single amount in the posting,+    -- and the commodity of the amount matches the amount we're converting from,+    -- then set its price based on the ratio between fromamount and toamount.+    inferprice (fromamount, toamount) posting+        | [a] <- amounts (pamount posting), ptype posting == pt, acommodity a == acommodity fromamount+            = posting{ pamount   = mixedAmount a{aprice=Just conversionprice}+                     , poriginal = Just $ originalPosting posting }+        | otherwise = posting       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 $ totalpricesign (abs toamount) `withPrecision` NaturalPrecision-          | otherwise    = UnitPrice $ abs unitprice `withPrecision` unitprecision-          where-            fromcount     = length $ filter ((==fromcommodity).acommodity) pamounts-            fromamount    = head $ filter ((==fromcommodity).acommodity) sumamounts-            fromprecision = asprecision $ astyle fromamount-            tocommodity   = head $ filter (/=fromcommodity) sumcommodities-            toamount      = head $ filter ((==tocommodity).acommodity) sumamounts-            toprecision   = asprecision $ astyle toamount-            unitprice     = (aquantity fromamount) `divideAmount` toamount-            -- Sum two display precisions, capping the result at the maximum bound-            unitprecision = case (fromprecision, toprecision) of-                (Precision a, Precision b) -> Precision $ if maxBound - a < b then maxBound else max 2 (a + b)-                _                          -> NaturalPrecision-    inferprice p = p+        -- If only one Amount in the posting list matches fromamount we can use TotalPrice.+        -- Otherwise divide the conversion equally among the Amounts by using a unit price.+        conversionprice = case filter (== acommodity fromamount) pcommodities of+            [_] -> TotalPrice $ negate toamount+            _   -> UnitPrice  $ negate unitprice `withPrecision` unitprecision +        unitprice     = aquantity fromamount `divideAmount` toamount+        unitprecision = case (asprecision $ astyle fromamount, asprecision $ astyle toamount) of+            (Precision a, Precision b) -> Precision . max 2 $ saturatedAdd a b+            _                          -> NaturalPrecision+        saturatedAdd a b = if maxBound - a < b then maxBound else a + b+ -- Get a transaction's secondary date, defaulting to the primary date. transactionDate2 :: Transaction -> Day transactionDate2 t = fromMaybe (tdate t) $ tdate2 t@@ -596,13 +627,6 @@ 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, and reference dates. -- See amountApplyValuation.@@ -612,7 +636,7 @@  -- | Convert this transaction's amounts to cost, and apply the appropriate amount styles. transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction-transactionToCost styles t@Transaction{tpostings=ps} = t{tpostings=map (postingToCost styles) ps}+transactionToCost styles = transactionTransformPostings (postingToCost styles)  -- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases. -- This can fail due to a bad replacement pattern in a regular expression alias.@@ -627,8 +651,8 @@ 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))+transactionMapPostingAmounts :: (MixedAmount -> MixedAmount) -> Transaction -> Transaction+transactionMapPostingAmounts f  = transactionMapPostings (postingTransformAmount f)  -- | The file path from which this transaction was parsed. transactionFile :: Transaction -> FilePath@@ -643,19 +667,19 @@ tests_Transaction =   tests "Transaction" [ -      tests "postingAsLines" [-          test "null posting" $ postingAsLines False False [posting] posting @?= [""]+      tests "showPostingLines" [+          test "null posting" $ showPostingLines nullposting @?= ["                   0"]         , test "non-null posting" $            let p =                 posting                   { pstatus = Cleared                   , paccount = "a"-                  , pamount = Mixed [usd 1, hrs 2]+                  , pamount = mixed [usd 1, hrs 2]                   , pcomment = "pcomment1\npcomment2\n  tag3: val3  \n"                   , ptype = RegularPosting                   , ptags = [("ptag1", "val1"), ("ptag2", "val2")]                   }-           in postingAsLines False False [p] p @?=+           in showPostingLines p @?=               [ "    * a         $1.00  ; pcomment1"               , "    ; pcomment2"               , "    ;   tag3: val3  "@@ -731,7 +755,7 @@                   [ nullposting                       { pstatus = Cleared                       , paccount = "a"-                      , pamount = Mixed [usd 1, hrs 2]+                      , pamount = mixed [usd 1, hrs 2]                       , pcomment = "\npcomment2\n"                       , ptype = RegularPosting                       , ptags = [("ptag1", "val1"), ("ptag2", "val2")]@@ -760,8 +784,8 @@                    "coopportunity"                    ""                    []-                   [ posting {paccount = "expenses:food:groceries", pamount = Mixed [usd 47.18], ptransaction = Just t}-                   , posting {paccount = "assets:checking", pamount = Mixed [usd (-47.18)], ptransaction = Just t}+                   [ posting {paccount = "expenses:food:groceries", pamount = mixedAmount (usd 47.18), ptransaction = Just t}+                   , posting {paccount = "assets:checking", pamount = mixedAmount (usd (-47.18)), ptransaction = Just t}                    ]             in showTransaction t) @?=           (T.unlines@@ -784,8 +808,8 @@                 "coopportunity"                 ""                 []-                [ posting {paccount = "expenses:food:groceries", pamount = Mixed [usd 47.18]}-                , posting {paccount = "assets:checking", pamount = Mixed [usd (-47.19)]}+                [ posting {paccount = "expenses:food:groceries", pamount = mixedAmount (usd 47.18)}+                , posting {paccount = "assets:checking", pamount = mixedAmount (usd (-47.19))}                 ])) @?=           (T.unlines              [ "2007-01-28 coopportunity"@@ -823,7 +847,7 @@                 "x"                 ""                 []-                [ posting {paccount = "a", pamount = Mixed [num 1 `at` (usd 2 `withPrecision` Precision 0)]}+                [ posting {paccount = "a", pamount = mixedAmount $ num 1 `at` (usd 2 `withPrecision` Precision 0)}                 , posting {paccount = "b", pamount = missingmixedamt}                 ])) @?=           (T.unlines ["2010-01-01 x", "    a          1 @ $2", "    b", ""])@@ -831,8 +855,7 @@     , tests "balanceTransaction" [          test "detect unbalanced entry, sign error" $           assertLeft-            (balanceTransaction-               Nothing+            (balanceTransaction def                (Transaction                   0                   ""@@ -844,11 +867,10 @@                   "test"                   ""                   []-                  [posting {paccount = "a", pamount = Mixed [usd 1]}, posting {paccount = "b", pamount = Mixed [usd 1]}]))+                  [posting {paccount = "a", pamount = mixedAmount (usd 1)}, posting {paccount = "b", pamount = mixedAmount (usd 1)}]))         ,test "detect unbalanced entry, multiple missing amounts" $           assertLeft $-             balanceTransaction-               Nothing+             balanceTransaction def                (Transaction                   0                   ""@@ -865,8 +887,7 @@                   ])         ,test "one missing amount is inferred" $           (pamount . last . tpostings <$>-           balanceTransaction-             Nothing+           balanceTransaction def              (Transaction                 0                 ""@@ -878,12 +899,11 @@                 ""                 ""                 []-                [posting {paccount = "a", pamount = Mixed [usd 1]}, posting {paccount = "b", pamount = missingmixedamt}])) @?=-          Right (Mixed [usd (-1)])+                [posting {paccount = "a", pamount = mixedAmount (usd 1)}, posting {paccount = "b", pamount = missingmixedamt}])) @?=+          Right (mixedAmount $ usd (-1))         ,test "conversion price is inferred" $           (pamount . head . tpostings <$>-           balanceTransaction-             Nothing+           balanceTransaction def              (Transaction                 0                 ""@@ -895,14 +915,13 @@                 ""                 ""                 []-                [ posting {paccount = "a", pamount = Mixed [usd 1.35]}-                , posting {paccount = "b", pamount = Mixed [eur (-1)]}+                [ posting {paccount = "a", pamount = mixedAmount (usd 1.35)}+                , posting {paccount = "b", pamount = mixedAmount (eur (-1))}                 ])) @?=-          Right (Mixed [usd 1.35 @@ (eur 1 `withPrecision` NaturalPrecision)])+          Right (mixedAmount $ usd 1.35 @@ eur 1)         ,test "balanceTransaction balances based on cost if there are unit prices" $           assertRight $-          balanceTransaction-            Nothing+          balanceTransaction def             (Transaction                0                ""@@ -914,13 +933,12 @@                ""                ""                []-               [ posting {paccount = "a", pamount = Mixed [usd 1 `at` eur 2]}-               , posting {paccount = "a", pamount = Mixed [usd (-2) `at` eur 1]}+               [ posting {paccount = "a", pamount = mixedAmount $ usd 1 `at` eur 2}+               , posting {paccount = "a", pamount = mixedAmount $ usd (-2) `at` eur 1}                ])         ,test "balanceTransaction balances based on cost if there are total prices" $           assertRight $-          balanceTransaction-            Nothing+          balanceTransaction def             (Transaction                0                ""@@ -932,14 +950,14 @@                ""                ""                []-               [ posting {paccount = "a", pamount = Mixed [usd 1 @@ eur 1]}-               , posting {paccount = "a", pamount = Mixed [usd (-2) @@ eur (-1)]}+               [ posting {paccount = "a", pamount = mixedAmount $ usd 1 @@ eur 1}+               , posting {paccount = "a", pamount = mixedAmount $ usd (-2) @@ eur (-1)}                ])         ]     , tests "isTransactionBalanced" [          test "detect balanced" $           assertBool "" $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -951,13 +969,13 @@             "a"             ""             []-            [ posting {paccount = "b", pamount = Mixed [usd 1.00]}-            , posting {paccount = "c", pamount = Mixed [usd (-1.00)]}+            [ posting {paccount = "b", pamount = mixedAmount (usd 1.00)}+            , posting {paccount = "c", pamount = mixedAmount (usd (-1.00))}             ]         ,test "detect unbalanced" $           assertBool "" $           not $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -969,13 +987,13 @@             "a"             ""             []-            [ posting {paccount = "b", pamount = Mixed [usd 1.00]}-            , posting {paccount = "c", pamount = Mixed [usd (-1.01)]}+            [ posting {paccount = "b", pamount = mixedAmount (usd 1.00)}+            , posting {paccount = "c", pamount = mixedAmount (usd (-1.01))}             ]         ,test "detect unbalanced, one posting" $           assertBool "" $           not $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -987,10 +1005,10 @@             "a"             ""             []-            [posting {paccount = "b", pamount = Mixed [usd 1.00]}]+            [posting {paccount = "b", pamount = mixedAmount (usd 1.00)}]         ,test "one zero posting is considered balanced for now" $           assertBool "" $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -1002,10 +1020,10 @@             "a"             ""             []-            [posting {paccount = "b", pamount = Mixed [usd 0]}]+            [posting {paccount = "b", pamount = mixedAmount (usd 0)}]         ,test "virtual postings don't need to balance" $           assertBool "" $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -1017,14 +1035,14 @@             "a"             ""             []-            [ posting {paccount = "b", pamount = Mixed [usd 1.00]}-            , posting {paccount = "c", pamount = Mixed [usd (-1.00)]}-            , posting {paccount = "d", pamount = Mixed [usd 100], ptype = VirtualPosting}+            [ posting {paccount = "b", pamount = mixedAmount (usd 1.00)}+            , posting {paccount = "c", pamount = mixedAmount (usd (-1.00))}+            , posting {paccount = "d", pamount = mixedAmount (usd 100), ptype = VirtualPosting}             ]         ,test "balanced virtual postings need to balance among themselves" $           assertBool "" $           not $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -1036,13 +1054,13 @@             "a"             ""             []-            [ posting {paccount = "b", pamount = Mixed [usd 1.00]}-            , posting {paccount = "c", pamount = Mixed [usd (-1.00)]}-            , posting {paccount = "d", pamount = Mixed [usd 100], ptype = BalancedVirtualPosting}+            [ posting {paccount = "b", pamount = mixedAmount (usd 1.00)}+            , posting {paccount = "c", pamount = mixedAmount (usd (-1.00))}+            , posting {paccount = "d", pamount = mixedAmount (usd 100), ptype = BalancedVirtualPosting}             ]         ,test "balanced virtual postings need to balance among themselves (2)" $           assertBool "" $-          isTransactionBalanced Nothing $+          isTransactionBalanced def $           Transaction             0             ""@@ -1054,10 +1072,10 @@             "a"             ""             []-            [ posting {paccount = "b", pamount = Mixed [usd 1.00]}-            , posting {paccount = "c", pamount = Mixed [usd (-1.00)]}-            , posting {paccount = "d", pamount = Mixed [usd 100], ptype = BalancedVirtualPosting}-            , posting {paccount = "3", pamount = Mixed [usd (-100)], ptype = BalancedVirtualPosting}+            [ posting {paccount = "b", pamount = mixedAmount (usd 1.00)}+            , posting {paccount = "c", pamount = mixedAmount (usd (-1.00))}+            , posting {paccount = "d", pamount = mixedAmount (usd 100), ptype = BalancedVirtualPosting}+            , posting {paccount = "3", pamount = mixedAmount (usd (-100)), ptype = BalancedVirtualPosting}             ]         ]     ]
Hledger/Data/TransactionModifier.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE CPP #-} {-|  A 'TransactionModifier' is a rule that modifies certain 'Transaction's,@@ -13,20 +12,18 @@ ) where -import Control.Applicative ((<|>))-import Data.Maybe-#if !(MIN_VERSION_base(4,11,0))-import Data.Monoid ((<>))-#endif+import Control.Applicative ((<|>), liftA2)+import Data.Maybe (catMaybes) import qualified Data.Text as T-import Data.Time.Calendar+import Data.Time.Calendar (Day) import Hledger.Data.Types import Hledger.Data.Dates import Hledger.Data.Amount-import Hledger.Data.Transaction-import Hledger.Query+import Hledger.Data.Transaction (txnTieKnot)+import Hledger.Query (Query, filterQuery, matchesAmount, matchesPosting,+                      parseQuery, queryIsAmt, queryIsSym, simplifyQuery) import Hledger.Data.Posting (commentJoin, commentAddTag)-import Hledger.Utils+import Hledger.Utils (dbg6, wrap)  -- $setup -- >>> :set -XOverloadedStrings@@ -61,8 +58,8 @@ -- Currently the only kind of modification possible is adding automated -- postings when certain other postings are present. ----- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]} -- >>> import qualified Data.Text.IO as T+-- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]} -- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction nulldate -- >>> test $ TransactionModifier "" ["pong" `post` usd 2] -- 0000-01-01@@ -83,9 +80,8 @@ transactionModifierToFunction refdate TransactionModifier{tmquerytxt, tmpostingrules} = do   q <- simplifyQuery . fst <$> parseQuery refdate tmquerytxt   let-    fs = map (tmPostingRuleToFunction tmquerytxt) tmpostingrules-    generatePostings ps = [p' | p <- ps-                              , p' <- if q `matchesPosting` p then p:[f p | f <- fs] else [p]]+    fs = map (tmPostingRuleToFunction q tmquerytxt) tmpostingrules+    generatePostings ps = concatMap (\p -> p : map ($p) (if q `matchesPosting` p then fs else [])) ps   Right $ \t@(tpostings -> ps) -> txnTieKnot t{tpostings=generatePostings ps}  -- | Converts a 'TransactionModifier''s posting rule to a 'Posting'-generating function,@@ -96,8 +92,8 @@ -- and a hidden _generated-posting: tag which does not. -- The TransactionModifier's query text is also provided, and saved -- as the tags' value.-tmPostingRuleToFunction :: T.Text -> TMPostingRule -> (Posting -> Posting)-tmPostingRuleToFunction querytxt pr =+tmPostingRuleToFunction :: Query -> T.Text -> TMPostingRule -> (Posting -> Posting)+tmPostingRuleToFunction query querytxt pr =   \p -> renderPostingCommentDates $ pr       { pdate    = pdate  pr <|> pdate  p       , pdate2   = pdate2 pr <|> pdate2 p@@ -109,31 +105,31 @@       }   where     qry = "= " <> querytxt+    symq = filterQuery (liftA2 (||) queryIsSym queryIsAmt) query     amount' = case postingRuleMultiplier pr of         Nothing -> const $ pamount pr         Just n  -> \p ->           -- Multiply the old posting's amount by the posting rule's multiplier.           let-            pramount = dbg6 "pramount" $ head $ amounts $ pamount pr-            matchedamount = dbg6 "matchedamount" $ pamount p+            pramount = dbg6 "pramount" . head . amountsRaw $ pamount pr+            matchedamount = dbg6 "matchedamount" . filterMixedAmount (symq `matchesAmount`) $ pamount p             -- Handle a matched amount with a total price carefully so as to keep the transaction balanced (#928).             -- 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 `multiplyMixedAmount` matchedamount+            as = dbg6 "multipliedamount" $ multiplyMixedAmount n matchedamount           in             case acommodity pramount of-              "" -> Mixed as+              "" -> as               -- TODO multipliers with commodity symbols are not yet a documented feature.               -- For now: in addition to multiplying the quantity, it also replaces the               -- matched amount's commodity, display style, and price with those of the posting rule.-              c  -> Mixed [a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount} | a <- as]+              c  -> mapMixedAmount (\a -> a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount}) as  postingRuleMultiplier :: TMPostingRule -> Maybe Quantity-postingRuleMultiplier p =-    case amounts $ pamount p of-        [a] | aismultiplier a -> Just $ aquantity a-        _                   -> Nothing+postingRuleMultiplier p = case amountsRaw $ pamount p of+    [a] | aismultiplier a -> Just $ aquantity a+    _                     -> Nothing  renderPostingCommentDates :: Posting -> Posting renderPostingCommentDates p = p { pcomment = comment' }
Hledger/Data/Types.hs view
@@ -17,12 +17,11 @@ -}  -- {-# LANGUAGE DeriveAnyClass #-}  -- https://hackage.haskell.org/package/deepseq-1.4.4.0/docs/Control-DeepSeq.html#v:rnf-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE DeriveGeneric        #-}+{-# LANGUAGE FlexibleInstances    #-}+{-# LANGUAGE OverloadedStrings    #-}+{-# LANGUAGE RecordWildCards      #-}+{-# LANGUAGE StandaloneDeriving   #-}  module Hledger.Data.Types where@@ -38,13 +37,12 @@ --You will eventually need all the values stored. --The stored values don't represent large virtual data structures to be lazily computed. import qualified Data.Map as M+import Data.Ord (comparing) import Data.Text (Text)--- import qualified Data.Text as T import Data.Time.Calendar import Data.Time.LocalTime import Data.Word (Word8) import System.Time (ClockTime(..))-import Text.Printf  import Hledger.Utils.Regex @@ -191,13 +189,15 @@ } deriving (Eq,Ord,Read,Generic)  instance Show AmountStyle where-  show AmountStyle{..} =-    printf "AmountStylePP \"%s %s %s %s %s..\""-    (show ascommodityside)-    (show ascommodityspaced)-    (show asprecision)-    (show asdecimalpoint)-    (show asdigitgroups)+  show AmountStyle{..} = concat+    [ "AmountStylePP \""+    , show ascommodityside+    , show ascommodityspaced+    , show asprecision+    , 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.@@ -230,7 +230,38 @@       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)+newtype MixedAmount = Mixed (M.Map MixedAmountKey Amount) deriving (Eq,Ord,Generic,Show)++-- | Stores the CommoditySymbol of the Amount, along with the CommoditySymbol of+-- the price, and its unit price if being used.+data MixedAmountKey+  = MixedAmountKeyNoPrice    !CommoditySymbol+  | MixedAmountKeyTotalPrice !CommoditySymbol !CommoditySymbol+  | MixedAmountKeyUnitPrice  !CommoditySymbol !CommoditySymbol !Quantity+  deriving (Eq,Generic,Show)++-- | We don't auto-derive the Ord instance because it would give an undesired ordering.+-- We want the keys to be sorted lexicographically:+-- (1) By the primary commodity of the amount.+-- (2) By the commodity of the price, with no price being first.+-- (3) By the unit price, from most negative to most positive, with total prices+-- before unit prices.+-- For example, we would like the ordering to give+-- MixedAmountKeyNoPrice "X" < MixedAmountKeyTotalPrice "X" "Z" < MixedAmountKeyNoPrice "Y"+instance Ord MixedAmountKey where+  compare = comparing commodity <> comparing pCommodity <> comparing pPrice+    where+      commodity (MixedAmountKeyNoPrice    c)     = c+      commodity (MixedAmountKeyTotalPrice c _)   = c+      commodity (MixedAmountKeyUnitPrice  c _ _) = c++      pCommodity (MixedAmountKeyNoPrice    _)      = Nothing+      pCommodity (MixedAmountKeyTotalPrice _ pc)   = Just pc+      pCommodity (MixedAmountKeyUnitPrice  _ pc _) = Just pc++      pPrice (MixedAmountKeyNoPrice    _)     = Nothing+      pPrice (MixedAmountKeyTotalPrice _ _)   = Nothing+      pPrice (MixedAmountKeyUnitPrice  _ _ q) = Just q  data PostingType = RegularPosting | VirtualPosting | BalancedVirtualPosting                    deriving (Eq,Show,Generic)
Hledger/Data/Valuation.hs view
@@ -17,9 +17,7 @@   ,ValuationType(..)   ,PriceOracle   ,journalPriceOracle-  -- ,amountApplyValuation-  -- ,amountValueAtDate-  ,mixedAmountApplyCostValuation+  ,mixedAmountToCost   ,mixedAmountApplyValuation   ,mixedAmountValueAtDate   ,marketPriceReverse@@ -99,18 +97,9 @@ ------------------------------------------------------------------------------ -- 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+-- | Convert all component amounts to cost/selling price if requested, and style them.+mixedAmountToCost :: Costing -> M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount+mixedAmountToCost cost styles = mapMixedAmount (amountToCost cost styles)  -- | Apply a specified valuation to this mixed amount, using the -- provided price oracle, commodity styles, and reference dates.@@ -119,6 +108,11 @@ mixedAmountApplyValuation priceoracle styles periodlast today postingdate v =   mapMixedAmount (amountApplyValuation priceoracle styles periodlast today postingdate v) +-- | Convert an Amount to its cost if requested, and style it appropriately.+amountToCost :: Costing -> M.Map CommoditySymbol AmountStyle -> Amount -> Amount+amountToCost NoCost _      = id+amountToCost Cost   styles = styleAmount styles . amountCost+ -- | Apply a specified valuation to this amount, using the provided -- price oracle, reference dates, and whether this is for a -- multiperiod report or not. Also fix up its display style using the@@ -142,7 +136,7 @@ -- -- - the provided "today" date - (--value=now, or -V/X with no report --   end date).--- +-- -- 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.
Hledger/Query.hs view
@@ -5,7 +5,6 @@  -} -{-# LANGUAGE CPP                #-} {-# LANGUAGE FlexibleContexts   #-} {-# LANGUAGE OverloadedStrings  #-} {-# LANGUAGE ViewPatterns       #-}@@ -65,9 +64,6 @@ import Data.Either (partitionEithers) import Data.List (partition) import Data.Maybe (fromMaybe, isJust, mapMaybe)-#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 )@@ -78,7 +74,7 @@ import Hledger.Utils hiding (words') import Hledger.Data.Types import Hledger.Data.AccountName-import Hledger.Data.Amount (nullamt, usd)+import Hledger.Data.Amount (amountsRaw, mixedAmount, nullamt, usd) import Hledger.Data.Dates import Hledger.Data.Posting import Hledger.Data.Transaction@@ -562,8 +558,9 @@ matchesAccount _ _ = True  matchesMixedAmount :: Query -> MixedAmount -> Bool-matchesMixedAmount q (Mixed []) = q `matchesAmount` nullamt-matchesMixedAmount q (Mixed as) = any (q `matchesAmount`) as+matchesMixedAmount q ma = case amountsRaw ma of+    [] -> q `matchesAmount` nullamt+    as -> any (q `matchesAmount`) as  matchesCommodity :: Query -> CommoditySymbol -> Bool matchesCommodity (Sym r) = regexMatchText r@@ -607,15 +604,15 @@ matchesPosting (And qs) p = all (`matchesPosting` p) qs matchesPosting (Code r) p = maybe False (regexMatchText r . tcode) $ ptransaction p matchesPosting (Desc r) p = maybe False (regexMatchText r . tdescription) $ ptransaction p-matchesPosting (Acct r) p = matches p || matches (originalPosting p)+matchesPosting (Acct r) p = matches p || maybe False matches (poriginal p)   where matches = regexMatchText r . paccount matchesPosting (Date span) p = span `spanContainsDate` postingDate p matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p matchesPosting (StatusQ s) p = postingStatus p == s matchesPosting (Real v) p = v == isReal p matchesPosting q@(Depth _) Posting{paccount=a} = q `matchesAccount` a-matchesPosting q@(Amt _ _) Posting{pamount=amt} = q `matchesMixedAmount` amt-matchesPosting (Sym r) Posting{pamount=Mixed as} = any (matchesCommodity (Sym r)) $ map acommodity as+matchesPosting q@(Amt _ _) Posting{pamount=as} = q `matchesMixedAmount` as+matchesPosting (Sym r) Posting{pamount=as} = any (matchesCommodity (Sym r)) . map acommodity $ amountsRaw as matchesPosting (Tag n v) p = case (reString n, v) of   ("payee", Just v) -> maybe False (regexMatchText v . transactionPayee) $ ptransaction p   ("note", Just v) -> maybe False (regexMatchText v . transactionNote) $ ptransaction p@@ -811,10 +808,10 @@     ,test "a tag match on a posting also sees inherited tags" $ assertBool "" $ (Tag (toRegex' "txntag") Nothing) `matchesPosting` nullposting{ptransaction=Just nulltransaction{ttags=[("txntag","")]}}     ,test "cur:" $ do       let toSym = either id (const $ error' "No query opts") . either error' id . parseQueryTerm (fromGregorian 2000 01 01) . ("cur:"<>)-      assertBool "" $ not $ toSym "$" `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- becomes "^$$", ie testing for null symbol-      assertBool "" $ (toSym "\\$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- have to quote $ for regexpr-      assertBool "" $ (toSym "shekels") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}-      assertBool "" $ not $ (toSym "shek") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}+      assertBool "" $ not $ toSym "$" `matchesPosting` nullposting{pamount=mixedAmount $ usd 1} -- becomes "^$$", ie testing for null symbol+      assertBool "" $ (toSym "\\$") `matchesPosting` nullposting{pamount=mixedAmount $ usd 1} -- have to quote $ for regexpr+      assertBool "" $ (toSym "shekels") `matchesPosting` nullposting{pamount=mixedAmount nullamt{acommodity="shekels"}}+      assertBool "" $ not $ (toSym "shek") `matchesPosting` nullposting{pamount=mixedAmount nullamt{acommodity="shekels"}}   ]    ,test "matchesTransaction" $ do
Hledger/Read.hs view
@@ -11,7 +11,6 @@ -}  --- ** language-{-# LANGUAGE CPP                 #-} {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE PackageImports      #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -54,9 +53,6 @@ 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
Hledger/Read/Common.hs view
@@ -95,9 +95,10 @@   rawnumberp,    -- ** comments+  isLineCommentStart,+  isSameLineCommentStart,   multilinecommentp,   emptyorcommentlinep,-   followingcommentp,   transactioncommentp,   postingcommentp,@@ -106,8 +107,11 @@   bracketeddatetagsp,    -- ** misc-  singlespacedtextp,-  singlespacedtextsatisfyingp,+  noncommenttextp,+  noncommenttext1p,+  singlespacedtext1p,+  singlespacednoncommenttext1p,+  singlespacedtextsatisfying1p,   singlespacep,   skipNonNewlineSpaces,   skipNonNewlineSpaces1,@@ -151,7 +155,6 @@  import Hledger.Data import Hledger.Utils-import Safe (headMay) import Text.Printf (printf)  --- ** doctest setup@@ -197,13 +200,12 @@     ,mrules_file_       :: Maybe FilePath       -- ^ a conversion rules file to use (when reading CSV)     ,aliases_           :: [String]             -- ^ account name aliases to apply     ,anon_              :: Bool                 -- ^ do light anonymisation/obfuscation of the data-    ,ignore_assertions_ :: Bool                 -- ^ don't check balance assertions     ,new_               :: Bool                 -- ^ read only new transactions since this file was last read     ,new_save_          :: Bool                 -- ^ save latest new transactions state for next time     ,pivot_             :: String               -- ^ use the given field's value as the account name     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed-    ,commoditystyles_   :: Maybe (M.Map CommoditySymbol AmountStyle) -- ^ optional commodity display styles affecting all files-    ,strict_            :: Bool                 -- ^ do extra error checking (eg, all posted accounts are declared)+    ,balancingopts_     :: BalancingOpts        -- ^ options for balancing transactions+    ,strict_            :: Bool                 -- ^ do extra error checking (eg, all posted accounts are declared, no prices are inferred)  } deriving (Show)  instance Default InputOpts where def = definputopts@@ -214,30 +216,31 @@     , mrules_file_       = Nothing     , aliases_           = []     , anon_              = False-    , ignore_assertions_ = False     , new_               = False     , new_save_          = True     , pivot_             = ""     , auto_              = False-    , commoditystyles_   = Nothing+    , balancingopts_     = def     , strict_            = False     }  rawOptsToInputOpts :: RawOpts -> InputOpts rawOptsToInputOpts rawopts = InputOpts{-   -- files_             = listofstringopt "file" rawopts-   mformat_           = Nothing-  ,mrules_file_       = maybestringopt "rules-file" rawopts-  ,aliases_           = listofstringopt "alias" rawopts-  ,anon_              = boolopt "anon" rawopts-  ,ignore_assertions_ = boolopt "ignore-assertions" rawopts-  ,new_               = boolopt "new" rawopts-  ,new_save_          = True-  ,pivot_             = stringopt "pivot" rawopts-  ,auto_              = boolopt "auto" rawopts-  ,commoditystyles_   = Nothing-  ,strict_            = boolopt "strict" rawopts-  }+     -- files_             = listofstringopt "file" rawopts+     mformat_           = Nothing+    ,mrules_file_       = maybestringopt "rules-file" rawopts+    ,aliases_           = listofstringopt "alias" rawopts+    ,anon_              = boolopt "anon" rawopts+    ,new_               = boolopt "new" rawopts+    ,new_save_          = True+    ,pivot_             = stringopt "pivot" rawopts+    ,auto_              = boolopt "auto" rawopts+    ,balancingopts_     = def{ ignore_assertions_ = boolopt "ignore-assertions" rawopts+                             , infer_prices_      = not noinferprice+                             }+    ,strict_            = boolopt "strict" rawopts+    }+  where noinferprice = boolopt "strict" rawopts || stringopt "args" rawopts == "balancednoautoconversion"  --- ** parsing utilities @@ -325,11 +328,11 @@ -- - infer transaction-implied market prices from transaction prices -- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal-journalFinalise InputOpts{auto_,ignore_assertions_,commoditystyles_,strict_} f txt pj = do+journalFinalise InputOpts{auto_,balancingopts_,strict_} f txt pj = do   t <- liftIO getClockTime   d <- liftIO getCurrentDay   let pj' =-        pj{jglobalcommoditystyles=fromMaybe M.empty commoditystyles_}  -- save any global commodity styles+        pj{jglobalcommoditystyles=fromMaybe M.empty $ commodity_styles_ balancingopts_}  -- save any global commodity styles         & journalAddFile (f, txt)  -- save the main file's info         & journalSetLastReadTime t -- save the last read time         & journalReverse -- convert all lists to the order they were parsed@@ -354,11 +357,11 @@                 then                   -- Auto postings are not active.                   -- Balance all transactions and maybe check balance assertions.-                  journalBalanceTransactions (not ignore_assertions_)+                  journalBalanceTransactions balancingopts_                 else \j -> do  -- Either monad                   -- Auto postings are active.                   -- Balance all transactions without checking balance assertions,-                  j' <- journalBalanceTransactions False j+                  j' <- journalBalanceTransactions balancingopts_{ignore_assertions_=True} j                   -- then add the auto postings                   -- (Note adding auto postings after balancing means #893b fails;                   -- adding them before balancing probably means #893a, #928, #938 fail.)@@ -368,7 +371,7 @@                       -- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?)                       j''' <- journalApplyCommodityStyles j''                       -- then check balance assertions.-                      journalBalanceTransactions (not ignore_assertions_) j'''+                      journalBalanceTransactions balancingopts_ j'''                 )             & fmap journalInferMarketPricesFromTransactions  -- infer market prices from commodity-exchanging transactions @@ -408,7 +411,7 @@ -- | Check that all the commodities used in this journal's postings have been declared -- by commodity directives, returning an error message otherwise. journalCheckCommoditiesDeclared :: Journal -> Either String ()-journalCheckCommoditiesDeclared j = +journalCheckCommoditiesDeclared j =   sequence_ $ map checkcommodities $ journalPostings j   where     checkcommodities Posting{..} =@@ -423,10 +426,8 @@                           (linesPrepend "  " . (<>"\n") . textChomp $ showTransaction t)       where         mfirstundeclaredcomm =-          headMay $ filter (not . (`elem` cs)) $ catMaybes $-          (acommodity . baamount <$> pbalanceassertion) :-          (map (Just . acommodity) . filter (/= missingamt) $ amounts pamount)-        cs = journalCommoditiesDeclared j+          find (`M.notMember` jcommodities j) . map acommodity $+          (maybe id ((:) . baamount) pbalanceassertion) . filter (/= missingamt) $ amountsRaw pamount   setYear :: Year -> JournalParser m ()@@ -535,9 +536,11 @@   char ')' <?> "closing bracket ')' for transaction code"   pure code +-- | Parse possibly empty text until a semicolon or newline.+-- Whitespace is preserved (for now - perhaps helps preserve alignment +-- of same-line comments ?). descriptionp :: TextParser m Text-descriptionp = takeWhileP Nothing (not . semicolonOrNewline)-  where semicolonOrNewline c = c == ';' || c == '\n'+descriptionp = noncommenttextp <?> "description"  --- *** dates @@ -697,19 +700,32 @@ -- It should have required parts to start with an alphanumeric; -- for now it remains as-is for backwards compatibility. accountnamep :: TextParser m AccountName-accountnamep = singlespacedtextp+accountnamep = singlespacedtext1p +-- | Parse possibly empty text, including whitespace, +-- until a comment start (semicolon) or newline.+noncommenttextp :: TextParser m T.Text+noncommenttextp = takeWhileP Nothing (\c -> not $ isSameLineCommentStart c || isNewline c) --- | Parse any text beginning with a non-whitespace character, until a--- double space or the end of input.--- TODO including characters which normally start a comment (;#) - exclude those ? -singlespacedtextp :: TextParser m T.Text-singlespacedtextp = singlespacedtextsatisfyingp (const True)+-- | Parse non-empty text, including whitespace, +-- until a comment start (semicolon) or newline.+noncommenttext1p :: TextParser m T.Text+noncommenttext1p = takeWhile1P Nothing (\c -> not $ isSameLineCommentStart c || isNewline c) --- | Similar to 'singlespacedtextp', except that the text must only contain--- characters satisfying the given predicate.-singlespacedtextsatisfyingp :: (Char -> Bool) -> TextParser m T.Text-singlespacedtextsatisfyingp pred = do+-- | Parse non-empty, single-spaced text starting and ending with non-whitespace,+-- until a double space or newline.+singlespacedtext1p :: TextParser m T.Text+singlespacedtext1p = singlespacedtextsatisfying1p (const True)++-- | Parse non-empty, single-spaced text starting and ending with non-whitespace,+-- until a comment start (semicolon), double space, or newline.+singlespacednoncommenttext1p :: TextParser m T.Text+singlespacednoncommenttext1p = singlespacedtextsatisfying1p (not . isSameLineCommentStart)++-- | Parse non-empty, single-spaced text starting and ending with non-whitespace,+-- where all characters satisfy the given predicate.+singlespacedtextsatisfying1p :: (Char -> Bool) -> TextParser m T.Text+singlespacedtextsatisfying1p pred = do   firstPart <- partp   otherParts <- many $ try $ singlespacep *> partp   pure $! T.unwords $ firstPart : otherParts@@ -729,7 +745,7 @@ spaceandamountormissingp =   option missingmixedamt $ try $ do     lift $ skipNonNewlineSpaces1-    Mixed . (:[]) <$> amountp+    mixedAmount <$> amountp  -- | Parse a single-commodity amount, with optional symbol on the left -- or right, followed by, in any order: an optional transaction price,@@ -855,7 +871,7 @@  -- | Parse a mixed amount from a string, or get an error. mamountp' :: String -> MixedAmount-mamountp' = Mixed . (:[]) . amountp'+mamountp' = mixedAmount . amountp'  -- | Parse a minus or plus sign followed by zero or more spaces, -- or nothing, returning a function that negates or does nothing.@@ -1182,13 +1198,27 @@   where     skiplinecommentp :: TextParser m ()     skiplinecommentp = do-      satisfy $ \c -> c == ';' || c == '#' || c == '*'-      void $ takeWhileP Nothing (\c -> c /= '\n')+      satisfy isLineCommentStart+      void $ takeWhileP Nothing (/= '\n')       optional newline       pure ()  {-# INLINABLE emptyorcommentlinep #-} +-- | Is this a character that, as the first non-whitespace on a line,+-- starts a comment line ?+isLineCommentStart :: Char -> Bool+isLineCommentStart '#' = True+isLineCommentStart '*' = True+isLineCommentStart ';' = True+isLineCommentStart _   = False++-- | Is this a character that, appearing anywhere within a line,+-- starts a comment ?+isSameLineCommentStart :: Char -> Bool+isSameLineCommentStart ';' = True+isSameLineCommentStart _   = False+ -- A parser combinator for parsing (possibly multiline) comments -- following journal items. --@@ -1560,7 +1590,7 @@      assertParseError p "1.5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555" ""    ,tests "spaceandamountormissingp" [-     test "space and amount" $ assertParseEq spaceandamountormissingp " $47.18" (Mixed [usd 47.18])+     test "space and amount" $ assertParseEq spaceandamountormissingp " $47.18" (mixedAmount $ usd 47.18)     ,test "empty string" $ assertParseEq spaceandamountormissingp "" missingmixedamt     -- ,test "just space" $ assertParseEq spaceandamountormissingp " " missingmixedamt  -- XXX should it ?     -- ,test "just amount" $ assertParseError spaceandamountormissingp "$47.18" ""  -- succeeds, consuming nothing
Hledger/Read/CsvReader.hs view
@@ -14,13 +14,11 @@ {-# LANGUAGE FlexibleContexts     #-} {-# LANGUAGE FlexibleInstances    #-} {-# LANGUAGE MultiWayIf           #-}-{-# LANGUAGE NamedFieldPuns       #-} {-# LANGUAGE OverloadedStrings    #-} {-# LANGUAGE PackageImports       #-} {-# LANGUAGE RecordWildCards      #-} {-# LANGUAGE ScopedTypeVariables  #-} {-# LANGUAGE TypeFamilies         #-}-{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE ViewPatterns         #-}  --- ** exports@@ -118,7 +116,7 @@               -- apply any command line account aliases. Can fail with a bad replacement pattern.               in case journalApplyAliases (aliasesFromOpts iopts) pj' of                   Left e -> throwError e-                  Right pj'' -> journalFinalise iopts{ignore_assertions_=True} f t pj''+                  Right pj'' -> journalFinalise iopts{balancingopts_=(balancingopts_ iopts){ignore_assertions_=True}} f t pj''  --- ** reading rules files --- *** rules utilities@@ -813,8 +811,8 @@  printCSV :: CSV -> TL.Text printCSV = TB.toLazyText . unlinesB . map printRecord-    where printRecord = mconcat . map TB.fromText . intersperse "," . map printField-          printField = wrap "\"" "\"" . T.replace "\"" "\\\"\\\""+    where printRecord = foldMap 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]@@ -976,9 +974,10 @@ -- If there's multiple non-zeros, or no non-zeros but multiple zeros, it throws an error. 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-  -- tests: hledger/test/csv.test ~ 13, 31-34+  -- Warning! Many tricky corner cases here.+  -- Keep synced with:+  -- hledger_csv.m4.md -> CSV FORMAT -> "amount", "Setting amounts",+  -- hledger/test/csv.test -> 13, 31-34   let     unnumberedfieldnames = ["amount","amount-in","amount-out"] @@ -992,10 +991,11 @@     assignments = [(f,a') | f <- fieldnames                           , Just v <- [T.strip . renderTemplate rules record <$> hledgerField rules record f]                           , not $ T.null v+                          -- XXX maybe ignore rule-generated values like "", "-", "$", "-$", "$-" ? cf CSV FORMAT -> "amount", "Setting amounts",                           , 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-                          , let a' = if f `elem` unnumberedfieldnames && n==2 then mixedAmountCost (-a) else a+                          , let a' = if f `elem` unnumberedfieldnames && n==2 then mixedAmountCost (maNegate a) else a                           ]      -- if any of the numbered field names are present, discard all the unnumbered ones@@ -1013,7 +1013,8 @@   in case -- dbg0 ("amounts for posting "++show n)           assignments'' of       [] -> Nothing-      [(f,a)] | "-out" `T.isSuffixOf` f -> Just (-a)  -- for -out fields, flip the sign+      [(f,a)] | "-out" `T.isSuffixOf` f -> Just (maNegate a)  -- for -out fields, flip the sign+                                                              -- XXX unless it's already negative ? back compat issues / too confusing ?       [(_,a)] -> Just a       fs      -> error' . T.unpack . T.unlines $ [  -- PARTIAL:          "multiple non-zero amounts or multiple zero amounts assigned,"@@ -1048,7 +1049,7 @@ -- The whole CSV record is provided for the error message. parseAmount :: CsvRules -> CsvRecord -> Text -> Text -> MixedAmount parseAmount rules record currency s =-    either mkerror (Mixed . (:[])) $  -- PARTIAL:+    either mkerror mixedAmount $  -- PARTIAL:     runParser (evalStateT (amountp <* eof) journalparsestate) "" $     currency <> simplifySign s   where@@ -1182,6 +1183,8 @@ -- "" simplifySign :: CsvAmountString -> CsvAmountString simplifySign amtstr+  | Just (' ',t) <- T.uncons amtstr = simplifySign t+  | Just (t,' ') <- T.unsnoc amtstr = simplifySign t   | 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
Hledger/Read/JournalReader.hs view
@@ -26,15 +26,12 @@  --- ** language -{-# LANGUAGE CPP #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE NoMonoLocalBinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE FlexibleContexts    #-}+{-# LANGUAGE NamedFieldPuns      #-}+{-# LANGUAGE NoMonoLocalBinds    #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE PackageImports      #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}  --- ** exports module Hledger.Read.JournalReader (@@ -85,9 +82,6 @@ import Data.Char (toLower) import Data.Either (isRight) import qualified Data.Map.Strict as M-#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup ((<>))-#endif import Data.Text (Text) import Data.String import Data.List@@ -535,7 +529,7 @@ payeedirectivep = do   string "payee" <?> "payee directive"   lift skipNonNewlineSpaces1-  payee <- lift descriptionp  -- all text until ; or \n+  payee <- lift $ T.strip <$> noncommenttext1p   (comment, tags) <- lift transactioncommentp   addPayeeDeclaration (payee, comment, tags)   return ()@@ -626,7 +620,7 @@                   Nothing -> today                   Just y  -> fromGregorian y 1 1   periodExcerpt <- lift $ excerpt_ $-                    singlespacedtextsatisfyingp (\c -> c /= ';' && c /= '\n')+                    singlespacedtextsatisfying1p (\c -> c /= ';' && c /= '\n')   let periodtxt = T.strip $ getExcerptText periodExcerpt    -- first parsing with 'singlespacedtextp', then "re-parsing" with@@ -711,7 +705,7 @@     return (status, account)   let (ptype, account') = (accountNamePostingType account, textUnbracket account)   lift skipNonNewlineSpaces-  amount <- option missingmixedamt $ Mixed . (:[]) <$> amountp+  amount <- optional amountp   lift skipNonNewlineSpaces   massertion <- optional balanceassertionp   lift skipNonNewlineSpaces@@ -721,7 +715,7 @@    , pdate2=mdate2    , pstatus=status    , paccount=account'-   , pamount=amount+   , pamount=maybe missingmixedamt mixedAmount amount    , pcomment=comment    , ptype=ptype    , ptags=tags@@ -823,7 +817,7 @@       "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n"       posting{         paccount="expenses:food:dining",-        pamount=Mixed [usd 10],+        pamount=mixedAmount (usd 10),         pcomment="a: a a\nb: b b\n",         ptags=[("a","a a"), ("b","b b")]         }@@ -832,7 +826,7 @@       " a  1. ; date:2012/11/28, date2=2012/11/29,b:b\n"       nullposting{          paccount="a"-        ,pamount=Mixed [num 1]+        ,pamount=mixedAmount (num 1)         ,pcomment="date:2012/11/28, date2=2012/11/29,b:b\n"         ,ptags=[("date", "2012/11/28"), ("date2=2012/11/29,b", "b")] -- TODO tag name parsed too greedily         ,pdate=Just $ fromGregorian 2012 11 28@@ -843,7 +837,7 @@       " a  1. ; [2012/11/28=2012/11/29]\n"       nullposting{          paccount="a"-        ,pamount=Mixed [num 1]+        ,pamount=mixedAmount (num 1)         ,pcomment="[2012/11/28=2012/11/29]\n"         ,ptags=[]         ,pdate= Just $ fromGregorian 2012 11 28@@ -872,7 +866,7 @@       "= (some value expr)\n some:postings  1.\n"       nulltransactionmodifier {         tmquerytxt = "(some value expr)"-       ,tmpostingrules = [nullposting{paccount="some:postings", pamount=Mixed[num 1]}]+       ,tmpostingrules = [nullposting{paccount="some:postings", pamount=mixedAmount (num 1)}]       }     ] @@ -905,7 +899,7 @@             pdate=Nothing,             pstatus=Cleared,             paccount="a",-            pamount=Mixed [usd 1],+            pamount=mixedAmount (usd 1),             pcomment="pcomment1\npcomment2\nptag1: val1\nptag2: val2\n",             ptype=RegularPosting,             ptags=[("ptag1","val1"),("ptag2","val2")],
Hledger/Read/TimedotReader.hs view
@@ -182,7 +182,7 @@         tstatus    = Cleared,         tpostings  = [           nullposting{paccount=a-                     ,pamount=Mixed [amountSetPrecision (Precision 2) $ num hours]  -- don't assume hours; do set precision to 2+                     ,pamount=mixedAmount . amountSetPrecision (Precision 2) $ num hours  -- don't assume hours; do set precision to 2                      ,ptype=VirtualPosting                      ,ptransaction=Just t                      }
Hledger/Reports.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE OverloadedStrings, RecordWildCards, FlexibleInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+ {-|  Generate several common kinds of report from a journal, as \"*Report\" -
Hledger/Reports/AccountTransactionsReport.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE OverloadedStrings, RecordWildCards, FlexibleInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-} {-|  An account-centric transactions report.@@ -15,12 +16,12 @@ ) where -import Data.List-import Data.Ord-import Data.Maybe+import Data.List (mapAccumL, nub, partition, sortBy)+import Data.Ord (comparing)+import Data.Maybe (catMaybes) import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar+import Data.Time.Calendar (Day)  import Hledger.Data import Hledger.Query@@ -82,45 +83,28 @@   where     -- a depth limit should not affect the account transactions report     -- seems unnecessary for some reason XXX-    reportq' = -- filterQuery (not . queryIsDepth)-               reportq--    -- get all transactions-    ts1 =-      -- ptraceAtWith 5 (("ts1:\n"++).pshowTransactions) $-      jtxns j--    -- apply any cur:SYM filters in reportq'-    symq  = filterQuery queryIsSym reportq'-    ts2 =-      ptraceAtWith 5 (("ts2:\n"++).pshowTransactions) $-      (if queryIsNull symq then id else map (filterTransactionAmounts symq)) ts1--    -- keep just the transactions affecting this account (via possibly realness or status-filtered postings)-    realq = filterQuery queryIsReal reportq'-    statusq = filterQuery queryIsStatus reportq'-    ts3 =-      traceAt 3 ("thisacctq: "++show thisacctq) $-      ptraceAtWith 5 (("ts3:\n"++).pshowTransactions) $-      filter (matchesTransaction thisacctq . filterTransactionPostings (And [realq, statusq])) ts2--    -- maybe convert these transactions to cost or value-    -- PARTIAL:-    prices = journalPriceOracle (infer_value_ ropts) j-    styles = journalCommodityStyles j-    periodlast =-      fromMaybe (error' "journalApplyValuation: expected a non-empty journal") $ -- XXX shouldn't happen-      reportPeriodOrJournalLastDay rspec j-    tval = transactionApplyCostValuation prices styles periodlast (rsToday rspec) (cost_ ropts) $ value_ ropts-    ts4 =-      ptraceAtWith 5 (("ts4:\n"++).pshowTransactions) $-      map tval ts3+    reportq'   = reportq -- filterQuery (not . queryIsDepth)+    symq       = filterQuery queryIsSym reportq'+    realq      = filterQuery queryIsReal reportq'+    statusq    = filterQuery queryIsStatus reportq'      -- 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-    ts5 =-      ptraceAtWith 5 (("ts5:\n"++).pshowTransactions) $-      sortBy (comparing (transactionRegisterDate reportq' thisacctq)) ts4+    transactions =+        ptraceAtWith 5 (("ts5:\n"++).pshowTransactions)+      . sortBy (comparing (transactionRegisterDate reportq' thisacctq))+      . jtxns+      . ptraceAtWith 5 (("ts4:\n"++).pshowTransactions.jtxns)+      -- keep just the transactions affecting this account (via possibly realness or status-filtered postings)+      . traceAt 3 ("thisacctq: "++show thisacctq)+      . ptraceAtWith 5 (("ts3:\n"++).pshowTransactions.jtxns)+      . filterJournalTransactions thisacctq+      . filterJournalPostings (And [realq, statusq])+      -- apply any cur:SYM filters in reportq'+      . ptraceAtWith 5 (("ts2:\n"++).pshowTransactions.jtxns)+      . (if queryIsNull symq then id else filterJournalAmounts symq)+      -- maybe convert these transactions to cost or value+      $ journalApplyValuationFromOpts rspec j      startbal       | balancetype_ ropts == HistoricalBalance = sumPostings priorps@@ -130,7 +114,7 @@                   filter (matchesPosting                           (dbg5 "priorq" $                            And [thisacctq, tostartdateq, datelessreportq]))-                         $ transactionsPostings ts5+                         $ transactionsPostings transactions         tostartdateq =           case mstartdate of             Just _  -> Date (DateSpan Nothing mstartdate)@@ -145,9 +129,9 @@     filtertxns = txn_dates_ ropts      items = reverse $-            accountTransactionsReportItems reportq' thisacctq startbal negate $+            accountTransactionsReportItems reportq' thisacctq startbal maNegate $             (if filtertxns then filter (reportq' `matchesTransaction`) else id) $-            ts5+            transactions  pshowTransactions :: [Transaction] -> String pshowTransactions = pshow . map (\t -> unwords [show $ tdate t, T.unpack $ tdescription t])@@ -179,8 +163,8 @@                   otheracctstr | thisacctq == None  = summarisePostingAccounts reportps     -- no current account ? summarise all matched postings                                | numotheraccts == 0 = summarisePostingAccounts thisacctps   -- only postings to current account ? summarise those                                | otherwise          = summarisePostingAccounts otheracctps  -- summarise matched postings to other account(s)-                  a = signfn $ negate $ sum $ map pamount thisacctps-                  b = bal + a+                  a = signfn . maNegate $ sumPostings thisacctps+                  b = bal `maPlus` a  -- | What is the transaction's date in the context of a particular account -- (specified with a query) and report query, as in an account register ?
Hledger/Reports/BalanceReport.hs view
@@ -4,7 +4,9 @@  -} -{-# LANGUAGE FlexibleInstances, RecordWildCards, ScopedTypeVariables, OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances   #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Hledger.Reports.BalanceReport (   BalanceReport,@@ -76,7 +78,7 @@ -- tests  Right samplejournal2 =-  journalBalanceTransactions False+  journalBalanceTransactions balancingOpts     nulljournal{       jtxns = [         txnTieKnot Transaction{@@ -90,7 +92,7 @@           tcomment="",           ttags=[],           tpostings=-            [posting {paccount="assets:bank:checking", pamount=Mixed [usd 1]}+            [posting {paccount="assets:bank:checking", pamount=mixedAmount (usd 1)}             ,posting {paccount="income:salary", pamount=missingmixedamt}             ],           tprecedingcomment=""@@ -112,7 +114,7 @@     tests "balanceReport" [       test "no args, null journal" $-     (defreportspec, nulljournal) `gives` ([], 0)+     (defreportspec, nulljournal) `gives` ([], nullmixedamt)      ,test "no args, sample journal" $      (defreportspec, samplejournal) `gives`@@ -125,7 +127,7 @@        ,("income:gifts","income:gifts",0, mamountp' "$-1.00")        ,("income:salary","income:salary",0, mamountp' "$-1.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with --tree" $      (defreportspec{rsOpts=defreportopts{accountlistmode_=ALTree}}, samplejournal) `gives`@@ -142,7 +144,7 @@        ,("income:gifts","gifts",1, mamountp' "$-1.00")        ,("income:salary","salary",1, mamountp' "$-1.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with --depth=N" $      (defreportspec{rsOpts=defreportopts{depth_=Just 1}}, samplejournal) `gives`@@ -150,7 +152,7 @@        ("expenses",    "expenses",    0, mamountp'  "$2.00")        ,("income",      "income",      0, mamountp' "$-2.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with depth:N" $      (defreportspec{rsQuery=Depth 1}, samplejournal) `gives`@@ -158,11 +160,11 @@        ("expenses",    "expenses",    0, mamountp'  "$2.00")        ,("income",      "income",      0, mamountp' "$-2.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with date:" $      (defreportspec{rsQuery=Date $ DateSpan (Just $ fromGregorian 2009 01 01) (Just $ fromGregorian 2010 01 01)}, samplejournal2) `gives`-      ([], 0)+      ([], nullmixedamt)      ,test "with date2:" $      (defreportspec{rsQuery=Date2 $ DateSpan (Just $ fromGregorian 2009 01 01) (Just $ fromGregorian 2010 01 01)}, samplejournal2) `gives`@@ -170,7 +172,7 @@         ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")        ,("income:salary","income:salary",0,mamountp' "$-1.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with desc:" $      (defreportspec{rsQuery=Desc $ toRegexCI' "income"}, samplejournal) `gives`@@ -178,7 +180,7 @@         ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")        ,("income:salary","income:salary",0, mamountp' "$-1.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with not:desc:" $      (defreportspec{rsQuery=Not . Desc $ toRegexCI' "income"}, samplejournal) `gives`@@ -189,7 +191,7 @@        ,("expenses:supplies","expenses:supplies",0, mamountp' "$1.00")        ,("income:gifts","income:gifts",0, mamountp' "$-1.00")        ],-       Mixed [usd 0])+       mixedAmount (usd 0))      ,test "with period on a populated period" $       (defreportspec{rsOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}}, samplejournal) `gives`@@ -198,11 +200,11 @@          ("assets:bank:checking","assets:bank:checking",0, mamountp' "$1.00")         ,("income:salary","income:salary",0, mamountp' "$-1.00")         ],-        Mixed [usd 0])+        mixedAmount (usd 0))       ,test "with period on an unpopulated period" $       (defreportspec{rsOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}}, samplejournal) `gives`-       ([], 0)+       ([], nullmixedamt)   
Hledger/Reports/BudgetReport.hs view
@@ -1,12 +1,7 @@-{- |--}--{-# LANGUAGE CPP                 #-} {-# LANGUAGE NamedFieldPuns      #-} {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE RecordWildCards     #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections       #-}  module Hledger.Reports.BudgetReport (   BudgetGoal,@@ -20,36 +15,30 @@   budgetReportAsText,   budgetReportAsCsv,   -- * Helpers-  reportPeriodName,   combineBudgetAndActual,   -- * Tests   tests_BudgetReport ) where +import Control.Applicative ((<|>)) import Data.Decimal (roundTo) import Data.Default (def) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM-import Data.List (nub, partition, transpose)+import Data.List (find, partition, transpose) import Data.List.Extra (nubSort) import Data.Maybe (fromMaybe)-#if !(MIN_VERSION_base(4,11,0))-import Data.Monoid ((<>))-#endif-import Safe (headDef)---import Data.List---import Data.Maybe-import qualified Data.Map as Map import Data.Map (Map)+import qualified Data.Map as Map+import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB --import System.Console.CmdArgs.Explicit as C --import Lucid as L-import Text.Tabular as T-import Text.Tabular.AsciiWide as T+import Text.Tabular.AsciiWide as Tab  import Hledger.Data import Hledger.Utils@@ -74,8 +63,8 @@ -- from all periodic transactions, calculate actual balance changes  -- from the regular transactions, and compare these to get a 'BudgetReport'. -- Unbudgeted accounts may be hidden or renamed (see journalWithBudgetAccountNames).-budgetReport :: ReportSpec -> Bool -> DateSpan -> Journal -> BudgetReport-budgetReport rspec assrt reportspan j = dbg4 "sortedbudgetreport" budgetreport+budgetReport :: ReportSpec -> BalancingOpts -> DateSpan -> Journal -> BudgetReport+budgetReport rspec bopts reportspan j = dbg4 "sortedbudgetreport" budgetreport   where     -- Budget report demands ALTree mode to ensure subaccounts and subaccount budgets are properly handled     -- and that reports with and without --empty make sense when compared side by side@@ -83,14 +72,14 @@     showunbudgeted = empty_ ropts     budgetedaccts =       dbg3 "budgetedacctsinperiod" $-      nub $-      concatMap expandAccountName $+      S.fromList $+      expandAccountNames $       accountNamesFromPostings $       concatMap tpostings $       concatMap (`runPeriodicTransaction` reportspan) $       jperiodictxns j     actualj = journalWithBudgetAccountNames budgetedaccts showunbudgeted j-    budgetj = journalAddBudgetGoalTransactions assrt ropts reportspan j+    budgetj = journalAddBudgetGoalTransactions bopts ropts reportspan j     actualreport@(PeriodicReport actualspans _ _) =         dbg5 "actualreport" $ multiBalanceReport rspec{rsOpts=ropts{empty_=True}} actualj     budgetgoalreport@(PeriodicReport _ budgetgoalitems budgetgoaltotals) =@@ -108,9 +97,9 @@ -- Budget goal transactions are similar to forecast transactions except -- their purpose and effect is to define balance change goals, per account and period, -- for BudgetReport.-journalAddBudgetGoalTransactions :: Bool -> ReportOpts -> DateSpan -> Journal -> Journal-journalAddBudgetGoalTransactions assrt _ropts reportspan j =-  either error' id $ journalBalanceTransactions assrt j{ jtxns = budgetts }  -- PARTIAL:+journalAddBudgetGoalTransactions :: BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal+journalAddBudgetGoalTransactions bopts _ropts reportspan j =+  either error' id $ journalBalanceTransactions bopts j{ jtxns = budgetts }  -- PARTIAL:   where     budgetspan = dbg3 "budget span" $ reportspan     budgetts =@@ -130,25 +119,20 @@ --    with a budget goal, so that only budgeted accounts are shown. --    This can be disabled by -E/--empty. ---journalWithBudgetAccountNames :: [AccountName] -> Bool -> Journal -> Journal-journalWithBudgetAccountNames budgetedaccts showunbudgeted j = -  dbg5With (("budget account names: "++).pshow.journalAccountNamesUsed) $ +journalWithBudgetAccountNames :: S.Set AccountName -> Bool -> Journal -> Journal+journalWithBudgetAccountNames budgetedaccts showunbudgeted j =+  dbg5With (("budget account names: "++).pshow.journalAccountNamesUsed) $   j { jtxns = remapTxn <$> jtxns j }   where-    remapTxn = mapPostings (map remapPosting)+    remapTxn = txnTieKnot . transactionTransformPostings remapPosting+    remapPosting p = p { paccount = remapAccount $ paccount p, poriginal = poriginal p <|> Just p }+    remapAccount a+      | a `S.member` budgetedaccts = a+      | Just p <- budgetedparent   = if showunbudgeted then a else p+      | otherwise                  = if showunbudgeted then u <> acctsep <> a else u       where-        mapPostings f t = txnTieKnot $ t { tpostings = f $ tpostings t }-        remapPosting p = p { paccount = remapAccount $ paccount p, poriginal = Just . fromMaybe p $ poriginal p }-          where-            remapAccount a-              | hasbudget         = a-              | hasbudgetedparent = if showunbudgeted then a else budgetedparent-              | otherwise         = if showunbudgeted then u <> acctsep <> a else u-              where-                hasbudget = a `elem` budgetedaccts-                hasbudgetedparent = not $ T.null budgetedparent-                budgetedparent = headDef "" $ filter (`elem` budgetedaccts) $ parentAccountNames a-                u = unbudgetedAccountName+        budgetedparent = find (`S.member` budgetedaccts) $ parentAccountNames a+        u = unbudgetedAccountName  -- | Combine a per-account-and-subperiod report of budget goals, and one -- of actual change amounts, into a budget performance report.@@ -206,7 +190,7 @@     sortedrows :: [BudgetReportRow] = sortRowsLike (mbrsorted unbudgetedrows ++ mbrsorted rows') rows       where         (unbudgetedrows, rows') = partition ((==unbudgetedAccountName) . prrFullName) rows-        mbrsorted = map prrFullName . sortRows ropts j . map (fmap $ fromMaybe 0 . fst)+        mbrsorted = map prrFullName . sortRows ropts j . map (fmap $ fromMaybe nullmixedamt . fst)         rows = rows1 ++ rows2      -- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells@@ -244,7 +228,7 @@      displayCell (actual, budget) = (showamt actual', budgetAndPerc <$> budget)       where-        actual' = fromMaybe 0 actual+        actual' = fromMaybe nullmixedamt actual         budgetAndPerc b = (showamt b, showper <$> percentage actual' b)         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)@@ -280,15 +264,15 @@     -- - the goal is zero     percentage :: Change -> BudgetGoal -> Maybe Percentage     percentage actual budget =-      case (maybecost $ normaliseMixedAmount actual, maybecost $ normaliseMixedAmount budget) of-        (Mixed [a], Mixed [b]) | (acommodity a == acommodity b || amountLooksZero a) && not (amountLooksZero b)+      case (costedAmounts actual, costedAmounts budget) of+        ([a], [b]) | (acommodity a == acommodity b || amountLooksZero a) && not (amountLooksZero b)             -> Just $ 100 * aquantity a / aquantity b         _   -> -- trace (pshow $ (maybecost actual, maybecost budget))  -- debug missing percentage                Nothing       where-        maybecost = case cost_ of-            Cost   -> mixedAmountCost-            NoCost -> id+        costedAmounts = case cost_ of+            Cost   -> amounts . mixedAmountCost+            NoCost -> amounts      maybetranspose | transpose_ = \(Table rh ch vals) -> Table ch rh (transpose vals)                    | otherwise  = id@@ -300,8 +284,8 @@   (PeriodicReport spans rows (PeriodicReportRow _ coltots grandtot grandavg)) =     addtotalrow $     Table-      (T.Group NoLine $ map Header accts)-      (T.Group NoLine $ map Header colheadings)+      (Tab.Group NoLine $ map Header accts)+      (Tab.Group NoLine $ map Header colheadings)       (map rowvals rows)   where     colheadings = map (reportPeriodName balancetype_ spans) spans@@ -323,27 +307,6 @@                        coltots ++ [grandtot | row_total_ ropts && not (null coltots)]                                ++ [grandavg | average_ ropts && not (null coltots)]                     ))---- | Make a name for the given period in a multiperiod report, given--- the type of balance being reported and the full set of report--- periods. This will be used as a column heading (or row heading, in--- a register summary report). We try to pick a useful name as follows:------ - ending-balance reports: the period's end date------ - balance change reports where the periods are months and all in the same year:---   the short month name in the current locale------ - all other balance change reports: a description of the datespan,---   abbreviated to compact form if possible (see showDateSpan).----reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> T.Text-reportPeriodName balancetype spans =-  case balancetype of-    PeriodChange -> if multiyear then showDateSpan else showDateSpanMonthAbbrev-      where-        multiyear = (>1) $ length $ nubSort $ map spanStartYear spans-    _ -> maybe "" (showDate . prevday) . spanEnd  -- XXX generalise this with multiBalanceReportAsCsv ? -- | Render a budget report as CSV. Like multiBalanceReportAsCsv,
Hledger/Reports/EntriesReport.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE OverloadedStrings, RecordWildCards, FlexibleInstances, ScopedTypeVariables #-}+{-# LANGUAGE FlexibleInstances   #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-} {-|  Journal entries report, used by the print command.@@ -15,12 +17,11 @@ where  import Data.List (sortBy)-import Data.Maybe (fromMaybe) import Data.Ord (comparing) import Data.Time (fromGregorian)  import Hledger.Data-import Hledger.Query+import Hledger.Query (Query(..)) import Hledger.Reports.ReportOptions import Hledger.Utils @@ -33,15 +34,9 @@  -- | Select transactions for an entries report. entriesReport :: ReportSpec -> Journal -> EntriesReport-entriesReport rspec@ReportSpec{rsOpts=ropts@ReportOpts{..}} j@Journal{..} =-  sortBy (comparing getdate) $ filter (rsQuery rspec `matchesTransaction`) $ map tvalue jtxns-  where-    getdate = transactionDateFn ropts-    -- 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 = postingApplyCostValuation (journalPriceOracle infer_value_ j) (journalCommodityStyles j) periodlast (rsToday rspec) cost_ value_-          where periodlast  = fromMaybe (rsToday rspec) $ reportPeriodOrJournalLastDay rspec j+entriesReport rspec@ReportSpec{rsOpts=ropts} =+    sortBy (comparing $ transactionDateFn ropts) . jtxns . filterJournalTransactions (rsQuery rspec)+    . journalApplyValuationFromOpts rspec{rsOpts=ropts{show_costs_=True}}  tests_EntriesReport = tests "EntriesReport" [   tests "entriesReport" [
Hledger/Reports/MultiBalanceReport.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP                 #-} {-# LANGUAGE FlexibleInstances   #-} {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE RecordWildCards     #-}@@ -42,13 +41,10 @@ import qualified Data.HashMap.Strict as HM import Data.Map (Map) import qualified Data.Map as M-import Data.Maybe (fromMaybe, mapMaybe)+import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Ord (Down(..))-#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup ((<>))-#endif import Data.Semigroup (sconcat)-import Data.Time.Calendar (Day, addDays, fromGregorian)+import Data.Time.Calendar (Day, fromGregorian) import Safe (lastDef, minimumMay)  import Hledger.Data@@ -115,7 +111,7 @@     rspec      = dbg3 "reportopts" $ makeReportQuery rspec' reportspan      -- Group postings into their columns.-    colps    = dbg5 "colps"  $ getPostingsByColumn rspec j reportspan+    colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle 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.@@ -143,7 +139,7 @@     rspec      = dbg3 "reportopts" $ makeReportQuery rspec' reportspan      -- Group postings into their columns.-    colps    = dbg5 "colps"  $ getPostingsByColumn rspec j reportspan+    colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle 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.@@ -174,7 +170,7 @@         (r:rs) -> sconcat $ fmap subreportTotal (r:|rs)       where         subreportTotal (_, sr, increasestotal) =-            (if increasestotal then id else fmap negate) $ prTotals sr+            (if increasestotal then id else fmap maNegate) $ prTotals sr      cbr = CompoundPeriodicReport "" (M.keys colps) subreports overalltotals @@ -191,7 +187,7 @@     fmap (M.findWithDefault nullacct precedingspan) acctmap   where     acctmap = calculateReportMatrix rspec' j priceoracle mempty-            . M.singleton precedingspan . map fst $ getPostings rspec' j+            . M.singleton precedingspan . map fst $ getPostings rspec' j priceoracle      rspec' = rspec{rsQuery=startbalq,rsOpts=ropts'}     -- If we're re-valuing every period, we need to have the unvalued start@@ -229,11 +225,11 @@     dateqcons        = if date2_ (rsOpts rspec) then Date2 else Date  -- | Group postings, grouped by their column-getPostingsByColumn :: ReportSpec -> Journal -> DateSpan -> Map DateSpan [Posting]-getPostingsByColumn rspec j reportspan = columns+getPostingsByColumn :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> Map DateSpan [Posting]+getPostingsByColumn rspec j priceoracle reportspan = columns   where     -- Postings matching the query within the report period.-    ps :: [(Posting, Day)] = dbg5 "getPostingsByColumn" $ getPostings rspec j+    ps :: [(Posting, Day)] = dbg5 "getPostingsByColumn" $ getPostings rspec j priceoracle      -- The date spans to be included as report columns.     colspans = dbg3 "colspans" $ splitSpan (interval_ $ rsOpts rspec) reportspan@@ -244,12 +240,13 @@     columns = foldr addPosting emptyMap ps  -- | Gather postings matching the query within the report period.-getPostings :: ReportSpec -> Journal -> [(Posting, Day)]-getPostings ReportSpec{rsQuery=query,rsOpts=ropts} =+getPostings :: ReportSpec -> Journal -> PriceOracle -> [(Posting, Day)]+getPostings rspec@ReportSpec{rsQuery=query,rsOpts=ropts} j priceoracle =     map (\p -> (p, date p)) .     journalPostings .-    filterJournalAmounts symq .    -- remove amount parts excluded by cur:-    filterJournalPostings reportq  -- remove postings not matched by (adjusted) query+    filterJournalAmounts symq .      -- remove amount parts excluded by cur:+    filterJournalPostings reportq $  -- remove postings not matched by (adjusted) query+    valuedJournal   where     symq = dbg3 "symq" . filterQuery queryIsSym $ dbg3 "requested q" query     -- The user's query with no depth limit, and expanded to the report span@@ -257,6 +254,8 @@     -- handles the hledger-ui+future txns case above).     reportq = dbg3 "reportq" $ depthless query     depthless = dbg3 "depthless" . filterQuery (not . queryIsDepth)+    valuedJournal | isJust (valuationAfterSum ropts) = j+                  | otherwise = journalApplyValuationFromOptsWith rspec j priceoracle      date = case whichDateFromOpts ropts of         PrimaryDate   -> postingDate@@ -295,7 +294,7 @@     -- starting-balance-based historical balances.     rowbals name changes = dbg5 "rowbals" $ case balancetype_ ropts of         PeriodChange      -> changeamts-        CumulativeChange  -> cumulativeSum avalue nullacct changeamts+        CumulativeChange  -> cumulative         HistoricalBalance -> historical       where         -- changes to report on: usually just the changes itself, but use the@@ -304,6 +303,7 @@             ChangeReport      -> M.mapWithKey avalue changes             BudgetReport      -> M.mapWithKey avalue changes             ValueChangeReport -> periodChanges valuedStart historical+        cumulative = cumulativeSum avalue nullacct changeamts         historical = cumulativeSum avalue startingBalance changes         startingBalance = HM.lookupDefault nullacct name startbals         valuedStart = avalue (DateSpan Nothing historicalDate) startingBalance@@ -312,10 +312,10 @@     -- 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+    colacctchanges = dbg5 "colacctchanges" $ fmap (acctChangesFromPostings rspec) colps -    (pvalue, avalue) = postingAndAccountValuations rspec j priceoracle+    avalue = acctApplyBoth . mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle+    acctApplyBoth f a = a{aibalance = f $ aibalance a, aebalance = f $ aebalance a}     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]@@ -338,7 +338,7 @@     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+    rows = dbg5 "rows" . (if invert_ ropts then map (fmap maNegate) else id)  -- Negate amounts if applicable              $ buildReportRows ropts displaynames matrix      -- Calculate column totals@@ -357,7 +357,7 @@                 -> HashMap AccountName DisplayName                 -> HashMap AccountName (Map DateSpan Account)                 -> [MultiBalanceReportRow]-buildReportRows ropts displaynames = +buildReportRows ropts displaynames =   toList . HM.mapMaybeWithKey mkRow  -- toList of HashMap's Foldable instance - does not sort consistently   where     mkRow name accts = do@@ -369,8 +369,8 @@         -- These are always simply the sum/average of the displayed row amounts.         -- Total for a cumulative/historical report is always the last column.         rowtot = case balancetype_ ropts of-            PeriodChange -> sum rowbals-            _            -> lastDef 0 rowbals+            PeriodChange -> maSum rowbals+            _            -> lastDef nullmixedamt rowbals         rowavg = averageMixedAmounts rowbals     balance = case accountlistmode_ ropts of ALTree -> aibalance; ALFlat -> aebalance @@ -407,8 +407,9 @@            || not (isZeroRow balance amts))            -- Throw out anything with zero balance       where         d = accountNameLevel name-        balance | ALTree <- accountlistmode_ ropts, d == depth = aibalance-                | otherwise = aebalance+        balance | ALTree <- accountlistmode_ ropts, d == depth = maybeStripPrices . aibalance+                | otherwise = maybeStripPrices . aebalance+          where maybeStripPrices = if show_costs_ ropts then id else mixedAmountStripPrices      -- Accounts interesting because they are a fork for interesting subaccounts     interestingParents = dbg5 "interestingParents" $ case accountlistmode_ ropts of@@ -439,7 +440,7 @@         -- Set the inclusive balance of an account from the rows, or sum the         -- subaccounts if it's not present         accounttreewithbals = mapAccounts setibalance accounttree-        setibalance a = a{aibalance = maybe (sum . map aibalance $ asubs a) prrTotal $+        setibalance a = a{aibalance = maybe (maSum . map aibalance $ asubs a) prrTotal $                                           HM.lookup (aname a) rowMap}         sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals         sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree@@ -449,7 +450,7 @@     sortFlatMBRByAmount = case fromMaybe NormallyPositive $ normalbalance_ ropts of         NormallyPositive -> sortOn (\r -> (Down $ amt r, prrFullName r))         NormallyNegative -> sortOn (\r -> (amt r, prrFullName r))-      where amt = normaliseMixedAmountSquashPricesForDisplay . prrTotal+      where amt = mixedAmountStripPrices . prrTotal      -- Sort the report rows by account declaration order then account name.     sortMBRByAccountDeclaration :: [MultiBalanceReportRow] -> [MultiBalanceReportRow]@@ -470,14 +471,14 @@      colamts = transpose . map prrAmounts $ filter isTopRow rows -    coltotals :: [MixedAmount] = dbg5 "coltotals" $ map sum colamts+    coltotals :: [MixedAmount] = dbg5 "coltotals" $ map maSum colamts      -- Calculate the grand total and average. These are always the sum/average     -- of the column totals.     -- Total for a cumulative/historical report is always the last column.     grandtotal = case balancetype_ ropts of-        PeriodChange -> sum coltotals-        _            -> lastDef 0 coltotals+        PeriodChange -> maSum coltotals+        _            -> lastDef nullmixedamt coltotals     grandaverage = averageMixedAmounts coltotals  -- | Map the report rows to percentages if needed@@ -515,8 +516,7 @@ -- those which fork between different branches subaccountTallies :: [AccountName] -> HashMap AccountName Int subaccountTallies = foldr incrementParent mempty . expandAccountNames-  where-    incrementParent a = HM.insertWith (+) (parentAccountName a) 1+  where incrementParent a = HM.insertWith (+) (parentAccountName a) 1  -- | A helper: what percentage is the second mixed amount of the first ? -- Keeps the sign of the first amount.@@ -535,12 +535,12 @@ -- 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}+    a{aibalance = i1 `maPlus` i2, aebalance = e1 `maPlus` 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}+    a{aibalance = i1 `maMinus` i2, aebalance = e1 `maMinus` e2}  -- | Extract period changes from a cumulative list periodChanges :: Account -> Map k Account -> Map k Account@@ -554,21 +554,6 @@ 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  tests_MultiBalanceReport = tests "MultiBalanceReport" [@@ -586,13 +571,13 @@   in    tests "multiBalanceReport" [       test "null journal"  $-      (defreportspec, nulljournal) `gives` ([], Mixed [nullamt])+      (defreportspec, nulljournal) `gives` ([], nullmixedamt)       ,test "with -H on a populated period"  $       (defreportspec{rsOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}}, samplejournal) `gives`        (-        [ PeriodicReportRow (flatDisplayName "assets:bank:checking") [mamountp' "$1.00"]  (mamountp' "$1.00")  (Mixed [amt0 {aquantity=1}])-        , PeriodicReportRow (flatDisplayName "income:salary")        [mamountp' "$-1.00"] (mamountp' "$-1.00") (Mixed [amt0 {aquantity=(-1)}])+        [ PeriodicReportRow (flatDisplayName "assets:bank:checking") [mamountp' "$1.00"]  (mamountp' "$1.00")  (mixedAmount amt0{aquantity=1})+        , PeriodicReportRow (flatDisplayName "income:salary")        [mamountp' "$-1.00"] (mamountp' "$-1.00") (mixedAmount amt0{aquantity=(-1)})         ],         mamountp' "$0.00") @@ -600,23 +585,23 @@      --  (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3), balancetype_=HistoricalBalance}, samplejournal) `gives`      --   (      --    [-     --     ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amt0 {aquantity=1}])-     --    ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amt0 {aquantity=(-1)}])+     --     ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=1})+     --    ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",mixedAmount amt0 {aquantity=(-1)})      --    ],-     --    Mixed [usd0])+     --    mixedAmount usd0)       -- ,test "a valid history on an empty period (more complex)"  $      --  (defreportopts{period_= PeriodBetween (fromGregorian 2009 1 1) (fromGregorian 2009 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`      --   (      --    [-     --    ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amt0 {aquantity=1}])-     --    ,("assets:bank:saving","saving",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amt0 {aquantity=1}])-     --    ,("assets:cash","cash",2, [mamountp' "$-2.00"], mamountp' "$-2.00",Mixed [amt0 {aquantity=(-2)}])-     --    ,("expenses:food","food",2, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amt0 {aquantity=(1)}])-     --    ,("expenses:supplies","supplies",2, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amt0 {aquantity=(1)}])-     --    ,("income:gifts","gifts",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amt0 {aquantity=(-1)}])-     --    ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amt0 {aquantity=(-1)}])+     --    ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=1})+     --    ,("assets:bank:saving","saving",3, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=1})+     --    ,("assets:cash","cash",2, [mamountp' "$-2.00"], mamountp' "$-2.00",mixedAmount amt0 {aquantity=(-2)})+     --    ,("expenses:food","food",2, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=(1)})+     --    ,("expenses:supplies","supplies",2, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=(1)})+     --    ,("income:gifts","gifts",2, [mamountp' "$-1.00"], mamountp' "$-1.00",mixedAmount amt0 {aquantity=(-1)})+     --    ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",mixedAmount amt0 {aquantity=(-1)})      --    ],-     --    Mixed [usd0])+     --    mixedAmount usd0)     ]  ]
Hledger/Reports/PostingsReport.hs view
@@ -4,11 +4,11 @@  -} -{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE FlexibleInstances   #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE RecordWildCards     #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TupleSections       #-}  module Hledger.Reports.PostingsReport (   PostingsReport,@@ -21,11 +21,11 @@ ) where -import Data.List+import Data.List (nub, sortOn) import Data.List.Extra (nubSort)-import Data.Maybe+import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text)-import Data.Time.Calendar+import Data.Time.Calendar (Day, addDays) import Safe (headMay, lastMay)  import Hledger.Data@@ -68,28 +68,18 @@       reportspan  = reportSpanBothDates j rspec       whichdate   = whichDateFromOpts ropts       mdepth      = queryDepth $ rsQuery rspec-      styles      = journalCommodityStyles j-      priceoracle = journalPriceOracle infer_value_ j       multiperiod = interval_ /= NoInterval        -- 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, 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]+        | multiperiod = [(p, Just periodend) | (p, periodend) <- summariseps reportps]+        | otherwise   = [(p, Nothing) | p <- reportps]         where           summariseps = summarisePostingsByInterval interval_ whichdate mdepth showempty reportspan-          valuedps = map (pvalue reportorjournallast) reportps           showempty = empty_ || average_-          reportorjournallast =-            fromMaybe (error' "postingsReport: expected a non-empty journal") $  -- PARTIAL: shouldn't happen-            reportPeriodOrJournalLastDay rspec j        -- Posting report items ready for display.       items =@@ -101,16 +91,11 @@           -- of --value on reports".           -- XXX balance report doesn't value starting balance.. should this ?           historical = balancetype_ == HistoricalBalance-          startbal | average_  = if historical then precedingavg else 0-                   | otherwise = if historical then precedingsum else 0+          startbal | average_  = if historical then precedingavg else nullmixedamt+                   | otherwise = if historical then precedingsum else nullmixedamt             where-              precedingsum = sumPostings $ map (pvalue daybeforereportstart) precedingps-              precedingavg | null precedingps = 0-                           | otherwise        = divideMixedAmount (fromIntegral $ length precedingps) precedingsum-              daybeforereportstart =-                maybe (error' "postingsReport: expected a non-empty journal")  -- PARTIAL: shouldn't happen-                (addDays (-1))-                $ reportPeriodOrJournalStart rspec j+              precedingsum = sumPostings precedingps+              precedingavg = divideMixedAmount (fromIntegral $ length precedingps) precedingsum            runningcalc = registerRunningCalculationFn ropts           startnum = if historical then length precedingps + 1 else 1@@ -121,18 +106,18 @@ -- and return the new average/total. registerRunningCalculationFn :: ReportOpts -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) registerRunningCalculationFn ropts-  | average_ ropts = \i avg amt -> avg + divideMixedAmount (fromIntegral i) (amt - avg)-  | otherwise      = \_ bal amt -> bal + amt+  | average_ ropts = \i avg amt -> avg `maPlus` divideMixedAmount (fromIntegral i) (amt `maMinus` avg)+  | otherwise      = \_ bal amt -> bal `maPlus` amt  -- | 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. -- A helper for the postings report. matchedPostingsBeforeAndDuring :: ReportSpec -> Journal -> DateSpan -> ([Posting],[Posting])-matchedPostingsBeforeAndDuring ReportSpec{rsOpts=ropts,rsQuery=q} j (DateSpan mstart mend) =+matchedPostingsBeforeAndDuring rspec@ReportSpec{rsOpts=ropts,rsQuery=q} j reportspan =   dbg5 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps   where-    beforestartq = dbg3 "beforestartq" $ dateqtype $ DateSpan Nothing mstart+    beforestartq = dbg3 "beforestartq" $ dateqtype $ DateSpan Nothing $ spanStart reportspan     beforeandduringps =       dbg5 "ps5" $ sortOn sortdate $                                             -- sort postings by date or date2       dbg5 "ps4" $ (if invert_ ropts then map negatePostingAmount else id) $     -- with --invert, invert amounts@@ -140,13 +125,13 @@       dbg5 "ps2" $ (if related_ ropts then concatMap relatedPostings else id) $  -- with -r, replace each with its sibling postings       dbg5 "ps1" $ filter (beforeandduringq `matchesPosting`) $                  -- filter postings by the query, with no start date or depth limit                   journalPostings $-                  journalSelectingAmountFromOpts ropts j    -- maybe convert to cost early, will be seen by amt:. XXX what about converting to value ?+                  journalApplyValuationFromOpts rspec j                          -- convert to cost and apply valuation       where         beforeandduringq = dbg4 "beforeandduringq" $ And [depthless $ dateless q, beforeendq]           where             depthless  = filterQuery (not . queryIsDepth)             dateless   = filterQuery (not . queryIsDateOrDate2)-            beforeendq = dateqtype $ DateSpan Nothing mend+            beforeendq = dateqtype $ DateSpan Nothing $ spanEnd reportspan         sortdate = if date2_ ropts then postingDate2 else postingDate         symq = dbg4 "symq" $ filterQuery queryIsSym q     dateqtype@@ -169,7 +154,7 @@     isdifferentdate = case wd of PrimaryDate   -> postingDate p  /= postingDate pprev                                  SecondaryDate -> postingDate2 p /= postingDate2 pprev     p' = p{paccount= clipOrEllipsifyAccountName d $ paccount p}-    b' = runningcalcfn itemnum b (pamount p)+    b' = runningcalcfn itemnum b $ pamount p  -- | Generate one postings report line item, containing the posting, -- the current running balance, and optionally the posting date and/or@@ -218,7 +203,7 @@     e' = fromMaybe (maybe (addDays 1 nulldate) postingdate $ lastMay ps) e     summaryp = nullposting{pdate=Just b'}     clippedanames = nub $ map (clipAccountName mdepth) anames-    summaryps | mdepth == Just 0 = [summaryp{paccount="...",pamount=sum $ map pamount ps}]+    summaryps | mdepth == Just 0 = [summaryp{paccount="...",pamount=sumPostings ps}]               | otherwise        = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]     summarypes = map (, e') $ (if showempty then id else filter (not . mixedAmountLooksZero . pamount)) summaryps     anames = nubSort $ map paccount ps@@ -227,10 +212,10 @@     balance a = maybe nullmixedamt bal $ lookupAccount a accts       where         bal = if isclipped a then aibalance else aebalance-        isclipped a = maybe True (accountNameLevel a >=) mdepth+        isclipped a = maybe False (accountNameLevel a >=) mdepth  negatePostingAmount :: Posting -> Posting-negatePostingAmount p = p { pamount = negate $ pamount p }+negatePostingAmount = postingTransformAmount negate   -- tests@@ -407,10 +392,10 @@     --           (summarisePostingsInDateSpan (DateSpan b e) depth showempty ps `is`)     --   let ps =     --           [-    --            nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]}-    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 2]}-    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food",          lpamount=Mixed [usd 4]}-    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 8]}+    --            nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=mixedAmount (usd 1)}+    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=mixedAmount (usd 2)}+    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food",          lpamount=mixedAmount (usd 4)}+    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=mixedAmount (usd 8)}     --           ]     --   ("2008/01/01","2009/01/01",0,9999,False,[]) `gives`     --    []@@ -420,21 +405,21 @@     --    ]     --   ("2008/01/01","2009/01/01",0,9999,False,ts) `gives`     --    [-    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food",          lpamount=Mixed [usd 4]}-    --    ,nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 10]}-    --    ,nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]}+    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food",          lpamount=mixedAmount (usd 4)}+    --    ,nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food:dining",   lpamount=mixedAmount (usd 10)}+    --    ,nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=mixedAmount (usd 1)}     --    ]     --   ("2008/01/01","2009/01/01",0,2,False,ts) `gives`     --    [-    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [usd 15]}+    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=mixedAmount (usd 15)}     --    ]     --   ("2008/01/01","2009/01/01",0,1,False,ts) `gives`     --    [-    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [usd 15]}+    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=mixedAmount (usd 15)}     --    ]     --   ("2008/01/01","2009/01/01",0,0,False,ts) `gives`     --    [-    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [usd 15]}+    --     nullposting{lpdate=fromGregorian 2008 01 01,lpdescription="- 2008/12/31",lpaccount="",lpamount=mixedAmount (usd 15)}     --    ]   ]
Hledger/Reports/ReportOptions.hs view
@@ -28,7 +28,10 @@   reportOptsToggleStatus,   simplifyStatuses,   whichDateFromOpts,-  journalSelectingAmountFromOpts,+  journalApplyValuationFromOpts,+  journalApplyValuationFromOptsWith,+  mixedAmountApplyValuationAfterSumFromOptsWith,+  valuationAfterSum,   intervalFromRawOpts,   forecastPeriodFromRawOpts,   queryFromFlags,@@ -42,20 +45,19 @@   reportPeriodOrJournalStart,   reportPeriodLastDay,   reportPeriodOrJournalLastDay,+  reportPeriodName ) where  import Control.Applicative ((<|>))+import Control.Monad ((<=<)) import Data.List.Extra (nubSort)-import Data.Maybe (fromMaybe, isJust, mapMaybe)+import Data.Maybe (fromMaybe, mapMaybe) import qualified Data.Text as T import Data.Time.Calendar (Day, addDays) import Data.Default (Default(..)) import Safe (headMay, lastDef, lastMay, maximumMay) -import System.Console.ANSI (hSupportsANSIColor)-import System.Environment (lookupEnv)-import System.IO (stdout) import Text.Megaparsec.Custom  import Hledger.Data@@ -118,6 +120,7 @@     ,drop_           :: Int     ,row_total_      :: Bool     ,no_total_       :: Bool+    ,show_costs_     :: Bool  -- ^ Whether to show costs for reports which normally don't show them     ,pretty_tables_  :: Bool     ,sort_amount_    :: Bool     ,percent_        :: Bool@@ -165,6 +168,7 @@     , drop_            = 0     , row_total_       = False     , no_total_        = False+    , show_costs_      = False     , pretty_tables_   = False     , sort_amount_     = False     , percent_         = False@@ -178,11 +182,8 @@ rawOptsToReportOpts :: RawOpts -> IO ReportOpts rawOptsToReportOpts rawopts = do     d <- getCurrentDay-    no_color <- isJust <$> lookupEnv "NO_COLOR"-    supports_color <- hSupportsANSIColor stdout -    let colorflag    = stringopt "color" rawopts-        formatstring = T.pack <$> maybestringopt "format" rawopts+    let formatstring = T.pack <$> maybestringopt "format" rawopts         querystring  = map T.pack $ listofstringopt "args" rawopts  -- doesn't handle an arg like "" right         (costing, valuation) = valuationTypeFromRawOpts rawopts @@ -214,14 +215,12 @@           ,drop_        = posintopt "drop" rawopts           ,row_total_   = boolopt "row-total" rawopts           ,no_total_    = boolopt "no-total" rawopts+          ,show_costs_  = boolopt "show-costs" rawopts           ,sort_amount_ = boolopt "sort-amount" rawopts           ,percent_     = boolopt "percent" rawopts           ,invert_      = boolopt "invert" rawopts           ,pretty_tables_ = boolopt "pretty-tables" rawopts-          ,color_       = and [not no_color-                              ,not $ colorflag `elem` ["never","no"]-                              ,colorflag `elem` ["always","yes"] || supports_color-                              ]+          ,color_       = useColorOnStdout -- a lower-level helper           ,forecast_    = forecastPeriodFromRawOpts d rawopts           ,transpose_   = boolopt "transpose" rawopts           }@@ -493,14 +492,62 @@ -- depthFromOpts :: ReportOpts -> Int -- 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/--cost).--- Maybe soon superseded by newer valuation code.-journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal-journalSelectingAmountFromOpts opts = case cost_ opts of-    Cost   -> journalToCost-    NoCost -> id+-- | Convert this journal's postings' amounts to cost and/or to value, if specified+-- by options (-B/--cost/-V/-X/--value etc.). Strip prices if not needed. This+-- should be the main stop for performing costing and valuation. The exception is+-- whenever you need to perform valuation _after_ summing up amounts, as in a+-- historical balance report with --value=end. valuationAfterSum will check for this+-- condition.+journalApplyValuationFromOpts :: ReportSpec -> Journal -> Journal+journalApplyValuationFromOpts rspec j =+    journalApplyValuationFromOptsWith rspec j priceoracle+  where priceoracle = journalPriceOracle (infer_value_ $ rsOpts rspec) j +-- | Like journalApplyValuationFromOpts, but takes PriceOracle as an argument.+journalApplyValuationFromOptsWith :: ReportSpec -> Journal -> PriceOracle -> Journal+journalApplyValuationFromOptsWith rspec@ReportSpec{rsOpts=ropts} j priceoracle =+    journalMapPostings valuation $ costing j+  where+    valuation p = maybe id (postingApplyValuation priceoracle styles (periodEnd p) (rsToday rspec)) (value_ ropts) p+    costing = case cost_ ropts of+        Cost   -> journalToCost+        NoCost -> id++    -- Find the end of the period containing this posting+    periodEnd  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDate+    mPeriodEnd = spanEnd <=< latestSpanContaining (historical : spans)+    historical = DateSpan Nothing $ spanStart =<< headMay spans+    spans = splitSpan (interval_ ropts) $ reportSpanBothDates j rspec+    styles = journalCommodityStyles j+    err = error "journalApplyValuationFromOpts: expected all spans to have an end date"++-- | Select the Account valuation functions required for performing valuation after summing+-- amounts. Used in MultiBalanceReport to value historical and similar reports.+mixedAmountApplyValuationAfterSumFromOptsWith :: ReportOpts -> Journal -> PriceOracle+                                              -> (DateSpan -> MixedAmount -> MixedAmount)+mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle =+    case valuationAfterSum ropts of+      Just mc -> \span -> valuation mc span . costing+      Nothing -> const id+  where+    valuation mc span = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)+      where err = error "mixedAmountApplyValuationAfterSumFromOptsWith: expected all spans to have an end date"+    costing = case cost_ ropts of+        Cost   -> styleMixedAmount styles . mixedAmountCost+        NoCost -> id+    styles = journalCommodityStyles j++-- | If the ReportOpts specify that we are performing valuation after summing amounts,+-- return Just the commodity symbol we're converting to, otherwise return Nothing.+-- Used for example with historical reports with --value=end.+valuationAfterSum :: ReportOpts -> Maybe (Maybe CommoditySymbol)+valuationAfterSum ropts = case value_ ropts of+    Just (AtEnd mc) | valueAfterSum -> Just mc+    _                               -> Nothing+  where valueAfterSum = reporttype_  ropts == ValueChangeReport+                     || balancetype_ ropts /= PeriodChange++ -- | Convert report options to a query, ignoring any non-flag command line arguments. queryFromFlags :: ReportOpts -> Query queryFromFlags ReportOpts{..} = simplifyQuery $ And flagsq@@ -519,8 +566,6 @@ -- 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 = reportSpanHelper False @@ -594,3 +639,23 @@         Just (AtEnd _) -> max (journalEndDate False j) lastPriceDirective         _              -> journalEndDate False j     lastPriceDirective = fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j++-- | Make a name for the given period in a multiperiod report, given+-- the type of balance being reported and the full set of report+-- periods. This will be used as a column heading (or row heading, in+-- a register summary report). We try to pick a useful name as follows:+--+-- - ending-balance reports: the period's end date+--+-- - balance change reports where the periods are months and all in the same year:+--   the short month name in the current locale+--+-- - all other balance change reports: a description of the datespan,+--   abbreviated to compact form if possible (see showDateSpan).+reportPeriodName :: BalanceType -> [DateSpan] -> DateSpan -> T.Text+reportPeriodName balancetype spans =+  case balancetype of+    PeriodChange -> if multiyear then showDateSpan else showDateSpanMonthAbbrev+      where+        multiyear = (>1) $ length $ nubSort $ map spanStartYear spans+    _ -> maybe "" (showDate . prevday) . spanEnd
Hledger/Reports/ReportTypes.hs view
@@ -1,7 +1,6 @@ {- | New common report types, used by the BudgetReport for now, perhaps all reports later. -}-{-# LANGUAGE CPP            #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveFunctor  #-} {-# LANGUAGE DeriveGeneric  #-}@@ -36,9 +35,6 @@ import Data.Decimal (Decimal) import Data.Maybe (mapMaybe) import Data.Text (Text)-#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup (Semigroup(..))-#endif import GHC.Generics (Generic)  import Hledger.Data@@ -98,11 +94,11 @@   , prrAverage :: b    -- The average of this row's values.   } deriving (Show, Functor, Generic, ToJSON) -instance Num b => Semigroup (PeriodicReportRow a b) where+instance Semigroup b => Semigroup (PeriodicReportRow a b) where   (PeriodicReportRow _ amts1 t1 a1) <> (PeriodicReportRow n2 amts2 t2 a2) =-      PeriodicReportRow n2 (sumPadded amts1 amts2) (t1 + t2) (a1 + a2)+      PeriodicReportRow n2 (sumPadded amts1 amts2) (t1 <> t2) (a1 <> a2)     where-      sumPadded (a:as) (b:bs) = (a + b) : sumPadded as bs+      sumPadded (a:as) (b:bs) = (a <> b) : sumPadded as bs       sumPadded as     []     = as       sumPadded []     bs     = bs 
Hledger/Reports/TransactionsReport.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE OverloadedStrings, RecordWildCards, FlexibleInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-} {-|  A transactions report. Like an EntriesReport, but with more@@ -21,10 +22,10 @@ ) where -import Data.List+import Data.List (sortBy) import Data.List.Extra (nubSort)+import Data.Ord (comparing) import Data.Text (Text)-import Data.Ord  import Hledger.Data import Hledger.Query@@ -60,13 +61,13 @@ -- "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 = items+transactionsReport :: ReportSpec -> Journal -> Query -> TransactionsReport+transactionsReport rspec j q = items    where      -- XXX items' first element should be the full transaction with all postings      items = reverse $ accountTransactionsReportItems q None nullmixedamt id ts-     ts    = sortBy (comparing date) $ filter (q `matchesTransaction`) $ jtxns $ journalSelectingAmountFromOpts opts j-     date  = transactionDateFn opts+     ts    = sortBy (comparing date) $ filter (q `matchesTransaction`) $ jtxns $ journalApplyValuationFromOpts rspec j+     date = transactionDateFn $ rsOpts rspec  -- | Split a transactions report whose items may involve several commodities, -- into one or more single-commodity transactions reports.@@ -99,7 +100,7 @@         startbal = filterMixedAmountByCommodity c $ triBalance i         go _ [] = []         go bal ((t,t2,s,o,amt,_):is) = (t,t2,s,o,amt,bal'):go bal' is-          where bal' = bal + amt+          where bal' = bal `maPlus` amt  -- tests 
Hledger/Utils.hs view
@@ -4,7 +4,9 @@ in the module hierarchy. This is the bottom of hledger's module graph.  -}-{-# LANGUAGE OverloadedStrings, LambdaCase #-}+{-# LANGUAGE CPP               #-}+{-# LANGUAGE LambdaCase        #-}+{-# LANGUAGE OverloadedStrings #-}  module Hledger.Utils (---- provide these frequently used modules - or not, for clearer api:                           -- module Control.Monad,@@ -35,25 +37,21 @@ where  import Control.Monad (liftM, when)--- import Data.Char import Data.FileEmbed (makeRelativeToProject, embedStringFile)-import Data.List--- import Data.Maybe--- import Data.PPrint+import Data.List (foldl', foldl1') -- import Data.String.Here (hereFile) import Data.Text (Text) import qualified Data.Text.IO as T-import Data.Time.Clock-import Data.Time.LocalTime--- import Data.Text (Text)--- import qualified Data.Text as T+import Data.Time.Clock (getCurrentTime)+import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone,+                            utcToLocalTime, utcToZonedTime) -- import Language.Haskell.TH.Quote (QuasiQuoter(..)) import Language.Haskell.TH.Syntax (Q, Exp) import System.Directory (getHomeDirectory)-import System.FilePath((</>), isRelative)+import System.FilePath (isRelative, (</>)) import System.IO--- import Text.Printf--- import qualified Data.Map as Map+  (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode,+   openFile, stdin, universalNewlineMode, utf8_bom)  import Hledger.Utils.Debug import Hledger.Utils.Parse@@ -160,7 +158,7 @@ expandPath _ "-" = return "-" expandPath curdir p = (if isRelative p then (curdir </>) else id) `liftM` expandHomePath p -- PARTIAL:-  + -- | Expand user home path indicated by tilde prefix expandHomePath :: FilePath -> IO FilePath expandHomePath = \case
Hledger/Utils/Color.hs view
@@ -1,8 +1,5 @@ -- | Basic color helpers for prettifying console output. -{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}- module Hledger.Utils.Color (   color,@@ -14,9 +11,6 @@ ) 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(..))
Hledger/Utils/Debug.hs view
@@ -87,14 +87,13 @@   ,dbg7IO   ,dbg8IO   ,dbg9IO-  -- ** Trace to a file-  ,plog-  ,plogAt   -- ** Trace the state of hledger parsers   ,traceParse   ,dbgparse   ,module Debug.Trace-)+  ,useColorOnStdout+  ,useColorOnStderr+  ) where  import           Control.Monad (when)@@ -105,20 +104,25 @@ import           Debug.Trace import           Hledger.Utils.Parse import           Safe (readDef)-import           System.Environment (getArgs)+import           System.Environment (getArgs, lookupEnv) import           System.Exit import           System.IO.Unsafe (unsafePerformIO) import           Text.Megaparsec import           Text.Printf import           Text.Pretty.Simple  -- (defaultOutputOptionsDarkBg, OutputOptions(..), pShowOpt, pPrintOpt)+import Data.Maybe (isJust)+import System.Console.ANSI (hSupportsANSIColor)+import System.IO (stdout, Handle, stderr)  prettyopts = -    defaultOutputOptionsDarkBg-    -- defaultOutputOptionsLightBg-    -- defaultOutputOptionsNoColor+  baseopts     { outputOptionsIndentAmount=2     , outputOptionsCompact=True     }+  where+    baseopts+      | useColorOnStderr = defaultOutputOptionsDarkBg -- defaultOutputOptionsLightBg+      | otherwise        = defaultOutputOptionsNoColor  -- | Pretty print. Generic alias for pretty-simple's pPrint. pprint :: Show a => a -> IO ()@@ -139,8 +143,8 @@ traceWith :: Show a => (a -> String) -> a -> a traceWith f a = trace (f a) a --- | Global debug level, which controls the verbosity of debug output--- on the console. The default is 0 meaning no debug output. The+-- | Global debug level, which controls the verbosity of debug errput+-- on the console. The default is 0 meaning no debug errput. The -- @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to -- a higher value (note: not @--debug N@ for some reason).  This uses -- unsafePerformIO and can be accessed from anywhere and before normal@@ -148,6 +152,7 @@ -- touch and reload this module to see the effect of a new --debug option. -- {-# OPTIONS_GHC -fno-cse #-} -- {-# NOINLINE debugLevel #-}+-- Avoid using dbg* in this function (infinite loop). debugLevel :: Int debugLevel = case snd $ break (=="--debug") args of                "--debug":[]  -> 1@@ -160,6 +165,108 @@     where       args = unsafePerformIO getArgs +-- Avoid using dbg*, pshow etc. in this function (infinite loop).+-- | Check the IO environment to see if ANSI colour codes should be used on stdout.+-- This is done using unsafePerformIO so it can be used anywhere, eg in +-- low-level debug utilities, which should be ok since we are just reading.+-- The logic is: use color if +-- a NO_COLOR environment variable is not defined+-- and the program was not started with --color=no|never+-- and (+--   the program was started with --color=yes|always+--   or stdout supports ANSI color and -o/--output-file was not used or is "-"+-- ).+-- Caveats:+-- When running code in GHCI, this module must be reloaded to see a change.+-- {-# OPTIONS_GHC -fno-cse #-}+-- {-# NOINLINE useColorOnStdout #-}+useColorOnStdout :: Bool+useColorOnStdout = not hasOutputFile && useColorOnHandle stdout++-- Avoid using dbg*, pshow etc. in this function (infinite loop).+-- | Like useColorOnStdout, but checks for ANSI color support on stderr,+-- and is not affected by -o/--output-file.+-- {-# OPTIONS_GHC -fno-cse #-}+-- {-# NOINLINE useColorOnStdout #-}+useColorOnStderr :: Bool+useColorOnStderr = useColorOnHandle stderr++-- Avoid using dbg*, pshow etc. in this function (infinite loop).+-- XXX sorry, I'm just cargo-culting these pragmas:+-- {-# OPTIONS_GHC -fno-cse #-}+-- {-# NOINLINE useColorOnHandle #-}+useColorOnHandle :: Handle -> Bool+useColorOnHandle h = unsafePerformIO $ do+  no_color       <- isJust <$> lookupEnv "NO_COLOR"+  supports_color <- hSupportsANSIColor h+  let coloroption = colorOption+  return $ and [+     not no_color+    ,not $ coloroption `elem` ["never","no"]+    ,coloroption `elem` ["always","yes"] || supports_color+    ]++-- Keep synced with color/colour flag definition in hledger:CliOptions.+-- Avoid using dbg*, pshow etc. in this function (infinite loop).+-- | Read the value of the --color or --colour command line option provided at program startup+-- using unsafePerformIO. If this option was not provided, returns the empty string.+-- (When running code in GHCI, this module must be reloaded to see a change.)+-- {-# OPTIONS_GHC -fno-cse #-}+-- {-# NOINLINE colorOption #-}+colorOption :: String+colorOption = +  -- similar to debugLevel+  let args = unsafePerformIO getArgs in+  case snd $ break (=="--color") args of+    -- --color ARG+    "--color":v:_ -> v+    _ ->+      case take 1 $ filter ("--color=" `isPrefixOf`) args of+        -- --color=ARG+        ['-':'-':'c':'o':'l':'o':'r':'=':v] -> v+        _ ->+          case snd $ break (=="--colour") args of+            -- --colour ARG+            "--colour":v:_ -> v+            _ ->+              case take 1 $ filter ("--colour=" `isPrefixOf`) args of+                -- --colour=ARG+                ['-':'-':'c':'o':'l':'o':'u':'r':'=':v] -> v+                _ -> ""++-- Avoid using dbg*, pshow etc. in this function (infinite loop).+-- | Check whether the -o/--output-file option has been used at program startup+-- with an argument other than "-", using unsafePerformIO.+-- {-# OPTIONS_GHC -fno-cse #-}+-- {-# NOINLINE hasOutputFile #-}+hasOutputFile :: Bool+hasOutputFile = not $ outputFileOption `elem` [Nothing, Just "-"]++-- Keep synced with output-file flag definition in hledger:CliOptions.+-- Avoid using dbg*, pshow etc. in this function (infinite loop).+-- | Read the value of the -o/--output-file command line option provided at program startup,+-- if any, using unsafePerformIO.+-- (When running code in GHCI, this module must be reloaded to see a change.)+-- {-# OPTIONS_GHC -fno-cse #-}+-- {-# NOINLINE outputFileOption #-}+outputFileOption :: Maybe String+outputFileOption = +  let args = unsafePerformIO getArgs in+  case snd $ break ("-o" `isPrefixOf`) args of+    -- -oARG+    ('-':'o':v@(_:_)):_ -> Just v+    -- -o ARG+    "-o":v:_ -> Just v+    _ ->+      case snd $ break (=="--output-file") args of+        -- --output-file ARG+        "--output-file":v:_ -> Just v+        _ ->+          case take 1 $ filter ("--output-file=" `isPrefixOf`) args of+            -- --output=file=ARG+            ['-':'-':'o':'u':'t':'p':'u':'t':'-':'f':'i':'l':'e':'=':v] -> Just v+            _ -> Nothing+ -- | Trace (print to stderr) a string if the global debug level is at -- or above the specified level. At level 0, always prints. Otherwise, -- uses unsafePerformIO.@@ -311,37 +418,6 @@  dbg9IO :: (MonadIO m, Show a) => String -> a -> m () dbg9IO = ptraceAtIO 9---- | Log a label and a pretty-printed showable value to ./debug.log, then return it.--- Can fail, see plogAt.-plog :: Show a => String -> a -> a-plog = plogAt 0---- | Log a label and a pretty-printed showable value to ./debug.log,--- if the global debug level is at or above the specified level.--- At level 0, always logs. Otherwise, uses unsafePerformIO.--- Tends to fail if called more than once too quickly, at least when built with -threaded--- ("Exception: debug.log: openFile: resource busy (file is locked)").-plogAt :: Show a => Int -> String -> a -> a-plogAt lvl-    | lvl > 0 && debugLevel < lvl = flip const-    | otherwise = \s a ->-        let p = pshow a-            ls = lines p-            nlorspace | length ls > 1 = "\n"-                      | otherwise     = " " ++ take (10 - length s) (repeat ' ')-            ls' | length ls > 1 = map (" "++) ls-                | otherwise     = ls-            output = s++":"++nlorspace++intercalate "\n" ls'++"\n"-        in unsafePerformIO $ appendFile "debug.log" output >> return a---- XXX redundant ? More/less robust than plogAt ?--- -- | Like dbg, but writes the output to "debug.log" in the current directory.--- dbglog :: Show a => String -> a -> a--- dbglog label a =---   (unsafePerformIO $---     appendFile "debug.log" $ label ++ ": " ++ ppShow a ++ "\n")---   `seq` a  -- | Print the provided label (if non-null) and current parser state -- (position and next input) to the console. See also megaparsec's dbg.
Hledger/Utils/Parse.hs view
@@ -19,6 +19,7 @@   parseerror,   showDateParseError,   nonspace,+  isNewline,   isNonNewlineSpace,   restofline,   eolof,@@ -119,13 +120,15 @@   :: (Show t, Show (Token t), Show e) => ParseErrorBundle t e -> String showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e) +isNewline :: Char -> Bool +isNewline '\n' = True+isNewline _    = False+ nonspace :: TextParser m Char nonspace = satisfy (not . isSpace)  isNonNewlineSpace :: Char -> Bool-isNonNewlineSpace c = c /= '\n' && isSpace c--- XXX support \r\n ?--- isNonNewlineSpace c = c /= '\n' && c /= '\r' && isSpace c+isNonNewlineSpace c = not (isNewline c) && isSpace c  spacenonewline :: (Stream s, Char ~ Token s) => ParsecT CustomErr s m Char spacenonewline = satisfy isNonNewlineSpace
Hledger/Utils/Regex.hs view
@@ -1,9 +1,7 @@-{-# LANGUAGE CPP                   #-} {-# LANGUAGE FlexibleInstances     #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings     #-} {-# LANGUAGE ScopedTypeVariables   #-}-{-# LANGUAGE TypeSynonymInstances  #-} {-|  Easy regular expression helpers, currently based on regex-tdfa. These should:@@ -69,9 +67,6 @@ 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 (
Hledger/Utils/String.hs view
@@ -7,7 +7,6 @@  uppercase,  underline,  stripbrackets,- unbracket,  -- quoting  quoteIfNeeded,  singleQuoteIfNeeded,@@ -26,43 +25,23 @@  elideLeft,  elideRight,  formatString,- -- * multi-line layout- concatTopPadded,- concatBottomPadded,- concatOneLine,- vConcatLeftAligned,- vConcatRightAligned,- padtop,- padbottom,- padleft,- padright,- cliptopleft,- fitto,  -- * wide-character-aware layout  charWidth,  strWidth,  strWidthAnsi,  takeWidth,- fitString,- fitStringMulti,- padLeftWide,- padRightWide  ) where   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)  import Hledger.Utils.Parse import Hledger.Utils.Regex (toRegex', regexReplace)-import Text.Tabular (Header(..), Properties(..))-import Text.Tabular.AsciiWide (Align(..), TableOpts(..), textCell, renderRow) import Text.WideString (charWidth, strWidth)  @@ -176,149 +155,7 @@ isDoubleQuoted s@(_:_:_) = head s == '"' && last s == '"' isDoubleQuoted _ = False -unbracket :: String -> String-unbracket s-    | (head s == '[' && last s == ']') || (head s == '(' && last s == ')') = init $ tail s-    | otherwise = s---- | 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 = 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 = 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.-concatOneLine :: [String] -> String-concatOneLine strs = concat $ map ((intercalate ", ").lines) strs---- | Join strings vertically, left-aligned and right-padded.-vConcatLeftAligned :: [String] -> String-vConcatLeftAligned ss = intercalate "\n" $ map showfixedwidth ss-    where-      showfixedwidth = printf (printf "%%-%ds" width)-      width = maximum $ map length ss---- | Join strings vertically, right-aligned and left-padded.-vConcatRightAligned :: [String] -> String-vConcatRightAligned ss = intercalate "\n" $ map showfixedwidth ss-    where-      showfixedwidth = printf (printf "%%%ds" width)-      width = maximum $ map length ss---- | Convert a multi-line string to a rectangular string top-padded to the specified height.-padtop :: Int -> String -> String-padtop h s = intercalate "\n" xpadded-    where-      ls = lines s-      sh = length ls-      sw | null ls = 0-         | otherwise = maximum $ map length ls-      ypadded = replicate (difforzero h sh) "" ++ ls-      xpadded = map (padleft sw) ypadded---- | Convert a multi-line string to a rectangular string bottom-padded to the specified height.-padbottom :: Int -> String -> String-padbottom h s = intercalate "\n" xpadded-    where-      ls = lines s-      sh = length ls-      sw | null ls = 0-         | otherwise = maximum $ map length ls-      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-padleft w "" = concat $ replicate w " "-padleft w s = intercalate "\n" $ map (printf (printf "%%%ds" w)) $ lines s---- | Convert a multi-line string to a rectangular string right-padded to the specified width.--- Treats wide characters as double width.-padright :: Int -> String -> String-padright w "" = concat $ replicate w " "-padright w s = intercalate "\n" $ map (printf (printf "%%-%ds" w)) $ lines s---- | Clip a multi-line string to the specified width and height from the top left.-cliptopleft :: Int -> Int -> String -> String-cliptopleft w h = intercalate "\n" . take h . map (take w) . lines---- | Clip and pad a multi-line string to fill the specified width and height.-fitto :: Int -> Int -> String -> String-fitto w h s = intercalate "\n" $ take h $ rows ++ repeat blankline-    where-      rows = map (fit w) $ lines s-      fit w = take w . (++ repeat ' ')-      blankline = replicate w ' '- -- Functions below treat wide (eg CJK) characters as double-width.---- | General-purpose wide-char-aware single-line string layout function.--- It can left- or right-pad a short string to a minimum width.--- It can left- or right-clip a long string to a maximum width, optionally inserting an ellipsis (the third argument).--- It clips and pads on the right when the fourth argument is true, otherwise on the left.--- It treats wide characters as double width.-fitString :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String-fitString mminwidth mmaxwidth ellipsify rightside s = (clip . pad) s-  where-    clip :: String -> String-    clip s =-      case mmaxwidth of-        Just w-          | strWidth s > w ->-            case rightside of-              True  -> takeWidth (w - length ellipsis) s ++ ellipsis-              False -> ellipsis ++ reverse (takeWidth (w - length ellipsis) $ reverse s)-          | otherwise -> s-          where-            ellipsis = if ellipsify then ".." else ""-        Nothing -> s-    pad :: String -> String-    pad s =-      case mminwidth of-        Just w-          | sw < w ->-            case rightside of-              True  -> s ++ replicate (w - sw) ' '-              False -> replicate (w - sw) ' ' ++ s-          | otherwise -> s-        Nothing -> s-      where sw = strWidth s---- | A version of fitString that works on multi-line strings,--- separate for now to avoid breakage.--- This will rewrite any line endings to unix newlines.-fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String-fitStringMulti mminwidth mmaxwidth ellipsify rightside s =-  (intercalate "\n" . map (fitString mminwidth mmaxwidth ellipsify rightside) . lines) s---- | Left-pad a string to the specified width.--- Treats wide characters as double width.--- Works on multi-line strings too (but will rewrite non-unix line endings).-padLeftWide :: Int -> String -> String-padLeftWide w "" = replicate w ' '-padLeftWide w s  = intercalate "\n" $ map (fitString (Just w) Nothing False False) $ lines s--- XXX not yet replaceable by--- padLeftWide w = fitStringMulti (Just w) Nothing False False---- | Right-pad a string to the specified width.--- Treats wide characters as double width.--- Works on multi-line strings too (but will rewrite non-unix line endings).-padRightWide :: Int -> String -> String-padRightWide w "" = replicate w ' '-padRightWide w s  = intercalate "\n" $ map (fitString (Just w) Nothing False True) $ lines s--- XXX not yet replaceable by--- padRightWide w = fitStringMulti (Just w) Nothing False True  -- | Double-width-character-aware string truncation. Take as many -- characters as possible from a string without exceeding the
Hledger/Utils/Test.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -26,10 +25,6 @@ import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.State.Strict (StateT, evalStateT, execStateT) import Data.Default (Default(..))-#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup ((<>))-#endif--- import Data.CallStack import Data.List (isInfixOf) import qualified Data.Text as T import Test.Tasty hiding (defaultMain)
Hledger/Utils/Text.hs view
@@ -2,82 +2,65 @@ -- There may be better alternatives out there.  {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-}  module Hledger.Utils.Text   (- -- -- * misc- -- lowercase,- -- uppercase,- -- underline,- -- stripbrackets,+  -- * misc+  -- lowercase,+  -- uppercase,+  -- underline,+  -- stripbrackets,   textUnbracket,   wrap,   textChomp,- -- -- quoting+  -- quoting   quoteIfSpaced,   textQuoteIfNeeded,- -- singleQuoteIfNeeded,- -- -- quotechars,- -- -- whitespacechars,+  -- singleQuoteIfNeeded,+  -- quotechars,+  -- whitespacechars,   escapeDoubleQuotes,- -- escapeSingleQuotes,- -- escapeQuotes,- -- words',- -- unwords',+  -- escapeSingleQuotes,+  -- escapeQuotes,+  -- words',+  -- unwords',   stripquotes,- -- isSingleQuoted,- -- isDoubleQuoted,- -- -- * single-line layout- -- elideLeft,+  -- isSingleQuoted,+  -- isDoubleQuoted,+  -- * single-line layout+  -- elideLeft,   textElideRight,   formatText,- -- -- * multi-line layout+  -- * multi-line layout   textConcatTopPadded,   textConcatBottomPadded,- -- concatOneLine,- -- vConcatLeftAligned,- -- vConcatRightAligned,- -- padtop,- -- padbottom,- -- padleft,- -- padright,- -- cliptopleft,- -- fitto,   fitText,   linesPrepend,   linesPrepend2,   unlinesB,- -- -- * wide-character-aware layout+  -- * wide-character-aware layout   WideBuilder(..),   wbToText,   wbUnpack,   textWidth,   textTakeWidth,- -- fitString,- -- fitStringMulti,-  textPadLeftWide,-  textPadRightWide,-  -- -- * Reading+  -- * Reading   readDecimal,-  -- -- * tests+  -- * tests   tests_Text   ) where  import Data.Char (digitToInt) import Data.Default (def)-#if !(MIN_VERSION_base(4,11,0))-import Data.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  import Hledger.Utils.Test ((@?=), test, tests)-import Text.Tabular (Header(..), Properties(..))-import Text.Tabular.AsciiWide (Align(..), TableOpts(..), textCell, renderRow)+import Text.Tabular.AsciiWide+  (Align(..), Header(..), Properties(..), TableOpts(..), renderRow, textCell) import Text.WideString (WideBuilder(..), wbToText, wbUnpack, charWidth, textWidth)  @@ -207,71 +190,6 @@ 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.--- concatOneLine :: [String] -> String--- concatOneLine strs = concat $ map ((intercalate ", ").lines) strs---- -- | Join strings vertically, left-aligned and right-padded.--- vConcatLeftAligned :: [String] -> String--- vConcatLeftAligned ss = intercalate "\n" $ map showfixedwidth ss---     where---       showfixedwidth = printf (printf "%%-%ds" width)---       width = maximum $ map length ss---- -- | Join strings vertically, right-aligned and left-padded.--- vConcatRightAligned :: [String] -> String--- vConcatRightAligned ss = intercalate "\n" $ map showfixedwidth ss---     where---       showfixedwidth = printf (printf "%%%ds" width)---       width = maximum $ map length ss---- -- | Convert a multi-line string to a rectangular string top-padded to the specified height.--- padtop :: Int -> String -> String--- padtop h s = intercalate "\n" xpadded---     where---       ls = lines s---       sh = length ls---       sw | null ls = 0---          | otherwise = maximum $ map length ls---       ypadded = replicate (difforzero h sh) "" ++ ls---       xpadded = map (padleft sw) ypadded---- -- | Convert a multi-line string to a rectangular string bottom-padded to the specified height.--- padbottom :: Int -> String -> String--- padbottom h s = intercalate "\n" xpadded---     where---       ls = lines s---       sh = length ls---       sw | null ls = 0---          | otherwise = maximum $ map length ls---       ypadded = ls ++ replicate (difforzero h sh) ""---       xpadded = map (padleft sw) ypadded---- -- | 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--- padleft w "" = concat $ replicate w " "--- padleft w s = intercalate "\n" $ map (printf (printf "%%%ds" w)) $ lines s---- -- | Convert a multi-line string to a rectangular string right-padded to the specified width.--- -- Treats wide characters as double width.--- padright :: Int -> String -> String--- padright w "" = concat $ replicate w " "--- padright w s = intercalate "\n" $ map (printf (printf "%%-%ds" w)) $ lines s---- -- | Clip a multi-line string to the specified width and height from the top left.--- cliptopleft :: Int -> Int -> String -> String--- cliptopleft w h = intercalate "\n" . take h . map (take w) . lines---- -- | Clip and pad a multi-line string to fill the specified width and height.--- fitto :: Int -> Int -> String -> String--- fitto w h s = intercalate "\n" $ take h $ rows ++ repeat blankline---     where---       rows = map (fit w) $ lines s---       fit w = take w . (++ repeat ' ')---       blankline = replicate w ' '- -- -- Functions below treat wide (eg CJK) characters as double-width.  -- | General-purpose wide-char-aware single-line text layout function.@@ -305,31 +223,6 @@           | otherwise -> s         Nothing -> s       where sw = textWidth s---- -- | A version of fitString that works on multi-line strings,--- -- separate for now to avoid breakage.--- -- This will rewrite any line endings to unix newlines.--- fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String--- fitStringMulti mminwidth mmaxwidth ellipsify rightside s =---   (intercalate "\n" . map (fitString mminwidth mmaxwidth ellipsify rightside) . lines) s---- | Left-pad a text to the specified width.--- Treats wide characters as double width.--- Works on multi-line texts too (but will rewrite non-unix line endings).-textPadLeftWide :: Int -> Text -> Text-textPadLeftWide w "" = T.replicate w " "-textPadLeftWide w s  = T.intercalate "\n" $ map (fitText (Just w) Nothing False False) $ T.lines s--- XXX not yet replaceable by--- padLeftWide w = fitStringMulti (Just w) Nothing False False---- | Right-pad a string to the specified width.--- Treats wide characters as double width.--- Works on multi-line strings too (but will rewrite non-unix line endings).-textPadRightWide :: Int -> Text -> Text-textPadRightWide w "" = T.replicate w " "-textPadRightWide w s  = T.intercalate "\n" $ map (fitText (Just w) Nothing False True) $ T.lines s--- XXX not yet replaceable by--- padRightWide w = fitStringMulti (Just w) Nothing False True  -- | Double-width-character-aware string truncation. Take as many -- characters as possible from a string without exceeding the
Text/Megaparsec/Custom.hs view
@@ -52,8 +52,8 @@  import Control.Monad.Except import Control.Monad.State.Strict (StateT, evalStateT)-import Data.Foldable (asum, toList) import qualified Data.List.NonEmpty as NE+import Data.Monoid (Alt(..)) import qualified Data.Set as S import Data.Text (Text) import Text.Megaparsec@@ -244,7 +244,7 @@       _ -> Nothing      finds :: (Foldable t) => (a -> Maybe b) -> t a -> Maybe b-    finds f = asum . map f . toList+    finds f = getAlt . foldMap (Alt . f)   --- * "Final" parse errors
Text/Tabular/AsciiWide.hs view
@@ -1,17 +1,28 @@ -- | 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+module Text.Tabular.AsciiWide+( module Text.Tabular +, TableOpts(..)+, render+, renderTable+, renderTableB+, renderRow+, renderRowB++, Cell(..)+, Align(..)+, emptyCell+, textCell+, cellWidth+) 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
Text/WideString.hs view
@@ -1,7 +1,5 @@ -- | Calculate the width of String and Text, being aware of wide characters. -{-# LANGUAGE CPP #-}- module Text.WideString (   -- * wide-character-aware layout   strWidth,@@ -13,9 +11,6 @@   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@@ -26,16 +21,13 @@ data WideBuilder = WideBuilder   { wbBuilder :: !TB.Builder   , wbWidth   :: !Int-  }+  } deriving (Show)  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
hledger-lib.cabal view
@@ -1,13 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack------ hash: 2724f18a071add9d644b3060aba58a3f4f6c71b66d88af1e8ca3f526043d461f  name:           hledger-lib-version:        1.21+version:        1.22 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@@ -27,8 +25,9 @@ maintainer:     Simon Michael <simon@joyful.com> license:        GPL-3 license-file:   LICENSE-tested-with:    GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.3, GHC==8.10.0.20200123 build-type:     Simple+tested-with:+    GHC==8.6.5, GHC==8.8.4, GHC==8.10.4 extra-source-files:     CHANGES.md     README.md@@ -94,7 +93,7 @@       Text.WideString       Paths_hledger_lib   hs-source-dirs:-      ./.+      ./   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans   build-depends:       Decimal >=0.5.1@@ -103,7 +102,7 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.9 && <4.15+    , base >=4.10.1.0 && <4.16     , base-compat-batteries >=0.10.1 && <0.12     , blaze-markup >=0.5.1     , bytestring@@ -111,7 +110,7 @@     , cassava     , cassava-megaparsec     , cmdargs >=0.10-    , containers+    , containers >=0.5.9     , data-default >=0.5     , directory     , extra >=1.6.3@@ -142,7 +141,7 @@   type: exitcode-stdio-1.0   main-is: doctests.hs   hs-source-dirs:-      ./.+      ./       test   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans   build-depends:@@ -152,7 +151,7 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.9 && <4.15+    , base >=4.10.1.0 && <4.16     , base-compat-batteries >=0.10.1 && <0.12     , blaze-markup >=0.5.1     , bytestring@@ -160,7 +159,7 @@     , cassava     , cassava-megaparsec     , cmdargs >=0.10-    , containers+    , containers >=0.5.9     , data-default >=0.5     , directory     , doctest >=0.16.3@@ -194,7 +193,7 @@   type: exitcode-stdio-1.0   main-is: unittest.hs   hs-source-dirs:-      ./.+      ./       test   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans   build-depends:@@ -204,7 +203,7 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.9 && <4.15+    , base >=4.10.1.0 && <4.16     , base-compat-batteries >=0.10.1 && <0.12     , blaze-markup >=0.5.1     , bytestring@@ -212,7 +211,7 @@     , cassava     , cassava-megaparsec     , cmdargs >=0.10-    , containers+    , containers >=0.5.9     , data-default >=0.5     , directory     , extra >=1.6.3