mirror of
https://github.com/simonmichael/hledger.git
synced 2025-01-06 02:23:46 +03:00
173 lines
4.7 KiB
Haskell
173 lines
4.7 KiB
Haskell
{-|
|
|
|
|
A reader for the "timedot" file format.
|
|
Example:
|
|
|
|
@
|
|
#DATE
|
|
#ACCT DOTS # Each dot represents 15m, spaces are ignored
|
|
#ACCT 8 # numbers with or without a following h represent hours
|
|
#ACCT 5m # numbers followed by m represent minutes
|
|
|
|
# on 2/1, 1h was spent on FOSS haskell work, 0.25h on research, etc.
|
|
2/1
|
|
fos.haskell .... ..
|
|
biz.research .
|
|
inc.client1 .... .... .... .... .... ....
|
|
|
|
2/2
|
|
biz.research .
|
|
inc.client1 .... .... ..
|
|
|
|
@
|
|
|
|
-}
|
|
|
|
{-# LANGUAGE OverloadedStrings, PackageImports #-}
|
|
|
|
module Hledger.Read.TimedotReader (
|
|
-- * Reader
|
|
reader,
|
|
-- * Misc other exports
|
|
timedotfilep,
|
|
)
|
|
where
|
|
import Prelude ()
|
|
import "base-compat-batteries" Prelude.Compat
|
|
import Control.Monad
|
|
import Control.Monad.Except (ExceptT)
|
|
import Control.Monad.State.Strict
|
|
import Data.Char (isSpace)
|
|
import Data.List (foldl')
|
|
import Data.Maybe
|
|
import Data.Text (Text)
|
|
import Text.Megaparsec hiding (parse)
|
|
import Text.Megaparsec.Char
|
|
|
|
import Hledger.Data
|
|
import Hledger.Read.Common
|
|
import Hledger.Utils hiding (traceParse)
|
|
|
|
-- easier to toggle this here sometimes
|
|
-- import qualified Hledger.Utils (parsertrace)
|
|
-- parsertrace = Hledger.Utils.parsertrace
|
|
traceParse :: Monad m => a -> m a
|
|
traceParse = return
|
|
|
|
reader :: Reader
|
|
reader = Reader
|
|
{rFormat = "timedot"
|
|
,rExtensions = ["timedot"]
|
|
,rParser = parse
|
|
,rExperimental = False
|
|
}
|
|
|
|
-- | Parse and post-process a "Journal" from the timedot format, or give an error.
|
|
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
|
|
parse = parseAndFinaliseJournal' timedotfilep
|
|
|
|
timedotfilep :: JournalParser m ParsedJournal
|
|
timedotfilep = do many timedotfileitemp
|
|
eof
|
|
get
|
|
where
|
|
timedotfileitemp :: JournalParser m ()
|
|
timedotfileitemp = do
|
|
traceParse "timedotfileitemp"
|
|
choice [
|
|
void $ lift emptyorcommentlinep
|
|
,timedotdayp >>= \ts -> modify' (addTransactions ts)
|
|
] <?> "timedot day entry, or default year or comment line or blank line"
|
|
|
|
addTransactions :: [Transaction] -> Journal -> Journal
|
|
addTransactions ts j = foldl' (flip ($)) j (map addTransaction ts)
|
|
|
|
-- | Parse timedot day entries to zero or more time transactions for that day.
|
|
-- @
|
|
-- 2/1
|
|
-- fos.haskell .... ..
|
|
-- biz.research .
|
|
-- inc.client1 .... .... .... .... .... ....
|
|
-- @
|
|
timedotdayp :: JournalParser m [Transaction]
|
|
timedotdayp = do
|
|
traceParse " timedotdayp"
|
|
d <- datep <* lift eolof
|
|
es <- catMaybes <$> many (const Nothing <$> try (lift emptyorcommentlinep) <|>
|
|
Just <$> (notFollowedBy datep >> timedotentryp))
|
|
return $ map (\t -> t{tdate=d}) es -- <$> many timedotentryp
|
|
|
|
-- | Parse a single timedot entry to one (dateless) transaction.
|
|
-- @
|
|
-- fos.haskell .... ..
|
|
-- @
|
|
timedotentryp :: JournalParser m Transaction
|
|
timedotentryp = do
|
|
traceParse " timedotentryp"
|
|
pos <- genericSourcePos <$> getSourcePos
|
|
lift (skipMany spacenonewline)
|
|
a <- modifiedaccountnamep
|
|
lift (skipMany spacenonewline)
|
|
hours <-
|
|
try (lift followingcommentp >> return 0)
|
|
<|> (timedotdurationp <*
|
|
(try (lift followingcommentp) <|> (newline >> return "")))
|
|
let t = nulltransaction{
|
|
tsourcepos = pos,
|
|
tstatus = Cleared,
|
|
tpostings = [
|
|
nullposting{paccount=a
|
|
,pamount=Mixed [setAmountPrecision 2 $ num hours] -- don't assume hours; do set precision to 2
|
|
,ptype=VirtualPosting
|
|
,ptransaction=Just t
|
|
}
|
|
]
|
|
}
|
|
return t
|
|
|
|
timedotdurationp :: JournalParser m Quantity
|
|
timedotdurationp = try timedotnumericp <|> timedotdotsp
|
|
|
|
-- | 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
|
|
-- if there is no unit. Returns the duration as hours, assuming
|
|
-- 1m = 60s, 1h = 60m, 1d = 24h, 1w = 7d, 1mo = 30d, 1y=365d.
|
|
-- @
|
|
-- 1.5
|
|
-- 1.5h
|
|
-- 90m
|
|
-- @
|
|
timedotnumericp :: JournalParser m Quantity
|
|
timedotnumericp = do
|
|
(q, _, _, _) <- lift $ numberp Nothing
|
|
msymbol <- optional $ choice $ map (string . fst) timeUnits
|
|
lift (skipMany spacenonewline)
|
|
let q' =
|
|
case msymbol of
|
|
Nothing -> q
|
|
Just sym ->
|
|
case lookup sym timeUnits of
|
|
Just mult -> q * mult
|
|
Nothing -> q -- shouldn't happen.. ignore
|
|
return q'
|
|
|
|
-- (symbol, equivalent in hours).
|
|
timeUnits =
|
|
[("s",2.777777777777778e-4)
|
|
,("mo",5040) -- before "m"
|
|
,("m",1.6666666666666666e-2)
|
|
,("h",1)
|
|
,("d",24)
|
|
,("w",168)
|
|
,("y",61320)
|
|
]
|
|
|
|
-- | Parse a quantity written as a line of dots, each representing 0.25.
|
|
-- @
|
|
-- .... ..
|
|
-- @
|
|
timedotdotsp :: JournalParser m Quantity
|
|
timedotdotsp = do
|
|
dots <- filter (not.isSpace) <$> many (oneOf (". " :: [Char]))
|
|
return $ (/4) $ fromIntegral $ length dots
|