packages feed

hledger-lib 1.51.2 → 1.52

raw patch · 21 files changed

+613/−269 lines, 21 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Hledger.Read.Common: [posting_account_tags_] :: InputOpts -> Bool
- Hledger.Read.Common: posting_account_tags :: HasInputOpts c => Lens' c Bool
- Hledger.Read.InputOptions: [posting_account_tags_] :: InputOpts -> Bool
- Hledger.Read.InputOptions: posting_account_tags :: HasInputOpts c => Lens' c Bool
- Hledger.Utils.IO: setupPager :: IO ()
+ Hledger.Data: CostBasis :: !Maybe Amount -> !Maybe Day -> !Maybe Text -> CostBasis
+ Hledger.Data: Gain :: AccountType
+ Hledger.Data: SmartRelativeMonth :: Ordering -> Month -> SmartDate
+ Hledger.Data: SmartRelativeWeekDay :: Ordering -> WeekDay -> SmartDate
+ Hledger.Data: [acostbasis] :: Amount -> !Maybe CostBasis
+ Hledger.Data: [cbCost] :: CostBasis -> !Maybe Amount
+ Hledger.Data: [cbDate] :: CostBasis -> !Maybe Day
+ Hledger.Data: [cbLabel] :: CostBasis -> !Maybe Text
+ Hledger.Data: [ccomment] :: Commodity -> Text
+ Hledger.Data: [ctags] :: Commodity -> [Tag]
+ Hledger.Data: [jdeclaredcommoditytags] :: Journal -> Map CommoditySymbol [Tag]
+ Hledger.Data: data CostBasis
+ Hledger.Data.AccountName: gainAccountRegex :: Regexp
+ Hledger.Data.Amount: [displayCostBasis] :: AmountFormat -> Bool
+ Hledger.Data.Amount: showAmountCostBasis :: Amount -> String
+ Hledger.Data.Amount: showAmountCostBasisB :: AmountFormat -> Amount -> WideBuilder
+ Hledger.Data.Journal: journalCommodityTags :: Journal -> CommoditySymbol -> [Tag]
+ Hledger.Data.Journal: journalPostingsAddCommodityTags :: Journal -> Journal
+ Hledger.Data.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.CostBasis
+ Hledger.Data.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.CostBasis
+ Hledger.Data.Posting: postingCommodities :: Posting -> [CommoditySymbol]
+ Hledger.Data.Types: CostBasis :: !Maybe Amount -> !Maybe Day -> !Maybe Text -> CostBasis
+ Hledger.Data.Types: Gain :: AccountType
+ Hledger.Data.Types: SmartRelativeMonth :: Ordering -> Month -> SmartDate
+ Hledger.Data.Types: SmartRelativeWeekDay :: Ordering -> WeekDay -> SmartDate
+ Hledger.Data.Types: [acostbasis] :: Amount -> !Maybe CostBasis
+ Hledger.Data.Types: [cbCost] :: CostBasis -> !Maybe Amount
+ Hledger.Data.Types: [cbDate] :: CostBasis -> !Maybe Day
+ Hledger.Data.Types: [cbLabel] :: CostBasis -> !Maybe Text
+ Hledger.Data.Types: [ccomment] :: Commodity -> Text
+ Hledger.Data.Types: [ctags] :: Commodity -> [Tag]
+ Hledger.Data.Types: [jdeclaredcommoditytags] :: Journal -> Map CommoditySymbol [Tag]
+ Hledger.Data.Types: data CostBasis
+ Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.CostBasis
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.CostBasis
+ Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.CostBasis
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.CostBasis
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.CostBasis
+ Hledger.Query: QueryOptInterval :: Interval -> QueryOpt
+ Hledger.Read: readPossibleJournalFiles :: InputOpts -> [PrefixedFilePath] -> ExceptT String IO Journal
+ Hledger.Read.Common: [auto_posting_tags_] :: InputOpts -> Bool
+ Hledger.Read.Common: auto_posting_tags :: HasInputOpts c => Lens' c Bool
+ Hledger.Read.InputOptions: [auto_posting_tags_] :: InputOpts -> Bool
+ Hledger.Read.InputOptions: auto_posting_tags :: HasInputOpts c => Lens' c Bool
+ Hledger.Utils: divideSafe :: (Eq a, Fractional a) => a -> a -> a
+ Hledger.Utils.IO: lessIsWorking :: Maybe [(String, String)] -> IO Bool
+ Hledger.Utils.IO: lessVarValue :: Maybe String -> Maybe String -> Bool -> String
- Hledger.Data: Amount :: !CommoditySymbol -> !Quantity -> !AmountStyle -> !Maybe AmountCost -> Amount
+ Hledger.Data: Amount :: !CommoditySymbol -> !Quantity -> !AmountStyle -> !Maybe AmountCost -> !Maybe CostBasis -> Amount
- Hledger.Data: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Commodity
+ Hledger.Data: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Text -> [Tag] -> Commodity
- Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(FilePath, FilePath)] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
+ Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(FilePath, FilePath)] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol [Tag] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data.Amount: AmountFormat :: Bool -> Bool -> Maybe [CommoditySymbol] -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> Bool -> Bool -> Bool -> AmountFormat
+ Hledger.Data.Amount: AmountFormat :: Bool -> Bool -> Maybe [CommoditySymbol] -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> AmountFormat
- Hledger.Data.Types: Amount :: !CommoditySymbol -> !Quantity -> !AmountStyle -> !Maybe AmountCost -> Amount
+ Hledger.Data.Types: Amount :: !CommoditySymbol -> !Quantity -> !AmountStyle -> !Maybe AmountCost -> !Maybe CostBasis -> Amount
- Hledger.Data.Types: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Commodity
+ Hledger.Data.Types: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Text -> [Tag] -> Commodity
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(FilePath, FilePath)] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(FilePath, FilePath)] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol [Tag] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Read.Common: lotcostp :: forall (m :: Type -> Type). JournalParser m ()
+ Hledger.Read.Common: lotcostp :: forall (m :: Type -> Type). Quantity -> JournalParser m (Maybe Amount)

Files

CHANGES.md view
@@ -17,14 +17,28 @@ For user-visible changes, see the hledger package changelog.  +# 1.52 2026-03-20++Breaking changes++- A cost basis field (`acostbasis`) has been added to Amount (to store Ledger/Beancount-style cost basis annotations).++Fixes++- invertAmount: with zero amounts, do nothing instead of failing.+  Previously `invertAmount` would raise a "Ratio has zero denominator"+  exception if the amount's quantity was zero. Now it's a no-op in that case.+  [#2476]++[#2476]: https://github.com/simonmichael/hledger/issues/2476++ # 1.51.2 2026-01-08  - Allow base 4.22 / ghc 9.14. - # 1.51.1 2025-12-08 - # 1.51 2025-12-05  Breaking changes@@ -40,16 +54,8 @@   quoteForCommandLine now quotes some additional problem characters, and no longer quotes "7".   [#2468] -API -- Hledger.Read:-  defaultExistingJournalPath-  defaultExistingJournalPathSafely-  readPossibleJournalFile--- Hledger.Utils.IO:-  expandPathOrGlob-+# 1.50.5 2025-12-08  # 1.50.4 2025-12-04 
Hledger/Data/AccountName.hs view
@@ -30,6 +30,7 @@   ,equityAccountRegex   ,conversionAccountRegex   ,revenueAccountRegex+  ,gainAccountRegex   ,expenseAccountRegex   ,acctsep   ,acctsepchar@@ -99,6 +100,7 @@ equityAccountRegex     = toRegexCI' "^equity(:|$)" conversionAccountRegex = toRegexCI' "^equity:(trade|trades|trading|conversion)(:|$)" revenueAccountRegex    = toRegexCI' "^(income|revenue)s?(:|$)"+gainAccountRegex       = toRegexCI' "^(income|revenue)s?:(capital[- ]?)?(gains?|loss(es)?)(:|$)" expenseAccountRegex    = toRegexCI' "^expenses?(:|$)"  -- | Try to guess an account's type from its name,@@ -110,6 +112,7 @@   | regexMatchText liabilityAccountRegex  a = Just Liability   | regexMatchText conversionAccountRegex a = Just Conversion   | regexMatchText equityAccountRegex     a = Just Equity+  | regexMatchText gainAccountRegex       a = Just Gain   | regexMatchText revenueAccountRegex    a = Just Revenue   | regexMatchText expenseAccountRegex    a = Just Expense   | otherwise                               = Nothing@@ -447,6 +450,13 @@     accountNameInferType "revenues"          @?= Just Revenue     accountNameInferType "revenue"           @?= Just Revenue     accountNameInferType "income"            @?= Just Revenue+    accountNameInferType "income:gains"          @?= Just Gain+    accountNameInferType "revenue:gain"          @?= Just Gain+    accountNameInferType "revenues:capital-gains" @?= Just Gain+    accountNameInferType "income:capitalgain"    @?= Just Gain+    accountNameInferType "income:losses"         @?= Just Gain+    accountNameInferType "revenue:capital-loss"  @?= Just Gain+    accountNameInferType "income:gains:realized" @?= Just Gain   ,testCase "joinAccountNames" $ do     joinAccountNames "assets" "cash"     @?= "assets:cash"     joinAccountNames "assets:cash" "a"   @?= "assets:cash:a"
Hledger/Data/Amount.hs view
@@ -94,6 +94,8 @@   showAmountB,   showAmountCost,   showAmountCostB,+  showAmountCostBasis,+  showAmountCostBasisB,   cshowAmount,   showAmountWithZeroCommodity,   showAmountDebug,@@ -247,6 +249,7 @@   , displayMinWidth         :: Maybe Int  -- ^ Minimum width to pad to   , displayMaxWidth         :: Maybe Int  -- ^ Maximum width to clip to   , displayCost             :: Bool       -- ^ Whether to display Amounts' costs.+  , displayCostBasis        :: Bool       -- ^ Whether to display Amounts' cost basis (Ledger-style lot syntax).   , displayColour           :: Bool       -- ^ Whether to ansi-colourise negative Amounts.   , displayQuotes           :: Bool       -- ^ Whether to enclose complex symbols in quotes (normally true)   } deriving (Show)@@ -266,6 +269,7 @@   , displayMinWidth         = Just 0   , displayMaxWidth         = Nothing   , displayCost             = True+  , displayCostBasis        = True   , displayColour           = False   , displayQuotes           = True   }@@ -274,9 +278,9 @@ fullZeroFmt :: AmountFormat fullZeroFmt = defaultFmt{displayZeroCommodity=True} --- | Like defaultFmt but don't show costs.+-- | Like defaultFmt but don't show costs or cost basis. noCostFmt :: AmountFormat-noCostFmt = defaultFmt{displayCost=False}+noCostFmt = defaultFmt{displayCost=False, displayCostBasis=False}  -- | Like defaultFmt but display all amounts on one line. oneLineFmt :: AmountFormat@@ -305,7 +309,7 @@ -- | The empty simple amount - a zero with no commodity symbol or cost -- and the default amount display style. nullamt :: Amount-nullamt = Amount{acommodity="", aquantity=0, acost=Nothing, astyle=amountstyle}+nullamt = Amount{acommodity="", aquantity=0, astyle=amountstyle, acost=Nothing, acostbasis=Nothing}  -- | A special amount used as a marker, meaning -- "no explicit amount provided here, infer it when needed".@@ -381,8 +385,10 @@ multiplyAmount n = transformAmount (*n)  -- | Invert an amount (replace its quantity q with 1/q).--- (Its cost if any is not changed, currently.)+-- The amount's transacted price, if any, is not changed.+-- An amount with zero quantity is left unchanged. invertAmount :: Amount -> Amount+invertAmount a@Amount{aquantity=0} = a invertAmount a@Amount{aquantity=q} = a{aquantity=1/q}  -- | Is this amount negative ? The cost is ignored.@@ -716,11 +722,11 @@ showAmountB _ Amount{acommodity="AUTO"} = mempty showAmountB   afmt@AmountFormat{displayCommodity, displayZeroCommodity, displayDigitGroups-                   ,displayForceDecimalMark, displayCost, displayColour, displayQuotes}+                   ,displayForceDecimalMark, displayCost, displayCostBasis, displayColour, displayQuotes}   a@Amount{astyle=style} =     color $ case ascommodityside style of-      L -> (if displayCommodity then wbFromText comm <> space else mempty) <> quantity' <> cost-      R -> quantity' <> (if displayCommodity then space <> wbFromText comm else mempty) <> cost+      L -> (if displayCommodity then wbFromText comm <> space else mempty) <> quantity' <> cost <> costbasis+      R -> quantity' <> (if displayCommodity then space <> wbFromText comm else mempty) <> cost <> costbasis   where     color = if displayColour && isNegativeAmount a then colorB Dull Red else id     quantity = showAmountQuantity displayForceDecimalMark $@@ -730,6 +736,7 @@       | otherwise = (quantity, (if displayQuotes then quoteCommoditySymbolIfNeeded else id) $ acommodity a)     space = if not (T.null comm) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty     cost = if displayCost then showAmountCostB afmt a else mempty+    costbasis = if displayCostBasis then showAmountCostBasisB afmt a else mempty  -- Show an amount's cost as @ UNITCOST or @@ TOTALCOST, plus a leading space, or "" if there's no cost. showAmountCost :: Amount -> String@@ -747,6 +754,27 @@ showAmountCostDebug Nothing                = "" showAmountCostDebug (Just (UnitCost pa))  = "@ "  ++ showAmountDebug pa showAmountCostDebug (Just (TotalCost pa)) = "@@ " ++ showAmountDebug pa++-- | Show an amount's cost basis as Ledger-style lot syntax: {LOTCOST} [LOTDATE] (LOTNOTE).+showAmountCostBasis :: Amount -> String+showAmountCostBasis = wbUnpack . showAmountCostBasisB defaultFmt++-- showAmountCostBasis, efficient builder version.+showAmountCostBasisB :: AmountFormat -> Amount -> WideBuilder+showAmountCostBasisB afmt amt = case acostbasis amt of+  Nothing -> mempty+  Just CostBasis{cbCost, cbDate, cbLabel} ->+    lotcost <> lotdate <> lotnote+    where+      lotcost = case cbCost of+        Nothing -> mempty+        Just a  -> WideBuilder (TB.fromString " {") 2 <> showAmountB afmt a <> WideBuilder (TB.singleton '}') 1+      lotdate = case cbDate of+        Nothing -> mempty+        Just d  -> WideBuilder (TB.fromString " [") 2 <> wbFromText (T.pack $ show d) <> WideBuilder (TB.singleton ']') 1+      lotnote = case cbLabel of+        Nothing -> mempty+        Just l  -> WideBuilder (TB.fromString " (") 2 <> wbFromText l <> WideBuilder (TB.singleton ')') 1  -- | Colour version. For a negative amount, adds ANSI codes to change the colour, -- currently to hard-coded red.
Hledger/Data/Dates.hs view
@@ -436,6 +436,12 @@           span' (SmartRelative n Month)       = (Flex $ addGregorianMonthsClip n d, Flex $ addGregorianMonthsClip (n+1) d) where d = thismonth refdate           span' (SmartRelative n Quarter)     = (Flex $ addGregorianMonthsClip (3*n) d, Flex $ addGregorianMonthsClip (3*n+3) d) where d = thisquarter refdate           span' (SmartRelative n Year)        = (Flex $ addGregorianYearsClip n d, Flex $ addGregorianYearsClip (n+1) d) where d = thisyear refdate+          span' (SmartRelativeMonth   LT m)   = (Flex d, Flex $ nextmonth d) where d = prevnamedmonth m refdate+          span' (SmartRelativeMonth   EQ m)   = (Flex d, Flex $ nextmonth d) where d = thisnamedmonth m refdate+          span' (SmartRelativeMonth   GT m)   = (Flex d, Flex $ nextmonth d) where d = nextnamedmonth m refdate+          span' (SmartRelativeWeekDay LT wd)  = (Exact d, Exact $ nextday d) where d = prevnamedweekday wd refdate+          span' (SmartRelativeWeekDay EQ wd)  = (Exact d, Exact $ nextday d) where d = thisnamedweekday wd refdate+          span' (SmartRelativeWeekDay GT wd)  = (Exact d, Exact $ nextday d) where d = nextnamedweekday wd refdate  -- showDay :: Day -> String -- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day@@ -524,12 +530,25 @@ -- >>> t "next year" -- "2009-01-01" ----- t "last wed"+-- refdate is Wednesday, 2008-11-26+-- >>> t "last wednesday" -- "2008-11-19"--- t "next friday"--- "2008-11-28"--- t "next january"+-- >>> t "this wednesday"+-- "2008-12-03"+-- >>> t "next wednesday"+-- "2008-12-03"+-- >>> t "last january"+-- "2008-01-01"+-- >>> t "this january" -- "2009-01-01"+-- >>> t "next january"+-- "2009-01-01"+-- >>> t "last november"+-- "2007-11-01"+-- >>> t "this november"+-- "2009-11-01"+-- >>> t "next november"+-- "2009-11-01" -- -- >>> t "in 5 days" -- "2008-12-01"@@ -554,6 +573,12 @@     fix (SmartRelative n Month)   = Flex  $ addGregorianMonthsClip n $ thismonth refdate     fix (SmartRelative n Quarter) = Flex  $ addGregorianMonthsClip (3*n) $ thisquarter refdate     fix (SmartRelative n Year)    = Flex  $ addGregorianYearsClip n $ thisyear refdate+    fix (SmartRelativeMonth LT m)    = Flex $ prevnamedmonth m refdate+    fix (SmartRelativeMonth EQ m)    = Flex $ thisnamedmonth m refdate+    fix (SmartRelativeMonth GT m)    = Flex $ nextnamedmonth m refdate+    fix (SmartRelativeWeekDay LT wd) = Exact $ prevnamedweekday wd refdate+    fix (SmartRelativeWeekDay EQ wd) = Exact $ thisnamedweekday wd refdate+    fix (SmartRelativeWeekDay GT wd) = Exact $ nextnamedweekday wd refdate     (ry, rm, _) = toGregorian refdate  prevday :: Day -> Day@@ -574,6 +599,36 @@ startofmonth day = fromGregorian y m 1 where (y,m,_) = toGregorian day nthdayofmonth d day = fromGregorian y m d where (y,m,_) = toGregorian day +prevnamedmonth :: Month -> Day -> Day+prevnamedmonth targetmonth refdate = fromGregorian y' targetmonth 1+  where+    (y, m, _) = toGregorian refdate+    y' = if targetmonth < m then y else y - 1++nextnamedmonth :: Month -> Day -> Day+nextnamedmonth targetmonth refdate = fromGregorian y' targetmonth 1+  where+    (y, m, _) = toGregorian refdate+    y' = if targetmonth > m then y else y + 1++thisnamedmonth :: Month -> Day -> Day+thisnamedmonth = nextnamedmonth  -- "this" and "next" both mean next occurrence after current month++prevnamedweekday :: WeekDay -> Day -> Day+prevnamedweekday targetwd refdate = addDays (negate $ fromIntegral daysback) refdate+  where+    (_, curwd) = mondayStartWeek refdate+    daysback = 1 + (curwd - targetwd - 1) `mod` 7++nextnamedweekday :: WeekDay -> Day -> Day+nextnamedweekday targetwd refdate = addDays (fromIntegral daysforward) refdate+  where+    (_, curwd) = mondayStartWeek refdate+    daysforward = 1 + (targetwd - curwd - 1) `mod` 7++thisnamedweekday :: WeekDay -> Day -> Day+thisnamedweekday = nextnamedweekday  -- "this" and "next" both mean next occurrence after today+ thisquarter = startofquarter startofquarter day = fromGregorian y (firstmonthofquarter m) 1     where@@ -780,6 +835,8 @@ > october, oct                                (start of month in current year) > yesterday, today, tomorrow                  (-1, 0, 1 days from today) > last/this/next day/week/month/quarter/year  (-1, 0, 1 periods from the current period)+> last/this/next monday/mon                   (the previous or next named weekday; this=next)+> last/next january/jan                       (previous or next start of named month; this disallowed to avoid confusion) > in n days/weeks/months/quarters/years       (n periods from the current period) > n days/weeks/months/quarters/years ago      (-n periods from the current period) > 20181201                                    (8 digit YYYYMMDD with valid year month and day)@@ -819,27 +876,39 @@ smartdate :: TextParser m SmartDate smartdate = choice'   -- XXX maybe obscures date errors ? see ledgerdate-    [ relativeP+    [ relativeinterval+    , relativemonth+    , relativeweekday     , yyyymmdd     , ymd     , (\(m,d) -> SmartFromReference (Just m) d) <$> md     , failIfInvalidDate . SmartFromReference Nothing =<< decimal     , SmartMonth <$> (month <|> mon)-    , SmartRelative 0    Day <$ string' "today"     , SmartRelative (-1) Day <$ string' "yesterday"+    , SmartRelative 0    Day <$ string' "today"     , SmartRelative 1    Day <$ string' "tomorrow"     ]   where-    relativeP = do+    relativeinterval = do         optional $ string' "in" <* skipNonNewlineSpaces-        num      <- seqP <* skipNonNewlineSpaces-        interval <- intervalP <* skipNonNewlineSpaces+        num      <- relativenump <* skipNonNewlineSpaces+        interval <- intervalp <* skipNonNewlineSpaces         sign     <- choice [negate <$ string' "ago", id <$ string' "ahead", pure id]         return $ SmartRelative (sign num) interval--    seqP = choice [ 0 <$ string' "this", -1 <$ string' "last", 1 <$ string' "next", signed skipNonNewlineSpaces decimal ]-    intervalP = choice [ Day <$ string' "day", Week <$ string' "week", Month <$ string' "month"-                       , Quarter <$ string' "quarter", Year <$ string' "year" ] <* optional (char' 's')+        where+          relativenump = choice [ 0 <$ string' "this", -1 <$ string' "last", 1 <$ string' "next", signed skipNonNewlineSpaces decimal ]+          intervalp    = choice [ Day <$ string' "day", Week <$ string' "week", Month <$ string' "month"+                                , Quarter <$ string' "quarter", Year <$ string' "year" ] <* optional (char' 's')+    relativemonth = do+      dir <- choice [LT <$ string' "last", EQ <$ string' "this", GT <$ string' "next"]+      skipNonNewlineSpaces+      m <- (month <|> mon)+      return $ SmartRelativeMonth dir m+    relativeweekday = do+      dir <- choice [LT <$ string' "last", EQ <$ string' "this", GT <$ string' "next"]+      skipNonNewlineSpaces+      w <- weekday+      return $ SmartRelativeWeekDay dir w  -- | Like smartdate, but there must be nothing other than whitespace after the date. smartdateonly :: TextParser m SmartDate
Hledger/Data/Journal.hs view
@@ -35,7 +35,10 @@   journalSetLastReadTime,   journalRenumberAccountDeclarations,   journalPivot,-  -- * Filtering+  journalPostingsAddAccountTags,+  journalPostingsKeepAccountTagsOnly,+  journalPostingsAddCommodityTags,+-- * Filtering   filterJournalTransactions,   filterJournalPostings,   filterJournalRelatedPostings,@@ -59,6 +62,7 @@   journalLeafAccountNames,   journalAccountNameTree,   journalAccountTags,+  journalCommodityTags,   journalInheritedAccountTags,   -- journalAmountAndPriceCommodities,   -- journalAmountStyles,@@ -97,10 +101,8 @@   journalAccountType,   journalAccountTypes,   journalAddAccountTypes,-  journalPostingsAddAccountTags,-  journalPostingsKeepAccountTagsOnly,+  -- * Conversion accounts   defaultBaseConversionAccount,-  -- journalPrices,   journalBaseConversionAccount,   journalConversionAccounts,   -- * Misc@@ -284,6 +286,9 @@     -- ,jdeclaredcommodities           :: M.Map CommoditySymbol Commodity     ,jdeclaredcommodities               = (<>) (jdeclaredcommodities j1) (jdeclaredcommodities j2)     --+    -- ,jdeclaredcommoditytags     :: M.Map CommoditySymbol [Tag]+    ,jdeclaredcommoditytags     = M.unionWith (<>) (jdeclaredcommoditytags j1) (jdeclaredcommoditytags j2)+    --     -- ,jinferredcommoditystyles   :: M.Map CommoditySymbol AmountStyle     ,jinferredcommoditystyles       = (<>) (jinferredcommoditystyles j1) (jinferredcommoditystyles j2)     --@@ -345,8 +350,9 @@   ,jdeclaredaccounttypes      = M.empty   ,jaccounttypes              = M.empty   ,jglobalcommoditystyles     = M.empty-  ,jdeclaredcommodities               = M.empty-  ,jinferredcommoditystyles       = M.empty+  ,jdeclaredcommodities       = M.empty+  ,jdeclaredcommoditytags     = M.empty+  ,jinferredcommoditystyles   = M.empty   ,jpricedirectives           = []   ,jinferredmarketprices      = []   ,jtxnmodifiers              = []@@ -638,10 +644,22 @@  -- | To all postings in the journal, add any tags from their account -- (including those inherited from parent accounts).--- If the same tag exists on posting and account, the latter is ignored.+-- If a tag already exists on the posting, it is not changed (the account tag will be ignored). journalPostingsAddAccountTags :: Journal -> Journal journalPostingsAddAccountTags j = journalMapPostings addtags j   where addtags p = p `postingAddTags` (journalInheritedAccountTags j $ paccount p)++-- | Get any tags declared for this commodity.+journalCommodityTags :: Journal -> CommoditySymbol -> [Tag]+journalCommodityTags Journal{jdeclaredcommoditytags} c =+  M.findWithDefault [] c jdeclaredcommoditytags++-- | To all postings in the journal, add any tags from their amount's commodities.+-- If a tag already exists on the posting, it is not changed (the commodity tag will be ignored).+journalPostingsAddCommodityTags :: Journal -> Journal+journalPostingsAddCommodityTags j = journalMapPostings addtags j+  where+    addtags p = p `postingAddTags` concatMap (journalCommodityTags j) (postingCommodities p)  -- | Remove all tags from the journal's postings except those provided by their account. -- This is useful for the accounts report.
Hledger/Data/Json.hs view
@@ -83,6 +83,7 @@     , "floatingPoint"   .= (realToFrac d' :: Double)     ] +instance ToJSON CostBasis instance ToJSON Amount instance ToJSON Rounding instance ToJSON AmountStyle@@ -200,6 +201,7 @@ instance FromJSON Pos where   parseJSON = fmap mkPos . parseJSON +instance FromJSON CostBasis instance FromJSON Amount instance FromJSON Rounding instance FromJSON AmountStyle
Hledger/Data/Posting.hs view
@@ -78,6 +78,7 @@   postingToCost,   postingAddInferredEquityPostings,   postingPriceDirectivesFromCost,+  postingCommodities,   tests_Posting ) where@@ -429,6 +430,11 @@     Unmarked -> maybe Unmarked tstatus mt     _ -> s +-- | Get the commodity symbols used in a posting's amounts (not including costs).+postingCommodities :: Posting -> [CommoditySymbol]+postingCommodities = map acommodity . filter (not . isMissingAmount) . amountsRaw . pamount+  where isMissingAmount a = acommodity a == "AUTO"+ -- | Tags for this posting including any inherited from its parent transaction. postingAllTags :: Posting -> [Tag] postingAllTags p = ptags p ++ maybe [] ttags (ptransaction p)@@ -466,7 +472,7 @@ -- If the posting already has these tags (with any value), do nothing. postingAddHiddenAndMaybeVisibleTag :: Bool -> HiddenTag -> Posting -> Posting postingAddHiddenAndMaybeVisibleTag verbosetags ht p@Posting{pcomment=c, ptags} =-  (p `postingAddTags` ([ht] <> [vt|verbosetags]))+  (p `postingAddTags` ([ht] <> [vt |verbosetags]))   {pcomment=if verbosetags && not hadtag then c `commentAddTag` vt else c}   where     vt@(vname,_) = toVisibleTag ht@@ -516,7 +522,7 @@         cost = amountCost amt         amtCommodity  = commodity amt         costCommodity = commodity cost-        convp = p{pbalanceassertion=Nothing, poriginal=Nothing}+        convp = nullposting{pdate=pdate p, pdate2=pdate2 p, pstatus=pstatus p, ptransaction=ptransaction p}           & postingAddHiddenAndMaybeVisibleTag verbosetags (conversionPostingTagName,"")           & postingAddHiddenAndMaybeVisibleTag verbosetags (generatedPostingTagName, "")         accountPrefix = mconcat [ equityAcct, ":", T.intercalate "-" $ sort [amtCommodity, costCommodity], ":"]
Hledger/Data/Types.hs view
@@ -84,15 +84,23 @@ -- wildcard (so it would mean all days of that month). See the `smartdate` -- parser for more examples. ----- Or, one of the standard periods and an offset relative to the reference date:+-- Or, one of the standard periods and a numeric offset relative to the reference date: -- (last|this|next) (day|week|month|quarter|year), where "this" means the period -- containing the reference date.+--+-- Or, (last|this|next) weekdayname, where "this" "next".+--+-- Or, (last|this|next) monthname, where "this" means the previous 1st if in that month,+-- otherwise the start of the next occurrence of that month.+-- data SmartDate   = SmartCompleteDate Day   | SmartAssumeStart Year (Maybe Month)         -- XXX improve these constructor names   | SmartFromReference (Maybe Month) MonthDay   --   | SmartMonth Month   | SmartRelative Integer SmartInterval+  | SmartRelativeMonth Ordering Month           -- ^ EQ will be treated like GT+  | SmartRelativeWeekDay Ordering WeekDay   deriving (Show)  data SmartInterval = Day | Week | Month | Quarter | Year deriving (Show)@@ -181,6 +189,7 @@   | Expense   | Cash  -- ^ a subtype of Asset - liquid assets to show in cashflow report   | Conversion -- ^ a subtype of Equity - account with which to balance commodity conversions+  | Gain      -- ^ a subtype of Revenue - capital gains/losses   deriving (Eq,Ord,Generic)  instance Show AccountType where@@ -191,6 +200,7 @@   show Expense    = "X"   show Cash       = "C"   show Conversion = "V"+  show Gain       = "G"  isBalanceSheetAccountType :: AccountType -> Bool isBalanceSheetAccountType t = t `elem` [@@ -204,7 +214,8 @@ isIncomeStatementAccountType :: AccountType -> Bool isIncomeStatementAccountType t = t `elem` [   Revenue,-  Expense+  Expense,+  Gain   ]  -- | Check whether the first argument is a subtype of the second: either equal@@ -219,6 +230,8 @@ isAccountSubtypeOf Cash       Asset      = True isAccountSubtypeOf Conversion Conversion = True isAccountSubtypeOf Conversion Equity     = True+isAccountSubtypeOf Gain       Gain       = True+isAccountSubtypeOf Gain       Revenue    = True isAccountSubtypeOf _          _          = False  -- not worth the trouble, letters defined in accountdirectivep for now@@ -321,16 +334,29 @@ type CommoditySymbol = Text  data Commodity = Commodity {-  csymbol :: CommoditySymbol,-  cformat :: Maybe AmountStyle+  csymbol  :: CommoditySymbol,+  cformat  :: Maybe AmountStyle,+  ccomment :: Text,              -- ^ any comment lines following the commodity directive+  ctags    :: [Tag]              -- ^ tags extracted from the comment, if any   } deriving (Show,Eq,Generic) --,Ord) +-- | The cost basis of an individual lot - some quantity of an asset acquired at a given date and time.+-- This can represent a definite cost basis, which must have a cost and date; the label is optional.+-- Or it can represent a cost basis matcher for selecting lots.+-- Note: cost is always stored as a per-unit cost, even if the user specified total cost with {{}}.+data CostBasis = CostBasis {+  cbCost  :: !(Maybe Amount),    -- ^ nominal acquisition cost (per-unit)+  cbDate  :: !(Maybe Day),       -- ^ nominal acquisition date+  cbLabel :: !(Maybe Text)       -- ^ a short label to ensure uniqueness, correct intra-day order, or memorability, if needed+} deriving (Show,Eq,Generic,Ord)+ data Amount = Amount {-      acommodity  :: !CommoditySymbol,     -- commodity symbol, or special value "AUTO"-      aquantity   :: !Quantity,            -- numeric quantity, or zero in case of "AUTO"-      astyle      :: !AmountStyle,-      acost       :: !(Maybe AmountCost)  -- ^ the (fixed, transaction-specific) cost in another commodity of this amount, if any-    } deriving (Eq,Ord,Generic,Show)+  acommodity  :: !CommoditySymbol,     -- commodity symbol, or special value "AUTO"+  aquantity   :: !Quantity,            -- numeric quantity, or zero in case of "AUTO"+  astyle      :: !AmountStyle,+  acost       :: !(Maybe AmountCost),    -- ^ transacted exchange rate - the unit or total cost, in another commodity, used for this amount within its transaction+  acostbasis  :: !(Maybe CostBasis)    -- ^ the cost basis of an investment lot represented by this amount. Or, a matcher to select such a lot from the available lots.+} deriving (Eq,Ord,Generic,Show)  -- | Types with this class have one or more amounts, -- which can have display styles applied to them.@@ -642,6 +668,7 @@   ,jdeclaredaccounttypes    :: M.Map AccountType [AccountName]        -- ^ Accounts which were declared with a type: tag, grouped by the type.   ,jaccounttypes            :: M.Map AccountName AccountType          -- ^ All the account types known, from account declarations or account names or parent accounts.   ,jdeclaredcommodities     :: M.Map CommoditySymbol Commodity        -- ^ Commodities (and their display styles) declared by commodity directives, in parse order.+  ,jdeclaredcommoditytags   :: M.Map CommoditySymbol [Tag]            -- ^ Commodities which were declared with tags, and those tags.   ,jinferredcommoditystyles :: M.Map CommoditySymbol AmountStyle      -- ^ Commodity display styles inferred from amounts in the journal.   ,jglobalcommoditystyles   :: M.Map CommoditySymbol AmountStyle      -- ^ Commodity display styles declared by command line options (sometimes augmented, see the import command).   ,jpricedirectives         :: [PriceDirective]                       -- ^ P (market price) directives in the journal, in parse order.@@ -794,6 +821,7 @@ instance NFData AmountStyle instance NFData BalanceAssertion instance NFData Commodity+instance NFData CostBasis instance NFData DateSpan instance NFData DigitGroupStyle instance NFData EFDay
Hledger/Data/Valuation.hs view
@@ -32,13 +32,13 @@ where  import Control.Applicative ((<|>))-import Data.Function ((&), on)+import Data.Function ((&)) import Data.List (partition, intercalate, sortBy)-import Data.List.Extra (nubSortBy) import Data.Map qualified as M import Data.Set qualified as S import Data.Text qualified as T import Data.Time.Calendar (Day, fromGregorian)+import Data.Maybe (fromMaybe, mapMaybe) import Data.MemoUgly (memo) import GHC.Generics (Generic) import Safe (headMay, lastMay)@@ -84,6 +84,92 @@ -- given date. type PriceOracle = (Day, CommoditySymbol, Maybe CommoditySymbol) -> Maybe (CommoditySymbol, Quantity) +-- | An index of market prices for efficient lookup by commodity pair and date.+-- Maps each (from, to) commodity pair to a Map from date to the effective price,+-- where declared prices take precedence over inferred prices on the same day.+-- This allows O(log n) lookup per pair via M.lookupLE.+type PriceIndex = M.Map (CommoditySymbol, CommoditySymbol) (M.Map Day MarketPrice)++-- | Build a price index from declared and inferred market prices.+-- This is O(n log n) but done only once, enabling fast lookups later.+buildPriceIndex :: [MarketPrice] -> [MarketPrice] -> PriceIndex+buildPriceIndex declaredprices inferredprices =+  let+    -- Label each price with precedence (declared=True > inferred=False) and parse order+    declaredprices' = [(mpdate p, True, i, p) | (i, p) <- zip [1..] declaredprices]+    inferredprices' = [(mpdate p, False, i, p) | (i, p) <- zip [1..] inferredprices]+    allprices = declaredprices' ++ inferredprices'+    -- Group by commodity pair+    grouped = M.fromListWith (++)+      [((mpfrom p, mpto p), [(d, prec, order, p)]) | (d, prec, order, p) <- allprices]+    -- Build inner Map: sort ascending by (date, prec, order), then M.fromList+    -- keeps the last entry per date (highest precedence/parseorder wins)+    buildInnerMap prices =+      prices+      & sortBy compare+      & map (\(d, _, _, p) -> (d, p))+      & M.fromList+  in+    M.map buildInnerMap grouped++-- | Look up effective prices for all commodity pairs at a given date using the index.+-- Returns at most one price per commodity pair: the latest price on or before the date.+-- O(pairs × log n) where n is the number of prices per pair.+lookupEffectivePricesFromIndex :: Day -> PriceIndex -> [MarketPrice]+lookupEffectivePricesFromIndex d idx =+  mapMaybe (fmap snd . M.lookupLE d) (M.elems idx)++-- | Index for default valuation commodity lookup.+-- Maps source commodity to a map of (date -> destination), supporting O(log n) lookup+-- of the latest destination commodity on or before any given date.+type DefaultValuationIndex = M.Map CommoditySymbol (M.Map Day CommoditySymbol)++-- | Build an index for default valuation commodity lookup from a list of market prices.+buildDefaultValuationIndex :: [MarketPrice] -> DefaultValuationIndex+buildDefaultValuationIndex prices =+  let+    -- Label with parse order+    labeled = [(mpfrom p, mpdate p, i, mpto p) | (i, p) <- zip [1..] prices]+    -- Group by source commodity+    grouped = M.fromListWith (++) [(from, [(d, ord, to)]) | (from, d, ord, to) <- labeled]+    -- Build inner Map: sort by (date, parseorder), then M.fromList keeps last (highest parseorder per date)+    buildInnerMap entries = M.fromList [(d, to) | (d, _, to) <- sortBy compare entries]+  in+    M.map buildInnerMap grouped++-- | Combined indexes for efficient price lookup.+data PriceIndexes = PriceIndexes+  { piForward :: !PriceIndex                    -- ^ Index for forward prices (declared + inferred)+  , piDeclaredDefault :: !DefaultValuationIndex -- ^ Index for declared prices (for default valuation)+  , piInferredDefault :: !DefaultValuationIndex -- ^ Index for inferred prices (fallback for default valuation)+  }++-- | Build all price indexes from declared and inferred market prices.+-- This is O(n log n) but done only once.+buildPriceIndexes :: [MarketPrice] -> [MarketPrice] -> PriceIndexes+buildPriceIndexes declaredprices inferredprices = PriceIndexes+  { piForward = buildPriceIndex declaredprices inferredprices+  , piDeclaredDefault = buildDefaultValuationIndex declaredprices+  , piInferredDefault = buildDefaultValuationIndex inferredprices+  }++-- | Look up default valuation commodities for all source commodities at a given date.+-- Fallback logic: declared at date d, then declared at any date, then inferred at date d.+lookupDefaultValuations :: Day -> PriceIndexes -> M.Map CommoditySymbol CommoditySymbol+lookupDefaultValuations d PriceIndexes{..} =+    fromMaybe fallback (tryDeclaredAtDate <|> tryDeclaredLatest)+  where+    nonEmpty m = if M.null m then Nothing else Just m++    tryDeclaredAtDate = nonEmpty $ lookupDefaults (Just d) piDeclaredDefault+    tryDeclaredLatest = nonEmpty $ lookupDefaults Nothing  piDeclaredDefault+    fallback          = lookupDefaults (Just d) piInferredDefault++    lookupDefaults mdate = M.mapMaybe $ \innerMap ->+      case mdate of+        Nothing -> snd <$> M.lookupMax innerMap+        Just dt -> snd <$> M.lookupLE dt innerMap+ -- | Generate a price oracle (memoising price lookup function) from a -- journal's directive-declared and transaction-inferred market -- prices. For best performance, generate this only once per journal,@@ -98,7 +184,9 @@     inferredprices =       (if infer then jinferredmarketprices else [])       & dbg2Msg ("use prices inferred from costs? " <> if infer then "yes" else "no")-    makepricegraph = memo $ makePriceGraph declaredprices inferredprices+    -- Build indexes once for all lookups+    indexes = buildPriceIndexes declaredprices inferredprices+    makepricegraph = memo $ makePriceGraph indexes   in     memo $ uncurry3 $ priceLookup makepricegraph @@ -318,7 +406,7 @@       ,p 2000 01 01 "E"  2 "D"       ,p 2001 01 01 "A" 11 "B"       ]-    makepricegraph = makePriceGraph ps1 []+    makepricegraph = makePriceGraph (buildPriceIndexes ps1 [])   in testCase "priceLookup" $ do     priceLookup makepricegraph (fromGregorian 1999 01 01) "A" Nothing    @?= Nothing     priceLookup makepricegraph (fromGregorian 2000 01 01) "A" Nothing    @?= Just ("B",10)@@ -466,8 +554,9 @@ --    the `--infer-market-prices` flag is used, then the price commodity from --    the latest transaction price for A on or before valuation date." ---makePriceGraph :: [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph-makePriceGraph alldeclaredprices allinferredprices d =+-- | Build the price graph using pre-built indexes for O(pairs × log n) lookup.+makePriceGraph :: PriceIndexes -> Day -> PriceGraph+makePriceGraph indexes d =   dbg9 ("makePriceGraph "++show d) $   PriceGraph{      pgDate = d@@ -478,10 +567,9 @@   where     -- XXX logic duplicated in Hledger.Cli.Commands.Prices.prices, keep synced -    -- prices in effect on date d, either declared or inferred-    visibledeclaredprices = dbg9 "visibledeclaredprices" $ filter ((<=d).mpdate) alldeclaredprices-    visibleinferredprices = dbg9 "visibleinferredprices" $ filter ((<=d).mpdate) allinferredprices-    forwardprices = effectiveMarketPrices visibledeclaredprices visibleinferredprices+    -- get the latest effective price for each commodity pair on or before date d+    forwardprices = dbg9 "effective forward prices" $+      lookupEffectivePricesFromIndex d (piForward indexes)      -- infer any additional reverse prices not already declared or inferred     reverseprices = dbg9 "additional reverse prices" $@@ -492,42 +580,9 @@         forwardpairs = S.fromList [(mpfrom,mpto) | MarketPrice{..} <- forwardprices]     allprices = forwardprices ++ reverseprices -    -- determine a default valuation commodity for each source commodity-    -- somewhat but not quite like effectiveMarketPrices-    defaultdests = M.fromList [(mpfrom,mpto) | MarketPrice{..} <- pricesfordefaultcomms]-      where-        pricesfordefaultcomms = dbg9 "prices for choosing default valuation commodities, by date then parse order" $-          ps-          & zip [1..]  -- label items with their parse order-          & sortBy (compare `on` (\(parseorder,MarketPrice{..})->(mpdate,parseorder)))  -- sort by increasing date then increasing parse order-          & map snd    -- discard labels-          where-            ps | not $ null visibledeclaredprices = visibledeclaredprices-               | not $ null alldeclaredprices     = alldeclaredprices-               | otherwise                        = visibleinferredprices  -- will be null without --infer-market-prices---- | Given a list of P-declared market prices in parse order and a--- list of transaction-inferred market prices in parse order, select--- just the latest prices that are in effect for each commodity pair.--- That is, for each commodity pair, the latest price by date then--- parse order, with declared prices having precedence over inferred--- prices on the same day.-effectiveMarketPrices :: [MarketPrice] -> [MarketPrice] -> [MarketPrice]-effectiveMarketPrices declaredprices inferredprices =-  let-    -- label each item with its same-day precedence, then parse order-    declaredprices' = [(1, i, p) | (i,p) <- zip [1..] declaredprices]-    inferredprices' = [(0, i, p) | (i,p) <- zip [1..] inferredprices]-  in-    dbg9 "effective forward prices" $-    -- combine-    declaredprices' ++ inferredprices'-    -- sort by decreasing date then decreasing precedence then decreasing parse order-    & sortBy (flip compare `on` (\(precedence,parseorder,mp)->(mpdate mp,precedence,parseorder)))-    -- discard the sorting labels-    & map third3-    -- keep only the first (ie the newest, highest precedence, latest parsed) price for each pair-    & nubSortBy (compare `on` (\(MarketPrice{..})->(mpfrom,mpto)))+    -- use indexed lookup for default valuation commodities+    defaultdests = dbg9 "default valuation commodities" $+      lookupDefaultValuations d indexes  marketPriceReverse :: MarketPrice -> MarketPrice marketPriceReverse mp@MarketPrice{..} = 
Hledger/Query.hs view
@@ -152,6 +152,7 @@ -- | A query option changes a query's/report's behaviour and output in some way. data QueryOpt = QueryOptInAcctOnly AccountName  -- ^ show an account register focussed on this account               | QueryOptInAcct AccountName      -- ^ as above but include sub-accounts in the account register+              | QueryOptInterval Interval       -- ^ report interval extracted from a date: query            -- | QueryOptCostBasis      -- ^ show amounts converted to cost where possible            -- | QueryOptDate2  -- ^ show secondary dates instead of primary dates     deriving (Show, Eq)@@ -302,11 +303,12 @@ parseQueryTerm _ (T.stripPrefix "note:" -> Just s) = (,[]) <$> noteTag (Just s) parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = (,[]) . Acct <$> toRegexCI s parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =-        case parsePeriodExpr d s of Left e         -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e-                                    Right (_,spn) -> Right (Date2 spn, [])+        case parsePeriodExpr d s of Left e                   -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e+                                    Right (_         , spn)  -> Right (Date2 spn, []) parseQueryTerm d (T.stripPrefix "date:" -> Just s) =-        case parsePeriodExpr d s of Left e         -> Left $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e-                                    Right (_,spn) -> Right (Date spn, [])+        case parsePeriodExpr d s of Left e                   -> Left $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e+                                    Right (NoInterval, spn)  -> Right (Date spn, [])+                                    Right (interval  , spn)  -> Right (Date spn, [QueryOptInterval interval]) parseQueryTerm _ (T.stripPrefix "status:" -> Just s) =         case parseStatus s of Left e   -> Left $ "\"status:"++T.unpack s++"\" gave a parse error: " ++ e                               Right st -> Right (StatusQ st, [])@@ -514,11 +516,11 @@     help = "type:'s argument should be one or more of " ++ accountTypeChoices False  accountTypeChoices :: Bool -> String-accountTypeChoices allowlongform = -  intercalate ", " +accountTypeChoices allowlongform =+  intercalate ", "     -- keep synced with parseAccountType-    $ ["A","L","E","R","X","C","V"]-    ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"] else []+    $ ["A","L","E","R","X","C","V","G"]+    ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain"] else []  -- | Case-insensitively parse one single-letter code, or one long-form word if permitted, to an account type. -- On failure, returns the unparseable text.@@ -533,6 +535,7 @@     "x"                          -> Right Expense     "c"                          -> Right Cash     "v"                          -> Right Conversion+    "g"                          -> Right Gain     "asset"      | allowlongform -> Right Asset     "liability"  | allowlongform -> Right Liability     "equity"     | allowlongform -> Right Equity@@ -540,6 +543,7 @@     "expense"    | allowlongform -> Right Expense     "cash"       | allowlongform -> Right Cash     "conversion" | allowlongform -> Right Conversion+    "gains"      | allowlongform -> Right Gain     _                            -> Left $ T.unpack s  -- | Parse the value part of a "status:" query, or return an error.@@ -799,6 +803,7 @@ inAccount [] = Nothing inAccount (QueryOptInAcctOnly a:_) = Just (a,False) inAccount (QueryOptInAcct a:_) = Just (a,True)+inAccount (QueryOptInterval _:rest) = inAccount rest  -- | A query for the account(s) we are currently focussed on, if any. -- Just looks at the first query option.@@ -806,6 +811,7 @@ inAccountQuery [] = Nothing inAccountQuery (QueryOptInAcctOnly a : _) = Just . Acct $ accountNameToAccountOnlyRegex a inAccountQuery (QueryOptInAcct a     : _) = Just . Acct $ accountNameToAccountRegex a+inAccountQuery (QueryOptInterval _   : rest) = inAccountQuery rest  -- -- | Convert a query to its inverse. -- negateQuery :: Query -> Query
Hledger/Read.hs view
@@ -106,6 +106,7 @@   readJournal,   readJournalFile,   readPossibleJournalFile,+  readPossibleJournalFiles,   readJournalFiles,   readJournalFilesAndLatestDates, @@ -142,7 +143,7 @@ import Data.Default (def) import Data.Foldable (asum) import Data.List (group, sort, sortBy)-import Data.List.NonEmpty (nonEmpty)+import Data.List.NonEmpty (nonEmpty, NonEmpty((:|))) import Data.Maybe (catMaybes, fromMaybe) import Data.Ord (comparing) import Data.Semigroup (sconcat)@@ -151,9 +152,9 @@ import Data.Text.IO qualified as T import Data.Time (Day) import Safe (headDef)-import System.Directory (doesFileExist)+import System.Directory (createDirectoryIfMissing, doesFileExist) import System.Environment (getEnv)-import System.FilePath ((<.>), (</>), splitDirectories, splitFileName, takeFileName)+import System.FilePath ((<.>), (</>), splitDirectories, splitFileName, takeDirectory, takeFileName) import System.Info (os) import System.IO (Handle, hPutStrLn, stderr) import Text.Printf (printf)@@ -341,11 +342,29 @@ readPossibleJournalFile :: InputOpts -> PrefixedFilePath -> ExceptT String IO Journal readPossibleJournalFile iopts prefixedfile = do   let (_, f) = splitReaderPrefix prefixedfile-  exists <- liftIO $ doesFileExist f-  if exists+  if f == "-"     then readJournalFile iopts prefixedfile-    else return $ nulljournal{jfiles = [(f, "")]}+    else do+      exists <- liftIO $ doesFileExist f+      if exists+        then readJournalFile iopts prefixedfile+        else return $ nulljournal{jfiles = [(f, "")]} +-- | Like readJournalFiles, but if the first file does not exist, provides an empty+-- journal with that file path set. Other files must exist as normal.+-- This is useful for commands like add and import that write to the first file,+-- which might not exist yet, while also reading other files for completions etc.+readPossibleJournalFiles :: InputOpts -> [PrefixedFilePath] -> ExceptT String IO Journal+readPossibleJournalFiles iopts pfs = case pfs of+  []     -> return nulljournal+  (f:fs) -> do+    let iopts' = iopts{_defer=True}+    j1 <- readPossibleJournalFile iopts' f+    js <- mapM (readJournalFile iopts') fs+    let combined = journalNumberTransactions $ sconcat (j1 :| js)+    when (strict_ iopts) $ liftEither $ journalStrictChecks combined+    return combined+ -- | Read a Journal from each specified file path (using @readJournalFile@) -- and combine them into one; or return the first error message. --@@ -428,6 +447,10 @@   exists <- doesFileExist f   unless exists $ do     hPutStrLn stderr $ "Creating hledger journal file " <> show f+    -- Create parent directories if they don't exist+    let dir = takeDirectory f+    unless (null dir || dir == ".") $+      createDirectoryIfMissing True dir     -- note Hledger.Utils.UTF8.* do no line ending conversion on windows,     -- we currently require unix line endings on all platforms.     newJournalContent >>= T.writeFile f
Hledger/Read/Common.hs view
@@ -128,7 +128,7 @@  --- ** imports import Control.Applicative.Permutations (runPermutation, toPermutationWithDefault)-import Control.Monad (foldM, liftM2, when, unless, (>=>), (<=<))+import Control.Monad (foldM, join, liftM2, when, unless, (>=>), (<=<)) import Control.Monad.Fail qualified as Fail (fail) import Control.Monad.Except (ExceptT(..), liftEither, withExceptT) import Control.Monad.IO.Class (MonadIO, liftIO)@@ -209,7 +209,7 @@ -- | Parse an InputOpts from a RawOpts and a provided date. -- This will fail with a usage error if the forecast period expression cannot be parsed. rawOptsToInputOpts :: Day -> Bool -> Bool -> RawOpts -> InputOpts-rawOptsToInputOpts day usecoloronstdout postingaccttags rawopts =+rawOptsToInputOpts day usecoloronstdout autopostingtags rawopts =      let         -- Allow/disallow implicit-cost conversion transactions, according to policy in Check.md.@@ -244,7 +244,7 @@       ,new_save_          = True       ,pivot_             = stringopt "pivot" rawopts       ,forecast_          = forecastPeriodFromRawOpts day rawopts-      ,posting_account_tags_ = postingaccttags+      ,auto_posting_tags_ = autopostingtags       ,verbose_tags_      = boolopt "verbose-tags" rawopts       ,reportspan_        = DateSpan (Exact <$> queryStartDate False datequery) (Exact <$> queryEndDate False datequery)       ,auto_              = boolopt "auto" rawopts@@ -290,7 +290,7 @@     optList = listofstringopt "commodity-style" rawOpts     parseCommodity optStr = case parseamount optStr of       Left _ -> Left optStr-      Right (Amount acommodity _ astyle _) -> Right (acommodity, astyle)+      Right (Amount acommodity _ astyle _ _) -> Right (acommodity, astyle)  transactionBalancingPrecisionFromOpts :: RawOpts -> Either String TransactionBalancingPrecision transactionBalancingPrecisionFromOpts rawopts =@@ -370,7 +370,7 @@ -- Others (commodities, accounts..) are done later by journalStrictChecks. -- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal-journalFinalise iopts@InputOpts{auto_,balancingopts_,infer_costs_,infer_equity_,strict_,posting_account_tags_,verbose_tags_,_ioDay} f txt pj = do+journalFinalise iopts@InputOpts{auto_,balancingopts_,infer_costs_,infer_equity_,strict_,auto_posting_tags_,verbose_tags_,_ioDay} f txt pj = do   let     BalancingOpts{commodity_styles_, ignore_assertions_} = balancingopts_     fname = "journalFinalise " <> takeFileName f@@ -394,7 +394,7 @@             -- XXX does not see conversion accounts generated by journalInferEquityFromCosts below, requiring a workaround in journalCheckAccounts. Do it later ?       &   journalStyleAmounts                            -- Infer and apply commodity styles (but don't round) - should be done early       <&> journalAddForecast verbose_tags_ (forecastPeriod iopts pj)   -- Add forecast transactions if enabled-      <&> (if posting_account_tags_ then journalPostingsAddAccountTags else id)     -- Propagate account tags to postings - unless printing a beancount journal+      <&> (if auto_posting_tags_ then journalPostingsAddAccountTags else id)     -- Maybe propagate account tags to postings       >>= journalTagCostsAndEquityAndMaybeInferCosts verbose_tags_ False   -- Tag equity conversion postings and redundant costs, to help journalBalanceTransactions ignore them.       >>= (if auto_ && not (null $ jtxnmodifiers pj)             then journalAddAutoPostings verbose_tags_ _ioDay balancingopts_  -- Add auto postings if enabled, and account tags if needed. Does preliminary transaction balancing.@@ -409,6 +409,7 @@       -- <&> dbg9With (("journalFinalise amounts after styling, forecasting, auto postings, transaction balancing"<>).showJournalPostingAmountsDebug)       >>= journalInferCommodityStyles                    -- infer commodity styles once more now that all posting amounts are present       -- >>= Right . dbg0With (pshow.journalCommodityStyles)+      <&> (if auto_posting_tags_ then journalPostingsAddCommodityTags else id)  -- Maybe propagate commodity tags to postings (after amounts are inferred)       >>= (if infer_costs_  then journalTagCostsAndEquityAndMaybeInferCosts verbose_tags_ True else pure)  -- With --infer-costs, infer costs from equity postings where possible       <&> (if infer_equity_ then journalInferEquityFromCosts verbose_tags_ else id)          -- With --infer-equity, infer equity postings from costs where possible       <&> dbg9With (lbl "amounts after equity-inferring".showJournalPostingAmountsDebug)@@ -833,18 +834,22 @@ -- if not, a commodity-less amount will have the default commodity applied to it. amountp' :: Bool -> JournalParser m Amount amountp' mult =-  -- dbg "amountp'" $ +  -- dbg "amountp'" $   label "amount" $ do   let spaces = lift $ skipNonNewlineSpaces   amt <- simpleamountp mult <* spaces-  (mcost, _valuationexpr, _mlotcost, _mlotdate, _mlotnote) <- runPermutation $+  (mcost, _valuationexpr, mlotcost, mlotdate, mlotnote) <- runPermutation $     -- costp, valuationexprp, lotnotep all parse things beginning with parenthesis, try needed     (,,,,) <$> toPermutationWithDefault Nothing (Just <$> try (costp amt) <* spaces)           <*> toPermutationWithDefault Nothing (Just <$> valuationexprp <* spaces)  -- XXX no try needed here ?-          <*> toPermutationWithDefault Nothing (Just <$> lotcostp <* spaces)+          <*> toPermutationWithDefault Nothing (Just <$> lotcostp (aquantity amt) <* spaces)           <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces)           <*> toPermutationWithDefault Nothing (Just <$> lotnotep <* spaces)-  pure $ amt { acost = mcost }+  let mcostbasis =+        case (mlotcost, mlotdate, mlotnote) of+          (Nothing, Nothing, Nothing) -> Nothing+          _ -> Just $ CostBasis { cbCost = join mlotcost, cbDate = mlotdate, cbLabel = mlotnote }+  pure $ amt { acost = mcost, acostbasis = mcostbasis }  -- An amount with optional cost, but no cost basis. amountnobasisp :: JournalParser m Amount@@ -1026,11 +1031,11 @@     , baposition  = sourcepos     } --- Parse a Ledger-style lot cost,--- {UNITCOST} or {{TOTALCOST}} or {=FIXEDUNITCOST} or {{=FIXEDTOTALCOST}} or {},--- and discard it.-lotcostp :: JournalParser m ()-lotcostp =+-- Parse a Ledger-style lot cost:+-- {UNITCOST} or {{TOTALCOST}} or {=FIXEDUNITCOST} or {{=FIXEDTOTALCOST}} or {}.+-- If total cost syntax {{}} is used, converts it to unit cost by dividing by the posting quantity.+lotcostp :: Quantity -> JournalParser m (Maybe Amount)+lotcostp postingqty =   -- dbg "lotcostp" $   label "ledger-style lot cost" $ do   char '{'@@ -1038,33 +1043,40 @@   lift skipNonNewlineSpaces   _fixed <- fmap isJust $ optional $ char '='   lift skipNonNewlineSpaces-  _a <- option 0 $ simpleamountp False+  ma <- optional $ simpleamountp False   lift skipNonNewlineSpaces   char '}'   when (doublebrace) $ void $ char '}'+  pure $ fmap (convertToUnitCost doublebrace) ma+  where+    -- Convert {{TOTALCOST}} to {UNITCOST} by dividing by posting quantity+    convertToUnitCost isTotal lotamt =+      if isTotal && postingqty /= 0+      then lotamt { aquantity = aquantity lotamt / postingqty }+      else lotamt --- Parse a Ledger-style [LOTDATE], and discard it.-lotdatep :: JournalParser m ()+-- Parse a Ledger-style [LOTDATE].+lotdatep :: JournalParser m Day lotdatep =   -- dbg "lotdatep" $   label "ledger-style lot date" $ do   char '['   lift skipNonNewlineSpaces-  _d <- datep+  d <- datep   lift skipNonNewlineSpaces   char ']'-  return ()+  return d --- Parse a Ledger-style (LOT NOTE), and discard it.-lotnotep :: JournalParser m ()+-- Parse a Ledger-style (LOT NOTE).+lotnotep :: JournalParser m Text lotnotep =   -- dbg "lotnotep" $   label "ledger-style lot note" $ do   char '('   lift skipNonNewlineSpaces-  _note <- stripEnd . T.pack <$> (many $ noneOf [')','\n'])  -- XXX other line endings ?+  note <- stripEnd . T.pack <$> (many $ noneOf [')','\n'])  -- XXX other line endings ?   char ')'-  return ()+  return note  -- | Parse a string representation of a number for its value and display -- attributes.
Hledger/Read/InputOptions.hs view
@@ -34,7 +34,7 @@     ,new_save_          :: Bool                 -- ^ save latest new transactions state for next time ?     ,pivot_             :: String               -- ^ use the given field's value as the account name     ,forecast_          :: Maybe DateSpan       -- ^ span in which to generate forecast transactions-    ,posting_account_tags_  :: Bool             -- ^ propagate account tags to postings ?+    ,auto_posting_tags_ :: Bool                 -- ^ propagate commodity and account tags to postings ? Can be disabled (for beancount export).     ,verbose_tags_      :: Bool                 -- ^ add user-visible tags when generating/modifying transactions & postings ?     ,reportspan_        :: DateSpan             -- ^ a dirty hack keeping the query dates in InputOpts. This rightfully lives in ReportSpec, but is duplicated here.     ,auto_              :: Bool                 -- ^ generate extra postings according to auto posting rules ?@@ -57,7 +57,7 @@     , new_save_          = True     , pivot_             = ""     , forecast_          = Nothing-    , posting_account_tags_ = False+    , auto_posting_tags_ = False     , verbose_tags_      = False     , reportspan_        = nulldatespan     , auto_              = False
Hledger/Read/JournalReader.hs view
@@ -318,12 +318,12 @@     return prefixedglob     ) <|> errorNoArg -  let (mprefix,glb) = splitReaderPrefix prefixedglob+  let (mprefix,path) = splitReaderPrefix prefixedglob   parentf <- sourcePosFilePath pos-  when (null $ dbg6 (parentf <> " include: glob pattern") glb) errorNoArg+  when (null $ dbg6 (parentf <> " include: path ") path) errorNoArg    -- Find the file or glob-matched files (just the ones from this include directive), with some IO error checking.-  paths <- findMatchedFiles eoff parentf glb+  paths <- findMatchedFiles eoff parentf path   -- Also report whether a glob pattern was used, and not just a literal file path.   -- (paths, isglob) <- findMatchedFiles off pos glb @@ -359,7 +359,7 @@     -- but ** will implicitly search non-top-level dot directories (see #2498, Glob#49).      findMatchedFiles :: (MonadIO m) => Int -> FilePath -> FilePath -> JournalParser m [FilePath]-    findMatchedFiles off parentf globpattern = do+    findMatchedFiles off parentf path = do        -- Some notes about the Glob library that we use (related: https://github.com/Deewiant/glob/issues/49):       -- It does not expand tilde.@@ -374,39 +374,39 @@       -- A **/ component matches any number of directory parts.       -- A **/ does not implicitly search top-level dot directories or implicitly match do files,       -- but it does search non-top-level dot directories. Eg ** will find the c file in a/.b/c.+      -- It tends to get attributes of all files in a directory.        -- expand a tilde at the start of the glob pattern, or throw an error-      expandedglob <- lift $ expandHomePath globpattern & handleIOError off "failed to expand ~"+      expandedpath <- lift $ expandHomePath path & handleIOError off "failed to expand ~"        -- get the directory of the including file       -- need to canonicalise a symlink parentf so takeDirectory works correctly [#2503]       cwd <- fmap takeDirectory <$> liftIO $ canonicalizePath parentf        -- Don't allow 3 or more stars.-      when ("***" `isInfixOf` expandedglob) $+      when ("***" `isInfixOf` expandedpath) $         customFailure $ parseErrorAt off $ "Invalid glob pattern: too many stars, use * or **"        -- Make ** also match file name parts like zsh's GLOB_STAR_SHORT.       let-        finalglob =+        finalpath =           -- ** without a slash is equivalent to **/*-          case regexReplace (toRegex' $ T.pack "\\*\\*([^/\\])") "**/*\\1" expandedglob of+          case regexReplace (toRegex' $ T.pack "\\*\\*([^/\\])") "**/*\\1" expandedpath of             Right s -> s-            Left  _ -> expandedglob   -- ignore any error, there should be none+            Left  _ -> expandedpath   -- ignore any error, there should be none        -- Compile as a Pattern. Can throw an error.-      pat <- case tryCompileWith compDefault{errorRecovery=False} finalglob of+      pat <- case tryCompileWith compDefault{errorRecovery=False} finalpath of         Left e  -> customFailure $ parseErrorAt off $ "Invalid glob pattern: " ++ e         Right x -> pure x -      -- Find all matched paths. These might include directories or the current file.-      -- Glob seems to get attributes of all files in a directory, which disturbs build systems-      -- which detect dependencies based on filesystem operations (eg tup).-      -- So avoid using it if not needed.-      paths <- liftIO $+      -- Find all paths matched by the glob pattern.+      -- If it is a literal (non-glob) path, don't use the Glob lib, because it gets attributes+      -- of all files in the directory, which confuses build systems like tup.+      paths <-         if isLiteral pat-        then return $ if isAbsolute finalglob then [finalglob] else [cwd </> finalglob]-        else globDir1 pat cwd+        then return $ if isAbsolute finalpath then [finalpath] else [cwd </> finalpath]+        else liftIO $ globDir1 pat cwd        -- Exclude any directories or symlinks to directories, and canonicalise, and sort.       files <- liftIO $@@ -429,7 +429,7 @@           | otherwise -> return f        -- Throw an error if no files were matched.-      when (null files2) $ customFailure $ parseErrorAt off $ "No files were matched by: " ++ globpattern+      when (null files2) $ customFailure $ parseErrorAt off $ "No files were matched by: " ++ path        -- If the current file got included, ignore it (last, to avoid triggering the error above).       let@@ -563,10 +563,12 @@     "c"          -> Right Cash     "conversion" -> Right Conversion     "v"          -> Right Conversion+    "gains"      -> Right Gain+    "g"          -> Right Gain     _            -> Left err   where     err = T.unpack $ "invalid account type code "<>s<>", should be one of " <>-            T.intercalate ", " ["A","L","E","R","X","C","V","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"]+            T.intercalate ", " ["A","L","E","R","X","C","V","G","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain"]  -- Add an account declaration to the journal, auto-numbering it. addAccountDeclaration :: (AccountName,Text,[Tag],SourcePos) -> JournalParser m ()@@ -626,11 +628,13 @@     amt <- amountp     pure $ (off, amt)   lift skipNonNewlineSpaces-  _ <- lift followingcommentp-  let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg7 "style from commodity directive" astyle}+  (comment, tags) <- lift transactioncommentp+  let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg7 "style from commodity directive" astyle, ccomment=comment, ctags=tags}   if isNothing $ asdecimalmark astyle   then customFailure $ parseErrorAt off pleaseincludedecimalpoint-  else modify' (\j -> j{jdeclaredcommodities=M.insert acommodity comm $ jdeclaredcommodities j})+  else modify' (\j -> j{jdeclaredcommodities=M.insert acommodity comm $ jdeclaredcommodities j+                       ,jdeclaredcommoditytags=if null tags then jdeclaredcommoditytags j+                                               else M.insert acommodity tags $ jdeclaredcommoditytags j})  pleaseincludedecimalpoint :: String pleaseincludedecimalpoint = chomp $ unlines [@@ -651,12 +655,14 @@   string "commodity"   lift skipNonNewlineSpaces1   sym <- lift commoditysymbolp-  _ <- lift followingcommentp+  (comment, tags) <- lift transactioncommentp   -- read all subdirectives, saving format subdirectives as Lefts   subdirectives <- many $ indented (eitherP (formatdirectivep sym) (lift restofline))   let mfmt = lastMay $ lefts subdirectives-  let comm = Commodity{csymbol=sym, cformat=mfmt}-  modify' (\j -> j{jdeclaredcommodities=M.insert sym comm $ jdeclaredcommodities j})+  let comm = Commodity{csymbol=sym, cformat=mfmt, ccomment=comment, ctags=tags}+  modify' (\j -> j{jdeclaredcommodities=M.insert sym comm $ jdeclaredcommodities j+                  ,jdeclaredcommoditytags=if null tags then jdeclaredcommoditytags j+                                          else M.insert sym tags $ jdeclaredcommoditytags j})   where     indented = (lift skipNonNewlineSpaces1 >>) 
Hledger/Reports/MultiBalanceReport.hs view
@@ -218,14 +218,14 @@ -- of 'Posting's. generateMultiBalanceAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe DayPartition -> [Posting] -> Account BalanceData generateMultiBalanceAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colspans =+    -- Set account declaration info (for sorting purposes)+    mapAccounts (accountSetDeclarationInfo j)     -- Add declared accounts if called with --declared and --empty-    (if (declared_ ropts && empty_ ropts) then addDeclaredAccounts rspec j else id)+    . (if (declared_ ropts && empty_ ropts) then addDeclaredAccounts rspec j else id)     -- Negate amounts if applicable     . (if invert_ ropts then fmap (mapBalanceData maNegate) else id)     -- Mark which accounts are boring and which are interesting     . markAccountBoring rspec-    -- Set account declaration info (for sorting purposes)-    . mapAccounts (accountSetDeclarationInfo j)     -- Process changes into normal, cumulative, or historical amounts, plus value them     . calculateReportAccount rspec j priceoracle colspans     -- Clip account names@@ -505,7 +505,7 @@ tests_MultiBalanceReport = testGroup "MultiBalanceReport" [    let-    amt0 = Amount {acommodity="$", aquantity=0, acost=Nothing,+    amt0 = Amount {acommodity="$", aquantity=0, acost=Nothing, acostbasis=Nothing,       astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asdigitgroups = Nothing,       asdecimalmark = Just '.', asprecision = Precision 2, asrounding = NoRounding}}     (rspec,journal) `gives` r = do
Hledger/Reports/ReportOptions.hs view
@@ -78,7 +78,7 @@ import Data.Functor.Identity (Identity(..)) import Data.List (partition) import Data.List.Extra (find, isPrefixOf, nubSort, stripPrefix)-import Data.Maybe (fromMaybe, isJust, isNothing)+import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) import Data.Text qualified as T import Data.Time.Calendar (Day, addDays) import Data.Default (Default(..))@@ -467,6 +467,14 @@ extractIntervalOrNothing (NoInterval, _) = Nothing extractIntervalOrNothing (interval, _) = Just interval +-- | Get the last interval specified in query opts, if any.+-- date: queries can specify a reporting interval.+intervalFromQueryOpts :: [QueryOpt] -> Maybe Interval+intervalFromQueryOpts = lastMay . mapMaybe getInterval+  where+    getInterval (QueryOptInterval i) = Just i+    getInterval _ = Nothing+ -- | Get any statuses to be matched, as specified by -U/--unmarked, -- -P/--pending, -C/--cleared flags. -UPC is equivalent to no flags, -- so this returns a list of 0-2 unique statuses.@@ -990,10 +998,14 @@ reportOptsToSpec :: Day -> ReportOpts -> Either String ReportSpec reportOptsToSpec day ropts = do     (argsquery, queryopts) <- parseQueryList day $ querystring_ ropts+    -- If there's an interval in the query opts, it overrides the interval from -p/--period/etc+    let ropts' = case intervalFromQueryOpts queryopts of+                   Just i  -> ropts{interval_=i}+                   Nothing -> ropts     return ReportSpec-      { _rsReportOpts = ropts+      { _rsReportOpts = ropts'       , _rsDay        = day-      , _rsQuery      = simplifyQuery $ And [queryFromFlags ropts, argsquery]+      , _rsQuery      = simplifyQuery $ And [queryFromFlags ropts', argsquery]       , _rsQueryOpts  = queryopts       } 
Hledger/Utils.hs view
@@ -18,6 +18,8 @@   curry4,   uncurry4, +  divideSafe,+   -- * Lists   maximum',   maximumStrict,@@ -145,6 +147,11 @@  uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e uncurry4 f (w, x, y, z) = f w x y z++-- | Division, returning 0 when the denominator is 0.+divideSafe :: (Eq a, Fractional a) => a -> a -> a+divideSafe _ 0 = 0+divideSafe a b = a / b  -- Lists 
Hledger/Utils/IO.hs view
@@ -67,9 +67,10 @@   getTerminalWidth,    -- * Pager output-  setupPager,   findPager,   runPager,+  lessVarValue,+  lessIsWorking,    -- * ANSI colour/styles @@ -149,15 +150,16 @@ import           System.Console.ANSI (Color(..),ColorIntensity(..), ConsoleLayer(..), SGR(..), hSupportsANSIColor, setSGRCode, getLayerColor, ConsoleIntensity (..)) import           System.Console.Terminal.Size (Window (Window), size) import           System.Directory (getHomeDirectory, getModificationTime, findExecutable)-import           System.Environment (getArgs, lookupEnv, setEnv, getProgName)-import           System.Exit (exitFailure)-import           System.FilePath (isRelative, (</>))+import           System.Environment (getArgs, getEnvironment, lookupEnv, getProgName)+import           System.Exit (ExitCode(ExitSuccess), exitFailure)+import           System.FilePath (isRelative, (</>), takeBaseName) import "Glob"    System.FilePath.Glob (glob) import           System.Info (os) import           System.IO (Handle, IOMode (..), hClose, hGetEncoding, hIsTerminalDevice, hPutStr, hPutStrLn, hSetNewlineMode, hSetEncoding, openFile, stderr, stdin, stdout, universalNewlineMode, utf8_bom, utf8) import System.IO.Encoding qualified as Enc import           System.IO.Unsafe (unsafePerformIO)-import           System.Process (CreateProcess(..), StdStream(CreatePipe), createPipe, shell, waitForProcess, withCreateProcess)+import           System.Process (CreateProcess(..), StdStream(CreatePipe), createPipe, proc, readCreateProcessWithExitCode, shell, waitForProcess, withCreateProcess)+import           System.Timeout (timeout) import           Text.Pretty.Simple (CheckColorTty(..), OutputOptions(..), defaultOutputOptionsDarkBg, defaultOutputOptionsNoColor, pShowOpt, pPrintOpt)  import Hledger.Utils.Text (WideBuilder(WideBuilder))@@ -398,10 +400,11 @@ getHomeSafe :: IO (Maybe FilePath) getHomeSafe = fmap Just getHomeDirectory `catch` (\(_ :: IOException) -> return Nothing) --- | Expand a tilde (representing home directory) at the start of a file path.--- ~username is not supported. Can raise an error.+-- | Expand a single tilde (representing home directory) at the start of a file path.+-- ~username is not supported. This can raise an IO error. expandHomePath :: FilePath -> IO FilePath expandHomePath = \case+    "~"          -> getHomeDirectory     ('~':'/':p)  -> (</> p) <$> getHomeDirectory     ('~':'\\':p) -> (</> p) <$> getHomeDirectory     ('~':_)      -> ioError $ userError "~USERNAME in paths is not supported"@@ -409,12 +412,14 @@  -- | Given a current directory, convert a possibly relative, possibly tilde-prefixed -- file path to an absolute one.--- ~username is not supported. Leaves "-" unchanged. Can raise an error.+-- ~username is not supported.+-- If the file path is "-", it is left as-is.+-- This can an raise an IO error. expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers expandPath _ "-" = return "-" expandPath curdir p = (if isRelative p then (curdir </>) else id) <$> expandHomePath p  -- PARTIAL: --- | Like expandPath, but treats the expanded path as a glob, and returns+-- | Like @expandPath@, but treats the expanded path as a glob, and returns -- zero or more matched absolute file paths, alphabetically sorted. -- Can raise an error. -- For a more elaborate glob expander, see 'findMatchedFiles' (used by the include directive).@@ -636,79 +641,16 @@ -- Pager output -- somewhat hledger-specific --- Configure some preferred options for the `less` pager,--- by modifying the LESS environment variable in this program's environment.--- If you are using some other pager, this will have no effect.--- By default, this sets the following options, appending them to LESS's current value:------   --chop-long-lines---   --hilite-unread---   --ignore-case---   --no-init---   --quit-at-eof---   --quit-if-one-screen---   --RAW-CONTROL-CHARS---   --shift=8---   --squeeze-blank-lines---   --use-backslash------ You can choose different options by setting the HLEDGER_LESS variable;--- if set, its value will be used instead of LESS.--- Or you can force hledger to use your exact LESS settings,--- by setting HLEDGER_LESS equal to LESS.----setupPager :: IO ()-setupPager = do-  let-    -- keep synced with doc above-    deflessopts = unwords [-       "--chop-long-lines"-      ,"--hilite-unread"-      ,"--ignore-case"-      ,"--no-init"-      ,"--quit-at-eof"-      ,"--quit-if-one-screen"-      ,"--RAW-CONTROL-CHARS"-      ,"--shift=8"-      ,"--squeeze-blank-lines"-      ,"--use-backslash"-      -- ,"--use-color"  #2335 rejected by older less versions (eg 551)-      ]-  mhledgerless <- lookupEnv "HLEDGER_LESS"-  mless        <- lookupEnv "LESS"-  setEnv "LESS" $-    case (mhledgerless, mless) of-      (Just hledgerless, _) -> hledgerless-      (_, Just less)        -> if deflessopts `isInfixOf` less then less else unwords [less, deflessopts]-      _                     -> deflessopts---- | Display the given text on the terminal, trying to use a pager ($PAGER, less, or more)--- when appropriate, otherwise printing to standard output. Uses maybePagerFor.------ hledger's output may contain ANSI style/color codes--- (if the terminal supports them and they are not disabled by --color=no or NO_COLOR),--- so the pager should be configured to handle these.--- setupPager tries to configure that automatically when using the `less` pager.----runPager :: String -> IO ()-runPager s = do-  mpager <- maybePagerFor s-  case mpager of-    Nothing -> putStr s-    Just pager -> do-      withCreateProcess (shell pager){std_in=CreatePipe} $-        \mhin _ _ p -> do-          -- Pipe in the text on stdin.-          case mhin of-            Nothing  -> return ()  -- shouldn't happen-            Just hin -> void $ forkIO $   -- Write from another thread to avoid deadlock ? Maybe unneeded, but just in case.-              (hPutStr hin s >> hClose hin)  -- Be sure to close the pipe so the pager knows we're done.-                -- If the pager quits early, we'll receive an EPIPE error; hide that.-                `catch` \(e::IOException) -> case e of-                  IOError{ioe_type=ResourceVanished, ioe_errno=Just ioe, ioe_handle=Just hdl} | Errno ioe==ePIPE, hdl==hin-                    -> return ()-                  _ -> throwIO e-          void $ waitForProcess p+-- | Try to find a pager executable robustly, safely handling various error conditions+-- like an unset PATH var or the specified pager not being found as an executable.+-- The pager can be specified by a path or program name in the PAGER environment variable.+-- If that is unset or has a problem, "less" is tried, then "more".+-- If successful, the pager's path or program name is returned.+findPager :: IO (Maybe String)  -- XXX probably a ByteString in fact ?+findPager = do+  mpagervar <- lookupEnv "PAGER"+  let pagers = [p | Just p <- [mpagervar]] <> ["less", "more"]+  headMay . catMaybes <$> mapM findExecutable pagers  -- | Should a pager be used for displaying the given text on stdout, and if so, which one ? -- Uses a pager if findPager finds one and none of the following conditions are true:@@ -737,17 +679,107 @@     guard $ oh > th || ow > tw     mpager --- | Try to find a pager executable robustly, safely handling various error conditions--- like an unset PATH var or the specified pager not being found as an executable.--- The pager can be specified by a path or program name in the PAGER environment variable.--- If that is unset or has a problem, "less" is tried, then "more".--- If successful, the pager's path or program name is returned.-findPager :: IO (Maybe String)  -- XXX probably a ByteString in fact ?-findPager = do-  mpagervar <- lookupEnv "PAGER"-  let pagers = [p | Just p <- [mpagervar]] <> ["less", "more"]-  headMay . catMaybes <$> mapM findExecutable pagers+-- | Display the given text on the terminal, trying to use a pager ($PAGER, less, or more)+-- when appropriate (see maybePagerFor), otherwise printing to standard output.+-- Also, if the pager is less, we modify the LESS environment variable (see lessVarValue)+-- and check for problems which could cause confusing output (see lessIsWorking).+runPager :: String -> IO ()+runPager s = do+  mpager <- maybePagerFor s+  case mpager of+    Nothing -> putStr s+    Just pager -> do +      -- If using less, customise the LESS environment variable and check if it works+      let pagerIsLess = map toLower (takeBaseName pager) == "less"+      (mCustomEnv, shouldUsePager) <- if not pagerIsLess+        then return (Nothing, True)+        else do+          mHLEDGER_LESS <- lookupEnv "HLEDGER_LESS"+          mLESS         <- lookupEnv "LESS"+          usecolor      <- useColorOnStdout+          let newlessvar = lessVarValue mHLEDGER_LESS mLESS usecolor+          env <- getEnvironment+          let customEnv = ("LESS", newlessvar) : filter ((/= "LESS") . fst) env+          -- Check that less --version is working (using our custom LESS) (#2544)+          lessHasError <- lessIsWorking (Just customEnv) `catch` \(_::IOException) -> return True+          when lessHasError $ warnIO $+            "less --version fails with current LESS settings; disabling. Check 'hledger setup' for details.\n"+          return (Just customEnv, not lessHasError)++      -- Run the pager, providing the text as input. Or if we found a problem already, just print.+      if not shouldUsePager+        then putStr s+        else (withCreateProcess (shell pager){std_in=CreatePipe, env=mCustomEnv} $+          \mhin _ _ p -> do+            case mhin of+              Nothing  -> fail "Failed to create pipe to pager"+              Just hin -> void $ forkIO $   -- Write from another thread to avoid deadlock ? Maybe unneeded, but just in case.+                (hPutStr hin s >> hClose hin)  -- Be sure to close the pipe so the pager knows we're done.+                  -- If the pager quits early, we'll receive an EPIPE error; hide that.+                  `catch` \(e::IOException) -> case e of+                    IOError{ioe_type=ResourceVanished, ioe_errno=Just ioe, ioe_handle=Just hdl} | Errno ioe==ePIPE, hdl==hin -> return ()+                    _ -> throwIO e+            void $ waitForProcess p)+          `catch` \(_::IOException) -> putStr s++-- | Test @less@, by running less --version and looking for a nonzero exit, timeout, or stderr output.+-- Uses the provided environment, containing a LESS variable, if any.+-- We do this because various LESS settings can cause some less versions to fail or cause confusing output without failing.+lessIsWorking :: Maybe [(String, String)] -> IO Bool+lessIsWorking mCustomEnv = do+  result <- timeout 300000 $ readCreateProcessWithExitCode (proc "less" ["--version"]){env=mCustomEnv} ""+  return $ case result of+    Nothing -> True  -- Timeout+    Just (exitCode, _, stderrOut) -> exitCode /= ExitSuccess || not (null stderrOut)++-- | Compute the LESS environment variable value that hledger will use for the less pager.+-- This used in runPager when invoking less, and also in the setup command for display.+-- It takes the current HLEDGER_LESS and LESS env var values, and whether we are showing colour on stdout,+-- and returns the adjusted LESS value that should be used. Specifically:+--+-- - If HLEDGER_LESS is defined, we use it in place of the LESS environment variable.+--+-- - Otherwise, if LESS is defined, append some preferred options (lessOptions and maybe lessColourOptions) to it.+--+-- - Otherwise, we set LESS to just use those preferred options.+--+lessVarValue :: Maybe String -> Maybe String -> Bool -> String+lessVarValue mHLEDGER_LESS mLESS usecolor =+  let extralessopts = unwords $ [lessOptions] <> [lessColourOptions | usecolor]+  in case (mHLEDGER_LESS, mLESS) of+       (Just hledgerlessvar, _) -> hledgerlessvar+       (_, Just lessvar) -> if extralessopts `isInfixOf` lessvar then lessvar else unwords [lessvar, extralessopts]+       _ -> extralessopts++-- keep synced: hledger.m4.md > Paging+-- | hledger's preferred less options, which it will append to the user's LESS environment variable.+-- The thinking here is: "Many people don't have their LESS optimised to get the best experience from modern less, as I didn't.+-- Also as they use hledger on different machines, LESS is likely not consistent. +-- So let's add some settings that I have found reasonably robust, compatible, and good for usability.+-- That will help provide a consistent good experience when viewing hledger's long outputs.+-- And power users can prevent this by setting exactly the options they want in HLEDGER_LESS."+-- Here's what they mean: https://manned.org/man/less#head5+--+-- Flags that might break older less versions (causing hledger to fall back to unpaged output) are avoided here.+-- Such as --mouse and --wheel-lines (less >=530, 2018) and --use-color (less >=551, 2019).+-- --hilite-unread (less >=443, 2011) is useful and considered old enough.+--+lessOptions = unwords [+   "--chop-long-lines"+  ,"--hilite-unread"+  ,"--ignore-case"+  ,"--no-init"+  ,"--quit-if-one-screen"+  ,"--shift=8"+  ,"--squeeze-blank-lines"+  ,"--use-backslash"+  ] ++-- | Additional less options to use if we are showing colour on stdout.+lessColourOptions = unwords [+   "--RAW-CONTROL-CHARS"+  ]   -- ANSI colour/styles
Hledger/Utils/Text.hs view
@@ -211,7 +211,7 @@     clip s =       case mmaxwidth of         Just w-          | realLength s > w ->+          | realLength s > max 0 w ->             if rightside               then textTakeWidth (w - T.length ellipsis) s <> ellipsis               else ellipsis <> T.reverse (textTakeWidth (w - T.length ellipsis) $ T.reverse s)@@ -287,5 +287,9 @@      textUnbracket "[([]())]" @?= "[]()"      textUnbracket "[([[[()]]])]" @?= ""      textUnbracket "[([[[(]]])]" @?= "("-     textUnbracket "[([[[)]]])]" @?= ")"+     textUnbracket "[([[[)]]])]" @?= ")",+   testCase "fitText" $ do+     fitText Nothing (Just (-2)) True True "" @?= ""+     fitText Nothing (Just 0) True True "" @?= ""+     fitText Nothing (Just 6) True True "Test Text" @?= "Test.."   ]
Hledger/Write/Beancount.hs view
@@ -43,7 +43,8 @@ import Data.Function ((&)) import Data.List.Extra (groupOnKey) import Data.Bifunctor (first)-import Data.List (sort)+import Data.List (intersperse, sort)+import Data.Maybe (catMaybes)  --- ** doctest setup -- $setup@@ -199,10 +200,11 @@     -- amtwidth at all.     shownAmounts       | elideamount = [mempty]-      | otherwise   = showMixedAmountLinesB displayopts a'+      | otherwise   = map addCostBasis $ showMixedAmountLinesPartsB displayopts a'         where-          displayopts = defaultFmt{ displayZeroCommodity=True, displayForceDecimalMark=True, displayQuotes=False }+          displayopts = defaultFmt{ displayZeroCommodity=True, displayForceDecimalMark=True, displayQuotes=False, displayCostBasis=False }           a' = mapMixedAmount amountToBeancount $ pamount p+          addCostBasis (builder, amt) = builder <> showAmountCostBasisBeancountB displayopts amt     thisamtwidth = maximumBound 0 $ map wbWidth shownAmounts      -- when there is a balance assertion, show it only on the last posting line@@ -314,6 +316,24 @@       where         costToBeancount (TotalCost amt) = TotalCost $ amountToBeancount amt         costToBeancount (UnitCost  amt) = UnitCost  $ amountToBeancount amt++-- | Show an amount's cost basis in Beancount lot syntax: {cost, date, label}+-- Returns a WideBuilder with the formatted cost basis, or mempty if there's no cost basis.+showAmountCostBasisBeancountB :: AmountFormat -> Amount -> WideBuilder+showAmountCostBasisBeancountB afmt amt = case acostbasis amt of+  Nothing -> mempty+  Just CostBasis{cbCost, cbDate, cbLabel} ->+    case parts of+      [] -> mempty+      _  -> WideBuilder (TB.fromString " {") 2 <> contents <> WideBuilder (TB.singleton '}') 1+    where+      parts = catMaybes+        [ fmap (showAmountB afmt . amountToBeancount) cbCost+        , fmap (wbFromText . T.pack . show) cbDate+        , fmap (wbFromText . quote) cbLabel+        ]+      contents = mconcat $ Data.List.intersperse (WideBuilder (TB.fromString ", ") 2) parts+      quote t = "\"" <> t <> "\""  type BeancountCommoditySymbol = CommoditySymbol 
hledger-lib.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.2 --- This file has been generated from package.yaml by hpack version 0.38.1.+-- This file has been generated from package.yaml by hpack version 0.39.1. -- -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        1.51.2+version:        1.52 synopsis:       A library providing the core functionality of hledger description:    This library contains hledger's core functionality.                 It is used by most hledger* packages so that they support the same@@ -124,7 +124,7 @@   hs-source-dirs:       ./   ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind-  cpp-options: -DVERSION="1.51.2"+  cpp-options: -DVERSION="1.52"   build-depends:       Decimal >=0.5.1     , Glob >=0.9@@ -184,7 +184,7 @@   hs-source-dirs:       test   ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind-  cpp-options: -DVERSION="1.51.2"+  cpp-options: -DVERSION="1.52"   build-depends:       Decimal >=0.5.1     , Glob >=0.7@@ -245,7 +245,7 @@   hs-source-dirs:       test   ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind-  cpp-options: -DVERSION="1.51.2"+  cpp-options: -DVERSION="1.52"   build-depends:       Decimal >=0.5.1     , Glob >=0.9