feat: register: add --sort as in ledger
This commit is contained in:
parent
c24c09337c
commit
00eb0aa16b
@ -15,15 +15,19 @@ module Hledger.Reports.PostingsReport (
|
|||||||
PostingsReportItem,
|
PostingsReportItem,
|
||||||
postingsReport,
|
postingsReport,
|
||||||
mkpostingsReportItem,
|
mkpostingsReportItem,
|
||||||
|
SortSpec,
|
||||||
|
getSortSpec,
|
||||||
|
|
||||||
-- * Tests
|
-- * Tests
|
||||||
tests_PostingsReport
|
tests_PostingsReport
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Data.List (nub, sortOn)
|
import Data.List (nub, sortBy, sortOn, stripPrefix, isPrefixOf)
|
||||||
|
import Data.List.Split (splitOn)
|
||||||
import Data.List.Extra (nubSort)
|
import Data.List.Extra (nubSort)
|
||||||
import Data.Maybe (isJust, isNothing)
|
import Data.Maybe (isJust, isNothing, fromMaybe)
|
||||||
|
import Data.Ord
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time.Calendar (Day)
|
import Data.Time.Calendar (Day)
|
||||||
import Safe (headMay)
|
import Safe (headMay)
|
||||||
@ -61,15 +65,44 @@ instance HasAmounts PostingsReportItem where
|
|||||||
-- with a tuple.
|
-- with a tuple.
|
||||||
type SummaryPosting = (Posting, Period)
|
type SummaryPosting = (Posting, Period)
|
||||||
|
|
||||||
|
-- Possible value expressions taken by the --sort command
|
||||||
|
-- Each of these takes a bool, which shows if it has been inverted
|
||||||
|
-- (True -> has been inverted, reverse the order)
|
||||||
|
data ValueExp
|
||||||
|
= Date' Bool
|
||||||
|
| Account' Bool
|
||||||
|
| Amount' Bool
|
||||||
|
deriving (Show)
|
||||||
|
type SortSpec = [ValueExp]
|
||||||
|
|
||||||
|
-- Load a SortSpec from the argument given to --sort
|
||||||
|
-- If there is no spec given, then sort by [Date' False] by default
|
||||||
|
getSortSpec :: RawOpts -> SortSpec
|
||||||
|
getSortSpec opts =
|
||||||
|
let opt = maybestringopt "sort" opts
|
||||||
|
optParser s =
|
||||||
|
let terms = map strip $ splitOn "," s
|
||||||
|
termParser t = case trimmed of
|
||||||
|
"date" -> Date' isNegated
|
||||||
|
"account" -> Account' isNegated
|
||||||
|
"amount" -> Amount' isNegated
|
||||||
|
_ -> error' $ "unsupported value expression '" ++ t ++ "' given to --sort"
|
||||||
|
where isNegated = isPrefixOf "-" t
|
||||||
|
trimmed = fromMaybe t (stripPrefix "-" t)
|
||||||
|
in map termParser terms
|
||||||
|
in maybe [Date' False] optParser opt
|
||||||
|
|
||||||
-- | Select postings from the journal and add running balance and other
|
-- | Select postings from the journal and add running balance and other
|
||||||
-- information to make a postings report. Used by eg hledger's register command.
|
-- information to make a postings report. Used by eg hledger's register command.
|
||||||
postingsReport :: ReportSpec -> Journal -> PostingsReport
|
postingsReport :: ReportSpec -> Maybe SortSpec -> Journal -> PostingsReport
|
||||||
postingsReport rspec@ReportSpec{_rsReportOpts=ropts@ReportOpts{..}} j = items
|
postingsReport rspec@ReportSpec{_rsReportOpts=ropts@ReportOpts{..}} sortspec j = items
|
||||||
where
|
where
|
||||||
(reportspan, colspans) = reportSpanBothDates j rspec
|
(reportspan, colspans) = reportSpanBothDates j rspec
|
||||||
whichdate = whichDate ropts
|
whichdate = whichDate ropts
|
||||||
mdepth = queryDepth $ _rsQuery rspec
|
mdepth = queryDepth $ _rsQuery rspec
|
||||||
multiperiod = interval_ /= NoInterval
|
multiperiod = interval_ /= NoInterval
|
||||||
|
-- Sort by the date (or date2) field if nothing else is specified
|
||||||
|
sspec = fromMaybe [Date' False] sortspec
|
||||||
|
|
||||||
-- postings to be included in the report, and similarly-matched postings before the report start date
|
-- postings to be included in the report, and similarly-matched postings before the report start date
|
||||||
(precedingps, reportps) = matchedPostingsBeforeAndDuring rspec j reportspan
|
(precedingps, reportps) = matchedPostingsBeforeAndDuring rspec j reportspan
|
||||||
@ -82,10 +115,12 @@ postingsReport rspec@ReportSpec{_rsReportOpts=ropts@ReportOpts{..}} j = items
|
|||||||
summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans
|
summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans
|
||||||
showempty = empty_ || average_
|
showempty = empty_ || average_
|
||||||
|
|
||||||
|
sortedps = sortPostings ropts sspec displayps
|
||||||
|
|
||||||
-- Posting report items ready for display.
|
-- Posting report items ready for display.
|
||||||
items =
|
items =
|
||||||
dbg4 "postingsReport items" $
|
dbg4 "postingsReport items" $
|
||||||
postingsReportItems displayps (nullposting,Nothing) whichdate mdepth startbal runningcalc startnum
|
postingsReportItems sortedps (nullposting,Nothing) whichdate mdepth startbal runningcalc startnum
|
||||||
where
|
where
|
||||||
-- In historical mode we'll need a starting balance, which we
|
-- In historical mode we'll need a starting balance, which we
|
||||||
-- may be converting to value per hledger_options.m4.md "Effect
|
-- may be converting to value per hledger_options.m4.md "Effect
|
||||||
@ -110,6 +145,24 @@ registerRunningCalculationFn ropts
|
|||||||
| average_ ropts = \i avg amt -> avg `maPlus` divideMixedAmount (fromIntegral i) (amt `maMinus` avg)
|
| average_ ropts = \i avg amt -> avg `maPlus` divideMixedAmount (fromIntegral i) (amt `maMinus` avg)
|
||||||
| otherwise = \_ bal amt -> bal `maPlus` amt
|
| otherwise = \_ bal amt -> bal `maPlus` amt
|
||||||
|
|
||||||
|
-- | Sort two postings by the current list of value expressions (given in SortSpec).
|
||||||
|
comparePostings :: ReportOpts -> SortSpec -> (Posting, Maybe Period) -> (Posting, Maybe Period) -> Ordering
|
||||||
|
comparePostings _ [] _ _ = EQ
|
||||||
|
comparePostings ropts (ex:es) (a, pa) (b, pb) =
|
||||||
|
let comparison = case ex of
|
||||||
|
Amount' False -> compare (pamount a) (pamount b)
|
||||||
|
Account' False -> compare (paccount a) (paccount b)
|
||||||
|
Date' False -> compare (postingDateOrDate2 (whichDate ropts) a) (postingDateOrDate2 (whichDate ropts) b)
|
||||||
|
Amount' True -> compare (Down (pamount a)) (Down (pamount b))
|
||||||
|
Account' True -> compare (Down (paccount a)) (Down (paccount b))
|
||||||
|
Date' True -> compare (Down (postingDateOrDate2 (whichDate ropts) a)) (Down (postingDateOrDate2 (whichDate ropts) b))
|
||||||
|
in
|
||||||
|
if comparison == EQ then comparePostings ropts es (a, pa) (b, pb) else comparison
|
||||||
|
|
||||||
|
-- | Sort postings by the current SortSpec.
|
||||||
|
sortPostings :: ReportOpts -> SortSpec -> [(Posting, Maybe Period)] -> [(Posting, Maybe Period)]
|
||||||
|
sortPostings ropts sspec = sortBy (comparePostings ropts sspec)
|
||||||
|
|
||||||
-- | Find postings matching a given query, within a given date span,
|
-- | Find postings matching a given query, within a given date span,
|
||||||
-- and also any similarly-matched postings before that date span.
|
-- and also any similarly-matched postings before that date span.
|
||||||
-- Date restrictions and depth restrictions in the query are ignored.
|
-- Date restrictions and depth restrictions in the query are ignored.
|
||||||
@ -220,8 +273,11 @@ negatePostingAmount = postingTransformAmount negate
|
|||||||
|
|
||||||
tests_PostingsReport = testGroup "PostingsReport" [
|
tests_PostingsReport = testGroup "PostingsReport" [
|
||||||
|
|
||||||
|
let sspec = Just [Date' False]
|
||||||
|
in
|
||||||
|
|
||||||
testCase "postingsReport" $ do
|
testCase "postingsReport" $ do
|
||||||
let (query, journal) `gives` n = (length $ postingsReport defreportspec{_rsQuery=query} journal) @?= n
|
let (query, journal) `gives` n = (length $ postingsReport defreportspec{_rsQuery=query} sspec journal) @?= n
|
||||||
-- with the query specified explicitly
|
-- with the query specified explicitly
|
||||||
(Any, nulljournal) `gives` 0
|
(Any, nulljournal) `gives` 0
|
||||||
(Any, samplejournal) `gives` 13
|
(Any, samplejournal) `gives` 13
|
||||||
@ -230,10 +286,10 @@ tests_PostingsReport = testGroup "PostingsReport" [
|
|||||||
(And [Depth 1, StatusQ Cleared, Acct (toRegex' "expenses")], samplejournal) `gives` 2
|
(And [Depth 1, StatusQ Cleared, Acct (toRegex' "expenses")], samplejournal) `gives` 2
|
||||||
(And [And [Depth 1, StatusQ Cleared], Acct (toRegex' "expenses")], samplejournal) `gives` 2
|
(And [And [Depth 1, StatusQ Cleared], Acct (toRegex' "expenses")], samplejournal) `gives` 2
|
||||||
-- with query and/or command-line options
|
-- with query and/or command-line options
|
||||||
(length $ postingsReport defreportspec samplejournal) @?= 13
|
(length $ postingsReport defreportspec sspec samplejournal) @?= 13
|
||||||
(length $ postingsReport defreportspec{_rsReportOpts=defreportopts{interval_=Months 1}} samplejournal) @?= 11
|
(length $ postingsReport defreportspec{_rsReportOpts=defreportopts{interval_=Months 1}} sspec samplejournal) @?= 11
|
||||||
(length $ postingsReport defreportspec{_rsReportOpts=defreportopts{interval_=Months 1, empty_=True}} samplejournal) @?= 20
|
(length $ postingsReport defreportspec{_rsReportOpts=defreportopts{interval_=Months 1, empty_=True}} sspec samplejournal) @?= 20
|
||||||
(length $ postingsReport defreportspec{_rsQuery=Acct $ toRegex' "assets:bank:checking"} samplejournal) @?= 5
|
(length $ postingsReport defreportspec{_rsQuery=Acct $ toRegex' "assets:bank:checking"} sspec samplejournal) @?= 5
|
||||||
|
|
||||||
-- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
|
-- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
|
||||||
-- [(Just (fromGregorian 2008 01 01,"income"),assets:bank:checking $1,$1)
|
-- [(Just (fromGregorian 2008 01 01,"income"),assets:bank:checking $1,$1)
|
||||||
|
|||||||
@ -147,6 +147,7 @@ library
|
|||||||
, pretty-simple >4 && <5
|
, pretty-simple >4 && <5
|
||||||
, regex-tdfa
|
, regex-tdfa
|
||||||
, safe >=0.3.20
|
, safe >=0.3.20
|
||||||
|
, split >=0.1
|
||||||
, tabular >=0.2
|
, tabular >=0.2
|
||||||
, tasty >=1.2.3
|
, tasty >=1.2.3
|
||||||
, tasty-hunit >=0.10.0.2
|
, tasty-hunit >=0.10.0.2
|
||||||
|
|||||||
@ -460,7 +460,7 @@ ensureOneNewlineTerminated = (<>"\n") . T.dropWhileEnd (=='\n')
|
|||||||
registerFromString :: T.Text -> IO TL.Text
|
registerFromString :: T.Text -> IO TL.Text
|
||||||
registerFromString s = do
|
registerFromString s = do
|
||||||
j <- readJournal' s
|
j <- readJournal' s
|
||||||
return . postingsReportAsText opts $ postingsReport rspec j
|
return . postingsReportAsText opts $ postingsReport rspec Nothing j
|
||||||
where
|
where
|
||||||
ropts = defreportopts{empty_=True}
|
ropts = defreportopts{empty_=True}
|
||||||
rspec = defreportspec{_rsReportOpts=ropts}
|
rspec = defreportspec{_rsReportOpts=ropts}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ registermode = hledgerCommandMode
|
|||||||
("fuzzy search for one recent posting with description closest to "++arg)
|
("fuzzy search for one recent posting with description closest to "++arg)
|
||||||
,flagNone ["related","r"] (setboolopt "related") "show postings' siblings instead"
|
,flagNone ["related","r"] (setboolopt "related") "show postings' siblings instead"
|
||||||
,flagNone ["invert"] (setboolopt "invert") "display all amounts with reversed sign"
|
,flagNone ["invert"] (setboolopt "invert") "display all amounts with reversed sign"
|
||||||
|
,flagReq ["sort"] (\s opts -> Right $ setopt "sort" s opts) "FlagHelp" "Help"
|
||||||
,flagReq ["width","w"] (\s opts -> Right $ setopt "width" s opts) "N"
|
,flagReq ["width","w"] (\s opts -> Right $ setopt "width" s opts) "N"
|
||||||
("set output width (default: " ++
|
("set output width (default: " ++
|
||||||
#ifdef mingw32_HOST_OS
|
#ifdef mingw32_HOST_OS
|
||||||
@ -85,7 +86,7 @@ register opts@CliOpts{rawopts_=rawopts, reportspec_=rspec} j
|
|||||||
| otherwise = writeOutputLazyText opts $ render $ styleAmounts styles rpt
|
| otherwise = writeOutputLazyText opts $ render $ styleAmounts styles rpt
|
||||||
where
|
where
|
||||||
styles = journalCommodityStylesWith HardRounding j
|
styles = journalCommodityStylesWith HardRounding j
|
||||||
rpt = postingsReport rspec j
|
rpt = postingsReport rspec (Just (getSortSpec rawopts)) j
|
||||||
render | fmt=="txt" = postingsReportAsText opts
|
render | fmt=="txt" = postingsReportAsText opts
|
||||||
| fmt=="csv" = printCSV . postingsReportAsCsv
|
| fmt=="csv" = printCSV . postingsReportAsCsv
|
||||||
| fmt=="tsv" = printTSV . postingsReportAsCsv
|
| fmt=="tsv" = printTSV . postingsReportAsCsv
|
||||||
@ -260,7 +261,7 @@ tests_Register = testGroup "Register" [
|
|||||||
testCase "unicode in register layout" $ do
|
testCase "unicode in register layout" $ do
|
||||||
j <- readJournal' "2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
j <- readJournal' "2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
||||||
let rspec = defreportspec
|
let rspec = defreportspec
|
||||||
(TL.unpack . postingsReportAsText defcliopts $ postingsReport rspec j)
|
(TL.unpack . postingsReportAsText defcliopts $ postingsReport rspec Nothing j)
|
||||||
@?=
|
@?=
|
||||||
unlines
|
unlines
|
||||||
["2009-01-01 медвежья шкура расходы:покупки 100 100"
|
["2009-01-01 медвежья шкура расходы:покупки 100 100"
|
||||||
|
|||||||
76
hledger/test/register/sort.test
Normal file
76
hledger/test/register/sort.test
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# * register command with --sort flag
|
||||||
|
|
||||||
|
# ** 1. --sort with non-date sorts by the correct account
|
||||||
|
<
|
||||||
|
2024-01-01 Demo
|
||||||
|
a 1
|
||||||
|
b
|
||||||
|
|
||||||
|
2024-01-02 Other
|
||||||
|
c 1
|
||||||
|
a
|
||||||
|
$ hledger -f - register --sort account
|
||||||
|
2024-01-01 Demo a 1 1
|
||||||
|
2024-01-02 Other a -1 0
|
||||||
|
2024-01-01 Demo b -1 -1
|
||||||
|
2024-01-02 Other c 1 0
|
||||||
|
|
||||||
|
# ** 2. --sort with two expressions sorts correctly
|
||||||
|
<
|
||||||
|
2024-01-01 Demo
|
||||||
|
a 1
|
||||||
|
b
|
||||||
|
|
||||||
|
2024-01-02 Other
|
||||||
|
c 1
|
||||||
|
a
|
||||||
|
$ hledger -f - register --sort account,amount
|
||||||
|
2024-01-02 Other a -1 -1
|
||||||
|
2024-01-01 Demo a 1 0
|
||||||
|
b -1 -1
|
||||||
|
2024-01-02 Other c 1 0
|
||||||
|
|
||||||
|
# ** 3. --sort with negation reverses
|
||||||
|
<
|
||||||
|
2024-01-01 Demo
|
||||||
|
a 1
|
||||||
|
b
|
||||||
|
|
||||||
|
2024-01-02 Other
|
||||||
|
c 1
|
||||||
|
a
|
||||||
|
$ hledger -f - register --sort -account
|
||||||
|
2024-01-02 Other c 1 1
|
||||||
|
2024-01-01 Demo b -1 0
|
||||||
|
a 1 1
|
||||||
|
2024-01-02 Other a -1 0
|
||||||
|
|
||||||
|
# ** 4. --sort with negation and multiple accounts
|
||||||
|
<
|
||||||
|
2024-01-01 Demo
|
||||||
|
a 1
|
||||||
|
b
|
||||||
|
|
||||||
|
2024-01-02 Other
|
||||||
|
c 1
|
||||||
|
a
|
||||||
|
$ hledger -f - register --sort amount,-account
|
||||||
|
2024-01-01 Demo b -1 -1
|
||||||
|
2024-01-02 Other a -1 -2
|
||||||
|
c 1 -1
|
||||||
|
2024-01-01 Demo a 1 0
|
||||||
|
|
||||||
|
# ** 5. --sort with date, reversed
|
||||||
|
<
|
||||||
|
2024-01-01 Demo
|
||||||
|
a 1
|
||||||
|
b
|
||||||
|
|
||||||
|
2024-01-02 Other
|
||||||
|
c 1
|
||||||
|
a
|
||||||
|
$ hledger -f - register --sort -date
|
||||||
|
2024-01-02 Other c 1 1
|
||||||
|
a -1 0
|
||||||
|
2024-01-01 Demo a 1 1
|
||||||
|
b -1 0
|
||||||
Loading…
Reference in New Issue
Block a user