packages feed

hledger-lib 0.23.3 → 0.24

raw patch · 29 files changed

+1743/−1033 lines, 29 filesdep +Decimaldep +arraydep +blaze-markupdep ~parsecPVP ok

version bump matches the API change (PVP)

Dependencies added: Decimal, array, blaze-markup

Dependency ranges changed: parsec

API changes (from Hackage documentation)

- Hledger.Data.Amount: canonicalStyles :: [Amount] -> Map Commodity AmountStyle
- Hledger.Data.Amount: normaliseMixedAmountPreservingFirstPrice :: MixedAmount -> MixedAmount
- Hledger.Data.Amount: normaliseMixedAmountPreservingPrices :: MixedAmount -> MixedAmount
- Hledger.Data.Amount: sumAmounts :: [Amount] -> MixedAmount
- Hledger.Data.Journal: filterJournalPostingAmounts :: Query -> Journal -> Journal
- Hledger.Data.Types: asseparator :: AmountStyle -> Char
- Hledger.Data.Types: asseparatorpositions :: AmountStyle -> [Int]
- Hledger.Data.Types: ctxCommodityAndStyle :: JournalContext -> !(Maybe (Commodity, AmountStyle))
- Hledger.Reports.TransactionsReports: triSimpleBalance :: (t, t1, t2, t3, t4, MixedAmount) -> [Char]
- Hledger.Utils: containsRegex :: String -> String -> Bool
- Hledger.Utils: dbg :: Show a => String -> a -> a
- Hledger.Utils: dbg0 :: Show a => String -> a -> a
- Hledger.Utils: dbg1 :: Show a => String -> a -> a
- Hledger.Utils: dbg2 :: Show a => String -> a -> a
- Hledger.Utils: dbgAt :: Show a => Int -> String -> a -> a
- Hledger.Utils: dbgAtM :: Show a => Int -> String -> a -> IO ()
- Hledger.Utils: dbgExit :: Show a => String -> a -> a
- Hledger.Utils: dbgppshow :: Show a => Int -> String -> a -> a
- Hledger.Utils: dbgshow :: Show a => Int -> String -> a -> a
- Hledger.Utils: debugLevel :: Int
- Hledger.Utils: ltrace :: Show a => String -> a -> a
- Hledger.Utils: mtrace :: (Monad m, Show a) => a -> m a
- Hledger.Utils: pdbg :: Int -> String -> ParsecT [Char] st Identity ()
- Hledger.Utils: ppShow :: Show a => a -> String
- Hledger.Utils: ptrace :: String -> GenParser Char st ()
- Hledger.Utils: regexMatch :: String -> String -> Maybe (RegexResult, MatchList)
- Hledger.Utils: regexMatchCI :: String -> String -> Maybe (RegexResult, MatchList)
- Hledger.Utils: regexMatches :: String -> String -> Bool
- Hledger.Utils: regexMatchesCI :: String -> String -> Bool
- Hledger.Utils: regexMatchesCIRegexCompat :: String -> String -> Bool
- Hledger.Utils: regexMatchesRegexCompat :: String -> String -> Bool
- Hledger.Utils: regexReplace :: String -> String -> String -> String
- Hledger.Utils: regexReplaceBy :: String -> (String -> String) -> String -> String
- Hledger.Utils: regexReplaceCI :: String -> String -> String -> String
- Hledger.Utils: regexSplit :: String -> String -> [String]
- Hledger.Utils: regexToCaseInsensitive :: String -> String
- Hledger.Utils: strace :: Show a => a -> a
- Hledger.Utils: trace :: String -> a -> a
- Hledger.Utils: traceWith :: (a -> String) -> a -> a
+ Hledger.Data.AccountName: clipOrEllipsifyAccountName :: Int -> AccountName -> AccountName
+ Hledger.Data.Amount: filterMixedAmount :: (Amount -> Bool) -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: normaliseMixedAmount :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: normaliseMixedAmountSquashPricesForDisplay :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: showAmountWithZeroCommodity :: Amount -> String
+ Hledger.Data.Amount: showMixedAmountOneLineWithoutPrice :: MixedAmount -> String
+ Hledger.Data.Amount: showMixedAmountWithZeroCommodity :: MixedAmount -> String
+ Hledger.Data.Dates: failIfInvalidDay :: Monad m => String -> m ()
+ Hledger.Data.Dates: failIfInvalidMonth :: Monad m => String -> m ()
+ Hledger.Data.Journal: canonicalStyles :: [Amount] -> Map Commodity AmountStyle
+ Hledger.Data.Journal: filterJournalAmounts :: Query -> Journal -> Journal
+ Hledger.Data.Journal: filterTransactionAmounts :: Query -> Transaction -> Transaction
+ Hledger.Data.Posting: accountNameApplyOneAlias :: [AccountAlias] -> AccountName -> AccountName
+ Hledger.Data.Transaction: nullsourcepos :: SourcePos
+ Hledger.Data.Types: DigitGroups :: Char -> [Int] -> DigitGroupStyle
+ Hledger.Data.Types: asdigitgroups :: AmountStyle -> Maybe DigitGroupStyle
+ Hledger.Data.Types: ctxDefaultCommodityAndStyle :: JournalContext -> !(Maybe (Commodity, AmountStyle))
+ Hledger.Data.Types: data DigitGroupStyle
+ Hledger.Data.Types: instance Data (DecimalRaw Integer)
+ Hledger.Data.Types: instance Data DigitGroupStyle
+ Hledger.Data.Types: instance Eq DigitGroupStyle
+ Hledger.Data.Types: instance Ord DigitGroupStyle
+ Hledger.Data.Types: instance Read DigitGroupStyle
+ Hledger.Data.Types: instance Show DigitGroupStyle
+ Hledger.Data.Types: instance ToMarkup Quantity
+ Hledger.Data.Types: instance Typeable DigitGroupStyle
+ Hledger.Data.Types: numberRepresentation :: String
+ Hledger.Data.Types: tlsourcepos :: TimeLogEntry -> SourcePos
+ Hledger.Data.Types: tsourcepos :: Transaction -> SourcePos
+ Hledger.Data.Types: type AccountAlias = (Regexp, Replacement)
+ Hledger.Query: queryDateSpan' :: Query -> DateSpan
+ Hledger.Query: queryEndDate :: Bool -> Query -> Maybe Day
+ Hledger.Query: queryIsDate2 :: Query -> Bool
+ Hledger.Query: queryIsDateOrDate2 :: Query -> Bool
+ Hledger.Reports.ReportOptions: ALDefault :: AccountListMode
+ Hledger.Reports.ReportOptions: ALFlat :: AccountListMode
+ Hledger.Reports.ReportOptions: ALTree :: AccountListMode
+ Hledger.Reports.ReportOptions: accountlistmode_ :: ReportOpts -> AccountListMode
+ Hledger.Reports.ReportOptions: data AccountListMode
+ Hledger.Reports.ReportOptions: instance Data AccountListMode
+ Hledger.Reports.ReportOptions: instance Default AccountListMode
+ Hledger.Reports.ReportOptions: instance Eq AccountListMode
+ Hledger.Reports.ReportOptions: instance Show AccountListMode
+ Hledger.Reports.ReportOptions: instance Typeable AccountListMode
+ Hledger.Reports.TransactionsReports: triAmount :: (t, t1, t2, t3, t4, t5) -> t4
+ Hledger.Reports.TransactionsReports: triCommodityAmount :: Commodity -> (t, t1, t2, t3, MixedAmount, t4) -> MixedAmount
+ Hledger.Reports.TransactionsReports: triCommodityBalance :: Commodity -> (t, t1, t2, t3, t4, MixedAmount) -> MixedAmount
+ Hledger.Reports.TransactionsReports: triOrigTransaction :: (t, t1, t2, t3, t4, t5) -> t
+ Hledger.Utils: chomp :: [Char] -> [Char]
+ Hledger.Utils.Debug: dbg :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg0 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg1 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg2 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg3 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg4 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg5 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg6 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg7 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg8 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbg9 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbgAt :: Show a => Int -> String -> a -> a
+ Hledger.Utils.Debug: dbgAtIO :: Show a => Int -> String -> a -> IO ()
+ Hledger.Utils.Debug: dbgAtM :: Show a => Int -> String -> a -> IO ()
+ Hledger.Utils.Debug: dbgExit :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dbgppshow :: Show a => Int -> String -> a -> a
+ Hledger.Utils.Debug: dbgshow :: Show a => Int -> String -> a -> a
+ Hledger.Utils.Debug: dbgtrace :: String -> a -> a
+ Hledger.Utils.Debug: debugLevel :: Int
+ Hledger.Utils.Debug: ltrace :: Show a => String -> a -> a
+ Hledger.Utils.Debug: mtrace :: (Monad m, Show a) => a -> m a
+ Hledger.Utils.Debug: pdbg :: Stream [Char] m t => Int -> String -> ParsecT [Char] st m ()
+ Hledger.Utils.Debug: ppShow :: Show a => a -> String
+ Hledger.Utils.Debug: ptrace :: Stream [Char] m t => String -> ParsecT [Char] st m ()
+ Hledger.Utils.Debug: strace :: Show a => a -> a
+ Hledger.Utils.Debug: traceWith :: (a -> String) -> a -> a
+ Hledger.Utils.Regex: regexMatches :: Regexp -> String -> Bool
+ Hledger.Utils.Regex: regexMatchesCI :: Regexp -> String -> Bool
+ Hledger.Utils.Regex: regexReplace :: Regexp -> Replacement -> String -> String
+ Hledger.Utils.Regex: regexReplaceBy :: Regexp -> (String -> String) -> String -> String
+ Hledger.Utils.Regex: regexReplaceByCI :: Regexp -> (String -> String) -> String -> String
+ Hledger.Utils.Regex: regexReplaceCI :: Regexp -> Replacement -> String -> String
+ Hledger.Utils.Regex: regexSplit :: Regexp -> String -> [Regexp]
+ Hledger.Utils.Regex: type Regexp = String
+ Hledger.Utils.Regex: type Replacement = String
- Hledger.Data.Amount: divideAmount :: Amount -> Double -> Amount
+ Hledger.Data.Amount: divideAmount :: Amount -> Quantity -> Amount
- Hledger.Data.Amount: divideMixedAmount :: MixedAmount -> Double -> MixedAmount
+ Hledger.Data.Amount: divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount
- Hledger.Data.Amount: eur :: Quantity -> Amount
+ Hledger.Data.Amount: eur :: DecimalRaw Integer -> Amount
- Hledger.Data.Amount: gbp :: Quantity -> Amount
+ Hledger.Data.Amount: gbp :: DecimalRaw Integer -> Amount
- Hledger.Data.Amount: hrs :: Quantity -> Amount
+ Hledger.Data.Amount: hrs :: DecimalRaw Integer -> Amount
- Hledger.Data.Amount: mixed :: Amount -> MixedAmount
+ Hledger.Data.Amount: mixed :: [Amount] -> MixedAmount
- Hledger.Data.Amount: usd :: Quantity -> Amount
+ Hledger.Data.Amount: usd :: DecimalRaw Integer -> Amount
- Hledger.Data.Dates: datesepchar :: ParsecT [Char] u Identity Char
+ Hledger.Data.Dates: datesepchar :: Stream [Char] m Char => ParsecT [Char] st m Char
- Hledger.Data.Dates: showDateSpan :: DateSpan -> [Char]
+ Hledger.Data.Dates: showDateSpan :: DateSpan -> String
- Hledger.Data.Dates: smartdate :: GenParser Char st SmartDate
+ Hledger.Data.Dates: smartdate :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
- Hledger.Data.Journal: journalApplyAliases :: [(AccountName, AccountName)] -> Journal -> Journal
+ Hledger.Data.Journal: journalApplyAliases :: [AccountAlias] -> Journal -> Journal
- Hledger.Data.Journal: journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Journal -> Either String Journal
+ Hledger.Data.Journal: journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Bool -> Journal -> Either String Journal
- Hledger.Data.OutputFormat: formatsp :: GenParser Char st [OutputFormat]
+ Hledger.Data.OutputFormat: formatsp :: Stream [Char] m Char => ParsecT [Char] st m [OutputFormat]
- Hledger.Data.Posting: accountNameApplyAliases :: [(AccountName, AccountName)] -> AccountName -> AccountName
+ Hledger.Data.Posting: accountNameApplyAliases :: [AccountAlias] -> AccountName -> AccountName
- Hledger.Data.Types: AmountStyle :: Side -> Bool -> Int -> Char -> Char -> [Int] -> AmountStyle
+ Hledger.Data.Types: AmountStyle :: Side -> Bool -> Int -> Maybe Char -> Maybe DigitGroupStyle -> AmountStyle
- Hledger.Data.Types: Ctx :: !(Maybe Year) -> !(Maybe (Commodity, AmountStyle)) -> ![AccountName] -> ![(AccountName, AccountName)] -> JournalContext
+ Hledger.Data.Types: Ctx :: !(Maybe Year) -> !(Maybe (Commodity, AmountStyle)) -> ![AccountName] -> ![AccountAlias] -> JournalContext
- Hledger.Data.Types: Reader :: StorageFormat -> (FilePath -> String -> Bool) -> (Maybe FilePath -> FilePath -> String -> ErrorT String IO Journal) -> Reader
+ Hledger.Data.Types: Reader :: StorageFormat -> (FilePath -> String -> Bool) -> (Maybe FilePath -> Bool -> FilePath -> String -> ErrorT String IO Journal) -> Reader
- Hledger.Data.Types: TimeLogEntry :: TimeLogCode -> LocalTime -> String -> TimeLogEntry
+ Hledger.Data.Types: TimeLogEntry :: SourcePos -> TimeLogCode -> LocalTime -> String -> TimeLogEntry
- Hledger.Data.Types: Transaction :: Day -> Maybe Day -> Bool -> String -> String -> String -> [Tag] -> [Posting] -> String -> Transaction
+ Hledger.Data.Types: Transaction :: SourcePos -> Day -> Maybe Day -> Bool -> String -> String -> String -> [Tag] -> [Posting] -> String -> Transaction
- Hledger.Data.Types: asdecimalpoint :: AmountStyle -> Char
+ Hledger.Data.Types: asdecimalpoint :: AmountStyle -> Maybe Char
- Hledger.Data.Types: ctxAliases :: JournalContext -> ![(AccountName, AccountName)]
+ Hledger.Data.Types: ctxAliases :: JournalContext -> ![AccountAlias]
- Hledger.Data.Types: rParser :: Reader -> Maybe FilePath -> FilePath -> String -> ErrorT String IO Journal
+ Hledger.Data.Types: rParser :: Reader -> Maybe FilePath -> Bool -> FilePath -> String -> ErrorT String IO Journal
- Hledger.Data.Types: type Quantity = Double
+ Hledger.Data.Types: type Quantity = Decimal
- Hledger.Read: accountnamep :: GenParser Char st AccountName
+ Hledger.Read: accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName
- Hledger.Read: amountp :: GenParser Char JournalContext Amount
+ Hledger.Read: amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
- Hledger.Read: codep :: GenParser Char JournalContext String
+ Hledger.Read: codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
- Hledger.Read: numberp :: GenParser Char JournalContext (Quantity, Int, Char, Char, [Int])
+ Hledger.Read: numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
- Hledger.Read: postingp :: GenParser Char JournalContext Posting
+ Hledger.Read: postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting
- Hledger.Read: readJournal :: Maybe StorageFormat -> Maybe FilePath -> Maybe FilePath -> String -> IO (Either String Journal)
+ Hledger.Read: readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal)
- Hledger.Read: readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> FilePath -> IO (Either String Journal)
+ Hledger.Read: readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> Bool -> FilePath -> IO (Either String Journal)
- Hledger.Read.CsvReader: parseRulesFile :: FilePath -> IO (Either ParseError CsvRules)
+ Hledger.Read.CsvReader: parseRulesFile :: FilePath -> ErrorT String IO CsvRules
- Hledger.Read.CsvReader: transactionFromCsvRecord :: CsvRules -> CsvRecord -> Transaction
+ Hledger.Read.CsvReader: transactionFromCsvRecord :: SourcePos -> CsvRules -> CsvRecord -> Transaction
- Hledger.Read.JournalReader: accountnamep :: GenParser Char st AccountName
+ Hledger.Read.JournalReader: accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName
- Hledger.Read.JournalReader: amountp :: GenParser Char JournalContext Amount
+ Hledger.Read.JournalReader: amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
- Hledger.Read.JournalReader: codep :: GenParser Char JournalContext String
+ Hledger.Read.JournalReader: codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
- Hledger.Read.JournalReader: datetimep :: GenParser Char JournalContext LocalTime
+ Hledger.Read.JournalReader: datetimep :: Stream [Char] m Char => ParsecT [Char] JournalContext m LocalTime
- Hledger.Read.JournalReader: defaultyeardirective :: GenParser Char JournalContext JournalUpdate
+ Hledger.Read.JournalReader: defaultyeardirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate
- Hledger.Read.JournalReader: directive :: GenParser Char JournalContext JournalUpdate
+ Hledger.Read.JournalReader: directive :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate
- Hledger.Read.JournalReader: emptyorcommentlinep :: GenParser Char JournalContext ()
+ Hledger.Read.JournalReader: emptyorcommentlinep :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
- Hledger.Read.JournalReader: followingcommentp :: GenParser Char JournalContext String
+ Hledger.Read.JournalReader: followingcommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
- Hledger.Read.JournalReader: getParentAccount :: GenParser tok JournalContext String
+ Hledger.Read.JournalReader: getParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
- Hledger.Read.JournalReader: historicalpricedirective :: GenParser Char JournalContext HistoricalPrice
+ Hledger.Read.JournalReader: historicalpricedirective :: ParsecT [Char] JournalContext (ErrorT String IO) HistoricalPrice
- Hledger.Read.JournalReader: journal :: GenParser Char JournalContext (JournalUpdate, JournalContext)
+ Hledger.Read.JournalReader: journal :: ParsecT [Char] JournalContext (ErrorT String IO) (JournalUpdate, JournalContext)
- Hledger.Read.JournalReader: numberp :: GenParser Char JournalContext (Quantity, Int, Char, Char, [Int])
+ Hledger.Read.JournalReader: numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
- Hledger.Read.JournalReader: parseJournalWith :: (GenParser Char JournalContext (JournalUpdate, JournalContext)) -> FilePath -> String -> ErrorT String IO Journal
+ Hledger.Read.JournalReader: parseJournalWith :: (ParsecT [Char] JournalContext (ErrorT String IO) (JournalUpdate, JournalContext)) -> Bool -> FilePath -> String -> ErrorT String IO Journal
- Hledger.Read.JournalReader: postingp :: GenParser Char JournalContext Posting
+ Hledger.Read.JournalReader: postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting
- Hledger.Reports.PostingsReport: mkpostingsReportItem :: Bool -> Bool -> WhichDate -> Posting -> MixedAmount -> PostingsReportItem
+ Hledger.Reports.PostingsReport: mkpostingsReportItem :: Bool -> Bool -> WhichDate -> Maybe Day -> Posting -> MixedAmount -> PostingsReportItem
- Hledger.Reports.PostingsReport: type PostingsReportItem = (Maybe Day, Maybe String, Posting, MixedAmount)
+ Hledger.Reports.PostingsReport: type PostingsReportItem = (Maybe Day, Maybe Day, Maybe String, Posting, MixedAmount)
- Hledger.Reports.ReportOptions: ReportOpts :: Maybe Day -> Maybe Day -> Maybe (Interval, DateSpan) -> Bool -> Bool -> Bool -> Maybe Int -> Maybe DisplayExp -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FormatStr -> String -> Bool -> Bool -> BalanceType -> Bool -> Bool -> Int -> Bool -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Maybe Day -> Maybe Day -> Maybe (Interval, DateSpan) -> Bool -> Bool -> Bool -> Maybe Int -> Maybe DisplayExp -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FormatStr -> String -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> ReportOpts
- Hledger.Reports.TransactionsReports: accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> TransactionsReport
+ Hledger.Reports.TransactionsReports: accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport
- Hledger.Reports.TransactionsReports: transactionsReportByCommodity :: TransactionsReport -> [TransactionsReport]
+ Hledger.Reports.TransactionsReports: transactionsReportByCommodity :: TransactionsReport -> [(Commodity, TransactionsReport)]
- Hledger.Reports.TransactionsReports: triDate :: (Transaction, t, t1, t2, t3, t4) -> Day
+ Hledger.Reports.TransactionsReports: triDate :: (t, Transaction, t1, t2, t3, t4) -> Day
- Hledger.Utils: choice' :: [GenParser tok st a] -> GenParser tok st a
+ Hledger.Utils: choice' :: Stream s m t => [ParsecT s u m a] -> ParsecT s u m a
- Hledger.Utils: eolof :: GenParser Char st ()
+ Hledger.Utils: eolof :: Stream [Char] m Char => ParsecT [Char] st m ()
- Hledger.Utils: nonspace :: GenParser Char st Char
+ Hledger.Utils: nonspace :: Stream [Char] m Char => ParsecT [Char] st m Char
- Hledger.Utils: parseWithCtx :: b -> GenParser Char b a -> String -> Either ParseError a
+ Hledger.Utils: parseWithCtx :: Stream s m t => u -> ParsecT s u m a -> s -> m (Either ParseError a)
- Hledger.Utils: parsewith :: Parser a -> String -> Either ParseError a
+ Hledger.Utils: parsewith :: Parsec [Char] () a -> String -> Either ParseError a
- Hledger.Utils: restofline :: GenParser Char st String
+ Hledger.Utils: restofline :: Stream [Char] m Char => ParsecT [Char] st m String
- Hledger.Utils: spacenonewline :: GenParser Char st Char
+ Hledger.Utils: spacenonewline :: Stream [Char] m Char => ParsecT [Char] st m Char

Files

CHANGES view
@@ -1,4 +1,21 @@-API-ish changes in hledger-lib. For user-visible changes, see the hledger changelog.+API-ish changes in hledger-lib. For user-visible changes, see hledger.++0.24 (unreleased)++- fix combineJournalUpdates folding order+- fix a regexReplaceCI bug+- fix a splitAtElement bug with adjacent separators+- mostly replace slow regexpr with regex-tdfa (fixes #189)+- use the modern Text.Parsec API+- allow transformers 0.4*+- regexReplace now supports backreferences+- Transactions now remember their parse location in the journal file+- export Regexp types, disambiguate CsvReader's similarly-named type+- export failIfInvalidMonth/Day (fixes #216)+- track the commodity of zero amounts when possible+  (useful eg for hledger-web's multi-commodity charts)+- show posting dates in debug output+- more debug helpers  0.23.3 (2014/9/12) 
Hledger/Data.hs view
@@ -1,4 +1,4 @@-{-| +{-|  The Hledger.Data library allows parsing and querying of C++ ledger-style journal files.  It generally provides a compatible subset of C++ ledger's
Hledger/Data/Account.hs view
@@ -115,7 +115,7 @@  -- | Remove all subaccounts below a certain depth. clipAccounts :: Int -> Account -> Account-clipAccounts 0 a = a{asubs=[]} +clipAccounts 0 a = a{asubs=[]} clipAccounts d a = a{asubs=subs}     where       subs = map (clipAccounts (d-1)) $ asubs a@@ -125,7 +125,7 @@ clipAccountsAndAggregate :: Int -> [Account] -> [Account] clipAccountsAndAggregate d as = combined     where-      clipped  = [a{aname=clipAccountName d $ aname a} | a <- as]+      clipped  = [a{aname=clipOrEllipsifyAccountName d $ aname a} | a <- as]       combined = [a{aebalance=sum (map aebalance same)}                   | same@(a:_) <- groupBy (\a1 a2 -> aname a1 == aname a2) clipped] {-
Hledger/Data/AccountName.hs view
@@ -63,7 +63,7 @@ isAccountNamePrefixOf = isPrefixOf . (++ [acctsepchar])  isSubAccountNameOf :: AccountName -> AccountName -> Bool-s `isSubAccountNameOf` p = +s `isSubAccountNameOf` p =     (p `isAccountNamePrefixOf` s) && (accountNameLevel s == (accountNameLevel p + 1))  -- | From a list of account names, select those which are direct@@ -73,7 +73,7 @@  -- | Convert a list of account names to a tree. accountNameTreeFrom :: [AccountName] -> Tree AccountName-accountNameTreeFrom accts = +accountNameTreeFrom accts =     Node "root" (accounttreesfrom (topAccountNames accts))         where           accounttreesfrom :: [AccountName] -> [Tree AccountName]@@ -85,7 +85,7 @@  -- | Elide an account name to fit in the specified width. -- From the ledger 2.6 news:--- +-- -- @ --   What Ledger now does is that if an account name is too long, it will --   start abbreviating the first parts of the account name down to two@@ -99,7 +99,7 @@ --     ..:Af:Lu:Sn:Ca:Ch:Cash  ; Abbreviated and elided! -- @ elideAccountName :: Int -> AccountName -> AccountName-elideAccountName width s = +elideAccountName width s =     elideLeft width $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s       where         elideparts :: Int -> [String] -> [String] -> [String]@@ -108,8 +108,16 @@           | length ss > 1 = elideparts width (done++[take 2 $ head ss]) (tail ss)           | otherwise = done++ss +-- | Keep only the first n components of an account name, where n+-- is a positive integer. If n is 0, returns the empty string. clipAccountName :: Int -> AccountName -> AccountName clipAccountName n = accountNameFromComponents . take n . accountNameComponents++-- | Keep only the first n components of an account name, where n+-- is a positive integer. If n is 0, returns "...".+clipOrEllipsifyAccountName :: Int -> AccountName -> AccountName+clipOrEllipsifyAccountName 0 = const "..."+clipOrEllipsifyAccountName n = accountNameFromComponents . take n . accountNameComponents  -- | Convert an account name to a regular expression matching it and its subaccounts. accountNameToAccountRegex :: String -> String
Hledger/Data/Amount.hs view
@@ -1,16 +1,16 @@-{-# LANGUAGE StandaloneDeriving, RecordWildCards  #-}+{-# LANGUAGE CPP, StandaloneDeriving, RecordWildCards #-} {-| A simple 'Amount' is some quantity of money, shares, or anything else. It has a (possibly null) 'Commodity' and a numeric quantity:  @-  $1 +  $1   £-50-  EUR 3.44 +  EUR 3.44   GOOG 500   1.5h   90 apples-  0 +  0 @  It may also have an assigned 'Price', representing this amount's per-unit@@ -57,10 +57,10 @@   -- ** arithmetic   costOfAmount,   divideAmount,-  sumAmounts,   -- ** rendering   amountstyle,   showAmount,+  showAmountWithZeroCommodity,   showAmountDebug,   showAmountWithoutPrice,   maxprecision,@@ -68,14 +68,15 @@   setAmountPrecision,   withPrecision,   canonicaliseAmount,-  canonicalStyles,   -- * MixedAmount   nullmixedamt,   missingmixedamt,   mixed,   amounts,-  normaliseMixedAmountPreservingFirstPrice,-  normaliseMixedAmountPreservingPrices,+  filterMixedAmount,+  filterMixedAmountByCommodity,+  normaliseMixedAmountSquashPricesForDisplay,+  normaliseMixedAmount,   -- ** arithmetic   costOfMixedAmount,   divideMixedAmount,@@ -87,6 +88,8 @@   showMixedAmount,   showMixedAmountDebug,   showMixedAmountWithoutPrice,+  showMixedAmountOneLineWithoutPrice,+  showMixedAmountWithZeroCommodity,   showMixedAmountWithPrecision,   setMixedAmountPrecision,   canonicaliseMixedAmount,@@ -96,9 +99,13 @@ ) where  import Data.Char (isDigit)+#ifndef DOUBLE+import Data.Decimal+#endif+import Data.Function (on) import Data.List import Data.Map (findWithDefault)-import Data.Ord (comparing)+import Data.Maybe import Test.HUnit import Text.Printf import qualified Data.Map as M@@ -110,7 +117,7 @@  deriving instance Show HistoricalPrice -amountstyle = AmountStyle L False 0 '.' ',' []+amountstyle = AmountStyle L False 0 (Just '.') Nothing  ------------------------------------------------------------------------------- -- Amount@@ -138,26 +145,35 @@ amount = Amount{acommodity="", aquantity=0, aprice=NoPrice, astyle=amountstyle} nullamt = amount +-- | A temporary value for parsed transactions which had no amount specified.+missingamt :: Amount+missingamt = amount{acommodity="AUTO"}+ -- handy amount constructors for tests+#ifdef DOUBLE+roundTo = flip const+#endif num n = amount{acommodity="",  aquantity=n}-usd n = amount{acommodity="$", aquantity=n, astyle=amountstyle{asprecision=2}}-eur n = amount{acommodity="€", aquantity=n, astyle=amountstyle{asprecision=2}}-gbp n = amount{acommodity="£", aquantity=n, astyle=amountstyle{asprecision=2}}-hrs n = amount{acommodity="h", aquantity=n, astyle=amountstyle{asprecision=1, ascommodityside=R}}+usd n = amount{acommodity="$", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=2}}+eur n = amount{acommodity="€", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=2}}+gbp n = amount{acommodity="£", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=2}}+hrs n = amount{acommodity="h", aquantity=roundTo 1 n, astyle=amountstyle{asprecision=1, ascommodityside=R}}+amt `at` priceamt = amt{aprice=UnitPrice priceamt}+amt @@ priceamt = amt{aprice=TotalPrice priceamt} --- | Apply a binary arithmetic operator to two amounts in the same--- commodity.  Warning, as a kludge to support folds (eg sum) we assign--- the second's commodity to the first so the same commodity requirement--- is not checked. The highest precision of either amount is preserved in--- the result. Any prices are currently ignored and discarded. The display--- style is that of the first amount, with precision set to the highest of--- either amount.-similarAmountsOp :: (Double -> Double -> Double) -> Amount -> Amount -> Amount-similarAmountsOp op Amount{acommodity=_,  aquantity=aq, astyle=AmountStyle{asprecision=ap}}-                    Amount{acommodity=bc, aquantity=bq, astyle=bs@AmountStyle{asprecision=bp}} =-   -- trace ("a:"++showAmount a) $ trace ("b:"++showAmount b++"\n") $ tracewith (("=:"++).showAmount)-   amount{acommodity=bc, aquantity=aq `op` bq, astyle=bs{asprecision=max ap bp}}-  --  ac==bc    = amount{acommodity=ac, aquantity=aq `op` bq, astyle=as{asprecision=max ap bp}}+-- | Apply a binary arithmetic operator to two amounts, which should+-- be in the same commodity if non-zero (warning, this is not checked).+-- A zero result keeps the commodity of the second amount.+-- The result's display style is that of the second amount, with+-- precision set to the highest of either amount.+-- Prices are ignored and discarded.+-- Remember: the caller is responsible for ensuring both amounts have the same commodity.+similarAmountsOp :: (Quantity -> Quantity -> Quantity) -> Amount -> Amount -> Amount+similarAmountsOp op Amount{acommodity=_,  aquantity=q1, astyle=AmountStyle{asprecision=p1}}+                    Amount{acommodity=c2, aquantity=q2, astyle=s2@AmountStyle{asprecision=p2}} =+   -- trace ("a1:"++showAmountDebug a1) $ trace ("a2:"++showAmountDebug a2) $ traceWith (("= :"++).showAmountDebug)+   amount{acommodity=c2, aquantity=q1 `op` q2, astyle=s2{asprecision=max p1 p2}}+  --  c1==c2 || q1==0 || q2==0 =   --  otherwise = error "tried to do simple arithmetic with amounts in different commodities"  -- | Convert an amount to the specified commodity, ignoring and discarding@@ -165,30 +181,6 @@ amountWithCommodity :: Commodity -> Amount -> Amount amountWithCommodity c a = a{acommodity=c, aprice=NoPrice} --- | A more complete amount adding operation.-sumAmounts :: [Amount] -> MixedAmount-sumAmounts = normaliseMixedAmountPreservingPrices . Mixed---- | Set an amount's unit price.-at :: Amount -> Amount -> Amount-amt `at` priceamt = amt{aprice=UnitPrice priceamt}---- | Set an amount's total price.-(@@) :: Amount -> Amount -> Amount-amt @@ priceamt = amt{aprice=TotalPrice priceamt}--tests_sumAmounts = [-  "sumAmounts" ~: do-    -- when adding, we don't convert to the price commodity - just-    -- combine what amounts we can.-    -- amounts with same unit price-    sumAmounts [usd 1 `at` eur 1, usd 1 `at` eur 1] `is` Mixed [usd 2 `at` eur 1]-    -- amounts with different unit prices-    -- amounts with total prices-    sumAmounts  [usd 1 @@ eur 1, usd 1 @@ eur 1] `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]-    -- amounts with no, unit, and/or total prices- ]- -- | Convert an amount to the commodity of its assigned price, if any.  Notes: -- -- - price amounts must be MixedAmounts with exactly one component Amount (or there will be a runtime error)@@ -202,7 +194,7 @@       TotalPrice p@Amount{aquantity=pq} -> p{aquantity=pq * signum q}  -- | Divide an amount's quantity by a constant.-divideAmount :: Amount -> Double -> Amount+divideAmount :: Amount -> Quantity -> Amount divideAmount a@Amount{aquantity=q} d = a{aquantity=q/d}  -- | Is this amount negative ? The price is ignored.@@ -219,9 +211,14 @@ -- | Is this amount "really" zero, regardless of the display precision ? -- Since we are using floating point, for now just test to some high precision. isReallyZeroAmount :: Amount -> Bool-isReallyZeroAmount a --  a==missingamt = False-                     | otherwise     = (null . filter (`elem` digits) . printf ("%."++show zeroprecision++"f") . aquantity) a-    where zeroprecision = 8+isReallyZeroAmount Amount{aquantity=q} = iszero q+  where+   iszero =+#ifdef DOUBLE+    null . filter (`elem` digits) . printf ("%."++show zeroprecision++"f") where zeroprecision = 8+#else+    (==0)+#endif  -- | Get the string representation of an amount, based on its commodity's -- display settings except using the specified precision.@@ -260,54 +257,73 @@ showPriceDebug (UnitPrice pa)  = " @ "  ++ showAmountDebug pa showPriceDebug (TotalPrice pa) = " @@ " ++ showAmountDebug pa --- | Get the string representation of an amount, based on its commodity's--- display settings. String representations equivalent to zero are--- converted to just \"0\".+-- | Get the string representation of an amount, based on its+-- commodity's display settings. String representations equivalent to+-- zero are converted to just \"0\". The special "missing" amount is+-- displayed as the empty string. showAmount :: Amount -> String-showAmount Amount{acommodity="AUTO"} = ""-showAmount a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) =+showAmount = showAmountHelper False++showAmountHelper :: Bool -> Amount -> String+showAmountHelper _ Amount{acommodity="AUTO"} = ""+showAmountHelper showzerocommodity a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) =     case ascommodityside of       L -> printf "%s%s%s%s" c' space quantity' price       R -> printf "%s%s%s%s" quantity' space c' price     where       quantity = showamountquantity a       displayingzero = null $ filter (`elem` digits) $ quantity-      (quantity',c') | displayingzero = ("0","")-                     | otherwise      = (quantity, quoteCommoditySymbolIfNeeded c)+      (quantity',c') | displayingzero && not showzerocommodity = ("0","")+                     | otherwise = (quantity, quoteCommoditySymbolIfNeeded c)       space = if (not (null c') && ascommodityspaced) then " " else "" :: String       price = showPrice p +-- | Like showAmount, but show a zero amount's commodity if it has one.+showAmountWithZeroCommodity :: Amount -> String+showAmountWithZeroCommodity = showAmountHelper True+ -- | Get the string representation of the number part of of an amount, -- using the display settings from its commodity. showamountquantity :: Amount -> String-showamountquantity Amount{aquantity=q, astyle=AmountStyle{asprecision=p, asdecimalpoint=d, asseparator=s, asseparatorpositions=spos}} =-    punctuatenumber d s spos $ qstr+showamountquantity Amount{aquantity=q, astyle=AmountStyle{asprecision=p, asdecimalpoint=mdec, asdigitgroups=mgrps}} =+    punctuatenumber (fromMaybe '.' mdec) mgrps $ qstr     where-    -- isint n = fromIntegral (round n) == n-    qstr -- p == maxprecision && isint q = printf "%d" (round q::Integer)-         | p == maxprecisionwithpoint    = printf "%f" q-         | p == maxprecision             = chopdotzero $ printf "%f" q-         | otherwise                    = printf ("%."++show p++"f") q+      -- isint n = fromIntegral (round n) == n+      qstr -- p == maxprecision && isint q = printf "%d" (round q::Integer)+#ifdef DOUBLE+        | p == maxprecisionwithpoint = printf "%f" q+        | p == maxprecision          = chopdotzero $ printf "%f" q+        | otherwise                  = printf ("%."++show p++"f") q+#else+        | p == maxprecisionwithpoint = show q+        | p == maxprecision          = chopdotzero $ show q+        | otherwise                  = show $ roundTo (fromIntegral p) q+#endif  -- | Replace a number string's decimal point with the specified character, -- and add the specified digit group separators. The last digit group will -- be repeated as needed.-punctuatenumber :: Char -> Char -> [Int] -> String -> String-punctuatenumber dec sep grps str = sign ++ reverse (addseps sep (extend grps) (reverse int)) ++ frac''+punctuatenumber :: Char -> Maybe DigitGroupStyle -> String -> String+punctuatenumber dec mgrps s = sign ++ reverse (applyDigitGroupStyle mgrps (reverse int)) ++ frac''     where-      (sign,num) = break isDigit str+      (sign,num) = break isDigit s       (int,frac) = break (=='.') num       frac' = dropWhile (=='.') frac       frac'' | null frac' = ""              | otherwise  = dec:frac'-      extend [] = []-      extend gs = init gs ++ repeat (last gs)-      addseps _ [] str = str-      addseps sep (g:gs) str-          | length str <= g = str-          | otherwise = let (s,rest) = splitAt g str-                        in s ++ [sep] ++ addseps sep gs rest +applyDigitGroupStyle :: Maybe DigitGroupStyle -> String -> String+applyDigitGroupStyle Nothing s = s+applyDigitGroupStyle (Just (DigitGroups c gs)) s = addseps (repeatLast gs) s+  where+    addseps [] s = s+    addseps (g:gs) s+      | length s <= g = s+      | otherwise     = let (part,rest) = splitAt g s+                        in part ++ [c] ++ addseps gs rest+    repeatLast [] = []+    repeatLast gs = init gs ++ repeat (last gs)+ chopdotzero str = reverse $ case reverse str of                               '0':'.':s -> s                               s         -> s@@ -339,108 +355,144 @@ instance Num MixedAmount where     fromInteger i = Mixed [fromInteger i]     negate (Mixed as) = Mixed $ map negate as-    (+) (Mixed as) (Mixed bs) = normaliseMixedAmountPreservingPrices $ Mixed $ as ++ bs-    (*)    = error' "programming error, mixed amounts do not support multiplication"-    abs    = error' "programming error, mixed amounts do not support abs"-    signum = error' "programming error, mixed amounts do not support signum"+    (+) (Mixed as) (Mixed bs) = normaliseMixedAmount $ Mixed $ as ++ bs+    (*)    = error' "error, mixed amounts do not support multiplication"+    abs    = error' "error, mixed amounts do not support abs"+    signum = error' "error, mixed amounts do not support signum"  -- | The empty mixed amount. nullmixedamt :: MixedAmount nullmixedamt = Mixed []  -- | A temporary value for parsed transactions which had no amount specified.-missingamt :: Amount-missingamt = amount{acommodity="AUTO"}- missingmixedamt :: MixedAmount missingmixedamt = Mixed [missingamt] -mixed :: Amount -> MixedAmount-mixed a = Mixed [a]-  --- | Simplify a mixed amount's component amounts: we can combine amounts--- with the same commodity and unit price. Also remove any zero or missing--- amounts and replace an empty amount list with a single zero amount.-normaliseMixedAmountPreservingPrices :: MixedAmount -> MixedAmount-normaliseMixedAmountPreservingPrices (Mixed as) = Mixed as''-    where-      as'' = if null nonzeros then [nullamt] else nonzeros-      (_,nonzeros) = partition isReallyZeroAmount as'-      as' = map sumAmountsUsingFirstPrice $ group $ sort $ filter (/= missingamt) as-      sort = sortBy (\a1 a2 -> compare (acommodity a1, aprice a1) (acommodity a2, aprice a2))-      group = groupBy (\a1 a2 -> acommodity a1 == acommodity a2 && sameunitprice a1 a2)-        where-          sameunitprice a1 a2 =-            case (aprice a1, aprice a2) of-              (NoPrice, NoPrice) -> True-              (UnitPrice p1, UnitPrice p2) -> p1 == p2-              _ -> False+-- | Convert amounts in various commodities into a normalised MixedAmount.+mixed :: [Amount] -> MixedAmount+mixed = normaliseMixedAmount . Mixed -tests_normaliseMixedAmountPreservingPrices = [-  "normaliseMixedAmountPreservingPrices" ~: do-   assertEqual "discard missing amount" (Mixed [nullamt]) (normaliseMixedAmountPreservingPrices $ Mixed [usd 0, missingamt])-   assertEqual "combine unpriced same-commodity amounts" (Mixed [usd 2]) (normaliseMixedAmountPreservingPrices $ Mixed [usd 0, usd 2])-   assertEqual "don't combine total-priced amounts"-     (Mixed-      [usd 1 @@ eur 1-      ,usd (-2) @@ eur 1-      ])-     (normaliseMixedAmountPreservingPrices $ Mixed-      [usd 1 @@ eur 1-      ,usd (-2) @@ eur 1-      ])+-- | Simplify a mixed amount's component amounts:+--+-- * amounts in the same commodity are combined unless they have different prices or total prices+--+-- * multiple zero amounts are replaced by just one. If they had the same commodity, it is preserved.+--+-- * an empty amount list is replaced with a single commodityless zero+--+-- * the special "missing" mixed amount remains unchanged+--+normaliseMixedAmount :: MixedAmount -> MixedAmount+normaliseMixedAmount = normaliseHelper False +normaliseHelper :: Bool -> MixedAmount -> MixedAmount+normaliseHelper squashprices (Mixed as)+  | missingamt `elem` as = missingmixedamt -- missingamt should always be alone, but detect it even if not+  | null nonzeros        = Mixed [newzero]+  | otherwise            = Mixed nonzeros+  where+    newzero = case filter (/= "") (map acommodity zeros) of+               [c] -> nullamt{acommodity=c}+               _   -> nullamt+    (zeros, nonzeros) = partition isReallyZeroAmount $+                        map sumSimilarAmountsUsingFirstPrice $+                        groupBy groupfn $+                        sortBy sortfn $+                        as+    sortfn  | squashprices = compare `on` acommodity+            | otherwise    = compare `on` \a -> (acommodity a, aprice a)+    groupfn | squashprices = (==) `on` acommodity+            | otherwise    = \a1 a2 -> acommodity a1 == acommodity a2 && combinableprices a1 a2++    combinableprices Amount{aprice=NoPrice} Amount{aprice=NoPrice} = True+    combinableprices Amount{aprice=UnitPrice p1} Amount{aprice=UnitPrice p2} = p1 == p2+    combinableprices _ _ = False++tests_normaliseMixedAmount = [+  "normaliseMixedAmount" ~: do+   -- assertEqual "missing amount is discarded" (Mixed [nullamt]) (normaliseMixedAmount $ Mixed [usd 0, missingamt])+   assertEqual "any missing amount means a missing mixed amount" missingmixedamt (normaliseMixedAmount $ Mixed [usd 0, missingamt])+   assertEqual "unpriced same-commodity amounts are combined" (Mixed [usd 2]) (normaliseMixedAmount $ Mixed [usd 0, usd 2])+   -- amounts with same unit price are combined+   normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) `is` Mixed [usd 2 `at` eur 1]+   -- amounts with different unit prices are not combined+   normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) `is` Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]+   -- amounts with total prices are not combined+   normaliseMixedAmount (Mixed  [usd 1 @@ eur 1, usd 1 @@ eur 1]) `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]  ] --- | Simplify a mixed amount's component amounts: combine amounts with--- the same commodity, using the first amount's price for subsequent--- amounts in each commodity (ie, this function alters the amount and--- is best used as a rendering helper.). Also remove any zero amounts--- and replace an empty amount list with a single zero amount.-normaliseMixedAmountPreservingFirstPrice :: MixedAmount -> MixedAmount-normaliseMixedAmountPreservingFirstPrice (Mixed as) = Mixed as''-    where -      as'' = if null nonzeros then [nullamt] else nonzeros-      (_,nonzeros) = partition (\a -> isReallyZeroAmount a && a /= missingamt) as'-      as' = map sumAmountsUsingFirstPrice $ group $ sort as-      sort = sortBy (\a1 a2 -> compare (acommodity a1) (acommodity a2))-      group = groupBy (\a1 a2 -> acommodity a1 == acommodity a2)+-- | Like normaliseMixedAmount, but combine each commodity's amounts+-- into just one by throwing away all prices except the first. This is+-- only used as a rendering helper, and could show a misleading price.+normaliseMixedAmountSquashPricesForDisplay :: MixedAmount -> MixedAmount+normaliseMixedAmountSquashPricesForDisplay = normaliseHelper True --- discardPrice :: Amount -> Amount--- discardPrice a = a{price=Nothing}+tests_normaliseMixedAmountSquashPricesForDisplay = [+  "normaliseMixedAmountSquashPricesForDisplay" ~: do+    normaliseMixedAmountSquashPricesForDisplay (Mixed []) `is` Mixed [nullamt]+    assertBool "" $ isZeroMixedAmount $ normaliseMixedAmountSquashPricesForDisplay+      (Mixed [usd 10+             ,usd 10 @@ eur 7+             ,usd (-10)+             ,usd (-10) @@ eur 7+             ])+ ] --- discardPrices :: MixedAmount -> MixedAmount--- discardPrices (Mixed as) = Mixed $ map discardPrice as+-- | Sum same-commodity amounts in a lossy way, applying the first+-- price to the result and discarding any other prices. Only used as a+-- rendering helper.+sumSimilarAmountsUsingFirstPrice :: [Amount] -> Amount+sumSimilarAmountsUsingFirstPrice [] = nullamt+sumSimilarAmountsUsingFirstPrice as = (sum as){aprice=aprice $ head as} -sumAmountsUsingFirstPrice [] = nullamt-sumAmountsUsingFirstPrice as = (sum as){aprice=aprice $ head as}+-- | Sum same-commodity amounts. If there were different prices, set+-- the price to a special marker indicating "various". Only used as a+-- rendering helper.+-- sumSimilarAmountsNotingPriceDifference :: [Amount] -> Amount+-- sumSimilarAmountsNotingPriceDifference [] = nullamt+-- sumSimilarAmountsNotingPriceDifference as = undefined  -- | Get a mixed amount's component amounts. amounts :: MixedAmount -> [Amount] amounts (Mixed as) = as +-- | Filter a mixed amount's component amounts by a predicate.+filterMixedAmount :: (Amount -> Bool) -> MixedAmount -> MixedAmount+filterMixedAmount p (Mixed as) = Mixed $ filter p as++-- | Return an unnormalised MixedAmount containing exactly one Amount+-- with the specified commodity and the quantity of that commodity+-- found in the original. NB if Amount's quantity is zero it will be+-- discarded next time the MixedAmount gets normalised.+filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount+filterMixedAmountByCommodity c (Mixed as) = Mixed as'+  where+    as' = case filter ((==c) . acommodity) as of+            []   -> [nullamt{acommodity=c}]+            as'' -> [sum as'']+ -- | Convert a mixed amount's component amounts to the commodity of their -- assigned price, if any. costOfMixedAmount :: MixedAmount -> MixedAmount costOfMixedAmount (Mixed as) = Mixed $ map costOfAmount as  -- | Divide a mixed amount's quantities by a constant.-divideMixedAmount :: MixedAmount -> Double -> MixedAmount+divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount divideMixedAmount (Mixed as) d = Mixed $ map (flip divideAmount d) as  -- | Is this mixed amount negative, if it can be normalised to a single commodity ? isNegativeMixedAmount :: MixedAmount -> Maybe Bool isNegativeMixedAmount m = case as of [a] -> Just $ isNegativeAmount a                                      _   -> Nothing-    where as = amounts $ normaliseMixedAmountPreservingFirstPrice m+    where as = amounts $ normaliseMixedAmountSquashPricesForDisplay m  -- | Does this mixed amount appear to be zero when displayed with its given precision ? isZeroMixedAmount :: MixedAmount -> Bool-isZeroMixedAmount = all isZeroAmount . amounts . normaliseMixedAmountPreservingFirstPrice+isZeroMixedAmount = all isZeroAmount . amounts . normaliseMixedAmountSquashPricesForDisplay  -- | Is this mixed amount "really" zero ? See isReallyZeroAmount. isReallyZeroMixedAmount :: MixedAmount -> Bool-isReallyZeroMixedAmount = all isReallyZeroAmount . amounts . normaliseMixedAmountPreservingFirstPrice+isReallyZeroMixedAmount = all isReallyZeroAmount . amounts . normaliseMixedAmountSquashPricesForDisplay  -- | Is this mixed amount "really" zero, after converting to cost -- commodities where possible ?@@ -452,15 +504,27 @@ -- -- For now, use this when cross-commodity zero equality is important. -- mixedAmountEquals :: MixedAmount -> MixedAmount -> Bool -- mixedAmountEquals a b = amounts a' == amounts b' || (isZeroMixedAmount a' && isZeroMixedAmount b')---     where a' = normaliseMixedAmountPreservingFirstPrice a---           b' = normaliseMixedAmountPreservingFirstPrice b+--     where a' = normaliseMixedAmountSquashPricesForDisplay a+--           b' = normaliseMixedAmountSquashPricesForDisplay b --- | Get the string representation of a mixed amount, showing each of--- its component amounts. NB a mixed amount can have an empty amounts--- list in which case it shows as \"\".+-- | Get the string representation of a mixed amount, after+-- normalising it to one amount per commodity. Assumes amounts have+-- no or similar prices, otherwise this can show misleading prices. showMixedAmount :: MixedAmount -> String-showMixedAmount m = vConcatRightAligned $ map showAmount $ amounts $  normaliseMixedAmountPreservingFirstPrice m+showMixedAmount = showMixedAmountHelper False +-- | Like showMixedAmount, but zero amounts are shown with their+-- commodity if they have one.+showMixedAmountWithZeroCommodity :: MixedAmount -> String+showMixedAmountWithZeroCommodity = showMixedAmountHelper True++showMixedAmountHelper :: Bool -> MixedAmount -> String+showMixedAmountHelper showzerocommodity m =+  vConcatRightAligned $ map showw $ amounts $ normaliseMixedAmountSquashPricesForDisplay m+  where+    showw | showzerocommodity = showAmountWithZeroCommodity+          | otherwise         = showAmount+ -- | Compact labelled trace of a mixed amount, for debugging. ltraceamount :: String -> MixedAmount -> MixedAmount ltraceamount s = traceWith (((s ++ ": ") ++).showMixedAmount)@@ -474,51 +538,42 @@ -- commoditys' display precision settings. showMixedAmountWithPrecision :: Int -> MixedAmount -> String showMixedAmountWithPrecision p m =-    vConcatRightAligned $ map (showAmountWithPrecision p) $ amounts $ normaliseMixedAmountPreservingFirstPrice m+    vConcatRightAligned $ map (showAmountWithPrecision p) $ amounts $ normaliseMixedAmountSquashPricesForDisplay m  -- | Get an unambiguous string representation of a mixed amount for debugging. showMixedAmountDebug :: MixedAmount -> String showMixedAmountDebug m | m == missingmixedamt = "(missing)"                        | otherwise       = printf "Mixed [%s]" as-    where as = intercalate "\n       " $ map showAmountDebug $ amounts m -- normaliseMixedAmountPreservingFirstPrice m+    where as = intercalate "\n       " $ map showAmountDebug $ amounts m  -- | Get the string representation of a mixed amount, but without -- any \@ prices. showMixedAmountWithoutPrice :: MixedAmount -> String showMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showfixedwidth as     where-      (Mixed as) = normaliseMixedAmountPreservingFirstPrice $ stripPrices m+      (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m       stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}       width = maximum $ map (length . showAmount) as       showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice +-- | Get the one-line string representation of a mixed amount, but without+-- any \@ prices.+showMixedAmountOneLineWithoutPrice :: MixedAmount -> String+showMixedAmountOneLineWithoutPrice m = concat $ intersperse ", " $ map showAmountWithoutPrice as+    where+      (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m+      stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}+ -- | Canonicalise a mixed amount's display styles using the provided commodity style map. canonicaliseMixedAmount :: M.Map Commodity AmountStyle -> MixedAmount -> MixedAmount canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as --- | Given a list of amounts in parse order, build a map from commodities--- to canonical display styles for amounts in that commodity.-canonicalStyles :: [Amount] -> M.Map Commodity AmountStyle-canonicalStyles amts = M.fromList commstyles-  where-    samecomm = \a1 a2 -> acommodity a1 == acommodity a2-    commamts = [(acommodity $ head as, as) | as <- groupBy samecomm $ sortBy (comparing acommodity) amts]-    commstyles = [(c, s)-                 | (c,as) <- commamts-                 , let styles = map astyle as-                 , let maxprec = maximum $ map asprecision styles-                 , let s = (head styles){asprecision=maxprec}-                 ]---- lookupStyle :: M.Map Commodity AmountStyle -> Commodity -> AmountStyle--- lookupStyle - ------------------------------------------------------------------------------- -- misc  tests_Hledger_Data_Amount = TestList $-     tests_normaliseMixedAmountPreservingPrices-  ++ tests_sumAmounts+     tests_normaliseMixedAmount+  ++ tests_normaliseMixedAmountSquashPricesForDisplay   ++ [    -- Amount@@ -561,25 +616,16 @@    -- MixedAmount -  ,"normaliseMixedAmountPreservingFirstPrice" ~: do-    normaliseMixedAmountPreservingFirstPrice (Mixed []) `is` Mixed [nullamt]-    assertBool "" $ isZeroMixedAmount $ normaliseMixedAmountPreservingFirstPrice-      (Mixed [usd 10-             ,usd 10 @@ eur 7-             ,usd (-10)-             ,usd (-10) @@ eur 7-             ])--  ,"adding mixed amounts" ~: do-    (sum $ map (Mixed . (\a -> [a]))+  ,"adding mixed amounts, preserving minimum precision and a single commodity on zero" ~: do+    (sum $ map (Mixed . (:[]))              [usd 1.25              ,usd (-1) `withPrecision` 0              ,usd (-0.25)              ])-      `is` Mixed [amount{aquantity=0}]-  +      `is` Mixed [usd 0 `withPrecision` 0]+   ,"adding mixed amounts with total prices" ~: do-    (sum $ map (Mixed . (\a -> [a]))+    (sum $ map (Mixed . (:[]))      [usd 1 @@ eur 1      ,usd (-2) @@ eur 1      ])
Hledger/Data/Commodity.hs view
@@ -43,8 +43,8 @@  -- | Look up one of the sample commodities' symbol by name. comm :: String -> Commodity-comm name = snd $ fromMaybe -              (error' "commodity lookup failed") +comm name = snd $ fromMaybe+              (error' "commodity lookup failed")               (find (\n -> fst n == name) commoditysymbols)  -- | Find the conversion rate between two commodities. Currently returns 1.
Hledger/Data/Dates.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoMonoLocalBinds #-}+{-# LANGUAGE FlexibleContexts #-} {-|  Date parsing and utilities for hledger.@@ -38,6 +39,8 @@   nulldatespan,   tests_Hledger_Data_Dates,   failIfInvalidYear,+  failIfInvalidMonth,+  failIfInvalidDay,   datesepchar,   datesepchars,   spanStart,@@ -60,18 +63,20 @@ ) where +import Control.Applicative ((<*)) import Control.Monad import Data.List import Data.Maybe import Data.Time.Format import Data.Time.Calendar import Data.Time.Calendar.OrdinalDate+import Data.Time.Calendar.WeekDate import Data.Time.Clock import Data.Time.LocalTime import Safe (headMay, lastMay, readMay) import System.Locale (defaultTimeLocale) import Test.HUnit-import Text.ParserCombinators.Parsec+import Text.Parsec import Text.Printf  import Hledger.Data.Types@@ -85,7 +90,58 @@ showDate :: Day -> String showDate = formatTime defaultTimeLocale "%0C%y/%m/%d" -showDateSpan (DateSpan from to) =+-- XXX review for more boundary crossing issues+-- | Render a datespan as a display string, abbreviating into a+-- compact form if possible.+showDateSpan ds@(DateSpan (Just from) (Just to)) =+  case (toGregorian from, toGregorian to) of+    -- special cases we can abbreviate:+    -- a year, YYYY+    ((fy,1,1), (ty,1,1))   | fy+1==ty           -> formatTime defaultTimeLocale "%0C%y" from+    -- a half, YYYYhN+    ((fy,1,1), (ty,7,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yh1" from+    ((fy,7,1), (ty,1,1))   | fy+1==ty           -> formatTime defaultTimeLocale "%0C%yh2" from+    -- a quarter, YYYYqN+    ((fy,1,1), (ty,4,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yq1" from+    ((fy,4,1), (ty,7,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yq2" from+    ((fy,7,1), (ty,10,1))  | fy==ty             -> formatTime defaultTimeLocale "%0C%yq3" from+    ((fy,10,1), (ty,1,1))  | fy+1==ty           -> formatTime defaultTimeLocale "%0C%yq4" from+    -- a month, YYYY/MM+    ((fy,fm,1), (ty,tm,1)) | fy==ty && fm+1==tm -> formatTime defaultTimeLocale "%0C%y/%m" from+    ((fy,12,1), (ty,1,1))  | fy+1==ty           -> formatTime defaultTimeLocale "%0C%y/%m" from+    -- a week (two successive mondays),+    -- YYYYwN ("week N of year YYYY")+    -- _ | let ((fy,fw,fd), (ty,tw,td)) = (toWeekDate from, toWeekDate to) in fy==ty && fw+1==tw && fd==1 && td==1+    --                                             -> formatTime defaultTimeLocale "%0f%gw%V" from+    -- YYYY/MM/DDwN ("week N, starting on YYYY/MM/DD")+    _ | let ((fy,fw,fd), (ty,tw,td)) = (toWeekDate from, toWeekDate to) in fy==ty && fw+1==tw && fd==1 && td==1+                                                -> formatTime defaultTimeLocale "%0C%y/%m/%dw%V" from+    -- a day, YYYY/MM/DDd (d suffix is to distinguish from a regular date in register)+    ((fy,fm,fd), (ty,tm,td)) | fy==ty && fm==tm && fd+1==td -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from+    -- crossing a year boundary+    ((fy,fm,fd), (ty,tm,td)) | fy+1==ty && fm==12 && tm==1 && fd==31 && td==1 -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from+    -- crossing a month boundary XXX wrongly shows LEAPYEAR/2/28-LEAPYEAR/3/1 as LEAPYEAR/2/28+    ((fy,fm,fd), (ty,tm,td)) | fy==ty && fm+1==tm && fd `elem` fromMaybe [] (lookup fm lastdayofmonth) && td==1 -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from+    -- otherwise, YYYY/MM/DD-YYYY/MM/DD+    _                                           -> showDateSpan' ds+  where lastdayofmonth = [(1,[31])+                         ,(2,[28,29])+                         ,(3,[31])+                         ,(4,[30])+                         ,(5,[30])+                         ,(6,[31])+                         ,(7,[31])+                         ,(8,[31])+                         ,(9,[30])+                         ,(10,[31])+                         ,(11,[30])+                         ,(12,[31])+                         ]++showDateSpan ds = showDateSpan' ds++-- | Render a datespan as a display string.+showDateSpan' (DateSpan from to) =   concat     [maybe "" showDate from     ,"-"@@ -119,7 +175,7 @@ spanEnd :: DateSpan -> Maybe Day spanEnd (DateSpan _ d) = d --- might be useful later: http://en.wikipedia.org/wiki/Allen%27s_interval_algebra +-- might be useful later: http://en.wikipedia.org/wiki/Allen%27s_interval_algebra  -- | Get overall span enclosing multiple sequentially ordered spans. spansSpan :: [DateSpan] -> DateSpan@@ -170,7 +226,7 @@ spanContainsDate (DateSpan Nothing (Just e))  d = d < e spanContainsDate (DateSpan (Just b) Nothing)  d = d >= b spanContainsDate (DateSpan (Just b) (Just e)) d = d >= b && d < e-    + -- | Calculate the intersection of a number of datespans. spansIntersect [] = nulldatespan spansIntersect [d] = d@@ -210,7 +266,7 @@ -- | Parse a period expression to an Interval and overall DateSpan using -- the provided reference date, or return a parse error. parsePeriodExpr :: Day -> String -> Either ParseError (Interval, DateSpan)-parsePeriodExpr refdate = parsewith (periodexpr refdate)+parsePeriodExpr refdate = parsewith (periodexpr refdate <* eof)  maybePeriod :: Day -> String -> Maybe (Interval,DateSpan) maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate@@ -221,7 +277,7 @@ -- dateSpanAsText (DateSpan Nothing (Just e))  = printf "to %s" (show e) -- dateSpanAsText (DateSpan (Just b) Nothing)  = printf "from %s" (show b) -- dateSpanAsText (DateSpan (Just b) (Just e)) = printf "%s to %s" (show b) (show e)-    + -- | Convert a single smart date string to a date span using the provided -- reference date, or raise an error. -- spanFromSmartDateString :: Day -> String -> DateSpan@@ -366,9 +422,9 @@  -- | Parse a couple of date string formats to a time type. parsedateM :: String -> Maybe Day-parsedateM s = firstJust [ +parsedateM s = firstJust [      parseTime defaultTimeLocale "%Y/%m/%d" s,-     parseTime defaultTimeLocale "%Y-%m-%d" s +     parseTime defaultTimeLocale "%Y-%m-%d" s      ]  -- -- | Parse a date-time string to a time type, or raise an error.@@ -386,7 +442,7 @@ parsetimewith :: ParseTime t => String -> String -> t -> t parsetimewith pat s def = fromMaybe def $ parseTime defaultTimeLocale pat s -{-| +{-| Parse a date in any of the formats allowed in ledger's period expressions, and maybe some others: @@ -402,14 +458,14 @@ Returns a SmartDate, to be converted to a full date later (see fixSmartDate). Assumes any text in the parse stream has been lowercased. -}-smartdate :: GenParser Char st SmartDate+smartdate :: Stream [Char] m Char => ParsecT [Char] st m SmartDate smartdate = do   -- XXX maybe obscures date errors ? see ledgerdate   (y,m,d) <- choice' [yyyymmdd, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing]   return (y,m,d)  -- | Like smartdate, but there must be nothing other than whitespace after the date.-smartdateonly :: GenParser Char st SmartDate+smartdateonly :: Stream [Char] m Char => ParsecT [Char] st m SmartDate smartdateonly = do   d <- smartdate   many spacenonewline@@ -417,6 +473,7 @@   return d  datesepchars = "/-."+datesepchar :: Stream [Char] m Char => ParsecT [Char] st m Char datesepchar = oneOf datesepchars  validYear, validMonth, validDay :: String -> Bool@@ -429,7 +486,7 @@ failIfInvalidMonth s = unless (validMonth s) $ fail $ "bad month number: " ++ s failIfInvalidDay s   = unless (validDay s)   $ fail $ "bad day number: " ++ s -yyyymmdd :: GenParser Char st SmartDate+yyyymmdd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate yyyymmdd = do   y <- count 4 digit   m <- count 2 digit@@ -438,19 +495,19 @@   failIfInvalidDay d   return (y,m,d) -ymd :: GenParser Char st SmartDate+ymd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate ymd = do   y <- many1 digit   failIfInvalidYear y-  datesepchar+  sep <- datesepchar   m <- many1 digit   failIfInvalidMonth m-  datesepchar+  char sep   d <- many1 digit   failIfInvalidDay d   return $ (y,m,d) -ym :: GenParser Char st SmartDate+ym :: Stream [Char] m Char => ParsecT [Char] st m SmartDate ym = do   y <- many1 digit   failIfInvalidYear y@@ -459,19 +516,19 @@   failIfInvalidMonth m   return (y,m,"") -y :: GenParser Char st SmartDate+y :: Stream [Char] m Char => ParsecT [Char] st m SmartDate y = do   y <- many1 digit   failIfInvalidYear y   return (y,"","") -d :: GenParser Char st SmartDate+d :: Stream [Char] m Char => ParsecT [Char] st m SmartDate d = do   d <- many1 digit   failIfInvalidDay d   return ("","",d) -md :: GenParser Char st SmartDate+md :: Stream [Char] m Char => ParsecT [Char] st m SmartDate md = do   m <- many1 digit   failIfInvalidMonth m@@ -489,24 +546,24 @@ monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs -month :: GenParser Char st SmartDate+month :: Stream [Char] m Char => ParsecT [Char] st m SmartDate month = do   m <- choice $ map (try . string) months   let i = monthIndex m   return ("",show i,"") -mon :: GenParser Char st SmartDate+mon :: Stream [Char] m Char => ParsecT [Char] st m SmartDate mon = do   m <- choice $ map (try . string) monthabbrevs   let i = monIndex m   return ("",show i,"") -today,yesterday,tomorrow :: GenParser Char st SmartDate+today,yesterday,tomorrow :: Stream [Char] m Char => ParsecT [Char] st m SmartDate today     = string "today"     >> return ("","","today") yesterday = string "yesterday" >> return ("","","yesterday") tomorrow  = string "tomorrow"  >> return ("","","tomorrow") -lastthisnextthing :: GenParser Char st SmartDate+lastthisnextthing :: Stream [Char] m Char => ParsecT [Char] st m SmartDate lastthisnextthing = do   r <- choice [         string "last"@@ -523,10 +580,10 @@       ] -- XXX support these in fixSmartDate --       ++ (map string $ months ++ monthabbrevs ++ weekdays ++ weekdayabbrevs)-            +   return ("",r,p) -periodexpr :: Day -> GenParser Char st (Interval, DateSpan)+periodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan) periodexpr rdate = choice $ map try [                     intervalanddateperiodexpr rdate,                     intervalperiodexpr,@@ -534,7 +591,7 @@                     (return (NoInterval,DateSpan Nothing Nothing))                    ] -intervalanddateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)+intervalanddateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan) intervalanddateperiodexpr rdate = do   many spacenonewline   i <- reportinginterval@@ -542,20 +599,20 @@   s <- periodexprdatespan rdate   return (i,s) -intervalperiodexpr :: GenParser Char st (Interval, DateSpan)+intervalperiodexpr :: Stream [Char] m Char => ParsecT [Char] st m (Interval, DateSpan) intervalperiodexpr = do   many spacenonewline   i <- reportinginterval   return (i, DateSpan Nothing Nothing) -dateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)+dateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan) dateperiodexpr rdate = do   many spacenonewline   s <- periodexprdatespan rdate   return (NoInterval, s)  -- Parse a reporting interval.-reportinginterval :: GenParser Char st Interval+reportinginterval :: Stream [Char] m Char => ParsecT [Char] st m Interval reportinginterval = choice' [                        tryinterval "day"     "daily"     Days,                        tryinterval "week"    "weekly"    Weeks,@@ -595,7 +652,7 @@       thsuffix = choice' $ map string ["st","nd","rd","th"]        -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".-      tryinterval :: String -> String -> (Int -> Interval) -> GenParser Char st Interval+      tryinterval :: Stream [Char] m Char => String -> String -> (Int -> Interval) -> ParsecT [Char] st m Interval       tryinterval singular compact intcons =           choice' [            do string compact@@ -613,7 +670,7 @@            ]           where plural = singular ++ "s" -periodexprdatespan :: Day -> GenParser Char st DateSpan+periodexprdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan periodexprdatespan rdate = choice $ map try [                             doubledatespan rdate,                             fromdatespan rdate,@@ -621,7 +678,7 @@                             justdatespan rdate                            ] -doubledatespan :: Day -> GenParser Char st DateSpan+doubledatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan doubledatespan rdate = do   optional (string "from" >> many spacenonewline)   b <- smartdate@@ -630,7 +687,7 @@   e <- smartdate   return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e) -fromdatespan :: Day -> GenParser Char st DateSpan+fromdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan fromdatespan rdate = do   b <- choice [     do@@ -644,13 +701,13 @@     ]   return $ DateSpan (Just $ fixSmartDate rdate b) Nothing -todatespan :: Day -> GenParser Char st DateSpan+todatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan todatespan rdate = do   choice [string "to", string "-"] >> many spacenonewline   e <- smartdate   return $ DateSpan Nothing (Just $ fixSmartDate rdate e) -justdatespan :: Day -> GenParser Char st DateSpan+justdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan justdatespan rdate = do   optional (string "in" >> many spacenonewline)   d <- smartdate
Hledger/Data/Journal.hs view
@@ -22,7 +22,8 @@   -- * Filtering   filterJournalTransactions,   filterJournalPostings,-  filterJournalPostingAmounts,+  filterJournalAmounts,+  filterTransactionAmounts,   filterPostingAmount,   -- * Querying   journalAccountNames,@@ -45,6 +46,7 @@   journalEquityAccountQuery,   journalCashAccountQuery,   -- * Misc+  canonicalStyles,   matchpats,   nullctx,   nulljournal,@@ -58,6 +60,7 @@ -- import Data.Map (findWithDefault) import Data.Maybe import Data.Ord+import Safe (headMay) import Data.Time.Calendar import Data.Time.LocalTime import Data.Tree@@ -131,7 +134,7 @@                       }  nullctx :: JournalContext-nullctx = Ctx { ctxYear = Nothing, ctxCommodityAndStyle = Nothing, ctxAccount = [], ctxAliases = [] }+nullctx = Ctx { ctxYear = Nothing, ctxDefaultCommodityAndStyle = Nothing, ctxAccount = [], ctxAliases = [] }  journalFilePath :: Journal -> FilePath journalFilePath = fst . mainfile@@ -143,19 +146,19 @@ mainfile = headDef ("", "") . files  addTransaction :: Transaction -> Journal -> Journal-addTransaction t l0 = l0 { jtxns = t : jtxns l0 }+addTransaction t j = j { jtxns = t : jtxns j }  addModifierTransaction :: ModifierTransaction -> Journal -> Journal-addModifierTransaction mt l0 = l0 { jmodifiertxns = mt : jmodifiertxns l0 }+addModifierTransaction mt j = j { jmodifiertxns = mt : jmodifiertxns j }  addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal-addPeriodicTransaction pt l0 = l0 { jperiodictxns = pt : jperiodictxns l0 }+addPeriodicTransaction pt j = j { jperiodictxns = pt : jperiodictxns j }  addHistoricalPrice :: HistoricalPrice -> Journal -> Journal-addHistoricalPrice h l0 = l0 { historical_prices = h : historical_prices l0 }+addHistoricalPrice h j = j { historical_prices = h : historical_prices j }  addTimeLogEntry :: TimeLogEntry -> Journal -> Journal-addTimeLogEntry tle l0 = l0 { open_timelog_entries = tle : open_timelog_entries l0 }+addTimeLogEntry tle j = j { open_timelog_entries = tle : open_timelog_entries j }  -- | Unique transaction descriptions used in this journal. journalDescriptions :: Journal -> [String]@@ -231,6 +234,10 @@ ------------------------------------------------------------------------------- -- filtering V2 +-- | Keep only transactions matching the query expression.+filterJournalTransactions :: Query -> Journal -> Journal+filterJournalTransactions q j@Journal{jtxns=ts} = j{jtxns=filter (q `matchesTransaction`) ts}+ -- | Keep only postings matching the query expression. -- This can leave unbalanced transactions. filterJournalPostings :: Query -> Journal -> Journal@@ -238,21 +245,20 @@     where       filtertransactionpostings t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps} --- Within each posting's amount, keep only the parts matching the query.+-- | Within each posting's amount, keep only the parts matching the query. -- This can leave unbalanced transactions.-filterJournalPostingAmounts :: Query -> Journal -> Journal-filterJournalPostingAmounts q j@Journal{jtxns=ts} = j{jtxns=map filtertransactionpostings ts}-    where-      filtertransactionpostings t@Transaction{tpostings=ps} = t{tpostings=map (filterPostingAmount q) ps}+filterJournalAmounts :: Query -> Journal -> Journal+filterJournalAmounts q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionAmounts q) ts} +-- | Filter out all parts of this transaction's amounts which do not match the query.+-- This can leave the transaction unbalanced.+filterTransactionAmounts :: Query -> Transaction -> Transaction+filterTransactionAmounts q t@Transaction{tpostings=ps} = t{tpostings=map (filterPostingAmount q) ps}+ -- | Filter out all parts of this posting's amount which do not match the query. filterPostingAmount :: Query -> Posting -> Posting filterPostingAmount q p@Posting{pamount=Mixed as} = p{pamount=Mixed $ filter (q `matchesAmount`) as} --- | Keep only transactions matching the query expression.-filterJournalTransactions :: Query -> Journal -> Journal-filterJournalTransactions q j@Journal{jtxns=ts} = j{jtxns=filter (q `matchesTransaction`) ts}- {- ------------------------------------------------------------------------------- -- filtering V1@@ -379,22 +385,36 @@ -}  -- | Apply additional account aliases (eg from the command-line) to all postings in a journal.-journalApplyAliases :: [(AccountName,AccountName)] -> Journal -> Journal-journalApplyAliases aliases j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts}+journalApplyAliases :: [AccountAlias] -> Journal -> Journal+journalApplyAliases aliases j@Journal{jtxns=ts} =+  -- (if null aliases+  --  then id+  --  else (dbgtrace $+  --        "applying additional command-line aliases:\n"+  --        ++ chomp (unlines $ map (" "++) $ lines $ ppShow aliases))) $+  j{jtxns=map fixtransaction ts}     where       fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps}       fixposting p@Posting{paccount=a} = p{paccount=accountNameApplyAliases aliases a}  -- | Do post-parse processing on a journal to make it ready for use: check -- all transactions balance, canonicalise amount formats, close any open--- timelog entries and so on.-journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Journal -> Either String Journal-journalFinalise tclock tlocal path txt ctx j@Journal{files=fs} = do+-- timelog entries, maybe check balance assertions and so on.+journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Bool -> Journal -> Either String Journal+journalFinalise tclock tlocal path txt ctx assrt j@Journal{files=fs} = do   (journalBalanceTransactions $     journalCanonicaliseAmounts $-    journalCloseTimeLogEntries tlocal-    j{files=(path,txt):fs, filereadtime=tclock, jContext=ctx})-  >>= journalCheckBalanceAssertions+    journalCloseTimeLogEntries tlocal $+    j{ files=(path,txt):fs+     , filereadtime=tclock+     , jContext=ctx+     , jtxns=reverse $ jtxns j -- NOTE: see addTransaction+     , jmodifiertxns=reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction+     , jperiodictxns=reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction+     , historical_prices=reverse $ historical_prices j -- NOTE: see addHistoricalPrice+     , open_timelog_entries=reverse $ open_timelog_entries j -- NOTE: see addTimeLogEntry+     })+  >>= if assrt then journalCheckBalanceAssertions else return  -- | Check any balance assertions in the journal and return an error -- message if any of them fail.@@ -423,26 +443,29 @@ -- If it does, return the new balance, otherwise add an error to the -- error list. Intended to be called from a fold. checkBalanceAssertion :: ([String],MixedAmount) -> [Posting] -> ([String],MixedAmount)-checkBalanceAssertion (errs,bal) ps-  | null ps = (errs,bal)-  | isNothing assertion = (errs,bal)+checkBalanceAssertion (errs,startbal) ps+  | null ps = (errs,startbal)+  | isNothing assertion = (errs,startbal)   |     -- bal' /= assertedbal  -- MixedAmount's Eq instance currently gets confused by different precisions-    not $ isReallyZeroMixedAmount (bal' - assertedbal)-      = (errs++[err], bal')-  | otherwise = (errs,bal')+    not $ isReallyZeroMixedAmount (bal - assertedbal) = (errs++[err], bal)+  | otherwise = (errs,bal)   where     p = last ps     assertion = pbalanceassertion p-    Just assertedbal = assertion-    bal' = sum $ [bal] ++ map pamount ps-    err = printf "Balance assertion failed for account %s on %s\n%sAfter posting:\n   %s\nexpected balance is %s, actual balance was %s."+    Just assertedbal = dbg2 "assertedbal" assertion+    assertedcomm = dbg2 "assertedcomm" $ maybe "" acommodity $ headMay $ amounts assertedbal+    fullbal = dbg2 "fullbal" $ sum $ [dbg2 "startbal" startbal] ++ map pamount ps+    singlebal = dbg2 "singlebal" $ filterMixedAmount (\a -> acommodity a == assertedcomm) fullbal+    bal = singlebal -- check single-commodity balance like Ledger; maybe add == FULLBAL later+    err = printf "Balance assertion failed for account %s on %s\n%sAfter posting:\n   %s\nexpected balance in commodity \"%s\" is %s, calculated balance was %s."                  (paccount p)                  (show $ postingDate p)                  (maybe "" (("In transaction:\n"++).show) $ ptransaction p)                  (show p)+                 assertedcomm                  (showMixedAmount assertedbal)-                 (showMixedAmount bal')+                 (showMixedAmount singlebal)  -- Given a sequence of postings to a single account, split it into -- sub-sequences consisting of ordinary postings followed by a single@@ -454,7 +477,7 @@   | otherwise = (ps'++[head rest]):splitAssertions (tail rest)   where     (ps',rest) = break (isJust . pbalanceassertion) ps-    + -- | Fill in any missing amounts and check that all journal transactions -- balance, or return an error message. This is done after parsing all -- amounts and working out the canonical commodities, since balancing@@ -473,12 +496,34 @@ journalCanonicaliseAmounts j@Journal{jtxns=ts} = j''     where       j'' = j'{jtxns=map fixtransaction ts}-      j' = j{jcommoditystyles = canonicalStyles $ journalAmounts j}+      j' = j{jcommoditystyles = canonicalStyles $ dbgAt 8 "journalAmounts" $ journalAmounts j}       fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps}       fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a}       fixmixedamount (Mixed as) = Mixed $ map fixamount as       fixamount a@Amount{acommodity=c} = a{astyle=journalCommodityStyle j' c} +-- | Given a list of amounts in parse order, build a map from commodities+-- to canonical display styles for amounts in that commodity.+canonicalStyles :: [Amount] -> M.Map Commodity AmountStyle+canonicalStyles amts = M.fromList commstyles+  where+    samecomm = \a1 a2 -> acommodity a1 == acommodity a2+    commamts = [(acommodity $ head as, as) | as <- groupBy samecomm $ sortBy (comparing acommodity) amts]+    commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts]++-- Given an ordered list of amount styles for a commodity, build a canonical style.+canonicalStyleFrom :: [AmountStyle] -> AmountStyle+canonicalStyleFrom [] = amountstyle+canonicalStyleFrom ss@(first:_) =+  first{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}+  where+    -- precision is the maximum of all precisions seen+    prec = maximum $ map asprecision ss+    -- find the first decimal point and the first digit group style seen,+    -- or use defaults.+    mdec  = Just $ headDef '.' $ catMaybes $ map asdecimalpoint ss+    mgrps = maybe Nothing Just $ headMay $ catMaybes $ map asdigitgroups ss+ -- | Get this journal's canonical amount style for the given commodity, or the null style. journalCommodityStyle :: Journal -> Commodity -> AmountStyle journalCommodityStyle j c = M.findWithDefault amountstyle c $ jcommoditystyles j@@ -623,10 +668,11 @@ --     liabilities:debts  $1 --     assets:bank:checking ---Right samplejournal = journalBalanceTransactions $ +Right samplejournal = journalBalanceTransactions $          nulljournal          {jtxns = [            txnTieKnot $ Transaction {+             tsourcepos=nullsourcepos,              tdate=parsedate "2008/01/01",              tdate2=Nothing,              tstatus=False,@@ -642,6 +688,7 @@            }           ,            txnTieKnot $ Transaction {+             tsourcepos=nullsourcepos,              tdate=parsedate "2008/06/01",              tdate2=Nothing,              tstatus=False,@@ -657,6 +704,7 @@            }           ,            txnTieKnot $ Transaction {+             tsourcepos=nullsourcepos,              tdate=parsedate "2008/06/02",              tdate2=Nothing,              tstatus=False,@@ -672,6 +720,7 @@            }           ,            txnTieKnot $ Transaction {+             tsourcepos=nullsourcepos,              tdate=parsedate "2008/06/03",              tdate2=Nothing,              tstatus=True,@@ -687,6 +736,7 @@            }           ,            txnTieKnot $ Transaction {+             tsourcepos=nullsourcepos,              tdate=parsedate "2008/12/31",              tdate2=Nothing,              tstatus=False,
Hledger/Data/Ledger.hs view
@@ -43,7 +43,7 @@ ledgerFromJournal q j = nullledger{ljournal=j'', laccounts=as}   where     (q',depthq)  = (filterQuery (not . queryIsDepth) q, filterQuery queryIsDepth q)-    j'  = filterJournalPostingAmounts (filterQuery queryIsSym q) $ -- remove amount parts which the query's sym: terms would exclude+    j'  = filterJournalAmounts (filterQuery queryIsSym q) $ -- remove amount parts which the query's sym: terms would exclude           filterJournalPostings q' j     as  = accountsFromPostings $ journalPostings j'     j'' = filterJournalPostings depthq j'
Hledger/Data/OutputFormat.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE FlexibleContexts #-} module Hledger.Data.OutputFormat (           parseStringFormat         , formatsp@@ -11,7 +12,7 @@ import Data.Char (isPrint) import Data.Maybe import Test.HUnit-import Text.ParserCombinators.Parsec+import Text.Parsec import Text.Printf  import Hledger.Data.Types@@ -34,7 +35,7 @@ Parsers -} -field :: GenParser Char st HledgerFormatField+field :: Stream [Char] m Char => ParsecT [Char] st m HledgerFormatField field = do         try (string "account" >> return AccountField)     <|> try (string "depth_spacer" >> return DepthSpacerField)@@ -43,7 +44,7 @@     <|> try (string "total" >> return TotalField)     <|> try (many1 digit >>= (\s -> return $ FieldNo $ read s)) -formatField :: GenParser Char st OutputFormat+formatField :: Stream [Char] m Char => ParsecT [Char] st m OutputFormat formatField = do     char '%'     leftJustified <- optionMaybe (char '-')@@ -58,7 +59,7 @@         Just text -> Just m where ((m,_):_) = readDec text         _ -> Nothing -formatLiteral :: GenParser Char st OutputFormat+formatLiteral :: Stream [Char] m Char => ParsecT [Char] st m OutputFormat formatLiteral = do     s <- many1 c     return $ FormatLiteral s@@ -67,12 +68,12 @@       c =     (satisfy isPrintableButNotPercentage <?> "printable character")           <|> try (string "%%" >> return '%') -formatp :: GenParser Char st OutputFormat+formatp :: Stream [Char] m Char => ParsecT [Char] st m OutputFormat formatp =         formatField     <|> formatLiteral -formatsp :: GenParser Char st [OutputFormat]+formatsp :: Stream [Char] m Char => ParsecT [Char] st m [OutputFormat] formatsp = many formatp  testFormat :: OutputFormat -> String -> String -> Assertion
Hledger/Data/Posting.hs view
@@ -37,6 +37,7 @@   joinAccountNames,   concatAccountNames,   accountNameApplyAliases,+  accountNameApplyOneAlias,   -- * arithmetic   sumPostings,   -- * rendering@@ -79,11 +80,12 @@ posting = nullposting  post :: AccountName -> Amount -> Posting-post acct amt = posting {paccount=acct, pamount=mixed amt}+post acct amt = posting {paccount=acct, pamount=Mixed [amt]} +-- XXX once rendered user output, but just for debugging now; clean up showPosting :: Posting -> String showPosting p@Posting{paccount=a,pamount=amt,ptype=t} =-    unlines $ [concatTopPadded [showaccountname a ++ " ", showamount amt, showComment (pcomment p)]]+    unlines $ [concatTopPadded [show (postingDate p) ++ " ", showaccountname a ++ " ", showamount amt, showComment (pcomment p)]]     where       ledger3ishlayout = False       acctnamewidth = if ledger3ishlayout then 25 else 22@@ -121,7 +123,7 @@ -- there is no parent transaction. postingDate :: Posting -> Day postingDate p = fromMaybe txndate $ pdate p-    where +    where       txndate = maybe nulldate tdate $ ptransaction p  -- | Get a posting's secondary (secondary) date, which is the first of:@@ -217,14 +219,22 @@ concatAccountNames as = accountNameWithPostingType t $ intercalate ":" $ map accountNameWithoutPostingType as     where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as +-- | Rewrite an account name using all applicable aliases from the given list, in sequence.+accountNameApplyAliases :: [AccountAlias] -> AccountName -> AccountName+accountNameApplyAliases aliases a = accountNameWithPostingType atype aname'+  where+    (aname,atype) = (accountNameWithoutPostingType a, accountNamePostingType a)+    matchingaliases = filter (\(re,_) -> regexMatchesCI re aname) aliases+    aname' = foldl (flip (uncurry regexReplaceCI)) aname matchingaliases+ -- | Rewrite an account name using the first applicable alias from the given list, if any.-accountNameApplyAliases :: [(AccountName,AccountName)] -> AccountName -> AccountName-accountNameApplyAliases aliases a = withorigtype-    where-      (a',t) = (accountNameWithoutPostingType a, accountNamePostingType a)-      firstmatchingalias = headDef Nothing $ map Just $ filter (\(orig,_) -> orig == a' || orig `isAccountNamePrefixOf` a') aliases-      rewritten = maybe a' (\(orig,alias) -> alias++drop (length orig) a') firstmatchingalias-      withorigtype = accountNameWithPostingType t rewritten+accountNameApplyOneAlias :: [AccountAlias] -> AccountName -> AccountName+accountNameApplyOneAlias aliases a = accountNameWithPostingType atype aname'+  where+    (aname,atype) = (accountNameWithoutPostingType a, accountNamePostingType a)+    firstmatchingalias = headDef Nothing $ map Just $ filter (\(re,_) -> regexMatchesCI re aname) aliases+    applyAlias = uncurry regexReplaceCI+    aname' = maybe id applyAlias firstmatchingalias $ aname  tests_Hledger_Data_Posting = TestList [ @@ -250,4 +260,4 @@     concatAccountNames ["a","(b)","[c:d]"] `is` "(a:b:c:d)"   ]- +
Hledger/Data/RawOptions.hs view
@@ -45,7 +45,7 @@ boolopt = inRawOpts  maybestringopt :: String -> RawOpts -> Maybe String-maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name+maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name . reverse  stringopt :: String -> RawOpts -> String stringopt name = fromMaybe "" . maybestringopt name
Hledger/Data/TimeLog.hs view
@@ -24,17 +24,17 @@ import Hledger.Data.Posting import Hledger.Data.Transaction -instance Show TimeLogEntry where +instance Show TimeLogEntry where     show t = printf "%s %s %s" (show $ tlcode t) (show $ tldatetime t) (tlcomment t) -instance Show TimeLogCode where +instance Show TimeLogCode where     show SetBalance = "b"     show SetRequiredHours = "h"     show In = "i"     show Out = "o"     show FinalOut = "O" -instance Read TimeLogCode where +instance Read TimeLogCode where     readsPrec _ ('b' : xs) = [(SetBalance, xs)]     readsPrec _ ('h' : xs) = [(SetRequiredHours, xs)]     readsPrec _ ('i' : xs) = [(In, xs)]@@ -51,7 +51,7 @@     | odate > idate = entryFromTimeLogInOut i o' : timeLogEntriesToTransactions now [i',o]     | otherwise = [entryFromTimeLogInOut i o]     where-      o = TimeLogEntry Out end ""+      o = TimeLogEntry (tlsourcepos i) Out end ""       end = if itime > now then itime else now       (itime,otime) = (tldatetime i,tldatetime o)       (idate,odate) = (localDay itime,localDay otime)@@ -72,18 +72,19 @@ entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction entryFromTimeLogInOut i o     | otime >= itime = t-    | otherwise = +    | otherwise =         error' $ "clock-out time less than clock-in time in:\n" ++ showTransaction t     where       t = Transaction {-            tdate         = idate,-            tdate2 = Nothing,-            tstatus       = True,-            tcode         = "",-            tdescription  = showtime itod ++ "-" ++ showtime otod,-            tcomment      = "",-            ttags     = [],-            tpostings = ps,+            tsourcepos   = tlsourcepos i,+            tdate        = idate,+            tdate2       = Nothing,+            tstatus      = True,+            tcode        = "",+            tdescription = showtime itod ++ "-" ++ showtime otod,+            tcomment     = "",+            ttags        = [],+            tpostings    = ps,             tpreceding_comment_lines=""           }       showtime = take 5 . show@@ -107,7 +108,7 @@      let now = utcToLocalTime tz now'          nowstr = showtime now          yesterday = prevday today-         clockin = TimeLogEntry In+         clockin = TimeLogEntry nullsourcepos In          mktime d = LocalTime d . fromMaybe midnight . parseTime defaultTimeLocale "%H:%M:%S"          showtime = formatTime defaultTimeLocale "%H:%M"          assertEntriesGiveStrings name es ss = assertEqual name ss (map tdescription $ timeLogEntriesToTransactions now es)@@ -118,8 +119,8 @@      assertEntriesGiveStrings "split multi-day sessions at each midnight"                                   [clockin (mktime (addDays (-2) today) "23:00:00") ""]                                   ["23:00-23:59","00:00-23:59","00:00-"++nowstr]-     assertEntriesGiveStrings "auto-clock-out if needed" -                                  [clockin (mktime today "00:00:00") ""] +     assertEntriesGiveStrings "auto-clock-out if needed"+                                  [clockin (mktime today "00:00:00") ""]                                   ["00:00-"++nowstr]      let future = utcToLocalTime tz $ addUTCTime 100 now'          futurestr = showtime future
Hledger/Data/Transaction.hs view
@@ -9,6 +9,7 @@  module Hledger.Data.Transaction (   -- * Transaction+  nullsourcepos,   nulltransaction,   txnTieKnot,   -- settxn,@@ -39,6 +40,7 @@ import Test.HUnit import Text.Printf import qualified Data.Map as Map+import Text.Parsec.Pos  import Hledger.Utils import Hledger.Data.Types@@ -48,19 +50,23 @@  instance Show Transaction where show = showTransactionUnelided -instance Show ModifierTransaction where +instance Show ModifierTransaction where     show t = "= " ++ mtvalueexpr t ++ "\n" ++ unlines (map show (mtpostings t)) -instance Show PeriodicTransaction where +instance Show PeriodicTransaction where     show t = "~ " ++ ptperiodicexpr t ++ "\n" ++ unlines (map show (ptpostings t)) +nullsourcepos :: SourcePos+nullsourcepos = initialPos ""+ nulltransaction :: Transaction nulltransaction = Transaction {+                    tsourcepos=nullsourcepos,                     tdate=nulldate,                     tdate2=Nothing,-                    tstatus=False, -                    tcode="", -                    tdescription="", +                    tstatus=False,+                    tcode="",+                    tdescription="",                     tcomment="",                     ttags=[],                     tpostings=[],@@ -122,7 +128,7 @@       ]  ] --- XXX overlaps showPosting+-- cf showPosting showTransaction' :: Bool -> Transaction -> String showTransaction' elide t =     unlines $ [descriptionline]@@ -280,7 +286,7 @@       ramounts  = map pamount rwithamounts       bvamounts = map pamount bvwithamounts       t' = t{tpostings=map inferamount ps}-          where +          where             inferamount p | not (hasAmount p) && isReal p            = p{pamount = costOfMixedAmount (- sum ramounts)}                           | not (hasAmount p) && isBalancedVirtual p = p{pamount = costOfMixedAmount (- sum bvamounts)}                           | otherwise                             = p@@ -376,7 +382,7 @@         ,"    assets:checking"         ,""         ])-       (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []+       (let t = Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []                 [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18], ptransaction=Just t}                 ,posting{paccount="assets:checking", pamount=Mixed [usd (-47.18)], ptransaction=Just t}                 ] ""@@ -390,7 +396,7 @@         ,"    assets:checking               $-47.18"         ,""         ])-       (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []+       (let t = Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []                 [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18], ptransaction=Just t}                 ,posting{paccount="assets:checking", pamount=Mixed [usd (-47.18)], ptransaction=Just t}                 ] ""@@ -406,7 +412,7 @@         ,""         ])        (showTransaction-        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []+        (txnTieKnot $ Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []          [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18]}          ,posting{paccount="assets:checking", pamount=Mixed [usd (-47.19)]}          ] ""))@@ -419,7 +425,7 @@         ,""         ])        (showTransaction-        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []+        (txnTieKnot $ Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []          [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18]}          ] "")) @@ -431,7 +437,7 @@         ,""         ])        (showTransaction-        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []+        (txnTieKnot $ Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []          [posting{paccount="expenses:food:groceries", pamount=missingmixedamt}          ] "")) @@ -444,7 +450,7 @@         ,""         ])        (showTransaction-        (txnTieKnot $ Transaction (parsedate "2010/01/01") Nothing False "" "x" "" []+        (txnTieKnot $ Transaction nullsourcepos (parsedate "2010/01/01") Nothing False "" "x" "" []          [posting{paccount="a", pamount=Mixed [num 1 `at` (usd 2 `withPrecision` 0)]}          ,posting{paccount="b", pamount= missingmixedamt}          ] ""))@@ -452,19 +458,19 @@   ,"balanceTransaction" ~: do      assertBool "detect unbalanced entry, sign error"                     (isLeft $ balanceTransaction Nothing-                           (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" []+                           (Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "test" "" []                             [posting{paccount="a", pamount=Mixed [usd 1]}                             ,posting{paccount="b", pamount=Mixed [usd 1]}                             ] ""))       assertBool "detect unbalanced entry, multiple missing amounts"                     (isLeft $ balanceTransaction Nothing-                           (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" []+                           (Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "test" "" []                             [posting{paccount="a", pamount=missingmixedamt}                             ,posting{paccount="b", pamount=missingmixedamt}                             ] "")) -     let e = balanceTransaction Nothing (Transaction (parsedate "2007/01/28") Nothing False "" "" "" []+     let e = balanceTransaction Nothing (Transaction nullsourcepos (parsedate "2007/01/28") Nothing False "" "" "" []                            [posting{paccount="a", pamount=Mixed [usd 1]}                            ,posting{paccount="b", pamount=missingmixedamt}                            ] "")@@ -475,7 +481,7 @@                         Right e' -> (pamount $ last $ tpostings e')                         Left _ -> error' "should not happen") -     let e = balanceTransaction Nothing (Transaction (parsedate "2011/01/01") Nothing False "" "" "" []+     let e = balanceTransaction Nothing (Transaction nullsourcepos (parsedate "2011/01/01") Nothing False "" "" "" []                            [posting{paccount="a", pamount=Mixed [usd 1.35]}                            ,posting{paccount="b", pamount=Mixed [eur (-1)]}                            ] "")@@ -487,49 +493,49 @@                         Left _ -> error' "should not happen")       assertBool "balanceTransaction balances based on cost if there are unit prices" (isRight $-       balanceTransaction Nothing (Transaction (parsedate "2011/01/01") Nothing False "" "" "" []+       balanceTransaction Nothing (Transaction nullsourcepos (parsedate "2011/01/01") Nothing False "" "" "" []                            [posting{paccount="a", pamount=Mixed [usd 1 `at` eur 2]}                            ,posting{paccount="a", pamount=Mixed [usd (-2) `at` eur 1]}                            ] ""))       assertBool "balanceTransaction balances based on cost if there are total prices" (isRight $-       balanceTransaction Nothing (Transaction (parsedate "2011/01/01") Nothing False "" "" "" []+       balanceTransaction Nothing (Transaction nullsourcepos (parsedate "2011/01/01") Nothing False "" "" "" []                            [posting{paccount="a", pamount=Mixed [usd 1    @@ eur 1]}                            ,posting{paccount="a", pamount=Mixed [usd (-2) @@ eur 1]}                            ] ""))    ,"isTransactionBalanced" ~: do-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}              ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}              ] ""      assertBool "detect balanced" (isTransactionBalanced Nothing t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}              ,posting{paccount="c", pamount=Mixed [usd (-1.01)], ptransaction=Just t}              ] ""      assertBool "detect unbalanced" (not $ isTransactionBalanced Nothing t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}              ] ""      assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced Nothing t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 0], ptransaction=Just t}              ] ""      assertBool "one zero posting is considered balanced for now" (isTransactionBalanced Nothing t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}              ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}              ,posting{paccount="d", pamount=Mixed [usd 100], ptype=VirtualPosting, ptransaction=Just t}              ] ""      assertBool "virtual postings don't need to balance" (isTransactionBalanced Nothing t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}              ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}              ,posting{paccount="d", pamount=Mixed [usd 100], ptype=BalancedVirtualPosting, ptransaction=Just t}              ] ""      assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced Nothing t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []+     let t = Transaction nullsourcepos (parsedate "2009/01/01") Nothing False "" "a" "" []              [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}              ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}              ,posting{paccount="d", pamount=Mixed [usd 100], ptype=BalancedVirtualPosting, ptransaction=Just t}
Hledger/Data/Types.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveDataTypeable, StandaloneDeriving #-}+{-# LANGUAGE CPP, DeriveDataTypeable, StandaloneDeriving, TypeSynonymInstances, FlexibleInstances #-} {-|  Most data types are defined here to avoid import cycles.@@ -21,12 +21,19 @@ where import Control.Monad.Error (ErrorT) import Data.Data+#ifndef DOUBLE+import Data.Decimal+import Text.Blaze (ToMarkup(..))+#endif import qualified Data.Map as M import Data.Time.Calendar import Data.Time.LocalTime import System.Time (ClockTime(..))+import Text.Parsec.Pos +import Hledger.Utils.Regex + type SmartDate = (String,String,String)  data WhichDate = PrimaryDate | SecondaryDate deriving (Eq,Show)@@ -41,11 +48,28 @@  type AccountName = String +type AccountAlias = (Regexp,Replacement)+ data Side = L | R deriving (Eq,Show,Read,Ord,Typeable,Data)  type Commodity = String-      ++-- | The basic numeric type used in amounts. Different implementations+-- can be selected via cabal flag for testing and benchmarking purposes.+numberRepresentation :: String+#ifdef DOUBLE type Quantity = Double+numberRepresentation = "Double"+#else+type Quantity = Decimal+deriving instance Data (Quantity)+-- The following is for hledger-web, and requires blaze-markup.+-- Doing it here avoids needing a matching flag on the hledger-web package.+instance ToMarkup (Quantity) + where+   toMarkup = toMarkup . show+numberRepresentation = "Decimal"+#endif  -- | An amount's price (none, per unit, or total) in another commodity. -- Note the price should be a positive number, although this is not enforced.@@ -56,11 +80,19 @@       ascommodityside :: Side,       -- ^ does the symbol appear on the left or the right ?       ascommodityspaced :: Bool,     -- ^ space between symbol and quantity ?       asprecision :: Int,            -- ^ number of digits displayed after the decimal point-      asdecimalpoint :: Char,        -- ^ character used as decimal point-      asseparator :: Char,           -- ^ character used for separating digit groups (eg thousands)-      asseparatorpositions :: [Int]  -- ^ positions of digit group separators, counting leftward from decimal point+      asdecimalpoint :: Maybe Char,  -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"+      asdigitgroups :: Maybe DigitGroupStyle -- ^ style for displaying digit groups, if any } deriving (Eq,Ord,Read,Show,Typeable,Data) +-- | A style for displaying digit groups in the integer part of a+-- floating point number. It consists of the character used to+-- separate groups (comma or period, whichever is not used as decimal+-- point), and the size of each group, starting with the one nearest+-- the decimal point. The last group size is assumed to repeat. Eg,+-- comma between thousands is DigitGroups ',' [3].+data DigitGroupStyle = DigitGroups Char [Int]+  deriving (Eq,Ord,Read,Show,Typeable,Data)+ data Amount = Amount {       acommodity :: Commodity,       aquantity :: Quantity,@@ -95,6 +127,7 @@     (==) (Posting a1 b1 c1 d1 e1 f1 g1 h1 i1 _) (Posting a2 b2 c2 d2 e2 f2 g2 h2 i2 _) =  a1==a2 && b1==b2 && c1==c2 && d1==d2 && e1==e2 && f1==f2 && g1==g2 && h1==h2 && i1==i2  data Transaction = Transaction {+      tsourcepos :: SourcePos,       tdate :: Day,       tdate2 :: Maybe Day,       tstatus :: Bool,  -- XXX tcleared ?@@ -119,6 +152,7 @@ data TimeLogCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data)  data TimeLogEntry = TimeLogEntry {+      tlsourcepos :: SourcePos,       tlcode :: TimeLogCode,       tldatetime :: LocalTime,       tlcomment :: String@@ -138,11 +172,11 @@ -- is saved for later use by eg the add command. data JournalContext = Ctx {       ctxYear      :: !(Maybe Year)      -- ^ the default year most recently specified with Y-    , ctxCommodityAndStyle :: !(Maybe (Commodity,AmountStyle)) -- ^ the default commodity and amount style most recently specified with D+    , ctxDefaultCommodityAndStyle :: !(Maybe (Commodity,AmountStyle)) -- ^ the default commodity and amount style most recently specified with D     , ctxAccount   :: ![AccountName]     -- ^ the current stack of parent accounts/account name components                                         --   specified with "account" directive(s). Concatenated, these                                         --   are the account prefix prepended to parsed account names.-    , ctxAliases   :: ![(AccountName,AccountName)] -- ^ the current list of account name aliases in effect+    , ctxAliases   :: ![AccountAlias]   -- ^ the current list of account name aliases in effect     } deriving (Read, Show, Eq, Data, Typeable)  deriving instance Data (ClockTime)@@ -159,7 +193,7 @@       files :: [(FilePath, String)],        -- ^ the file path and raw text of the main and                                             -- any included journal files. The main file is                                             -- first followed by any included files in the-                                            -- order encountered (XXX reversed, cf journalAddFile).+                                            -- order encountered.       filereadtime :: ClockTime,            -- ^ when this journal was last read from its file(s)       jcommoditystyles :: M.Map Commodity AmountStyle  -- ^ how to display amounts in each commodity     } deriving (Eq, Typeable, Data)@@ -179,10 +213,10 @@      -- quickly check if this reader can probably handle the given file path and file content     ,rDetector :: FilePath -> String -> Bool      -- parse the given string, using the given parse rules file if any, returning a journal or error aware of the given file path-    ,rParser   :: Maybe FilePath -> FilePath -> String -> ErrorT String IO Journal+    ,rParser   :: Maybe FilePath -> Bool -> FilePath -> String -> ErrorT String IO Journal     } -instance Show Reader where show r = "Reader for "++rFormat r+instance Show Reader where show r = rFormat r ++ " reader"  -- format strings 
Hledger/Query.hs view
@@ -19,10 +19,14 @@   queryIsAcct,   queryIsDepth,   queryIsDate,+  queryIsDate2,+  queryIsDateOrDate2,   queryIsStartDateOnly,   queryIsSym,   queryStartDate,+  queryEndDate,   queryDateSpan,+  queryDateSpan',   queryDepth,   queryEmpty,   inAccount,@@ -45,7 +49,8 @@ import Data.Time.Calendar import Safe (readDef, headDef, headMay) import Test.HUnit-import Text.ParserCombinators.Parsec+-- import Text.ParserCombinators.Parsec+import Text.Parsec hiding (Empty)  import Hledger.Utils import Hledger.Data.Types@@ -206,7 +211,7 @@     ,"desc"     ,"acct"     ,"date"-    ,"edate"+    ,"date2"     ,"status"     ,"cur"     ,"real"@@ -227,7 +232,8 @@ -- query :: GenParser String () Query -- query = undefined --- | Parse a single query term as either a query or a query option.+-- | Parse a single query term as either a query or a query option,+-- or raise an error if it has invalid syntax. parseQueryTerm :: Day -> String -> Either Query QueryOpt parseQueryTerm _ ('i':'n':'a':'c':'c':'t':'o':'n':'l':'y':':':s) = Right $ QueryOptInAcctOnly s parseQueryTerm _ ('i':'n':'a':'c':'c':'t':':':s) = Right $ QueryOptInAcct s@@ -237,12 +243,12 @@ parseQueryTerm _ ('c':'o':'d':'e':':':s) = Left $ Code s parseQueryTerm _ ('d':'e':'s':'c':':':s) = Left $ Desc s parseQueryTerm _ ('a':'c':'c':'t':':':s) = Left $ Acct s+parseQueryTerm d ('d':'a':'t':'e':'2':':':s) =+        case parsePeriodExpr d s of Left e         -> error' $ "\"date2:"++s++"\" gave a "++showDateParseError e+                                    Right (_,span) -> Left $ Date2 span parseQueryTerm d ('d':'a':'t':'e':':':s) =-        case parsePeriodExpr d s of Left _ -> Left None -- XXX should warn+        case parsePeriodExpr d s of Left e         -> error' $ "\"date:"++s++"\" gave a "++showDateParseError e                                     Right (_,span) -> Left $ Date span-parseQueryTerm d ('e':'d':'a':'t':'e':':':s) =-        case parsePeriodExpr d s of Left _ -> Left None -- XXX should warn-                                    Right (_,span) -> Left $ Date2 span parseQueryTerm _ ('s':'t':'a':'t':'u':'s':':':s) = Left $ Status $ parseStatus s parseQueryTerm _ ('r':'e':'a':'l':':':s) = Left $ Real $ parseBool s parseQueryTerm _ ('a':'m':'t':':':s) = Left $ Amt ord q where (ord, q) = parseAmountQueryTerm s@@ -274,7 +280,7 @@  ]  -data OrdPlus = Lt | Gt | Eq | AbsLt | AbsGt | AbsEq+data OrdPlus = Lt | LtEq | Gt | GtEq | Eq | AbsLt | AbsLtEq | AbsGt | AbsGtEq | AbsEq  deriving (Show,Eq,Data,Typeable)  -- can fail@@ -282,21 +288,29 @@ parseAmountQueryTerm s' =   case s' of     -- feel free to do this a smarter way-    ""        -> err-    '<':'+':s -> (Lt, readDef err s)-    '>':'+':s -> (Gt, readDef err s)-    '=':'+':s -> (Eq, readDef err s)-    '+':s     -> (Eq, readDef err s)-    '<':'-':s -> (Lt, negate $ readDef err s)-    '>':'-':s -> (Gt, negate $ readDef err s)-    '=':'-':s -> (Eq, negate $ readDef err s)-    '-':s     -> (Eq, negate $ readDef err s)-    '<':s     -> let n = readDef err s in case n of 0 -> (Lt, 0)-                                                    _ -> (AbsLt, n)-    '>':s     -> let n = readDef err s in case n of 0 -> (Gt, 0)-                                                    _ -> (AbsGt, n)-    '=':s     -> (AbsEq, readDef err s)-    s         -> (AbsEq, readDef err s)+    ""              -> err+    '<':'+':s       -> (Lt, readDef err s)+    '<':'=':'+':s   -> (LtEq, readDef err s)+    '>':'+':s       -> (Gt, readDef err s)+    '>':'=':'+':s   -> (GtEq, readDef err s)+    '=':'+':s       -> (Eq, readDef err s)+    '+':s           -> (Eq, readDef err s)+    '<':'-':s       -> (Lt, negate $ readDef err s)+    '<':'=':'-':s   -> (LtEq, negate $ readDef err s)+    '>':'-':s       -> (Gt, negate $ readDef err s)+    '>':'=':'-':s   -> (GtEq, negate $ readDef err s)+    '=':'-':s       -> (Eq, negate $ readDef err s)+    '-':s           -> (Eq, negate $ readDef err s)+    '<':'=':s       -> let n = readDef err s in case n of 0 -> (LtEq, 0)+                                                          _ -> (AbsLtEq, n)+    '<':s           -> let n = readDef err s in case n of 0 -> (Lt, 0)+                                                          _ -> (AbsLt, n)+    '>':'=':s       -> let n = readDef err s in case n of 0 -> (GtEq, 0)+                                                          _ -> (AbsGtEq, n)+    '>':s           -> let n = readDef err s in case n of 0 -> (Gt, 0)+                                                          _ -> (AbsGt, n)+    '=':s           -> (AbsEq, readDef err s)+    s               -> (AbsEq, readDef err s)   where     err = error' $ "could not parse as '=', '<', or '>' (optional) followed by a (optionally signed) numeric quantity: " ++ s' @@ -308,7 +322,7 @@     ">10000.10" `gives` (AbsGt,10000.1)     "=0.23" `gives` (AbsEq,0.23)     "0.23" `gives` (AbsEq,0.23)-    "=+0.23" `gives` (Eq,0.23)+    "<=+0.23" `gives` (LtEq,0.23)     "-0.23" `gives` (Eq,(-0.23))   ] @@ -317,18 +331,18 @@            | otherwise    = (s, Nothing)            where (n,v) = break (=='=') s --- | Parse the boolean value part of a "status:" query, allowing "*" as--- another way to spell True, similar to the journal file format.+-- -- , treating "*" or "!" as synonyms for "1".+-- | Parse the boolean value part of a "status:" query. parseStatus :: String -> Bool-parseStatus s = s `elem` (truestrings ++ ["*"])+parseStatus s = s `elem` (truestrings) -- ++ ["*","!"]) --- | Parse the boolean value part of a "status:" query. A true value can--- be spelled as "1", "t" or "true".+-- | Parse the boolean value part of a "status:" query. "1" means true,+-- anything else will be parsed as false without error. parseBool :: String -> Bool parseBool s = s `elem` truestrings  truestrings :: [String]-truestrings = ["1","t","true"]+truestrings = ["1"]  simplifyQuery :: Query -> Query simplifyQuery q =@@ -349,6 +363,7 @@                      -- all queryIsDate qs = Date $ spansUnion $ mapMaybe queryTermDateSpan qs  ?                      | otherwise = Or $ map simplify $ filter (/=None) qs     simplify (Date (DateSpan Nothing Nothing)) = Any+    simplify (Date2 (DateSpan Nothing Nothing)) = Any     simplify q = q  tests_simplifyQuery = [@@ -403,9 +418,17 @@  queryIsDate :: Query -> Bool queryIsDate (Date _) = True-queryIsDate (Date2 _) = True queryIsDate _ = False +queryIsDate2 :: Query -> Bool+queryIsDate2 (Date2 _) = True+queryIsDate2 _ = False++queryIsDateOrDate2 :: Query -> Bool+queryIsDateOrDate2 (Date _) = True+queryIsDateOrDate2 (Date2 _) = True+queryIsDateOrDate2 _ = False+ queryIsDesc :: Query -> Bool queryIsDesc (Desc _) = True queryIsDesc _ = False@@ -439,6 +462,15 @@ queryStartDate True (Date2 (DateSpan (Just d) _)) = Just d queryStartDate _ _ = Nothing +-- | What end date (or secondary date) does this query specify, if any ?+-- For OR expressions, use the latest of the dates. NOT is ignored.+queryEndDate :: Bool -> Query -> Maybe Day+queryEndDate secondary (Or ms) = latestMaybeDate' $ map (queryEndDate secondary) ms+queryEndDate secondary (And ms) = earliestMaybeDate' $ map (queryEndDate secondary) ms+queryEndDate False (Date (DateSpan _ (Just d))) = Just d+queryEndDate True (Date2 (DateSpan _ (Just d))) = Just d+queryEndDate _ _ = Nothing+ queryTermDateSpan (Date span) = Just span queryTermDateSpan _ = Nothing @@ -456,14 +488,36 @@ queryDateSpans True (Date2 span) = [span] queryDateSpans _ _ = [] --- | What is the earliest of these dates, where Nothing is earliest ?+-- | What date span (or secondary date span) does this query specify ?+-- For OR expressions, use the widest possible span. NOT is ignored.+queryDateSpan' :: Query -> DateSpan+queryDateSpan' q = spansUnion $ queryDateSpans' q++-- | Extract all date (or secondary date) spans specified in this query.+-- NOT is ignored.+queryDateSpans' :: Query -> [DateSpan]+queryDateSpans' (Or qs) = concatMap queryDateSpans' qs+queryDateSpans' (And qs) = concatMap queryDateSpans' qs+queryDateSpans' (Date span) = [span]+queryDateSpans' (Date2 span) = [span]+queryDateSpans' _ = []++-- | What is the earliest of these dates, where Nothing is latest ? earliestMaybeDate :: [Maybe Day] -> Maybe Day-earliestMaybeDate = headDef Nothing . sortBy compareMaybeDates+earliestMaybeDate mds = head $ sortBy compareMaybeDates mds ++ [Nothing]  -- | What is the latest of these dates, where Nothing is earliest ? latestMaybeDate :: [Maybe Day] -> Maybe Day latestMaybeDate = headDef Nothing . sortBy (flip compareMaybeDates) +-- | What is the earliest of these dates, ignoring Nothings ?+earliestMaybeDate' :: [Maybe Day] -> Maybe Day+earliestMaybeDate' = headDef Nothing . sortBy compareMaybeDates . filter isJust++-- | What is the latest of these dates, ignoring Nothings ?+latestMaybeDate' :: [Maybe Day] -> Maybe Day+latestMaybeDate' = headDef Nothing . sortBy (flip compareMaybeDates) . filter isJust+ -- | Compare two maybe dates, Nothing is earliest. compareMaybeDates :: Maybe Day -> Maybe Day -> Ordering compareMaybeDates Nothing Nothing = EQ@@ -566,12 +620,16 @@  -- | Is this amount's quantity less than, greater than, equal to, or unsignedly equal to this number ? compareAmount :: OrdPlus -> Quantity -> Amount -> Bool-compareAmount ord q Amount{aquantity=aq} = case ord of Lt    -> aq <  q-                                                       Gt    -> aq >  q-                                                       Eq    -> aq == q-                                                       AbsLt -> abs aq <  abs q-                                                       AbsGt -> abs aq >  abs q-                                                       AbsEq -> abs aq == abs q+compareAmount ord q Amount{aquantity=aq} = case ord of Lt      -> aq <  q+                                                       LtEq    -> aq <= q+                                                       Gt      -> aq >  q+                                                       GtEq    -> aq >= q+                                                       Eq      -> aq == q+                                                       AbsLt   -> abs aq <  abs q+                                                       AbsLtEq -> abs aq <= abs q+                                                       AbsGt   -> abs aq >  abs q+                                                       AbsGtEq -> abs aq >= abs q+                                                       AbsEq   -> abs aq == abs q  -- | Does the match expression match this posting ? matchesPosting :: Query -> Posting -> Bool
Hledger/Read.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-}-{-| +{-|  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@@ -91,11 +91,11 @@  -- | Read the default journal file specified by the environment, or raise an error. defaultJournal :: IO Journal-defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing >>= either error' return+defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return  -- | Read a journal from the given string, trying all known formats, or simply throw an error. readJournal' :: String -> IO Journal-readJournal' s = readJournal Nothing Nothing Nothing s >>= either error' return+readJournal' s = readJournal Nothing Nothing True Nothing s >>= either error' return  tests_readJournal' = [   "readJournal' parses sample journal" ~: do@@ -114,9 +114,9 @@ -- - otherwise, try them all. -- -- A CSV conversion rules file may also be specified for use by the CSV reader.-readJournal :: Maybe StorageFormat -> Maybe FilePath -> Maybe FilePath -> String -> IO (Either String Journal)-readJournal format rulesfile path s =-  -- trace (show (format, rulesfile, path)) $+-- Also there is a flag specifying whether to check or ignore balance assertions in the journal.+readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal)+readJournal format rulesfile assrt path s =   tryReaders $ readersFor (format, path, s)   where     -- try each reader in turn, returning the error of the first if all fail@@ -126,8 +126,9 @@         firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal)         firstSuccessOrBestError [] []        = return $ Left "no readers found"         firstSuccessOrBestError errs (r:rs) = do-          -- printf "trying %s reader\n" (rFormat r)-          result <- (runErrorT . (rParser r) rulesfile path') s+          dbgAtM 1 "trying reader" (rFormat r)+          result <- (runErrorT . (rParser r) rulesfile assrt path') s+          dbgAtM 1 "reader result" $ either id show result           case result of Right j -> return $ Right j                       -- success!                          Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying         firstSuccessOrBestError (e:_) []    = return $ Left e              -- none left, return first error@@ -136,11 +137,11 @@ -- | Which readers are worth trying for this (possibly unspecified) format, filepath, and data ? readersFor :: (Maybe StorageFormat, Maybe FilePath, String) -> [Reader] readersFor (format,path,s) =-    case format of +    dbg ("possible readers for "++show (format,path,elideRight 30 s)) $+    case format of      Just f  -> case readerForStorageFormat f of Just r  -> [r]                                                  Nothing -> []      Nothing -> case path of Nothing  -> readers-                             Just "-" -> readers                              Just p   -> case readersForPathAndData (p,s) of [] -> readers                                                                              rs -> rs @@ -148,7 +149,7 @@ readerForStorageFormat :: StorageFormat -> Maybe Reader readerForStorageFormat s | null rs = Nothing                   | otherwise = Just $ head rs-    where +    where       rs = filter ((s==).rFormat) readers :: [Reader]  -- | Find the readers which think they can handle the given file path and data, if any.@@ -158,16 +159,17 @@ -- | Read a Journal from this file (or stdin if the filename is -) or give -- an error message, using the specified data format or trying all known -- formats. A CSV conversion rules file may be specified for better--- conversion of that format.-readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> FilePath -> IO (Either String Journal)-readJournalFile format rulesfile "-" = do+-- conversion of that format. Also there is a flag specifying whether+-- to check or ignore balance assertions in the journal.+readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> Bool -> FilePath -> IO (Either String Journal)+readJournalFile format rulesfile assrt "-" = do   hSetNewlineMode stdin universalNewlineMode-  getContents >>= readJournal format rulesfile (Just "(stdin)")-readJournalFile format rulesfile f = do+  getContents >>= readJournal format rulesfile assrt (Just "-")+readJournalFile format rulesfile assrt f = do   requireJournalFileExists f   withFile f ReadMode $ \h -> do     hSetNewlineMode h universalNewlineMode-    hGetContents h >>= readJournal format rulesfile (Just f)+    hGetContents h >>= readJournal format rulesfile assrt (Just f)  -- | If the specified journal file does not exist, give a helpful error and quit. requireJournalFileExists :: FilePath -> IO ()@@ -202,6 +204,11 @@  ,"    assets:bank:checking  $1"  ,"    income:salary"  ,""+ ,"comment"+ ,"multi line comment here"+ ,"for testing purposes"+ ,"end comment"+ ,""  ,"2008/06/01 gift"  ,"    assets:bank:checking  $1"  ,"    income:gifts"@@ -228,8 +235,9 @@    tests_Hledger_Read_CsvReader,     "journal" ~: do-    assertBool "journal should parse an empty file" (isRight $ parseWithCtx nullctx JournalReader.journal "")-    jE <- readJournal Nothing Nothing Nothing "" -- don't know how to get it from journal+    r <- runErrorT $ parseWithCtx nullctx JournalReader.journal ""+    assertBool "journal should parse an empty file" (isRight $ r)+    jE <- readJournal Nothing Nothing True Nothing "" -- don't know how to get it from journal     either error' (assertBool "journal parsing an empty file should give an empty journal" . null . jtxns) jE    ]
Hledger/Read/CsvReader.hs view
@@ -3,6 +3,8 @@ A reader for CSV data, using an extra rules file to help interpret the data.  -}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Hledger.Read.CsvReader (   -- * Reader@@ -35,9 +37,9 @@ import System.Locale (defaultTimeLocale) import Test.HUnit import Text.CSV (parseCSV, CSV)-import Text.ParserCombinators.Parsec  hiding (parse)-import Text.ParserCombinators.Parsec.Error-import Text.ParserCombinators.Parsec.Pos+import Text.Parsec hiding (parse)+import Text.Parsec.Pos+import Text.Parsec.Error import Text.Printf (hPrintf,printf)  import Hledger.Data@@ -53,15 +55,16 @@ format :: String format = "csv" --- | Does the given file path and data look like CSV ?+-- | Does the given file path and data look like it might be CSV ? detect :: FilePath -> String -> Bool-detect f _ = takeExtension f == '.':format+detect f s+  | f /= "-"  = takeExtension f == '.':format  -- from a file: yes if the extension is .csv+  | otherwise = length (filter (==',') s) >= 2 -- from stdin: yes if there are two or more commas  -- | Parse and post-process a "Journal" from CSV data, or give an error. -- XXX currently ignores the string and reads from the file path-parse :: Maybe FilePath -> FilePath -> String -> ErrorT String IO Journal-parse rulesfile f s = -- trace ("running "++format++" reader") $- do+parse :: Maybe FilePath -> Bool -> FilePath -> String -> ErrorT String IO Journal+parse rulesfile _ f s = do   r <- liftIO $ readJournalFromCsv rulesfile f s   case r of Left e -> throwError e             Right j -> return j@@ -78,7 +81,7 @@ -- 5. convert the CSV records to a journal using the rules -- @ readJournalFromCsv :: Maybe FilePath -> FilePath -> String -> IO (Either String Journal)-readJournalFromCsv Nothing "-" _ = return $ Left "please use --rules-file when converting stdin"+readJournalFromCsv Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin" readJournalFromCsv mrulesfile csvfile csvdata =  handle (\e -> return $ Left $ show (e :: IOException)) $ do   let throwerr = throw.userError@@ -89,8 +92,11 @@   if created    then hPrintf stderr "creating default conversion rules file %s, edit this file for better results\n" rulesfile    else hPrintf stderr "using conversion rules file %s\n" rulesfile-  rules <- either (throwerr.show) id `fmap` parseRulesFile rulesfile-  return $ dbg "" rules+  rules_ <- liftIO $ runErrorT $ parseRulesFile rulesfile+  let rules = case rules_ of+              Right (t::CsvRules) -> t+              Left err -> throwerr $ show err+  dbgAtM 2 "rules" rules    -- apply skip directive   let skip = maybe 0 oneorerror $ getDirective "skip" rules@@ -99,17 +105,30 @@           oneorerror s  = readDef (throwerr $ "could not parse skip value: " ++ show s) s    -- parse csv-  records <- (either throwerr id . validateCsv skip) `fmap` parseCsv csvfile csvdata-  dbgAtM 1 "" $ take 3 records+  -- parsec seems to fail if you pass it "-" here+  let parsecfilename = if csvfile == "-" then "(stdin)" else csvfile+  records <- (either throwerr id .+              dbgAt 2 "validateCsv" . validateCsv skip .+              dbgAt 2 "parseCsv")+             `fmap` parseCsv parsecfilename csvdata+  dbgAtM 1 "first 3 csv records" $ take 3 records    -- identify header lines   -- let (headerlines, datalines) = identifyHeaderLines records   --     mfieldnames = lastMay headerlines    -- convert to transactions and return as a journal-  let txns = map (transactionFromCsvRecord rules) records-  return $ Right nulljournal{jtxns=sortBy (comparing tdate) txns}+  let txns = snd $ mapAccumL+                     (\pos r -> (pos, transactionFromCsvRecord (incSourceLine pos 1) rules r))+                     (initialPos parsecfilename) records +  -- heuristic: if the records appear to have been in reverse date order,+  -- reverse them all as well as doing a txn date sort,+  -- so that same-day txns' original order is preserved+      txns' | length txns > 1 && tdate (head txns) > tdate (last txns) = reverse txns+            | otherwise = txns+  return $ Right nulljournal{jtxns=sortBy (comparing tdate) txns'}+ parseCsv :: FilePath -> String -> IO (Either ParseError CSV) parseCsv path csvdata =   case path of@@ -173,7 +192,7 @@ newRulesFileContent :: FilePath -> String newRulesFileContent f = unlines   ["# hledger csv conversion rules for " ++ csvFileFor (takeFileName f)-  ,"# cf http://hledger.org/MANUAL.html"+  ,"# cf http://hledger.org/manual#csv-files"   ,""   ,"account1 assets:bank:checking"   ,""@@ -278,10 +297,10 @@ type JournalFieldName = String type FieldTemplate    = String type ConditionalBlock = ([RecordMatcher], [(JournalFieldName, FieldTemplate)]) -- block matches if all RecordMatchers match-type RecordMatcher    = [Regexp] -- match if any regexps match any of the csv fields--- type FieldMatcher     = (CsvFieldName, [Regexp]) -- match if any regexps match this csv field+type RecordMatcher    = [RegexpPattern] -- match if any regexps match any of the csv fields+-- type FieldMatcher     = (CsvFieldName, [RegexpPattern]) -- match if any regexps match this csv field type DateFormat       = String-type Regexp           = String+type RegexpPattern           = String  rules = CsvRules {   rdirectives=[],@@ -316,28 +335,31 @@ getDirective directivename = lookup directivename . rdirectives  -parseRulesFile :: FilePath -> IO (Either ParseError CsvRules)+parseRulesFile :: FilePath -> ErrorT String IO CsvRules parseRulesFile f = do-  s <- readFile' f >>= expandIncludes+  s <- liftIO $ (readFile' f >>= expandIncludes (takeDirectory f))   let rules = parseCsvRules f s-  return $ case rules of-             Left e -> Left e-             Right r -> case validateRules r of-                          Left e -> Left $ toParseError e-                          Right r -> Right r+  case rules of+    Left e -> ErrorT $ return $ Left $ show e+    Right r -> do+               r_ <- liftIO $ runErrorT $ validateRules r+               ErrorT $ case r_ of+                 Left e -> return $ Left $ show $ toParseError e+                 Right r -> return $ Right r   where     toParseError s = newErrorMessage (Message s) (initialPos "")  -- | Pre-parse csv rules to interpolate included files, recursively. -- This is a cheap hack to avoid rewriting the existing parser.-expandIncludes :: String -> IO String-expandIncludes s = do-  let (ls,rest) = break (isPrefixOf "include") $ lines s+expandIncludes :: FilePath -> String -> IO String+expandIncludes basedir content = do+  let (ls,rest) = break (isPrefixOf "include") $ lines content   case rest of     [] -> return $ unlines ls     (('i':'n':'c':'l':'u':'d':'e':f):ls') -> do-      let f' = dropWhile isSpace f-      included <- readFile f' >>= expandIncludes+      let f'       = basedir </> dropWhile isSpace f+          basedir' = takeDirectory f'+      included <- readFile f' >>= expandIncludes basedir'       return $ unlines [unlines ls, included, unlines ls']     ls' -> return $ unlines $ ls ++ ls'   -- should never get here @@ -347,13 +369,13 @@   runParser rulesp rules rulesfile s  -- | Return the validated rules, or an error.-validateRules :: CsvRules -> Either String CsvRules+validateRules :: CsvRules -> ErrorT String IO CsvRules validateRules rules = do-  unless (isAssigned "date")   $ Left "Please specify (at top level) the date field. Eg: date %1\n"+  unless (isAssigned "date")   $ ErrorT $ return $ Left "Please specify (at top level) the date field. Eg: date %1\n"   unless ((amount && not (amountin || amountout)) ||           (not amount && (amountin && amountout)))-    $ Left "Please specify (at top level) either the amount field, or both the amount-in and amount-out fields. Eg: amount %2\n"-  Right rules+    $ ErrorT $ return $ Left "Please specify (at top level) either the amount field, or both the amount-in and amount-out fields. Eg: amount %2\n"+  ErrorT $ return $ Right rules   where     amount = isAssigned "amount"     amountin = isAssigned "amount-in"@@ -362,14 +384,14 @@  -- parsers -rulesp :: GenParser Char CsvRules CsvRules+rulesp :: Stream [Char] m t => ParsecT [Char] CsvRules m CsvRules rulesp = do   many $ choice'     [blankorcommentline                                                    <?> "blank or comment line"-    ,(directive        >>= updateState . addDirective)                     <?> "directive"-    ,(fieldnamelist    >>= updateState . setIndexesAndAssignmentsFromList) <?> "field name list"-    ,(fieldassignment  >>= updateState . addAssignment)                    <?> "field assignment"-    ,(conditionalblock >>= updateState . addConditionalBlock)              <?> "conditional block"+    ,(directive        >>= modifyState . addDirective)                     <?> "directive"+    ,(fieldnamelist    >>= modifyState . setIndexesAndAssignmentsFromList) <?> "field name list"+    ,(fieldassignment  >>= modifyState . addAssignment)                    <?> "field assignment"+    ,(conditionalblock >>= modifyState . addConditionalBlock)              <?> "conditional block"     ]   eof   r <- getState@@ -378,13 +400,21 @@           ,rconditionalblocks=reverse $ rconditionalblocks r           } -blankorcommentline = pdbg 1 "trying blankorcommentline" >> choice' [blankline, commentline]+blankorcommentline :: Stream [Char] m t => ParsecT [Char] CsvRules m ()+blankorcommentline = pdbg 3 "trying blankorcommentline" >> choice' [blankline, commentline]++blankline :: Stream [Char] m t => ParsecT [Char] CsvRules m () blankline = many spacenonewline >> newline >> return () <?> "blank line"++commentline :: Stream [Char] m t => ParsecT [Char] CsvRules m () commentline = many spacenonewline >> commentchar >> restofline >> return () <?> "comment line"++commentchar :: Stream [Char] m t => ParsecT [Char] CsvRules m Char commentchar = oneOf ";#" +directive :: Stream [Char] m t => ParsecT [Char] CsvRules m (DirectiveName, String) directive = do-  pdbg 1 "trying directive"+  pdbg 3 "trying directive"   d <- choice' $ map string directives   v <- (((char ':' >> many spacenonewline) <|> many1 spacenonewline) >> directiveval)        <|> (optional (char ':') >> many spacenonewline >> eolof >> return "")@@ -401,10 +431,12 @@    -- ,"base-currency"   ] +directiveval :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] directiveval = anyChar `manyTill` eolof +fieldnamelist :: Stream [Char] m t => ParsecT [Char] CsvRules m [CsvFieldName] fieldnamelist = (do-  pdbg 1 "trying fieldnamelist"+  pdbg 3 "trying fieldnamelist"   string "fields"   optional $ char ':'   many1 spacenonewline@@ -415,24 +447,29 @@   return $ map (map toLower) $ f:fs   ) <?> "field name list" +fieldname :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] fieldname = quotedfieldname <|> barefieldname +quotedfieldname :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] quotedfieldname = do   char '"'   f <- many1 $ noneOf "\"\n:;#~"   char '"'   return f +barefieldname :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] barefieldname = many1 $ noneOf " \t\n,;#~" +fieldassignment :: Stream [Char] m t => ParsecT [Char] CsvRules m (JournalFieldName, FieldTemplate) fieldassignment = do-  pdbg 1 "trying fieldassignment"+  pdbg 3 "trying fieldassignment"   f <- journalfieldname   assignmentseparator   v <- fieldval   return (f,v)   <?> "field assignment" +journalfieldname :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] journalfieldname = pdbg 2 "trying journalfieldname" >> choice' (map string journalfieldnames)  journalfieldnames =@@ -452,6 +489,7 @@   ,"comment"   ] +assignmentseparator :: Stream [Char] m t => ParsecT [Char] CsvRules m () assignmentseparator = do   pdbg 3 "trying assignmentseparator"   choice [@@ -459,14 +497,17 @@     try (many spacenonewline >> char ':'),     space     ]-  many spacenonewline+  _ <- many spacenonewline+  return () +fieldval :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] fieldval = do   pdbg 2 "trying fieldval"   anyChar `manyTill` eolof +conditionalblock :: Stream [Char] m t => ParsecT [Char] CsvRules m ConditionalBlock conditionalblock = do-  pdbg 1 "trying conditionalblock"+  pdbg 3 "trying conditionalblock"   string "if" >> many spacenonewline >> optional newline   ms <- many1 recordmatcher   as <- many (many1 spacenonewline >> fieldassignment)@@ -475,6 +516,7 @@   return (ms, as)   <?> "conditional block" +recordmatcher :: Stream [Char] m t => ParsecT [Char] CsvRules m [[Char]] recordmatcher = do   pdbg 2 "trying recordmatcher"   -- pos <- currentPos@@ -485,6 +527,7 @@   return ps   <?> "record matcher" +matchoperator :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] matchoperator = choice' $ map string   ["~"   -- ,"!~"@@ -492,11 +535,13 @@   -- ,"!="   ] +patterns :: Stream [Char] m t => ParsecT [Char] CsvRules m [[Char]] patterns = do   pdbg 3 "trying patterns"   ps <- many regexp   return ps +regexp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char] regexp = do   pdbg 3 "trying regexp"   notFollowedBy matchoperator@@ -524,8 +569,8 @@  -- Convert a CSV record to a transaction using the rules, or raise an -- error if the data can not be parsed.-transactionFromCsvRecord :: CsvRules -> CsvRecord -> Transaction-transactionFromCsvRecord rules record = t+transactionFromCsvRecord :: SourcePos -> CsvRules -> CsvRecord -> Transaction+transactionFromCsvRecord sourcepos rules record = t   where     mdirective       = (`getDirective` rules)     mfieldtemplate   = getEffectiveAssignment rules record@@ -559,7 +604,7 @@     precomment  = maybe "" render $ mfieldtemplate "precomment"     currency    = maybe (fromMaybe "" mdefaultcurrency) render $ mfieldtemplate "currency"     amountstr   = (currency++) $ negateIfParenthesised $ getAmountStr rules record-    amount      = either amounterror mixed $ runParser (do {a <- amountp; eof; return a}) nullctx "" amountstr+    amount      = either amounterror (Mixed . (:[])) $ runParser (do {a <- amountp; eof; return a}) nullctx "" amountstr     amounterror err = error' $ unlines       ["error: could not parse \""++amountstr++"\" as an amount"       ,showRecord record@@ -585,6 +630,7 @@      -- build the transaction     t = nulltransaction{+      tsourcepos               = sourcepos,       tdate                    = date',       tdate2                   = mdate2',       tstatus                  = status,@@ -648,8 +694,8 @@                 -- matcherMatches pats = any patternMatches pats                 matcherMatches pats = patternMatches $  "(" ++ intercalate "|" pats ++ ")"                   where-                    patternMatches :: Regexp -> Bool-                    patternMatches pat = regexMatchesCIRegexCompat pat csvline+                    patternMatches :: RegexpPattern -> Bool+                    patternMatches pat = regexMatchesCI pat csvline                       where                         csvline = intercalate "," record 
Hledger/Read/JournalReader.hs view
@@ -1,5 +1,6 @@ -- {-# OPTIONS_GHC -F -pgmF htfpp #-}-{-# LANGUAGE CPP, RecordWildCards, NoMonoLocalBinds #-}+{-# LANGUAGE CPP, RecordWildCards, NoMonoLocalBinds, ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-} {-|  A reader for hledger's journal file format@@ -58,7 +59,7 @@ import Test.Framework import Text.Parsec.Error #endif-import Text.ParserCombinators.Parsec hiding (parse)+import Text.Parsec hiding (parse) import Text.Printf import System.FilePath import System.Time (getClockTime)@@ -76,83 +77,86 @@ format :: String format = "journal" --- | Does the given file path and data provide hledger's journal file format ?+-- | Does the given file path and data look like it might be hledger's journal format ? detect :: FilePath -> String -> Bool-detect f _ = takeExtension f `elem` ['.':format, ".j"]+detect f s+  | f /= "-"  = takeExtension f `elem` ['.':format, ".j"]  -- from a file: yes if the extension is .journal or .j+  -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented)+  | otherwise = regexMatches "^[0-9]+.*\n[ \t]+" s  -- | Parse and post-process a "Journal" from hledger's journal file -- format, or give an error.-parse :: Maybe FilePath -> FilePath -> String -> ErrorT String IO Journal-parse _ = -- trace ("running "++format++" reader") .-          parseJournalWith journal+parse :: Maybe FilePath -> Bool -> FilePath -> String -> ErrorT String IO Journal+parse _ = parseJournalWith journal  -- parsing utils  -- | Flatten a list of JournalUpdate's into a single equivalent one. combineJournalUpdates :: [JournalUpdate] -> JournalUpdate-combineJournalUpdates us = liftM (foldl' (.) id) $ sequence us+combineJournalUpdates us = liftM (foldl' (\acc new x -> new (acc x)) id) $ sequence us  -- | Given a JournalUpdate-generating parsec parser, file path and data string, -- parse and post-process a Journal so that it's ready to use, or give an error.-parseJournalWith :: (GenParser Char JournalContext (JournalUpdate,JournalContext)) -> FilePath -> String -> ErrorT String IO Journal-parseJournalWith p f s = do+parseJournalWith :: (ParsecT [Char] JournalContext (ErrorT String IO) (JournalUpdate,JournalContext)) -> Bool -> FilePath -> String -> ErrorT String IO Journal+parseJournalWith p assrt f s = do   tc <- liftIO getClockTime   tl <- liftIO getCurrentLocalTime   y <- liftIO getCurrentYear-  case runParser p nullctx{ctxYear=Just y} f s of+  r <- runParserT p nullctx{ctxYear=Just y} f s+  case r of     Right (updates,ctx) -> do                            j <- updates `ap` return nulljournal-                           case journalFinalise tc tl f s ctx j of+                           case journalFinalise tc tl f s ctx assrt j of                              Right j'  -> return j'                              Left estr -> throwError estr     Left e -> throwError $ show e -setYear :: Integer -> GenParser tok JournalContext ()-setYear y = updateState (\ctx -> ctx{ctxYear=Just y})+setYear :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()+setYear y = modifyState (\ctx -> ctx{ctxYear=Just y}) -getYear :: GenParser tok JournalContext (Maybe Integer)+getYear :: Stream [Char] m Char => ParsecT s JournalContext m (Maybe Integer) getYear = liftM ctxYear getState -setCommodityAndStyle :: (Commodity,AmountStyle) -> GenParser tok JournalContext ()-setCommodityAndStyle cs = updateState (\ctx -> ctx{ctxCommodityAndStyle=Just cs})+setDefaultCommodityAndStyle :: Stream [Char] m Char => (Commodity,AmountStyle) -> ParsecT [Char] JournalContext m ()+setDefaultCommodityAndStyle cs = modifyState (\ctx -> ctx{ctxDefaultCommodityAndStyle=Just cs}) -getCommodityAndStyle :: GenParser tok JournalContext (Maybe (Commodity,AmountStyle))-getCommodityAndStyle = ctxCommodityAndStyle `fmap` getState+getDefaultCommodityAndStyle :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe (Commodity,AmountStyle))+getDefaultCommodityAndStyle = ctxDefaultCommodityAndStyle `fmap` getState -pushParentAccount :: String -> GenParser tok JournalContext ()-pushParentAccount parent = updateState addParentAccount+pushParentAccount :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m ()+pushParentAccount parent = modifyState addParentAccount     where addParentAccount ctx0 = ctx0 { ctxAccount = parent : ctxAccount ctx0 } -popParentAccount :: GenParser tok JournalContext ()+popParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m () popParentAccount = do ctx0 <- getState                       case ctxAccount ctx0 of                         [] -> unexpected "End of account block with no beginning"                         (_:rest) -> setState $ ctx0 { ctxAccount = rest } -getParentAccount :: GenParser tok JournalContext String+getParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m String getParentAccount = liftM (concatAccountNames . reverse . ctxAccount) getState -addAccountAlias :: (AccountName,AccountName) -> GenParser tok JournalContext ()-addAccountAlias a = updateState (\(ctx@Ctx{..}) -> ctx{ctxAliases=a:ctxAliases})+addAccountAlias :: Stream [Char] m Char => AccountAlias -> ParsecT [Char] JournalContext m ()+addAccountAlias a = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=a:ctxAliases}) -getAccountAliases :: GenParser tok JournalContext [(AccountName,AccountName)]+getAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m [AccountAlias] getAccountAliases = liftM ctxAliases getState -clearAccountAliases :: GenParser tok JournalContext ()-clearAccountAliases = updateState (\(ctx@Ctx{..}) -> ctx{ctxAliases=[]})+clearAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()+clearAccountAliases = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=[]})  -- parsers  -- | Top-level journal parser. Returns a single composite, I/O performing, -- error-raising "JournalUpdate" (and final "JournalContext") which can be -- applied to an empty journal to get the final result.-journal :: GenParser Char JournalContext (JournalUpdate,JournalContext)+journal :: ParsecT [Char] JournalContext (ErrorT String IO) (JournalUpdate,JournalContext) journal = do   journalupdates <- many journalItem   eof   finalctx <- getState   return $ (combineJournalUpdates journalupdates, finalctx)-    where +    where       -- As all journal line types can be distinguished by the first       -- character, excepting transactions versus empty (blank or       -- comment-only) lines, can use choice w/o try@@ -162,10 +166,11 @@                            , liftM (return . addPeriodicTransaction) periodictransaction                            , liftM (return . addHistoricalPrice) historicalpricedirective                            , emptyorcommentlinep >> return (return id)+                           , multilinecommentp >> return (return id)                            ] <?> "journal transaction or directive"  -- cf http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives-directive :: GenParser Char JournalContext JournalUpdate+directive :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate directive = do   optional $ char '!'   choice' [@@ -183,7 +188,7 @@    ]   <?> "directive" -includedirective :: GenParser Char JournalContext JournalUpdate+includedirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate includedirective = do   string "include"   many1 spacenonewline@@ -191,36 +196,48 @@   outerState <- getState   outerPos <- getPosition   let curdir = takeDirectory (sourceName outerPos)-  return $ do filepath <- expandPath curdir filename-              txt <- readFileOrError outerPos filepath-              let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"-              case runParser journal outerState filepath txt of-                Right (ju,_) -> combineJournalUpdates [return $ journalAddFile (filepath,txt), ju] `catchError` (throwError . (inIncluded ++))-                Left err     -> throwError $ inIncluded ++ show err-      where readFileOrError pos fp =+  let (u::ErrorT String IO (Journal -> Journal, JournalContext)) = do+       filepath <- expandPath curdir filename+       txt <- readFileOrError outerPos filepath+       let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"+       r <- runParserT journal outerState filepath txt+       case r of+         Right (ju, ctx) -> do+                            u <- combineJournalUpdates [ return $ journalAddFile (filepath,txt)+                                                       , ju+                                                       ] `catchError` (throwError . (inIncluded ++))+                            return (u, ctx)+         Left err -> throwError $ inIncluded ++ show err+       where readFileOrError pos fp =                 ErrorT $ liftM Right (readFile' fp) `C.catch`                   \e -> return $ Left $ printf "%s reading %s:\n%s" (show pos) fp (show (e::C.IOException))+  r <- liftIO $ runErrorT u+  case r of+    Left err -> return $ throwError err+    Right (ju, ctx) -> return $ ErrorT $ return $ Right ju  journalAddFile :: (FilePath,String) -> Journal -> Journal journalAddFile f j@Journal{files=fs} = j{files=fs++[f]}-  -- XXX currently called in reverse order of includes, I can't see why+ -- NOTE: first encountered file to left, to avoid a reverse -accountdirective :: GenParser Char JournalContext JournalUpdate+accountdirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate accountdirective = do   string "account"   many1 spacenonewline   parent <- accountnamep   newline   pushParentAccount parent-  return $ return id+  -- return $ return id+  return $ ErrorT $ return $ Right id -enddirective :: GenParser Char JournalContext JournalUpdate+enddirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate enddirective = do   string "end"   popParentAccount-  return (return id)+  -- return (return id)+  return $ ErrorT $ return $ Right id -aliasdirective :: GenParser Char JournalContext JournalUpdate+aliasdirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate aliasdirective = do   string "alias"   many1 spacenonewline@@ -231,13 +248,13 @@                   ,accountNameWithoutPostingType $ strip alias)   return $ return id -endaliasesdirective :: GenParser Char JournalContext JournalUpdate+endaliasesdirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate endaliasesdirective = do   string "end aliases"   clearAccountAliases   return (return id) -tagdirective :: GenParser Char JournalContext JournalUpdate+tagdirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate tagdirective = do   string "tag" <?> "tag directive"   many1 spacenonewline@@ -245,13 +262,13 @@   restofline   return $ return id -endtagdirective :: GenParser Char JournalContext JournalUpdate+endtagdirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate endtagdirective = do   (string "end tag" <|> string "pop") <?> "end tag or pop directive"   restofline   return $ return id -defaultyeardirective :: GenParser Char JournalContext JournalUpdate+defaultyeardirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate defaultyeardirective = do   char 'Y' <?> "default year"   many spacenonewline@@ -261,20 +278,20 @@   setYear y'   return $ return id -defaultcommoditydirective :: GenParser Char JournalContext JournalUpdate+defaultcommoditydirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate defaultcommoditydirective = do   char 'D' <?> "default commodity"   many1 spacenonewline   Amount{..} <- amountp-  setCommodityAndStyle (acommodity, astyle)+  setDefaultCommodityAndStyle (acommodity, astyle)   restofline   return $ return id -historicalpricedirective :: GenParser Char JournalContext HistoricalPrice+historicalpricedirective :: ParsecT [Char] JournalContext (ErrorT String IO) HistoricalPrice historicalpricedirective = do   char 'P' <?> "historical price"   many spacenonewline-  date <- try (do {LocalTime d _ <- datetimep; return d}) <|> date -- a time is ignored+  date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored   many1 spacenonewline   symbol <- commoditysymbol   many spacenonewline@@ -282,7 +299,7 @@   restofline   return $ HistoricalPrice date symbol price -ignoredpricecommoditydirective :: GenParser Char JournalContext JournalUpdate+ignoredpricecommoditydirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate ignoredpricecommoditydirective = do   char 'N' <?> "ignored-price commodity"   many1 spacenonewline@@ -290,7 +307,7 @@   restofline   return $ return id -commodityconversiondirective :: GenParser Char JournalContext JournalUpdate+commodityconversiondirective :: ParsecT [Char] JournalContext (ErrorT String IO) JournalUpdate commodityconversiondirective = do   char 'C' <?> "commodity conversion"   many1 spacenonewline@@ -302,7 +319,7 @@   restofline   return $ return id -modifiertransaction :: GenParser Char JournalContext ModifierTransaction+modifiertransaction :: ParsecT [Char] JournalContext (ErrorT String IO) ModifierTransaction modifiertransaction = do   char '=' <?> "modifier transaction"   many spacenonewline@@ -310,7 +327,7 @@   postings <- postings   return $ ModifierTransaction valueexpr postings -periodictransaction :: GenParser Char JournalContext PeriodicTransaction+periodictransaction :: ParsecT [Char] JournalContext (ErrorT String IO) PeriodicTransaction periodictransaction = do   char '~' <?> "periodic transaction"   many spacenonewline@@ -319,18 +336,20 @@   return $ PeriodicTransaction periodexpr postings  -- | Parse a (possibly unbalanced) transaction.-transaction :: GenParser Char JournalContext Transaction+transaction :: ParsecT [Char] JournalContext (ErrorT String IO) Transaction transaction = do   -- ptrace "transaction"-  date <- date <?> "transaction"-  edate <- optionMaybe (secondarydate date) <?> "secondary date"+  sourcepos <- getPosition+  date <- datep <?> "transaction"+  edate <- optionMaybe (secondarydatep date) <?> "secondary date"+  lookAhead (spacenonewline <|> newline) <?> "whitespace or newline"   status <- status <?> "cleared flag"   code <- codep <?> "transaction code"   description <- descriptionp >>= return . strip   comment <- try followingcommentp <|> (newline >> return "")   let tags = tagsInComment comment   postings <- postings-  return $ txnTieKnot $ Transaction date edate status code description comment tags postings ""+  return $ txnTieKnot $ Transaction sourcepos date edate status code description comment tags postings ""  descriptionp = many (noneOf ";\n") @@ -350,7 +369,7 @@                         assertEqual (ttags t) (ttags t2)                         assertEqual (tpreceding_comment_lines t) (tpreceding_comment_lines t2)                         assertEqual (show $ tpostings t) (show $ tpostings t2)-    -- "0000/01/01\n\n" `gives` nulltransaction +    -- "0000/01/01\n\n" `gives` nulltransaction     unlines [       "2012/05/14=2012/05/15 (code) desc  ; tcomment1",       "    ; tcomment2",@@ -408,7 +427,7 @@         ,"  b"         ," "         ]-                    +     let p = parseWithCtx nullctx transaction $ unlines              ["2009/1/1 x  ; transaction comment"              ," a  1  ; posting 1 comment"@@ -418,16 +437,18 @@              ]     assertRight p     assertEqual 2 (let Right t = p in length $ tpostings t)-#endif       +#endif  -- | Parse a date in YYYY/MM/DD format. Fewer digits are allowed. The year -- may be omitted if a default year has already been set.-date :: GenParser Char JournalContext Day-date = do+datep :: Stream [Char] m t => ParsecT [Char] JournalContext m Day+datep = do   -- hacky: try to ensure precise errors for invalid dates   -- XXX reported error position is not too good   -- pos <- getPosition   datestr <- many1 $ choice' [digit, datesepchar]+  let sepchars = nub $ sort $ filter (`elem` datesepchars) datestr+  when (length sepchars /= 1) $ fail $ "bad date, different separators used: " ++ datestr   let dateparts = wordsBy (`elem` datesepchars) datestr   currentyear <- getYear   [y,m,d] <- case (dateparts,currentyear) of@@ -445,9 +466,9 @@ -- timezone will be ignored; the time is treated as local time.  Fewer -- digits are allowed, except in the timezone. The year may be omitted if -- a default year has already been set.-datetimep :: GenParser Char JournalContext LocalTime+datetimep :: Stream [Char] m Char => ParsecT [Char] JournalContext m LocalTime datetimep = do-  day <- date+  day <- datep   many1 spacenonewline   h <- many1 digit   let h' = read h@@ -473,8 +494,8 @@   -- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')   return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s') -secondarydate :: Day -> GenParser Char JournalContext Day-secondarydate primarydate = do+secondarydatep :: Stream [Char] m Char => Day -> ParsecT [Char] JournalContext m Day+secondarydatep primarydate = do   char '='   -- kludgy way to use primary date for default year   let withDefaultYear d p = do@@ -483,27 +504,27 @@         r <- p         when (isJust y) $ setYear $ fromJust y         return r-  edate <- withDefaultYear primarydate date+  edate <- withDefaultYear primarydate datep   return edate -status :: GenParser Char JournalContext Bool+status :: Stream [Char] m Char => ParsecT [Char] JournalContext m Bool status = try (do { many spacenonewline; (char '*' <|> char '!') <?> "status"; return True } ) <|> return False -codep :: GenParser Char JournalContext String+codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String codep = try (do { many1 spacenonewline; char '(' <?> "codep"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""  -- Parse the following whitespace-beginning lines as postings, posting tags, and/or comments.-postings :: GenParser Char JournalContext [Posting]+postings :: Stream [Char] m Char => ParsecT [Char] JournalContext m [Posting] postings = many1 (try postingp) <?> "postings"-            --- linebeginningwithspaces :: GenParser Char JournalContext String++-- linebeginningwithspaces :: Stream [Char] m Char => ParsecT [Char] JournalContext m String -- linebeginningwithspaces = do --   sp <- many1 spacenonewline --   c <- nonspace --   cs <- restofline --   return $ sp ++ (c:cs) ++ "\n" -postingp :: GenParser Char JournalContext Posting+postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting postingp = do   many1 spacenonewline   status <- status@@ -511,22 +532,40 @@   account <- modifiedaccountname   let (ptype, account') = (accountNamePostingType account, unbracket account)   amount <- spaceandamountormissing-  massertion <- balanceassertion+  massertion <- partialbalanceassertion   _ <- fixedlotprice   many spacenonewline   ctx <- getState   comment <- try followingcommentp <|> (newline >> return "")   let tags = tagsInComment comment   -- oh boy-  d  <- maybe (return Nothing) (either (fail.show) (return.Just)) (parseWithCtx ctx date `fmap` dateValueFromTags tags)-  d2 <- maybe (return Nothing) (either (fail.show) (return.Just)) (parseWithCtx ctx date `fmap` date2ValueFromTags tags)-  return posting{pdate=d, pdate2=d2, pstatus=status, paccount=account', pamount=amount, pcomment=comment, ptype=ptype, ptags=tags, pbalanceassertion=massertion}+  date <- case dateValueFromTags tags of+        Nothing -> return Nothing+        Just v -> case runParser datep ctx "" v of+                    Right d -> return $ Just d+                    Left err -> parserFail $ show err+  date2 <- case date2ValueFromTags tags of+        Nothing -> return Nothing+        Just v -> case runParser datep ctx "" v of+                    Right d -> return $ Just d+                    Left err -> parserFail $ show err+  return posting+   { pdate=date+   , pdate2=date2+   , pstatus=status+   , paccount=account'+   , pamount=amount+   , pcomment=comment+   , ptype=ptype+   , ptags=tags+   , pbalanceassertion=massertion+   }  #ifdef TESTS test_postingp = do     let s `gives` ep = do                          let parse = parseWithCtx nullctx postingp s-                         assertBool -- "postingp parser" +                         assertBool -- "postingp parser"                            $ isRight parse                          let Right ap = parse                              same f = assertEqual (f ep) (f ap)@@ -541,21 +580,21 @@     "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n" `gives`       posting{paccount="expenses:food:dining", pamount=Mixed [usd 10], pcomment=" a: a a \n b: b b \n", ptags=[("a","a a"), ("b","b b")]} -    " a  1 ; [2012/11/28]\n" `gives` +    " a  1 ; [2012/11/28]\n" `gives`       ("a" `post` num 1){pcomment=" [2012/11/28]\n"                         ,ptags=[("date","2012/11/28")]                         ,pdate=parsedateM "2012/11/28"} -    " a  1 ; a:a, [=2012/11/28]\n" `gives` +    " a  1 ; a:a, [=2012/11/28]\n" `gives`       ("a" `post` num 1){pcomment=" a:a, [=2012/11/28]\n"                         ,ptags=[("a","a"), ("date2","2012/11/28")]                         ,pdate=Nothing} -    " a  1 ; a:a\n  ; [2012/11/28=2012/11/29],b:b\n" `gives` +    " a  1 ; a:a\n  ; [2012/11/28=2012/11/29],b:b\n" `gives`       ("a" `post` num 1){pcomment=" a:a\n [2012/11/28=2012/11/29],b:b\n"                         ,ptags=[("a","a"), ("date","2012/11/28"), ("date2","2012/11/29"), ("b","b")]                         ,pdate=parsedateM "2012/11/28"}-     +     assertBool -- "postingp parses a quoted commodity with numbers"       (isRight $ parseWithCtx nullctx postingp "  a  1 \"DE123\"\n") @@ -567,10 +606,10 @@     -- let Right p = parse     -- assertEqual "next-line comment\n" (pcomment p)     -- assertEqual (Just nullmixedamt) (pbalanceassertion p)-#endif       +#endif  -- | Parse an account name, then apply any parent account prefix and/or account aliases currently in effect.-modifiedaccountname :: GenParser Char JournalContext AccountName+modifiedaccountname :: Stream [Char] m Char => ParsecT [Char] JournalContext m AccountName modifiedaccountname = do   a <- accountnamep   prefix <- getParentAccount@@ -582,14 +621,14 @@ -- them, and are terminated by two or more spaces. They should have one or -- more components of at least one character, separated by the account -- separator char.-accountnamep :: GenParser Char st AccountName+accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName accountnamep = do     a <- many1 (nonspace <|> singlespace)     let a' = striptrailingspace a     when (accountNameFromComponents (accountNameComponents a') /= a')          (fail $ "account name seems ill-formed: "++a')     return a'-    where +    where       singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})       -- couldn't avoid consuming a final space sometimes, harmless       striptrailingspace s = if last s == ' ' then init s else s@@ -600,7 +639,7 @@ -- | Parse whitespace then an amount, with an optional left or right -- currency symbol and optional price, or return the special -- "missing" marker amount.-spaceandamountormissing :: GenParser Char JournalContext MixedAmount+spaceandamountormissing :: Stream [Char] m Char => ParsecT [Char] JournalContext m MixedAmount spaceandamountormissing =   try (do         many1 spacenonewline@@ -619,12 +658,12 @@     assertParseEqual' (parseWithCtx nullctx spaceandamountormissing "$47.18") missingmixedamt     assertParseEqual' (parseWithCtx nullctx spaceandamountormissing " ") missingmixedamt     assertParseEqual' (parseWithCtx nullctx spaceandamountormissing "") missingmixedamt-#endif       +#endif  -- | Parse a single-commodity amount, with optional symbol on the left or -- right, optional unit or total price, and optional (ignored) -- ledger-style balance assertion or fixed lot price declaration.-amountp :: GenParser Char JournalContext Amount+amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount amountp = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount  #ifdef TESTS@@ -639,69 +678,73 @@     assertParseEqual'      (parseWithCtx nullctx amountp "$10 @@ €5")      (usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0))-#endif       +#endif  -- | Parse an amount from a string, or get an error. amountp' :: String -> Amount-amountp' s = either (error' . show) id $ parseWithCtx nullctx amountp s+amountp' s =+  case runParser amountp nullctx "" s of+    Right t -> t+    Left err -> error' $ show err  -- | Parse a mixed amount from a string, or get an error. mamountp' :: String -> MixedAmount-mamountp' = mixed . amountp'+mamountp' = Mixed . (:[]) . amountp' -signp :: GenParser Char JournalContext String+signp :: Stream [Char] m t => ParsecT [Char] JournalContext m String signp = do   sign <- optionMaybe $ oneOf "+-"   return $ case sign of Just '-' -> "-"                         _        -> "" -leftsymbolamount :: GenParser Char JournalContext Amount+leftsymbolamount :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount leftsymbolamount = do   sign <- signp-  c <- commoditysymbol +  c <- commoditysymbol   sp <- many spacenonewline-  (q,prec,dec,sep,seppos) <- numberp-  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}+  (q,prec,mdec,mgrps) <- numberp+  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}   p <- priceamount   let applysign = if sign=="-" then negate else id   return $ applysign $ Amount c q p s   <?> "left-symbol amount" -rightsymbolamount :: GenParser Char JournalContext Amount+rightsymbolamount :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount rightsymbolamount = do-  (q,prec,dec,sep,seppos) <- numberp+  (q,prec,mdec,mgrps) <- numberp   sp <- many spacenonewline   c <- commoditysymbol   p <- priceamount-  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}+  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}   return $ Amount c q p s   <?> "right-symbol amount" -nosymbolamount :: GenParser Char JournalContext Amount+nosymbolamount :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount nosymbolamount = do-  (q,prec,dec,sep,seppos) <- numberp+  (q,prec,mdec,mgrps) <- numberp   p <- priceamount-  defcs <- getCommodityAndStyle+  -- apply the most recently seen default commodity and style to this commodityless amount+  defcs <- getDefaultCommodityAndStyle   let (c,s) = case defcs of         Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})-        Nothing          -> ("", amountstyle{asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos})+        Nothing          -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})   return $ Amount c q p s   <?> "no-symbol amount" -commoditysymbol :: GenParser Char JournalContext String+commoditysymbol :: Stream [Char] m t => ParsecT [Char] JournalContext m String commoditysymbol = (quotedcommoditysymbol <|> simplecommoditysymbol) <?> "commodity symbol" -quotedcommoditysymbol :: GenParser Char JournalContext String+quotedcommoditysymbol :: Stream [Char] m t => ParsecT [Char] JournalContext m String quotedcommoditysymbol = do   char '"'   s <- many1 $ noneOf ";\n\""   char '"'   return s -simplecommoditysymbol :: GenParser Char JournalContext String+simplecommoditysymbol :: Stream [Char] m t => ParsecT [Char] JournalContext m String simplecommoditysymbol = many1 (noneOf nonsimplecommoditychars) -priceamount :: GenParser Char JournalContext Price+priceamount :: Stream [Char] m t => ParsecT [Char] JournalContext m Price priceamount =     try (do           many spacenonewline@@ -717,8 +760,8 @@             return $ UnitPrice a))          <|> return NoPrice -balanceassertion :: GenParser Char JournalContext (Maybe MixedAmount)-balanceassertion =+partialbalanceassertion :: Stream [Char] m t => ParsecT [Char] JournalContext m (Maybe MixedAmount)+partialbalanceassertion =     try (do           many spacenonewline           char '='@@ -727,8 +770,18 @@           return $ Just $ Mixed [a])          <|> return Nothing +-- balanceassertion :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe MixedAmount)+-- balanceassertion =+--     try (do+--           many spacenonewline+--           string "=="+--           many spacenonewline+--           a <- amountp -- XXX should restrict to a simple amount+--           return $ Just $ Mixed [a])+--          <|> return Nothing+ -- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices-fixedlotprice :: GenParser Char JournalContext (Maybe Amount)+fixedlotprice :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe Amount) fixedlotprice =     try (do           many spacenonewline@@ -742,59 +795,72 @@           return $ Just a)          <|> return Nothing --- | Parse a numeric quantity for its value and display attributes.  Some--- international number formats (cf--- http://en.wikipedia.org/wiki/Decimal_separator) are accepted: either--- period or comma may be used for the decimal point, and the other of--- these may be used for separating digit groups in the integer part (eg a--- thousands separator).  This returns the numeric value, the precision--- (number of digits to the right of the decimal point), the decimal point--- and separator characters (defaulting to . and ,), and the positions of--- separators (counting leftward from the decimal point, the last is--- assumed to repeat).-numberp :: GenParser Char JournalContext (Quantity, Int, Char, Char, [Int])+-- | Parse a string representation of a number for its value and display+-- attributes.+--+-- Some international number formats are accepted, eg either period or comma+-- may be used for the decimal point, and the other of these may be used for+-- separating digit groups in the integer part. See+-- http://en.wikipedia.org/wiki/Decimal_separator for more examples.+--+-- This returns: the parsed numeric value, the precision (number of digits+-- seen following the decimal point), the decimal point character used if any,+-- and the digit group style if any.+--+numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle) numberp = do+  -- a number is an optional sign followed by a sequence of digits possibly+  -- interspersed with periods, commas, or both+  -- ptrace "numberp"   sign <- signp   parts <- many1 $ choice' [many1 digit, many1 $ char ',', many1 $ char '.']-  let numeric = isNumber . headDef '_'-      (numparts, puncparts) = partition numeric parts-      (ok,decimalpoint',separator') =-          case (numparts,puncparts) of-            ([],_)     -> (False, Nothing, Nothing)  -- no digits-            (_,[])     -> (True, Nothing, Nothing)  -- no punctuation chars-            (_,[d:""]) -> (True, Just d, Nothing)   -- just one punctuation char, assume it's a decimal point-            (_,[_])    -> (False, Nothing, Nothing) -- adjacent punctuation chars, not ok-            (_,_:_:_)  -> let (s:ss, d) = (init puncparts, last puncparts) -- two or more punctuation chars-                          in if (any ((/=1).length) puncparts  -- adjacent punctuation chars, not ok-                                 || any (s/=) ss                -- separator chars differ, not ok-                                 || head parts == s)            -- number begins with a separator char, not ok-                              then (False, Nothing, Nothing)-                              else if s == d-                                    then (True, Nothing, Just $ head s) -- just one kind of punctuation, assume separator chars-                                    else (True, Just $ head d, Just $ head s) -- separators and a decimal point+  dbgAt 8 "numberp parsed" (sign,parts) `seq` return ()++  -- check the number is well-formed and identify the decimal point and digit+  -- group separator characters used, if any+  let (numparts, puncparts) = partition numeric parts+      (ok, mdecimalpoint, mseparator) =+          case (numparts, puncparts) of+            ([],_)     -> (False, Nothing, Nothing)  -- no digits, not ok+            (_,[])     -> (True, Nothing, Nothing)   -- digits with no punctuation, ok+            (_,[[d]])  -> (True, Just d, Nothing)    -- just a single punctuation of length 1, assume it's a decimal point+            (_,[_])    -> (False, Nothing, Nothing)  -- a single punctuation of some other length, not ok+            (_,_:_:_)  ->                                       -- two or more punctuations+              let (s:ss, d) = (init puncparts, last puncparts)  -- the leftmost is a separator and the rightmost may be a decimal point+              in if (any ((/=1).length) puncparts               -- adjacent punctuation chars, not ok+                     || any (s/=) ss                            -- separator chars vary, not ok+                     || head parts == s)                        -- number begins with a separator char, not ok+                 then (False, Nothing, Nothing)+                 else if s == d+                      then (True, Nothing, Just $ head s)       -- just one kind of punctuation - must be separators+                      else (True, Just $ head d, Just $ head s) -- separator(s) and a decimal point   when (not ok) (fail $ "number seems ill-formed: "++concat parts)-  let (intparts',fracparts') = span ((/= decimalpoint') . Just . head) parts++  -- get the digit group sizes and digit group style if any+  let (intparts',fracparts') = span ((/= mdecimalpoint) . Just . head) parts       (intparts, fracpart) = (filter numeric intparts', filter numeric fracparts')-      separatorpositions = reverse $ map length $ drop 1 intparts-      int = concat $ "":intparts+      groupsizes = reverse $ case map length intparts of+                               (a:b:cs) | a < b -> b:cs+                               gs               -> gs+      mgrps = maybe Nothing (Just . (`DigitGroups` groupsizes)) $ mseparator++  -- put the parts back together without digit group separators, get the precision and parse the value+  let int = concat $ "":intparts       frac = concat $ "":fracpart       precision = length frac       int' = if null int then "0" else int       frac' = if null frac then "0" else frac       quantity = read $ sign++int'++"."++frac' -- this read should never fail-      (decimalpoint, separator) = case (decimalpoint', separator') of (Just d,  Just s)   -> (d,s)-                                                                      (Just '.',Nothing)  -> ('.',',')-                                                                      (Just ',',Nothing)  -> (',','.')-                                                                      (Nothing, Just '.') -> (',','.')-                                                                      (Nothing, Just ',') -> ('.',',')-                                                                      _                   -> ('.',',')-  return (quantity,precision,decimalpoint,separator,separatorpositions)++  return $ dbgAt 8 "numberp quantity,precision,mdecimalpoint,mgrps" (quantity,precision,mdecimalpoint,mgrps)   <?> "numberp"+  where+    numeric = isNumber . headDef '_'  #ifdef TESTS test_numberp = do       let s `is` n = assertParseEqual' (parseWithCtx nullctx numberp s) n-          assertFails = assertBool . isLeft . parseWithCtx nullctx numberp +          assertFails = assertBool . isLeft . parseWithCtx nullctx numberp       assertFails ""       "0"          `is` (0, 0, '.', ',', [])       "1"          `is` (1, 0, '.', ',', [])@@ -813,29 +879,38 @@       assertFails "1..1"       assertFails ".1,"       assertFails ",1."-#endif       +#endif  -- comment parsers -emptyorcommentlinep :: GenParser Char JournalContext ()+multilinecommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()+multilinecommentp = do+  string "comment" >> newline+  go+  where+    go = try (string "end comment" >> newline >> return ())+         <|> (anyLine >> go)+    anyLine = anyChar `manyTill` newline++emptyorcommentlinep :: Stream [Char] m Char => ParsecT [Char] JournalContext m () emptyorcommentlinep = do   many spacenonewline >> (comment <|> (many spacenonewline >> newline >> return ""))   return () -followingcommentp :: GenParser Char JournalContext String+followingcommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String followingcommentp =   -- ptrace "followingcommentp"   do samelinecomment <- many spacenonewline >> (try semicoloncomment <|> (newline >> return ""))      newlinecomments <- many (try (many1 spacenonewline >> semicoloncomment))      return $ unlines $ samelinecomment:newlinecomments -comment :: GenParser Char JournalContext String+comment :: Stream [Char] m Char => ParsecT [Char] JournalContext m String comment = commentStartingWith "#;" -semicoloncomment :: GenParser Char JournalContext String+semicoloncomment :: Stream [Char] m Char => ParsecT [Char] JournalContext m String semicoloncomment = commentStartingWith ";" -commentStartingWith :: String -> GenParser Char JournalContext String+commentStartingWith :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m String commentStartingWith cs = do   -- ptrace "commentStartingWith"   oneOf cs@@ -848,11 +923,11 @@ tagsInComment c = concatMap tagsInCommentLine $ lines c'   where     c' = ledgerDateSyntaxToTags c-    + tagsInCommentLine :: String -> [Tag] tagsInCommentLine = catMaybes . map maybetag . map strip . splitAtElement ','   where-    maybetag s = case parseWithCtx nullctx tag s of+    maybetag s = case runParser tag nullctx "" s of                   Right t -> Just t                   Left _ -> Nothing @@ -883,7 +958,7 @@     replace' ('=':s) | isdate s = date2tag s     replace' s | last s =='=' && isdate (init s) = datetag (init s)     replace' s | length ds == 2 && isdate d1 && isdate d1 = datetag d1 ++ date2tag d2-      where +      where         ds = splitAtElement '=' s         d1 = headDef "" ds         d2 = lastDef "" ds@@ -892,17 +967,17 @@     isdate = isJust . parsedateM     datetag s = "date:"++s++", "     date2tag s = "date2:"++s++", "-    + #ifdef TESTS test_ledgerDateSyntaxToTags = do      assertEqual "date2:2012/11/28, " $ ledgerDateSyntaxToTags "[=2012/11/28]"-#endif       -  +#endif+ dateValueFromTags, date2ValueFromTags :: [Tag] -> Maybe String dateValueFromTags  ts = maybe Nothing (Just . snd) $ find ((=="date") . fst) ts date2ValueFromTags ts = maybe Nothing (Just . snd) $ find ((=="date2") . fst) ts -    + {- old hunit tests  test_Hledger_Read_JournalReader = TestList $ concat [@@ -932,10 +1007,10 @@      assertParse (parseWithCtx nullctx comment " \t; x\n")      assertParse (parseWithCtx nullctx comment "#x") -  ,"date" ~: do-     assertParse (parseWithCtx nullctx date "2011/1/1")-     assertParseFailure (parseWithCtx nullctx date "1/1")-     assertParse (parseWithCtx nullctx{ctxYear=Just 2011} date "1/1")+  ,"datep" ~: do+     assertParse (parseWithCtx nullctx datep "2011/1/1")+     assertParseFailure (parseWithCtx nullctx datep "1/1")+     assertParse (parseWithCtx nullctx{ctxYear=Just 2011} datep "1/1")    ,"datetimep" ~: do       let p = do {t <- datetimep; eof; return t}
Hledger/Read/TimelogReader.hs view
@@ -49,8 +49,9 @@ where import Control.Monad import Control.Monad.Error+import Data.List (isPrefixOf, foldl') import Test.HUnit-import Text.ParserCombinators.Parsec hiding (parse)+import Text.Parsec hiding (parse) import System.FilePath  import Hledger.Data@@ -68,23 +69,24 @@ format :: String format = "timelog" --- | Does the given file path and data provide timeclock.el's timelog format ?+-- | Does the given file path and data look like it might be timeclock.el's timelog format ? detect :: FilePath -> String -> Bool-detect f _ = takeExtension f == '.':format+detect f s+  | f /= "-"  = takeExtension f == '.':format               -- from a file: yes if the extension is .timelog+  | otherwise = "i " `isPrefixOf` s || "o " `isPrefixOf` s  -- from stdin: yes if it starts with "i " or "o "  -- | Parse and post-process a "Journal" from timeclock.el's timelog -- format, saving the provided file path and the current time, or give an -- error.-parse :: Maybe FilePath -> FilePath -> String -> ErrorT String IO Journal-parse _ = -- trace ("running "++format++" reader") .-          parseJournalWith timelogFile+parse :: Maybe FilePath -> Bool -> FilePath -> String -> ErrorT String IO Journal+parse _ = parseJournalWith timelogFile -timelogFile :: GenParser Char JournalContext (JournalUpdate,JournalContext)+timelogFile :: ParsecT [Char] JournalContext (ErrorT String IO) (JournalUpdate, JournalContext) timelogFile = do items <- many timelogItem                  eof                  ctx <- getState-                 return (liftM (foldr (.) id) $ sequence items, ctx)-    where +                 return (liftM (foldl' (\acc new x -> new (acc x)) id) $ sequence items, ctx)+    where       -- As all ledger line types can be distinguished by the first       -- character, excepting transactions versus empty (blank or       -- comment-only) lines, can use choice w/o try@@ -96,13 +98,14 @@                           ] <?> "timelog entry, or default year or historical price directive"  -- | Parse a timelog entry.-timelogentry :: GenParser Char JournalContext TimeLogEntry+timelogentry :: ParsecT [Char] JournalContext (ErrorT String IO) TimeLogEntry timelogentry = do+  sourcepos <- getPosition   code <- oneOf "bhioO"   many1 spacenonewline   datetime <- datetimep   comment <- optionMaybe (many1 spacenonewline >> liftM2 (++) getParentAccount restofline)-  return $ TimeLogEntry (read [code]) datetime (maybe "" rstrip comment)+  return $ TimeLogEntry sourcepos (read [code]) datetime (maybe "" rstrip comment)  tests_Hledger_Read_TimelogReader = TestList [  ]
Hledger/Reports/BalanceReport.hs view
@@ -39,9 +39,9 @@ -- It has: -- -- * The full account name--- +-- -- * The ledger-style short elided account name (the leaf name, prefixed by any boring parents immediately above)--- +-- -- * The number of indentation steps to use when rendering a ledger-style account tree --   (normally the 0-based depth of this account excluding boring parents, or 0 with --flat). type RenderableAccountName = (AccountName, AccountName, Int)@@ -67,15 +67,19 @@        accts = ledgerRootAccount $ ledgerFromJournal q $ journalSelectingAmountFromOpts opts j       accts' :: [Account]-          | flat_ opts = dbg "accts" $ +          | queryDepth q == 0 =+                         dbg "accts" $+                         take 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts+          | flat_ opts = dbg "accts" $                          filterzeros $                          filterempty $                          drop 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts-          | otherwise  = dbg "accts" $ +          | otherwise  = dbg "accts" $                          filter (not.aboring) $                          drop 1 $ flattenAccounts $-                         markboring $ -                         prunezeros $ clipAccounts (queryDepth q) accts+                         markboring $+                         prunezeros $+                         clipAccounts (queryDepth q) accts           where             balance     = if flat_ opts then aebalance else aibalance             filterzeros = if empty_ opts then id else filter (not . isZeroMixedAmount . balance)@@ -99,14 +103,18 @@            | otherwise = a  balanceReportItem :: ReportOpts -> Query -> Account -> BalanceReportItem-balanceReportItem opts _ a@Account{aname=name}+balanceReportItem opts q a   | flat_ opts = ((name, name,       0),      (if flatShowsExclusiveBalance then aebalance else aibalance) a)   | otherwise  = ((name, elidedname, indent), aibalance a)   where+    name | queryDepth q > 0 = aname a+         | otherwise        = "..."     elidedname = accountNameFromComponents (adjacentboringparentnames ++ [accountLeafName name])     adjacentboringparentnames = reverse $ map (accountLeafName.aname) $ takeWhile aboring $ parents     indent = length $ filter (not.aboring) parents-    parents = init $ parentAccounts a+    -- parents exclude the tree's root node+    parents = case parentAccounts a of [] -> []+                                       as -> init as  -- -- the above using the newer multi balance report code: -- balanceReport' opts q j = (items, total)@@ -116,13 +124,14 @@ --     total = headDef 0 mbrtotals  tests_balanceReport =-  let (opts,journal) `gives` r = do-         let (eitems, etotal) = r-             (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal-         assertEqual "items" eitems aitems-         -- assertEqual "" (length eitems) (length aitems)-         -- mapM (\(e,a) -> assertEqual "" e a) $ zip eitems aitems-         assertEqual "total" etotal atotal+  let+    (opts,journal) `gives` r = do+      let (eitems, etotal) = r+          (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal+          showw (acct,amt) = (acct, showMixedAmountDebug amt)+      assertEqual "items" (map showw eitems) (map showw aitems)+      assertEqual "total" (showMixedAmountDebug etotal) (showMixedAmountDebug atotal)+    usd0 = nullamt{acommodity="$"}   in [     "balanceReport with no args on null journal" ~: do@@ -142,7 +151,7 @@      ,(("income:salary","salary",1), mamountp' "$-1.00")      ,(("liabilities:debts","liabilities:debts",0), mamountp' "$1.00")      ],-     Mixed [nullamt])+     Mixed [usd0])    ,"balanceReport with --depth=N" ~: do    (defreportopts{depth_=Just 1}, samplejournal) `gives`@@ -152,7 +161,7 @@      ,(("income",      "income",      0), mamountp' "$-2.00")      ,(("liabilities", "liabilities", 0), mamountp'  "$1.00")      ],-     Mixed [nullamt])+     Mixed [usd0])    ,"balanceReport with depth:N" ~: do    (defreportopts{query_="depth:1"}, samplejournal) `gives`@@ -162,18 +171,18 @@      ,(("income",      "income",      0), mamountp' "$-2.00")      ,(("liabilities", "liabilities", 0), mamountp'  "$1.00")      ],-     Mixed [nullamt])+     Mixed [usd0])    ,"balanceReport with a date or secondary date span" ~: do    (defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives`     ([],      Mixed [nullamt])-   (defreportopts{query_="edate:'in 2009'"}, samplejournal2) `gives`+   (defreportopts{query_="date2:'in 2009'"}, samplejournal2) `gives`     ([       (("assets:bank:checking","assets:bank:checking",0),mamountp' "$1.00")      ,(("income:salary","income:salary",0),mamountp' "$-1.00")      ],-     Mixed [nullamt])+     Mixed [usd0])    ,"balanceReport with desc:" ~: do    (defreportopts{query_="desc:income"}, samplejournal) `gives`@@ -181,13 +190,13 @@       (("assets:bank:checking","assets:bank:checking",0),mamountp' "$1.00")      ,(("income:salary","income:salary",0), mamountp' "$-1.00")      ],-     Mixed [nullamt])+     Mixed [usd0])    ,"balanceReport with not:desc:" ~: do    (defreportopts{query_="not:desc:income"}, samplejournal) `gives`     ([       (("assets","assets",0), mamountp' "$-2.00")-     ,(("assets:bank","bank",1), Mixed [nullamt])+     ,(("assets:bank","bank",1), Mixed [usd0])      ,(("assets:bank:checking","checking",2),mamountp' "$-1.00")      ,(("assets:bank:saving","saving",2), mamountp' "$1.00")      ,(("assets:cash","cash",1), mamountp' "$-2.00")@@ -197,7 +206,7 @@      ,(("income:gifts","income:gifts",0), mamountp' "$-1.00")      ,(("liabilities:debts","liabilities:debts",0), mamountp' "$1.00")      ],-     Mixed [nullamt])+     Mixed [usd0])   {-@@ -246,7 +255,7 @@      ,"                   0"      ] -    ,"accounts report with unmatched parent of two matched subaccounts" ~: +    ,"accounts report with unmatched parent of two matched subaccounts" ~:      defreportopts{patterns_=["cash","saving"]} `gives`      ["                 $-1  assets"      ,"                  $1    bank:saving"@@ -255,7 +264,7 @@      ,"                 $-1"      ] -    ,"accounts report with multi-part account name" ~: +    ,"accounts report with multi-part account name" ~:      defreportopts{patterns_=["expenses:food"]} `gives`      ["                  $1  expenses:food"      ,"--------------------"@@ -275,13 +284,13 @@      ,"                  $1"      ] -    ,"accounts report negative account pattern always matches full name" ~: +    ,"accounts report negative account pattern always matches full name" ~:      defreportopts{patterns_=["not:e"]} `gives`      ["--------------------"      ,"                   0"      ] -    ,"accounts report negative patterns affect totals" ~: +    ,"accounts report negative patterns affect totals" ~:      defreportopts{patterns_=["expenses","not:food"]} `gives`      ["                  $1  expenses:supplies"      ,"--------------------"@@ -316,10 +325,11 @@ -}  ] -Right samplejournal2 = journalBalanceTransactions $ +Right samplejournal2 = journalBalanceTransactions $          nulljournal          {jtxns = [            txnTieKnot $ Transaction {+             tsourcepos=nullsourcepos,              tdate=parsedate "2008/01/01",              tdate2=Just $ parsedate "2009/01/01",              tstatus=False,@@ -335,12 +345,12 @@            }           ]          }-         + -- tests_isInterestingIndented = [---   "isInterestingIndented" ~: do +--   "isInterestingIndented" ~: do --    let (opts, journal, acctname) `gives` r = isInterestingIndented opts l acctname `is` r --           where l = ledgerFromJournal (queryFromOpts nulldate opts) journal-     + --    (defreportopts, samplejournal, "expenses") `gives` True --  ] 
Hledger/Reports/MultiBalanceReports.hs view
@@ -71,7 +71,7 @@       depthq     = dbg "depthq" $ filterQuery queryIsDepth q       depth      = queryDepth depthq       depthless  = dbg "depthless" . filterQuery (not . queryIsDepth)-      datelessq  = dbg "datelessq"  $ filterQuery (not . queryIsDate) q+      datelessq  = dbg "datelessq"  $ filterQuery (not . queryIsDateOrDate2) q       dateqcons  = if date2_ opts then Date2 else Date       precedingq = dbg "precedingq" $ And [datelessq, dateqcons $ DateSpan Nothing (spanStart reportspan)]       requestedspan  = dbg "requestedspan"  $ queryDateSpan (date2_ opts) q                              -- span specified by -b/-e/-p options and query args@@ -85,7 +85,7 @@       ps :: [Posting] =           dbg "ps" $           journalPostings $-          filterJournalPostingAmounts symq $     -- remove amount parts excluded by cur:+          filterJournalAmounts symq $     -- remove amount parts excluded by cur:           filterJournalPostings reportq $        -- remove postings not matched by (adjusted) query           journalSelectingAmountFromOpts opts j @@ -107,21 +107,30 @@             postingAcctBals :: [Posting] -> [(ClippedAccountName, MixedAmount)]             postingAcctBals ps = [(aname a, (if tree_ opts then aibalance else aebalance) a) | a <- as]                 where-                  as = depthLimit $ +                  as = depthLimit $                        (if tree_ opts then id else filter ((>0).anumpostings)) $                        drop 1 $ accountsFromPostings ps                   depthLimit                       | tree_ opts = filter ((depthq `matchesAccount`).aname) -- exclude deeper balances                       | otherwise  = clipAccountsAndAggregate depth -- aggregate deeper balances at the depth limit -      postedAccts :: [AccountName] =-          dbg "postedAccts" $-          sort $ accountNamesFromPostings ps+      postedAccts :: [AccountName] = dbg "postedAccts" $ sort $ accountNamesFromPostings ps +      -- starting balances and accounts from transactions before the report start date+      startacctbals = dbg "startacctbals" $ map (\((a,_,_),b) -> (a,b)) startbalanceitems+          where+            (startbalanceitems,_) = dbg "starting balance report" $ balanceReport opts' precedingq j+                                    where+                                      opts' | tree_ opts = opts{no_elide_=True}+                                            | otherwise  = opts{accountlistmode_=ALFlat}+      startingBalanceFor a = fromMaybe nullmixedamt $ lookup a startacctbals+      startAccts = dbg "startAccts" $ map fst startacctbals+       displayedAccts :: [ClippedAccountName] =           dbg "displayedAccts" $           (if tree_ opts then expandAccountNames else id) $-          nub $ map (clipAccountName depth) postedAccts+          nub $ map (clipOrEllipsifyAccountName depth) $+          if empty_ opts then nub $ sort $ startAccts ++ postedAccts else postedAccts        acctBalChangesPerSpan :: [[(ClippedAccountName, MixedAmount)]] =           dbg "acctBalChangesPerSpan" $@@ -133,15 +142,6 @@           dbg "acctBalChanges" $           [(a, map snd abs) | abs@((a,_):_) <- transpose acctBalChangesPerSpan] -- never null, or used when null... -      -- starting balances and accounts from transactions before the report start date-      startacctbals = dbg "startacctbals" $ map (\((a,_,_),b) -> (a,b)) $ startbalanceitems-          where-            (startbalanceitems,_) = dbg "starting balance report" $ balanceReport opts' precedingq j-                                    where-                                      opts' | tree_ opts = opts{no_elide_=True}-                                            | otherwise  = opts{flat_=True}-      startingBalanceFor a = fromMaybe nullmixedamt $ lookup a startacctbals-       items :: [MultiBalanceReportRow] =           dbg "items" $           [((a, accountLeafName a, accountNameLevel a), displayedBals)@@ -150,7 +150,7 @@                                   HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor a) changes                                   CumulativeBalance -> drop 1 $ scanl (+) nullmixedamt changes                                   _                 -> changes-           , empty_ opts || any (not . isZeroMixedAmount) displayedBals+           , empty_ opts || depth == 0 || any (not . isZeroMixedAmount) displayedBals            ]        totals :: [MixedAmount] =@@ -162,6 +162,6 @@                 dbg "highestlevelaccts" $                 [a | a <- displayedAccts, not $ any (`elem` displayedAccts) $ init $ expandAccountName a] -      dbg s = let p = "multiBalanceReport" in Hledger.Utils.dbg (p++" "++s)  -- add prefix in debug output-      -- dbg = const id  -- exclude from debug output+      dbg s = let p = "multiBalanceReport" in Hledger.Utils.dbg (p++" "++s)  -- add prefix in this function's debug output+      -- dbg = const id  -- exclude this function from debug output 
Hledger/Reports/PostingsReport.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances #-}+{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections #-} {-|  Postings report, used by the register command.@@ -35,10 +35,17 @@ type PostingsReport = (String               -- label for the running balance column XXX remove                       ,[PostingsReportItem] -- line items, one per posting                       )-type PostingsReportItem = (Maybe Day    -- posting date, if this is the first posting in a transaction or if it's different from the previous posting's date-                          ,Maybe String -- transaction description, if this is the first posting in a transaction-                          ,Posting      -- the posting, possibly with account name depth-clipped-                          ,MixedAmount  -- the running total after this posting (or with --average, the running average)+type PostingsReportItem = (Maybe Day    -- The posting date, if this is the first posting in a+                                        -- transaction or if it's different from the previous+                                        -- posting's date. Or if this a summary posting, the+                                        -- report interval's start date if this is the first+                                        -- summary posting in the interval.+                          ,Maybe Day    -- If this is a summary posting, the report interval's+                                        -- end date if this is the first summary posting in+                                        -- the interval.+                          ,Maybe String -- The posting's transaction's description, if this is the first posting in the transaction.+                          ,Posting      -- The posting, possibly with the account name depth-clipped.+                          ,MixedAmount  -- The running total after this posting (or with --average, the running average).                           )  -- | Select postings from the journal and add running balance and other@@ -50,11 +57,13 @@       symq = dbg "symq"   $ filterQuery queryIsSym $ dbg "requested q" q       depth = queryDepth q       depthless = filterQuery (not . queryIsDepth)-      datelessq = filterQuery (not . queryIsDate) q-      (dateqcons,pdate) | date2_ opts = (Date2, postingDate2)-                        | otherwise   = (Date, postingDate)-      requestedspan  = dbg "requestedspan"  $ queryDateSpan (date2_ opts) q                              -- span specified by -b/-e/-p options and query args-      requestedspan' = dbg "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan (date2_ opts) j  -- if open-ended, close it using the journal's end dates+      datelessq = filterQuery (not . queryIsDateOrDate2) q+      -- XXX date:/date2:/--date2 handling is not robust, combinations of these can confuse it+      dateq = filterQuery queryIsDateOrDate2 q+      (dateqcons,pdate) | queryIsDate2 dateq || (queryIsDate dateq && date2_ opts) = (Date2, postingDate2)+                        | otherwise = (Date, postingDate)+      requestedspan  = dbg "requestedspan"  $ queryDateSpan' q   -- span specified by -b/-e/-p options and query args+      requestedspan' = dbg "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan ({-date2_ opts-} False) j  -- if open-ended, close it using the journal's end dates       intervalspans  = dbg "intervalspans"  $ splitSpan (intervalFromOpts opts) requestedspan' -- interval spans enclosing it       reportstart    = dbg "reportstart"    $ maybe Nothing spanStart $ headMay intervalspans       reportend      = dbg "reportend"      $ maybe Nothing spanEnd   $ lastMay intervalspans@@ -63,7 +72,7 @@       beforeendq     = dbg "beforeendq"     $ dateqcons $ DateSpan Nothing reportend       reportq        = dbg "reportq"        $ depthless $ And [datelessq, beforeendq] -- user's query with no start date, end date on an interval boundary and no depth limit -      pstoend = +      pstoend =           dbg "ps4" $ sortBy (comparing pdate) $                                  -- sort postings by date (or date2)           dbg "ps3" $ map (filterPostingAmount symq) $                            -- remove amount parts which the query's cur: terms would exclude           dbg "ps2" $ (if related_ opts then concatMap relatedPostings else id) $ -- with -r, replace each with its sibling postings@@ -71,14 +80,14 @@                       journalPostings $ journalSelectingAmountFromOpts opts j       (precedingps, reportps) = dbg "precedingps, reportps" $ span (beforestartq `matchesPosting`) pstoend -      empty = queryEmpty q+      showempty = queryEmpty q || average_ opts       -- displayexpr = display_ opts  -- XXX       interval = intervalFromOpts opts -- XXX        whichdate = whichDateFromOpts opts-      itemps | interval == NoInterval = reportps-             | otherwise              = summarisePostingsByInterval interval whichdate depth empty reportspan reportps-      items = postingsReportItems itemps nullposting whichdate depth startbal runningcalc 1+      itemps | interval == NoInterval = map (,Nothing) reportps+             | otherwise              = summarisePostingsByInterval interval whichdate depth showempty reportspan reportps+      items = dbg "items" $ postingsReportItems itemps (nullposting,Nothing) whichdate depth startbal runningcalc 1         where           startbal = if balancetype_ opts == HistoricalBalance then sumPostings precedingps else 0           runningcalc | average_ opts = \i avg amt -> avg + (amt - avg) `divideMixedAmount` (fromIntegral i) -- running average@@ -89,37 +98,43 @@  totallabel = "Total" --- | Generate postings report line items.-postingsReportItems :: [Posting] -> Posting -> WhichDate -> Int -> MixedAmount -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) -> Int -> [PostingsReportItem]+-- | Generate postings report line items from a list of postings or (with+-- non-Nothing dates attached) summary postings.+postingsReportItems :: [(Posting,Maybe Day)] -> (Posting,Maybe Day) -> WhichDate -> Int -> MixedAmount -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) -> Int -> [PostingsReportItem] postingsReportItems [] _ _ _ _ _ _ = []-postingsReportItems (p:ps) pprev wd d b runningcalcfn itemnum = i:(postingsReportItems ps p wd d b' runningcalcfn (itemnum+1))+postingsReportItems ((p,menddate):ps) (pprev,menddateprev) wd d b runningcalcfn itemnum = i:(postingsReportItems ps (p,menddate) wd d b' runningcalcfn (itemnum+1))     where-      i = mkpostingsReportItem showdate showdesc wd p' b'-      showdate = isfirstintxn || isdifferentdate-      showdesc = isfirstintxn+      i = mkpostingsReportItem showdate showdesc wd menddate p' b'+      (showdate, showdesc) | isJust menddate = (menddate /= menddateprev,        False)+                           | otherwise       = (isfirstintxn || isdifferentdate, isfirstintxn)       isfirstintxn = ptransaction p /= ptransaction pprev       isdifferentdate = case wd of PrimaryDate   -> postingDate p  /= postingDate pprev                                    SecondaryDate -> postingDate2 p /= postingDate2 pprev-      p' = p{paccount=clipAccountName d $ paccount p}+      p' = p{paccount= clipOrEllipsifyAccountName d $ paccount p}       b' = runningcalcfn itemnum b (pamount p)  -- | Generate one postings report line item, containing the posting, -- the current running balance, and optionally the posting date and/or -- the transaction description.-mkpostingsReportItem :: Bool -> Bool -> WhichDate -> Posting -> MixedAmount -> PostingsReportItem-mkpostingsReportItem showdate showdesc wd p b = (if showdate then Just date else Nothing, if showdesc then Just desc else Nothing, p, b)-    where-      date = case wd of PrimaryDate   -> postingDate p-                        SecondaryDate -> postingDate2 p-      desc = maybe "" tdescription $ ptransaction p+mkpostingsReportItem :: Bool -> Bool -> WhichDate -> Maybe Day -> Posting -> MixedAmount -> PostingsReportItem+mkpostingsReportItem showdate showdesc wd menddate p b =+  (if showdate then Just date else Nothing+  ,menddate+  ,if showdesc then Just desc else Nothing+  ,p+  ,b+  )+  where+    date = case wd of PrimaryDate   -> postingDate p+                      SecondaryDate -> postingDate2 p+    desc = maybe "" tdescription $ ptransaction p --- | Convert a list of postings into summary postings. Summary postings--- are one per account per interval and aggregated to the specified depth--- if any.-summarisePostingsByInterval :: Interval -> WhichDate -> Int -> Bool -> DateSpan -> [Posting] -> [Posting]-summarisePostingsByInterval interval wd depth empty reportspan ps = concatMap summarisespan $ splitSpan interval reportspan+-- | Convert a list of postings into summary postings, one per interval,+-- aggregated to the specified depth if any.+summarisePostingsByInterval :: Interval -> WhichDate -> Int -> Bool -> DateSpan -> [Posting] -> [SummaryPosting]+summarisePostingsByInterval interval wd depth showempty reportspan ps = concatMap summarisespan $ splitSpan interval reportspan     where-      summarisespan s = summarisePostingsInDateSpan s wd depth empty (postingsinspan s)+      summarisespan s = summarisePostingsInDateSpan s wd depth showempty (postingsinspan s)       postingsinspan s = filter (isPostingInDateSpan' wd s) ps  tests_summarisePostingsByInterval = [@@ -127,37 +142,43 @@     summarisePostingsByInterval (Quarters 1) PrimaryDate 99999 False (DateSpan Nothing Nothing) [] ~?= []  ] +-- | A summary posting summarises the activity in one account within a report+-- interval. It is currently kludgily represented by a regular Posting with no+-- description, the interval's start date stored as the posting date, and the+-- interval's end date attached with a tuple.+type SummaryPosting = (Posting, Maybe Day)+ -- | Given a date span (representing a reporting interval) and a list of--- postings within it: aggregate the postings so there is only one per--- account, and adjust their date/description so that they will render--- as a summary for this interval.------ As usual with date spans the end date is exclusive, but for display--- purposes we show the previous day as end date, like ledger.+-- postings within it, aggregate the postings into one summary posting per+-- account. -- -- When a depth argument is present, postings to accounts of greater--- depth are aggregated where possible.+-- depth are also aggregated where possible. If the depth is 0, all+-- postings in the span are aggregated into a single posting with+-- account name "...". -- -- The showempty flag includes spans with no postings and also postings -- with 0 amount.-summarisePostingsInDateSpan :: DateSpan -> WhichDate -> Int -> Bool -> [Posting] -> [Posting]+--+summarisePostingsInDateSpan :: DateSpan -> WhichDate -> Int -> Bool -> [Posting] -> [SummaryPosting] summarisePostingsInDateSpan (DateSpan b e) wd depth showempty ps     | null ps && (isNothing b || isNothing e) = []-    | null ps && showempty = [summaryp]-    | otherwise = summaryps'+    | null ps && showempty = [(summaryp, Just e')]+    | otherwise = summarypes     where-      summaryp = summaryPosting b' ("- "++ showDate (addDays (-1) e'))+      postingdate = if wd == PrimaryDate then postingDate else postingDate2       b' = fromMaybe (maybe nulldate postingdate $ headMay ps) b       e' = fromMaybe (maybe (addDays 1 nulldate) postingdate $ lastMay ps) e-      postingdate = if wd == PrimaryDate then postingDate else postingDate2-      summaryPosting date desc = nullposting{ptransaction=Just nulltransaction{tdate=date,tdescription=desc}}-      summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps-      summaryps = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]-      clippedanames = nub $ map (clipAccountName depth) anames+      summaryp = nullposting{pdate=Just b'}+      clippedanames | depth > 0 = nub $ map (clipAccountName depth) anames+                    | otherwise = ["..."]+      summaryps | depth > 0 = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]+                | otherwise = [summaryp{paccount="...",pamount=sum $ map pamount ps}]+      summarypes = map (, Just e') $ (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps       anames = sort $ nub $ map paccount ps       -- aggregate balances by account, like ledgerFromJournal, then do depth-clipping       accts = accountsFromPostings ps-      balance a = maybe nullmixedamt bal $ lookupAccount a accts +      balance a = maybe nullmixedamt bal $ lookupAccount a accts         where           bal = if isclipped a then aibalance else aebalance           isclipped a = accountNameLevel a >= depth@@ -247,7 +268,7 @@      ]    ,"postings report with cleared option" ~:-   do +   do     let opts = defreportopts{cleared_=True}     j <- readJournal' sample_journal_str     (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines@@ -259,7 +280,7 @@      ]    ,"postings report with uncleared option" ~:-   do +   do     let opts = defreportopts{uncleared_=True}     j <- readJournal' sample_journal_str     (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines@@ -272,7 +293,7 @@      ]    ,"postings report sorts by date" ~:-   do +   do     j <- readJournal' $ unlines         ["2008/02/02 a"         ,"  b  1"@@ -294,7 +315,7 @@      ]    ,"postings report with account pattern, case insensitive" ~:-   do +   do     j <- samplejournal     let opts = defreportopts{patterns_=["cAsH"]}     (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines@@ -302,9 +323,9 @@      ]    ,"postings report with display expression" ~:-   do +   do     j <- samplejournal-    let gives displayexpr = +    let gives displayexpr =             (registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is`)                 where opts = defreportopts{display_=Just displayexpr}     "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"]@@ -314,7 +335,7 @@     "d>[2008/6/2]"  `gives` ["2008/06/03","2008/12/31"]    ,"postings report with period expression" ~:-   do +   do     j <- samplejournal     let periodexpr `gives` dates = do           j' <- samplejournal@@ -344,7 +365,7 @@   ]    , "postings report with depth arg" ~:-   do +   do     j <- samplejournal     let opts = defreportopts{depth_=Just 2}     (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
Hledger/Reports/ReportOptions.hs view
@@ -8,9 +8,12 @@ module Hledger.Reports.ReportOptions (   ReportOpts(..),   BalanceType(..),+  AccountListMode(..),   FormatStr,   defreportopts,   rawOptsToReportOpts,+  flat_,+  tree_,   dateSpanFromOpts,   intervalFromOpts,   clearedValueFromOpts,@@ -47,6 +50,11 @@  instance Default BalanceType where def = PeriodBalance +-- | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ?+data AccountListMode = ALDefault | ALTree | ALFlat deriving (Eq, Show, Data, Typeable)++instance Default AccountListMode where def = ALDefault+ -- | Standard options for customising report filtering and output, -- corresponding to hledger's command-line options and query language -- arguments. Used in hledger-lib and above.@@ -75,8 +83,7 @@     ,related_        :: Bool     -- balance     ,balancetype_    :: BalanceType-    ,flat_           :: Bool -- mutually-    ,tree_           :: Bool -- exclusive+    ,accountlistmode_  :: AccountListMode     ,drop_           :: Int     ,no_total_       :: Bool  } deriving (Show, Data, Typeable)@@ -110,7 +117,6 @@     def     def     def-    def  rawOptsToReportOpts :: RawOpts -> IO ReportOpts rawOptsToReportOpts rawopts = do@@ -138,12 +144,18 @@     ,average_     = boolopt "average" rawopts     ,related_     = boolopt "related" rawopts     ,balancetype_ = balancetypeopt rawopts-    ,flat_        = boolopt "flat" rawopts-    ,tree_        = boolopt "tree" rawopts+    ,accountlistmode_ = accountlistmodeopt rawopts     ,drop_        = intopt "drop" rawopts     ,no_total_    = boolopt "no-total" rawopts     } +accountlistmodeopt :: RawOpts -> AccountListMode+accountlistmodeopt rawopts =+  case reverse $ filter (`elem` ["tree","flat"]) $ map fst rawopts of+    ("tree":_) -> ALTree+    ("flat":_) -> ALFlat+    _          -> ALDefault+ balancetypeopt :: RawOpts -> BalanceType balancetypeopt rawopts     | length [o | o <- ["cumulative","historical"], isset o] > 1@@ -181,6 +193,13 @@                 Just                 $ parsePeriodExpr d s +-- | Legacy-compatible convenience aliases for accountlistmode_.+tree_ :: ReportOpts -> Bool+tree_ = (==ALTree) . accountlistmode_++flat_ :: ReportOpts -> Bool+flat_ = (==ALFlat) . accountlistmode_+ -- | Figure out the date span we should report on, based on any -- begin/end/period options provided. A period option will cause begin and -- end options to be ignored.@@ -266,7 +285,7 @@                                                       ,query_="date:'to 2013'"                                                       })   assertEqual "" (Date2 $ mkdatespan "2012/01/01" "2013/01/01")-                 (queryFromOpts nulldate defreportopts{query_="edate:'in 2012'"})+                 (queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"})   assertEqual "" (Or [Acct "a a", Acct "'b"])                  (queryFromOpts nulldate defreportopts{query_="'a a' 'b"})  ]
Hledger/Reports/TransactionsReports.hs view
@@ -1,16 +1,22 @@ {-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances #-} {-| -Whole-journal, account-centric, and per-commodity transactions reports, used by hledger-web.+Here are several variants of a transactions report.+Transactions reports are like a postings report, but more+transaction-oriented, and (in the account-centric variant) relative to+a some base account.  They are used by hledger-web.  -}  module Hledger.Reports.TransactionsReports (   TransactionsReport,   TransactionsReportItem,+  triOrigTransaction,   triDate,+  triAmount,   triBalance,-  triSimpleBalance,+  triCommodityAmount,+  triCommodityBalance,   journalTransactionsReport,   accountTransactionsReport,   transactionsReportByCommodity@@ -21,7 +27,6 @@ where  import Data.List-import Data.Maybe import Data.Ord -- import Test.HUnit @@ -40,59 +45,86 @@ type TransactionsReport = (String                   -- label for the balance column, eg "balance" or "total"                           ,[TransactionsReportItem] -- line items, one per transaction                           )-type TransactionsReportItem = (Transaction -- the corresponding transaction-                              ,Transaction -- the transaction with postings to the current account(s) removed+type TransactionsReportItem = (Transaction -- the original journal transaction, unmodified+                              ,Transaction -- the transaction as seen from a particular account                               ,Bool        -- is this a split, ie more than one other account posting                               ,String      -- a display string describing the other account(s), if any                               ,MixedAmount -- the amount posted to the current account(s) (or total amount posted)                               ,MixedAmount -- the running balance for the current account(s) after this transaction                               ) -triDate (t,_,_,_,_,_) = tdate t+triOrigTransaction (torig,_,_,_,_,_) = torig+triDate (_,tacct,_,_,_,_) = tdate tacct triAmount (_,_,_,_,a,_) = a triBalance (_,_,_,_,_,a) = a-triSimpleBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0"-                                                 (Amount{aquantity=q}):_ -> show q+triCommodityAmount c = filterMixedAmountByCommodity c  . triAmount+triCommodityBalance c = filterMixedAmountByCommodity c  . triBalance  -------------------------------------------------------------------------------  -- | Select transactions from the whole journal. This is similar to a -- "postingsReport" except with transaction-based report items which--- are ordered most recent first. This is used by eg hledger-web's journal view.+-- are ordered most recent first. XXX Or an EntriesReport - use that instead ?+-- This is used by hledger-web's journal view. journalTransactionsReport :: ReportOpts -> Journal -> Query -> TransactionsReport-journalTransactionsReport _ Journal{jtxns=ts} m = (totallabel, items)+journalTransactionsReport opts j q = (totallabel, items)    where-     ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts-     items = reverse $ accountTransactionsReportItems m Nothing nullmixedamt id ts'      -- XXX items' first element should be the full transaction with all postings+     items = reverse $ accountTransactionsReportItems q None nullmixedamt id ts+     ts    = sortBy (comparing date) $ filter (q `matchesTransaction`) $ jtxns $ journalSelectingAmountFromOpts opts j+     date  = transactionDateFn opts  ------------------------------------------------------------------------------- --- | Select transactions within one or more current accounts, and make a--- transactions report relative to those account(s). This means:+-- | An account transactions report represents transactions affecting+-- a particular account (or possibly several accounts, but we don't+-- use that). It is used by hledger-web's account register view, where+-- we want to show one row per journal transaction, with: ----- 1. it shows transactions from the point of view of the current account(s).---    The transaction amount is the amount posted to the current account(s).---    The other accounts' names are provided. +-- - the total increase/decrease to the current account ----- 2. With no transaction filtering in effect other than a start date, it---    shows the accurate historical running balance for the current account(s).---    Otherwise it shows a running total starting at 0.+-- - the names of the other account(s) posted to/from ----- This is used by eg hledger-web's account register view. Currently,--- reporting intervals are not supported, and report items are most--- recent first.-accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> TransactionsReport-accountTransactionsReport opts j m thisacctquery = (label, items)+-- - transaction dates adjusted to the date of the earliest posting to+--   the current account, if those postings have their own dates+--+-- Currently, reporting intervals are not supported, and report items+-- are most recent first.+--+type AccountTransactionsReport =+  (String                          -- label for the balance column, eg "balance" or "total"+  ,[AccountTransactionsReportItem] -- line items, one per transaction+  )++type AccountTransactionsReportItem =+  (+   Transaction -- the original journal transaction+  ,Transaction -- the adjusted account transaction+  ,Bool        -- is this a split, ie with more than one posting to other account(s)+  ,String      -- a display string describing the other account(s), if any+  ,MixedAmount -- the amount posted to the current account(s) (or total amount posted)+  ,MixedAmount -- the running balance for the current account(s) after this transaction+  )++accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport+accountTransactionsReport opts j q thisacctquery = (label, items)  where-     -- transactions affecting this account, in date order-     ts = sortBy (comparing tdate) $ filter (matchesTransaction thisacctquery) $ jtxns $-          journalSelectingAmountFromOpts opts j+     -- transactions with excluded currencies removed+     ts1 = jtxns $+           filterJournalAmounts (filterQuery queryIsSym q) $+           journalSelectingAmountFromOpts opts j+     -- affecting this account+     ts2 = filter (matchesTransaction thisacctquery) ts1+     -- with dates adjusted for account transactions report+     ts3 = map (setTransactionDateToPostingDate q thisacctquery) ts2+     -- and sorted+     ts = sortBy (comparing tdate) ts3+      -- starting balance: if we are filtering by a start date and nothing else,      -- the sum of postings to this account before that date; otherwise zero.-     (startbal,label) | queryIsNull m                           = (nullmixedamt,        balancelabel)-                      | queryIsStartDateOnly (date2_ opts) m = (sumPostings priorps, balancelabel)-                      | otherwise                                 = (nullmixedamt,        totallabel)+     (startbal,label) | queryIsNull q                        = (nullmixedamt,        balancelabel)+                      | queryIsStartDateOnly (date2_ opts) q = (sumPostings priorps, balancelabel)+                      | otherwise                            = (nullmixedamt,        totallabel)                       where                         priorps = -- ltrace "priorps" $                                   filter (matchesPosting@@ -100,50 +132,70 @@                                            And [thisacctquery, tostartdatequery]))                                          $ transactionsPostings ts                         tostartdatequery = Date (DateSpan Nothing startdate)-                        startdate = queryStartDate (date2_ opts) m-     items = reverse $ accountTransactionsReportItems m (Just thisacctquery) startbal negate ts+                        startdate = queryStartDate (date2_ opts) q -totallabel = "Total"-balancelabel = "Balance"+     items = reverse $ -- see also registerChartHtml+             accountTransactionsReportItems q thisacctquery startbal negate ts +-- | Adjust a transaction's date to the earliest date of postings to a+-- particular account, if any, after filtering with a certain query.+setTransactionDateToPostingDate :: Query -> Query -> Transaction -> Transaction+setTransactionDateToPostingDate query thisacctquery t = t'+  where+    queryps = tpostings $ filterTransactionPostings query t+    thisacctps = filter (matchesPosting thisacctquery) queryps+    t' = case thisacctps of+          [] -> t+          _  -> t{tdate=d}+            where+              d | null ds   = tdate t+                | otherwise = minimum ds+              ds = map postingDate thisacctps+                   -- no opts here, don't even bother with that date/date2 rigmarole++totallabel = "Running Total"+balancelabel = "Historical Balance"+ -- | Generate transactions report items from a list of transactions,--- using the provided query and current account queries, starting balance,--- sign-setting function and balance-summing function.-accountTransactionsReportItems :: Query -> Maybe Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [TransactionsReportItem]+-- using the provided query and current account queries, starting+-- balance, sign-setting function and balance-summing function. With a+-- "this account" query of None, this can be used the for the+-- journalTransactionsReport also.+accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [TransactionsReportItem] accountTransactionsReportItems _ _ _ _ [] = []-accountTransactionsReportItems query thisacctquery bal signfn (t:ts) =+accountTransactionsReportItems query thisacctquery bal signfn (torig:ts) =     -- This is used for both accountTransactionsReport and journalTransactionsReport,     -- which makes it a bit overcomplicated     case i of Just i' -> i':is               Nothing -> is     where-      tmatched@Transaction{tpostings=psmatched} = filterTransactionPostings query t-      (psthisacct,psotheracct) = case thisacctquery of Just m  -> partition (matchesPosting m) psmatched-                                                       Nothing -> ([],psmatched)-      numotheraccts = length $ nub $ map paccount psotheracct-      amt = negate $ sum $ map pamount psthisacct-      acct | isNothing thisacctquery = summarisePostings psmatched -- journal register-           | numotheraccts == 0 = "transfer between " ++ summarisePostingAccounts psthisacct-           | otherwise          = prefix              ++ summarisePostingAccounts psotheracct-           where prefix = maybe "" (\b -> if b then "from " else "to ") $ isNegativeMixedAmount amt-      (i,bal') = case psmatched of+      -- XXX I've lost my grip on this, let's just hope for the best+      origps = tpostings torig+      tacct@Transaction{tpostings=queryps} = filterTransactionPostings query torig+      (thisacctps, otheracctps) = partition (matchesPosting thisacctquery) origps+      amt = negate $ sum $ map pamount thisacctps+      numotheraccts = length $ nub $ map paccount otheracctps+      otheracctstr | thisacctquery == None = summarisePostingAccounts origps+                   | numotheraccts == 0    = summarisePostingAccounts thisacctps+                   | otherwise             = summarisePostingAccounts otheracctps+      (i,bal') = case queryps of            [] -> (Nothing,bal)-           _  -> (Just (t, tmatched, numotheraccts > 1, acct, a, b), b)+           _  -> (Just (torig, tacct, numotheraccts > 1, otheracctstr, a, b), b)                  where                   a = signfn amt                   b = bal + a       is = accountTransactionsReportItems query thisacctquery bal' signfn ts --- | Generate a short readable summary of some postings, like--- "from (negatives) to (positives)".-summarisePostings :: [Posting] -> String-summarisePostings ps =-    case (summarisePostingAccounts froms, summarisePostingAccounts tos) of-       ("",t) -> "to "++t-       (f,"") -> "from "++f-       (f,t)  -> "from "++f++" to "++t-    where-      (froms,tos) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps+-- -- | Generate a short readable summary of some postings, like+-- -- "from (negatives) to (positives)".+-- summarisePostings :: [Posting] -> String+-- summarisePostings ps =+--     case (summarisePostingAccounts froms, summarisePostingAccounts tos) of+--        ("",t) -> "to "++t+--        (f,"") -> "from "++f+--        (f,t)  -> "from "++f++" to "++t+--     where+--       (froms,tos) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps  -- | Generate a simplified summary of some postings' accounts. summarisePostingAccounts :: [Posting] -> String@@ -156,9 +208,9 @@  -- | Split a transactions report whose items may involve several commodities, -- into one or more single-commodity transactions reports.-transactionsReportByCommodity :: TransactionsReport -> [TransactionsReport]+transactionsReportByCommodity :: TransactionsReport -> [(Commodity, TransactionsReport)] transactionsReportByCommodity tr =-  [filterTransactionsReportByCommodity c tr | c <- transactionsReportCommodities tr]+  [(c, filterTransactionsReportByCommodity c tr) | c <- transactionsReportCommodities tr]   where     transactionsReportCommodities (_,items) =       nub $ sort $ map acommodity $ concatMap (amounts . triAmount) items@@ -187,10 +239,6 @@         go _ [] = []         go bal ((t,t2,s,o,amt,_):is) = (t,t2,s,o,amt,bal'):go bal' is           where bal' = bal + amt---- | Filter out all but the specified commodity from this amount.-filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount-filterMixedAmountByCommodity c (Mixed as) = Mixed $ filter ((==c). acommodity) as  ------------------------------------------------------------------------------- 
Hledger/Utils.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-} {-|  Standard imports and utilities which are useful everywhere, or needed low@@ -14,69 +14,62 @@                           -- module Data.Time.Clock,                           -- module Data.Time.LocalTime,                           -- module Data.Tree,-                          -- module Debug.Trace,                           -- module Text.RegexPR,                           -- module Test.HUnit,                           -- module Text.Printf,                           ---- all of this one:                           module Hledger.Utils,-                          Debug.Trace.trace,+                          module Hledger.Utils.Debug,+                          module Hledger.Utils.Regex,+                          -- Debug.Trace.trace,                           -- module Data.PPrint,                           -- module Hledger.Utils.UTF8IOCompat                           SystemString,fromSystemString,toSystemString,error',userError',-#if __GLASGOW_HASKELL__ >= 704-                          ppShow-#endif                           -- the rest need to be done in each module I think                           ) where-import Control.Monad (liftM, when)+import Control.Monad (liftM) import Control.Monad.Error (MonadIO) import Control.Monad.IO.Class (liftIO) import Data.Char import Data.List import qualified Data.Map as M-import Data.Maybe+-- import Data.Maybe -- import Data.PPrint import Data.Time.Clock import Data.Time.LocalTime import Data.Tree-import Debug.Trace-import Safe (readDef) import System.Directory (getHomeDirectory)-import System.Environment (getArgs)-import System.Exit import System.FilePath((</>), isRelative) import System.IO-import System.IO.Unsafe (unsafePerformIO) import Test.HUnit-import Text.ParserCombinators.Parsec+import Text.Parsec import Text.Printf-import Text.Regex.TDFA-import Text.RegexPR -- import qualified Data.Map as Map--- ++import Hledger.Utils.Debug+import Hledger.Utils.Regex -- import Prelude hiding (readFile,writeFile,appendFile,getContents,putStr,putStrLn) -- import Hledger.Utils.UTF8IOCompat   (readFile,writeFile,appendFile,getContents,putStr,putStrLn) import Hledger.Utils.UTF8IOCompat (SystemString,fromSystemString,toSystemString,error',userError') -#if __GLASGOW_HASKELL__ >= 704-import Text.Show.Pretty (ppShow)-#else--- the required pretty-show version requires GHC >= 7.4-ppShow :: Show a => a -> String-ppShow = show-#endif- -- strings  lowercase = map toLower uppercase = map toUpper +-- | Remove leading and trailing whitespace. strip = lstrip . rstrip-lstrip = dropWhile (`elem` " \t") :: String -> String++-- | Remove leading whitespace.+lstrip = dropWhile (`elem` " \t") :: String -> String -- XXX isSpace ?++-- | Remove trailing whitespace. rstrip = reverse . lstrip . reverse +-- | Remove trailing newlines/carriage returns.+chomp = reverse . dropWhile (`elem` "\r\n") . reverse+ stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse :: String -> String  elideLeft width s =@@ -248,54 +241,17 @@ difforzero :: (Num a, Ord a) => a -> a -> a difforzero a b = maximum [(a - b), 0] --- regexps--- Note many of these will die on malformed regexps.---- regexMatch :: String -> String -> MatchFun Maybe-regexMatch r s = matchRegexPR r s---- regexMatchCI :: String -> String -> MatchFun Maybe-regexMatchCI r s = regexMatch (regexToCaseInsensitive r) s--regexMatches :: String -> String -> Bool-regexMatches r s = isJust $ matchRegexPR r s--regexMatchesCI :: String -> String -> Bool-regexMatchesCI r s = regexMatches (regexToCaseInsensitive r) s--containsRegex = regexMatchesCI--regexReplace :: String -> String -> String -> String-regexReplace r repl s = gsubRegexPR r repl s--regexReplaceCI :: String -> String -> String -> String-regexReplaceCI r s = regexReplace (regexToCaseInsensitive r) s--regexReplaceBy :: String -> (String -> String) -> String -> String-regexReplaceBy r replfn s = gsubRegexPRBy r replfn s--regexToCaseInsensitive :: String -> String-regexToCaseInsensitive r = "(?i)"++ r--regexSplit :: String -> String -> [String]-regexSplit = splitRegexPR---- regex-compat (regex-posix) functions that perform better than regexpr.-regexMatchesRegexCompat :: String -> String -> Bool-regexMatchesRegexCompat = flip (=~)--regexMatchesCIRegexCompat :: String -> String -> Bool-regexMatchesCIRegexCompat r = match (makeRegexOpts defaultCompOpt { multiline = True, caseSensitive = False, newSyntax = True } defaultExecOpt r)- -- lists  splitAtElement :: Eq a => a -> [a] -> [[a]]-splitAtElement e l = -    case dropWhile (e==) l of-      [] -> []-      l' -> first : splitAtElement e rest-        where-          (first,rest) = break (e==) l'+splitAtElement x l =+  case l of+    []          -> []+    e:es | e==x -> split es+    es          -> split es+  where+    split es = let (first,rest) = break (x==) es+               in first : splitAtElement x rest  -- trees @@ -324,7 +280,7 @@ subtreeinforest v (t:ts) = case (subtreeat v t) of                              Just t' -> Just t'                              Nothing -> subtreeinforest v ts-          + -- | remove all nodes past a certain depth treeprune :: Int -> Tree a -> Tree a treeprune 0 t = Node (root t) []@@ -336,15 +292,15 @@  -- | remove all subtrees whose nodes do not fulfill predicate treefilter :: (a -> Bool) -> Tree a -> Tree a-treefilter f t = Node -                 (root t) +treefilter f t = Node+                 (root t)                  (map (treefilter f) $ filter (treeany f) $ branches t)-    + -- | is predicate true in any node of tree ? treeany :: (a -> Bool) -> Tree a -> Bool treeany f t = f (root t) || any (treeany f) (branches t)-    --- treedrop -- remove the leaves which do fulfill predicate. ++-- treedrop -- remove the leaves which do fulfill predicate. -- treedropall -- do this repeatedly.  -- | show a compact ascii representation of a tree@@ -374,146 +330,18 @@ treeFromPaths = foldl' mergeTrees emptyTree . map treeFromPath  --- debugging---- more:--- http://hackage.haskell.org/packages/archive/TraceUtils/0.1.0.2/doc/html/Debug-TraceUtils.html--- http://hackage.haskell.org/packages/archive/trace-call/0.1/doc/html/Debug-TraceCall.html--- http://hackage.haskell.org/packages/archive/htrace/0.1/doc/html/Debug-HTrace.html--- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html---- | Trace (print on stdout at runtime) a showable value.--- (for easily tracing in the middle of a complex expression)-strace :: Show a => a -> a-strace a = trace (show a) a---- | Labelled trace - like strace, with a label prepended.-ltrace :: Show a => String -> a -> a-ltrace l a = trace (l ++ ": " ++ show a) a---- | Monadic trace - like strace, but works as a standalone line in a monad.-mtrace :: (Monad m, Show a) => a -> m a-mtrace a = strace a `seq` return a---- | Custom trace - like strace, with a custom show function.-traceWith :: (a -> String) -> a -> a-traceWith f e = trace (f e) e---- | Parsec trace - show the current parsec position and next input,--- and the provided label if it's non-null.-ptrace :: String -> GenParser Char st ()-ptrace msg = do-  pos <- getPosition-  next <- take peeklength `fmap` getInput-  let (l,c) = (sourceLine pos, sourceColumn pos)-      s  = printf "at line %2d col %2d: %s" l c (show next) :: String-      s' = printf ("%-"++show (peeklength+30)++"s") s ++ " " ++ msg-  trace s' $ return ()-  where-    peeklength = 30---- | Global debug level, which controls the verbosity of debug output--- on the console. The default is 0 meaning no debug output. The--- @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to--- a higher value (note: not @--debug N@ for some reason).  This uses--- unsafePerformIO and can be accessed from anywhere and before normal--- command-line processing. After command-line processing, it is also--- available as the @debug_@ field of 'Hledger.Cli.Options.CliOpts'.-debugLevel :: Int-debugLevel = case snd $ break (=="--debug") args of-               "--debug":[]  -> 1-               "--debug":n:_ -> readDef 1 n-               _             ->-                 case take 1 $ filter ("--debug" `isPrefixOf`) args of-                   ['-':'-':'d':'e':'b':'u':'g':'=':v] -> readDef 1 v-                   _                                   -> 0--    where-      args = unsafePerformIO getArgs---- | Print a message and a showable value to the console if the global--- debug level is non-zero.  Uses unsafePerformIO.-dbg :: Show a => String -> a -> a-dbg = dbg1--dbg0 :: Show a => String -> a -> a-dbg0 = dbgAt 0--dbg1 :: Show a => String -> a -> a-dbg1 = dbgAt 1--dbg2 :: Show a => String -> a -> a-dbg2 = dbgAt 2---- | Print a message and a showable value to the console if the global--- debug level is at or above the specified level.  Uses unsafePerformIO.-dbgAt :: Show a => Int -> String -> a -> a-dbgAt lvl = dbgppshow lvl--dbgAtM :: Show a => Int -> String -> a -> IO ()-dbgAtM lvl lbl x = dbgAt lvl lbl x `seq` return ()---- | Print a showable value to the console, with a message, if the--- debug level is at or above the specified level (uses--- unsafePerformIO).--- Values are displayed with show, all on one line, which is hard to read.-dbgshow :: Show a => Int -> String -> a -> a-dbgshow level-    | debugLevel >= level = ltrace-    | otherwise           = flip const---- | Print a showable value to the console, with a message, if the--- debug level is at or above the specified level (uses--- unsafePerformIO).--- Values are displayed with ppShow, each field/constructor on its own line.-dbgppshow :: Show a => Int -> String -> a -> a-dbgppshow level-    | debugLevel < level = flip const-    | otherwise = \s a -> let p = ppShow a-                              ls = lines p-                              nlorspace | length ls > 1 = "\n"-                                        | otherwise     = " " ++ take (10 - length s) (repeat ' ')-                              ls' | length ls > 1 = map (" "++) ls-                                  | otherwise     = ls-                          in trace (s++":"++nlorspace++intercalate "\n" ls') a---- -- | Print a showable value to the console, with a message, if the--- -- debug level is at or above the specified level (uses--- -- unsafePerformIO).--- -- Values are displayed with pprint. Field names are not shown, but the--- -- output is compact with smart line wrapping, long data elided,--- -- and slow calculations timed out.--- dbgpprint :: Data a => Int -> String -> a -> a--- dbgpprint level msg a---     | debugLevel >= level = unsafePerformIO $ do---                               pprint a >>= putStrLn . ((msg++": \n") ++) . show---                               return a---     | otherwise           = a----- | Like dbg, then exit the program. Uses unsafePerformIO.-dbgExit :: Show a => String -> a -> a-dbgExit msg = const (unsafePerformIO exitFailure) . dbg msg---- | Print a message and parsec debug info (parse position and next--- input) to the console when the debug level is at or above--- this level. Uses unsafePerformIO.--- pdbgAt :: GenParser m => Float -> String -> m ()-pdbg level msg = when (level <= debugLevel) $ ptrace msg-- -- parsing  -- | Backtracking choice, use this when alternatives share a prefix. -- Consumes no input if all choices fail.-choice' :: [GenParser tok st a] -> GenParser tok st a-choice' = choice . map Text.ParserCombinators.Parsec.try+choice' :: Stream s m t => [ParsecT s u m a] -> ParsecT s u m a+choice' = choice . map Text.Parsec.try -parsewith :: Parser a -> String -> Either ParseError a-parsewith p = parse p ""+parsewith :: Parsec [Char] () a -> String -> Either ParseError a+parsewith p = runParser p () "" -parseWithCtx :: b -> GenParser Char b a -> String -> Either ParseError a-parseWithCtx ctx p = runParser p ctx ""+parseWithCtx :: Stream s m t => u -> ParsecT s u m a -> s -> m (Either ParseError a)+parseWithCtx ctx p = runParserT p ctx ""  fromparse :: Either ParseError a -> a fromparse = either parseerror id@@ -527,16 +355,16 @@ showDateParseError :: ParseError -> String showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e) -nonspace :: GenParser Char st Char+nonspace :: (Stream [Char] m Char) => ParsecT [Char] st m Char nonspace = satisfy (not . isSpace) -spacenonewline :: GenParser Char st Char+spacenonewline :: (Stream [Char] m Char) => ParsecT [Char] st m Char spacenonewline = satisfy (`elem` " \v\f\t") -restofline :: GenParser Char st String+restofline :: (Stream [Char] m Char) => ParsecT [Char] st m String restofline = anyChar `manyTill` newline -eolof :: GenParser Char st ()+eolof :: (Stream [Char] m Char) => ParsecT [Char] st m () eolof = (newline >> return ()) <|> eof  -- time@@ -599,7 +427,7 @@ applyN n f = (!! n) . iterate f  -- | Convert a possibly relative, possibly tilde-containing file path to an absolute one,--- given the current directory. ~username is not supported. Leave "-" unchanged. +-- given the current directory. ~username is not supported. Leave "-" unchanged. expandPath :: MonadIO m => FilePath -> FilePath -> m FilePath -- general type sig for use in reader parsers expandPath _ "-" = return "-" expandPath curdir p = (if isRelative p then (curdir </>) else id) `liftM` expandPath' p
+ Hledger/Utils/Debug.hs view
@@ -0,0 +1,205 @@+{-# LANGUAGE CPP, FlexibleContexts #-}+-- | Debugging helpers++-- more:+-- http://hackage.haskell.org/packages/archive/TraceUtils/0.1.0.2/doc/html/Debug-TraceUtils.html+-- http://hackage.haskell.org/packages/archive/trace-call/0.1/doc/html/Debug-TraceCall.html+-- http://hackage.haskell.org/packages/archive/htrace/0.1/doc/html/Debug-HTrace.html+-- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html++module Hledger.Utils.Debug (+  module Hledger.Utils.Debug+  ,module Debug.Trace+#if __GLASGOW_HASKELL__ >= 704+  ,ppShow+#endif+)+where++import Control.Monad (when)+import Data.List+import Debug.Trace+import Safe (readDef)+import System.Environment (getArgs)+import System.Exit+import System.IO.Unsafe (unsafePerformIO)+import Text.Parsec+import Text.Printf++#if __GLASGOW_HASKELL__ >= 704+import Text.Show.Pretty (ppShow)+#else+-- the required pretty-show version requires GHC >= 7.4+ppShow :: Show a => a -> String+ppShow = show+#endif+++-- | Trace (print on stdout at runtime) a showable value.+-- (for easily tracing in the middle of a complex expression)+strace :: Show a => a -> a+strace a = trace (show a) a++-- | Labelled trace - like strace, with a label prepended.+ltrace :: Show a => String -> a -> a+ltrace l a = trace (l ++ ": " ++ show a) a++-- | Monadic trace - like strace, but works as a standalone line in a monad.+mtrace :: (Monad m, Show a) => a -> m a+mtrace a = strace a `seq` return a++-- | Custom trace - like strace, with a custom show function.+traceWith :: (a -> String) -> a -> a+traceWith f e = trace (f e) e++-- | Parsec trace - show the current parsec position and next input,+-- and the provided label if it's non-null.+ptrace :: Stream [Char] m t => String -> ParsecT [Char] st m ()+ptrace msg = do+  pos <- getPosition+  next <- take peeklength `fmap` getInput+  let (l,c) = (sourceLine pos, sourceColumn pos)+      s  = printf "at line %2d col %2d: %s" l c (show next) :: String+      s' = printf ("%-"++show (peeklength+30)++"s") s ++ " " ++ msg+  trace s' $ return ()+  where+    peeklength = 30++-- | Global debug level, which controls the verbosity of debug output+-- on the console. The default is 0 meaning no debug output. The+-- @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to+-- a higher value (note: not @--debug N@ for some reason).  This uses+-- unsafePerformIO and can be accessed from anywhere and before normal+-- command-line processing. After command-line processing, it is also+-- available as the @debug_@ field of 'Hledger.Cli.Options.CliOpts'.+-- {-# OPTIONS_GHC -fno-cse #-} +-- {-# NOINLINE debugLevel #-}+debugLevel :: Int+debugLevel = case snd $ break (=="--debug") args of+               "--debug":[]  -> 1+               "--debug":n:_ -> readDef 1 n+               _             ->+                 case take 1 $ filter ("--debug" `isPrefixOf`) args of+                   ['-':'-':'d':'e':'b':'u':'g':'=':v] -> readDef 1 v+                   _                                   -> 0++    where+      args = unsafePerformIO getArgs++-- | Print a message and a showable value to the console if the global+-- debug level is non-zero.  Uses unsafePerformIO.+dbg :: Show a => String -> a -> a+dbg = dbg1++-- always prints+dbg0 :: Show a => String -> a -> a+dbg0 = dbgAt 0++dbg1 :: Show a => String -> a -> a+dbg1 = dbgAt 1++dbg2 :: Show a => String -> a -> a+dbg2 = dbgAt 2++dbg3 :: Show a => String -> a -> a+dbg3 = dbgAt 3++dbg4 :: Show a => String -> a -> a+dbg4 = dbgAt 4++dbg5 :: Show a => String -> a -> a+dbg5 = dbgAt 5++dbg6 :: Show a => String -> a -> a+dbg6 = dbgAt 6++dbg7 :: Show a => String -> a -> a+dbg7 = dbgAt 7++dbg8 :: Show a => String -> a -> a+dbg8 = dbgAt 8++dbg9 :: Show a => String -> a -> a+dbg9 = dbgAt 9++-- | Print a message and a showable value to the console if the global+-- debug level is at or above the specified level.  Uses unsafePerformIO.+dbgAt :: Show a => Int -> String -> a -> a+dbgAt lvl = dbgppshow lvl++    -- Could not deduce (a ~ ())+    -- from the context (Show a)+    --   bound by the type signature for+    --              dbgM :: Show a => String -> a -> IO ()+    --   at hledger/Hledger/Cli/Main.hs:200:13-42+    --   ‘a’ is a rigid type variable bound by+    --       the type signature for dbgM :: Show a => String -> a -> IO ()+    --       at hledger/Hledger/Cli/Main.hs:200:13+    -- Expected type: String -> a -> IO ()+    --   Actual type: String -> a -> IO a+-- dbgAtM :: (Monad m, Show a) => Int -> String -> a -> m a+-- dbgAtM lvl lbl x = dbgAt lvl lbl x `seq` return x+-- XXX temporary:+dbgAtM :: Show a => Int -> String -> a -> IO ()+dbgAtM = dbgAtIO++dbgAtIO :: Show a => Int -> String -> a -> IO ()+dbgAtIO lvl lbl x = dbgAt lvl lbl x `seq` return ()++-- | print this string to the console before evaluating the expression,+-- if the global debug level is non-zero.  Uses unsafePerformIO.+dbgtrace :: String -> a -> a+dbgtrace+    | debugLevel > 0 = trace+    | otherwise      = flip const++-- | Print a showable value to the console, with a message, if the+-- debug level is at or above the specified level (uses+-- unsafePerformIO).+-- Values are displayed with show, all on one line, which is hard to read.+dbgshow :: Show a => Int -> String -> a -> a+dbgshow level+    | debugLevel >= level = ltrace+    | otherwise           = flip const++-- | Print a showable value to the console, with a message, if the+-- debug level is at or above the specified level (uses+-- unsafePerformIO).+-- Values are displayed with ppShow, each field/constructor on its own line.+dbgppshow :: Show a => Int -> String -> a -> a+dbgppshow level+    | debugLevel < level = flip const+    | otherwise = \s a -> let p = ppShow a+                              ls = lines p+                              nlorspace | length ls > 1 = "\n"+                                        | otherwise     = " " ++ take (10 - length s) (repeat ' ')+                              ls' | length ls > 1 = map (" "++) ls+                                  | otherwise     = ls+                          in trace (s++":"++nlorspace++intercalate "\n" ls') a++-- -- | Print a showable value to the console, with a message, if the+-- -- debug level is at or above the specified level (uses+-- -- unsafePerformIO).+-- -- Values are displayed with pprint. Field names are not shown, but the+-- -- output is compact with smart line wrapping, long data elided,+-- -- and slow calculations timed out.+-- dbgpprint :: Data a => Int -> String -> a -> a+-- dbgpprint level msg a+--     | debugLevel >= level = unsafePerformIO $ do+--                               pprint a >>= putStrLn . ((msg++": \n") ++) . show+--                               return a+--     | otherwise           = a+++-- | Like dbg, then exit the program. Uses unsafePerformIO.+dbgExit :: Show a => String -> a -> a+dbgExit msg = const (unsafePerformIO exitFailure) . dbg msg++-- | Print a message and parsec debug info (parse position and next+-- input) to the console when the debug level is at or above+-- this level. Uses unsafePerformIO.+-- pdbgAt :: GenParser m => Float -> String -> m ()+pdbg :: Stream [Char] m t => Int -> String -> ParsecT [Char] st m ()+pdbg level msg = when (level <= debugLevel) $ ptrace msg++
+ Hledger/Utils/Regex.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-|++Easy regular expression helpers, based on regex-tdfa and (a little) on+regexpr. These should:++- be cross-platform, not requiring C libraries++- support unicode++- support extended regular expressions++- support replacement, with backreferences etc.++- support splitting++- have mnemonic names++- have simple monomorphic types++- work with strings++Current limitations:++- (?i) and similar are not supported++-}++module Hledger.Utils.Regex (+   -- * type aliases+   Regexp+  ,Replacement+   -- * based on regex-tdfa+  ,regexMatches+  ,regexMatchesCI+  ,regexReplace+  ,regexReplaceCI+  ,regexReplaceBy+  ,regexReplaceByCI+   -- * based on regexpr+  ,regexSplit+  )+where++import Data.Array+import Data.Char+import Data.List (foldl')+import Text.RegexPR (splitRegexPR)+import Text.Regex.TDFA (+  Regex, CompOption(..), ExecOption(..), defaultCompOpt, defaultExecOpt,+  makeRegexOpts, AllMatches(getAllMatches), match, (=~), MatchText+  )++-- import Hledger.Utils.Debug+import Hledger.Utils.UTF8IOCompat (error')+++-- | Regular expression. Extended regular expression-ish syntax ? But does not support eg (?i) syntax.+type Regexp = String++-- | A replacement pattern. May include numeric backreferences (\N).+type Replacement = String++-- | Convert our string-based regexps to real ones. Can fail if the+-- string regexp is malformed.+toRegex :: Regexp -> Regex+toRegex = makeRegexOpts compOpt execOpt++toRegexCI :: Regexp -> Regex+toRegexCI = makeRegexOpts compOpt{caseSensitive=False} execOpt++compOpt :: CompOption+compOpt = defaultCompOpt++execOpt :: ExecOption+execOpt = defaultExecOpt++-- regexMatch' :: RegexContext Regexp String a => Regexp -> String -> a+-- regexMatch' r s = s =~ (toRegex r)++regexMatches :: Regexp -> String -> Bool+regexMatches = flip (=~)++regexMatchesCI :: Regexp -> String -> Bool+regexMatchesCI r = match (toRegexCI r)++-- | Replace all occurrences of the regexp, transforming each match with the given function.+regexReplaceBy :: Regexp -> (String -> String) -> String -> String+regexReplaceBy r = replaceAllBy (toRegex r)++regexReplaceByCI :: Regexp -> (String -> String) -> String -> String+regexReplaceByCI r = replaceAllBy (toRegexCI r)++-- | Replace all occurrences of the regexp with the replacement+-- pattern. The replacement pattern supports numeric backreferences+-- (\N) but no other RE syntax.+regexReplace :: Regexp -> Replacement -> String -> String+regexReplace re = replaceRegex (toRegex re)++regexReplaceCI :: Regexp -> Replacement -> String -> String+regexReplaceCI re = replaceRegex (toRegexCI re)++--++replaceRegex :: Regex -> Replacement -> String -> String+replaceRegex re repl s = foldl (replaceMatch repl) s (reverse $ match re s :: [MatchText String])++replaceMatch :: Replacement -> String -> MatchText String -> String+replaceMatch replpat s matchgroups = pre ++ repl ++ post+  where+    ((_,(off,len)):_) = elems matchgroups  -- groups should have 0-based indexes, and there should always be at least one, since this is a match+    (pre, post') = splitAt off s+    post = drop len post'+    repl = replaceAllBy (toRegex "\\\\[0-9]+") (replaceBackReference matchgroups) replpat++replaceBackReference :: MatchText String -> String -> String+replaceBackReference grps ('\\':s@(_:_)) | all isDigit s =+  case read s of n | n `elem` indices grps -> fst (grps ! n)+                 _                         -> error' $ "no match group exists for backreference \"\\"++s++"\""+replaceBackReference _ s = error' $ "replaceBackReference called on non-numeric-backreference \""++s++"\", shouldn't happen"++--++-- http://stackoverflow.com/questions/9071682/replacement-substition-with-haskell-regex-libraries :+-- | Replace all occurrences of a regexp in a string, transforming each match with the given function.+replaceAllBy :: Regex -> (String -> String) -> String -> String+replaceAllBy re f s = start end+  where+    (_, end, start) = foldl' go (0, s, id) $ getAllMatches $ match re s+    go (ind,read,write) (off,len) =+      let (skip, start) = splitAt (off - ind) read+          (matched, remaining) = splitAt len start+      in (off + len, remaining, write . (skip++) . (f matched ++))++-- uses regexpr, may be slow:++regexSplit :: Regexp -> String -> [Regexp]+regexSplit = splitRegexPR+
hledger-lib.cabal view
@@ -1,5 +1,5 @@ name:           hledger-lib-version: 0.23.3+version: 0.24 stability:      stable category:       Finance, Console synopsis:       Core data types, parsers and utilities for the hledger accounting tool.@@ -30,9 +30,22 @@ --   sample.ledger --   sample.timelog +source-repository head+  type:     git+  location: https://github.com/simonmichael/hledger++flag double+    Description:   Use old Double number representation (instead of Decimal), for testing/benchmarking.+    Default:       False++ library   -- should set patchlevel here as in Makefile-  cpp-options:    -DPATCHLEVEL=0+  cpp-options: -DPATCHLEVEL=0+  ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures+  ghc-options: -fno-warn-type-defaults -fno-warn-orphans+  if flag(double)+    cpp-options: -DDOUBLE   default-language: Haskell2010   exposed-modules:                   Hledger@@ -64,20 +77,25 @@                   Hledger.Reports.PostingsReport                   Hledger.Reports.TransactionsReports                   Hledger.Utils+                  Hledger.Utils.Debug+                  Hledger.Utils.Regex                   Hledger.Utils.UTF8IOCompat   build-depends:                   base >= 4.3 && < 5+                 ,array+                 ,blaze-markup >= 0.5.1                  ,bytestring                  ,cmdargs >= 0.10 && < 0.11                  ,containers                  ,csv                  -- ,data-pprint >= 0.2.3 && < 0.3+                 ,Decimal                  ,directory                  ,filepath                  ,mtl                  ,old-locale                  ,old-time-                 ,parsec+                 ,parsec >= 3                  ,regex-tdfa                  ,regexpr >= 0.5.1                  ,safe >= 0.2@@ -89,28 +107,30 @@   if impl(ghc >= 7.4)     build-depends: pretty-show >= 1.6.4 -source-repository head-  type:     git-  location: https://github.com/simonmichael/hledger  test-suite tests   type:     exitcode-stdio-1.0-  main-is:  tests/suite.hs-  ghc-options: -Wall+  main-is:  suite.hs+  hs-source-dirs: tests+  ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures+  ghc-options: -fno-warn-type-defaults -fno-warn-orphans   default-language: Haskell2010   build-depends: hledger-lib                , base >= 4.3 && < 5+               , array+               , blaze-markup >= 0.5.1                , cmdargs                , containers                , csv                -- , data-pprint >= 0.2.3 && < 0.3+               , Decimal                , directory                , filepath                , HUnit                , mtl                , old-locale                , old-time-               , parsec+               , parsec >= 3                , regex-tdfa                , regexpr                , safe