{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
module Hledger.Read (
defaultJournal,
defaultJournalSafely,
defaultJournalWith,
defaultJournalWithSafely,
defaultJournalPath,
defaultJournalPathSafely,
requireJournalFileExists,
ensureJournalFileExists,
journalEnvVar,
journalDefaultFilename,
runExceptT,
readJournal,
readJournalFile,
readJournalFiles,
readJournalFilesAndLatestDates,
readJournal',
readJournal'',
readJournalFile',
readJournalFiles',
orDieTrying,
saveLatestDates,
saveLatestDatesForFiles,
isWindowsUnsafeDotPath,
JournalReader.tmpostingrulep,
findReader,
splitReaderPrefix,
runJournalParser,
module Hledger.Read.Common,
module Hledger.Read.InputOptions,
tests_Read,
) where
import Control.Exception qualified as C
import Control.Monad (unless, when, forM, (>=>))
import "mtl" Control.Monad.Except (ExceptT(..), runExceptT, liftEither)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Default (def)
import Data.Foldable (asum)
import Data.List (group, sort, sortBy)
import Data.List.NonEmpty (nonEmpty)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Ord (comparing)
import Data.Semigroup (sconcat)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Time (Day)
import Safe (headDef, headMay)
import System.Directory (doesFileExist)
import System.Environment (getEnv)
import System.FilePath ((<.>), (</>), splitDirectories, splitFileName, takeFileName)
import System.Info (os)
import System.IO (Handle, hPutStrLn, stderr)
import Hledger.Data.Dates (getCurrentDay, parsedate, showDate)
import Hledger.Data.Types
import Hledger.Read.Common
import Hledger.Read.InputOptions
import Hledger.Read.JournalReader as JournalReader
import Hledger.Read.CsvReader (tests_CsvReader)
import Hledger.Read.RulesReader (tests_RulesReader)
import Hledger.Utils
import Prelude hiding (getContents, writeFile)
import Hledger.Data.JournalChecks (journalStrictChecks)
import Text.Printf (printf)
import Hledger.Data.Journal (journalNumberTransactions)
journalEnvVar :: String
journalEnvVar = String
"LEDGER_FILE"
journalDefaultFilename :: String
journalDefaultFilename = String
".hledger.journal"
defaultJournal :: IO Journal
defaultJournal :: IO Journal
defaultJournal = IO (Either String Journal)
defaultJournalSafely IO (Either String Journal)
-> (Either String Journal -> IO Journal) -> IO Journal
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= (String -> IO Journal)
-> (Journal -> IO Journal) -> Either String Journal -> IO Journal
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either String -> IO Journal
forall a. String -> a
error' Journal -> IO Journal
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return
defaultJournalSafely :: IO (Either String Journal)
defaultJournalSafely :: IO (Either String Journal)
defaultJournalSafely = InputOpts -> IO (Either String Journal)
defaultJournalWithSafely InputOpts
definputopts
defaultJournalWith :: InputOpts -> IO Journal
defaultJournalWith :: InputOpts -> IO Journal
defaultJournalWith InputOpts
iopts = InputOpts -> IO (Either String Journal)
defaultJournalWithSafely InputOpts
iopts IO (Either String Journal)
-> (Either String Journal -> IO Journal) -> IO Journal
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= (String -> IO Journal)
-> (Journal -> IO Journal) -> Either String Journal -> IO Journal
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either String -> IO Journal
forall a. String -> a
error' Journal -> IO Journal
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return
defaultJournalWithSafely :: InputOpts -> IO (Either String Journal)
defaultJournalWithSafely :: InputOpts -> IO (Either String Journal)
defaultJournalWithSafely InputOpts
iopts = (do
String
f <- IO String
defaultJournalPath
ExceptT String IO Journal -> IO (Either String Journal)
forall e (m :: * -> *) a. ExceptT e m a -> m (Either e a)
runExceptT (ExceptT String IO Journal -> IO (Either String Journal))
-> ExceptT String IO Journal -> IO (Either String Journal)
forall a b. (a -> b) -> a -> b
$ InputOpts -> String -> ExceptT String IO Journal
readJournalFile InputOpts
iopts String
f
)
IO (Either String Journal)
-> [Handler (Either String Journal)] -> IO (Either String Journal)
forall a. IO a -> [Handler a] -> IO a
`C.catches` [
(ErrorCall -> IO (Either String Journal))
-> Handler (Either String Journal)
forall a e. Exception e => (e -> IO a) -> Handler a
C.Handler (\(ErrorCall
e :: C.ErrorCall) -> Either String Journal -> IO (Either String Journal)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String Journal -> IO (Either String Journal))
-> Either String Journal -> IO (Either String Journal)
forall a b. (a -> b) -> a -> b
$ String -> Either String Journal
forall a b. a -> Either a b
Left (String -> Either String Journal)
-> String -> Either String Journal
forall a b. (a -> b) -> a -> b
$ ErrorCall -> String
forall a. Show a => a -> String
show ErrorCall
e)
,(IOException -> IO (Either String Journal))
-> Handler (Either String Journal)
forall a e. Exception e => (e -> IO a) -> Handler a
C.Handler (\(IOException
e :: C.IOException) -> Either String Journal -> IO (Either String Journal)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String Journal -> IO (Either String Journal))
-> Either String Journal -> IO (Either String Journal)
forall a b. (a -> b) -> a -> b
$ String -> Either String Journal
forall a b. a -> Either a b
Left (String -> Either String Journal)
-> String -> Either String Journal
forall a b. (a -> b) -> a -> b
$ IOException -> String
forall a. Show a => a -> String
show IOException
e)
]
defaultJournalPath :: IO String
defaultJournalPath :: IO String
defaultJournalPath = do
String
ledgerfile <- String -> IO String
getEnv String
journalEnvVar IO String -> (IOException -> IO String) -> IO String
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`C.catch` (\(IOException
_::C.IOException) -> String -> IO String
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return String
"")
if String -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null String
ledgerfile
then do
String
homedir <- String -> Maybe String -> String
forall a. a -> Maybe a -> a
fromMaybe String
"" (Maybe String -> String) -> IO (Maybe String) -> IO String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO (Maybe String)
getHomeSafe
let defaultfile :: String
defaultfile = String
homedir String -> String -> String
</> String
journalDefaultFilename
Bool
exists <- String -> IO Bool
doesFileExist String
defaultfile
if Bool
exists then String -> IO String
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return String
defaultfile
else String -> IO String
forall a. String -> a
error' (String -> IO String) -> String -> IO String
forall a b. (a -> b) -> a -> b
$ String
"neither LEDGER_FILE nor \"" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
defaultfile String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\" was found"
else do
Maybe String
mf <- [String] -> Maybe String
forall a. [a] -> Maybe a
headMay ([String] -> Maybe String) -> IO [String] -> IO (Maybe String)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> String -> IO [String]
expandGlob String
"." String
ledgerfile IO [String] -> (IOException -> IO [String]) -> IO [String]
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`C.catch` (\(IOException
_::C.IOException) -> [String] -> IO [String]
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return [])
case Maybe String
mf of
Just String
f -> String -> IO String
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return String
f
Maybe String
Nothing -> String -> IO String
forall a. String -> a
error' (String -> IO String) -> String -> IO String
forall a b. (a -> b) -> a -> b
$ String
"LEDGER_FILE points to nonexistent \"" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
ledgerfile String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\""
defaultJournalPathSafely :: IO (Either String String)
defaultJournalPathSafely :: IO (Either String String)
defaultJournalPathSafely = (do
String
f <- IO String
defaultJournalPath
Either String String -> IO (Either String String)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String String -> IO (Either String String))
-> Either String String -> IO (Either String String)
forall a b. (a -> b) -> a -> b
$ String -> Either String String
forall a b. b -> Either a b
Right String
f
)
IO (Either String String)
-> [Handler (Either String String)] -> IO (Either String String)
forall a. IO a -> [Handler a] -> IO a
`C.catches` [
(ErrorCall -> IO (Either String String))
-> Handler (Either String String)
forall a e. Exception e => (e -> IO a) -> Handler a
C.Handler (\(ErrorCall
e :: C.ErrorCall) -> Either String String -> IO (Either String String)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String String -> IO (Either String String))
-> Either String String -> IO (Either String String)
forall a b. (a -> b) -> a -> b
$ String -> Either String String
forall a b. a -> Either a b
Left (String -> Either String String) -> String -> Either String String
forall a b. (a -> b) -> a -> b
$ ErrorCall -> String
forall a. Show a => a -> String
show ErrorCall
e)
,(IOException -> IO (Either String String))
-> Handler (Either String String)
forall a e. Exception e => (e -> IO a) -> Handler a
C.Handler (\(IOException
e :: C.IOException) -> Either String String -> IO (Either String String)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String String -> IO (Either String String))
-> Either String String -> IO (Either String String)
forall a b. (a -> b) -> a -> b
$ String -> Either String String
forall a b. a -> Either a b
Left (String -> Either String String) -> String -> Either String String
forall a b. (a -> b) -> a -> b
$ IOException -> String
forall a. Show a => a -> String
show IOException
e)
]
readJournal :: InputOpts -> Maybe FilePath -> Handle -> ExceptT String IO Journal
readJournal :: InputOpts -> Maybe String -> Handle -> ExceptT String IO Journal
readJournal iopts :: InputOpts
iopts@InputOpts{Bool
strict_ :: Bool
strict_ :: InputOpts -> Bool
strict_, Bool
_defer :: Bool
_defer :: InputOpts -> Bool
_defer} Maybe String
mpath Handle
hdl = do
let Reader IO
r :: Reader IO = Reader IO -> Maybe (Reader IO) -> Reader IO
forall a. a -> Maybe a -> a
fromMaybe Reader IO
forall (m :: * -> *). MonadIO m => Reader m
JournalReader.reader (Maybe (Reader IO) -> Reader IO) -> Maybe (Reader IO) -> Reader IO
forall a b. (a -> b) -> a -> b
$ Maybe StorageFormat -> Maybe String -> Maybe (Reader IO)
forall (m :: * -> *).
MonadIO m =>
Maybe StorageFormat -> Maybe String -> Maybe (Reader m)
findReader (InputOpts -> Maybe StorageFormat
mformat_ InputOpts
iopts) Maybe String
mpath
String -> StorageFormat -> ExceptT String IO ()
forall (m :: * -> *) a. (MonadIO m, Show a) => String -> a -> m ()
dbg6IO String
"readJournal: trying reader" (Reader IO -> StorageFormat
forall (m :: * -> *). Reader m -> StorageFormat
rFormat Reader IO
r)
Journal
j <- Reader IO
-> InputOpts -> String -> Handle -> ExceptT String IO Journal
forall (m :: * -> *).
Reader m
-> InputOpts -> String -> Handle -> ExceptT String IO Journal
rReadFn Reader IO
r InputOpts
iopts (String -> Maybe String -> String
forall a. a -> Maybe a -> a
fromMaybe String
"-" Maybe String
mpath) Handle
hdl
Bool -> ExceptT String IO () -> ExceptT String IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool
strict_ Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
_defer) (ExceptT String IO () -> ExceptT String IO ())
-> ExceptT String IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ Either String () -> ExceptT String IO ()
forall e (m :: * -> *) a. MonadError e m => Either e a -> m a
liftEither (Either String () -> ExceptT String IO ())
-> Either String () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ Journal -> Either String ()
journalStrictChecks Journal
j
Journal -> ExceptT String IO Journal
forall a. a -> ExceptT String IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Journal
j
readJournalFile :: InputOpts -> PrefixedFilePath -> ExceptT String IO Journal
readJournalFile :: InputOpts -> String -> ExceptT String IO Journal
readJournalFile iopts :: InputOpts
iopts@InputOpts{Bool
new_ :: Bool
new_ :: InputOpts -> Bool
new_, Bool
new_save_ :: Bool
new_save_ :: InputOpts -> Bool
new_save_, Bool
_defer :: InputOpts -> Bool
_defer :: Bool
_defer} String
prefixedfile = do
(Journal
j, Maybe LatestDatesForFile
mlatestdates) <- InputOpts
-> String -> ExceptT String IO (Journal, Maybe LatestDatesForFile)
readJournalFileAndLatestDates InputOpts
iopts String
prefixedfile
Bool -> ExceptT String IO () -> ExceptT String IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool
new_ Bool -> Bool -> Bool
&& Bool
new_save_ Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
_defer) (ExceptT String IO () -> ExceptT String IO ())
-> ExceptT String IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> ExceptT String IO ()
forall a. IO a -> ExceptT String IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ExceptT String IO ()) -> IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$
case Maybe LatestDatesForFile
mlatestdates of
Maybe LatestDatesForFile
Nothing -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Just (LatestDatesForFile String
f LatestDates
ds) -> LatestDates -> String -> IO ()
saveLatestDates LatestDates
ds String
f
Journal -> ExceptT String IO Journal
forall a. a -> ExceptT String IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Journal
j
readJournalFileAndLatestDates :: InputOpts -> PrefixedFilePath -> ExceptT String IO (Journal, Maybe LatestDatesForFile)
readJournalFileAndLatestDates :: InputOpts
-> String -> ExceptT String IO (Journal, Maybe LatestDatesForFile)
readJournalFileAndLatestDates InputOpts
iopts String
prefixedfile = do
let
(Maybe StorageFormat
mfmt, String
f) = String -> (Maybe StorageFormat, String)
splitReaderPrefix String
prefixedfile
iopts' :: InputOpts
iopts' = InputOpts
iopts{mformat_=asum [mfmt, mformat_ iopts]}
IO () -> ExceptT String IO ()
forall a. IO a -> ExceptT String IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ExceptT String IO ()) -> IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
requireJournalFileExists String
f
Handle
h <-
String -> ExceptT String IO Handle -> ExceptT String IO Handle
forall a. String -> a -> a
dbg6Msg (String
"readJournalFile: "String -> String -> String
forall a. [a] -> [a] -> [a]
++String -> String
takeFileName String
f) (ExceptT String IO Handle -> ExceptT String IO Handle)
-> ExceptT String IO Handle -> ExceptT String IO Handle
forall a b. (a -> b) -> a -> b
$
IO Handle -> ExceptT String IO Handle
forall a. IO a -> ExceptT String IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Handle -> ExceptT String IO Handle)
-> IO Handle -> ExceptT String IO Handle
forall a b. (a -> b) -> a -> b
$ String -> IO Handle
openFileOrStdin String
f
Journal
j <- InputOpts -> Maybe String -> Handle -> ExceptT String IO Journal
readJournal InputOpts
iopts' (String -> Maybe String
forall a. a -> Maybe a
Just String
f) Handle
h
if InputOpts -> Bool
new_ InputOpts
iopts
then do
LatestDates
ds <- IO LatestDates -> ExceptT String IO LatestDates
forall a. IO a -> ExceptT String IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO LatestDates -> ExceptT String IO LatestDates)
-> IO LatestDates -> ExceptT String IO LatestDates
forall a b. (a -> b) -> a -> b
$ String -> IO LatestDates
previousLatestDates String
f
let (Journal
newj, LatestDates
newds) = LatestDates -> Journal -> (Journal, LatestDates)
journalFilterSinceLatestDates LatestDates
ds Journal
j
(Journal, Maybe LatestDatesForFile)
-> ExceptT String IO (Journal, Maybe LatestDatesForFile)
forall a. a -> ExceptT String IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Journal
newj, LatestDatesForFile -> Maybe LatestDatesForFile
forall a. a -> Maybe a
Just (LatestDatesForFile -> Maybe LatestDatesForFile)
-> LatestDatesForFile -> Maybe LatestDatesForFile
forall a b. (a -> b) -> a -> b
$ String -> LatestDates -> LatestDatesForFile
LatestDatesForFile String
f LatestDates
newds)
else
(Journal, Maybe LatestDatesForFile)
-> ExceptT String IO (Journal, Maybe LatestDatesForFile)
forall a. a -> ExceptT String IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Journal
j, Maybe LatestDatesForFile
forall a. Maybe a
Nothing)
readJournalFiles :: InputOpts -> [PrefixedFilePath] -> ExceptT String IO Journal
readJournalFiles :: InputOpts -> [String] -> ExceptT String IO Journal
readJournalFiles iopts :: InputOpts
iopts@InputOpts{Bool
strict_ :: InputOpts -> Bool
strict_ :: Bool
strict_, Bool
new_ :: InputOpts -> Bool
new_ :: Bool
new_, Bool
new_save_ :: InputOpts -> Bool
new_save_ :: Bool
new_save_} [String]
prefixedfiles = do
let iopts' :: InputOpts
iopts' = InputOpts
iopts{_defer=True}
(Journal
j, [LatestDatesForFile]
latestdatesforfiles) <-
String
-> ExceptT String IO (Journal, [LatestDatesForFile])
-> ExceptT String IO (Journal, [LatestDatesForFile])
forall a. String -> a -> a
dbg6Msg (String
"readJournalFiles: "String -> String -> String
forall a. [a] -> [a] -> [a]
++[String] -> String
forall a. Show a => a -> String
show [String]
prefixedfiles) (ExceptT String IO (Journal, [LatestDatesForFile])
-> ExceptT String IO (Journal, [LatestDatesForFile]))
-> ExceptT String IO (Journal, [LatestDatesForFile])
-> ExceptT String IO (Journal, [LatestDatesForFile])
forall a b. (a -> b) -> a -> b
$
InputOpts
-> [String] -> ExceptT String IO (Journal, [LatestDatesForFile])
readJournalFilesAndLatestDates InputOpts
iopts' [String]
prefixedfiles
Bool -> ExceptT String IO () -> ExceptT String IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when Bool
strict_ (ExceptT String IO () -> ExceptT String IO ())
-> ExceptT String IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ Either String () -> ExceptT String IO ()
forall e (m :: * -> *) a. MonadError e m => Either e a -> m a
liftEither (Either String () -> ExceptT String IO ())
-> Either String () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ Journal -> Either String ()
journalStrictChecks Journal
j
Bool -> ExceptT String IO () -> ExceptT String IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool
new_ Bool -> Bool -> Bool
&& Bool
new_save_) (ExceptT String IO () -> ExceptT String IO ())
-> ExceptT String IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> ExceptT String IO ()
forall a. IO a -> ExceptT String IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ExceptT String IO ()) -> IO () -> ExceptT String IO ()
forall a b. (a -> b) -> a -> b
$ [LatestDatesForFile] -> IO ()
saveLatestDatesForFiles [LatestDatesForFile]
latestdatesforfiles
Journal -> ExceptT String IO Journal
forall a. a -> ExceptT String IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Journal
j
readJournalFilesAndLatestDates :: InputOpts -> [PrefixedFilePath] -> ExceptT String IO (Journal, [LatestDatesForFile])
readJournalFilesAndLatestDates :: InputOpts
-> [String] -> ExceptT String IO (Journal, [LatestDatesForFile])
readJournalFilesAndLatestDates InputOpts
iopts [String]
pfs = do
([Journal]
js, [Maybe LatestDatesForFile]
lastdates) <- [(Journal, Maybe LatestDatesForFile)]
-> ([Journal], [Maybe LatestDatesForFile])
forall a b. [(a, b)] -> ([a], [b])
unzip ([(Journal, Maybe LatestDatesForFile)]
-> ([Journal], [Maybe LatestDatesForFile]))
-> ExceptT String IO [(Journal, Maybe LatestDatesForFile)]
-> ExceptT String IO ([Journal], [Maybe LatestDatesForFile])
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (String -> ExceptT String IO (Journal, Maybe LatestDatesForFile))
-> [String]
-> ExceptT String IO [(Journal, Maybe LatestDatesForFile)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> [a] -> m [b]
mapM (InputOpts
-> String -> ExceptT String IO (Journal, Maybe LatestDatesForFile)
readJournalFileAndLatestDates InputOpts
iopts) [String]
pfs
(Journal, [LatestDatesForFile])
-> ExceptT String IO (Journal, [LatestDatesForFile])
forall a. a -> ExceptT String IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Journal -> Journal
journalNumberTransactions (Journal -> Journal) -> Journal -> Journal
forall a b. (a -> b) -> a -> b
$ Journal
-> (NonEmpty Journal -> Journal)
-> Maybe (NonEmpty Journal)
-> Journal
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Journal
forall a. Default a => a
def NonEmpty Journal -> Journal
forall a. Semigroup a => NonEmpty a -> a
sconcat (Maybe (NonEmpty Journal) -> Journal)
-> Maybe (NonEmpty Journal) -> Journal
forall a b. (a -> b) -> a -> b
$ [Journal] -> Maybe (NonEmpty Journal)
forall a. [a] -> Maybe (NonEmpty a)
nonEmpty [Journal]
js, [Maybe LatestDatesForFile] -> [LatestDatesForFile]
forall a. [Maybe a] -> [a]
catMaybes [Maybe LatestDatesForFile]
lastdates)
readJournal' :: Handle -> IO Journal
readJournal' :: Handle -> IO Journal
readJournal' = ExceptT String IO Journal -> IO Journal
forall (m :: * -> *) a. MonadIO m => ExceptT String m a -> m a
orDieTrying (ExceptT String IO Journal -> IO Journal)
-> (Handle -> ExceptT String IO Journal) -> Handle -> IO Journal
forall b c a. (b -> c) -> (a -> b) -> a -> c
. InputOpts -> Maybe String -> Handle -> ExceptT String IO Journal
readJournal InputOpts
definputopts Maybe String
forall a. Maybe a
Nothing
readJournal'' :: Text -> IO Journal
readJournal'' :: Text -> IO Journal
readJournal'' = Text -> IO Handle
textToHandle (Text -> IO Handle) -> (Handle -> IO Journal) -> Text -> IO Journal
forall (m :: * -> *) a b c.
Monad m =>
(a -> m b) -> (b -> m c) -> a -> m c
>=> Handle -> IO Journal
readJournal'
readJournalFile' :: PrefixedFilePath -> IO Journal
readJournalFile' :: String -> IO Journal
readJournalFile' = ExceptT String IO Journal -> IO Journal
forall (m :: * -> *) a. MonadIO m => ExceptT String m a -> m a
orDieTrying (ExceptT String IO Journal -> IO Journal)
-> (String -> ExceptT String IO Journal) -> String -> IO Journal
forall b c a. (b -> c) -> (a -> b) -> a -> c
. InputOpts -> String -> ExceptT String IO Journal
readJournalFile InputOpts
definputopts
readJournalFiles' :: [PrefixedFilePath] -> IO Journal
readJournalFiles' :: [String] -> IO Journal
readJournalFiles' = ExceptT String IO Journal -> IO Journal
forall (m :: * -> *) a. MonadIO m => ExceptT String m a -> m a
orDieTrying (ExceptT String IO Journal -> IO Journal)
-> ([String] -> ExceptT String IO Journal)
-> [String]
-> IO Journal
forall b c a. (b -> c) -> (a -> b) -> a -> c
. InputOpts -> [String] -> ExceptT String IO Journal
readJournalFiles InputOpts
definputopts
orDieTrying :: MonadIO m => ExceptT String m a -> m a
orDieTrying :: forall (m :: * -> *) a. MonadIO m => ExceptT String m a -> m a
orDieTrying ExceptT String m a
a = (String -> m a) -> (a -> m a) -> Either String a -> m a
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either (IO a -> m a
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO a -> m a) -> (String -> IO a) -> String -> m a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> IO a
forall a. String -> IO a
forall (m :: * -> *) a. MonadFail m => String -> m a
fail) a -> m a
forall a. a -> m a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String a -> m a) -> m (Either String a) -> m a
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< ExceptT String m a -> m (Either String a)
forall e (m :: * -> *) a. ExceptT e m a -> m (Either e a)
runExceptT ExceptT String m a
a
requireJournalFileExists :: FilePath -> IO ()
requireJournalFileExists :: String -> IO ()
requireJournalFileExists String
"-" = () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
requireJournalFileExists String
f = do
Bool
exists <- String -> IO Bool
doesFileExist String
f
Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless Bool
exists (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
forall a. String -> a
error' (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ [String] -> String
unlines
[ String
"data file \"" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
f String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\" was not found."
,String
"Please create it first, eg with \"hledger add\" or a text editor."
,String
"Or, specify an existing data file with -f or $LEDGER_FILE."
]
ensureJournalFileExists :: FilePath -> IO ()
ensureJournalFileExists :: String -> IO ()
ensureJournalFileExists String
f = do
Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (String
osString -> String -> Bool
forall a. Eq a => a -> a -> Bool
==String
"mingw32" Bool -> Bool -> Bool
&& String -> Bool
isWindowsUnsafeDotPath String
f) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
String -> IO ()
forall a. String -> a
error' (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Part of file path \"" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String -> String
forall a. Show a => a -> String
show String
f String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\"\n ends with a dot, which is unsafe on Windows; please use a different path.\n"
Bool
exists <- String -> IO Bool
doesFileExist String
f
Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless Bool
exists (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Handle -> String -> IO ()
hPutStrLn Handle
stderr (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Creating hledger journal file " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String -> String
forall a. Show a => a -> String
show String
f
IO Text
newJournalContent IO Text -> (Text -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= String -> Text -> IO ()
T.writeFile String
f
isWindowsUnsafeDotPath :: FilePath -> Bool
isWindowsUnsafeDotPath :: String -> Bool
isWindowsUnsafeDotPath = (String -> Bool) -> [String] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (\String
x -> String -> Char
forall a. HasCallStack => [a] -> a
last String
x Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'.' Bool -> Bool -> Bool
&& (Char -> Bool) -> String -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/=Char
'.') String
x) ([String] -> Bool) -> (String -> [String]) -> String -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> [String]
splitDirectories
newJournalContent :: IO Text
newJournalContent :: IO Text
newJournalContent = do
Day
d <- IO Day
getCurrentDay
Text -> IO Text
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Text -> IO Text) -> Text -> IO Text
forall a b. (a -> b) -> a -> b
$ Text
"; journal created " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> String -> Text
T.pack (Day -> String
forall a. Show a => a -> String
show Day
d) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" by hledger\n"
type LatestDates = [Day]
data LatestDatesForFile = LatestDatesForFile FilePath LatestDates
deriving Int -> LatestDatesForFile -> String -> String
[LatestDatesForFile] -> String -> String
LatestDatesForFile -> String
(Int -> LatestDatesForFile -> String -> String)
-> (LatestDatesForFile -> String)
-> ([LatestDatesForFile] -> String -> String)
-> Show LatestDatesForFile
forall a.
(Int -> a -> String -> String)
-> (a -> String) -> ([a] -> String -> String) -> Show a
$cshowsPrec :: Int -> LatestDatesForFile -> String -> String
showsPrec :: Int -> LatestDatesForFile -> String -> String
$cshow :: LatestDatesForFile -> String
show :: LatestDatesForFile -> String
$cshowList :: [LatestDatesForFile] -> String -> String
showList :: [LatestDatesForFile] -> String -> String
Show
latestDates :: [Day] -> LatestDates
latestDates :: LatestDates -> LatestDates
latestDates = {-# HLINT ignore "Avoid reverse" #-}
LatestDates -> [LatestDates] -> LatestDates
forall a. a -> [a] -> a
headDef [] ([LatestDates] -> LatestDates)
-> (LatestDates -> [LatestDates]) -> LatestDates -> LatestDates
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> [LatestDates] -> [LatestDates]
forall a. Int -> [a] -> [a]
take Int
1 ([LatestDates] -> [LatestDates])
-> (LatestDates -> [LatestDates]) -> LatestDates -> [LatestDates]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. LatestDates -> [LatestDates]
forall a. Eq a => [a] -> [[a]]
group (LatestDates -> [LatestDates])
-> (LatestDates -> LatestDates) -> LatestDates -> [LatestDates]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. LatestDates -> LatestDates
forall a. [a] -> [a]
reverse (LatestDates -> LatestDates)
-> (LatestDates -> LatestDates) -> LatestDates -> LatestDates
forall b c a. (b -> c) -> (a -> b) -> a -> c
. LatestDates -> LatestDates
forall a. Ord a => [a] -> [a]
sort
saveLatestDates :: LatestDates -> FilePath -> IO ()
saveLatestDates :: LatestDates -> String -> IO ()
saveLatestDates LatestDates
dates String
f = Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$ LatestDates -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null LatestDates
dates) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
String -> Text -> IO ()
T.writeFile (String -> String
latestDatesFileFor String
f) (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [Text] -> Text
T.unlines ([Text] -> Text) -> [Text] -> Text
forall a b. (a -> b) -> a -> b
$ (Day -> Text) -> LatestDates -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Day -> Text
showDate LatestDates
dates
saveLatestDatesForFiles :: [LatestDatesForFile] -> IO ()
saveLatestDatesForFiles :: [LatestDatesForFile] -> IO ()
saveLatestDatesForFiles = (LatestDatesForFile -> IO ()) -> [LatestDatesForFile] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (\(LatestDatesForFile String
f LatestDates
ds) -> LatestDates -> String -> IO ()
saveLatestDates LatestDates
ds String
f)
previousLatestDates :: FilePath -> IO LatestDates
previousLatestDates :: String -> IO LatestDates
previousLatestDates String
f = do
let latestfile :: String
latestfile = String -> String
latestDatesFileFor String
f
Bool
exists <- String -> IO Bool
doesFileExist String
latestfile
Text
t <- if Bool
exists then String -> IO Text
readFileStrictly String
latestfile else Text -> IO Text
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Text
T.empty
let nls :: [(Int, Text)]
nls = [Int] -> [Text] -> [(Int, Text)]
forall a b. [a] -> [b] -> [(a, b)]
zip [Int
1::Int ..] ([Text] -> [(Int, Text)]) -> [Text] -> [(Int, Text)]
forall a b. (a -> b) -> a -> b
$ Text -> [Text]
T.lines Text
t
([Maybe Day] -> LatestDates) -> IO [Maybe Day] -> IO LatestDates
forall a b. (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap [Maybe Day] -> LatestDates
forall a. [Maybe a] -> [a]
catMaybes (IO [Maybe Day] -> IO LatestDates)
-> IO [Maybe Day] -> IO LatestDates
forall a b. (a -> b) -> a -> b
$ [(Int, Text)] -> ((Int, Text) -> IO (Maybe Day)) -> IO [Maybe Day]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [(Int, Text)]
nls (((Int, Text) -> IO (Maybe Day)) -> IO [Maybe Day])
-> ((Int, Text) -> IO (Maybe Day)) -> IO [Maybe Day]
forall a b. (a -> b) -> a -> b
$ \(Int
n,Text
l) -> do
let s :: String
s = Text -> String
T.unpack (Text -> String) -> Text -> String
forall a b. (a -> b) -> a -> b
$ Text -> Text
T.strip Text
l
case (String
s, String -> Maybe Day
parsedate String
s) of
(String
"", Maybe Day
_) -> Maybe Day -> IO (Maybe Day)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe Day
forall a. Maybe a
Nothing
(String
_, Maybe Day
Nothing) -> String -> IO (Maybe Day)
forall a. String -> a
error' (String -> String -> Int -> String -> String
forall r. PrintfType r => String -> r
printf String
"%s:%d: invalid date: \"%s\"" String
latestfile Int
n String
s)
(String
_, Just Day
d) -> Maybe Day -> IO (Maybe Day)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Maybe Day -> IO (Maybe Day)) -> Maybe Day -> IO (Maybe Day)
forall a b. (a -> b) -> a -> b
$ Day -> Maybe Day
forall a. a -> Maybe a
Just Day
d
latestDatesFileFor :: FilePath -> FilePath
latestDatesFileFor :: String -> String
latestDatesFileFor String
f = String
dir String -> String -> String
</> String
".latest" String -> String -> String
<.> String
fname
where
(String
dir, String
fname) = String -> (String, String)
splitFileName String
f
journalFilterSinceLatestDates :: LatestDates -> Journal -> (Journal, LatestDates)
journalFilterSinceLatestDates :: LatestDates -> Journal -> (Journal, LatestDates)
journalFilterSinceLatestDates [] Journal
j = (Journal
j, LatestDates -> LatestDates
latestDates (LatestDates -> LatestDates) -> LatestDates -> LatestDates
forall a b. (a -> b) -> a -> b
$ (Transaction -> Day) -> [Transaction] -> LatestDates
forall a b. (a -> b) -> [a] -> [b]
map Transaction -> Day
tdate ([Transaction] -> LatestDates) -> [Transaction] -> LatestDates
forall a b. (a -> b) -> a -> b
$ Journal -> [Transaction]
jtxns Journal
j)
journalFilterSinceLatestDates ds :: LatestDates
ds@(Day
d:LatestDates
_) Journal
j = (Journal
j', LatestDates
ds')
where
samedateorlaterts :: [Transaction]
samedateorlaterts = (Transaction -> Bool) -> [Transaction] -> [Transaction]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Day -> Day -> Bool
forall a. Ord a => a -> a -> Bool
>= Day
d)(Day -> Bool) -> (Transaction -> Day) -> Transaction -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Transaction -> Day
tdate) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$ Journal -> [Transaction]
jtxns Journal
j
([Transaction]
samedatets, [Transaction]
laterts) = (Transaction -> Bool)
-> [Transaction] -> ([Transaction], [Transaction])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span ((Day -> Day -> Bool
forall a. Eq a => a -> a -> Bool
== Day
d)(Day -> Bool) -> (Transaction -> Day) -> Transaction -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Transaction -> Day
tdate) ([Transaction] -> ([Transaction], [Transaction]))
-> [Transaction] -> ([Transaction], [Transaction])
forall a b. (a -> b) -> a -> b
$ (Transaction -> Transaction -> Ordering)
-> [Transaction] -> [Transaction]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy ((Transaction -> Day) -> Transaction -> Transaction -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing Transaction -> Day
tdate) [Transaction]
samedateorlaterts
newsamedatets :: [Transaction]
newsamedatets = Int -> [Transaction] -> [Transaction]
forall a. Int -> [a] -> [a]
drop (LatestDates -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length LatestDates
ds) [Transaction]
samedatets
j' :: Journal
j' = Journal
j{jtxns=newsamedatets++laterts}
ds' :: LatestDates
ds' = LatestDates -> LatestDates
latestDates (LatestDates -> LatestDates) -> LatestDates -> LatestDates
forall a b. (a -> b) -> a -> b
$ (Transaction -> Day) -> [Transaction] -> LatestDates
forall a b. (a -> b) -> [a] -> [b]
map Transaction -> Day
tdate ([Transaction] -> LatestDates) -> [Transaction] -> LatestDates
forall a b. (a -> b) -> a -> b
$ [Transaction]
samedatets[Transaction] -> [Transaction] -> [Transaction]
forall a. [a] -> [a] -> [a]
++[Transaction]
laterts
tests_Read :: TestTree
tests_Read = String -> [TestTree] -> TestTree
testGroup String
"Read" [
TestTree
tests_Common
,TestTree
tests_CsvReader
,TestTree
tests_JournalReader
,TestTree
tests_RulesReader
]