feat:timedot: tagged time logging with letters
This commit is contained in:
parent
6a232e247a
commit
b6a46f637f
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user