    --- * -*- outline-regexp:"--- \\*"; -*-
    --- ** doc
    -- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
    {-|

    This is the entry point to hledger's reading system, which can read
    Journals from various data formats. Use this module if you want to parse
    journal data or read journal files. Generally it should not be necessary
    to import modules below this one.

    -}

    --- ** language
    {-# LANGUAGE OverloadedStrings #-}
    {-# LANGUAGE PackageImports #-}
    {-# LANGUAGE ScopedTypeVariables #-}

    --- ** exports
    module Hledger.Read (

      -- * Journal files
      PrefixedFilePath,
      defaultJournal,
      defaultJournalPath,
      readJournalFiles,
      readJournalFile,
      requireJournalFileExists,
      ensureJournalFileExists,

      -- * Journal parsing
      readJournal,
      readJournal',

      -- * Re-exported
      JournalReader.accountaliasp,
      JournalReader.postingp,
      findReader,
      splitReaderPrefix,
      module Hledger.Read.Common,

      -- * Tests
      tests_Read,

    ) where

    --- ** imports
    import Control.Arrow (right)
    import qualified Control.Exception as C
    import Control.Monad (when)
    import "mtl" Control.Monad.Except (runExceptT)
    import Data.Default (def)
    import Data.Foldable (asum)
    import Data.List (group, sort, sortBy)
    import Data.List.NonEmpty (nonEmpty)
    import Data.Maybe (fromMaybe)
    import Data.Ord (comparing)
    import Data.Semigroup (sconcat)
    import Data.Text (Text)
    import qualified Data.Text as T
    import Data.Time (Day)
    import Safe (headDef)
    import System.Directory (doesFileExist, getHomeDirectory)
    import System.Environment (getEnv)
    import System.Exit (exitFailure)
    import System.FilePath ((<.>), (</>), splitDirectories, splitFileName)
    import System.Info (os)
    import System.IO (stderr, writeFile)
    import Text.Printf (hPrintf, printf)

    import Hledger.Data.Dates (getCurrentDay, parsedateM, showDate)
    import Hledger.Data.Types
    import Hledger.Read.Common
    import Hledger.Read.JournalReader as JournalReader
    import Hledger.Read.CsvReader (tests_CsvReader)
    -- import Hledger.Read.TimedotReader (tests_TimedotReader)
    -- import Hledger.Read.TimeclockReader (tests_TimeclockReader)
    import Hledger.Utils
    import Prelude hiding (getContents, writeFile)

    --- ** doctest setup
    -- $setup
    -- >>> :set -XOverloadedStrings

    --- ** journal reading

    journalEnvVar :: String
    journalEnvVar           = "LEDGER_FILE"
    journalEnvVar2 :: String
    journalEnvVar2          = "LEDGER"
    journalDefaultFilename :: String
    journalDefaultFilename  = ".hledger.journal"

    -- | Read a Journal from the given text, assuming journal format; or
    -- throw an error.
    readJournal' :: Text -> IO Journal
    readJournal' :: Text -> IO Journal
    readJournal' t :: Text
    t = InputOpts -> Maybe String -> Text -> IO (Either String Journal)
    readJournal InputOpts
    forall a. Default a => a
    def Maybe String
    forall a. Maybe a
    Nothing Text
    t IO (Either String Journal)
    -> (Either String Journal -> IO Journal) -> IO Journal
    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 (m :: * -> *) a. Monad m => a -> m a
    return  -- PARTIAL:

    -- | @readJournal iopts mfile txt@
    --
    -- Read a Journal from some text, or return an error message.
    --
    -- The reader (data format) is chosen based on, in this order:
    --
    -- - a reader name provided in @iopts@
    --
    -- - a reader prefix in the @mfile@ path
    --
    -- - a file extension in @mfile@
    --
    -- If none of these is available, or if the reader name is unrecognised,
    -- we use the journal reader. (We used to try all readers in this case;
    -- since hledger 1.17, we prefer predictability.)
    readJournal :: InputOpts -> Maybe FilePath -> Text -> IO (Either String Journal)
    readJournal :: InputOpts -> Maybe String -> Text -> IO (Either String Journal)
    readJournal iopts :: InputOpts
    iopts mpath :: Maybe String
    mpath txt :: Text
    txt = 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 String -> Maybe String -> Maybe (Reader IO)
    forall (m :: * -> *).
    MonadIO m =>
    Maybe String -> Maybe String -> Maybe (Reader m)
    findReader (InputOpts -> Maybe String
    mformat_ InputOpts
    iopts) Maybe String
    mpath
      String -> String -> IO ()
    forall (m :: * -> *) a. (MonadIO m, Show a) => String -> a -> m ()
    dbg6IO "trying reader" (Reader IO -> String
    forall (m :: * -> *). Reader m -> String
    rFormat Reader IO
    r)
      (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))
    -> (Text -> ExceptT String IO Journal)
    -> Text
    -> IO (Either String Journal)
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Reader IO
    -> InputOpts -> String -> Text -> ExceptT String IO Journal
    forall (m :: * -> *).
    Reader m
    -> InputOpts -> String -> Text -> 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)) Text
    txt

    -- | Read the default journal file specified by the environment, or raise an error.
    defaultJournal :: IO Journal
    defaultJournal :: IO Journal
    defaultJournal = IO String
    defaultJournalPath IO String
    -> (String -> IO (Either String Journal))
    -> IO (Either String Journal)
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= InputOpts -> String -> IO (Either String Journal)
    readJournalFile InputOpts
    forall a. Default a => a
    def IO (Either String Journal)
    -> (Either String Journal -> IO Journal) -> IO Journal
    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 (m :: * -> *) a. Monad m => a -> m a
    return  -- PARTIAL:

    -- | Get the default journal file path specified by the environment.
    -- Like ledger, we look first for the LEDGER_FILE environment
    -- variable, and if that does not exist, for the legacy LEDGER
    -- environment variable. If neither is set, or the value is blank,
    -- return the hard-coded default, which is @.hledger.journal@ in the
    -- users's home directory (or in the current directory, if we cannot
    -- determine a home directory).
    defaultJournalPath :: IO String
    defaultJournalPath :: IO String
    defaultJournalPath = do
      String
    s <- IO String
    envJournalPath
      if String -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null String
    s then IO String
    defaultJournalPath else String -> IO String
    forall (m :: * -> *) a. Monad m => a -> m a
    return String
    s
        where
          envJournalPath :: IO String
    envJournalPath =
            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
    getEnv String
    journalEnvVar2
                                                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 (m :: * -> *) a. Monad m => a -> m a
    return ""))
          defaultJournalPath :: IO String
    defaultJournalPath = do
                      String
    home <- IO String
    getHomeDirectory 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 (m :: * -> *) a. Monad m => a -> m a
    return "")
                      String -> IO String
    forall (m :: * -> *) a. Monad m => a -> m a
    return (String -> IO String) -> String -> IO String
    forall a b. (a -> b) -> a -> b
    $ String
    home String -> String -> String
    </> String
    journalDefaultFilename

    -- | A file path optionally prefixed by a reader name and colon
    -- (journal:, csv:, timedot:, etc.).
    type PrefixedFilePath = FilePath

    -- | Read a Journal from each specified file path and combine them into one.
    -- Or, return the first error message.
    --
    -- Combining Journals means concatenating them, basically.
    -- The parse state resets at the start of each file, which means that
    -- directives & aliases do not affect subsequent sibling or parent files.
    -- They do affect included child files though.
    -- Also the final parse state saved in the Journal does span all files.
    readJournalFiles :: InputOpts -> [PrefixedFilePath] -> IO (Either String Journal)
    readJournalFiles :: InputOpts -> [String] -> IO (Either String Journal)
    readJournalFiles iopts :: InputOpts
    iopts =
      ([Either String Journal] -> Either String Journal)
    -> IO [Either String Journal] -> IO (Either String Journal)
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    fmap (([Journal] -> Journal)
    -> Either String [Journal] -> Either String Journal
    forall (a :: * -> * -> *) b c d.
    ArrowChoice a =>
    a b c -> a (Either d b) (Either d c)
    right (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)
    -> ([Journal] -> Maybe (NonEmpty Journal)) -> [Journal] -> Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [Journal] -> Maybe (NonEmpty Journal)
    forall a. [a] -> Maybe (NonEmpty a)
    nonEmpty) (Either String [Journal] -> Either String Journal)
    -> ([Either String Journal] -> Either String [Journal])
    -> [Either String Journal]
    -> Either String Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [Either String Journal] -> Either String [Journal]
    forall (t :: * -> *) (m :: * -> *) a.
    (Traversable t, Monad m) =>
    t (m a) -> m (t a)
    sequence) (IO [Either String Journal] -> IO (Either String Journal))
    -> ([String] -> IO [Either String Journal])
    -> [String]
    -> IO (Either String Journal)
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (String -> IO (Either String Journal))
    -> [String] -> IO [Either String Journal]
    forall (t :: * -> *) (m :: * -> *) a b.
    (Traversable t, Monad m) =>
    (a -> m b) -> t a -> m (t b)
    mapM (InputOpts -> String -> IO (Either String Journal)
    readJournalFile InputOpts
    iopts)

    -- | Read a Journal from this file, or from stdin if the file path is -,
    -- or return an error message. The file path can have a READER: prefix.
    --
    -- The reader (data format) to use is determined from (in priority order):
    -- the @mformat_@ specified in the input options, if any;
    -- the file path's READER: prefix, if any;
    -- a recognised file name extension.
    -- if none of these identify a known reader, the journal reader is used.
    --
    -- The input options can also configure balance assertion checking, automated posting
    -- generation, a rules file for converting CSV data, etc.
    readJournalFile :: InputOpts -> PrefixedFilePath -> IO (Either String Journal)
    readJournalFile :: InputOpts -> String -> IO (Either String Journal)
    readJournalFile iopts :: InputOpts
    iopts prefixedfile :: String
    prefixedfile = do
      let
        (mfmt :: Maybe String
    mfmt, f :: String
    f) = String -> (Maybe String, String)
    splitReaderPrefix String
    prefixedfile
        iopts' :: InputOpts
    iopts' = InputOpts
    iopts{mformat_ :: Maybe String
    mformat_=[Maybe String] -> Maybe String
    forall (t :: * -> *) (f :: * -> *) a.
    (Foldable t, Alternative f) =>
    t (f a) -> f a
    asum [Maybe String
    mfmt, InputOpts -> Maybe String
    mformat_ InputOpts
    iopts]}
      String -> IO ()
    requireJournalFileExists String
    f
      Text
    t <- String -> IO Text
    readFileOrStdinPortably String
    f
        -- <- T.readFile f  -- or without line ending translation, for testing
      Either String Journal
    ej <- InputOpts -> Maybe String -> Text -> IO (Either String Journal)
    readJournal InputOpts
    iopts' (String -> Maybe String
    forall a. a -> Maybe a
    Just String
    f) Text
    t
      case Either String Journal
    ej of
        Left e :: String
    e  -> Either String Journal -> IO (Either String Journal)
    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
    e
        Right j :: Journal
    j | InputOpts -> Bool
    new_ InputOpts
    iopts -> do
          LatestDates
    ds <- String -> IO LatestDates
    previousLatestDates String
    f
          let (newj :: Journal
    newj, newds :: LatestDates
    newds) = LatestDates -> Journal -> (Journal, LatestDates)
    journalFilterSinceLatestDates LatestDates
    ds Journal
    j
          Bool -> IO () -> IO ()
    forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
    when (InputOpts -> Bool
    new_save_ InputOpts
    iopts Bool -> Bool -> Bool
    && Bool -> Bool
    not (LatestDates -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null LatestDates
    newds)) (IO () -> IO ()) -> IO () -> IO ()
    forall a b. (a -> b) -> a -> b
    $ LatestDates -> String -> IO ()
    saveLatestDates LatestDates
    newds String
    f
          Either String Journal -> IO (Either String Journal)
    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
    $ Journal -> Either String Journal
    forall a b. b -> Either a b
    Right Journal
    newj
        Right j :: Journal
    j -> Either String Journal -> IO (Either String Journal)
    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
    $ Journal -> Either String Journal
    forall a b. b -> Either a b
    Right Journal
    j

    --- ** utilities

    -- | If the specified journal file does not exist (and is not "-"),
    -- give a helpful error and quit.
    requireJournalFileExists :: FilePath -> IO ()
    requireJournalFileExists :: String -> IO ()
    requireJournalFileExists "-" = () -> IO ()
    forall (m :: * -> *) a. Monad m => a -> m a
    return ()
    requireJournalFileExists f :: String
    f = do
      Bool
    exists <- String -> IO Bool
    doesFileExist String
    f
      Bool -> IO () -> IO ()
    forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
    when (Bool -> Bool
    not Bool
    exists) (IO () -> IO ()) -> IO () -> IO ()
    forall a b. (a -> b) -> a -> b
    $ do  -- XXX might not be a journal file
        Handle -> String -> String -> IO ()
    forall r. HPrintfType r => Handle -> String -> r
    hPrintf Handle
    stderr "The hledger journal file \"%s\" was not found.\n" String
    f
        Handle -> String -> IO ()
    forall r. HPrintfType r => Handle -> String -> r
    hPrintf Handle
    stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"
        Handle -> String -> IO ()
    forall r. HPrintfType r => Handle -> String -> r
    hPrintf Handle
    stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n"
        IO ()
    forall a. IO a
    exitFailure

    -- | Ensure there is a journal file at the given path, creating an empty one if needed.
    -- On Windows, also ensure that the path contains no trailing dots
    -- which could cause data loss (see 'isWindowsUnsafeDotPath').
    ensureJournalFileExists :: FilePath -> IO ()
    ensureJournalFileExists :: String -> IO ()
    ensureJournalFileExists f :: 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
    /="mingw32" Bool -> Bool -> Bool
    && String -> Bool
    isWindowsUnsafeDotPath String
    f) (IO () -> IO ()) -> IO () -> IO ()
    forall a b. (a -> b) -> a -> b
    $ do
        Handle -> String -> String -> IO ()
    forall r. HPrintfType r => Handle -> String -> r
    hPrintf Handle
    stderr "Part of file path %s\n ends with a dot, which is unsafe on Windows; please use a different path.\n" (String -> String
    forall a. Show a => a -> String
    show String
    f)
        IO ()
    forall a. IO a
    exitFailure
      Bool
    exists <- String -> IO Bool
    doesFileExist String
    f
      Bool -> IO () -> IO ()
    forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
    when (Bool -> Bool
    not Bool
    exists) (IO () -> IO ()) -> IO () -> IO ()
    forall a b. (a -> b) -> a -> b
    $ do
        Handle -> String -> String -> IO ()
    forall r. HPrintfType r => Handle -> String -> r
    hPrintf Handle
    stderr "Creating hledger journal file %s.\n" String
    f
        -- note Hledger.Utils.UTF8.* do no line ending conversion on windows,
        -- we currently require unix line endings on all platforms.
        IO String
    newJournalContent IO String -> (String -> IO ()) -> IO ()
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= String -> String -> IO ()
    writeFile String
    f

    -- | Does any part of this path contain non-. characters and end with a . ?
    -- Such paths are not safe to use on Windows (cf #1056).
    isWindowsUnsafeDotPath :: FilePath -> Bool
    isWindowsUnsafeDotPath :: String -> Bool
    isWindowsUnsafeDotPath =
      Bool -> Bool
    not (Bool -> Bool) -> (String -> Bool) -> String -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [String] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null ([String] -> Bool) -> (String -> [String]) -> String -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
      (String -> Bool) -> [String] -> [String]
    forall a. (a -> Bool) -> [a] -> [a]
    filter (Bool -> Bool
    not (Bool -> Bool) -> (String -> Bool) -> String -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Char -> Bool) -> String -> Bool
    forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
    all (Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    =='.')) ([String] -> [String])
    -> (String -> [String]) -> String -> [String]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
      (String -> Bool) -> [String] -> [String]
    forall a. (a -> Bool) -> [a] -> [a]
    filter ((Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    =='.')(Char -> Bool) -> (String -> Char) -> String -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .String -> Char
    forall a. [a] -> a
    last) ([String] -> [String])
    -> (String -> [String]) -> String -> [String]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
      String -> [String]
    splitDirectories

    -- | Give the content for a new auto-created journal file.
    newJournalContent :: IO String
    newJournalContent :: IO String
    newJournalContent = do
      Day
    d <- IO Day
    getCurrentDay
      String -> IO String
    forall (m :: * -> *) a. Monad m => a -> m a
    return (String -> IO String) -> String -> IO String
    forall a b. (a -> b) -> a -> b
    $ String -> String -> String
    forall r. PrintfType r => String -> r
    printf "; journal created %s by hledger\n" (Day -> String
    forall a. Show a => a -> String
    show Day
    d)

    -- A "LatestDates" is zero or more copies of the same date,
    -- representing the latest transaction date read from a file,
    -- and how many transactions there were on that date.
    type LatestDates = [Day]

    -- | Get all instances of the latest date in an unsorted list of dates.
    -- Ie, if the latest date appears once, return it in a one-element list,
    -- if it appears three times (anywhere), return three of it.
    latestDates :: [Day] -> LatestDates
    latestDates :: LatestDates -> LatestDates
    latestDates = 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 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

    -- | Remember that these transaction dates were the latest seen when
    -- reading this journal file.
    saveLatestDates :: LatestDates -> FilePath -> IO ()
    saveLatestDates :: LatestDates -> String -> IO ()
    saveLatestDates dates :: LatestDates
    dates f :: String
    f = String -> String -> IO ()
    writeFile (String -> String
    latestDatesFileFor String
    f) (String -> IO ()) -> String -> IO ()
    forall a b. (a -> b) -> a -> b
    $ [String] -> String
    unlines ([String] -> String) -> [String] -> String
    forall a b. (a -> b) -> a -> b
    $ (Day -> String) -> LatestDates -> [String]
    forall a b. (a -> b) -> [a] -> [b]
    map Day -> String
    showDate LatestDates
    dates

    -- | What were the latest transaction dates seen the last time this
    -- journal file was read ? If there were multiple transactions on the
    -- latest date, that number of dates is returned, otherwise just one.
    -- Or none if no transactions were read, or if latest dates info is not
    -- available for this file.
    previousLatestDates :: FilePath -> IO LatestDates
    previousLatestDates :: String -> IO LatestDates
    previousLatestDates f :: String
    f = do
      let latestfile :: String
    latestfile = String -> String
    latestDatesFileFor String
    f
          parsedate :: String -> m Day
    parsedate s :: String
    s = m Day -> (Day -> m Day) -> Maybe Day -> m Day
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe (String -> m Day
    forall (m :: * -> *) a. MonadFail m => String -> m a
    fail (String -> m Day) -> String -> m Day
    forall a b. (a -> b) -> a -> b
    $ "could not parse date \"" String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    s String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ "\"") Day -> m Day
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Maybe Day -> m Day) -> Maybe Day -> m Day
    forall a b. (a -> b) -> a -> b
    $
                          String -> Maybe Day
    parsedateM String
    s
      Bool
    exists <- String -> IO Bool
    doesFileExist String
    latestfile
      if Bool
    exists
      then (Text -> IO Day) -> [Text] -> IO LatestDates
    forall (t :: * -> *) (f :: * -> *) a b.
    (Traversable t, Applicative f) =>
    (a -> f b) -> t a -> f (t b)
    traverse (String -> IO Day
    forall (m :: * -> *). MonadFail m => String -> m Day
    parsedate (String -> IO Day) -> (Text -> String) -> Text -> IO Day
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> String
    T.unpack (Text -> String) -> (Text -> Text) -> Text -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> Text
    T.strip) ([Text] -> IO LatestDates)
    -> (Text -> [Text]) -> Text -> IO LatestDates
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> [Text]
    T.lines (Text -> IO LatestDates) -> IO Text -> IO LatestDates
    forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
    =<< String -> IO Text
    readFileStrictly String
    latestfile
      else LatestDates -> IO LatestDates
    forall (m :: * -> *) a. Monad m => a -> m a
    return []

    -- | Where to save latest transaction dates for the given file path.
    -- (.latest.FILE)
    latestDatesFileFor :: FilePath -> FilePath
    latestDatesFileFor :: String -> String
    latestDatesFileFor f :: String
    f = String
    dir String -> String -> String
    </> ".latest" String -> String -> String
    <.> String
    fname
      where
        (dir :: String
    dir, fname :: String
    fname) = String -> (String, String)
    splitFileName String
    f

    readFileStrictly :: FilePath -> IO Text
    readFileStrictly :: String -> IO Text
    readFileStrictly f :: String
    f = String -> IO Text
    readFilePortably String
    f IO Text -> (Text -> IO Text) -> IO Text
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= \t :: Text
    t -> Int -> IO Int
    forall a. a -> IO a
    C.evaluate (Text -> Int
    T.length Text
    t) IO Int -> IO Text -> IO Text
    forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
    >> Text -> IO Text
    forall (m :: * -> *) a. Monad m => a -> m a
    return Text
    t

    -- | Given zero or more latest dates (all the same, representing the
    -- latest previously seen transaction date, and how many transactions
    -- were seen on that date), remove transactions with earlier dates
    -- from the journal, and the same number of transactions on the
    -- latest date, if any, leaving only transactions that we can assume
    -- are newer. Also returns the new latest dates of the new journal.
    journalFilterSinceLatestDates :: LatestDates -> Journal -> (Journal, LatestDates)
    journalFilterSinceLatestDates :: LatestDates -> Journal -> (Journal, LatestDates)
    journalFilterSinceLatestDates [] j :: 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@(d :: Day
    d:_) j :: 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
        (samedatets :: [Transaction]
    samedatets, laterts :: [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 (t :: * -> *) a. Foldable t => t a -> Int
    length LatestDates
    ds) [Transaction]
    samedatets
        j' :: Journal
    j'                    = Journal
    j{jtxns :: [Transaction]
    jtxns=[Transaction]
    newsamedatets[Transaction] -> [Transaction] -> [Transaction]
    forall a. [a] -> [a] -> [a]
    ++[Transaction]
    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

    tests_Read :: TestTree
    tests_Read = String -> [TestTree] -> TestTree
    tests "Read" [
       TestTree
    tests_Common
      ,TestTree
    tests_CsvReader
      ,TestTree
    tests_JournalReader
      ]