feat:timedot: tagged time logging with letters

This commit is contained in:
Simon Michael 2023-11-23 12:29:08 -10:00
parent 6a232e247a
commit b6a46f637f
4 changed files with 118 additions and 36 deletions

View File

@ -52,6 +52,7 @@ module Hledger.Data.Posting (
-- * comment/tag operations -- * comment/tag operations
commentJoin, commentJoin,
commentAddTag, commentAddTag,
commentAddTagUnspaced,
commentAddTagNextLine, commentAddTagNextLine,
-- * arithmetic -- * arithmetic
sumPostings, sumPostings,
@ -611,6 +612,15 @@ commentAddTag c (t,v)
c' = T.stripEnd c c' = T.stripEnd c
tag = t <> ": " <> v tag = t <> ": " <> v
-- | Like commentAddTag, but omits the space after the colon.
commentAddTagUnspaced :: Text -> Tag -> Text
commentAddTagUnspaced c (t,v)
| T.null c' = tag
| otherwise = c' `commentJoin` tag
where
c' = T.stripEnd c
tag = t <> ":" <> v
-- | Add a tag on its own line to a comment, preserving any prior content. -- | Add a tag on its own line to a comment, preserving any prior content.
-- A space is inserted following the colon, before the value. -- A space is inserted following the colon, before the value.
commentAddTagNextLine :: Text -> Tag -> Text commentAddTagNextLine :: Text -> Tag -> Text

View File

@ -53,6 +53,10 @@ import Hledger.Data
import Hledger.Read.Common hiding (emptyorcommentlinep) import Hledger.Read.Common hiding (emptyorcommentlinep)
import Hledger.Utils import Hledger.Utils
import Data.Decimal (roundTo) import Data.Decimal (roundTo)
import Data.Functor ((<&>))
import Data.List (sort)
import Data.List (group)
-- import Text.Megaparsec.Debug (dbg)
--- ** doctest setup --- ** doctest setup
-- $setup -- $setup
@ -126,9 +130,8 @@ dayp = label "timedot day entry" $ do
pos <- getSourcePos pos <- getSourcePos
(date,desc,comment,tags) <- datelinep (date,desc,comment,tags) <- datelinep
commentlinesp commentlinesp
ps <- many $ timedotentryp <* commentlinesp ps <- (many $ timedotentryp <* commentlinesp) <&> concat
endpos <- getSourcePos endpos <- getSourcePos
-- lift $ traceparse' "dayp end"
let t = txnTieKnot $ nulltransaction{ let t = txnTieKnot $ nulltransaction{
tsourcepos = (pos, endpos), tsourcepos = (pos, endpos),
tdate = date, tdate = date,
@ -147,7 +150,6 @@ datelinep = do
date <- datep date <- datep
desc <- T.strip <$> lift descriptionp desc <- T.strip <$> lift descriptionp
(comment, tags) <- lift transactioncommentp (comment, tags) <- lift transactioncommentp
-- lift $ traceparse' "datelinep end"
return (date, desc, comment, tags) return (date, desc, comment, tags)
-- | Zero or more empty lines or hash/semicolon comment lines -- | Zero or more empty lines or hash/semicolon comment lines
@ -165,51 +167,52 @@ commentlinesp = do
-- void $ lift restofline -- void $ lift restofline
-- lift $ traceparse' "orgnondatelinep" -- lift $ traceparse' "orgnondatelinep"
orgheadingprefixp = do orgheadingprefixp = skipSome (char '*') >> skipNonNewlineSpaces1
-- traceparse "orgheadingprefixp"
skipSome (char '*') >> skipNonNewlineSpaces1
-- | Parse a single timedot entry to one (dateless) transaction. -- | Parse a single timedot entry to one (dateless) transaction.
-- @ -- @
-- fos.haskell .... .. -- fos.haskell .... ..
-- @ -- @
timedotentryp :: JournalParser m Posting timedotentryp :: JournalParser m [Posting]
timedotentryp = do timedotentryp = do
lift $ traceparse "timedotentryp" lift $ traceparse "timedotentryp"
notFollowedBy datelinep notFollowedBy datelinep
lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1] lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1]
a <- modifiedaccountnamep a <- modifiedaccountnamep
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
(hours, comment, tags) <- taggedhours <- lift durationsp
try (do (comment0, tags0) <-
(c,ts) <- lift transactioncommentp -- or postingp, but let's not bother supporting date:/date2: lift transactioncommentp -- not postingp, don't bother with date: tags here
return (0, c, ts) <|> (newline >> return ("",[]))
)
<|> (do
h <- lift durationp
(c,ts) <- try (lift transactioncommentp) <|> (newline >> return ("",[]))
return (h,c,ts)
)
mcs <- getDefaultCommodityAndStyle mcs <- getDefaultCommodityAndStyle
let let
(c,s) = case mcs of (c,s) = case mcs of
Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) (Precision 2)}) Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) (Precision 2)})
_ -> ("", amountstyle{asprecision=Precision 2}) _ -> ("", amountstyle{asprecision=Precision 2})
-- lift $ traceparse' "timedotentryp end" ps = [
return $ nullposting{paccount=a nullposting{paccount=a
,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hours, astyle=s} ,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hours, astyle=s}
,ptype=VirtualPosting ,ptype=VirtualPosting
,pcomment=comment ,pcomment=comment
,ptags=tags ,ptags=tags
} }
| (hours,tagval) <- taggedhours
, let tag = ("t",tagval)
, let tags = if T.null tagval then tags0 else tags0 ++ [tag]
, let comment = if T.null tagval then comment0 else comment0 `commentAddTagUnspaced` tag
]
return ps
type Hours = Quantity type Hours = Quantity
durationp :: TextParser m Hours -- | Parse one or more durations in hours, each with an optional tag value
durationp = do -- (or empty string for none).
traceparse "durationp" durationsp :: TextParser m [(Hours,TagValue)]
try numericquantityp <|> dotquantityp durationsp =
-- <* traceparse' "durationp" (dotquantityp <&> \h -> [(h,"")])
<|> (numericquantityp <&> \h -> [(h,"")])
<|> letterquantitiesp
<|> pure [(0,"")]
-- | Parse a duration of seconds, minutes, hours, days, weeks, months or years, -- | Parse a duration of seconds, minutes, hours, days, weeks, months or years,
-- written as a decimal number followed by s, m, h, d, w, mo or y, assuming h -- written as a decimal number followed by s, m, h, d, w, mo or y, assuming h
@ -246,15 +249,33 @@ timeUnits =
,("y",61320) ,("y",61320)
] ]
-- | Parse a quantity written as a line of dots, each representing 0.25. -- | Parse a quantity written as a line of one or more dots,
-- each representing 0.25, ignoring any interspersed spaces
-- after the first dot.
-- @ -- @
-- .... .. -- .... ..
-- @ -- @
dotquantityp :: TextParser m Quantity dotquantityp :: TextParser m Hours
dotquantityp = do dotquantityp = do
-- lift $ traceparse "dotquantityp" -- lift $ traceparse "dotquantityp"
dots <- filter (not.isSpace) <$> many (oneOf (". " :: [Char])) char '.'
return $ fromIntegral (length dots) / 4 dots <- many (oneOf ['.', ' ']) <&> filter (not.isSpace)
return $ fromIntegral (1 + length dots) / 4
-- | Parse a quantity written as a line of one or more letters,
-- each representing 0.25 with a tag "t" whose value is the letter,
-- ignoring any interspersed spaces after the first letter.
letterquantitiesp :: TextParser m [(Hours, TagValue)]
letterquantitiesp =
-- dbg "letterquantitiesp" $
do
letter1 <- letterChar
letters <- many (letterChar <|> spacenonewline) <&> filter (not.isSpace)
let groups =
[ (fromIntegral (length t) / 4, T.singleton c)
| t@(c:_) <- group $ sort $ letter1:letters
]
return groups
-- | XXX new comment line parser, move to Hledger.Read.Common.emptyorcommentlinep -- | XXX new comment line parser, move to Hledger.Read.Common.emptyorcommentlinep
-- Parse empty lines, all-blank lines, and lines beginning with any of the provided -- Parse empty lines, all-blank lines, and lines beginning with any of the provided

View File

@ -4218,6 +4218,12 @@ After the date line are zero or more time postings, consisting of:
These are the dots in "timedot". These are the dots in "timedot".
Spaces are ignored and can be used for grouping/alignment. Spaces are ignored and can be used for grouping/alignment.
- one or more letters. These are like dots but they also generate
a tag `t:` (short for "type") with the letter as its value,
and a separate posting for each of the values.
This provides a second dimension of categorisation,
viewable in reports with `--pivot t`.
- **An optional comment** following a semicolon (a hledger-style [posting comment](#posting-comments)). - **An optional comment** following a semicolon (a hledger-style [posting comment](#posting-comments)).
There is some flexibility to help with keeping time log data and notes in the same file: There is some flexibility to help with keeping time log data and notes in the same file:
@ -4282,6 +4288,37 @@ Balance changes in 2016-02-01-2016-02-03:
|| 7.75 2.25 8.00 || 7.75 2.25 8.00
``` ```
Letters:
```timedot
# Activity types: cleanup, enhancement, learning, support
2023-11-01
work:adm ccecces
```
```journal
$ hledger -f a.timedot print
2023-11-01
(work:adm) 1 ; t:c
(work:adm) 0.5 ; t:e
(work:adm) 0.25 ; t:s
```
```shell
$ hledger -f a.timedot bal
1.75 work:adm
--------------------
1.75
```
```shell
$ hledger -f a.timedot bal --pivot t
1.00 c
0.50 e
0.25 s
--------------------
1.75
```
Org: Org:
```timedot ```timedot

View File

@ -16,6 +16,10 @@ fos:haskell .... ; a posting comment and posting-tag:
; more posting comment lines ? currently ignored ; more posting comment lines ? currently ignored
per:admin .... per:admin ....
2023-01-02
a ; no quantity means zero
b aabbaca ; letter "dots" are tagged with t:LETTER
** 2023-01-02 ; dates are allowed to be org headings ** 2023-01-02 ; dates are allowed to be org headings
# ** 1. The above timedot is converted to these transactions. # ** 1. The above timedot is converted to these transactions.
@ -28,19 +32,29 @@ $ hledger -ftimedot:- print
(fos:haskell) 1.00 ; a posting comment and posting-tag: (fos:haskell) 1.00 ; a posting comment and posting-tag:
(per:admin) 1.00 (per:admin) 1.00
2023-01-02 *
(a) 0.00 ; no quantity means zero
(b) 1.00 ; letter "dots" are tagged with t:LETTER, t:a
(b) 0.50 ; letter "dots" are tagged with t:LETTER, t:b
(b) 0.25 ; letter "dots" are tagged with t:LETTER, t:c
2023-01-02 * ; dates are allowed to be org headings 2023-01-02 * ; dates are allowed to be org headings
>= >=
# ** 2. And this register. # ** 2. And this register.
$ hledger -ftimedot:- reg $ hledger -ftimedot:- reg -w80
2023-01-01 transaction descr.. (biz:research) 1.00 1.00 2023-01-01 transaction descr.. (biz:research) 1.00 1.00
(inc:client1) 1.50 2.50 (inc:client1) 1.50 2.50
2023-01-01 different transac.. (fos:haskell) 1.00 3.50 2023-01-01 different transac.. (fos:haskell) 1.00 3.50
(per:admin) 1.00 4.50 (per:admin) 1.00 4.50
2023-01-02 (a) 0 4.50
(b) 1.00 5.50
(b) 0.50 6.00
(b) 0.25 6.25
# ** 3. Tags are recognised. Account aliases are applied. # ** 3. Tags are recognised. Account aliases are applied.
$ hledger -ftimedot:- reg tag:posting-tag --alias fos:haskell=λ $ hledger -ftimedot:- reg -w80 tag:posting-tag --alias fos:haskell=λ
2023-01-01 different transac.. (λ) 1.00 1.00 2023-01-01 different transac.. (λ) 1.00 1.00
# ** 4. Each of these formats is printed as exactly a quarter hour. # ** 4. Each of these formats is printed as exactly a quarter hour.