packages feed

hledger-lib 0.9 → 0.10

raw patch · 31 files changed

+3371/−3355 lines, 31 filesdep +safePVP ok

version bump matches the API change (PVP)

Dependencies added: safe

API changes (from Hackage documentation)

- Ledger: tests_Ledger :: Test
- Ledger.Account: instance Eq Account
- Ledger.Account: instance Show Account
- Ledger.AccountName: T :: (Map a (Tree' a)) -> Tree' a
- Ledger.AccountName: accountLeafName :: AccountName -> String
- Ledger.AccountName: accountNameComponents :: AccountName -> [String]
- Ledger.AccountName: accountNameFromComponents :: [String] -> AccountName
- Ledger.AccountName: accountNameLevel :: AccountName -> Int
- Ledger.AccountName: accountNameTreeFrom :: [AccountName] -> Tree AccountName
- Ledger.AccountName: accountNameTreeFrom4 :: [AccountName] -> Tree AccountName
- Ledger.AccountName: clipAccountName :: Int -> AccountName -> AccountName
- Ledger.AccountName: converttree :: Tree' AccountName -> [Tree AccountName]
- Ledger.AccountName: elideAccountName :: Int -> AccountName -> AccountName
- Ledger.AccountName: expandAccountNames :: [AccountName] -> [AccountName]
- Ledger.AccountName: expandTreeNames :: Tree AccountName -> Tree AccountName
- Ledger.AccountName: fromPaths :: (Ord a) => [[a]] -> Tree' a
- Ledger.AccountName: instance (Eq a) => Eq (Tree' a)
- Ledger.AccountName: instance (Ord a) => Ord (Tree' a)
- Ledger.AccountName: instance (Show a) => Show (Tree' a)
- Ledger.AccountName: isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
- Ledger.AccountName: isSubAccountNameOf :: AccountName -> AccountName -> Bool
- Ledger.AccountName: mergeTrees :: (Ord a) => Tree' a -> Tree' a -> Tree' a
- Ledger.AccountName: newtype Tree' a
- Ledger.AccountName: parentAccountName :: AccountName -> AccountName
- Ledger.AccountName: parentAccountNames :: AccountName -> [AccountName]
- Ledger.AccountName: pathtree :: [a] -> Tree' a
- Ledger.AccountName: subAccountNamesFrom :: [AccountName] -> AccountName -> [AccountName]
- Ledger.AccountName: topAccountNames :: [AccountName] -> [AccountName]
- Ledger.Amount: amountop :: (Double -> Double -> Double) -> Amount -> Amount -> Amount
- Ledger.Amount: amountopPreservingHighestPrecision :: (Double -> Double -> Double) -> Amount -> Amount -> Amount
- Ledger.Amount: amounts :: MixedAmount -> [Amount]
- Ledger.Amount: convertAmountTo :: Commodity -> Amount -> Amount
- Ledger.Amount: convertMixedAmountTo :: Commodity -> MixedAmount -> Amount
- Ledger.Amount: costOfAmount :: Amount -> Amount
- Ledger.Amount: costOfMixedAmount :: MixedAmount -> MixedAmount
- Ledger.Amount: instance Num Amount
- Ledger.Amount: instance Num MixedAmount
- Ledger.Amount: instance Ord Amount
- Ledger.Amount: instance Ord MixedAmount
- Ledger.Amount: instance Show Amount
- Ledger.Amount: instance Show HistoricalPrice
- Ledger.Amount: instance Show MixedAmount
- Ledger.Amount: isReallyZeroAmount :: Amount -> Bool
- Ledger.Amount: isReallyZeroMixedAmount :: MixedAmount -> Bool
- Ledger.Amount: isReallyZeroMixedAmountCost :: MixedAmount -> Bool
- Ledger.Amount: isZeroAmount :: Amount -> Bool
- Ledger.Amount: isZeroMixedAmount :: MixedAmount -> Bool
- Ledger.Amount: missingamt :: MixedAmount
- Ledger.Amount: mixedAmountEquals :: MixedAmount -> MixedAmount -> Bool
- Ledger.Amount: normaliseMixedAmount :: MixedAmount -> MixedAmount
- Ledger.Amount: normaliseMixedAmountIgnoringPrice :: MixedAmount -> MixedAmount
- Ledger.Amount: normaliseMixedAmountPreservingHighestPrecision :: MixedAmount -> MixedAmount
- Ledger.Amount: nullamt :: Amount
- Ledger.Amount: nullmixedamt :: MixedAmount
- Ledger.Amount: punctuatethousands :: String -> String
- Ledger.Amount: showAmount :: Amount -> String
- Ledger.Amount: showAmount' :: Amount -> String
- Ledger.Amount: showAmountDebug :: Amount -> String
- Ledger.Amount: showAmountWithoutPrice :: Amount -> String
- Ledger.Amount: showMixedAmount :: MixedAmount -> String
- Ledger.Amount: showMixedAmountDebug :: MixedAmount -> String
- Ledger.Amount: showMixedAmountOrZero :: MixedAmount -> String
- Ledger.Amount: showMixedAmountOrZeroWithoutPrice :: MixedAmount -> String
- Ledger.Amount: showMixedAmountWithoutPrice :: MixedAmount -> String
- Ledger.Amount: sumAmountsPreservingHighestPrecision :: [Amount] -> Amount
- Ledger.Amount: sumMixedAmountsPreservingHighestPrecision :: [MixedAmount] -> MixedAmount
- Ledger.Commodity: comm :: String -> Commodity
- Ledger.Commodity: conversionRate :: Commodity -> Commodity -> Double
- Ledger.Dates: d :: GenParser Char st SmartDate
- Ledger.Dates: dateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)
- Ledger.Dates: daysInSpan :: DateSpan -> Maybe Integer
- Ledger.Dates: doubledatespan :: Day -> GenParser Char st DateSpan
- Ledger.Dates: elapsedSeconds :: (Fractional a) => UTCTime -> UTCTime -> a
- Ledger.Dates: fixSmartDate :: Day -> SmartDate -> Day
- Ledger.Dates: fixSmartDateStr :: Day -> String -> String
- Ledger.Dates: fixSmartDateStrEither :: Day -> String -> Either ParseError String
- Ledger.Dates: fromdatespan :: Day -> GenParser Char st DateSpan
- Ledger.Dates: getCurrentDay :: IO Day
- Ledger.Dates: intervalanddateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)
- Ledger.Dates: intervalperiodexpr :: GenParser Char st (Interval, DateSpan)
- Ledger.Dates: justdatespan :: Day -> GenParser Char st DateSpan
- Ledger.Dates: lastthisnextthing :: GenParser Char st SmartDate
- Ledger.Dates: md :: GenParser Char st SmartDate
- Ledger.Dates: mkdatespan :: String -> String -> DateSpan
- Ledger.Dates: mon :: GenParser Char st SmartDate
- Ledger.Dates: month :: GenParser Char st SmartDate
- Ledger.Dates: parsePeriodExpr :: Day -> String -> (Interval, DateSpan)
- Ledger.Dates: parsedate :: String -> Day
- Ledger.Dates: parsedateM :: String -> Maybe Day
- Ledger.Dates: parsedatetime :: String -> LocalTime
- Ledger.Dates: parsedatetimeM :: String -> Maybe LocalTime
- Ledger.Dates: parsetimewith :: (ParseTime t) => String -> String -> t -> t
- Ledger.Dates: periodexpr :: Day -> GenParser Char st (Interval, DateSpan)
- Ledger.Dates: periodexprdatespan :: Day -> GenParser Char st DateSpan
- Ledger.Dates: periodexprinterval :: GenParser Char st Interval
- Ledger.Dates: prevday :: Day -> Day
- Ledger.Dates: showDate :: Day -> String
- Ledger.Dates: showDay :: Day -> String
- Ledger.Dates: smartdate :: GenParser Char st SmartDate
- Ledger.Dates: smartdateonly :: GenParser Char st SmartDate
- Ledger.Dates: spanFromSmartDate :: Day -> SmartDate -> DateSpan
- Ledger.Dates: spanFromSmartDateString :: Day -> String -> DateSpan
- Ledger.Dates: splitSpan :: Interval -> DateSpan -> [DateSpan]
- Ledger.Dates: splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan]
- Ledger.Dates: todatespan :: Day -> GenParser Char st DateSpan
- Ledger.Dates: today :: GenParser Char st SmartDate
- Ledger.Dates: tomorrow :: GenParser Char st SmartDate
- Ledger.Dates: y :: GenParser Char st SmartDate
- Ledger.Dates: yesterday :: GenParser Char st SmartDate
- Ledger.Dates: ym :: GenParser Char st SmartDate
- Ledger.Dates: ymd :: GenParser Char st SmartDate
- Ledger.Dates: yyyymmdd :: GenParser Char st SmartDate
- Ledger.IO: journalFromString :: String -> IO Journal
- Ledger.IO: myLedger :: IO Ledger
- Ledger.IO: myLedgerPath :: IO String
- Ledger.IO: myTimelog :: IO Ledger
- Ledger.IO: myTimelogPath :: IO String
- Ledger.IO: readLedger :: FilePath -> IO Ledger
- Ledger.Journal: addHistoricalPrice :: HistoricalPrice -> Journal -> Journal
- Ledger.Journal: addModifierTransaction :: ModifierTransaction -> Journal -> Journal
- Ledger.Journal: addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal
- Ledger.Journal: addTimeLogEntry :: TimeLogEntry -> Journal -> Journal
- Ledger.Journal: addTransaction :: Transaction -> Journal -> Journal
- Ledger.Journal: calculateBalances :: Tree AccountName -> (AccountName -> [Posting]) -> Tree (AccountName, (MixedAmount, MixedAmount))
- Ledger.Journal: canonicaliseAmounts :: Bool -> Journal -> Journal
- Ledger.Journal: crunchJournal :: Journal -> (Tree AccountName, Map AccountName Account)
- Ledger.Journal: filterJournalPostings :: FilterSpec -> Journal -> Journal
- Ledger.Journal: filterJournalPostingsByAccount :: [String] -> Journal -> Journal
- Ledger.Journal: filterJournalPostingsByClearedStatus :: Maybe Bool -> Journal -> Journal
- Ledger.Journal: filterJournalPostingsByDepth :: Maybe Int -> Journal -> Journal
- Ledger.Journal: filterJournalPostingsByEmpty :: Bool -> Journal -> Journal
- Ledger.Journal: filterJournalPostingsByRealness :: Bool -> Journal -> Journal
- Ledger.Journal: filterJournalTransactions :: FilterSpec -> Journal -> Journal
- Ledger.Journal: filterJournalTransactionsByAccount :: [String] -> Journal -> Journal
- Ledger.Journal: filterJournalTransactionsByClearedStatus :: Maybe Bool -> Journal -> Journal
- Ledger.Journal: filterJournalTransactionsByDate :: DateSpan -> Journal -> Journal
- Ledger.Journal: filterJournalTransactionsByDepth :: Maybe Int -> Journal -> Journal
- Ledger.Journal: filterJournalTransactionsByDescription :: [String] -> Journal -> Journal
- Ledger.Journal: groupPostings :: [Posting] -> (Tree AccountName, AccountName -> [Posting], AccountName -> MixedAmount, AccountName -> MixedAmount)
- Ledger.Journal: instance Show Journal
- Ledger.Journal: journalAccountNameTree :: Journal -> Tree AccountName
- Ledger.Journal: journalAccountNames :: Journal -> [AccountName]
- Ledger.Journal: journalAccountNamesUsed :: Journal -> [AccountName]
- Ledger.Journal: journalAmounts :: Journal -> [MixedAmount]
- Ledger.Journal: journalCommodities :: Journal -> [Commodity]
- Ledger.Journal: journalConvertTimeLog :: LocalTime -> Journal -> Journal
- Ledger.Journal: journalDateSpan :: Journal -> DateSpan
- Ledger.Journal: journalPostings :: Journal -> [Posting]
- Ledger.Journal: journalPrecisions :: Journal -> [Int]
- Ledger.Journal: journalSelectingDate :: WhichDate -> Journal -> Journal
- Ledger.Journal: matchpats :: [String] -> String -> Bool
- Ledger.Journal: nulljournal :: Journal
- Ledger.Journal: postingsByAccount :: [Posting] -> Map AccountName [Posting]
- Ledger.Ledger: account :: Ledger -> AccountName -> Account
- Ledger.Ledger: accountnames :: Ledger -> [AccountName]
- Ledger.Ledger: accounts :: Ledger -> [Account]
- Ledger.Ledger: accountsmatching :: [String] -> Ledger -> [Account]
- Ledger.Ledger: accounttree :: Int -> Ledger -> Tree Account
- Ledger.Ledger: accounttreeat :: Ledger -> Account -> Maybe (Tree Account)
- Ledger.Ledger: cacheLedger :: Journal -> Ledger
- Ledger.Ledger: cacheLedger' :: Ledger -> CachedLedger
- Ledger.Ledger: commodities :: Ledger -> [Commodity]
- Ledger.Ledger: instance Show Ledger
- Ledger.Ledger: ledgerAccount :: Ledger -> AccountName -> Account
- Ledger.Ledger: ledgerAccountNames :: Ledger -> [AccountName]
- Ledger.Ledger: ledgerAccountTree :: Int -> Ledger -> Tree Account
- Ledger.Ledger: ledgerAccountTreeAt :: Ledger -> Account -> Maybe (Tree Account)
- Ledger.Ledger: ledgerAccounts :: Ledger -> [Account]
- Ledger.Ledger: ledgerAccountsMatching :: [String] -> Ledger -> [Account]
- Ledger.Ledger: ledgerDateSpan :: Ledger -> DateSpan
- Ledger.Ledger: ledgerPostings :: Ledger -> [Posting]
- Ledger.Ledger: ledgerSubAccounts :: Ledger -> Account -> [Account]
- Ledger.Ledger: ledgerTopAccounts :: Ledger -> [Account]
- Ledger.Ledger: ledgeramounts :: Ledger -> [MixedAmount]
- Ledger.Ledger: nullledger :: Ledger
- Ledger.Ledger: postings :: Ledger -> [Posting]
- Ledger.Ledger: rawdatespan :: Ledger -> DateSpan
- Ledger.Ledger: subaccounts :: Ledger -> Account -> [Account]
- Ledger.Ledger: topaccounts :: Ledger -> [Account]
- Ledger.Ledger: type CachedLedger = Ledger
- Ledger.Parse: Ctx :: !Maybe Integer -> !Maybe String -> ![String] -> LedgerFileCtx
- Ledger.Parse: amountquantity :: GenParser Char st (Double, Int, Bool)
- Ledger.Parse: commoditysymbol :: GenParser Char st String
- Ledger.Parse: ctxAccount :: LedgerFileCtx -> ![String]
- Ledger.Parse: ctxCommod :: LedgerFileCtx -> !Maybe String
- Ledger.Parse: ctxYear :: LedgerFileCtx -> !Maybe Integer
- Ledger.Parse: data LedgerFileCtx
- Ledger.Parse: datedisplayexpr :: GenParser Char st (Posting -> Bool)
- Ledger.Parse: emptyCtx :: LedgerFileCtx
- Ledger.Parse: emptyLine :: GenParser Char st ()
- Ledger.Parse: expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath
- Ledger.Parse: getParentAccount :: GenParser tok LedgerFileCtx String
- Ledger.Parse: getYear :: GenParser tok LedgerFileCtx (Maybe Integer)
- Ledger.Parse: instance Read LedgerFileCtx
- Ledger.Parse: instance Show LedgerFileCtx
- Ledger.Parse: ledgerAccountBegin :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerAccountEnd :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerCommodityConversion :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerDefaultCommodity :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerDefaultYear :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerEndTagDirective :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerExclamationDirective :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerFile :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerHistoricalPrice :: GenParser Char LedgerFileCtx HistoricalPrice
- Ledger.Parse: ledgerIgnoredPriceCommodity :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerInclude :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerModifierTransaction :: GenParser Char LedgerFileCtx ModifierTransaction
- Ledger.Parse: ledgerPeriodicTransaction :: GenParser Char LedgerFileCtx PeriodicTransaction
- Ledger.Parse: ledgerTagDirective :: GenParser Char LedgerFileCtx JournalUpdate
- Ledger.Parse: ledgerTransaction :: GenParser Char LedgerFileCtx Transaction
- Ledger.Parse: ledgeraccountname :: GenParser Char st AccountName
- Ledger.Parse: ledgercode :: GenParser Char st String
- Ledger.Parse: ledgercomment :: GenParser Char st String
- Ledger.Parse: ledgercommentline :: GenParser Char st String
- Ledger.Parse: ledgerdate :: GenParser Char LedgerFileCtx Day
- Ledger.Parse: ledgerdatetime :: GenParser Char LedgerFileCtx LocalTime
- Ledger.Parse: ledgereffectivedate :: Day -> GenParser Char LedgerFileCtx (Maybe Day)
- Ledger.Parse: ledgerfulldate :: GenParser Char LedgerFileCtx Day
- Ledger.Parse: ledgerpartialdate :: GenParser Char LedgerFileCtx Day
- Ledger.Parse: ledgerposting :: GenParser Char LedgerFileCtx Posting
- Ledger.Parse: ledgerpostings :: GenParser Char LedgerFileCtx [Posting]
- Ledger.Parse: ledgerstatus :: GenParser Char st Bool
- Ledger.Parse: leftsymbolamount :: GenParser Char st MixedAmount
- Ledger.Parse: linebeginningwithspaces :: GenParser Char st String
- Ledger.Parse: nosymbolamount :: GenParser Char st MixedAmount
- Ledger.Parse: numberparts :: GenParser Char st (String, String)
- Ledger.Parse: numberpartsstartingwithdigit :: GenParser Char st (String, String)
- Ledger.Parse: numberpartsstartingwithpoint :: GenParser Char st (String, String)
- Ledger.Parse: parseLedger :: LocalTime -> FilePath -> String -> ErrorT String IO Journal
- Ledger.Parse: parseLedgerFile :: LocalTime -> FilePath -> ErrorT String IO Journal
- Ledger.Parse: popParentAccount :: GenParser tok LedgerFileCtx ()
- Ledger.Parse: postingamount :: GenParser Char st MixedAmount
- Ledger.Parse: priceamount :: GenParser Char st (Maybe MixedAmount)
- Ledger.Parse: pushParentAccount :: String -> GenParser tok LedgerFileCtx ()
- Ledger.Parse: quotedcommoditysymbol :: GenParser Char st String
- Ledger.Parse: rightsymbolamount :: GenParser Char st MixedAmount
- Ledger.Parse: setYear :: Integer -> GenParser tok LedgerFileCtx ()
- Ledger.Parse: someamount :: GenParser Char st MixedAmount
- Ledger.Parse: timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry
- Ledger.Parse: transactionaccountname :: GenParser Char LedgerFileCtx AccountName
- Ledger.Parse: type JournalUpdate = ErrorT String IO (Journal -> Journal)
- Ledger.Posting: accountNamesFromPostings :: [Posting] -> [AccountName]
- Ledger.Posting: hasAmount :: Posting -> Bool
- Ledger.Posting: instance Show Posting
- Ledger.Posting: isBalancedVirtual :: Posting -> Bool
- Ledger.Posting: isEmptyPosting :: Posting -> Bool
- Ledger.Posting: isPostingInDateSpan :: DateSpan -> Posting -> Bool
- Ledger.Posting: isReal :: Posting -> Bool
- Ledger.Posting: isVirtual :: Posting -> Bool
- Ledger.Posting: postingCleared :: Posting -> Bool
- Ledger.Posting: postingDate :: Posting -> Day
- Ledger.Posting: postingsDateSpan :: [Posting] -> DateSpan
- Ledger.Posting: showPosting :: Posting -> String
- Ledger.Posting: sumPostings :: [Posting] -> MixedAmount
- Ledger.TimeLog: entriesFromTimeLogEntries :: LocalTime -> [TimeLogEntry] -> [Transaction]
- Ledger.TimeLog: entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction
- Ledger.TimeLog: instance Read TimeLogCode
- Ledger.TimeLog: instance Show TimeLogCode
- Ledger.TimeLog: instance Show TimeLogEntry
- Ledger.Transaction: balanceTransaction :: Transaction -> Either String Transaction
- Ledger.Transaction: balancedVirtualPostings :: Transaction -> [Posting]
- Ledger.Transaction: instance Show ModifierTransaction
- Ledger.Transaction: instance Show PeriodicTransaction
- Ledger.Transaction: instance Show Transaction
- Ledger.Transaction: isTransactionBalanced :: Transaction -> Bool
- Ledger.Transaction: ledgerTransactionWithDate :: WhichDate -> Transaction -> Transaction
- Ledger.Transaction: nonzerobalanceerror :: Transaction -> String
- Ledger.Transaction: nulltransaction :: Transaction
- Ledger.Transaction: realPostings :: Transaction -> [Posting]
- Ledger.Transaction: settxn :: Transaction -> Posting -> Posting
- Ledger.Transaction: showAccountName :: Maybe Int -> PostingType -> AccountName -> String
- Ledger.Transaction: showTransaction :: Transaction -> String
- Ledger.Transaction: showTransaction' :: Bool -> Bool -> Transaction -> String
- Ledger.Transaction: showTransactionForPrint :: Bool -> Transaction -> String
- Ledger.Transaction: showTransactionUnelided :: Transaction -> String
- Ledger.Transaction: transactionPostingBalances :: Transaction -> (MixedAmount, MixedAmount, MixedAmount)
- Ledger.Transaction: txnTieKnot :: Transaction -> Transaction
- Ledger.Transaction: virtualPostings :: Transaction -> [Posting]
- Ledger.Types: Account :: AccountName -> [Posting] -> MixedAmount -> Account
- Ledger.Types: ActualDate :: WhichDate
- Ledger.Types: Amount :: Commodity -> Double -> Maybe MixedAmount -> Amount
- Ledger.Types: BalancedVirtualPosting :: PostingType
- Ledger.Types: Commodity :: String -> Side -> Bool -> Bool -> Int -> Commodity
- Ledger.Types: Daily :: Interval
- Ledger.Types: DateSpan :: (Maybe Day) -> (Maybe Day) -> DateSpan
- Ledger.Types: EffectiveDate :: WhichDate
- Ledger.Types: FilterSpec :: DateSpan -> Maybe Bool -> Bool -> Bool -> Bool -> [String] -> [String] -> WhichDate -> Maybe Int -> FilterSpec
- Ledger.Types: FinalOut :: TimeLogCode
- Ledger.Types: HistoricalPrice :: Day -> String -> MixedAmount -> HistoricalPrice
- Ledger.Types: In :: TimeLogCode
- Ledger.Types: Journal :: [ModifierTransaction] -> [PeriodicTransaction] -> [Transaction] -> [TimeLogEntry] -> [HistoricalPrice] -> String -> FilePath -> ClockTime -> String -> Journal
- Ledger.Types: L :: Side
- Ledger.Types: Ledger :: Journal -> Tree AccountName -> Map AccountName Account -> Ledger
- Ledger.Types: Mixed :: [Amount] -> MixedAmount
- Ledger.Types: ModifierTransaction :: String -> [Posting] -> ModifierTransaction
- Ledger.Types: Monthly :: Interval
- Ledger.Types: NoInterval :: Interval
- Ledger.Types: Out :: TimeLogCode
- Ledger.Types: PeriodicTransaction :: String -> [Posting] -> PeriodicTransaction
- Ledger.Types: Posting :: Bool -> AccountName -> MixedAmount -> String -> PostingType -> Maybe Transaction -> Posting
- Ledger.Types: Quarterly :: Interval
- Ledger.Types: R :: Side
- Ledger.Types: RegularPosting :: PostingType
- Ledger.Types: SetBalance :: TimeLogCode
- Ledger.Types: SetRequiredHours :: TimeLogCode
- Ledger.Types: TimeLogEntry :: TimeLogCode -> LocalTime -> String -> TimeLogEntry
- Ledger.Types: Transaction :: Day -> Maybe Day -> Bool -> String -> String -> String -> [Posting] -> String -> Transaction
- Ledger.Types: VirtualPosting :: PostingType
- Ledger.Types: Weekly :: Interval
- Ledger.Types: Yearly :: Interval
- Ledger.Types: abalance :: Account -> MixedAmount
- Ledger.Types: accountmap :: Ledger -> Map AccountName Account
- Ledger.Types: accountnametree :: Ledger -> Tree AccountName
- Ledger.Types: acctpats :: FilterSpec -> [String]
- Ledger.Types: aname :: Account -> AccountName
- Ledger.Types: apostings :: Account -> [Posting]
- Ledger.Types: cleared :: FilterSpec -> Maybe Bool
- Ledger.Types: comma :: Commodity -> Bool
- Ledger.Types: commodity :: Amount -> Commodity
- Ledger.Types: costbasis :: FilterSpec -> Bool
- Ledger.Types: data Account
- Ledger.Types: data Amount
- Ledger.Types: data Commodity
- Ledger.Types: data DateSpan
- Ledger.Types: data FilterSpec
- Ledger.Types: data HistoricalPrice
- Ledger.Types: data Interval
- Ledger.Types: data Journal
- Ledger.Types: data Ledger
- Ledger.Types: data ModifierTransaction
- Ledger.Types: data PeriodicTransaction
- Ledger.Types: data Posting
- Ledger.Types: data PostingType
- Ledger.Types: data Side
- Ledger.Types: data TimeLogCode
- Ledger.Types: data TimeLogEntry
- Ledger.Types: data Transaction
- Ledger.Types: data WhichDate
- Ledger.Types: datespan :: FilterSpec -> DateSpan
- Ledger.Types: depth :: FilterSpec -> Maybe Int
- Ledger.Types: descpats :: FilterSpec -> [String]
- Ledger.Types: empty :: FilterSpec -> Bool
- Ledger.Types: filepath :: Journal -> FilePath
- Ledger.Types: filereadtime :: Journal -> ClockTime
- Ledger.Types: final_comment_lines :: Journal -> String
- Ledger.Types: hamount :: HistoricalPrice -> MixedAmount
- Ledger.Types: hdate :: HistoricalPrice -> Day
- Ledger.Types: historical_prices :: Journal -> [HistoricalPrice]
- Ledger.Types: hsymbol :: HistoricalPrice -> String
- Ledger.Types: instance Eq Amount
- Ledger.Types: instance Eq Commodity
- Ledger.Types: instance Eq DateSpan
- Ledger.Types: instance Eq HistoricalPrice
- Ledger.Types: instance Eq Interval
- Ledger.Types: instance Eq Journal
- Ledger.Types: instance Eq MixedAmount
- Ledger.Types: instance Eq ModifierTransaction
- Ledger.Types: instance Eq PeriodicTransaction
- Ledger.Types: instance Eq Posting
- Ledger.Types: instance Eq PostingType
- Ledger.Types: instance Eq Side
- Ledger.Types: instance Eq TimeLogCode
- Ledger.Types: instance Eq TimeLogEntry
- Ledger.Types: instance Eq Transaction
- Ledger.Types: instance Eq WhichDate
- Ledger.Types: instance Ord Commodity
- Ledger.Types: instance Ord DateSpan
- Ledger.Types: instance Ord Interval
- Ledger.Types: instance Ord Side
- Ledger.Types: instance Ord TimeLogCode
- Ledger.Types: instance Ord TimeLogEntry
- Ledger.Types: instance Show Commodity
- Ledger.Types: instance Show DateSpan
- Ledger.Types: instance Show FilterSpec
- Ledger.Types: instance Show Interval
- Ledger.Types: instance Show PostingType
- Ledger.Types: instance Show Side
- Ledger.Types: instance Show WhichDate
- Ledger.Types: instance Typeable Ledger
- Ledger.Types: jmodifiertxns :: Journal -> [ModifierTransaction]
- Ledger.Types: journal :: Ledger -> Journal
- Ledger.Types: jperiodictxns :: Journal -> [PeriodicTransaction]
- Ledger.Types: jtext :: Journal -> String
- Ledger.Types: jtxns :: Journal -> [Transaction]
- Ledger.Types: mtpostings :: ModifierTransaction -> [Posting]
- Ledger.Types: mtvalueexpr :: ModifierTransaction -> String
- Ledger.Types: newtype MixedAmount
- Ledger.Types: open_timelog_entries :: Journal -> [TimeLogEntry]
- Ledger.Types: paccount :: Posting -> AccountName
- Ledger.Types: pamount :: Posting -> MixedAmount
- Ledger.Types: pcomment :: Posting -> String
- Ledger.Types: precision :: Commodity -> Int
- Ledger.Types: price :: Amount -> Maybe MixedAmount
- Ledger.Types: pstatus :: Posting -> Bool
- Ledger.Types: ptperiodicexpr :: PeriodicTransaction -> String
- Ledger.Types: ptpostings :: PeriodicTransaction -> [Posting]
- Ledger.Types: ptransaction :: Posting -> Maybe Transaction
- Ledger.Types: ptype :: Posting -> PostingType
- Ledger.Types: quantity :: Amount -> Double
- Ledger.Types: real :: FilterSpec -> Bool
- Ledger.Types: side :: Commodity -> Side
- Ledger.Types: spaced :: Commodity -> Bool
- Ledger.Types: symbol :: Commodity -> String
- Ledger.Types: tcode :: Transaction -> String
- Ledger.Types: tcomment :: Transaction -> String
- Ledger.Types: tdate :: Transaction -> Day
- Ledger.Types: tdescription :: Transaction -> String
- Ledger.Types: teffectivedate :: Transaction -> Maybe Day
- Ledger.Types: tlcode :: TimeLogEntry -> TimeLogCode
- Ledger.Types: tlcomment :: TimeLogEntry -> String
- Ledger.Types: tldatetime :: TimeLogEntry -> LocalTime
- Ledger.Types: tpostings :: Transaction -> [Posting]
- Ledger.Types: tpreceding_comment_lines :: Transaction -> String
- Ledger.Types: tstatus :: Transaction -> Bool
- Ledger.Types: type AccountName = String
- Ledger.Types: type SmartDate = (String, String, String)
- Ledger.Types: whichdate :: FilterSpec -> WhichDate
- Ledger.Utils: assertParse :: (Either ParseError a) -> Assertion
- Ledger.Utils: assertParseEqual :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion
- Ledger.Utils: cliptopleft :: Int -> Int -> String -> String
- Ledger.Utils: concatBottomPadded :: [String] -> String
- Ledger.Utils: concatTopPadded :: [String] -> String
- Ledger.Utils: containsRegex :: String -> String -> Bool
- Ledger.Utils: difforzero :: (Num a, Ord a) => a -> a -> a
- Ledger.Utils: fitto :: Int -> Int -> String -> String
- Ledger.Utils: fromparse :: Either ParseError a -> a
- Ledger.Utils: getCurrentLocalTime :: IO LocalTime
- Ledger.Utils: is :: (Eq a, Show a) => a -> a -> Assertion
- Ledger.Utils: isLeft :: Either a b -> Bool
- Ledger.Utils: isRight :: Either a b -> Bool
- Ledger.Utils: leaves :: Tree a -> [a]
- Ledger.Utils: ltrace :: (Show a) => String -> a -> a
- Ledger.Utils: nonspace :: GenParser Char st Char
- Ledger.Utils: padbottom :: Int -> String -> String
- Ledger.Utils: padleft :: Int -> String -> String
- Ledger.Utils: padright :: Int -> String -> String
- Ledger.Utils: padtop :: Int -> String -> String
- Ledger.Utils: parseWithCtx :: b -> GenParser Char b a -> String -> Either ParseError a
- Ledger.Utils: parsewith :: Parser a -> String -> Either ParseError a
- Ledger.Utils: printParseError :: (Show a) => a -> IO ()
- Ledger.Utils: restofline :: GenParser Char st String
- Ledger.Utils: showforest :: (Show a) => Forest a -> String
- Ledger.Utils: showtree :: (Show a) => Tree a -> String
- Ledger.Utils: spacenonewline :: GenParser Char st Char
- Ledger.Utils: splitAtElement :: (Eq a) => a -> [a] -> [[a]]
- Ledger.Utils: strace :: (Show a) => a -> a
- Ledger.Utils: strictReadFile :: FilePath -> IO String
- Ledger.Utils: subtreeat :: (Eq a) => a -> Tree a -> Maybe (Tree a)
- Ledger.Utils: subtreeinforest :: (Eq a) => a -> [Tree a] -> Maybe (Tree a)
- Ledger.Utils: tfilter :: (Test -> Bool) -> Test -> Test
- Ledger.Utils: tflatten :: Test -> [Test]
- Ledger.Utils: tname :: Test -> String
- Ledger.Utils: treeany :: (a -> Bool) -> Tree a -> Bool
- Ledger.Utils: treefilter :: (a -> Bool) -> Tree a -> Tree a
- Ledger.Utils: treemap :: (a -> b) -> Tree a -> Tree b
- Ledger.Utils: treeprune :: Int -> Tree a -> Tree a
- Ledger.Utils: unbracket :: String -> String
- Ledger.Utils: underline :: String -> String
- Ledger.Utils: vConcatRightAligned :: [String] -> String
+ Hledger.Data: tests_Hledger_Data :: Test
+ Hledger.Data.Account: instance Eq Account
+ Hledger.Data.Account: instance Show Account
+ Hledger.Data.AccountName: T :: (Map a (Tree' a)) -> Tree' a
+ Hledger.Data.AccountName: accountLeafName :: AccountName -> String
+ Hledger.Data.AccountName: accountNameComponents :: AccountName -> [String]
+ Hledger.Data.AccountName: accountNameFromComponents :: [String] -> AccountName
+ Hledger.Data.AccountName: accountNameLevel :: AccountName -> Int
+ Hledger.Data.AccountName: accountNameTreeFrom :: [AccountName] -> Tree AccountName
+ Hledger.Data.AccountName: accountNameTreeFrom4 :: [AccountName] -> Tree AccountName
+ Hledger.Data.AccountName: clipAccountName :: Int -> AccountName -> AccountName
+ Hledger.Data.AccountName: converttree :: Tree' AccountName -> [Tree AccountName]
+ Hledger.Data.AccountName: elideAccountName :: Int -> AccountName -> AccountName
+ Hledger.Data.AccountName: expandAccountNames :: [AccountName] -> [AccountName]
+ Hledger.Data.AccountName: expandTreeNames :: Tree AccountName -> Tree AccountName
+ Hledger.Data.AccountName: fromPaths :: (Ord a) => [[a]] -> Tree' a
+ Hledger.Data.AccountName: instance (Eq a) => Eq (Tree' a)
+ Hledger.Data.AccountName: instance (Ord a) => Ord (Tree' a)
+ Hledger.Data.AccountName: instance (Show a) => Show (Tree' a)
+ Hledger.Data.AccountName: isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
+ Hledger.Data.AccountName: isSubAccountNameOf :: AccountName -> AccountName -> Bool
+ Hledger.Data.AccountName: mergeTrees :: (Ord a) => Tree' a -> Tree' a -> Tree' a
+ Hledger.Data.AccountName: newtype Tree' a
+ Hledger.Data.AccountName: parentAccountName :: AccountName -> AccountName
+ Hledger.Data.AccountName: parentAccountNames :: AccountName -> [AccountName]
+ Hledger.Data.AccountName: pathtree :: [a] -> Tree' a
+ Hledger.Data.AccountName: subAccountNamesFrom :: [AccountName] -> AccountName -> [AccountName]
+ Hledger.Data.AccountName: topAccountNames :: [AccountName] -> [AccountName]
+ Hledger.Data.Amount: amountop :: (Double -> Double -> Double) -> Amount -> Amount -> Amount
+ Hledger.Data.Amount: amountopPreservingHighestPrecision :: (Double -> Double -> Double) -> Amount -> Amount -> Amount
+ Hledger.Data.Amount: amounts :: MixedAmount -> [Amount]
+ Hledger.Data.Amount: convertAmountTo :: Commodity -> Amount -> Amount
+ Hledger.Data.Amount: convertMixedAmountTo :: Commodity -> MixedAmount -> Amount
+ Hledger.Data.Amount: costOfAmount :: Amount -> Amount
+ Hledger.Data.Amount: costOfMixedAmount :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: instance Num Amount
+ Hledger.Data.Amount: instance Num MixedAmount
+ Hledger.Data.Amount: instance Ord Amount
+ Hledger.Data.Amount: instance Ord MixedAmount
+ Hledger.Data.Amount: instance Show Amount
+ Hledger.Data.Amount: instance Show HistoricalPrice
+ Hledger.Data.Amount: instance Show MixedAmount
+ Hledger.Data.Amount: isReallyZeroAmount :: Amount -> Bool
+ Hledger.Data.Amount: isReallyZeroMixedAmount :: MixedAmount -> Bool
+ Hledger.Data.Amount: isReallyZeroMixedAmountCost :: MixedAmount -> Bool
+ Hledger.Data.Amount: isZeroAmount :: Amount -> Bool
+ Hledger.Data.Amount: isZeroMixedAmount :: MixedAmount -> Bool
+ Hledger.Data.Amount: missingamt :: MixedAmount
+ Hledger.Data.Amount: mixedAmountEquals :: MixedAmount -> MixedAmount -> Bool
+ Hledger.Data.Amount: normaliseMixedAmount :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: normaliseMixedAmountIgnoringPrice :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: normaliseMixedAmountPreservingHighestPrecision :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: nullamt :: Amount
+ Hledger.Data.Amount: nullmixedamt :: MixedAmount
+ Hledger.Data.Amount: punctuatethousands :: String -> String
+ Hledger.Data.Amount: showAmount :: Amount -> String
+ Hledger.Data.Amount: showAmount' :: Amount -> String
+ Hledger.Data.Amount: showAmountDebug :: Amount -> String
+ Hledger.Data.Amount: showAmountWithoutPrice :: Amount -> String
+ Hledger.Data.Amount: showMixedAmount :: MixedAmount -> String
+ Hledger.Data.Amount: showMixedAmountDebug :: MixedAmount -> String
+ Hledger.Data.Amount: showMixedAmountOrZero :: MixedAmount -> String
+ Hledger.Data.Amount: showMixedAmountOrZeroWithoutPrice :: MixedAmount -> String
+ Hledger.Data.Amount: showMixedAmountWithoutPrice :: MixedAmount -> String
+ Hledger.Data.Amount: sumAmountsPreservingHighestPrecision :: [Amount] -> Amount
+ Hledger.Data.Amount: sumMixedAmountsPreservingHighestPrecision :: [MixedAmount] -> MixedAmount
+ Hledger.Data.Commodity: comm :: String -> Commodity
+ Hledger.Data.Commodity: conversionRate :: Commodity -> Commodity -> Double
+ Hledger.Data.Dates: d :: GenParser Char st SmartDate
+ Hledger.Data.Dates: dateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)
+ Hledger.Data.Dates: daysInSpan :: DateSpan -> Maybe Integer
+ Hledger.Data.Dates: doubledatespan :: Day -> GenParser Char st DateSpan
+ Hledger.Data.Dates: elapsedSeconds :: (Fractional a) => UTCTime -> UTCTime -> a
+ Hledger.Data.Dates: fixSmartDate :: Day -> SmartDate -> Day
+ Hledger.Data.Dates: fixSmartDateStr :: Day -> String -> String
+ Hledger.Data.Dates: fixSmartDateStrEither :: Day -> String -> Either ParseError String
+ Hledger.Data.Dates: fromdatespan :: Day -> GenParser Char st DateSpan
+ Hledger.Data.Dates: getCurrentDay :: IO Day
+ Hledger.Data.Dates: intervalanddateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)
+ Hledger.Data.Dates: intervalperiodexpr :: GenParser Char st (Interval, DateSpan)
+ Hledger.Data.Dates: justdatespan :: Day -> GenParser Char st DateSpan
+ Hledger.Data.Dates: lastthisnextthing :: GenParser Char st SmartDate
+ Hledger.Data.Dates: md :: GenParser Char st SmartDate
+ Hledger.Data.Dates: mkdatespan :: String -> String -> DateSpan
+ Hledger.Data.Dates: mon :: GenParser Char st SmartDate
+ Hledger.Data.Dates: month :: GenParser Char st SmartDate
+ Hledger.Data.Dates: parsePeriodExpr :: Day -> String -> (Interval, DateSpan)
+ Hledger.Data.Dates: parsedate :: String -> Day
+ Hledger.Data.Dates: parsedateM :: String -> Maybe Day
+ Hledger.Data.Dates: parsedatetime :: String -> LocalTime
+ Hledger.Data.Dates: parsedatetimeM :: String -> Maybe LocalTime
+ Hledger.Data.Dates: parsetimewith :: (ParseTime t) => String -> String -> t -> t
+ Hledger.Data.Dates: periodexpr :: Day -> GenParser Char st (Interval, DateSpan)
+ Hledger.Data.Dates: periodexprdatespan :: Day -> GenParser Char st DateSpan
+ Hledger.Data.Dates: periodexprinterval :: GenParser Char st Interval
+ Hledger.Data.Dates: prevday :: Day -> Day
+ Hledger.Data.Dates: showDate :: Day -> String
+ Hledger.Data.Dates: showDay :: Day -> String
+ Hledger.Data.Dates: smartdate :: GenParser Char st SmartDate
+ Hledger.Data.Dates: smartdateonly :: GenParser Char st SmartDate
+ Hledger.Data.Dates: spanFromSmartDate :: Day -> SmartDate -> DateSpan
+ Hledger.Data.Dates: spanFromSmartDateString :: Day -> String -> DateSpan
+ Hledger.Data.Dates: splitSpan :: Interval -> DateSpan -> [DateSpan]
+ Hledger.Data.Dates: splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan]
+ Hledger.Data.Dates: todatespan :: Day -> GenParser Char st DateSpan
+ Hledger.Data.Dates: today :: GenParser Char st SmartDate
+ Hledger.Data.Dates: tomorrow :: GenParser Char st SmartDate
+ Hledger.Data.Dates: validDay :: String -> Bool
+ Hledger.Data.Dates: validMonth :: String -> Bool
+ Hledger.Data.Dates: validYear :: String -> Bool
+ Hledger.Data.Dates: y :: GenParser Char st SmartDate
+ Hledger.Data.Dates: yesterday :: GenParser Char st SmartDate
+ Hledger.Data.Dates: ym :: GenParser Char st SmartDate
+ Hledger.Data.Dates: ymd :: GenParser Char st SmartDate
+ Hledger.Data.Dates: yyyymmdd :: GenParser Char st SmartDate
+ Hledger.Data.IO: journalFromString :: String -> IO Journal
+ Hledger.Data.IO: myJournal :: IO Journal
+ Hledger.Data.IO: myLedgerPath :: IO String
+ Hledger.Data.IO: myTimelog :: IO Journal
+ Hledger.Data.IO: myTimelogPath :: IO String
+ Hledger.Data.IO: readJournal :: FilePath -> IO Journal
+ Hledger.Data.Journal: addHistoricalPrice :: HistoricalPrice -> Journal -> Journal
+ Hledger.Data.Journal: addModifierTransaction :: ModifierTransaction -> Journal -> Journal
+ Hledger.Data.Journal: addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal
+ Hledger.Data.Journal: addTimeLogEntry :: TimeLogEntry -> Journal -> Journal
+ Hledger.Data.Journal: addTransaction :: Transaction -> Journal -> Journal
+ Hledger.Data.Journal: amountCommodities :: Amount -> [Commodity]
+ Hledger.Data.Journal: calculateBalances :: Tree AccountName -> (AccountName -> [Posting]) -> Tree (AccountName, (MixedAmount, MixedAmount))
+ Hledger.Data.Journal: filterJournalPostings :: FilterSpec -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalPostingsByAccount :: [String] -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalPostingsByClearedStatus :: Maybe Bool -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalPostingsByDepth :: Maybe Int -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalPostingsByEmpty :: Bool -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalPostingsByRealness :: Bool -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalTransactions :: FilterSpec -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalTransactionsByAccount :: [String] -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalTransactionsByClearedStatus :: Maybe Bool -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalTransactionsByDate :: DateSpan -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalTransactionsByDepth :: Maybe Int -> Journal -> Journal
+ Hledger.Data.Journal: filterJournalTransactionsByDescription :: [String] -> Journal -> Journal
+ Hledger.Data.Journal: groupPostings :: [Posting] -> (Tree AccountName, AccountName -> [Posting], AccountName -> MixedAmount, AccountName -> MixedAmount)
+ Hledger.Data.Journal: instance Show Journal
+ Hledger.Data.Journal: journalAccountInfo :: Journal -> (Tree AccountName, Map AccountName Account)
+ Hledger.Data.Journal: journalAccountNameTree :: Journal -> Tree AccountName
+ Hledger.Data.Journal: journalAccountNames :: Journal -> [AccountName]
+ Hledger.Data.Journal: journalAccountNamesUsed :: Journal -> [AccountName]
+ Hledger.Data.Journal: journalAmountAndPriceCommodities :: Journal -> [Commodity]
+ Hledger.Data.Journal: journalAmountCommodities :: Journal -> [Commodity]
+ Hledger.Data.Journal: journalAmounts :: Journal -> [MixedAmount]
+ Hledger.Data.Journal: journalApplyHistoricalPrices :: Journal -> Journal
+ Hledger.Data.Journal: journalCanonicalCommodities :: Journal -> Map String Commodity
+ Hledger.Data.Journal: journalCanonicaliseAmounts :: Journal -> Journal
+ Hledger.Data.Journal: journalCloseTimeLogEntries :: LocalTime -> Journal -> Journal
+ Hledger.Data.Journal: journalConvertAmountsToCost :: Journal -> Journal
+ Hledger.Data.Journal: journalDateSpan :: Journal -> DateSpan
+ Hledger.Data.Journal: journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> Journal -> Journal
+ Hledger.Data.Journal: journalHistoricalPriceFor :: Journal -> Day -> Commodity -> Maybe MixedAmount
+ Hledger.Data.Journal: journalPostings :: Journal -> [Posting]
+ Hledger.Data.Journal: journalSelectingDate :: WhichDate -> Journal -> Journal
+ Hledger.Data.Journal: matchpats :: [String] -> String -> Bool
+ Hledger.Data.Journal: nulljournal :: Journal
+ Hledger.Data.Journal: postingsByAccount :: [Posting] -> Map AccountName [Posting]
+ Hledger.Data.Ledger: account :: Ledger -> AccountName -> Account
+ Hledger.Data.Ledger: accountnames :: Ledger -> [AccountName]
+ Hledger.Data.Ledger: accounts :: Ledger -> [Account]
+ Hledger.Data.Ledger: accountsmatching :: [String] -> Ledger -> [Account]
+ Hledger.Data.Ledger: accounttree :: Int -> Ledger -> Tree Account
+ Hledger.Data.Ledger: accounttreeat :: Ledger -> Account -> Maybe (Tree Account)
+ Hledger.Data.Ledger: commodities :: Ledger -> Map String Commodity
+ Hledger.Data.Ledger: instance Show Ledger
+ Hledger.Data.Ledger: journalToLedger :: FilterSpec -> Journal -> Ledger
+ Hledger.Data.Ledger: ledgerAccount :: Ledger -> AccountName -> Account
+ Hledger.Data.Ledger: ledgerAccountNames :: Ledger -> [AccountName]
+ Hledger.Data.Ledger: ledgerAccountTree :: Int -> Ledger -> Tree Account
+ Hledger.Data.Ledger: ledgerAccountTreeAt :: Ledger -> Account -> Maybe (Tree Account)
+ Hledger.Data.Ledger: ledgerAccounts :: Ledger -> [Account]
+ Hledger.Data.Ledger: ledgerAccountsMatching :: [String] -> Ledger -> [Account]
+ Hledger.Data.Ledger: ledgerDateSpan :: Ledger -> DateSpan
+ Hledger.Data.Ledger: ledgerPostings :: Ledger -> [Posting]
+ Hledger.Data.Ledger: ledgerSubAccounts :: Ledger -> Account -> [Account]
+ Hledger.Data.Ledger: ledgerTopAccounts :: Ledger -> [Account]
+ Hledger.Data.Ledger: ledgeramounts :: Ledger -> [MixedAmount]
+ Hledger.Data.Ledger: nullledger :: Ledger
+ Hledger.Data.Ledger: postings :: Ledger -> [Posting]
+ Hledger.Data.Ledger: rawdatespan :: Ledger -> DateSpan
+ Hledger.Data.Ledger: subaccounts :: Ledger -> Account -> [Account]
+ Hledger.Data.Ledger: topaccounts :: Ledger -> [Account]
+ Hledger.Data.Parse: Ctx :: !Maybe Integer -> !Maybe String -> ![String] -> LedgerFileCtx
+ Hledger.Data.Parse: amountquantity :: GenParser Char st (Double, Int, Bool)
+ Hledger.Data.Parse: commoditysymbol :: GenParser Char st String
+ Hledger.Data.Parse: ctxAccount :: LedgerFileCtx -> ![String]
+ Hledger.Data.Parse: ctxCommod :: LedgerFileCtx -> !Maybe String
+ Hledger.Data.Parse: ctxYear :: LedgerFileCtx -> !Maybe Integer
+ Hledger.Data.Parse: data LedgerFileCtx
+ Hledger.Data.Parse: datedisplayexpr :: GenParser Char st (Posting -> Bool)
+ Hledger.Data.Parse: emptyCtx :: LedgerFileCtx
+ Hledger.Data.Parse: emptyLine :: GenParser Char st ()
+ Hledger.Data.Parse: expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath
+ Hledger.Data.Parse: getParentAccount :: GenParser tok LedgerFileCtx String
+ Hledger.Data.Parse: getYear :: GenParser tok LedgerFileCtx (Maybe Integer)
+ Hledger.Data.Parse: instance Read LedgerFileCtx
+ Hledger.Data.Parse: instance Show LedgerFileCtx
+ Hledger.Data.Parse: ledgerAccountBegin :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerAccountEnd :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerCommodityConversion :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerDefaultCommodity :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerDefaultYear :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerEndTagDirective :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerExclamationDirective :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerFile :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerHistoricalPrice :: GenParser Char LedgerFileCtx HistoricalPrice
+ Hledger.Data.Parse: ledgerIgnoredPriceCommodity :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerInclude :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerModifierTransaction :: GenParser Char LedgerFileCtx ModifierTransaction
+ Hledger.Data.Parse: ledgerPeriodicTransaction :: GenParser Char LedgerFileCtx PeriodicTransaction
+ Hledger.Data.Parse: ledgerTagDirective :: GenParser Char LedgerFileCtx JournalUpdate
+ Hledger.Data.Parse: ledgerTransaction :: GenParser Char LedgerFileCtx Transaction
+ Hledger.Data.Parse: ledgeraccountname :: GenParser Char st AccountName
+ Hledger.Data.Parse: ledgercode :: GenParser Char st String
+ Hledger.Data.Parse: ledgercomment :: GenParser Char st String
+ Hledger.Data.Parse: ledgercommentline :: GenParser Char st String
+ Hledger.Data.Parse: ledgerdate :: GenParser Char LedgerFileCtx Day
+ Hledger.Data.Parse: ledgerdatetime :: GenParser Char LedgerFileCtx LocalTime
+ Hledger.Data.Parse: ledgereffectivedate :: Day -> GenParser Char LedgerFileCtx Day
+ Hledger.Data.Parse: ledgerfulldate :: GenParser Char LedgerFileCtx Day
+ Hledger.Data.Parse: ledgerpartialdate :: GenParser Char LedgerFileCtx Day
+ Hledger.Data.Parse: ledgerposting :: GenParser Char LedgerFileCtx Posting
+ Hledger.Data.Parse: ledgerpostings :: GenParser Char LedgerFileCtx [Posting]
+ Hledger.Data.Parse: ledgerstatus :: GenParser Char st Bool
+ Hledger.Data.Parse: leftsymbolamount :: GenParser Char st MixedAmount
+ Hledger.Data.Parse: linebeginningwithspaces :: GenParser Char st String
+ Hledger.Data.Parse: nosymbolamount :: GenParser Char st MixedAmount
+ Hledger.Data.Parse: numberparts :: GenParser Char st (String, String)
+ Hledger.Data.Parse: numberpartsstartingwithdigit :: GenParser Char st (String, String)
+ Hledger.Data.Parse: numberpartsstartingwithpoint :: GenParser Char st (String, String)
+ Hledger.Data.Parse: parseJournal :: FilePath -> String -> ErrorT String IO Journal
+ Hledger.Data.Parse: parseJournalFile :: FilePath -> ErrorT String IO Journal
+ Hledger.Data.Parse: popParentAccount :: GenParser tok LedgerFileCtx ()
+ Hledger.Data.Parse: postingamount :: GenParser Char st MixedAmount
+ Hledger.Data.Parse: priceamount :: GenParser Char st (Maybe MixedAmount)
+ Hledger.Data.Parse: pushParentAccount :: String -> GenParser tok LedgerFileCtx ()
+ Hledger.Data.Parse: quotedcommoditysymbol :: GenParser Char st String
+ Hledger.Data.Parse: rightsymbolamount :: GenParser Char st MixedAmount
+ Hledger.Data.Parse: setYear :: Integer -> GenParser tok LedgerFileCtx ()
+ Hledger.Data.Parse: someamount :: GenParser Char st MixedAmount
+ Hledger.Data.Parse: timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry
+ Hledger.Data.Parse: transactionaccountname :: GenParser Char LedgerFileCtx AccountName
+ Hledger.Data.Parse: type JournalUpdate = ErrorT String IO (Journal -> Journal)
+ Hledger.Data.Posting: accountNamesFromPostings :: [Posting] -> [AccountName]
+ Hledger.Data.Posting: hasAmount :: Posting -> Bool
+ Hledger.Data.Posting: instance Show Posting
+ Hledger.Data.Posting: isBalancedVirtual :: Posting -> Bool
+ Hledger.Data.Posting: isEmptyPosting :: Posting -> Bool
+ Hledger.Data.Posting: isPostingInDateSpan :: DateSpan -> Posting -> Bool
+ Hledger.Data.Posting: isReal :: Posting -> Bool
+ Hledger.Data.Posting: isVirtual :: Posting -> Bool
+ Hledger.Data.Posting: postingCleared :: Posting -> Bool
+ Hledger.Data.Posting: postingDate :: Posting -> Day
+ Hledger.Data.Posting: postingsDateSpan :: [Posting] -> DateSpan
+ Hledger.Data.Posting: showPosting :: Posting -> String
+ Hledger.Data.Posting: sumPostings :: [Posting] -> MixedAmount
+ Hledger.Data.TimeLog: entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction
+ Hledger.Data.TimeLog: instance Read TimeLogCode
+ Hledger.Data.TimeLog: instance Show TimeLogCode
+ Hledger.Data.TimeLog: instance Show TimeLogEntry
+ Hledger.Data.TimeLog: timeLogEntriesToTransactions :: LocalTime -> [TimeLogEntry] -> [Transaction]
+ Hledger.Data.Transaction: balanceTransaction :: Transaction -> Either String Transaction
+ Hledger.Data.Transaction: balancedVirtualPostings :: Transaction -> [Posting]
+ Hledger.Data.Transaction: instance Show ModifierTransaction
+ Hledger.Data.Transaction: instance Show PeriodicTransaction
+ Hledger.Data.Transaction: instance Show Transaction
+ Hledger.Data.Transaction: isTransactionBalanced :: Transaction -> Bool
+ Hledger.Data.Transaction: ledgerTransactionWithDate :: WhichDate -> Transaction -> Transaction
+ Hledger.Data.Transaction: nonzerobalanceerror :: Transaction -> String
+ Hledger.Data.Transaction: nulltransaction :: Transaction
+ Hledger.Data.Transaction: realPostings :: Transaction -> [Posting]
+ Hledger.Data.Transaction: settxn :: Transaction -> Posting -> Posting
+ Hledger.Data.Transaction: showAccountName :: Maybe Int -> PostingType -> AccountName -> String
+ Hledger.Data.Transaction: showTransaction :: Transaction -> String
+ Hledger.Data.Transaction: showTransaction' :: Bool -> Bool -> Transaction -> String
+ Hledger.Data.Transaction: showTransactionForPrint :: Bool -> Transaction -> String
+ Hledger.Data.Transaction: showTransactionUnelided :: Transaction -> String
+ Hledger.Data.Transaction: transactionPostingBalances :: Transaction -> (MixedAmount, MixedAmount, MixedAmount)
+ Hledger.Data.Transaction: txnTieKnot :: Transaction -> Transaction
+ Hledger.Data.Transaction: virtualPostings :: Transaction -> [Posting]
+ Hledger.Data.Types: Account :: AccountName -> [Posting] -> MixedAmount -> Account
+ Hledger.Data.Types: ActualDate :: WhichDate
+ Hledger.Data.Types: Amount :: Commodity -> Double -> Maybe MixedAmount -> Amount
+ Hledger.Data.Types: BalancedVirtualPosting :: PostingType
+ Hledger.Data.Types: Commodity :: String -> Side -> Bool -> Bool -> Int -> Commodity
+ Hledger.Data.Types: Daily :: Interval
+ Hledger.Data.Types: DateSpan :: (Maybe Day) -> (Maybe Day) -> DateSpan
+ Hledger.Data.Types: EffectiveDate :: WhichDate
+ Hledger.Data.Types: FilterSpec :: DateSpan -> Maybe Bool -> Bool -> Bool -> Bool -> [String] -> [String] -> WhichDate -> Maybe Int -> FilterSpec
+ Hledger.Data.Types: FinalOut :: TimeLogCode
+ Hledger.Data.Types: HistoricalPrice :: Day -> String -> MixedAmount -> HistoricalPrice
+ Hledger.Data.Types: In :: TimeLogCode
+ Hledger.Data.Types: Journal :: [ModifierTransaction] -> [PeriodicTransaction] -> [Transaction] -> [TimeLogEntry] -> [HistoricalPrice] -> String -> FilePath -> ClockTime -> String -> Journal
+ Hledger.Data.Types: L :: Side
+ Hledger.Data.Types: Ledger :: Journal -> Tree AccountName -> Map AccountName Account -> Ledger
+ Hledger.Data.Types: Mixed :: [Amount] -> MixedAmount
+ Hledger.Data.Types: ModifierTransaction :: String -> [Posting] -> ModifierTransaction
+ Hledger.Data.Types: Monthly :: Interval
+ Hledger.Data.Types: NoInterval :: Interval
+ Hledger.Data.Types: Out :: TimeLogCode
+ Hledger.Data.Types: PeriodicTransaction :: String -> [Posting] -> PeriodicTransaction
+ Hledger.Data.Types: Posting :: Bool -> AccountName -> MixedAmount -> String -> PostingType -> Maybe Transaction -> Posting
+ Hledger.Data.Types: Quarterly :: Interval
+ Hledger.Data.Types: R :: Side
+ Hledger.Data.Types: RegularPosting :: PostingType
+ Hledger.Data.Types: SetBalance :: TimeLogCode
+ Hledger.Data.Types: SetRequiredHours :: TimeLogCode
+ Hledger.Data.Types: TimeLogEntry :: TimeLogCode -> LocalTime -> String -> TimeLogEntry
+ Hledger.Data.Types: Transaction :: Day -> Maybe Day -> Bool -> String -> String -> String -> [Posting] -> String -> Transaction
+ Hledger.Data.Types: VirtualPosting :: PostingType
+ Hledger.Data.Types: Weekly :: Interval
+ Hledger.Data.Types: Yearly :: Interval
+ Hledger.Data.Types: abalance :: Account -> MixedAmount
+ Hledger.Data.Types: accountmap :: Ledger -> Map AccountName Account
+ Hledger.Data.Types: accountnametree :: Ledger -> Tree AccountName
+ Hledger.Data.Types: acctpats :: FilterSpec -> [String]
+ Hledger.Data.Types: aname :: Account -> AccountName
+ Hledger.Data.Types: apostings :: Account -> [Posting]
+ Hledger.Data.Types: cleared :: FilterSpec -> Maybe Bool
+ Hledger.Data.Types: comma :: Commodity -> Bool
+ Hledger.Data.Types: commodity :: Amount -> Commodity
+ Hledger.Data.Types: costbasis :: FilterSpec -> Bool
+ Hledger.Data.Types: data Account
+ Hledger.Data.Types: data Amount
+ Hledger.Data.Types: data Commodity
+ Hledger.Data.Types: data DateSpan
+ Hledger.Data.Types: data FilterSpec
+ Hledger.Data.Types: data HistoricalPrice
+ Hledger.Data.Types: data Interval
+ Hledger.Data.Types: data Journal
+ Hledger.Data.Types: data Ledger
+ Hledger.Data.Types: data ModifierTransaction
+ Hledger.Data.Types: data PeriodicTransaction
+ Hledger.Data.Types: data Posting
+ Hledger.Data.Types: data PostingType
+ Hledger.Data.Types: data Side
+ Hledger.Data.Types: data TimeLogCode
+ Hledger.Data.Types: data TimeLogEntry
+ Hledger.Data.Types: data Transaction
+ Hledger.Data.Types: data WhichDate
+ Hledger.Data.Types: datespan :: FilterSpec -> DateSpan
+ Hledger.Data.Types: depth :: FilterSpec -> Maybe Int
+ Hledger.Data.Types: descpats :: FilterSpec -> [String]
+ Hledger.Data.Types: empty :: FilterSpec -> Bool
+ Hledger.Data.Types: filepath :: Journal -> FilePath
+ Hledger.Data.Types: filereadtime :: Journal -> ClockTime
+ Hledger.Data.Types: final_comment_lines :: Journal -> String
+ Hledger.Data.Types: hamount :: HistoricalPrice -> MixedAmount
+ Hledger.Data.Types: hdate :: HistoricalPrice -> Day
+ Hledger.Data.Types: historical_prices :: Journal -> [HistoricalPrice]
+ Hledger.Data.Types: hsymbol :: HistoricalPrice -> String
+ Hledger.Data.Types: instance Eq Amount
+ Hledger.Data.Types: instance Eq Commodity
+ Hledger.Data.Types: instance Eq DateSpan
+ Hledger.Data.Types: instance Eq HistoricalPrice
+ Hledger.Data.Types: instance Eq Interval
+ Hledger.Data.Types: instance Eq Journal
+ Hledger.Data.Types: instance Eq MixedAmount
+ Hledger.Data.Types: instance Eq ModifierTransaction
+ Hledger.Data.Types: instance Eq PeriodicTransaction
+ Hledger.Data.Types: instance Eq Posting
+ Hledger.Data.Types: instance Eq PostingType
+ Hledger.Data.Types: instance Eq Side
+ Hledger.Data.Types: instance Eq TimeLogCode
+ Hledger.Data.Types: instance Eq TimeLogEntry
+ Hledger.Data.Types: instance Eq Transaction
+ Hledger.Data.Types: instance Eq WhichDate
+ Hledger.Data.Types: instance Ord Commodity
+ Hledger.Data.Types: instance Ord DateSpan
+ Hledger.Data.Types: instance Ord Interval
+ Hledger.Data.Types: instance Ord Side
+ Hledger.Data.Types: instance Ord TimeLogCode
+ Hledger.Data.Types: instance Ord TimeLogEntry
+ Hledger.Data.Types: instance Show Commodity
+ Hledger.Data.Types: instance Show DateSpan
+ Hledger.Data.Types: instance Show FilterSpec
+ Hledger.Data.Types: instance Show Interval
+ Hledger.Data.Types: instance Show PostingType
+ Hledger.Data.Types: instance Show Side
+ Hledger.Data.Types: instance Show WhichDate
+ Hledger.Data.Types: instance Typeable Journal
+ Hledger.Data.Types: jmodifiertxns :: Journal -> [ModifierTransaction]
+ Hledger.Data.Types: journal :: Ledger -> Journal
+ Hledger.Data.Types: jperiodictxns :: Journal -> [PeriodicTransaction]
+ Hledger.Data.Types: jtext :: Journal -> String
+ Hledger.Data.Types: jtxns :: Journal -> [Transaction]
+ Hledger.Data.Types: mtpostings :: ModifierTransaction -> [Posting]
+ Hledger.Data.Types: mtvalueexpr :: ModifierTransaction -> String
+ Hledger.Data.Types: newtype MixedAmount
+ Hledger.Data.Types: open_timelog_entries :: Journal -> [TimeLogEntry]
+ Hledger.Data.Types: paccount :: Posting -> AccountName
+ Hledger.Data.Types: pamount :: Posting -> MixedAmount
+ Hledger.Data.Types: pcomment :: Posting -> String
+ Hledger.Data.Types: precision :: Commodity -> Int
+ Hledger.Data.Types: price :: Amount -> Maybe MixedAmount
+ Hledger.Data.Types: pstatus :: Posting -> Bool
+ Hledger.Data.Types: ptperiodicexpr :: PeriodicTransaction -> String
+ Hledger.Data.Types: ptpostings :: PeriodicTransaction -> [Posting]
+ Hledger.Data.Types: ptransaction :: Posting -> Maybe Transaction
+ Hledger.Data.Types: ptype :: Posting -> PostingType
+ Hledger.Data.Types: quantity :: Amount -> Double
+ Hledger.Data.Types: real :: FilterSpec -> Bool
+ Hledger.Data.Types: side :: Commodity -> Side
+ Hledger.Data.Types: spaced :: Commodity -> Bool
+ Hledger.Data.Types: symbol :: Commodity -> String
+ Hledger.Data.Types: tcode :: Transaction -> String
+ Hledger.Data.Types: tcomment :: Transaction -> String
+ Hledger.Data.Types: tdate :: Transaction -> Day
+ Hledger.Data.Types: tdescription :: Transaction -> String
+ Hledger.Data.Types: teffectivedate :: Transaction -> Maybe Day
+ Hledger.Data.Types: tlcode :: TimeLogEntry -> TimeLogCode
+ Hledger.Data.Types: tlcomment :: TimeLogEntry -> String
+ Hledger.Data.Types: tldatetime :: TimeLogEntry -> LocalTime
+ Hledger.Data.Types: tpostings :: Transaction -> [Posting]
+ Hledger.Data.Types: tpreceding_comment_lines :: Transaction -> String
+ Hledger.Data.Types: tstatus :: Transaction -> Bool
+ Hledger.Data.Types: type AccountName = String
+ Hledger.Data.Types: type SmartDate = (String, String, String)
+ Hledger.Data.Types: whichdate :: FilterSpec -> WhichDate
+ Hledger.Data.Utils: assertParse :: (Either ParseError a) -> Assertion
+ Hledger.Data.Utils: assertParseEqual :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion
+ Hledger.Data.Utils: cliptopleft :: Int -> Int -> String -> String
+ Hledger.Data.Utils: concatBottomPadded :: [String] -> String
+ Hledger.Data.Utils: concatTopPadded :: [String] -> String
+ Hledger.Data.Utils: containsRegex :: String -> String -> Bool
+ Hledger.Data.Utils: difforzero :: (Num a, Ord a) => a -> a -> a
+ Hledger.Data.Utils: fitto :: Int -> Int -> String -> String
+ Hledger.Data.Utils: fromparse :: Either ParseError a -> a
+ Hledger.Data.Utils: getCurrentLocalTime :: IO LocalTime
+ Hledger.Data.Utils: is :: (Eq a, Show a) => a -> a -> Assertion
+ Hledger.Data.Utils: isLeft :: Either a b -> Bool
+ Hledger.Data.Utils: isRight :: Either a b -> Bool
+ Hledger.Data.Utils: leaves :: Tree a -> [a]
+ Hledger.Data.Utils: ltrace :: (Show a) => String -> a -> a
+ Hledger.Data.Utils: nonspace :: GenParser Char st Char
+ Hledger.Data.Utils: padbottom :: Int -> String -> String
+ Hledger.Data.Utils: padleft :: Int -> String -> String
+ Hledger.Data.Utils: padright :: Int -> String -> String
+ Hledger.Data.Utils: padtop :: Int -> String -> String
+ Hledger.Data.Utils: parseWithCtx :: b -> GenParser Char b a -> String -> Either ParseError a
+ Hledger.Data.Utils: parsewith :: Parser a -> String -> Either ParseError a
+ Hledger.Data.Utils: printParseError :: (Show a) => a -> IO ()
+ Hledger.Data.Utils: restofline :: GenParser Char st String
+ Hledger.Data.Utils: showforest :: (Show a) => Forest a -> String
+ Hledger.Data.Utils: showtree :: (Show a) => Tree a -> String
+ Hledger.Data.Utils: spacenonewline :: GenParser Char st Char
+ Hledger.Data.Utils: splitAtElement :: (Eq a) => a -> [a] -> [[a]]
+ Hledger.Data.Utils: strace :: (Show a) => a -> a
+ Hledger.Data.Utils: strictReadFile :: FilePath -> IO String
+ Hledger.Data.Utils: subtreeat :: (Eq a) => a -> Tree a -> Maybe (Tree a)
+ Hledger.Data.Utils: subtreeinforest :: (Eq a) => a -> [Tree a] -> Maybe (Tree a)
+ Hledger.Data.Utils: tfilter :: (Test -> Bool) -> Test -> Test
+ Hledger.Data.Utils: tflatten :: Test -> [Test]
+ Hledger.Data.Utils: tname :: Test -> String
+ Hledger.Data.Utils: treeany :: (a -> Bool) -> Tree a -> Bool
+ Hledger.Data.Utils: treefilter :: (a -> Bool) -> Tree a -> Tree a
+ Hledger.Data.Utils: treemap :: (a -> b) -> Tree a -> Tree b
+ Hledger.Data.Utils: treeprune :: Int -> Tree a -> Tree a
+ Hledger.Data.Utils: unbracket :: String -> String
+ Hledger.Data.Utils: underline :: String -> String
+ Hledger.Data.Utils: vConcatRightAligned :: [String] -> String

Files

+ Hledger/Data.hs view
@@ -0,0 +1,58 @@+{-| ++The Ledger library allows parsing and querying of ledger files.  It+generally provides a compatible subset of C++ ledger's functionality.+This package re-exports all the Ledger.* modules.++-}++module Hledger.Data (+               module Hledger.Data.Account,+               module Hledger.Data.AccountName,+               module Hledger.Data.Amount,+               module Hledger.Data.Commodity,+               module Hledger.Data.Dates,+               module Hledger.Data.IO,+               module Hledger.Data.Transaction,+               module Hledger.Data.Ledger,+               module Hledger.Data.Parse,+               module Hledger.Data.Journal,+               module Hledger.Data.Posting,+               module Hledger.Data.TimeLog,+               module Hledger.Data.Types,+               module Hledger.Data.Utils,+               tests_Hledger_Data+              )+where+import Hledger.Data.Account+import Hledger.Data.AccountName+import Hledger.Data.Amount+import Hledger.Data.Commodity+import Hledger.Data.Dates+import Hledger.Data.IO+import Hledger.Data.Transaction+import Hledger.Data.Ledger+import Hledger.Data.Parse+import Hledger.Data.Journal+import Hledger.Data.Posting+import Hledger.Data.TimeLog+import Hledger.Data.Types+import Hledger.Data.Utils++tests_Hledger_Data = TestList+    [+    --  Hledger.Data.Account.tests_Account+    -- ,Hledger.Data.AccountName.tests_AccountName+     Hledger.Data.Amount.tests_Amount+    -- ,Hledger.Data.Commodity.tests_Commodity+    ,Hledger.Data.Dates.tests_Dates+    -- ,Hledger.Data.IO.tests_IO+    ,Hledger.Data.Transaction.tests_Transaction+    -- ,Hledger.Data.Hledger.Data.tests_Hledger.Data+    ,Hledger.Data.Parse.tests_Parse+    -- ,Hledger.Data.Journal.tests_Journal+    -- ,Hledger.Data.Posting.tests_Posting+    ,Hledger.Data.TimeLog.tests_TimeLog+    -- ,Hledger.Data.Types.tests_Types+    -- ,Hledger.Data.Utils.tests_Utils+    ]
+ Hledger/Data/Account.hs view
@@ -0,0 +1,27 @@+{-|++A compound data type for efficiency. An 'Account' stores++- an 'AccountName',++- all 'Posting's in the account, excluding subaccounts++- a 'MixedAmount' representing the account balance, including subaccounts.++-}++module Hledger.Data.Account+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Amount+++instance Show Account where+    show (Account a ts b) = printf "Account %s with %d txns and %s balance" a (length ts) (showMixedAmount b)++instance Eq Account where+    (==) (Account n1 t1 b1) (Account n2 t2 b2) = n1 == n2 && t1 == t2 && b1 == b2++nullacct = Account "" [] nullmixedamt+
+ Hledger/Data/AccountName.hs view
@@ -0,0 +1,171 @@+{-# LANGUAGE NoMonomorphismRestriction#-}+{-|++'AccountName's are strings like @assets:cash:petty@.+From a set of these we derive the account hierarchy.++-}++module Hledger.Data.AccountName+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Data.Map (Map)+import qualified Data.Map as M++++-- change to use a different separator for nested accounts+acctsepchar = ':'++accountNameComponents :: AccountName -> [String]+accountNameComponents = splitAtElement acctsepchar++accountNameFromComponents :: [String] -> AccountName+accountNameFromComponents = concat . intersperse [acctsepchar]++accountLeafName :: AccountName -> String+accountLeafName = last . accountNameComponents++accountNameLevel :: AccountName -> Int+accountNameLevel "" = 0+accountNameLevel a = length (filter (==acctsepchar) a) + 1++-- | ["a:b:c","d:e"] -> ["a","a:b","a:b:c","d","d:e"]+expandAccountNames :: [AccountName] -> [AccountName]+expandAccountNames as = nub $ concatMap expand as+    where expand = map accountNameFromComponents . tail . inits . accountNameComponents++-- | ["a:b:c","d:e"] -> ["a","d"]+topAccountNames :: [AccountName] -> [AccountName]+topAccountNames as = [a | a <- expandAccountNames as, accountNameLevel a == 1]++parentAccountName :: AccountName -> AccountName+parentAccountName = accountNameFromComponents . init . accountNameComponents++parentAccountNames :: AccountName -> [AccountName]+parentAccountNames a = parentAccountNames' $ parentAccountName a+    where+      parentAccountNames' "" = []+      parentAccountNames' a = a : parentAccountNames' (parentAccountName a)++isAccountNamePrefixOf :: AccountName -> AccountName -> Bool+isAccountNamePrefixOf = isPrefixOf . (++ [acctsepchar])++isSubAccountNameOf :: AccountName -> AccountName -> Bool+s `isSubAccountNameOf` p = +    (p `isAccountNamePrefixOf` s) && (accountNameLevel s == (accountNameLevel p + 1))++-- | From a list of account names, select those which are direct+-- subaccounts of the given account name.+subAccountNamesFrom :: [AccountName] -> AccountName -> [AccountName]+subAccountNamesFrom accts a = filter (`isSubAccountNameOf` a) accts++-- | Convert a list of account names to a tree.+accountNameTreeFrom :: [AccountName] -> Tree AccountName+accountNameTreeFrom = accountNameTreeFrom1++accountNameTreeFrom1 accts = +    Node "top" (accounttreesfrom (topAccountNames accts))+        where+          accounttreesfrom :: [AccountName] -> [Tree AccountName]+          accounttreesfrom [] = []+          accounttreesfrom as = [Node a (accounttreesfrom $ subs a) | a <- as]+          subs = subAccountNamesFrom (expandAccountNames accts)++nullaccountnametree = Node "top" []++accountNameTreeFrom2 accts = +   Node "top" $ unfoldForest (\a -> (a, subs a)) $ topAccountNames accts+        where+          subs = subAccountNamesFrom allaccts+          allaccts = expandAccountNames accts+          -- subs' a = subsmap ! a+          -- subsmap :: Map AccountName [AccountName]+          -- subsmap = Data.Map.fromList [(a, subAccountNamesFrom allaccts a) | a <- allaccts]++accountNameTreeFrom3 accts = +    Node "top" $ forestfrom allaccts $ topAccountNames accts+        where+          -- drop accts from the list of potential subs as we add them to the tree+          forestfrom :: [AccountName] -> [AccountName] -> Forest AccountName+          forestfrom subaccts accts = +              [let subaccts' = subaccts \\ accts in Node a $ forestfrom subaccts' (subAccountNamesFrom subaccts' a) | a <- accts]+          allaccts = expandAccountNames accts+          ++-- a more efficient tree builder from Cale Gibbard+newtype Tree' a = T (Map a (Tree' a))+  deriving (Show, Eq, Ord)++mergeTrees :: (Ord a) => Tree' a -> Tree' a -> Tree' a+mergeTrees (T m) (T m') = T (M.unionWith mergeTrees m m')++emptyTree = T M.empty++pathtree :: [a] -> Tree' a+pathtree []     = T M.empty+pathtree (x:xs) = T (M.singleton x (pathtree xs))++fromPaths :: (Ord a) => [[a]] -> Tree' a+fromPaths = foldl' mergeTrees emptyTree . map pathtree++-- the above, but trying to build Tree directly++-- mergeTrees' :: (Ord a) => Tree a -> Tree a -> Tree a+-- mergeTrees' (Node m ms) (Node m' ms') = Node undefined (ms `union` ms')++-- emptyTree' = Node "top" []++-- pathtree' :: [a] -> Tree a+-- pathtree' []     = Node undefined []+-- pathtree' (x:xs) = Node x [pathtree' xs]++-- fromPaths' :: (Ord a) => [[a]] -> Tree a+-- fromPaths' = foldl' mergeTrees' emptyTree' . map pathtree'+++-- converttree :: [AccountName] -> Tree' AccountName -> [Tree AccountName]+-- converttree parents (T m) = [Node (accountNameFromComponents $ parents ++ [a]) (converttree (parents++[a]) b) | (a,b) <- M.toList m]++-- accountNameTreeFrom4 :: [AccountName] -> Tree AccountName+-- accountNameTreeFrom4 accts = Node "top" (converttree [] $ fromPaths $ map accountNameComponents accts)++converttree :: Tree' AccountName -> [Tree AccountName]+converttree (T m) = [Node a (converttree b) | (a,b) <- M.toList m]++expandTreeNames :: Tree AccountName -> Tree AccountName+expandTreeNames (Node x ts) = Node x (map (treemap (\n -> accountNameFromComponents [x,n]) . expandTreeNames) ts)++accountNameTreeFrom4 :: [AccountName] -> Tree AccountName+accountNameTreeFrom4 = Node "top" . map expandTreeNames . converttree . fromPaths . map accountNameComponents+++-- | 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+--   letters in length.  If this results in a string that is still too+--   long, the front will be elided -- not the end.  For example:+--+--     Expenses:Cash           ; OK, not too long+--     Ex:Wednesday:Cash       ; "Expenses" was abbreviated to fit+--     Ex:We:Afternoon:Cash    ; "Expenses" and "Wednesday" abbreviated+--     ; Expenses:Wednesday:Afternoon:Lunch:Snack:Candy:Chocolate:Cash+--     ..:Af:Lu:Sn:Ca:Ch:Cash  ; Abbreviated and elided!+-- @+elideAccountName :: Int -> AccountName -> AccountName+elideAccountName width s = +    elideLeft width $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s+      where+        elideparts :: Int -> [String] -> [String] -> [String]+        elideparts width done ss+          | length (accountNameFromComponents $ done++ss) <= width = done++ss+          | length ss > 1 = elideparts width (done++[take 2 $ head ss]) (tail ss)+          | otherwise = done++ss++clipAccountName :: Int -> AccountName -> AccountName+clipAccountName n = accountNameFromComponents . take n . accountNameComponents+
+ Hledger/Data/Amount.hs view
@@ -0,0 +1,340 @@+{-# LANGUAGE StandaloneDeriving #-}+{-|+An 'Amount' is some quantity of money, shares, or anything else.++A simple amount is a 'Commodity', quantity pair:++@+  $1 +  £-50+  EUR 3.44 +  GOOG 500+  1.5h+  90 apples+  0 +@++An amount may also have a per-unit price, or conversion rate, in terms+of some other commodity. If present, this is displayed after \@:++@+  EUR 3 \@ $1.35+@++A 'MixedAmount' is zero or more simple amounts.  Mixed amounts are+usually normalised so that there is no more than one amount in each+commodity, and no zero amounts (or, there is just a single zero amount+and no others.):++@+  $50 + EUR 3+  16h + $13.55 + AAPL 500 + 6 oranges+  0+@++We can do limited arithmetic with simple or mixed amounts: either+price-preserving arithmetic with similarly-priced amounts, or+price-discarding arithmetic which ignores and discards prices.++-}++module Hledger.Data.Amount+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Commodity+++instance Show Amount where show = showAmount+instance Show MixedAmount where show = showMixedAmount+deriving instance Show HistoricalPrice++instance Num Amount where+    abs (Amount c q p) = Amount c (abs q) p+    signum (Amount c q p) = Amount c (signum q) p+    fromInteger i = Amount (comm "") (fromInteger i) Nothing+    (+) = amountop (+)+    (-) = amountop (-)+    (*) = amountop (*)++instance Ord Amount where+    compare (Amount ac aq ap) (Amount bc bq bp) = compare (ac,aq,ap) (bc,bq,bp)++instance Num MixedAmount where+    fromInteger i = Mixed [Amount (comm "") (fromInteger i) Nothing]+    negate (Mixed as) = Mixed $ map negateAmountPreservingPrice as+    (+) (Mixed as) (Mixed bs) = normaliseMixedAmount $ 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"++instance Ord MixedAmount where+    compare (Mixed as) (Mixed bs) = compare as bs++negateAmountPreservingPrice a = (-a){price=price a}++-- | Apply a binary arithmetic operator to two amounts, converting to the+-- second one's commodity (and display precision), discarding any price+-- information. (Using the second commodity is best since sum and other+-- folds start with a no-commodity amount.)+amountop :: (Double -> Double -> Double) -> Amount -> Amount -> Amount+amountop op a@(Amount _ _ _) (Amount bc bq _) = +    Amount bc (quantity (convertAmountTo bc a) `op` bq) Nothing++-- | Convert an amount to the specified commodity using the appropriate+-- exchange rate (which is currently always 1).+convertAmountTo :: Commodity -> Amount -> Amount+convertAmountTo c2 (Amount c1 q _) = Amount c2 (q * conversionRate c1 c2) Nothing++-- | Convert mixed amount to the specified commodity+convertMixedAmountTo :: Commodity -> MixedAmount -> Amount+convertMixedAmountTo c2 (Mixed ams) = Amount c2 total Nothing+    where+    total = sum . map (quantity . convertAmountTo c2) $ ams++-- | Convert an amount to the commodity of its saved price, if any.+costOfAmount :: Amount -> Amount+costOfAmount a@(Amount _ _ Nothing) = a+costOfAmount (Amount _ q (Just price))+    | isZeroMixedAmount price = nullamt+    | otherwise = Amount pc (pq*q) Nothing+    where (Amount pc pq _) = head $ amounts price++-- | Get the string representation of an amount, based on its commodity's+-- display settings.+showAmount :: Amount -> String+showAmount (Amount (Commodity {symbol="AUTO"}) _ _) = "" -- can appear in an error message+showAmount a@(Amount (Commodity {symbol=sym,side=side,spaced=spaced}) _ pri) =+    case side of+      L -> printf "%s%s%s%s" sym space quantity price+      R -> printf "%s%s%s%s" quantity space sym price+    where +      space = if spaced then " " else ""+      quantity = showAmount' a+      price = case pri of (Just pamt) -> " @ " ++ showMixedAmount pamt+                          Nothing -> ""++-- XXX refactor+-- | Get the unambiguous string representation of an amount, for debugging.+showAmountDebug :: Amount -> String+showAmountDebug (Amount c q pri) = printf "Amount {commodity = %s, quantity = %s, price = %s}"+                                   (show c) (show q) (maybe "" showMixedAmountDebug pri)++-- | Get the string representation of an amount, without any \@ price.+showAmountWithoutPrice :: Amount -> String+showAmountWithoutPrice a = showAmount a{price=Nothing}++-- | Get the string representation (of the number part of) of an amount+showAmount' :: Amount -> String+showAmount' (Amount (Commodity {comma=comma,precision=p}) q _) = quantity+  where+    quantity = commad $ printf ("%."++show p++"f") q+    commad = if comma then punctuatethousands else id++-- | Add thousands-separating commas to a decimal number string+punctuatethousands :: String -> String+punctuatethousands s =+    sign ++ addcommas int ++ frac+    where +      (sign,num) = break isDigit s+      (int,frac) = break (=='.') num+      addcommas = reverse . concat . intersperse "," . triples . reverse+      triples [] = []+      triples l  = take 3 l : triples (drop 3 l)++-- | Does this amount appear to be zero when displayed with its given precision ?+isZeroAmount :: Amount -> Bool+isZeroAmount = null . filter (`elem` "123456789") . showAmountWithoutPrice++-- | 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 = null . filter (`elem` "123456789") . printf "%.10f" . quantity++-- | Access a mixed amount's components.+amounts :: MixedAmount -> [Amount]+amounts (Mixed as) = as++-- | Does this mixed amount appear to be zero - empty, or+-- containing only simple amounts which appear to be zero ?+isZeroMixedAmount :: MixedAmount -> Bool+isZeroMixedAmount = all isZeroAmount . amounts . normaliseMixedAmount++-- | Is this mixed amount "really" zero ? See isReallyZeroAmount.+isReallyZeroMixedAmount :: MixedAmount -> Bool+isReallyZeroMixedAmount = all isReallyZeroAmount . amounts . normaliseMixedAmount++-- | Is this mixed amount "really" zero, after converting to cost+-- commodities where possible ?+isReallyZeroMixedAmountCost :: MixedAmount -> Bool+isReallyZeroMixedAmountCost = isReallyZeroMixedAmount . costOfMixedAmount++-- | MixedAmount derives Eq in Types.hs, but that doesn't know that we+-- want $0 = EUR0 = 0. Yet we don't want to drag all this code in there.+-- When zero equality is important, use this, for now; should be used+-- everywhere.+mixedAmountEquals :: MixedAmount -> MixedAmount -> Bool+mixedAmountEquals a b = amounts a' == amounts b' || (isZeroMixedAmount a' && isZeroMixedAmount b')+    where a' = normaliseMixedAmount a+          b' = normaliseMixedAmount 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 \"\".+showMixedAmount :: MixedAmount -> String+showMixedAmount m = vConcatRightAligned $ map show $ amounts $ normaliseMixedAmount m++-- | Get an unambiguous string representation of a mixed amount for debugging.+showMixedAmountDebug :: MixedAmount -> String+showMixedAmountDebug m = printf "Mixed [%s]" as+    where as = intercalate "\n       " $ map showAmountDebug $ amounts $ normaliseMixedAmount 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) = normaliseMixedAmountIgnoringPrice m+      width = maximum $ map (length . show) as+      showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice++-- | Get the string representation of a mixed amount, and if it+-- appears to be all zero just show a bare 0, ledger-style.+showMixedAmountOrZero :: MixedAmount -> String+showMixedAmountOrZero a | a == missingamt = ""+                        | isZeroMixedAmount a = "0"+                        | otherwise = showMixedAmount a++-- | Get the string representation of a mixed amount, or a bare 0,+-- without any \@ prices.+showMixedAmountOrZeroWithoutPrice :: MixedAmount -> String+showMixedAmountOrZeroWithoutPrice a+    | isZeroMixedAmount a = "0"+    | otherwise = showMixedAmountWithoutPrice a++-- | Simplify a mixed amount by combining any component amounts which have+-- the same commodity and the same price. Also removes zero amounts,+-- or adds a single zero amount if there are no amounts at all.+normaliseMixedAmount :: MixedAmount -> MixedAmount+normaliseMixedAmount (Mixed as) = Mixed as''+    where +      as'' = map sumSamePricedAmountsPreservingPrice $ group $ sort as'+      sort = sortBy cmpsymbolandprice+      cmpsymbolandprice a1 a2 = compare (sym a1,price a1) (sym a2,price a2)+      group = groupBy samesymbolandprice +      samesymbolandprice a1 a2 = (sym a1 == sym a2) && (price a1 == price a2)+      sym = symbol . commodity+      as' | null nonzeros = [head $ zeros ++ [nullamt]]+          | otherwise = nonzeros+      (zeros,nonzeros) = partition isReallyZeroAmount as++-- various sum variants..++sumAmountsDiscardingPrice [] = nullamt+sumAmountsDiscardingPrice as = (sum as){price=Nothing}++sumSamePricedAmountsPreservingPrice [] = nullamt+sumSamePricedAmountsPreservingPrice as = (sum as){price=price $ head as}++-- | Simplify a mixed amount by combining any component amounts which have+-- the same commodity, ignoring and discarding their unit prices if any.+-- Also removes zero amounts, or adds a single zero amount if there are no+-- amounts at all.+normaliseMixedAmountIgnoringPrice :: MixedAmount -> MixedAmount+normaliseMixedAmountIgnoringPrice (Mixed as) = Mixed as''+    where+      as'' = map sumAmountsDiscardingPrice $ group $ sort as'+      group = groupBy samesymbol where samesymbol a1 a2 = sym a1 == sym a2+      sort = sortBy (comparing sym)+      sym = symbol . commodity+      as' | null nonzeros = [head $ zeros ++ [nullamt]]+          | otherwise = nonzeros+          where (zeros,nonzeros) = partition isZeroAmount as++sumMixedAmountsPreservingHighestPrecision :: [MixedAmount] -> MixedAmount+sumMixedAmountsPreservingHighestPrecision ms = foldl' (+~) 0 ms+    where (+~) (Mixed as) (Mixed bs) = normaliseMixedAmountPreservingHighestPrecision $ Mixed $ as ++ bs++normaliseMixedAmountPreservingHighestPrecision :: MixedAmount -> MixedAmount+normaliseMixedAmountPreservingHighestPrecision (Mixed as) = Mixed as''+    where+      as'' = map sumSamePricedAmountsPreservingPriceAndHighestPrecision $ group $ sort as'+      sort = sortBy cmpsymbolandprice+      cmpsymbolandprice a1 a2 = compare (sym a1,price a1) (sym a2,price a2)+      group = groupBy samesymbolandprice+      samesymbolandprice a1 a2 = (sym a1 == sym a2) && (price a1 == price a2)+      sym = symbol . commodity+      as' | null nonzeros = [head $ zeros ++ [nullamt]]+          | otherwise = nonzeros+      (zeros,nonzeros) = partition isReallyZeroAmount as++sumSamePricedAmountsPreservingPriceAndHighestPrecision [] = nullamt+sumSamePricedAmountsPreservingPriceAndHighestPrecision as = (sumAmountsPreservingHighestPrecision as){price=price $ head as}++sumAmountsPreservingHighestPrecision :: [Amount] -> Amount+sumAmountsPreservingHighestPrecision as = foldl' (+~) 0 as+    where (+~) = amountopPreservingHighestPrecision (+)++amountopPreservingHighestPrecision :: (Double -> Double -> Double) -> Amount -> Amount -> Amount+amountopPreservingHighestPrecision op a@(Amount ac@Commodity{precision=ap} _ _) (Amount bc@Commodity{precision=bp} bq _) = +    Amount c q Nothing+    where+      q = quantity (convertAmountTo bc a) `op` bq+      c = if ap > bp then ac else bc+--++-- | Convert a mixed amount's component amounts to the commodity of their+-- saved price, if any.+costOfMixedAmount :: MixedAmount -> MixedAmount+costOfMixedAmount (Mixed as) = Mixed $ map costOfAmount as++-- | The empty simple amount.+nullamt :: Amount+nullamt = Amount unknown 0 Nothing++-- | The empty mixed amount.+nullmixedamt :: MixedAmount+nullmixedamt = Mixed []++-- | A temporary value for parsed transactions which had no amount specified.+missingamt :: MixedAmount+missingamt = Mixed [Amount Commodity {symbol="AUTO",side=L,spaced=False,comma=False,precision=0} 0 Nothing]+++tests_Amount = TestList [++   "showMixedAmount" ~: do+     showMixedAmount (Mixed [Amount dollar 0 Nothing]) `is` "$0.00"+     showMixedAmount (Mixed []) `is` "0"+     showMixedAmount missingamt `is` ""++  ,"showMixedAmountOrZero" ~: do+     showMixedAmountOrZero (Mixed [Amount dollar 0 Nothing]) `is` "0"+     showMixedAmountOrZero (Mixed []) `is` "0"+     showMixedAmountOrZero missingamt `is` ""++  ,"amount arithmetic" ~: do+    let a1 = dollars 1.23+    let a2 = Amount (comm "$") (-1.23) Nothing+    let a3 = Amount (comm "$") (-1.23) Nothing+    (a1 + a2) `is` Amount (comm "$") 0 Nothing+    (a1 + a3) `is` Amount (comm "$") 0 Nothing+    (a2 + a3) `is` Amount (comm "$") (-2.46) Nothing+    (a3 + a3) `is` Amount (comm "$") (-2.46) Nothing+    sum [a2,a3] `is` Amount (comm "$") (-2.46) Nothing+    sum [a3,a3] `is` Amount (comm "$") (-2.46) Nothing+    sum [a1,a2,a3,-a3] `is` Amount (comm "$") 0 Nothing+    let dollar0 = dollar{precision=0}+    (sum [Amount dollar 1.25 Nothing, Amount dollar0 (-1) Nothing, Amount dollar (-0.25) Nothing])+      `is` (Amount dollar 0 Nothing)++  ,"mixed amount arithmetic" ~: do+    let dollar0 = dollar{precision=0}+    (sum $ map (Mixed . (\a -> [a]))+             [Amount dollar 1.25 Nothing,+              Amount dollar0 (-1) Nothing,+              Amount dollar (-0.25) Nothing])+      `is` Mixed [Amount dollar 0 Nothing]+++  ]
+ Hledger/Data/Commodity.hs view
@@ -0,0 +1,39 @@+{-|++A 'Commodity' is a symbol representing a currency or some other kind of+thing we are tracking, and some display preferences that tell how to+display 'Amount's of the commodity - is the symbol on the left or right,+are thousands separated by comma, significant decimal places and so on.++-}+module Hledger.Data.Commodity+where+import Hledger.Data.Utils+import Hledger.Data.Types+++-- convenient amount and commodity constructors, for tests etc.++unknown = Commodity {symbol="",   side=L,spaced=False,comma=False,precision=0}+dollar  = Commodity {symbol="$",  side=L,spaced=False,comma=False,precision=2}+euro    = Commodity {symbol="EUR",side=L,spaced=False,comma=False,precision=2}+pound   = Commodity {symbol="£",  side=L,spaced=False,comma=False,precision=2}+hour    = Commodity {symbol="h",  side=R,spaced=False,comma=False,precision=1}++dollars n = Amount dollar n Nothing+euros n   = Amount euro n Nothing+pounds n  = Amount pound n Nothing+hours n   = Amount hour n Nothing++defaultcommodities = [dollar,  euro,  pound, hour, unknown]++-- | Look up one of the hard-coded default commodities. For use in tests.+comm :: String -> Commodity+comm sym = fromMaybe +              (error "commodity lookup failed") +              $ find (\(Commodity{symbol=s}) -> s==sym) defaultcommodities++-- | Find the conversion rate between two commodities. Currently returns 1.+conversionRate :: Commodity -> Commodity -> Double+conversionRate _ _ = 1+
+ Hledger/Data/Dates.hs view
@@ -0,0 +1,471 @@+{-|++Date parsing and utilities for hledger.++For date and time values, we use the standard Day and UTCTime types.++A 'SmartDate' is a date which may be partially-specified or relative.+Eg 2008\/12\/31, but also 2008\/12, 12\/31, tomorrow, last week, next year.+We represent these as a triple of strings like (\"2008\",\"12\",\"\"),+(\"\",\"\",\"tomorrow\"), (\"\",\"last\",\"week\").++A 'DateSpan' is the span of time between two specific calendar dates, or+an open-ended span where one or both dates are unspecified. (A date span+with both ends unspecified matches all dates.)++An 'Interval' is ledger's "reporting interval" - weekly, monthly,+quarterly, etc.++-}++module Hledger.Data.Dates+where++import Data.Time.Format+import Data.Time.Calendar.OrdinalDate+import Safe (readMay)+import System.Locale (defaultTimeLocale)+import Text.ParserCombinators.Parsec+import Hledger.Data.Types+import Hledger.Data.Utils+++showDate :: Day -> String+showDate = formatTime defaultTimeLocale "%C%y/%m/%d"++getCurrentDay :: IO Day+getCurrentDay = do+    t <- getZonedTime+    return $ localDay (zonedTimeToLocalTime t)++elapsedSeconds :: Fractional a => UTCTime -> UTCTime -> a+elapsedSeconds t1 = realToFrac . diffUTCTime t1++-- | Split a DateSpan into one or more consecutive spans at the specified interval.+splitSpan :: Interval -> DateSpan -> [DateSpan]+splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]+splitSpan NoInterval s = [s]+splitSpan Daily s      = splitspan startofday     nextday     s+splitSpan Weekly s     = splitspan startofweek    nextweek    s+splitSpan Monthly s    = splitspan startofmonth   nextmonth   s+splitSpan Quarterly s  = splitspan startofquarter nextquarter s+splitSpan Yearly s     = splitspan startofyear    nextyear    s++splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan]+splitspan _ _ (DateSpan Nothing Nothing) = []+splitspan start next (DateSpan Nothing (Just e)) = [DateSpan (Just $ start e) (Just $ next $ start e)]+splitspan start next (DateSpan (Just b) Nothing) = [DateSpan (Just $ start b) (Just $ next $ start b)]+splitspan start next span@(DateSpan (Just b) (Just e))+    | b == e = [span]+    | otherwise = splitspan' start next span+    where+      splitspan' start next (DateSpan (Just b) (Just e))+          | b >= e = []+          | otherwise = DateSpan (Just s) (Just n)+                        : splitspan' start next (DateSpan (Just n) (Just e))+          where s = start b+                n = next s+      splitspan' _ _ _ = error "won't happen, avoids warnings"++-- | Count the days in a DateSpan, or if it is open-ended return Nothing.+daysInSpan :: DateSpan -> Maybe Integer+daysInSpan (DateSpan (Just d1) (Just d2)) = Just $ diffDays d2 d1+daysInSpan _ = Nothing+    +-- | Parse a period expression to an Interval and overall DateSpan using+-- the provided reference date, or raise an error.+parsePeriodExpr :: Day -> String -> (Interval, DateSpan)+parsePeriodExpr refdate expr = (interval,span)+    where (interval,span) = fromparse $ parsewith (periodexpr refdate) expr+    +-- | Convert a single smart date string to a date span using the provided+-- reference date, or raise an error.+spanFromSmartDateString :: Day -> String -> DateSpan+spanFromSmartDateString refdate s = spanFromSmartDate refdate sdate+    where+      sdate = fromparse $ parsewith smartdateonly s++spanFromSmartDate :: Day -> SmartDate -> DateSpan+spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e)+    where+      (ry,rm,_) = toGregorian refdate+      (b,e) = span sdate+      span :: SmartDate -> (Day,Day)+      span ("","","today")       = (refdate, nextday refdate)+      span ("","this","day")     = (refdate, nextday refdate)+      span ("","","yesterday")   = (prevday refdate, refdate)+      span ("","last","day")     = (prevday refdate, refdate)+      span ("","","tomorrow")    = (nextday refdate, addDays 2 refdate)+      span ("","next","day")     = (nextday refdate, addDays 2 refdate)+      span ("","last","week")    = (prevweek refdate, thisweek refdate)+      span ("","this","week")    = (thisweek refdate, nextweek refdate)+      span ("","next","week")    = (nextweek refdate, startofweek $ addDays 14 refdate)+      span ("","last","month")   = (prevmonth refdate, thismonth refdate)+      span ("","this","month")   = (thismonth refdate, nextmonth refdate)+      span ("","next","month")   = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate)+      span ("","last","quarter") = (prevquarter refdate, thisquarter refdate)+      span ("","this","quarter") = (thisquarter refdate, nextquarter refdate)+      span ("","next","quarter") = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate)+      span ("","last","year")    = (prevyear refdate, thisyear refdate)+      span ("","this","year")    = (thisyear refdate, nextyear refdate)+      span ("","next","year")    = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate)+      span ("","",d)             = (day, nextday day) where day = fromGregorian ry rm (read d)+      span ("",m,"")             = (startofmonth day, nextmonth day) where day = fromGregorian ry (read m) 1+      span ("",m,d)              = (day, nextday day) where day = fromGregorian ry (read m) (read d)+      span (y,"","")             = (startofyear day, nextyear day) where day = fromGregorian (read y) 1 1+      span (y,m,"")              = (startofmonth day, nextmonth day) where day = fromGregorian (read y) (read m) 1+      span (y,m,d)               = (day, nextday day) where day = fromGregorian (read y) (read m) (read d)++showDay :: Day -> String+showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day++-- | Convert a smart date string to an explicit yyyy\/mm\/dd string using+-- the provided reference date, or raise an error.+fixSmartDateStr :: Day -> String -> String+fixSmartDateStr t s = either parseerror id $ fixSmartDateStrEither t s++-- | A safe version of fixSmartDateStr.+fixSmartDateStrEither :: Day -> String -> Either ParseError String+fixSmartDateStrEither t s = case parsewith smartdateonly (lowercase s) of+                              Right sd -> Right $ showDay $ fixSmartDate t sd+                              Left e -> Left e++-- | Convert a SmartDate to an absolute date using the provided reference date.+fixSmartDate :: Day -> SmartDate -> Day+fixSmartDate refdate sdate = fix sdate+    where+      fix :: SmartDate -> Day+      fix ("","","today")       = fromGregorian ry rm rd+      fix ("","this","day")     = fromGregorian ry rm rd+      fix ("","","yesterday")   = prevday refdate+      fix ("","last","day")     = prevday refdate+      fix ("","","tomorrow")    = nextday refdate+      fix ("","next","day")     = nextday refdate+      fix ("","last","week")    = prevweek refdate+      fix ("","this","week")    = thisweek refdate+      fix ("","next","week")    = nextweek refdate+      fix ("","last","month")   = prevmonth refdate+      fix ("","this","month")   = thismonth refdate+      fix ("","next","month")   = nextmonth refdate+      fix ("","last","quarter") = prevquarter refdate+      fix ("","this","quarter") = thisquarter refdate+      fix ("","next","quarter") = nextquarter refdate+      fix ("","last","year")    = prevyear refdate+      fix ("","this","year")    = thisyear refdate+      fix ("","next","year")    = nextyear refdate+      fix ("","",d)             = fromGregorian ry rm (read d)+      fix ("",m,"")             = fromGregorian ry (read m) 1+      fix ("",m,d)              = fromGregorian ry (read m) (read d)+      fix (y,"","")             = fromGregorian (read y) 1 1+      fix (y,m,"")              = fromGregorian (read y) (read m) 1+      fix (y,m,d)               = fromGregorian (read y) (read m) (read d)+      (ry,rm,rd) = toGregorian refdate++prevday :: Day -> Day+prevday = addDays (-1)+nextday = addDays 1+startofday = id++thisweek = startofweek+prevweek = startofweek . addDays (-7)+nextweek = startofweek . addDays 7+startofweek day = fromMondayStartWeek y w 1+    where+      (y,_,_) = toGregorian day+      (w,_) = mondayStartWeek day++thismonth = startofmonth+prevmonth = startofmonth . addGregorianMonthsClip (-1)+nextmonth = startofmonth . addGregorianMonthsClip 1+startofmonth day = fromGregorian y m 1 where (y,m,_) = toGregorian day++thisquarter = startofquarter+prevquarter = startofquarter . addGregorianMonthsClip (-3)+nextquarter = startofquarter . addGregorianMonthsClip 3+startofquarter day = fromGregorian y (firstmonthofquarter m) 1+    where+      (y,m,_) = toGregorian day+      firstmonthofquarter m = ((m-1) `div` 3) * 3 + 1++thisyear = startofyear+prevyear = startofyear . addGregorianYearsClip (-1)+nextyear = startofyear . addGregorianYearsClip 1+startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day++----------------------------------------------------------------------+-- parsing++firstJust ms = case dropWhile (==Nothing) ms of+    [] -> Nothing+    (md:_) -> md++-- | Parse a couple of date-time string formats to a time type.+parsedatetimeM :: String -> Maybe LocalTime+parsedatetimeM s = firstJust [+    parseTime defaultTimeLocale "%Y/%m/%d %H:%M:%S" s,+    parseTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" s+    ]++-- | Parse a couple of date string formats to a time type.+parsedateM :: String -> Maybe Day+parsedateM s = firstJust [ +     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.+parsedatetime :: String -> LocalTime+parsedatetime s = fromMaybe (error $ "could not parse timestamp \"" ++ s ++ "\"")+                            (parsedatetimeM s)++-- | Parse a date string to a time type, or raise an error.+parsedate :: String -> Day+parsedate s =  fromMaybe (error $ "could not parse date \"" ++ s ++ "\"")+                         (parsedateM s)++-- | Parse a time string to a time type using the provided pattern, or+-- return the default.+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:++> 2004+> 2004/10+> 2004/10/1+> 10/1+> 21+> october, oct+> yesterday, today, tomorrow+> (not yet) this/next/last week/day/month/quarter/year++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 = do+  (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 = do+  d <- smartdate+  many spacenonewline+  eof+  return d++datesepchar = oneOf "/-."++validYear, validMonth, validDay :: String -> Bool+validYear s = length s >= 4 && isJust (readMay s :: Maybe Int)+validMonth s = maybe False (\n -> n>=1 && n<=12) $ readMay s+validDay s = maybe False (\n -> n>=1 && n<=31) $ readMay s++-- failIfInvalidYear, failIfInvalidMonth, failIfInvalidDay :: a+failIfInvalidYear s  = unless (validYear s)  $ fail $ "bad year number: " ++ s+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 = do+  y <- count 4 digit+  m <- count 2 digit+  failIfInvalidMonth m+  d <- count 2 digit+  failIfInvalidDay d+  return (y,m,d)++ymd :: GenParser Char st SmartDate+ymd = do+  y <- many1 digit+  failIfInvalidYear y+  datesepchar+  m <- many1 digit+  failIfInvalidMonth m+  datesepchar+  d <- many1 digit+  failIfInvalidDay d+  return $ (y,m,d)++ym :: GenParser Char st SmartDate+ym = do+  y <- many1 digit+  failIfInvalidYear y+  datesepchar+  m <- many1 digit+  failIfInvalidMonth m+  return (y,m,"")++y :: GenParser Char st SmartDate+y = do+  y <- many1 digit+  failIfInvalidYear y+  return (y,"","")++d :: GenParser Char st SmartDate+d = do+  d <- many1 digit+  failIfInvalidDay d+  return ("","",d)++md :: GenParser Char st SmartDate+md = do+  m <- many1 digit+  failIfInvalidMonth m+  datesepchar+  d <- many1 digit+  failIfInvalidDay d+  return ("",m,d)++months         = ["january","february","march","april","may","june",+                  "july","august","september","october","november","december"]+monthabbrevs   = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]+weekdays       = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]+weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]++monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months+monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs++month :: GenParser Char st SmartDate+month = do+  m <- choice $ map (try . string) months+  let i = monthIndex m+  return ("",show i,"")++mon :: GenParser Char st SmartDate+mon = do+  m <- choice $ map (try . string) monthabbrevs+  let i = monIndex m+  return ("",show i,"")++today,yesterday,tomorrow :: GenParser Char st SmartDate+today     = string "today"     >> return ("","","today")+yesterday = string "yesterday" >> return ("","","yesterday")+tomorrow  = string "tomorrow"  >> return ("","","tomorrow")++lastthisnextthing :: GenParser Char st SmartDate+lastthisnextthing = do+  r <- choice [+        string "last"+       ,string "this"+       ,string "next"+      ]+  many spacenonewline  -- make the space optional for easier scripting+  p <- choice [+        string "day"+       ,string "week"+       ,string "month"+       ,string "quarter"+       ,string "year"+      ]+-- XXX support these in fixSmartDate+--       ++ (map string $ months ++ monthabbrevs ++ weekdays ++ weekdayabbrevs)+            +  return ("",r,p)++periodexpr :: Day -> GenParser Char st (Interval, DateSpan)+periodexpr rdate = choice $ map try [+                    intervalanddateperiodexpr rdate,+                    intervalperiodexpr,+                    dateperiodexpr rdate,+                    (return (NoInterval,DateSpan Nothing Nothing))+                   ]++intervalanddateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)+intervalanddateperiodexpr rdate = do+  many spacenonewline+  i <- periodexprinterval+  many spacenonewline+  s <- periodexprdatespan rdate+  return (i,s)++intervalperiodexpr :: GenParser Char st (Interval, DateSpan)+intervalperiodexpr = do+  many spacenonewline+  i <- periodexprinterval+  return (i, DateSpan Nothing Nothing)++dateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)+dateperiodexpr rdate = do+  many spacenonewline+  s <- periodexprdatespan rdate+  return (NoInterval, s)++periodexprinterval :: GenParser Char st Interval+periodexprinterval = +    choice $ map try [+                tryinterval "day" "daily" Daily,+                tryinterval "week" "weekly" Weekly,+                tryinterval "month" "monthly" Monthly,+                tryinterval "quarter" "quarterly" Quarterly,+                tryinterval "year" "yearly" Yearly+               ]+    where+      tryinterval s1 s2 v = +          choice [try (string $ "every "++s1), try (string s2)] >> return v++periodexprdatespan :: Day -> GenParser Char st DateSpan+periodexprdatespan rdate = choice $ map try [+                            doubledatespan rdate,+                            fromdatespan rdate,+                            todatespan rdate,+                            justdatespan rdate+                           ]++doubledatespan :: Day -> GenParser Char st DateSpan+doubledatespan rdate = do+  optional (string "from" >> many spacenonewline)+  b <- smartdate+  many spacenonewline+  optional (string "to" >> many spacenonewline)+  e <- smartdate+  return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e)++fromdatespan :: Day -> GenParser Char st DateSpan+fromdatespan rdate = do+  string "from" >> many spacenonewline+  b <- smartdate+  return $ DateSpan (Just $ fixSmartDate rdate b) Nothing++todatespan :: Day -> GenParser Char st DateSpan+todatespan rdate = do+  string "to" >> many spacenonewline+  e <- smartdate+  return $ DateSpan Nothing (Just $ fixSmartDate rdate e)++justdatespan :: Day -> GenParser Char st DateSpan+justdatespan rdate = do+  optional (string "in" >> many spacenonewline)+  d <- smartdate+  return $ spanFromSmartDate rdate d++mkdatespan :: String -> String -> DateSpan+mkdatespan b = DateSpan (Just $ parsedate b) . Just . parsedate++nulldatespan = DateSpan Nothing Nothing++nulldate = parsedate "1900/01/01"++tests_Dates = TestList [++   "splitSpan" ~: do+    let gives (interval, span) = (splitSpan interval span `is`)+    (NoInterval,mkdatespan "2008/01/01" "2009/01/01") `gives`+     [mkdatespan "2008/01/01" "2009/01/01"]+    (Quarterly,mkdatespan "2008/01/01" "2009/01/01") `gives`+     [mkdatespan "2008/01/01" "2008/04/01"+     ,mkdatespan "2008/04/01" "2008/07/01"+     ,mkdatespan "2008/07/01" "2008/10/01"+     ,mkdatespan "2008/10/01" "2009/01/01"+     ]+    (Quarterly,nulldatespan) `gives`+     [nulldatespan]+    (Daily,mkdatespan "2008/01/01" "2008/01/01") `gives`+     [mkdatespan "2008/01/01" "2008/01/01"]+    (Quarterly,mkdatespan "2008/01/01" "2008/01/01") `gives`+     [mkdatespan "2008/01/01" "2008/01/01"]++    ]
+ Hledger/Data/IO.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE CPP #-}+{-|+Utilities for doing I/O with ledger files.+-}++module Hledger.Data.IO+where+import Control.Monad.Error+import Hledger.Data.Parse (parseJournal)+import Hledger.Data.Types (FilterSpec(..),WhichDate(..),Journal(..))+import Hledger.Data.Dates (nulldatespan)+import System.Directory (getHomeDirectory)+import System.Environment (getEnv)+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding (readFile)+import System.IO.UTF8+#endif+import System.FilePath ((</>))+++ledgerenvvar           = "LEDGER"+timelogenvvar          = "TIMELOG"+ledgerdefaultfilename  = ".ledger"+timelogdefaultfilename = ".timelog"++nullfilterspec = FilterSpec {+     datespan=nulldatespan+    ,cleared=Nothing+    ,real=False+    ,empty=False+    ,costbasis=False+    ,acctpats=[]+    ,descpats=[]+    ,whichdate=ActualDate+    ,depth=Nothing+    }++-- | Get the user's default ledger file path.+myLedgerPath :: IO String+myLedgerPath = +    getEnv ledgerenvvar `catch` +               (\_ -> do+                  home <- getHomeDirectory `catch` (\_ -> return "")+                  return $ home </> ledgerdefaultfilename)+  +-- | Get the user's default timelog file path.+myTimelogPath :: IO String+myTimelogPath =+    getEnv timelogenvvar `catch`+               (\_ -> do+                  home <- getHomeDirectory+                  return $ home </> timelogdefaultfilename)++-- | Read the user's default journal file, or give an error.+myJournal :: IO Journal+myJournal = myLedgerPath >>= readJournal++-- | Read the user's default timelog file, or give an error.+myTimelog :: IO Journal+myTimelog = myTimelogPath >>= readJournal++-- | Read a journal from this file, or throw an error.+readJournal :: FilePath -> IO Journal+readJournal f = do+  s <- readFile f+  j <- journalFromString s+  return j{filepath=f}++-- | Read a Journal from the given string, using the current time as+-- reference time, or throw an error.+journalFromString :: String -> IO Journal+journalFromString s = liftM (either error id) $ runErrorT $ parseJournal "(from string)" s++-- -- | Expand ~ in a file path (does not handle ~name).+-- tildeExpand :: FilePath -> IO FilePath+-- tildeExpand ('~':[])     = getHomeDirectory+-- tildeExpand ('~':'/':xs) = getHomeDirectory >>= return . (++ ('/':xs))+-- --handle ~name, requires -fvia-C or ghc 6.8:+-- --import System.Posix.User+-- -- tildeExpand ('~':xs)     =  do let (user, path) = span (/= '/') xs+-- --                                pw <- getUserEntryForName user+-- --                                return (homeDirectory pw ++ path)+-- tildeExpand xs           =  return xs+
+ Hledger/Data/Journal.hs view
@@ -0,0 +1,358 @@+{-|++A 'Journal' is a set of 'Transaction's and related data, usually parsed+from a hledger/ledger journal file or timelog.++-}++module Hledger.Data.Journal+where+import qualified Data.Map as Map+import Data.Map (findWithDefault, (!))+import System.Time (ClockTime(TOD))+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.AccountName+import Hledger.Data.Amount+import Hledger.Data.Transaction (ledgerTransactionWithDate)+import Hledger.Data.Posting+import Hledger.Data.TimeLog+++instance Show Journal where+    show j = printf "Journal %s with %d transactions, %d accounts: %s"+             (filepath j)+             (length (jtxns j) ++              length (jmodifiertxns j) ++              length (jperiodictxns j))+             (length accounts)+             (show accounts)+             -- ++ (show $ journalTransactions l)+             where accounts = flatten $ journalAccountNameTree j++nulljournal :: Journal+nulljournal = Journal { jmodifiertxns = []+                      , jperiodictxns = []+                      , jtxns = []+                      , open_timelog_entries = []+                      , historical_prices = []+                      , final_comment_lines = []+                      , filepath = ""+                      , filereadtime = TOD 0 0+                      , jtext = ""+                      }++addTransaction :: Transaction -> Journal -> Journal+addTransaction t l0 = l0 { jtxns = t : jtxns l0 }++addModifierTransaction :: ModifierTransaction -> Journal -> Journal+addModifierTransaction mt l0 = l0 { jmodifiertxns = mt : jmodifiertxns l0 }++addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal+addPeriodicTransaction pt l0 = l0 { jperiodictxns = pt : jperiodictxns l0 }++addHistoricalPrice :: HistoricalPrice -> Journal -> Journal+addHistoricalPrice h l0 = l0 { historical_prices = h : historical_prices l0 }++addTimeLogEntry :: TimeLogEntry -> Journal -> Journal+addTimeLogEntry tle l0 = l0 { open_timelog_entries = tle : open_timelog_entries l0 }++journalPostings :: Journal -> [Posting]+journalPostings = concatMap tpostings . jtxns++journalAccountNamesUsed :: Journal -> [AccountName]+journalAccountNamesUsed = accountNamesFromPostings . journalPostings++journalAccountNames :: Journal -> [AccountName]+journalAccountNames = sort . expandAccountNames . journalAccountNamesUsed++journalAccountNameTree :: Journal -> Tree AccountName+journalAccountNameTree = accountNameTreeFrom . journalAccountNames++-- Various kinds of filtering on journals. We do it differently depending+-- on the command.++-- | Keep only transactions we are interested in, as described by+-- the filter specification. May also massage the data a little.+filterJournalTransactions :: FilterSpec -> Journal -> Journal+filterJournalTransactions FilterSpec{datespan=datespan+                                    ,cleared=cleared+                                    -- ,real=real+                                    -- ,empty=empty+                                    -- ,costbasis=_+                                    ,acctpats=apats+                                    ,descpats=dpats+                                    ,whichdate=whichdate+                                    ,depth=depth+                                    } =+    filterJournalTransactionsByClearedStatus cleared .+    filterJournalPostingsByDepth depth .+    filterJournalTransactionsByAccount apats .+    filterJournalTransactionsByDescription dpats .+    filterJournalTransactionsByDate datespan .+    journalSelectingDate whichdate++-- | Keep only postings we are interested in, as described by+-- the filter specification. May also massage the data a little.+-- This can leave unbalanced transactions.+filterJournalPostings :: FilterSpec -> Journal -> Journal+filterJournalPostings FilterSpec{datespan=datespan+                                ,cleared=cleared+                                ,real=real+                                ,empty=empty+--                                ,costbasis=costbasis+                                ,acctpats=apats+                                ,descpats=dpats+                                ,whichdate=whichdate+                                ,depth=depth+                                } =+    filterJournalPostingsByRealness real .+    filterJournalPostingsByClearedStatus cleared .+    filterJournalPostingsByEmpty empty .+    filterJournalPostingsByDepth depth .+    filterJournalPostingsByAccount apats .+    filterJournalTransactionsByDescription dpats .+    filterJournalTransactionsByDate datespan .+    journalSelectingDate whichdate++-- | Keep only ledger transactions whose description matches the description patterns.+filterJournalTransactionsByDescription :: [String] -> Journal -> Journal+filterJournalTransactionsByDescription pats j@Journal{jtxns=ts} = j{jtxns=filter matchdesc ts}+    where matchdesc = matchpats pats . tdescription++-- | Keep only ledger transactions which fall between begin and end dates.+-- We include transactions on the begin date and exclude transactions on the end+-- date, like ledger.  An empty date string means no restriction.+filterJournalTransactionsByDate :: DateSpan -> Journal -> Journal+filterJournalTransactionsByDate (DateSpan begin end) j@Journal{jtxns=ts} = j{jtxns=filter match ts}+    where match t = maybe True (tdate t>=) begin && maybe True (tdate t<) end++-- | Keep only ledger transactions which have the requested+-- cleared/uncleared status, if there is one.+filterJournalTransactionsByClearedStatus :: Maybe Bool -> Journal -> Journal+filterJournalTransactionsByClearedStatus Nothing j = j+filterJournalTransactionsByClearedStatus (Just val) j@Journal{jtxns=ts} = j{jtxns=filter match ts}+    where match = (==val).tstatus++-- | Keep only postings which have the requested cleared/uncleared status,+-- if there is one.+filterJournalPostingsByClearedStatus :: Maybe Bool -> Journal -> Journal+filterJournalPostingsByClearedStatus Nothing j = j+filterJournalPostingsByClearedStatus (Just c) j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}+    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter ((==c) . postingCleared) ps}++-- | Strip out any virtual postings, if the flag is true, otherwise do+-- no filtering.+filterJournalPostingsByRealness :: Bool -> Journal -> Journal+filterJournalPostingsByRealness False l = l+filterJournalPostingsByRealness True j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}+    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter isReal ps}++-- | Strip out any postings with zero amount, unless the flag is true.+filterJournalPostingsByEmpty :: Bool -> Journal -> Journal+filterJournalPostingsByEmpty True l = l+filterJournalPostingsByEmpty False j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}+    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter (not . isEmptyPosting) ps}++-- | Keep only transactions which affect accounts deeper than the specified depth.+filterJournalTransactionsByDepth :: Maybe Int -> Journal -> Journal+filterJournalTransactionsByDepth Nothing j = j+filterJournalTransactionsByDepth (Just d) j@Journal{jtxns=ts} =+    j{jtxns=(filter (any ((<= d+1) . accountNameLevel . paccount) . tpostings) ts)}++-- | Strip out any postings to accounts deeper than the specified depth+-- (and any ledger transactions which have no postings as a result).+filterJournalPostingsByDepth :: Maybe Int -> Journal -> Journal+filterJournalPostingsByDepth Nothing j = j+filterJournalPostingsByDepth (Just d) j@Journal{jtxns=ts} =+    j{jtxns=filter (not . null . tpostings) $ map filtertxns ts}+    where filtertxns t@Transaction{tpostings=ps} =+              t{tpostings=filter ((<= d) . accountNameLevel . paccount) ps}++-- | Keep only transactions which affect accounts matched by the account patterns.+-- More precisely: each positive account pattern excludes transactions+-- which do not contain a posting to a matched account, and each negative+-- account pattern excludes transactions containing a posting to a matched+-- account.+filterJournalTransactionsByAccount :: [String] -> Journal -> Journal+filterJournalTransactionsByAccount apats j@Journal{jtxns=ts} = j{jtxns=filter tmatch ts}+    where+      tmatch t = (null positives || any positivepmatch ps) && (null negatives || not (any negativepmatch ps)) where ps = tpostings t+      positivepmatch p = any (`amatch` a) positives where a = paccount p+      negativepmatch p = any (`amatch` a) negatives where a = paccount p+      amatch pat a = containsRegex (abspat pat) a+      (negatives,positives) = partition isnegativepat apats++-- | Keep only postings which affect accounts matched by the account patterns.+-- This can leave transactions unbalanced.+filterJournalPostingsByAccount :: [String] -> Journal -> Journal+filterJournalPostingsByAccount apats j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}+    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter (matchpats apats . paccount) ps}++-- | Convert this journal's transactions' primary date to either the+-- actual or effective date.+journalSelectingDate :: WhichDate -> Journal -> Journal+journalSelectingDate ActualDate j = j+journalSelectingDate EffectiveDate j =+    j{jtxns=map (ledgerTransactionWithDate EffectiveDate) $ jtxns j}++-- | Do post-parse processing on a journal, to make it ready for use.+journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> Journal -> Journal+journalFinalise tclock tlocal path txt j = journalCanonicaliseAmounts $+                                           journalApplyHistoricalPrices $+                                           journalCloseTimeLogEntries tlocal+                                             j{filepath=path, filereadtime=tclock, jtext=txt}++-- | Convert all the journal's amounts to their canonical display+-- settings.  Ie, all amounts in a given commodity will use (a) the+-- display settings of the first, and (b) the greatest precision, of the+-- amounts in that commodity. Prices are canonicalised as well, so consider+-- calling journalApplyHistoricalPrices before this.+journalCanonicaliseAmounts :: Journal -> Journal+journalCanonicaliseAmounts j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts}+    where+      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{commodity=c,price=p} = a{commodity=fixcommodity c, price=maybe Nothing (Just . fixmixedamount) p}+      fixcommodity c@Commodity{symbol=s} = findWithDefault c s canonicalcommoditymap+      canonicalcommoditymap = journalCanonicalCommodities j++-- | Apply this journal's historical price records to unpriced amounts where possible.+journalApplyHistoricalPrices :: Journal -> Journal+journalApplyHistoricalPrices j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts}+    where+      fixtransaction t@Transaction{tdate=d, tpostings=ps} = t{tpostings=map fixposting ps}+       where+        fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a}+        fixmixedamount (Mixed as) = Mixed $ map fixamount as+        fixamount = fixprice+        fixprice a@Amount{price=Just _} = a+        fixprice a@Amount{commodity=c} = a{price=journalHistoricalPriceFor j d c}++-- | Get the price for a commodity on the specified day from the price database, if known.+-- Does only one lookup step, ie will not look up the price of a price.+journalHistoricalPriceFor :: Journal -> Day -> Commodity -> Maybe MixedAmount+journalHistoricalPriceFor j d Commodity{symbol=s} = do+  let ps = reverse $ filter ((<= d).hdate) $ filter ((s==).hsymbol) $ sortBy (comparing hdate) $ historical_prices j+  case ps of (HistoricalPrice{hamount=a}:_) -> Just a+             _ -> Nothing++-- | Close any open timelog sessions in this journal using the provided current time.+journalCloseTimeLogEntries :: LocalTime -> Journal -> Journal+journalCloseTimeLogEntries now j@Journal{jtxns=ts, open_timelog_entries=es} =+  j{jtxns = ts ++ (timeLogEntriesToTransactions now es), open_timelog_entries = []}++-- | Convert all this journal's amounts to cost by applying their prices, if any.+journalConvertAmountsToCost :: Journal -> Journal+journalConvertAmountsToCost j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts}+    where+      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 = costOfAmount++-- | Get this journal's unique, display-preference-canonicalised commodities, by symbol.+journalCanonicalCommodities :: Journal -> Map.Map String Commodity+journalCanonicalCommodities j =+    Map.fromList [(s,firstc{precision=maxp}) | s <- commoditysymbols,+                  let cs = commoditymap ! s,+                  let firstc = head cs,+                  let maxp = maximum $ map precision cs+                 ]+  where+    commoditymap = Map.fromList [(s,commoditieswithsymbol s) | s <- commoditysymbols]+    commoditieswithsymbol s = filter ((s==) . symbol) commodities+    commoditysymbols = nub $ map symbol commodities+    commodities = journalAmountAndPriceCommodities j++-- | Get all this journal's amounts' commodities, in the order parsed.+journalAmountCommodities :: Journal -> [Commodity]+journalAmountCommodities = map commodity . concatMap amounts . journalAmounts++-- | Get all this journal's amount and price commodities, in the order parsed.+journalAmountAndPriceCommodities :: Journal -> [Commodity]+journalAmountAndPriceCommodities = concatMap amountCommodities . concatMap amounts . journalAmounts++-- | Get this amount's commodity and any commodities referenced in its price.+amountCommodities :: Amount -> [Commodity]+amountCommodities Amount{commodity=c,price=Nothing} = [c]+amountCommodities Amount{commodity=c,price=Just ma} = c:(concatMap amountCommodities $ amounts ma)++-- | Get all this journal's amounts, in the order parsed.+journalAmounts :: Journal -> [MixedAmount]+journalAmounts = map pamount . journalPostings++-- | The (fully specified) date span containing this journal's transactions,+-- or DateSpan Nothing Nothing if there are none.+journalDateSpan :: Journal -> DateSpan+journalDateSpan j+    | null ts = DateSpan Nothing Nothing+    | otherwise = DateSpan (Just $ tdate $ head ts) (Just $ addDays 1 $ tdate $ last ts)+    where+      ts = sortBy (comparing tdate) $ jtxns j++-- | Check if a set of ledger account/description patterns matches the+-- given account name or entry description.  Patterns are case-insensitive+-- regular expressions. Prefixed with not:, they become anti-patterns.+matchpats :: [String] -> String -> Bool+matchpats pats str =+    (null positives || any match positives) && (null negatives || not (any match negatives))+    where+      (negatives,positives) = partition isnegativepat pats+      match "" = True+      match pat = containsRegex (abspat pat) str++negateprefix = "not:"++isnegativepat = (negateprefix `isPrefixOf`)++abspat pat = if isnegativepat pat then drop (length negateprefix) pat else pat++-- | Calculate the account tree and all account balances from a journal's+-- postings, returning the results for efficient lookup.+journalAccountInfo :: Journal -> (Tree AccountName, Map.Map AccountName Account)+journalAccountInfo j = (ant, amap)+    where+      (ant, psof, _, inclbalof) = (groupPostings . journalPostings) j+      amap = Map.fromList [(a, acctinfo a) | a <- flatten ant]+      acctinfo a = Account a (psof a) (inclbalof a)++-- | Given a list of postings, return an account name tree and three query+-- functions that fetch postings, subaccount-excluding-balance and+-- subaccount-including-balance by account name.+groupPostings :: [Posting] -> (Tree AccountName,+                             (AccountName -> [Posting]),+                             (AccountName -> MixedAmount),+                             (AccountName -> MixedAmount))+groupPostings ps = (ant, psof, exclbalof, inclbalof)+    where+      anames = sort $ nub $ map paccount ps+      ant = accountNameTreeFrom $ expandAccountNames anames+      allanames = flatten ant+      pmap = Map.union (postingsByAccount ps) (Map.fromList [(a,[]) | a <- allanames])+      psof = (pmap !)+      balmap = Map.fromList $ flatten $ calculateBalances ant psof+      exclbalof = fst . (balmap !)+      inclbalof = snd . (balmap !)++-- | Add subaccount-excluding and subaccount-including balances to a tree+-- of account names somewhat efficiently, given a function that looks up+-- transactions by account name.+calculateBalances :: Tree AccountName -> (AccountName -> [Posting]) -> Tree (AccountName, (MixedAmount, MixedAmount))+calculateBalances ant psof = addbalances ant+    where+      addbalances (Node a subs) = Node (a,(bal,bal+subsbal)) subs'+          where+            bal         = sumPostings $ psof a+            subsbal     = sum $ map (snd . snd . root) subs'+            subs'       = map addbalances subs++-- | Convert a list of postings to a map from account name to that+-- account's postings.+postingsByAccount :: [Posting] -> Map.Map AccountName [Posting]+postingsByAccount ps = m'+    where+      sortedps = sortBy (comparing paccount) ps+      groupedps = groupBy (\p1 p2 -> paccount p1 == paccount p2) sortedps+      m' = Map.fromList [(paccount $ head g, g) | g <- groupedps]
+ Hledger/Data/Ledger.hs view
@@ -0,0 +1,122 @@+{-|++A 'Ledger' is derived from a 'Journal' by applying a filter specification+to select 'Transaction's and 'Posting's of interest. It contains the+filtered journal and knows the resulting chart of accounts, account+balances, and postings in each account.++-}++module Hledger.Data.Ledger+where+import Data.Map (Map, findWithDefault, fromList)+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Account (nullacct)+import Hledger.Data.AccountName+import Hledger.Data.Journal+import Hledger.Data.Posting+++instance Show Ledger where+    show l = printf "Ledger with %d transactions, %d accounts\n%s"+             (length (jtxns $ journal l) ++              length (jmodifiertxns $ journal l) ++              length (jperiodictxns $ journal l))+             (length $ accountnames l)+             (showtree $ accountnametree l)++nullledger :: Ledger+nullledger = Ledger{+      journal = nulljournal,+      accountnametree = nullaccountnametree,+      accountmap = fromList []+    }++-- | Filter a ledger's transactions as specified and generate derived data.+journalToLedger :: FilterSpec -> Journal -> Ledger+journalToLedger fs j = nullledger{journal=j',accountnametree=t,accountmap=m}+    where j' = filterJournalPostings fs{depth=Nothing} j+          (t, m) = journalAccountInfo j'++-- | List a ledger's account names.+ledgerAccountNames :: Ledger -> [AccountName]+ledgerAccountNames = drop 1 . flatten . accountnametree++-- | Get the named account from a ledger.+ledgerAccount :: Ledger -> AccountName -> Account+ledgerAccount l a = findWithDefault nullacct a $ accountmap l++-- | List a ledger's accounts, in tree order+ledgerAccounts :: Ledger -> [Account]+ledgerAccounts = drop 1 . flatten . ledgerAccountTree 9999++-- | List a ledger's top-level accounts, in tree order+ledgerTopAccounts :: Ledger -> [Account]+ledgerTopAccounts = map root . branches . ledgerAccountTree 9999++-- | Accounts in ledger whose name matches the pattern, in tree order.+ledgerAccountsMatching :: [String] -> Ledger -> [Account]+ledgerAccountsMatching pats = filter (matchpats pats . aname) . accounts++-- | List a ledger account's immediate subaccounts+ledgerSubAccounts :: Ledger -> Account -> [Account]+ledgerSubAccounts l Account{aname=a} = +    map (ledgerAccount l) $ filter (`isSubAccountNameOf` a) $ accountnames l++-- | List a ledger's postings, in the order parsed.+ledgerPostings :: Ledger -> [Posting]+ledgerPostings = journalPostings . journal++-- | Get a ledger's tree of accounts to the specified depth.+ledgerAccountTree :: Int -> Ledger -> Tree Account+ledgerAccountTree depth l = treemap (ledgerAccount l) $ treeprune depth $ accountnametree l++-- | Get a ledger's tree of accounts rooted at the specified account.+ledgerAccountTreeAt :: Ledger -> Account -> Maybe (Tree Account)+ledgerAccountTreeAt l acct = subtreeat acct $ ledgerAccountTree 9999 l++-- | The (fully specified) date span containing all the ledger's (filtered) transactions,+-- or DateSpan Nothing Nothing if there are none.+ledgerDateSpan :: Ledger -> DateSpan+ledgerDateSpan = postingsDateSpan . ledgerPostings++-- | Convenience aliases.+accountnames :: Ledger -> [AccountName]+accountnames = ledgerAccountNames++account :: Ledger -> AccountName -> Account+account = ledgerAccount++accounts :: Ledger -> [Account]+accounts = ledgerAccounts++topaccounts :: Ledger -> [Account]+topaccounts = ledgerTopAccounts++accountsmatching :: [String] -> Ledger -> [Account]+accountsmatching = ledgerAccountsMatching++subaccounts :: Ledger -> Account -> [Account]+subaccounts = ledgerSubAccounts++postings :: Ledger -> [Posting]+postings = ledgerPostings++commodities :: Ledger -> Map String Commodity+commodities = journalCanonicalCommodities . journal++accounttree :: Int -> Ledger -> Tree Account+accounttree = ledgerAccountTree++accounttreeat :: Ledger -> Account -> Maybe (Tree Account)+accounttreeat = ledgerAccountTreeAt++-- datespan :: Ledger -> DateSpan+-- datespan = ledgerDateSpan++rawdatespan :: Ledger -> DateSpan+rawdatespan = journalDateSpan . journal++ledgeramounts :: Ledger -> [MixedAmount]+ledgeramounts = journalAmounts . journal
+ Hledger/Data/Parse.hs view
@@ -0,0 +1,731 @@+{-# LANGUAGE CPP #-}+{-|++Parsers for standard ledger and timelog files.++Here is the ledger grammar from the ledger 2.5 manual:++@+The ledger file format is quite simple, but also very flexible. It supports+many options, though typically the user can ignore most of them. They are+summarized below.  The initial character of each line determines what the+line means, and how it should be interpreted. Allowable initial characters+are:++NUMBER      A line beginning with a number denotes an entry. It may be followed by any+            number of lines, each beginning with whitespace, to denote the entry’s account+            transactions. The format of the first line is:++                    DATE[=EDATE] [*|!] [(CODE)] DESC++            If ‘*’ appears after the date (with optional effective date), it indicates the entry+            is “cleared”, which can mean whatever the user wants it t omean. If ‘!’ appears+            after the date, it indicates d the entry is “pending”; i.e., tentatively cleared from+            the user’s point of view, but not yet actually cleared. If a ‘CODE’ appears in+            parentheses, it may be used to indicate a check number, or the type of the+            transaction. Following these is the payee, or a description of the transaction.+            The format of each following transaction is:++                      ACCOUNT     AMOUNT    [; NOTE]++            The ‘ACCOUNT’ may be surrounded by parentheses if it is a virtual+            transactions, or square brackets if it is a virtual transactions that must+            balance. The ‘AMOUNT’ can be followed by a per-unit transaction cost,+            by specifying ‘ AMOUNT’, or a complete transaction cost with ‘\@ AMOUNT’.+            Lastly, the ‘NOTE’ may specify an actual and/or effective date for the+            transaction by using the syntax ‘[ACTUAL_DATE]’ or ‘[=EFFECTIVE_DATE]’ or+            ‘[ACTUAL_DATE=EFFECtIVE_DATE]’.++=           An automated entry. A value expression must appear after the equal sign.+            After this initial line there should be a set of one or more transactions, just as+            if it were normal entry. If the amounts of the transactions have no commodity,+            they will be applied as modifiers to whichever real transaction is matched by+            the value expression.+ +~           A period entry. A period expression must appear after the tilde.+            After this initial line there should be a set of one or more transactions, just as+            if it were normal entry.++!           A line beginning with an exclamation mark denotes a command directive. It+            must be immediately followed by the command word. The supported commands+            are:++           ‘!include’+                        Include the stated ledger file.+           ‘!account’+                        The account name is given is taken to be the parent of all transac-+                        tions that follow, until ‘!end’ is seen.+           ‘!end’       Ends an account block.+ +;          A line beginning with a colon indicates a comment, and is ignored.+ +Y          If a line begins with a capital Y, it denotes the year used for all subsequent+           entries that give a date without a year. The year should appear immediately+           after the Y, for example: ‘Y2004’. This is useful at the beginning of a file, to+           specify the year for that file. If all entries specify a year, however, this command+           has no effect.+           + +P          Specifies a historical price for a commodity. These are usually found in a pricing+           history file (see the ‘-Q’ option). The syntax is:++                  P DATE SYMBOL PRICE++N SYMBOL   Indicates that pricing information is to be ignored for a given symbol, nor will+           quotes ever be downloaded for that symbol. Useful with a home currency, such+           as the dollar ($). It is recommended that these pricing options be set in the price+           database file, which defaults to ‘~/.pricedb’. The syntax for this command is:++                  N SYMBOL++        +D AMOUNT   Specifies the default commodity to use, by specifying an amount in the expected+           format. The entry command will use this commodity as the default when none+           other can be determined. This command may be used multiple times, to set+           the default flags for different commodities; whichever is seen last is used as the+           default commodity. For example, to set US dollars as the default commodity,+           while also setting the thousands flag and decimal flag for that commodity, use:++                  D $1,000.00++C AMOUNT1 = AMOUNT2+           Specifies a commodity conversion, where the first amount is given to be equiv-+           alent to the second amount. The first amount should use the decimal precision+           desired during reporting:++                  C 1.00 Kb = 1024 bytes++i, o, b, h+           These four relate to timeclock support, which permits ledger to read timelog+           files. See the timeclock’s documentation for more info on the syntax of its+           timelog files.+@++Here is the timelog grammar from timeclock.el 2.6:++@+A timelog contains data in the form of a single entry per line.+Each entry has the form:++  CODE YYYY/MM/DD HH:MM:SS [COMMENT]++CODE is one of: b, h, i, o or O.  COMMENT is optional when the code is+i, o or O.  The meanings of the codes are:++  b  Set the current time balance, or \"time debt\".  Useful when+     archiving old log data, when a debt must be carried forward.+     The COMMENT here is the number of seconds of debt.++  h  Set the required working time for the given day.  This must+     be the first entry for that day.  The COMMENT in this case is+     the number of hours in this workday.  Floating point amounts+     are allowed.++  i  Clock in.  The COMMENT in this case should be the name of the+     project worked on.++  o  Clock out.  COMMENT is unnecessary, but can be used to provide+     a description of how the period went, for example.++  O  Final clock out.  Whatever project was being worked on, it is+     now finished.  Useful for creating summary reports.+@++Example:++@+i 2007/03/10 12:26:00 hledger+o 2007/03/10 17:26:02+@++-}++module Hledger.Data.Parse+where+import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError, catchError)+import Text.ParserCombinators.Parsec+import System.Directory+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding (readFile, putStr, putStrLn, print, getContents)+import System.IO.UTF8+#endif+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Dates+import Hledger.Data.AccountName (accountNameFromComponents,accountNameComponents)+import Hledger.Data.Amount+import Hledger.Data.Transaction+import Hledger.Data.Posting+import Hledger.Data.Journal+import Hledger.Data.Commodity (dollars,dollar,unknown)+import System.FilePath(takeDirectory,combine)+import System.Time (getClockTime)+++-- | A JournalUpdate is some transformation of a "Journal". It can do I/O+-- or raise an error.+type JournalUpdate = ErrorT String IO (Journal -> Journal)++-- | Some context kept during parsing.+data LedgerFileCtx = Ctx {+      ctxYear     :: !(Maybe Integer)  -- ^ the default year most recently specified with Y+    , ctxCommod   :: !(Maybe String)   -- ^ I don't know+    , ctxAccount  :: ![String]         -- ^ the current stack of parent accounts specified by !account+    } deriving (Read, Show)++emptyCtx :: LedgerFileCtx+emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] }++setYear :: Integer -> GenParser tok LedgerFileCtx ()+setYear y = updateState (\ctx -> ctx{ctxYear=Just y})++getYear :: GenParser tok LedgerFileCtx (Maybe Integer)+getYear = liftM ctxYear getState++pushParentAccount :: String -> GenParser tok LedgerFileCtx ()+pushParentAccount parent = updateState addParentAccount+    where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }+          normalize = (++ ":") ++popParentAccount :: GenParser tok LedgerFileCtx ()+popParentAccount = do ctx0 <- getState+                      case ctxAccount ctx0 of+                        [] -> unexpected "End of account block with no beginning"+                        (_:rest) -> setState $ ctx0 { ctxAccount = rest }++getParentAccount :: GenParser tok LedgerFileCtx String+getParentAccount = liftM (concat . reverse . ctxAccount) getState++expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath+expandPath pos fp = liftM mkRelative (expandHome fp)+  where+    mkRelative = combine (takeDirectory (sourceName pos))+    expandHome inname | "~/" `isPrefixOf` inname = do homedir <- liftIO getHomeDirectory+                                                      return $ homedir ++ drop 1 inname+                      | otherwise                = return inname++-- let's get to it++-- | Parse and post-process a journal file or timelog file to a "Journal",+-- or give an error.+parseJournalFile :: FilePath -> ErrorT String IO Journal+parseJournalFile "-" = liftIO getContents >>= parseJournal "-"+parseJournalFile f   = liftIO (readFile f) >>= parseJournal f++-- | Parse and post-process a "Journal" from a string, saving the provided+-- file path and the current time, or give an error.+parseJournal :: FilePath -> String -> ErrorT String IO Journal+parseJournal f s = do+  tc <- liftIO getClockTime+  tl <- liftIO getCurrentLocalTime+  case runParser ledgerFile emptyCtx f s of+    Right m  -> liftM (journalFinalise tc tl f s) $ m `ap` return nulljournal+    Left err -> throwError $ show err -- XXX raises an uncaught exception if we have a parsec user error, eg from many ?++-- | Top-level journal parser. Returns a single composite, I/O performing,+-- error-raising "JournalUpdate" which can be applied to an empty journal+-- to get the final result.+ledgerFile :: GenParser Char LedgerFileCtx JournalUpdate+ledgerFile = do items <- many ledgerItem+                eof+                return $ liftM (foldr (.) id) $ sequence items+    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+      ledgerItem = choice [ ledgerExclamationDirective+                          , liftM (return . addTransaction) ledgerTransaction+                          , liftM (return . addModifierTransaction) ledgerModifierTransaction+                          , liftM (return . addPeriodicTransaction) ledgerPeriodicTransaction+                          , liftM (return . addHistoricalPrice) ledgerHistoricalPrice+                          , ledgerDefaultYear+                          , ledgerIgnoredPriceCommodity+                          , ledgerTagDirective+                          , ledgerEndTagDirective+                          , emptyLine >> return (return id)+                          , liftM (return . addTimeLogEntry)  timelogentry+                          ] <?> "ledger transaction, timelog entry, or directive"++emptyLine :: GenParser Char st ()+emptyLine = do many spacenonewline+               optional $ (char ';' <?> "comment") >> many (noneOf "\n")+               newline+               return ()++ledgercomment :: GenParser Char st String+ledgercomment = do+  many1 $ char ';'+  many spacenonewline+  many (noneOf "\n")+  <?> "comment"++ledgercommentline :: GenParser Char st String+ledgercommentline = do+  many spacenonewline+  s <- ledgercomment+  optional newline+  eof+  return s+  <?> "comment"++ledgerExclamationDirective :: GenParser Char LedgerFileCtx JournalUpdate+ledgerExclamationDirective = do+  char '!' <?> "directive"+  directive <- many nonspace+  case directive of+    "include" -> ledgerInclude+    "account" -> ledgerAccountBegin+    "end"     -> ledgerAccountEnd+    _         -> mzero++ledgerInclude :: GenParser Char LedgerFileCtx JournalUpdate+ledgerInclude = do many1 spacenonewline+                   filename <- restofline+                   outerState <- getState+                   outerPos <- getPosition+                   let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"+                   return $ do contents <- expandPath outerPos filename >>= readFileE outerPos+                               case runParser ledgerFile outerState filename contents of+                                 Right l   -> l `catchError` (throwError . (inIncluded ++))+                                 Left perr -> throwError $ inIncluded ++ show perr+    where readFileE outerPos filename = ErrorT $ liftM Right (readFile filename) `catch` leftError+              where leftError err = return $ Left $ currentPos ++ whileReading ++ show err+                    currentPos = show outerPos+                    whileReading = " reading " ++ show filename ++ ":\n"++ledgerAccountBegin :: GenParser Char LedgerFileCtx JournalUpdate+ledgerAccountBegin = do many1 spacenonewline+                        parent <- ledgeraccountname+                        newline+                        pushParentAccount parent+                        return $ return id++ledgerAccountEnd :: GenParser Char LedgerFileCtx JournalUpdate+ledgerAccountEnd = popParentAccount >> return (return id)++ledgerModifierTransaction :: GenParser Char LedgerFileCtx ModifierTransaction+ledgerModifierTransaction = do+  char '=' <?> "modifier transaction"+  many spacenonewline+  valueexpr <- restofline+  postings <- ledgerpostings+  return $ ModifierTransaction valueexpr postings++ledgerPeriodicTransaction :: GenParser Char LedgerFileCtx PeriodicTransaction+ledgerPeriodicTransaction = do+  char '~' <?> "periodic transaction"+  many spacenonewline+  periodexpr <- restofline+  postings <- ledgerpostings+  return $ PeriodicTransaction periodexpr postings++ledgerHistoricalPrice :: GenParser Char LedgerFileCtx HistoricalPrice+ledgerHistoricalPrice = do+  char 'P' <?> "historical price"+  many spacenonewline+  date <- try (do {LocalTime d _ <- ledgerdatetime; return d}) <|> ledgerdate -- a time is ignored+  many1 spacenonewline+  symbol <- commoditysymbol+  many spacenonewline+  price <- someamount+  restofline+  return $ HistoricalPrice date symbol price++ledgerIgnoredPriceCommodity :: GenParser Char LedgerFileCtx JournalUpdate+ledgerIgnoredPriceCommodity = do+  char 'N' <?> "ignored-price commodity"+  many1 spacenonewline+  commoditysymbol+  restofline+  return $ return id++ledgerDefaultCommodity :: GenParser Char LedgerFileCtx JournalUpdate+ledgerDefaultCommodity = do+  char 'D' <?> "default commodity"+  many1 spacenonewline+  someamount+  restofline+  return $ return id++ledgerCommodityConversion :: GenParser Char LedgerFileCtx JournalUpdate+ledgerCommodityConversion = do+  char 'C' <?> "commodity conversion"+  many1 spacenonewline+  someamount+  many spacenonewline+  char '='+  many spacenonewline+  someamount+  restofline+  return $ return id++ledgerTagDirective :: GenParser Char LedgerFileCtx JournalUpdate+ledgerTagDirective = do+  string "tag" <?> "tag directive"+  many1 spacenonewline+  _ <- many1 nonspace+  restofline+  return $ return id++ledgerEndTagDirective :: GenParser Char LedgerFileCtx JournalUpdate+ledgerEndTagDirective = do+  string "end tag" <?> "end tag directive"+  restofline+  return $ return id++-- like ledgerAccountBegin, updates the LedgerFileCtx+ledgerDefaultYear :: GenParser Char LedgerFileCtx JournalUpdate+ledgerDefaultYear = do+  char 'Y' <?> "default year"+  many spacenonewline+  y <- many1 digit+  let y' = read y+  failIfInvalidYear y+  setYear y'+  return $ return id++-- | Try to parse a ledger entry. If we successfully parse an entry,+-- check it can be balanced, and fail if not.+ledgerTransaction :: GenParser Char LedgerFileCtx Transaction+ledgerTransaction = do+  date <- ledgerdate <?> "transaction"+  edate <- optionMaybe (ledgereffectivedate date) <?> "effective date"+  status <- ledgerstatus <?> "cleared flag"+  code <- ledgercode <?> "transaction code"+  (description, comment) <-+      (do {many1 spacenonewline; d <- liftM rstrip (many (noneOf ";\n")); c <- ledgercomment <|> return ""; newline; return (d, c)} <|>+       do {many spacenonewline; c <- ledgercomment <|> return ""; newline; return ("", c)}+      ) <?> "description and/or comment"+  postings <- ledgerpostings+  let t = txnTieKnot $ Transaction date edate status code description comment postings ""+  case balanceTransaction t of+    Right t' -> return t'+    Left err -> fail err++ledgerdate :: GenParser Char LedgerFileCtx Day+ledgerdate = choice' [ledgerfulldate, ledgerpartialdate] <?> "full or partial date"++ledgerfulldate :: GenParser Char LedgerFileCtx Day+ledgerfulldate = do+  (y,m,d) <- ymd+  return $ fromGregorian (read y) (read m) (read d)++-- | Match a partial M/D date in a ledger, and also require that a default+-- year directive was previously encountered.+ledgerpartialdate :: GenParser Char LedgerFileCtx Day+ledgerpartialdate = do+  (_,m,d) <- md+  y <- getYear+  when (isNothing y) $ fail "partial date found, but no default year specified"+  return $ fromGregorian (fromJust y) (read m) (read d)++ledgerdatetime :: GenParser Char LedgerFileCtx LocalTime+ledgerdatetime = do +  day <- ledgerdate+  many1 spacenonewline+  h <- many1 digit+  char ':'+  m <- many1 digit+  s <- optionMaybe $ do+      char ':'+      many1 digit+  let tod = TimeOfDay (read h) (read m) (maybe 0 (fromIntegral.read) s)+  return $ LocalTime day tod++ledgereffectivedate :: Day -> GenParser Char LedgerFileCtx Day+ledgereffectivedate actualdate = do+  char '='+  -- kludgy way to use actual date for default year+  let withDefaultYear d p = do+        y <- getYear+        let (y',_,_) = toGregorian d in setYear y'+        r <- p+        when (isJust y) $ setYear $ fromJust y+        return r+  edate <- withDefaultYear actualdate ledgerdate+  return edate++ledgerstatus :: GenParser Char st Bool+ledgerstatus = try (do { many1 spacenonewline; char '*' <?> "status"; return True } ) <|> return False++ledgercode :: GenParser Char st String+ledgercode = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""++ledgerpostings :: GenParser Char LedgerFileCtx [Posting]+ledgerpostings = do+  -- complicated to handle intermixed comment lines.. please make me better.+  ctx <- getState+  let parses p = isRight . parseWithCtx ctx p+  ls <- many1 $ try linebeginningwithspaces+  let ls' = filter (not . (ledgercommentline `parses`)) ls+  when (null ls') $ fail "no postings"+  return $ map (fromparse . parseWithCtx ctx ledgerposting) ls'+  <?> "postings"++linebeginningwithspaces :: GenParser Char st String+linebeginningwithspaces = do+  sp <- many1 spacenonewline+  c <- nonspace+  cs <- restofline+  return $ sp ++ (c:cs) ++ "\n"++ledgerposting :: GenParser Char LedgerFileCtx Posting+ledgerposting = do+  many1 spacenonewline+  status <- ledgerstatus+  account <- transactionaccountname+  let (ptype, account') = (postingTypeFromAccountName account, unbracket account)+  amount <- postingamount+  many spacenonewline+  comment <- ledgercomment <|> return ""+  newline+  return (Posting status account' amount comment ptype Nothing)++-- qualify with the parent account from parsing context+transactionaccountname :: GenParser Char LedgerFileCtx AccountName+transactionaccountname = liftM2 (++) getParentAccount ledgeraccountname++-- | Parse an account name. Account names may have single spaces inside+-- 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.+ledgeraccountname :: GenParser Char st AccountName+ledgeraccountname = do+    a <- many1 (nonspace <|> singlespace)+    let a' = striptrailingspace a+    when (accountNameFromComponents (accountNameComponents a') /= a')+         (fail $ "accountname seems ill-formed: "++a')+    return a'+    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++-- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace+--     <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"++-- | Parse an amount, with an optional left or right currency symbol and+-- optional price.+postingamount :: GenParser Char st MixedAmount+postingamount =+  try (do+        many1 spacenonewline+        someamount <|> return missingamt+      ) <|> return missingamt++someamount :: GenParser Char st MixedAmount+someamount = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount ++leftsymbolamount :: GenParser Char st MixedAmount+leftsymbolamount = do+  sym <- commoditysymbol +  sp <- many spacenonewline+  (q,p,comma) <- amountquantity+  pri <- priceamount+  let c = Commodity {symbol=sym,side=L,spaced=not $ null sp,comma=comma,precision=p}+  return $ Mixed [Amount c q pri]+  <?> "left-symbol amount"++rightsymbolamount :: GenParser Char st MixedAmount+rightsymbolamount = do+  (q,p,comma) <- amountquantity+  sp <- many spacenonewline+  sym <- commoditysymbol+  pri <- priceamount+  let c = Commodity {symbol=sym,side=R,spaced=not $ null sp,comma=comma,precision=p}+  return $ Mixed [Amount c q pri]+  <?> "right-symbol amount"++nosymbolamount :: GenParser Char st MixedAmount+nosymbolamount = do+  (q,p,comma) <- amountquantity+  pri <- priceamount+  let c = Commodity {symbol="",side=L,spaced=False,comma=comma,precision=p}+  return $ Mixed [Amount c q pri]+  <?> "no-symbol amount"++commoditysymbol :: GenParser Char st String+commoditysymbol = (quotedcommoditysymbol <|>+                   many1 (noneOf "0123456789-.@;\n \"")+                  ) <?> "commodity symbol"++quotedcommoditysymbol :: GenParser Char st String+quotedcommoditysymbol = do+  char '"'+  s <- many1 $ noneOf "-.@;\n \""+  char '"'+  return s++priceamount :: GenParser Char st (Maybe MixedAmount)+priceamount =+    try (do+          many spacenonewline+          char '@'+          many spacenonewline+          a <- someamount -- XXX could parse more prices ad infinitum, shouldn't+          return $ Just a+          ) <|> return Nothing++-- gawd.. trying to parse a ledger number without error:++-- | Parse a ledger-style numeric quantity and also return the number of+-- digits to the right of the decimal point and whether thousands are+-- separated by comma.+amountquantity :: GenParser Char st (Double, Int, Bool)+amountquantity = do+  sign <- optionMaybe $ string "-"+  (intwithcommas,frac) <- numberparts+  let comma = ',' `elem` intwithcommas+  let precision = length frac+  -- read the actual value. We expect this read to never fail.+  let int = filter (/= ',') intwithcommas+  let int' = if null int then "0" else int+  let frac' = if null frac then "0" else frac+  let sign' = fromMaybe "" sign+  let quantity = read $ sign'++int'++"."++frac'+  return (quantity, precision, comma)+  <?> "commodity quantity"++-- | parse the two strings of digits before and after a possible decimal+-- point.  The integer part may contain commas, or either part may be+-- empty, or there may be no point.+numberparts :: GenParser Char st (String,String)+numberparts = numberpartsstartingwithdigit <|> numberpartsstartingwithpoint++numberpartsstartingwithdigit :: GenParser Char st (String,String)+numberpartsstartingwithdigit = do+  let digitorcomma = digit <|> char ','+  first <- digit+  rest <- many digitorcomma+  frac <- try (do {char '.'; many digit}) <|> return ""+  return (first:rest,frac)+                     +numberpartsstartingwithpoint :: GenParser Char st (String,String)+numberpartsstartingwithpoint = do+  char '.'+  frac <- many1 digit+  return ("",frac)+                     ++-- | Parse a timelog entry.+timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry+timelogentry = do+  code <- oneOf "bhioO"+  many1 spacenonewline+  datetime <- ledgerdatetime+  comment <- optionMaybe (many1 spacenonewline >> liftM2 (++) getParentAccount restofline)+  return $ TimeLogEntry (read [code]) datetime (fromMaybe "" comment)+++-- | Parse a hledger display expression, which is a simple date test like+-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.+datedisplayexpr :: GenParser Char st (Posting -> Bool)+datedisplayexpr = do+  char 'd'+  op <- compareop+  char '['+  (y,m,d) <- smartdate+  char ']'+  let date    = parsedate $ printf "%04s/%02s/%02s" y m d+      test op = return $ (`op` date) . postingDate+  case op of+    "<"  -> test (<)+    "<=" -> test (<=)+    "="  -> test (==)+    "==" -> test (==)+    ">=" -> test (>=)+    ">"  -> test (>)+    _    -> mzero++compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]+++tests_Parse = TestList [++   "ledgerTransaction" ~: do+    assertParseEqual (parseWithCtx emptyCtx ledgerTransaction entry1_str) entry1+    assertBool "ledgerTransaction should not parse just a date"+                   $ isLeft $ parseWithCtx emptyCtx ledgerTransaction "2009/1/1\n"+    assertBool "ledgerTransaction should require some postings"+                   $ isLeft $ parseWithCtx emptyCtx ledgerTransaction "2009/1/1 a\n"+    let t = parseWithCtx emptyCtx ledgerTransaction "2009/1/1 a ;comment\n b 1\n"+    assertBool "ledgerTransaction should not include a comment in the description"+                   $ either (const False) ((== "a") . tdescription) t++  ,"ledgerModifierTransaction" ~: do+     assertParse (parseWithCtx emptyCtx ledgerModifierTransaction "= (some value expr)\n some:postings  1\n")++  ,"ledgerPeriodicTransaction" ~: do+     assertParse (parseWithCtx emptyCtx ledgerPeriodicTransaction "~ (some period expr)\n some:postings  1\n")++  ,"ledgerExclamationDirective" ~: do+     assertParse (parseWithCtx emptyCtx ledgerExclamationDirective "!include /some/file.x\n")+     assertParse (parseWithCtx emptyCtx ledgerExclamationDirective "!account some:account\n")+     assertParse (parseWithCtx emptyCtx (ledgerExclamationDirective >> ledgerExclamationDirective) "!account a\n!end\n")++  ,"ledgercommentline" ~: do+     assertParse (parseWithCtx emptyCtx ledgercommentline "; some comment \n")+     assertParse (parseWithCtx emptyCtx ledgercommentline " \t; x\n")+     assertParse (parseWithCtx emptyCtx ledgercommentline ";x")++  ,"ledgerDefaultYear" ~: do+     assertParse (parseWithCtx emptyCtx ledgerDefaultYear "Y 2010\n")+     assertParse (parseWithCtx emptyCtx ledgerDefaultYear "Y 10001\n")++  ,"ledgerHistoricalPrice" ~:+    assertParseEqual (parseWithCtx emptyCtx ledgerHistoricalPrice "P 2004/05/01 XYZ $55.00\n") (HistoricalPrice (parsedate "2004/05/01") "XYZ" $ Mixed [dollars 55])++  ,"ledgerIgnoredPriceCommodity" ~: do+     assertParse (parseWithCtx emptyCtx ledgerIgnoredPriceCommodity "N $\n")++  ,"ledgerDefaultCommodity" ~: do+     assertParse (parseWithCtx emptyCtx ledgerDefaultCommodity "D $1,000.0\n")++  ,"ledgerCommodityConversion" ~: do+     assertParse (parseWithCtx emptyCtx ledgerCommodityConversion "C 1h = $50.00\n")++  ,"ledgerTagDirective" ~: do+     assertParse (parseWithCtx emptyCtx ledgerTagDirective "tag foo \n")++  ,"ledgerEndTagDirective" ~: do+     assertParse (parseWithCtx emptyCtx ledgerEndTagDirective "end tag \n")++  ,"ledgeraccountname" ~: do+    assertBool "ledgeraccountname parses a normal accountname" (isRight $ parsewith ledgeraccountname "a:b:c")+    assertBool "ledgeraccountname rejects an empty inner component" (isLeft $ parsewith ledgeraccountname "a::c")+    assertBool "ledgeraccountname rejects an empty leading component" (isLeft $ parsewith ledgeraccountname ":b:c")+    assertBool "ledgeraccountname rejects an empty trailing component" (isLeft $ parsewith ledgeraccountname "a:b:")++ ,"ledgerposting" ~: do+    assertParseEqual (parseWithCtx emptyCtx ledgerposting "  expenses:food:dining  $10.00\n") +                     (Posting False "expenses:food:dining" (Mixed [dollars 10]) "" RegularPosting Nothing)+    assertBool "ledgerposting parses a quoted commodity with numbers"+                   (isRight $ parseWithCtx emptyCtx ledgerposting "  a  1 \"DE123\"\n")++  ,"someamount" ~: do+     let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity+         assertMixedAmountParse parseresult mixedamount =+             (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)+     assertMixedAmountParse (parsewith someamount "1 @ $2")+                            (Mixed [Amount unknown 1 (Just $ Mixed [Amount dollar{precision=0} 2 Nothing])])++  ,"postingamount" ~: do+    assertParseEqual (parseWithCtx emptyCtx postingamount " $47.18") (Mixed [dollars 47.18])+    assertParseEqual (parseWithCtx emptyCtx postingamount " $1.")+                (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,comma=False,precision=0} 1 Nothing])++ ]++entry1_str = unlines+ ["2007/01/28 coopportunity"+ ,"    expenses:food:groceries                   $47.18"+ ,"    assets:checking                          $-47.18"+ ,""+ ]++entry1 =+    txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""+     [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing, +      Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting Nothing] ""++
+ Hledger/Data/Posting.hs view
@@ -0,0 +1,94 @@+{-|++A 'Posting' represents a 'MixedAmount' being added to or subtracted from a+single 'Account'.  Each 'Transaction' contains two or more postings which+should add up to 0. Postings also reference their parent transaction, so+we can get a date or description for a posting (from the transaction).++-}++module Hledger.Data.Posting+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Amount+import Hledger.Data.AccountName+import Hledger.Data.Dates (nulldate)+++instance Show Posting where show = showPosting++nullposting = Posting False "" nullmixedamt "" RegularPosting Nothing++showPosting :: Posting -> String+showPosting (Posting{paccount=a,pamount=amt,pcomment=com,ptype=t}) =+    concatTopPadded [showaccountname a ++ " ", showamount amt, comment]+    where+      ledger3ishlayout = False+      acctnamewidth = if ledger3ishlayout then 25 else 22+      showaccountname = printf ("%-"++(show acctnamewidth)++"s") . bracket . elideAccountName width+      (bracket,width) = case t of+                          BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)+                          VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)+                          _ -> (id,acctnamewidth)+      showamount = padleft 12 . showMixedAmountOrZero+      comment = if null com then "" else "  ; " ++ com+-- XXX refactor+showPostingForRegister (Posting{paccount=a,pamount=amt,ptype=t}) =+    concatTopPadded [showaccountname a ++ " ", showamount amt]+    where+      ledger3ishlayout = False+      acctnamewidth = if ledger3ishlayout then 25 else 22+      showaccountname = printf ("%-"++(show acctnamewidth)++"s") . bracket . elideAccountName width+      (bracket,width) = case t of+                          BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)+                          VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)+                          _ -> (id,acctnamewidth)+      showamount = padleft 12 . showMixedAmountOrZeroWithoutPrice++isReal :: Posting -> Bool+isReal p = ptype p == RegularPosting++isVirtual :: Posting -> Bool+isVirtual p = ptype p == VirtualPosting++isBalancedVirtual :: Posting -> Bool+isBalancedVirtual p = ptype p == BalancedVirtualPosting++hasAmount :: Posting -> Bool+hasAmount = (/= missingamt) . pamount++postingTypeFromAccountName a+    | head a == '[' && last a == ']' = BalancedVirtualPosting+    | head a == '(' && last a == ')' = VirtualPosting+    | otherwise = RegularPosting++accountNamesFromPostings :: [Posting] -> [AccountName]+accountNamesFromPostings = nub . map paccount++sumPostings :: [Posting] -> MixedAmount+sumPostings = sumMixedAmountsPreservingHighestPrecision . map pamount++postingDate :: Posting -> Day+postingDate p = maybe nulldate tdate $ ptransaction p++postingCleared :: Posting -> Bool+postingCleared p = maybe False tstatus $ ptransaction p++-- | Does this posting fall within the given date span ?+isPostingInDateSpan :: DateSpan -> Posting -> Bool+isPostingInDateSpan (DateSpan Nothing Nothing)   _ = True+isPostingInDateSpan (DateSpan Nothing (Just e))  p = postingDate p < e+isPostingInDateSpan (DateSpan (Just b) Nothing)  p = postingDate p >= b+isPostingInDateSpan (DateSpan (Just b) (Just e)) p = d >= b && d < e where d = postingDate p++isEmptyPosting :: Posting -> Bool+isEmptyPosting = isZeroMixedAmount . pamount++-- | Get the minimal date span which contains all the postings, or+-- DateSpan Nothing Nothing if there are none.+postingsDateSpan :: [Posting] -> DateSpan+postingsDateSpan [] = DateSpan Nothing Nothing+postingsDateSpan ps = DateSpan (Just $ postingDate $ head ps') (Just $ addDays 1 $ postingDate $ last ps')+    where ps' = sortBy (comparing postingDate) ps+
+ Hledger/Data/TimeLog.hs view
@@ -0,0 +1,121 @@+{-|++A 'TimeLogEntry' is a clock-in, clock-out, or other directive in a timelog+file (see timeclock.el or the command-line version). These can be+converted to 'Transactions' and queried like a ledger.++-}++module Hledger.Data.TimeLog+where+import Data.Time.Format+import System.Locale (defaultTimeLocale)+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Dates+import Hledger.Data.Commodity+import Hledger.Data.Transaction++instance Show TimeLogEntry where +    show t = printf "%s %s %s" (show $ tlcode t) (show $ tldatetime t) (tlcomment t)++instance Show TimeLogCode where +    show SetBalance = "b"+    show SetRequiredHours = "h"+    show In = "i"+    show Out = "o"+    show FinalOut = "O"++instance Read TimeLogCode where +    readsPrec _ ('b' : xs) = [(SetBalance, xs)]+    readsPrec _ ('h' : xs) = [(SetRequiredHours, xs)]+    readsPrec _ ('i' : xs) = [(In, xs)]+    readsPrec _ ('o' : xs) = [(Out, xs)]+    readsPrec _ ('O' : xs) = [(FinalOut, xs)]+    readsPrec _ _ = []++-- | Convert time log entries to ledger transactions. When there is no+-- clockout, add one with the provided current time. Sessions crossing+-- midnight are split into days to give accurate per-day totals.+timeLogEntriesToTransactions :: LocalTime -> [TimeLogEntry] -> [Transaction]+timeLogEntriesToTransactions _ [] = []+timeLogEntriesToTransactions now [i]+    | odate > idate = entryFromTimeLogInOut i o' : timeLogEntriesToTransactions now [i',o]+    | otherwise = [entryFromTimeLogInOut i o]+    where+      o = TimeLogEntry Out end ""+      end = if itime > now then itime else now+      (itime,otime) = (tldatetime i,tldatetime o)+      (idate,odate) = (localDay itime,localDay otime)+      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}+      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}+timeLogEntriesToTransactions now (i:o:rest)+    | odate > idate = entryFromTimeLogInOut i o' : timeLogEntriesToTransactions now (i':o:rest)+    | otherwise = entryFromTimeLogInOut i o : timeLogEntriesToTransactions now rest+    where+      (itime,otime) = (tldatetime i,tldatetime o)+      (idate,odate) = (localDay itime,localDay otime)+      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}+      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}++-- | Convert a timelog clockin and clockout entry to an equivalent ledger+-- entry, representing the time expenditure. Note this entry is  not balanced,+-- since we omit the \"assets:time\" transaction for simpler output.+entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction+entryFromTimeLogInOut i o+    | otime >= itime = t+    | otherwise = +        error $ "clock-out time less than clock-in time in:\n" ++ showTransaction t+    where+      t = Transaction {+            tdate         = idate,+            teffectivedate = Nothing,+            tstatus       = True,+            tcode         = "",+            tdescription  = showtime itod ++ "-" ++ showtime otod,+            tcomment      = "",+            tpostings = ps,+            tpreceding_comment_lines=""+          }+      showtime = take 5 . show+      acctname = tlcomment i+      itime    = tldatetime i+      otime    = tldatetime o+      itod     = localTimeOfDay itime+      otod     = localTimeOfDay otime+      idate    = localDay itime+      hrs      = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc+      amount   = Mixed [hours hrs]+      ps       = [Posting{pstatus=False,paccount=acctname,pamount=amount,+                          pcomment="",ptype=VirtualPosting,ptransaction=Just t}]++tests_TimeLog = TestList [++   "timeLogEntriesToTransactions" ~: do+     today <- getCurrentDay+     now' <- getCurrentTime+     tz <- getCurrentTimeZone+     let now = utcToLocalTime tz now'+         nowstr = showtime now+         yesterday = prevday today+         clockin = TimeLogEntry 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)++     assertEntriesGiveStrings "started yesterday, split session at midnight"+                                  [clockin (mktime yesterday "23:00:00") ""]+                                  ["23:00-23:59","00:00-"++nowstr]+     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") ""] +                                  ["00:00-"++nowstr]+     let future = utcToLocalTime tz $ addUTCTime 100 now'+         futurestr = showtime future+     assertEntriesGiveStrings "use the clockin time for auto-clockout if it's in the future"+                                  [clockin future ""]+                                  [printf "%s-%s" futurestr futurestr]++ ]
+ Hledger/Data/Transaction.hs view
@@ -0,0 +1,253 @@+{-|++A 'Transaction' represents a single balanced entry in the ledger file. It+normally contains two or more balanced 'Posting's.++-}++module Hledger.Data.Transaction+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Dates+import Hledger.Data.Posting+import Hledger.Data.Amount+import Hledger.Data.Commodity (dollars, dollar, unknown)++instance Show Transaction where show = showTransactionUnelided++instance Show ModifierTransaction where +    show t = "= " ++ mtvalueexpr t ++ "\n" ++ unlines (map show (mtpostings t))++instance Show PeriodicTransaction where +    show t = "~ " ++ ptperiodicexpr t ++ "\n" ++ unlines (map show (ptpostings t))++nulltransaction :: Transaction+nulltransaction = Transaction {+                    tdate=nulldate,+                    teffectivedate=Nothing, +                    tstatus=False, +                    tcode="", +                    tdescription="", +                    tcomment="",+                    tpostings=[],+                    tpreceding_comment_lines=""+                  }++{-|+Show a ledger entry, formatted for the print command. ledger 2.x's+standard format looks like this:++@+yyyy/mm/dd[ *][ CODE] description.........          [  ; comment...............]+    account name 1.....................  ...$amount1[  ; comment...............]+    account name 2.....................  ..$-amount1[  ; comment...............]++pcodewidth    = no limit -- 10          -- mimicking ledger layout.+pdescwidth    = no limit -- 20          -- I don't remember what these mean,+pacctwidth    = 35 minimum, no maximum  -- they were important at the time.+pamtwidth     = 11+pcommentwidth = no limit -- 22+@+-}+showTransaction :: Transaction -> String+showTransaction = showTransaction' True False++showTransactionUnelided :: Transaction -> String+showTransactionUnelided = showTransaction' False False++showTransactionForPrint :: Bool -> Transaction -> String+showTransactionForPrint effective = showTransaction' False effective++showTransaction' :: Bool -> Bool -> Transaction -> String+showTransaction' elide effective t =+    unlines $ [description] ++ showpostings (tpostings t) ++ [""]+    where+      description = concat [date, status, code, desc, comment]+      date | effective = showdate $ fromMaybe (tdate t) $ teffectivedate t+           | otherwise = showdate (tdate t) ++ maybe "" showedate (teffectivedate t)+      status = if tstatus t then " *" else ""+      code = if length (tcode t) > 0 then printf " (%s)" $ tcode t else ""+      desc = ' ' : tdescription t+      comment = if null com then "" else "  ; " ++ com where com = tcomment t+      showdate = printf "%-10s" . showDate+      showedate = printf "=%s" . showdate+      showpostings ps+          | elide && length ps > 1 && isTransactionBalanced t+              = map showposting (init ps) ++ [showpostingnoamt (last ps)]+          | otherwise = map showposting ps+          where+            showposting p = showacct p ++ "  " ++ showamount (pamount p) ++ showcomment (pcomment p)+            showpostingnoamt p = rstrip $ showacct p ++ "              " ++ showcomment (pcomment p)+            showacct p = "    " ++ showstatus p ++ printf (printf "%%-%ds" w) (showAccountName Nothing (ptype p) (paccount p))+            w = maximum $ map (length . paccount) ps+            showamount = printf "%12s" . showMixedAmountOrZero+            showcomment s = if null s then "" else "  ; "++s+            showstatus p = if pstatus p then "* " else ""++-- | Show an account name, clipped to the given width if any, and+-- appropriately bracketed/parenthesised for the given posting type.+showAccountName :: Maybe Int -> PostingType -> AccountName -> String+showAccountName w = fmt+    where+      fmt RegularPosting = take w'+      fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse+      fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse+      w' = fromMaybe 999999 w+      parenthesise s = "("++s++")"+      bracket s = "["++s++"]"++realPostings :: Transaction -> [Posting]+realPostings = filter isReal . tpostings++virtualPostings :: Transaction -> [Posting]+virtualPostings = filter isVirtual . tpostings++balancedVirtualPostings :: Transaction -> [Posting]+balancedVirtualPostings = filter isBalancedVirtual . tpostings++-- | Get the sums of a transaction's real, virtual, and balanced virtual postings.+transactionPostingBalances :: Transaction -> (MixedAmount,MixedAmount,MixedAmount)+transactionPostingBalances t = (sumPostings $ realPostings t+                               ,sumPostings $ virtualPostings t+                               ,sumPostings $ balancedVirtualPostings t)++-- | Is this transaction balanced ? A balanced transaction's real+-- (non-virtual) postings sum to 0, and any balanced virtual postings+-- also sum to 0.+isTransactionBalanced :: Transaction -> Bool+isTransactionBalanced t = isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum+    where (rsum, _, bvsum) = transactionPostingBalances t++-- | Ensure that this entry is balanced, possibly auto-filling a missing+-- amount first. We can auto-fill if there is just one non-virtual+-- transaction without an amount. The auto-filled balance will be+-- converted to cost basis if possible. If the entry can not be balanced,+-- return an error message instead.+balanceTransaction :: Transaction -> Either String Transaction+balanceTransaction t@Transaction{tpostings=ps}+    | length rwithoutamounts > 1 || length bvwithoutamounts > 1+        = Left $ printerr "could not balance this transaction (too many missing amounts)"+    | not $ isTransactionBalanced t' = Left $ printerr $ nonzerobalanceerror t'+    | otherwise = Right t'+    where+      rps = filter isReal ps+      bvps = filter isBalancedVirtual ps+      (rwithamounts, rwithoutamounts) = partition hasAmount rps+      (bvwithamounts, bvwithoutamounts) = partition hasAmount bvps+      t' = t{tpostings=map balance ps}+          where +            balance p | not (hasAmount p) && isReal p+                          = p{pamount = costOfMixedAmount (-(sum $ map pamount rwithamounts))}+                      | not (hasAmount p) && isBalancedVirtual p+                          = p{pamount = costOfMixedAmount (-(sum $ map pamount bvwithamounts))}+                      | otherwise = p+      printerr s = intercalate "\n" [s, showTransactionUnelided t]++nonzerobalanceerror :: Transaction -> String+nonzerobalanceerror t = printf "could not balance this transaction (%s%s%s)" rmsg sep bvmsg+    where+      (rsum, _, bvsum) = transactionPostingBalances t+      rmsg | isReallyZeroMixedAmountCost rsum = ""+           | otherwise = "real postings are off by " ++ show rsum+      bvmsg | isReallyZeroMixedAmountCost bvsum = ""+            | otherwise = "balanced virtual postings are off by " ++ show bvsum+      sep = if not (null rmsg) && not (null bvmsg) then "; " else ""++-- | Convert the primary date to either the actual or effective date.+ledgerTransactionWithDate :: WhichDate -> Transaction -> Transaction+ledgerTransactionWithDate ActualDate t = t+ledgerTransactionWithDate EffectiveDate t = txnTieKnot t{tdate=fromMaybe (tdate t) (teffectivedate t)}+    ++-- | Ensure a transaction's postings refer back to it.+txnTieKnot :: Transaction -> Transaction+txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (settxn t) ps}++-- | Set a posting's parent transaction.+settxn :: Transaction -> Posting -> Posting+settxn t p = p{ptransaction=Just t}++tests_Transaction = TestList [+  "showTransaction" ~: do+     assertEqual "show a balanced transaction, eliding last amount"+       (unlines+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries        $47.18"+        ,"    assets:checking"+        ,""+        ])+       (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""+                [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting (Just t)+                ,Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting (Just t)+                ] ""+        in showTransaction t)++  ,"showTransaction" ~: do+     assertEqual "show a balanced transaction, no eliding"+       (unlines+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries        $47.18"+        ,"    assets:checking               $-47.18"+        ,""+        ])+       (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""+                [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting (Just t)+                ,Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting (Just t)+                ] ""+        in showTransactionUnelided t)++     -- document some cases that arise in debug/testing:+  ,"showTransaction" ~: do+     assertEqual "show an unbalanced transaction, should not elide"+       (unlines+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries        $47.18"+        ,"    assets:checking               $-47.19"+        ,""+        ])+       (showTransaction+        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""+         [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing+         ,Posting False "assets:checking" (Mixed [dollars (-47.19)]) "" RegularPosting Nothing+         ] ""))++  ,"showTransaction" ~: do+     assertEqual "show an unbalanced transaction with one posting, should not elide"+       (unlines+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries        $47.18"+        ,""+        ])+       (showTransaction+        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""+         [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing+         ] ""))++  ,"showTransaction" ~: do+     assertEqual "show a transaction with one posting and a missing amount"+       (unlines+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries              "+        ,""+        ])+       (showTransaction+        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""+         [Posting False "expenses:food:groceries" missingamt "" RegularPosting Nothing+         ] ""))++  ,"showTransaction" ~: do+     assertEqual "show a transaction with a priced commodityless amount"+       (unlines+        ["2010/01/01 x"+        ,"    a        1 @ $2"+        ,"    b              "+        ,""+        ])+       (showTransaction+        (txnTieKnot $ Transaction (parsedate "2010/01/01") Nothing False "" "x" ""+         [Posting False "a" (Mixed [Amount unknown 1 (Just $ Mixed [Amount dollar{precision=0} 2 Nothing])]) "" RegularPosting Nothing+         ,Posting False "b" missingamt "" RegularPosting Nothing+         ] ""))++  ]
+ Hledger/Data/Types.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-|++Most data types are defined here to avoid import cycles.+Here is an overview of the hledger data model:++> Journal                  -- a journal is derived from one or more data files. It contains..+>  [Transaction]           -- journal transactions, which have date, status, code, description and..+>   [Posting]              -- multiple account postings (entries), which have account name and amount.+>  [HistoricalPrice]       -- historical commodity prices+>+> Ledger                   -- a ledger is derived from a journal, by applying a filter specification. It contains..+>  Journal                 -- the filtered journal, containing only the transactions and postings we are interested in+>  Tree AccountName        -- account names referenced in the journal's transactions, as a tree+>  Map AccountName Account -- per-account postings and balances from the journal's transactions, as a  map from account name to account info++For more detailed documentation on each type, see the corresponding modules.++Evolution of transaction/entry/posting terminology:++  - ledger 2:    entries contain transactions++  - hledger 0.4: Entrys contain RawTransactions (which are flattened to Transactions)++  - ledger 3:    transactions contain postings++  - hledger 0.5: LedgerTransactions contain Postings (which are flattened to Transactions)++  - hledger 0.8: Transactions contain Postings (referencing Transactions, corecursively)++  - hledger 0.10: Postings should be called Entrys, but are left as-is for now++-}++module Hledger.Data.Types+where+import Hledger.Data.Utils+import qualified Data.Map as Map+import System.Time (ClockTime)+import Data.Typeable (Typeable)+++type SmartDate = (String,String,String)++data WhichDate = ActualDate | EffectiveDate deriving (Eq,Show)++data DateSpan = DateSpan (Maybe Day) (Maybe Day) deriving (Eq,Show,Ord)++data Interval = NoInterval | Daily | Weekly | Monthly | Quarterly | Yearly +                deriving (Eq,Show,Ord)++type AccountName = String++data Side = L | R deriving (Eq,Show,Ord) ++data Commodity = Commodity {+      symbol :: String,  -- ^ the commodity's symbol+      -- display preferences for amounts of this commodity+      side :: Side,      -- ^ should the symbol appear on the left or the right+      spaced :: Bool,    -- ^ should there be a space between symbol and quantity+      comma :: Bool,     -- ^ should thousands be comma-separated+      precision :: Int   -- ^ number of decimal places to display+    } deriving (Eq,Show,Ord)++data Amount = Amount {+      commodity :: Commodity,+      quantity :: Double,+      price :: Maybe MixedAmount  -- ^ unit price/conversion rate for this amount at posting time+    } deriving (Eq)++newtype MixedAmount = Mixed [Amount] deriving (Eq)++data PostingType = RegularPosting | VirtualPosting | BalancedVirtualPosting+                   deriving (Eq,Show)++data Posting = Posting {+      pstatus :: Bool,+      paccount :: AccountName,+      pamount :: MixedAmount,+      pcomment :: String,+      ptype :: PostingType,+      ptransaction :: Maybe Transaction  -- ^ this posting's parent transaction (co-recursive types).+                                        -- Tying this knot gets tedious, Maybe makes it easier/optional.+    }++-- The equality test for postings ignores the parent transaction's+-- identity, to avoid infinite loops.+instance Eq Posting where+    (==) (Posting a1 b1 c1 d1 e1 _) (Posting a2 b2 c2 d2 e2 _) =  a1==a2 && b1==b2 && c1==c2 && d1==d2 && e1==e2++data Transaction = Transaction {+      tdate :: Day,+      teffectivedate :: Maybe Day,+      tstatus :: Bool,  -- XXX tcleared ?+      tcode :: String,+      tdescription :: String,+      tcomment :: String,+      tpostings :: [Posting],            -- ^ this transaction's postings (co-recursive types).+      tpreceding_comment_lines :: String+    } deriving (Eq)++data ModifierTransaction = ModifierTransaction {+      mtvalueexpr :: String,+      mtpostings :: [Posting]+    } deriving (Eq)++data PeriodicTransaction = PeriodicTransaction {+      ptperiodicexpr :: String,+      ptpostings :: [Posting]+    } deriving (Eq)++data TimeLogCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord) ++data TimeLogEntry = TimeLogEntry {+      tlcode :: TimeLogCode,+      tldatetime :: LocalTime,+      tlcomment :: String+    } deriving (Eq,Ord)++data HistoricalPrice = HistoricalPrice {+      hdate :: Day,+      hsymbol :: String,+      hamount :: MixedAmount+    } deriving (Eq) -- & Show (in Amount.hs)++data Journal = Journal {+      jmodifiertxns :: [ModifierTransaction],+      jperiodictxns :: [PeriodicTransaction],+      jtxns :: [Transaction],+      open_timelog_entries :: [TimeLogEntry],+      historical_prices :: [HistoricalPrice],+      final_comment_lines :: String,+      filepath :: FilePath,+      filereadtime :: ClockTime,+      jtext :: String+    } deriving (Eq, Typeable)++data Ledger = Ledger {+      journal :: Journal,+      accountnametree :: Tree AccountName,+      accountmap :: Map.Map AccountName Account+    }++data Account = Account {+      aname :: AccountName,+      apostings :: [Posting],    -- ^ postings in this account+      abalance :: MixedAmount    -- ^ sum of postings in this account and subaccounts+    }++-- | A generic, pure specification of how to filter transactions and postings.+data FilterSpec = FilterSpec {+     datespan  :: DateSpan   -- ^ only include if in this date span+    ,cleared   :: Maybe Bool -- ^ only include if cleared\/uncleared\/don't care+    ,real      :: Bool       -- ^ only include if real\/don't care+    ,empty     :: Bool       -- ^ include if empty (ie amount is zero)+    ,costbasis :: Bool       -- ^ convert all amounts to cost basis+    ,acctpats  :: [String]   -- ^ only include if matching these account patterns+    ,descpats  :: [String]   -- ^ only include if matching these description patterns+    ,whichdate :: WhichDate  -- ^ which dates to use (actual or effective)+    ,depth     :: Maybe Int+    } deriving (Show)+
+ Hledger/Data/Utils.hs view
@@ -0,0 +1,323 @@+{-# LANGUAGE CPP #-}+{-|++Provide standard imports and utilities which are useful everywhere, or+needed low in the module hierarchy. This is the bottom of the dependency graph.++-}++module Hledger.Data.Utils (+module Data.Char,+module Control.Monad,+module Data.List,+--module Data.Map,+module Data.Maybe,+module Data.Ord,+module Data.Tree,+module Data.Time.Clock,+module Data.Time.Calendar,+module Data.Time.LocalTime,+module Debug.Trace,+module Hledger.Data.Utils,+module Text.Printf,+module Text.RegexPR,+module Test.HUnit,+)+where+import Data.Char+import Control.Exception+import Control.Monad+import Data.List+--import qualified Data.Map as Map+import Data.Maybe+import Data.Ord+import Data.Tree+import Data.Time.Clock+import Data.Time.Calendar+import Data.Time.LocalTime+import Debug.Trace+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding (readFile,putStr,print)+import System.IO.UTF8+#endif+import Test.HUnit+import Text.Printf+import Text.RegexPR+import Text.ParserCombinators.Parsec+++-- strings++lowercase = map toLower+uppercase = map toUpper++strip = lstrip . rstrip+lstrip = dropws+rstrip = reverse . dropws . reverse+dropws = dropWhile (`elem` " \t")++elideLeft width s =+    if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s++elideRight width s =+    if length s > width then take (width - 2) s ++ ".." else s++underline :: String -> String+underline s = s' ++ replicate (length s) '-' ++ "\n"+    where s'+            | last s == '\n' = s+            | otherwise = s ++ "\n"++unbracket :: String -> String+unbracket s+    | (head s == '[' && last s == ']') || (head s == '(' && last s == ')') = init $ tail s+    | otherwise = s++-- | Join multi-line strings as side-by-side rectangular strings of the same height, top-padded.+concatTopPadded :: [String] -> String+concatTopPadded strs = intercalate "\n" $ map concat $ transpose padded+    where+      lss = map lines strs+      h = maximum $ map length lss+      ypad ls = replicate (difforzero h (length ls)) "" ++ ls+      xpad ls = map (padleft w) ls where w | null ls = 0+                                           | otherwise = maximum $ map length ls+      padded = map (xpad . ypad) lss++-- | Join multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.+concatBottomPadded :: [String] -> String+concatBottomPadded strs = intercalate "\n" $ map concat $ transpose padded+    where+      lss = map lines strs+      h = maximum $ map length lss+      ypad ls = ls ++ replicate (difforzero h (length ls)) ""+      xpad ls = map (padleft w) ls where w | null ls = 0+                                           | otherwise = maximum $ map length ls+      padded = map (xpad . ypad) lss++-- | Compose strings vertically and right-aligned.+vConcatRightAligned :: [String] -> String+vConcatRightAligned ss = intercalate "\n" $ map showfixedwidth ss+    where+      showfixedwidth = printf (printf "%%%ds" width)+      width = maximum $ map length ss++-- | Convert a multi-line string to a rectangular string top-padded to the specified height.+padtop :: Int -> String -> String+padtop h s = intercalate "\n" xpadded+    where+      ls = lines s+      sh = length ls+      sw | null ls = 0+         | otherwise = maximum $ map length ls+      ypadded = replicate (difforzero h sh) "" ++ ls+      xpadded = map (padleft sw) ypadded++-- | Convert a multi-line string to a rectangular string bottom-padded to the specified height.+padbottom :: Int -> String -> String+padbottom h s = intercalate "\n" xpadded+    where+      ls = lines s+      sh = length ls+      sw | null ls = 0+         | otherwise = maximum $ map length ls+      ypadded = ls ++ replicate (difforzero h sh) ""+      xpadded = map (padleft sw) ypadded++-- | Convert a multi-line string to a rectangular string left-padded to the specified width.+padleft :: Int -> String -> String+padleft w "" = concat $ replicate w " "+padleft w s = intercalate "\n" $ map (printf (printf "%%%ds" w)) $ lines s++-- | Convert a multi-line string to a rectangular string right-padded to the specified width.+padright :: Int -> String -> String+padright w "" = concat $ replicate w " "+padright w s = intercalate "\n" $ map (printf (printf "%%-%ds" w)) $ lines s++-- | Clip a multi-line string to the specified width and height from the top left.+cliptopleft :: Int -> Int -> String -> String+cliptopleft w h = intercalate "\n" . take h . map (take w) . lines++-- | Clip and pad a multi-line string to fill the specified width and height.+fitto :: Int -> Int -> String -> String+fitto w h s = intercalate "\n" $ take h $ rows ++ repeat blankline+    where+      rows = map (fit w) $ lines s+      fit w = take w . (++ repeat ' ')+      blankline = replicate w ' '++-- math++difforzero :: (Num a, Ord a) => a -> a -> a+difforzero a b = maximum [(a - b), 0]++-- regexps++containsRegex :: String -> String -> Bool+containsRegex r s = case matchRegexPR ("(?i)"++r) s of+                      Just _ -> True+                      _ -> False+++-- 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'++-- trees++root = rootLabel+subs = subForest+branches = subForest++-- | List just the leaf nodes of a tree+leaves :: Tree a -> [a]+leaves (Node v []) = [v]+leaves (Node _ branches) = concatMap leaves branches++-- | get the sub-tree rooted at the first (left-most, depth-first) occurrence+-- of the specified node value+subtreeat :: Eq a => a -> Tree a -> Maybe (Tree a)+subtreeat v t+    | root t == v = Just t+    | otherwise = subtreeinforest v $ subs t++-- | get the sub-tree for the specified node value in the first tree in+-- forest in which it occurs.+subtreeinforest :: Eq a => a -> [Tree a] -> Maybe (Tree a)+subtreeinforest _ [] = Nothing+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) []+treeprune d t = Node (root t) (map (treeprune $ d-1) $ branches t)++-- | apply f to all tree nodes+treemap :: (a -> b) -> Tree a -> Tree b+treemap f t = Node (f $ root t) (map (treemap f) $ branches t)++-- | remove all subtrees whose nodes do not fulfill predicate+treefilter :: (a -> Bool) -> Tree a -> Tree a+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. +-- treedropall -- do this repeatedly.++-- | show a compact ascii representation of a tree+showtree :: Show a => Tree a -> String+showtree = unlines . filter (containsRegex "[^ \\|]") . lines . drawTree . treemap show++-- | show a compact ascii representation of a forest+showforest :: Show a => Forest a -> String+showforest = concatMap showtree++-- debugging++-- | trace (print on stdout at runtime) a showable expression+-- (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 newline and a label prepended+ltrace :: Show a => String -> a -> a+ltrace l a = trace (l ++ ": " ++ show a) a++-- | trace an expression using a custom show function+tracewith f e = trace (f e) e++-- parsing++choice' = choice . map Text.ParserCombinators.Parsec.try++parsewith :: Parser a -> String -> Either ParseError a+parsewith p = parse p ""++parseWithCtx :: b -> GenParser Char b a -> String -> Either ParseError a+parseWithCtx ctx p = runParser p ctx ""++fromparse :: Either ParseError a -> a+fromparse = either parseerror id++parseerror e = error $ showParseError e++showParseError e = "parse error at " ++ show e++showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e)++nonspace :: GenParser Char st Char+nonspace = satisfy (not . isSpace)++spacenonewline :: GenParser Char st Char+spacenonewline = satisfy (`elem` " \v\f\t")++restofline :: GenParser Char st String+restofline = anyChar `manyTill` newline++-- time++getCurrentLocalTime :: IO LocalTime+getCurrentLocalTime = do+  t <- getCurrentTime+  tz <- getCurrentTimeZone+  return $ utcToLocalTime tz t++-- testing++-- | Get a Test's label, or the empty string.+tname :: Test -> String+tname (TestLabel n _) = n+tname _ = ""++-- | Flatten a Test containing TestLists into a list of single tests.+tflatten :: Test -> [Test]+tflatten (TestLabel _ t@(TestList _)) = tflatten t+tflatten (TestList ts) = concatMap tflatten ts+tflatten t = [t]++-- | Filter TestLists in a Test, recursively, preserving the structure.+tfilter :: (Test -> Bool) -> Test -> Test+tfilter p (TestLabel l ts) = TestLabel l (tfilter p ts)+tfilter p (TestList ts) = TestList $ filter (any p . tflatten) $ map (tfilter p) ts+tfilter _ t = t++-- | Simple way to assert something is some expected value, with no label.+is :: (Eq a, Show a) => a -> a -> Assertion+a `is` e = assertEqual "" e a++-- | Assert a parse result is successful, printing the parse error on failure.+assertParse :: (Either ParseError a) -> Assertion+assertParse parse = either (assertFailure.show) (const (return ())) parse++-- | Assert a parse result is some expected value, printing the parse error on failure.+assertParseEqual :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion+assertParseEqual parse expected = either (assertFailure.show) (`is` expected) parse++printParseError :: (Show a) => a -> IO ()+printParseError e = do putStr "parse error at "; print e+++-- misc++isLeft :: Either a b -> Bool+isLeft (Left _) = True+isLeft _        = False++isRight :: Either a b -> Bool+isRight = not . isLeft++strictReadFile :: FilePath -> IO String+strictReadFile f = readFile f >>= \s -> Control.Exception.evaluate (length s) >> return s
− Ledger.hs
@@ -1,58 +0,0 @@-{-| --The Ledger library allows parsing and querying of ledger files.  It-generally provides a compatible subset of C++ ledger's functionality.-This package re-exports all the Ledger.* modules.---}--module Ledger (-               module Ledger.Account,-               module Ledger.AccountName,-               module Ledger.Amount,-               module Ledger.Commodity,-               module Ledger.Dates,-               module Ledger.IO,-               module Ledger.Transaction,-               module Ledger.Ledger,-               module Ledger.Parse,-               module Ledger.Journal,-               module Ledger.Posting,-               module Ledger.TimeLog,-               module Ledger.Types,-               module Ledger.Utils,-               tests_Ledger-              )-where-import Ledger.Account-import Ledger.AccountName-import Ledger.Amount-import Ledger.Commodity-import Ledger.Dates-import Ledger.IO-import Ledger.Transaction-import Ledger.Ledger-import Ledger.Parse-import Ledger.Journal-import Ledger.Posting-import Ledger.TimeLog-import Ledger.Types-import Ledger.Utils--tests_Ledger = TestList-    [-    --  Ledger.Account.tests_Account-    -- ,Ledger.AccountName.tests_AccountName-     Ledger.Amount.tests_Amount-    -- ,Ledger.Commodity.tests_Commodity-    ,Ledger.Dates.tests_Dates-    -- ,Ledger.IO.tests_IO-    ,Ledger.Transaction.tests_Transaction-    -- ,Ledger.Ledger.tests_Ledger-    ,Ledger.Parse.tests_Parse-    -- ,Ledger.Journal.tests_Journal-    -- ,Ledger.Posting.tests_Posting-    -- ,Ledger.TimeLog.tests_TimeLog-    -- ,Ledger.Types.tests_Types-    -- ,Ledger.Utils.tests_Utils-    ]
− Ledger/Account.hs
@@ -1,27 +0,0 @@-{-|--A compound data type for efficiency. An 'Account' stores--- an 'AccountName',--- all 'Posting's in the account, excluding subaccounts--- a 'MixedAmount' representing the account balance, including subaccounts.---}--module Ledger.Account-where-import Ledger.Utils-import Ledger.Types-import Ledger.Amount---instance Show Account where-    show (Account a ts b) = printf "Account %s with %d txns and %s balance" a (length ts) (showMixedAmount b)--instance Eq Account where-    (==) (Account n1 t1 b1) (Account n2 t2 b2) = n1 == n2 && t1 == t2 && b1 == b2--nullacct = Account "" [] nullmixedamt-
− Ledger/AccountName.hs
@@ -1,171 +0,0 @@-{-# LANGUAGE NoMonomorphismRestriction#-}-{-|--'AccountName's are strings like @assets:cash:petty@.-From a set of these we derive the account hierarchy.---}--module Ledger.AccountName-where-import Ledger.Utils-import Ledger.Types-import Data.Map (Map)-import qualified Data.Map as M------ change to use a different separator for nested accounts-acctsepchar = ':'--accountNameComponents :: AccountName -> [String]-accountNameComponents = splitAtElement acctsepchar--accountNameFromComponents :: [String] -> AccountName-accountNameFromComponents = concat . intersperse [acctsepchar]--accountLeafName :: AccountName -> String-accountLeafName = last . accountNameComponents--accountNameLevel :: AccountName -> Int-accountNameLevel "" = 0-accountNameLevel a = length (filter (==acctsepchar) a) + 1---- | ["a:b:c","d:e"] -> ["a","a:b","a:b:c","d","d:e"]-expandAccountNames :: [AccountName] -> [AccountName]-expandAccountNames as = nub $ concatMap expand as-    where expand = map accountNameFromComponents . tail . inits . accountNameComponents---- | ["a:b:c","d:e"] -> ["a","d"]-topAccountNames :: [AccountName] -> [AccountName]-topAccountNames as = [a | a <- expandAccountNames as, accountNameLevel a == 1]--parentAccountName :: AccountName -> AccountName-parentAccountName = accountNameFromComponents . init . accountNameComponents--parentAccountNames :: AccountName -> [AccountName]-parentAccountNames a = parentAccountNames' $ parentAccountName a-    where-      parentAccountNames' "" = []-      parentAccountNames' a = a : parentAccountNames' (parentAccountName a)--isAccountNamePrefixOf :: AccountName -> AccountName -> Bool-isAccountNamePrefixOf = isPrefixOf . (++ [acctsepchar])--isSubAccountNameOf :: AccountName -> AccountName -> Bool-s `isSubAccountNameOf` p = -    (p `isAccountNamePrefixOf` s) && (accountNameLevel s == (accountNameLevel p + 1))---- | From a list of account names, select those which are direct--- subaccounts of the given account name.-subAccountNamesFrom :: [AccountName] -> AccountName -> [AccountName]-subAccountNamesFrom accts a = filter (`isSubAccountNameOf` a) accts---- | Convert a list of account names to a tree.-accountNameTreeFrom :: [AccountName] -> Tree AccountName-accountNameTreeFrom = accountNameTreeFrom1--accountNameTreeFrom1 accts = -    Node "top" (accounttreesfrom (topAccountNames accts))-        where-          accounttreesfrom :: [AccountName] -> [Tree AccountName]-          accounttreesfrom [] = []-          accounttreesfrom as = [Node a (accounttreesfrom $ subs a) | a <- as]-          subs = subAccountNamesFrom (expandAccountNames accts)--nullaccountnametree = Node "top" []--accountNameTreeFrom2 accts = -   Node "top" $ unfoldForest (\a -> (a, subs a)) $ topAccountNames accts-        where-          subs = subAccountNamesFrom allaccts-          allaccts = expandAccountNames accts-          -- subs' a = subsmap ! a-          -- subsmap :: Map AccountName [AccountName]-          -- subsmap = Data.Map.fromList [(a, subAccountNamesFrom allaccts a) | a <- allaccts]--accountNameTreeFrom3 accts = -    Node "top" $ forestfrom allaccts $ topAccountNames accts-        where-          -- drop accts from the list of potential subs as we add them to the tree-          forestfrom :: [AccountName] -> [AccountName] -> Forest AccountName-          forestfrom subaccts accts = -              [let subaccts' = subaccts \\ accts in Node a $ forestfrom subaccts' (subAccountNamesFrom subaccts' a) | a <- accts]-          allaccts = expandAccountNames accts-          ---- a more efficient tree builder from Cale Gibbard-newtype Tree' a = T (Map a (Tree' a))-  deriving (Show, Eq, Ord)--mergeTrees :: (Ord a) => Tree' a -> Tree' a -> Tree' a-mergeTrees (T m) (T m') = T (M.unionWith mergeTrees m m')--emptyTree = T M.empty--pathtree :: [a] -> Tree' a-pathtree []     = T M.empty-pathtree (x:xs) = T (M.singleton x (pathtree xs))--fromPaths :: (Ord a) => [[a]] -> Tree' a-fromPaths = foldl' mergeTrees emptyTree . map pathtree---- the above, but trying to build Tree directly---- mergeTrees' :: (Ord a) => Tree a -> Tree a -> Tree a--- mergeTrees' (Node m ms) (Node m' ms') = Node undefined (ms `union` ms')---- emptyTree' = Node "top" []---- pathtree' :: [a] -> Tree a--- pathtree' []     = Node undefined []--- pathtree' (x:xs) = Node x [pathtree' xs]---- fromPaths' :: (Ord a) => [[a]] -> Tree a--- fromPaths' = foldl' mergeTrees' emptyTree' . map pathtree'----- converttree :: [AccountName] -> Tree' AccountName -> [Tree AccountName]--- converttree parents (T m) = [Node (accountNameFromComponents $ parents ++ [a]) (converttree (parents++[a]) b) | (a,b) <- M.toList m]---- accountNameTreeFrom4 :: [AccountName] -> Tree AccountName--- accountNameTreeFrom4 accts = Node "top" (converttree [] $ fromPaths $ map accountNameComponents accts)--converttree :: Tree' AccountName -> [Tree AccountName]-converttree (T m) = [Node a (converttree b) | (a,b) <- M.toList m]--expandTreeNames :: Tree AccountName -> Tree AccountName-expandTreeNames (Node x ts) = Node x (map (treemap (\n -> accountNameFromComponents [x,n]) . expandTreeNames) ts)--accountNameTreeFrom4 :: [AccountName] -> Tree AccountName-accountNameTreeFrom4 = Node "top" . map expandTreeNames . converttree . fromPaths . map accountNameComponents----- | 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---   letters in length.  If this results in a string that is still too---   long, the front will be elided -- not the end.  For example:------     Expenses:Cash           ; OK, not too long---     Ex:Wednesday:Cash       ; "Expenses" was abbreviated to fit---     Ex:We:Afternoon:Cash    ; "Expenses" and "Wednesday" abbreviated---     ; Expenses:Wednesday:Afternoon:Lunch:Snack:Candy:Chocolate:Cash---     ..:Af:Lu:Sn:Ca:Ch:Cash  ; Abbreviated and elided!--- @-elideAccountName :: Int -> AccountName -> AccountName-elideAccountName width s = -    elideLeft width $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s-      where-        elideparts :: Int -> [String] -> [String] -> [String]-        elideparts width done ss-          | length (accountNameFromComponents $ done++ss) <= width = done++ss-          | length ss > 1 = elideparts width (done++[take 2 $ head ss]) (tail ss)-          | otherwise = done++ss--clipAccountName :: Int -> AccountName -> AccountName-clipAccountName n = accountNameFromComponents . take n . accountNameComponents-
− Ledger/Amount.hs
@@ -1,340 +0,0 @@-{-# LANGUAGE StandaloneDeriving #-}-{-|-An 'Amount' is some quantity of money, shares, or anything else.--A simple amount is a 'Commodity', quantity pair:--@-  $1 -  £-50-  EUR 3.44 -  GOOG 500-  1.5h-  90 apples-  0 -@--An amount may also have a per-unit price, or conversion rate, in terms-of some other commodity. If present, this is displayed after \@:--@-  EUR 3 \@ $1.35-@--A 'MixedAmount' is zero or more simple amounts.  Mixed amounts are-usually normalised so that there is no more than one amount in each-commodity, and no zero amounts (or, there is just a single zero amount-and no others.):--@-  $50 + EUR 3-  16h + $13.55 + AAPL 500 + 6 oranges-  0-@--We can do limited arithmetic with simple or mixed amounts: either-price-preserving arithmetic with similarly-priced amounts, or-price-discarding arithmetic which ignores and discards prices.---}--module Ledger.Amount-where-import Ledger.Utils-import Ledger.Types-import Ledger.Commodity---instance Show Amount where show = showAmount-instance Show MixedAmount where show = showMixedAmount-deriving instance Show HistoricalPrice--instance Num Amount where-    abs (Amount c q p) = Amount c (abs q) p-    signum (Amount c q p) = Amount c (signum q) p-    fromInteger i = Amount (comm "") (fromInteger i) Nothing-    (+) = amountop (+)-    (-) = amountop (-)-    (*) = amountop (*)--instance Ord Amount where-    compare (Amount ac aq ap) (Amount bc bq bp) = compare (ac,aq,ap) (bc,bq,bp)--instance Num MixedAmount where-    fromInteger i = Mixed [Amount (comm "") (fromInteger i) Nothing]-    negate (Mixed as) = Mixed $ map negateAmountPreservingPrice as-    (+) (Mixed as) (Mixed bs) = normaliseMixedAmount $ 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"--instance Ord MixedAmount where-    compare (Mixed as) (Mixed bs) = compare as bs--negateAmountPreservingPrice a = (-a){price=price a}---- | Apply a binary arithmetic operator to two amounts, converting to the--- second one's commodity (and display precision), discarding any price--- information. (Using the second commodity is best since sum and other--- folds start with a no-commodity amount.)-amountop :: (Double -> Double -> Double) -> Amount -> Amount -> Amount-amountop op a@(Amount _ _ _) (Amount bc bq _) = -    Amount bc (quantity (convertAmountTo bc a) `op` bq) Nothing---- | Convert an amount to the specified commodity using the appropriate--- exchange rate (which is currently always 1).-convertAmountTo :: Commodity -> Amount -> Amount-convertAmountTo c2 (Amount c1 q _) = Amount c2 (q * conversionRate c1 c2) Nothing---- | Convert mixed amount to the specified commodity-convertMixedAmountTo :: Commodity -> MixedAmount -> Amount-convertMixedAmountTo c2 (Mixed ams) = Amount c2 total Nothing-    where-    total = sum . map (quantity . convertAmountTo c2) $ ams---- | Convert an amount to the commodity of its saved price, if any.-costOfAmount :: Amount -> Amount-costOfAmount a@(Amount _ _ Nothing) = a-costOfAmount (Amount _ q (Just price))-    | isZeroMixedAmount price = nullamt-    | otherwise = Amount pc (pq*q) Nothing-    where (Amount pc pq _) = head $ amounts price---- | Get the string representation of an amount, based on its commodity's--- display settings.-showAmount :: Amount -> String-showAmount (Amount (Commodity {symbol="AUTO"}) _ _) = "" -- can appear in an error message-showAmount a@(Amount (Commodity {symbol=sym,side=side,spaced=spaced}) _ pri) =-    case side of-      L -> printf "%s%s%s%s" sym space quantity price-      R -> printf "%s%s%s%s" quantity space sym price-    where -      space = if spaced then " " else ""-      quantity = showAmount' a-      price = case pri of (Just pamt) -> " @ " ++ showMixedAmount pamt-                          Nothing -> ""---- XXX refactor--- | Get the unambiguous string representation of an amount, for debugging.-showAmountDebug :: Amount -> String-showAmountDebug (Amount c q pri) = printf "Amount {commodity = %s, quantity = %s, price = %s}"-                                   (show c) (show q) (maybe "" showMixedAmountDebug pri)---- | Get the string representation of an amount, without any \@ price.-showAmountWithoutPrice :: Amount -> String-showAmountWithoutPrice a = showAmount a{price=Nothing}---- | Get the string representation (of the number part of) of an amount-showAmount' :: Amount -> String-showAmount' (Amount (Commodity {comma=comma,precision=p}) q _) = quantity-  where-    quantity = commad $ printf ("%."++show p++"f") q-    commad = if comma then punctuatethousands else id---- | Add thousands-separating commas to a decimal number string-punctuatethousands :: String -> String-punctuatethousands s =-    sign ++ addcommas int ++ frac-    where -      (sign,num) = break isDigit s-      (int,frac) = break (=='.') num-      addcommas = reverse . concat . intersperse "," . triples . reverse-      triples [] = []-      triples l  = take 3 l : triples (drop 3 l)---- | Does this amount appear to be zero when displayed with its given precision ?-isZeroAmount :: Amount -> Bool-isZeroAmount = null . filter (`elem` "123456789") . showAmountWithoutPrice---- | 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 = null . filter (`elem` "123456789") . printf "%.10f" . quantity---- | Access a mixed amount's components.-amounts :: MixedAmount -> [Amount]-amounts (Mixed as) = as---- | Does this mixed amount appear to be zero - empty, or--- containing only simple amounts which appear to be zero ?-isZeroMixedAmount :: MixedAmount -> Bool-isZeroMixedAmount = all isZeroAmount . amounts . normaliseMixedAmount---- | Is this mixed amount "really" zero ? See isReallyZeroAmount.-isReallyZeroMixedAmount :: MixedAmount -> Bool-isReallyZeroMixedAmount = all isReallyZeroAmount . amounts . normaliseMixedAmount---- | Is this mixed amount "really" zero, after converting to cost--- commodities where possible ?-isReallyZeroMixedAmountCost :: MixedAmount -> Bool-isReallyZeroMixedAmountCost = isReallyZeroMixedAmount . costOfMixedAmount---- | MixedAmount derives Eq in Types.hs, but that doesn't know that we--- want $0 = EUR0 = 0. Yet we don't want to drag all this code in there.--- When zero equality is important, use this, for now; should be used--- everywhere.-mixedAmountEquals :: MixedAmount -> MixedAmount -> Bool-mixedAmountEquals a b = amounts a' == amounts b' || (isZeroMixedAmount a' && isZeroMixedAmount b')-    where a' = normaliseMixedAmount a-          b' = normaliseMixedAmount 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 \"\".-showMixedAmount :: MixedAmount -> String-showMixedAmount m = vConcatRightAligned $ map show $ amounts $ normaliseMixedAmount m---- | Get an unambiguous string representation of a mixed amount for debugging.-showMixedAmountDebug :: MixedAmount -> String-showMixedAmountDebug m = printf "Mixed [%s]" as-    where as = intercalate "\n       " $ map showAmountDebug $ amounts $ normaliseMixedAmount 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) = normaliseMixedAmountIgnoringPrice m-      width = maximum $ map (length . show) as-      showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice---- | Get the string representation of a mixed amount, and if it--- appears to be all zero just show a bare 0, ledger-style.-showMixedAmountOrZero :: MixedAmount -> String-showMixedAmountOrZero a | a == missingamt = ""-                        | isZeroMixedAmount a = "0"-                        | otherwise = showMixedAmount a---- | Get the string representation of a mixed amount, or a bare 0,--- without any \@ prices.-showMixedAmountOrZeroWithoutPrice :: MixedAmount -> String-showMixedAmountOrZeroWithoutPrice a-    | isZeroMixedAmount a = "0"-    | otherwise = showMixedAmountWithoutPrice a---- | Simplify a mixed amount by combining any component amounts which have--- the same commodity and the same price. Also removes zero amounts,--- or adds a single zero amount if there are no amounts at all.-normaliseMixedAmount :: MixedAmount -> MixedAmount-normaliseMixedAmount (Mixed as) = Mixed as''-    where -      as'' = map sumSamePricedAmountsPreservingPrice $ group $ sort as'-      sort = sortBy cmpsymbolandprice-      cmpsymbolandprice a1 a2 = compare (sym a1,price a1) (sym a2,price a2)-      group = groupBy samesymbolandprice -      samesymbolandprice a1 a2 = (sym a1 == sym a2) && (price a1 == price a2)-      sym = symbol . commodity-      as' | null nonzeros = [head $ zeros ++ [nullamt]]-          | otherwise = nonzeros-      (zeros,nonzeros) = partition isReallyZeroAmount as---- various sum variants..--sumAmountsDiscardingPrice [] = nullamt-sumAmountsDiscardingPrice as = (sum as){price=Nothing}--sumSamePricedAmountsPreservingPrice [] = nullamt-sumSamePricedAmountsPreservingPrice as = (sum as){price=price $ head as}---- | Simplify a mixed amount by combining any component amounts which have--- the same commodity, ignoring and discarding their unit prices if any.--- Also removes zero amounts, or adds a single zero amount if there are no--- amounts at all.-normaliseMixedAmountIgnoringPrice :: MixedAmount -> MixedAmount-normaliseMixedAmountIgnoringPrice (Mixed as) = Mixed as''-    where-      as'' = map sumAmountsDiscardingPrice $ group $ sort as'-      group = groupBy samesymbol where samesymbol a1 a2 = sym a1 == sym a2-      sort = sortBy (comparing sym)-      sym = symbol . commodity-      as' | null nonzeros = [head $ zeros ++ [nullamt]]-          | otherwise = nonzeros-          where (zeros,nonzeros) = partition isZeroAmount as--sumMixedAmountsPreservingHighestPrecision :: [MixedAmount] -> MixedAmount-sumMixedAmountsPreservingHighestPrecision ms = foldl' (+~) 0 ms-    where (+~) (Mixed as) (Mixed bs) = normaliseMixedAmountPreservingHighestPrecision $ Mixed $ as ++ bs--normaliseMixedAmountPreservingHighestPrecision :: MixedAmount -> MixedAmount-normaliseMixedAmountPreservingHighestPrecision (Mixed as) = Mixed as''-    where-      as'' = map sumSamePricedAmountsPreservingPriceAndHighestPrecision $ group $ sort as'-      sort = sortBy cmpsymbolandprice-      cmpsymbolandprice a1 a2 = compare (sym a1,price a1) (sym a2,price a2)-      group = groupBy samesymbolandprice-      samesymbolandprice a1 a2 = (sym a1 == sym a2) && (price a1 == price a2)-      sym = symbol . commodity-      as' | null nonzeros = [head $ zeros ++ [nullamt]]-          | otherwise = nonzeros-      (zeros,nonzeros) = partition isReallyZeroAmount as--sumSamePricedAmountsPreservingPriceAndHighestPrecision [] = nullamt-sumSamePricedAmountsPreservingPriceAndHighestPrecision as = (sumAmountsPreservingHighestPrecision as){price=price $ head as}--sumAmountsPreservingHighestPrecision :: [Amount] -> Amount-sumAmountsPreservingHighestPrecision as = foldl' (+~) 0 as-    where (+~) = amountopPreservingHighestPrecision (+)--amountopPreservingHighestPrecision :: (Double -> Double -> Double) -> Amount -> Amount -> Amount-amountopPreservingHighestPrecision op a@(Amount ac@Commodity{precision=ap} _ _) (Amount bc@Commodity{precision=bp} bq _) = -    Amount c q Nothing-    where-      q = quantity (convertAmountTo bc a) `op` bq-      c = if ap > bp then ac else bc------- | Convert a mixed amount's component amounts to the commodity of their--- saved price, if any.-costOfMixedAmount :: MixedAmount -> MixedAmount-costOfMixedAmount (Mixed as) = Mixed $ map costOfAmount as---- | The empty simple amount.-nullamt :: Amount-nullamt = Amount unknown 0 Nothing---- | The empty mixed amount.-nullmixedamt :: MixedAmount-nullmixedamt = Mixed []---- | A temporary value for parsed transactions which had no amount specified.-missingamt :: MixedAmount-missingamt = Mixed [Amount Commodity {symbol="AUTO",side=L,spaced=False,comma=False,precision=0} 0 Nothing]---tests_Amount = TestList [--   "showMixedAmount" ~: do-     showMixedAmount (Mixed [Amount dollar 0 Nothing]) `is` "$0.00"-     showMixedAmount (Mixed []) `is` "0"-     showMixedAmount missingamt `is` ""--  ,"showMixedAmountOrZero" ~: do-     showMixedAmountOrZero (Mixed [Amount dollar 0 Nothing]) `is` "0"-     showMixedAmountOrZero (Mixed []) `is` "0"-     showMixedAmountOrZero missingamt `is` ""--  ,"amount arithmetic" ~: do-    let a1 = dollars 1.23-    let a2 = Amount (comm "$") (-1.23) Nothing-    let a3 = Amount (comm "$") (-1.23) Nothing-    (a1 + a2) `is` Amount (comm "$") 0 Nothing-    (a1 + a3) `is` Amount (comm "$") 0 Nothing-    (a2 + a3) `is` Amount (comm "$") (-2.46) Nothing-    (a3 + a3) `is` Amount (comm "$") (-2.46) Nothing-    sum [a2,a3] `is` Amount (comm "$") (-2.46) Nothing-    sum [a3,a3] `is` Amount (comm "$") (-2.46) Nothing-    sum [a1,a2,a3,-a3] `is` Amount (comm "$") 0 Nothing-    let dollar0 = dollar{precision=0}-    (sum [Amount dollar 1.25 Nothing, Amount dollar0 (-1) Nothing, Amount dollar (-0.25) Nothing])-      `is` (Amount dollar 0 Nothing)--  ,"mixed amount arithmetic" ~: do-    let dollar0 = dollar{precision=0}-    (sum $ map (Mixed . (\a -> [a]))-             [Amount dollar 1.25 Nothing,-              Amount dollar0 (-1) Nothing,-              Amount dollar (-0.25) Nothing])-      `is` Mixed [Amount dollar 0 Nothing]---  ]
− Ledger/Commodity.hs
@@ -1,39 +0,0 @@-{-|--A 'Commodity' is a symbol representing a currency or some other kind of-thing we are tracking, and some display preferences that tell how to-display 'Amount's of the commodity - is the symbol on the left or right,-are thousands separated by comma, significant decimal places and so on.---}-module Ledger.Commodity-where-import Ledger.Utils-import Ledger.Types----- convenient amount and commodity constructors, for tests etc.--unknown = Commodity {symbol="",   side=L,spaced=False,comma=False,precision=0}-dollar  = Commodity {symbol="$",  side=L,spaced=False,comma=False,precision=2}-euro    = Commodity {symbol="EUR",side=L,spaced=False,comma=False,precision=2}-pound   = Commodity {symbol="£",  side=L,spaced=False,comma=False,precision=2}-hour    = Commodity {symbol="h",  side=R,spaced=False,comma=False,precision=1}--dollars n = Amount dollar n Nothing-euros n   = Amount euro n Nothing-pounds n  = Amount pound n Nothing-hours n   = Amount hour n Nothing--defaultcommodities = [dollar,  euro,  pound, hour, unknown]---- | Look up one of the hard-coded default commodities. For use in tests.-comm :: String -> Commodity-comm sym = fromMaybe -              (error "commodity lookup failed") -              $ find (\(Commodity{symbol=s}) -> s==sym) defaultcommodities---- | Find the conversion rate between two commodities. Currently returns 1.-conversionRate :: Commodity -> Commodity -> Double-conversionRate _ _ = 1-
− Ledger/Dates.hs
@@ -1,464 +0,0 @@-{-|--Date parsing and utilities for hledger.--For date and time values, we use the standard Day and UTCTime types.--A 'SmartDate' is a date which may be partially-specified or relative.-Eg 2008\/12\/31, but also 2008\/12, 12\/31, tomorrow, last week, next year.-We represent these as a triple of strings like (\"2008\",\"12\",\"\"),-(\"\",\"\",\"tomorrow\"), (\"\",\"last\",\"week\").--A 'DateSpan' is the span of time between two specific calendar dates, or-an open-ended span where one or both dates are unspecified. (A date span-with both ends unspecified matches all dates.)--An 'Interval' is ledger's "reporting interval" - weekly, monthly,-quarterly, etc.---}--module Ledger.Dates-where--import Data.Time.Format-import Data.Time.Calendar.OrdinalDate-import System.Locale (defaultTimeLocale)-import Text.ParserCombinators.Parsec-import Text.ParserCombinators.Parsec.Char-import Text.ParserCombinators.Parsec.Combinator-import Ledger.Types-import Ledger.Utils---showDate :: Day -> String-showDate = formatTime defaultTimeLocale "%C%y/%m/%d"--getCurrentDay :: IO Day-getCurrentDay = do-    t <- getZonedTime-    return $ localDay (zonedTimeToLocalTime t)--elapsedSeconds :: Fractional a => UTCTime -> UTCTime -> a-elapsedSeconds t1 = realToFrac . diffUTCTime t1---- | Split a DateSpan into one or more consecutive spans at the specified interval.-splitSpan :: Interval -> DateSpan -> [DateSpan]-splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]-splitSpan NoInterval s = [s]-splitSpan Daily s      = splitspan startofday     nextday     s-splitSpan Weekly s     = splitspan startofweek    nextweek    s-splitSpan Monthly s    = splitspan startofmonth   nextmonth   s-splitSpan Quarterly s  = splitspan startofquarter nextquarter s-splitSpan Yearly s     = splitspan startofyear    nextyear    s--splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan]-splitspan _ _ (DateSpan Nothing Nothing) = []-splitspan start next (DateSpan Nothing (Just e)) = [DateSpan (Just $ start e) (Just $ next $ start e)]-splitspan start next (DateSpan (Just b) Nothing) = [DateSpan (Just $ start b) (Just $ next $ start b)]-splitspan start next span@(DateSpan (Just b) (Just e))-    | b == e = [span]-    | otherwise = splitspan' start next span-    where-      splitspan' start next (DateSpan (Just b) (Just e))-          | b >= e = []-          | otherwise = DateSpan (Just s) (Just n)-                        : splitspan' start next (DateSpan (Just n) (Just e))-          where s = start b-                n = next s-      splitspan' _ _ _ = error "won't happen, avoids warnings"---- | Count the days in a DateSpan, or if it is open-ended return Nothing.-daysInSpan :: DateSpan -> Maybe Integer-daysInSpan (DateSpan (Just d1) (Just d2)) = Just $ diffDays d2 d1-daysInSpan _ = Nothing-    --- | Parse a period expression to an Interval and overall DateSpan using--- the provided reference date, or raise an error.-parsePeriodExpr :: Day -> String -> (Interval, DateSpan)-parsePeriodExpr refdate expr = (interval,span)-    where (interval,span) = fromparse $ parsewith (periodexpr refdate) expr-    --- | Convert a single smart date string to a date span using the provided--- reference date, or raise an error.-spanFromSmartDateString :: Day -> String -> DateSpan-spanFromSmartDateString refdate s = spanFromSmartDate refdate sdate-    where-      sdate = fromparse $ parsewith smartdateonly s--spanFromSmartDate :: Day -> SmartDate -> DateSpan-spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e)-    where-      (ry,rm,_) = toGregorian refdate-      (b,e) = span sdate-      span :: SmartDate -> (Day,Day)-      span ("","","today")       = (refdate, nextday refdate)-      span ("","this","day")     = (refdate, nextday refdate)-      span ("","","yesterday")   = (prevday refdate, refdate)-      span ("","last","day")     = (prevday refdate, refdate)-      span ("","","tomorrow")    = (nextday refdate, addDays 2 refdate)-      span ("","next","day")     = (nextday refdate, addDays 2 refdate)-      span ("","last","week")    = (prevweek refdate, thisweek refdate)-      span ("","this","week")    = (thisweek refdate, nextweek refdate)-      span ("","next","week")    = (nextweek refdate, startofweek $ addDays 14 refdate)-      span ("","last","month")   = (prevmonth refdate, thismonth refdate)-      span ("","this","month")   = (thismonth refdate, nextmonth refdate)-      span ("","next","month")   = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate)-      span ("","last","quarter") = (prevquarter refdate, thisquarter refdate)-      span ("","this","quarter") = (thisquarter refdate, nextquarter refdate)-      span ("","next","quarter") = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate)-      span ("","last","year")    = (prevyear refdate, thisyear refdate)-      span ("","this","year")    = (thisyear refdate, nextyear refdate)-      span ("","next","year")    = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate)-      span ("","",d)             = (day, nextday day) where day = fromGregorian ry rm (read d)-      span ("",m,"")             = (startofmonth day, nextmonth day) where day = fromGregorian ry (read m) 1-      span ("",m,d)              = (day, nextday day) where day = fromGregorian ry (read m) (read d)-      span (y,"","")             = (startofyear day, nextyear day) where day = fromGregorian (read y) 1 1-      span (y,m,"")              = (startofmonth day, nextmonth day) where day = fromGregorian (read y) (read m) 1-      span (y,m,d)               = (day, nextday day) where day = fromGregorian (read y) (read m) (read d)--showDay :: Day -> String-showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day---- | Convert a smart date string to an explicit yyyy\/mm\/dd string using--- the provided reference date, or raise an error.-fixSmartDateStr :: Day -> String -> String-fixSmartDateStr t s = either parseerror id $ fixSmartDateStrEither t s---- | A safe version of fixSmartDateStr.-fixSmartDateStrEither :: Day -> String -> Either ParseError String-fixSmartDateStrEither t s = case parsewith smartdateonly (lowercase s) of-                              Right sd -> Right $ showDay $ fixSmartDate t sd-                              Left e -> Left e---- | Convert a SmartDate to an absolute date using the provided reference date.-fixSmartDate :: Day -> SmartDate -> Day-fixSmartDate refdate sdate = fix sdate-    where-      fix :: SmartDate -> Day-      fix ("","","today")       = fromGregorian ry rm rd-      fix ("","this","day")     = fromGregorian ry rm rd-      fix ("","","yesterday")   = prevday refdate-      fix ("","last","day")     = prevday refdate-      fix ("","","tomorrow")    = nextday refdate-      fix ("","next","day")     = nextday refdate-      fix ("","last","week")    = prevweek refdate-      fix ("","this","week")    = thisweek refdate-      fix ("","next","week")    = nextweek refdate-      fix ("","last","month")   = prevmonth refdate-      fix ("","this","month")   = thismonth refdate-      fix ("","next","month")   = nextmonth refdate-      fix ("","last","quarter") = prevquarter refdate-      fix ("","this","quarter") = thisquarter refdate-      fix ("","next","quarter") = nextquarter refdate-      fix ("","last","year")    = prevyear refdate-      fix ("","this","year")    = thisyear refdate-      fix ("","next","year")    = nextyear refdate-      fix ("","",d)             = fromGregorian ry rm (read d)-      fix ("",m,"")             = fromGregorian ry (read m) 1-      fix ("",m,d)              = fromGregorian ry (read m) (read d)-      fix (y,"","")             = fromGregorian (read y) 1 1-      fix (y,m,"")              = fromGregorian (read y) (read m) 1-      fix (y,m,d)               = fromGregorian (read y) (read m) (read d)-      (ry,rm,rd) = toGregorian refdate--prevday :: Day -> Day-prevday = addDays (-1)-nextday = addDays 1-startofday = id--thisweek = startofweek-prevweek = startofweek . addDays (-7)-nextweek = startofweek . addDays 7-startofweek day = fromMondayStartWeek y w 1-    where-      (y,_,_) = toGregorian day-      (w,_) = mondayStartWeek day--thismonth = startofmonth-prevmonth = startofmonth . addGregorianMonthsClip (-1)-nextmonth = startofmonth . addGregorianMonthsClip 1-startofmonth day = fromGregorian y m 1 where (y,m,_) = toGregorian day--thisquarter = startofquarter-prevquarter = startofquarter . addGregorianMonthsClip (-3)-nextquarter = startofquarter . addGregorianMonthsClip 3-startofquarter day = fromGregorian y (firstmonthofquarter m) 1-    where-      (y,m,_) = toGregorian day-      firstmonthofquarter m = ((m-1) `div` 3) * 3 + 1--thisyear = startofyear-prevyear = startofyear . addGregorianYearsClip (-1)-nextyear = startofyear . addGregorianYearsClip 1-startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day--------------------------------------------------------------------------- parsing--firstJust ms = case dropWhile (==Nothing) ms of-    [] -> Nothing-    (md:_) -> md---- | Parse a couple of date-time string formats to a time type.-parsedatetimeM :: String -> Maybe LocalTime-parsedatetimeM s = firstJust [-    parseTime defaultTimeLocale "%Y/%m/%d %H:%M:%S" s,-    parseTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" s-    ]---- | Parse a couple of date string formats to a time type.-parsedateM :: String -> Maybe Day-parsedateM s = firstJust [ -     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.-parsedatetime :: String -> LocalTime-parsedatetime s = fromMaybe (error $ "could not parse timestamp \"" ++ s ++ "\"")-                            (parsedatetimeM s)---- | Parse a date string to a time type, or raise an error.-parsedate :: String -> Day-parsedate s =  fromMaybe (error $ "could not parse date \"" ++ s ++ "\"")-                         (parsedateM s)---- | Parse a time string to a time type using the provided pattern, or--- return the default.-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:--> 2004-> 2004/10-> 2004/10/1-> 10/1-> 21-> october, oct-> yesterday, today, tomorrow-> (not yet) this/next/last week/day/month/quarter/year--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 = do-  let dateparsers = [yyyymmdd, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow,-                     lastthisnextthing-                    ]-  (y,m,d) <- choice $ map try dateparsers-  return (y,m,d)---- | Like smartdate, but there must be nothing other than whitespace after the date.-smartdateonly :: GenParser Char st SmartDate-smartdateonly = do-  d <- smartdate-  many spacenonewline-  eof-  return d--datesepchar = oneOf "/-."--yyyymmdd :: GenParser Char st SmartDate-yyyymmdd = do-  y <- count 4 digit-  m <- count 2 digit-  guard (read m <= 12)-  d <- count 2 digit-  guard (read d <= 31)-  return (y,m,d)--ymd :: GenParser Char st SmartDate-ymd = do-  y <- many1 digit-  datesepchar-  m <- try (count 2 digit) <|> count 1 digit-  when (read m < 1 || (read m > 12)) $ error $ "bad month number: " ++ m-  datesepchar-  d <- try (count 2 digit) <|> count 1 digit-  when (read d < 1 || (read d > 31)) $ error $ "bad day number: " ++ d-  return $ (y,m,d)--ym :: GenParser Char st SmartDate-ym = do-  y <- many1 digit-  guard (read y > 12)-  datesepchar-  m <- try (count 2 digit) <|> count 1 digit-  guard (read m >= 1 && (read m <= 12))-  return (y,m,"")--y :: GenParser Char st SmartDate-y = do-  y <- many1 digit-  guard (read y >= 1000)-  return (y,"","")--d :: GenParser Char st SmartDate-d = do-  d <- many1 digit-  guard (read d <= 31)-  return ("","",d)--md :: GenParser Char st SmartDate-md = do-  m <- try (count 2 digit) <|> count 1 digit-  guard (read m >= 1 && (read m <= 12))-  datesepchar-  d <- try (count 2 digit) <|> count 1 digit-  when (read d < 1 || (read d > 31)) $ fail "bad day number specified"-  return ("",m,d)--months         = ["january","february","march","april","may","june",-                  "july","august","september","october","november","december"]-monthabbrevs   = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]-weekdays       = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]-weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]--monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months-monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs--month :: GenParser Char st SmartDate-month = do-  m <- choice $ map (try . string) months-  let i = monthIndex m-  return ("",show i,"")--mon :: GenParser Char st SmartDate-mon = do-  m <- choice $ map (try . string) monthabbrevs-  let i = monIndex m-  return ("",show i,"")--today,yesterday,tomorrow :: GenParser Char st SmartDate-today     = string "today"     >> return ("","","today")-yesterday = string "yesterday" >> return ("","","yesterday")-tomorrow  = string "tomorrow"  >> return ("","","tomorrow")--lastthisnextthing :: GenParser Char st SmartDate-lastthisnextthing = do-  r <- choice [-        string "last"-       ,string "this"-       ,string "next"-      ]-  many spacenonewline  -- make the space optional for easier scripting-  p <- choice [-        string "day"-       ,string "week"-       ,string "month"-       ,string "quarter"-       ,string "year"-      ]--- XXX support these in fixSmartDate---       ++ (map string $ months ++ monthabbrevs ++ weekdays ++ weekdayabbrevs)-            -  return ("",r,p)--periodexpr :: Day -> GenParser Char st (Interval, DateSpan)-periodexpr rdate = choice $ map try [-                    intervalanddateperiodexpr rdate,-                    intervalperiodexpr,-                    dateperiodexpr rdate,-                    (return (NoInterval,DateSpan Nothing Nothing))-                   ]--intervalanddateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)-intervalanddateperiodexpr rdate = do-  many spacenonewline-  i <- periodexprinterval-  many spacenonewline-  s <- periodexprdatespan rdate-  return (i,s)--intervalperiodexpr :: GenParser Char st (Interval, DateSpan)-intervalperiodexpr = do-  many spacenonewline-  i <- periodexprinterval-  return (i, DateSpan Nothing Nothing)--dateperiodexpr :: Day -> GenParser Char st (Interval, DateSpan)-dateperiodexpr rdate = do-  many spacenonewline-  s <- periodexprdatespan rdate-  return (NoInterval, s)--periodexprinterval :: GenParser Char st Interval-periodexprinterval = -    choice $ map try [-                tryinterval "day" "daily" Daily,-                tryinterval "week" "weekly" Weekly,-                tryinterval "month" "monthly" Monthly,-                tryinterval "quarter" "quarterly" Quarterly,-                tryinterval "year" "yearly" Yearly-               ]-    where-      tryinterval s1 s2 v = -          choice [try (string $ "every "++s1), try (string s2)] >> return v--periodexprdatespan :: Day -> GenParser Char st DateSpan-periodexprdatespan rdate = choice $ map try [-                            doubledatespan rdate,-                            fromdatespan rdate,-                            todatespan rdate,-                            justdatespan rdate-                           ]--doubledatespan :: Day -> GenParser Char st DateSpan-doubledatespan rdate = do-  optional (string "from" >> many spacenonewline)-  b <- smartdate-  many spacenonewline-  optional (string "to" >> many spacenonewline)-  e <- smartdate-  return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e)--fromdatespan :: Day -> GenParser Char st DateSpan-fromdatespan rdate = do-  string "from" >> many spacenonewline-  b <- smartdate-  return $ DateSpan (Just $ fixSmartDate rdate b) Nothing--todatespan :: Day -> GenParser Char st DateSpan-todatespan rdate = do-  string "to" >> many spacenonewline-  e <- smartdate-  return $ DateSpan Nothing (Just $ fixSmartDate rdate e)--justdatespan :: Day -> GenParser Char st DateSpan-justdatespan rdate = do-  optional (string "in" >> many spacenonewline)-  d <- smartdate-  return $ spanFromSmartDate rdate d--mkdatespan :: String -> String -> DateSpan-mkdatespan b = DateSpan (Just $ parsedate b) . Just . parsedate--nulldatespan = DateSpan Nothing Nothing--nulldate = parsedate "1900/01/01"--tests_Dates = TestList [--   "splitSpan" ~: do-    let gives (interval, span) = (splitSpan interval span `is`)-    (NoInterval,mkdatespan "2008/01/01" "2009/01/01") `gives`-     [mkdatespan "2008/01/01" "2009/01/01"]-    (Quarterly,mkdatespan "2008/01/01" "2009/01/01") `gives`-     [mkdatespan "2008/01/01" "2008/04/01"-     ,mkdatespan "2008/04/01" "2008/07/01"-     ,mkdatespan "2008/07/01" "2008/10/01"-     ,mkdatespan "2008/10/01" "2009/01/01"-     ]-    (Quarterly,nulldatespan) `gives`-     [nulldatespan]-    (Daily,mkdatespan "2008/01/01" "2008/01/01") `gives`-     [mkdatespan "2008/01/01" "2008/01/01"]-    (Quarterly,mkdatespan "2008/01/01" "2008/01/01") `gives`-     [mkdatespan "2008/01/01" "2008/01/01"]--    ]
− Ledger/IO.hs
@@ -1,99 +0,0 @@-{-# LANGUAGE CPP #-}-{-|-Utilities for doing I/O with ledger files.--}--module Ledger.IO-where-import Control.Monad.Error-import Ledger.Ledger (cacheLedger', nullledger)-import Ledger.Parse (parseLedger)-import Ledger.Types (FilterSpec(..),WhichDate(..),Journal(..),Ledger(..))-import Ledger.Utils (getCurrentLocalTime)-import Ledger.Dates (nulldatespan)-import System.Directory (getHomeDirectory)-import System.Environment (getEnv)-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding (readFile)-import System.IO.UTF8-#endif-import System.FilePath ((</>))-import System.Time (getClockTime)---ledgerenvvar           = "LEDGER"-timelogenvvar          = "TIMELOG"-ledgerdefaultfilename  = ".ledger"-timelogdefaultfilename = ".timelog"--nullfilterspec = FilterSpec {-     datespan=nulldatespan-    ,cleared=Nothing-    ,real=False-    ,empty=False-    ,costbasis=False-    ,acctpats=[]-    ,descpats=[]-    ,whichdate=ActualDate-    ,depth=Nothing-    }---- | Get the user's default ledger file path.-myLedgerPath :: IO String-myLedgerPath = -    getEnv ledgerenvvar `catch` -               (\_ -> do-                  home <- getHomeDirectory `catch` (\_ -> return "")-                  return $ home </> ledgerdefaultfilename)-  --- | Get the user's default timelog file path.-myTimelogPath :: IO String-myTimelogPath =-    getEnv timelogenvvar `catch`-               (\_ -> do-                  home <- getHomeDirectory-                  return $ home </> timelogdefaultfilename)---- | Read the user's default ledger file, or give an error.-myLedger :: IO Ledger-myLedger = myLedgerPath >>= readLedger---- | Read the user's default timelog file, or give an error.-myTimelog :: IO Ledger-myTimelog = myTimelogPath >>= readLedger---- | Read a ledger from this file, with no filtering, or give an error.-readLedger :: FilePath -> IO Ledger-readLedger f = do-  t <- getClockTime-  s <- readFile f-  j <- journalFromString s-  return $ cacheLedger' $ nullledger{journal=j{filepath=f,filereadtime=t,jtext=s}}---- -- | Read a ledger from this file, filtering according to the filter spec.,--- -- | or give an error.--- readLedgerWithFilterSpec :: FilterSpec -> FilePath -> IO Ledger--- readLedgerWithFilterSpec fspec f = do---   s <- readFile f---   t <- getClockTime---   rl <- journalFromString s---   return $ filterAndCacheLedger fspec s rl{filepath=f, filereadtime=t}---- | Read a Journal from the given string, using the current time as--- reference time, or give a parse error.-journalFromString :: String -> IO Journal-journalFromString s = do-  t <- getCurrentLocalTime-  liftM (either error id) $ runErrorT $ parseLedger t "(string)" s---- -- | Expand ~ in a file path (does not handle ~name).--- tildeExpand :: FilePath -> IO FilePath--- tildeExpand ('~':[])     = getHomeDirectory--- tildeExpand ('~':'/':xs) = getHomeDirectory >>= return . (++ ('/':xs))--- --handle ~name, requires -fvia-C or ghc 6.8:--- --import System.Posix.User--- -- tildeExpand ('~':xs)     =  do let (user, path) = span (/= '/') xs--- --                                pw <- getUserEntryForName user--- --                                return (homeDirectory pw ++ path)--- tildeExpand xs           =  return xs-
− Ledger/Journal.hs
@@ -1,321 +0,0 @@-{-|--A 'Journal' is a parsed ledger file, containing 'Transaction's.-It can be filtered and massaged in various ways, then \"crunched\"-to form a 'Ledger'.---}--module Ledger.Journal-where-import qualified Data.Map as Map-import Data.Map (findWithDefault, (!))-import System.Time (ClockTime(TOD))-import Ledger.Utils-import Ledger.Types-import Ledger.AccountName-import Ledger.Amount-import Ledger.Transaction (ledgerTransactionWithDate)-import Ledger.Posting-import Ledger.TimeLog---instance Show Journal where-    show j = printf "Journal with %d transactions, %d accounts: %s"-             (length (jtxns j) +-              length (jmodifiertxns j) +-              length (jperiodictxns j))-             (length accounts)-             (show accounts)-             -- ++ (show $ journalTransactions l)-             where accounts = flatten $ journalAccountNameTree j--nulljournal :: Journal-nulljournal = Journal { jmodifiertxns = []-                      , jperiodictxns = []-                      , jtxns = []-                      , open_timelog_entries = []-                      , historical_prices = []-                      , final_comment_lines = []-                      , filepath = ""-                      , filereadtime = TOD 0 0-                      , jtext = ""-                      }--addTransaction :: Transaction -> Journal -> Journal-addTransaction t l0 = l0 { jtxns = t : jtxns l0 }--addModifierTransaction :: ModifierTransaction -> Journal -> Journal-addModifierTransaction mt l0 = l0 { jmodifiertxns = mt : jmodifiertxns l0 }--addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal-addPeriodicTransaction pt l0 = l0 { jperiodictxns = pt : jperiodictxns l0 }--addHistoricalPrice :: HistoricalPrice -> Journal -> Journal-addHistoricalPrice h l0 = l0 { historical_prices = h : historical_prices l0 }--addTimeLogEntry :: TimeLogEntry -> Journal -> Journal-addTimeLogEntry tle l0 = l0 { open_timelog_entries = tle : open_timelog_entries l0 }--journalPostings :: Journal -> [Posting]-journalPostings = concatMap tpostings . jtxns--journalAccountNamesUsed :: Journal -> [AccountName]-journalAccountNamesUsed = accountNamesFromPostings . journalPostings--journalAccountNames :: Journal -> [AccountName]-journalAccountNames = sort . expandAccountNames . journalAccountNamesUsed--journalAccountNameTree :: Journal -> Tree AccountName-journalAccountNameTree = accountNameTreeFrom . journalAccountNames---- Various kinds of filtering on journals. We do it differently depending--- on the command.---- | Keep only transactions we are interested in, as described by--- the filter specification. May also massage the data a little.-filterJournalTransactions :: FilterSpec -> Journal -> Journal-filterJournalTransactions FilterSpec{datespan=datespan-                                    ,cleared=cleared-                                    -- ,real=real-                                    -- ,empty=empty-                                    -- ,costbasis=_-                                    ,acctpats=apats-                                    ,descpats=dpats-                                    ,whichdate=whichdate-                                    ,depth=depth-                                    } =-    filterJournalTransactionsByClearedStatus cleared .-    filterJournalPostingsByDepth depth .-    filterJournalTransactionsByAccount apats .-    filterJournalTransactionsByDescription dpats .-    filterJournalTransactionsByDate datespan .-    journalSelectingDate whichdate---- | Keep only postings we are interested in, as described by--- the filter specification. May also massage the data a little.--- This can leave unbalanced transactions.-filterJournalPostings :: FilterSpec -> Journal -> Journal-filterJournalPostings FilterSpec{datespan=datespan-                                ,cleared=cleared-                                ,real=real-                                ,empty=empty---                                ,costbasis=costbasis-                                ,acctpats=apats-                                ,descpats=dpats-                                ,whichdate=whichdate-                                ,depth=depth-                                } =-    filterJournalPostingsByRealness real .-    filterJournalPostingsByClearedStatus cleared .-    filterJournalPostingsByEmpty empty .-    filterJournalPostingsByDepth depth .-    filterJournalPostingsByAccount apats .-    filterJournalTransactionsByDescription dpats .-    filterJournalTransactionsByDate datespan .-    journalSelectingDate whichdate---- | Keep only ledger transactions whose description matches the description patterns.-filterJournalTransactionsByDescription :: [String] -> Journal -> Journal-filterJournalTransactionsByDescription pats j@Journal{jtxns=ts} = j{jtxns=filter matchdesc ts}-    where matchdesc = matchpats pats . tdescription---- | Keep only ledger transactions which fall between begin and end dates.--- We include transactions on the begin date and exclude transactions on the end--- date, like ledger.  An empty date string means no restriction.-filterJournalTransactionsByDate :: DateSpan -> Journal -> Journal-filterJournalTransactionsByDate (DateSpan begin end) j@Journal{jtxns=ts} = j{jtxns=filter match ts}-    where match t = maybe True (tdate t>=) begin && maybe True (tdate t<) end---- | Keep only ledger transactions which have the requested--- cleared/uncleared status, if there is one.-filterJournalTransactionsByClearedStatus :: Maybe Bool -> Journal -> Journal-filterJournalTransactionsByClearedStatus Nothing j = j-filterJournalTransactionsByClearedStatus (Just val) j@Journal{jtxns=ts} = j{jtxns=filter match ts}-    where match = (==val).tstatus---- | Keep only postings which have the requested cleared/uncleared status,--- if there is one.-filterJournalPostingsByClearedStatus :: Maybe Bool -> Journal -> Journal-filterJournalPostingsByClearedStatus Nothing j = j-filterJournalPostingsByClearedStatus (Just c) j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}-    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter ((==c) . postingCleared) ps}---- | Strip out any virtual postings, if the flag is true, otherwise do--- no filtering.-filterJournalPostingsByRealness :: Bool -> Journal -> Journal-filterJournalPostingsByRealness False l = l-filterJournalPostingsByRealness True j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}-    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter isReal ps}---- | Strip out any postings with zero amount, unless the flag is true.-filterJournalPostingsByEmpty :: Bool -> Journal -> Journal-filterJournalPostingsByEmpty True l = l-filterJournalPostingsByEmpty False j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}-    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter (not . isEmptyPosting) ps}---- | Keep only transactions which affect accounts deeper than the specified depth.-filterJournalTransactionsByDepth :: Maybe Int -> Journal -> Journal-filterJournalTransactionsByDepth Nothing j = j-filterJournalTransactionsByDepth (Just d) j@Journal{jtxns=ts} =-    j{jtxns=(filter (any ((<= d+1) . accountNameLevel . paccount) . tpostings) ts)}---- | Strip out any postings to accounts deeper than the specified depth--- (and any ledger transactions which have no postings as a result).-filterJournalPostingsByDepth :: Maybe Int -> Journal -> Journal-filterJournalPostingsByDepth Nothing j = j-filterJournalPostingsByDepth (Just d) j@Journal{jtxns=ts} =-    j{jtxns=filter (not . null . tpostings) $ map filtertxns ts}-    where filtertxns t@Transaction{tpostings=ps} =-              t{tpostings=filter ((<= d) . accountNameLevel . paccount) ps}---- | Keep only transactions which affect accounts matched by the account patterns.-filterJournalTransactionsByAccount :: [String] -> Journal -> Journal-filterJournalTransactionsByAccount apats j@Journal{jtxns=ts} = j{jtxns=filter match ts}-    where match = any (matchpats apats . paccount) . tpostings---- | Keep only postings which affect accounts matched by the account patterns.--- This can leave transactions unbalanced.-filterJournalPostingsByAccount :: [String] -> Journal -> Journal-filterJournalPostingsByAccount apats j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}-    where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter (matchpats apats . paccount) ps}---- | Convert this journal's transactions' primary date to either the--- actual or effective date.-journalSelectingDate :: WhichDate -> Journal -> Journal-journalSelectingDate ActualDate j = j-journalSelectingDate EffectiveDate j =-    j{jtxns=map (ledgerTransactionWithDate EffectiveDate) $ jtxns j}---- | Convert all the journal's amounts to their canonical display settings.--- Ie, in each commodity, amounts will use the display settings of the first--- amount detected, and the greatest precision of the amounts detected.--- Also, missing unit prices are added if known from the price history.--- Also, amounts are converted to cost basis if that flag is active.--- XXX refactor-canonicaliseAmounts :: Bool -> Journal -> Journal-canonicaliseAmounts costbasis j@Journal{jtxns=ts} = j{jtxns=map fixledgertransaction ts}-    where-      fixledgertransaction (Transaction d ed s c de co ts pr) = Transaction d ed s c de co (map fixrawposting ts) pr-          where-            fixrawposting (Posting s ac a c t txn) = Posting s ac (fixmixedamount a) c t txn-            fixmixedamount (Mixed as) = Mixed $ map fixamount as-            fixamount = (if costbasis then costOfAmount else id) . fixprice . fixcommodity-            fixcommodity a = a{commodity=c} where c = canonicalcommoditymap ! symbol (commodity a)-            canonicalcommoditymap =-                Map.fromList [(s,firstc{precision=maxp}) | s <- commoditysymbols,-                        let cs = commoditymap ! s,-                        let firstc = head cs,-                        let maxp = maximum $ map precision cs-                       ]-            commoditymap = Map.fromList [(s,commoditieswithsymbol s) | s <- commoditysymbols]-            commoditieswithsymbol s = filter ((s==) . symbol) commodities-            commoditysymbols = nub $ map symbol commodities-            commodities = map commodity (concatMap (amounts . pamount) (journalPostings j)-                                         ++ concatMap (amounts . hamount) (historical_prices j))-            fixprice :: Amount -> Amount-            fixprice a@Amount{price=Just _} = a-            fixprice a@Amount{commodity=c} = a{price=journalHistoricalPriceFor j d c}--            -- | Get the price for a commodity on the specified day from the price database, if known.-            -- Does only one lookup step, ie will not look up the price of a price.-            journalHistoricalPriceFor :: Journal -> Day -> Commodity -> Maybe MixedAmount-            journalHistoricalPriceFor j d Commodity{symbol=s} = do-              let ps = reverse $ filter ((<= d).hdate) $ filter ((s==).hsymbol) $ sortBy (comparing hdate) $ historical_prices j-              case ps of (HistoricalPrice{hamount=a}:_) -> Just $ canonicaliseCommodities a-                         _ -> Nothing-                  where-                    canonicaliseCommodities (Mixed as) = Mixed $ map canonicaliseCommodity as-                        where canonicaliseCommodity a@Amount{commodity=Commodity{symbol=s}} =-                                  a{commodity=findWithDefault (error "programmer error: canonicaliseCommodity failed") s canonicalcommoditymap}---- | Get just the amounts from a ledger, in the order parsed.-journalAmounts :: Journal -> [MixedAmount]-journalAmounts = map pamount . journalPostings---- | Get just the ammount commodities from a ledger, in the order parsed.-journalCommodities :: Journal -> [Commodity]-journalCommodities = map commodity . concatMap amounts . journalAmounts---- | Get just the amount precisions from a ledger, in the order parsed.-journalPrecisions :: Journal -> [Int]-journalPrecisions = map precision . journalCommodities---- | Close any open timelog sessions using the provided current time.-journalConvertTimeLog :: LocalTime -> Journal -> Journal-journalConvertTimeLog t l0 = l0 { jtxns = convertedTimeLog ++ jtxns l0-                                  , open_timelog_entries = []-                                  }-    where convertedTimeLog = entriesFromTimeLogEntries t $ open_timelog_entries l0---- | The (fully specified) date span containing all the raw ledger's transactions,--- or DateSpan Nothing Nothing if there are none.-journalDateSpan :: Journal -> DateSpan-journalDateSpan j-    | null ts = DateSpan Nothing Nothing-    | otherwise = DateSpan (Just $ tdate $ head ts) (Just $ addDays 1 $ tdate $ last ts)-    where-      ts = sortBy (comparing tdate) $ jtxns j---- | Check if a set of ledger account/description patterns matches the--- given account name or entry description.  Patterns are case-insensitive--- regular expression strings; those beginning with - are anti-patterns.-matchpats :: [String] -> String -> Bool-matchpats pats str =-    (null positives || any match positives) && (null negatives || not (any match negatives))-    where-      (negatives,positives) = partition isnegativepat pats-      match "" = True-      match pat = containsRegex (abspat pat) str-      negateprefix = "not:"-      isnegativepat = (negateprefix `isPrefixOf`)-      abspat pat = if isnegativepat pat then drop (length negateprefix) pat else pat---- | Calculate the account tree and account balances from a journal's--- postings, and return the results for efficient lookup.-crunchJournal :: Journal -> (Tree AccountName, Map.Map AccountName Account)-crunchJournal j = (ant,amap)-    where-      (ant,psof,_,inclbalof) = (groupPostings . journalPostings) j-      amap = Map.fromList [(a, acctinfo a) | a <- flatten ant]-      acctinfo a = Account a (psof a) (inclbalof a)---- | Given a list of postings, return an account name tree and three query--- functions that fetch postings, balance, and subaccount-including--- balance by account name.  This factors out common logic from--- cacheLedger and summarisePostingsInDateSpan.-groupPostings :: [Posting] -> (Tree AccountName,-                             (AccountName -> [Posting]),-                             (AccountName -> MixedAmount),-                             (AccountName -> MixedAmount))-groupPostings ps = (ant,psof,exclbalof,inclbalof)-    where-      anames = sort $ nub $ map paccount ps-      ant = accountNameTreeFrom $ expandAccountNames anames-      allanames = flatten ant-      pmap = Map.union (postingsByAccount ps) (Map.fromList [(a,[]) | a <- allanames])-      psof = (pmap !)-      balmap = Map.fromList $ flatten $ calculateBalances ant psof-      exclbalof = fst . (balmap !)-      inclbalof = snd . (balmap !)---- | Add subaccount-excluding and subaccount-including balances to a tree--- of account names somewhat efficiently, given a function that looks up--- transactions by account name.-calculateBalances :: Tree AccountName -> (AccountName -> [Posting]) -> Tree (AccountName, (MixedAmount, MixedAmount))-calculateBalances ant psof = addbalances ant-    where-      addbalances (Node a subs) = Node (a,(bal,bal+subsbal)) subs'-          where-            bal         = sumPostings $ psof a-            subsbal     = sum $ map (snd . snd . root) subs'-            subs'       = map addbalances subs---- | Convert a list of postings to a map from account name to that--- account's postings.-postingsByAccount :: [Posting] -> Map.Map AccountName [Posting]-postingsByAccount ps = m'-    where-      sortedps = sortBy (comparing paccount) ps-      groupedps = groupBy (\p1 p2 -> paccount p1 == paccount p2) sortedps-      m' = Map.fromList [(paccount $ head g, g) | g <- groupedps]
− Ledger/Ledger.hs
@@ -1,180 +0,0 @@-{-|--A compound data type for efficiency. A 'Ledger' caches information derived-from a 'Journal' for easier querying. Also it typically has had-uninteresting 'Transaction's and 'Posting's filtered out. It-contains:--- the original unfiltered 'Journal'--- a tree of 'AccountName's--- a map from account names to 'Account's--- the full text of the journal file, when available--This is the main object you'll deal with as a user of the Ledger-library. The most useful functions also have shorter, lower-case-aliases for easier interaction. Here's an example:--> > import Ledger-> > l <- readLedger "sample.ledger"-> > accountnames l-> ["assets","assets:bank","assets:bank:checking","assets:bank:saving",...-> > accounts l-> [Account assets with 0 txns and $-1 balance,Account assets:bank with...-> > topaccounts l-> [Account assets with 0 txns and $-1 balance,Account expenses with...-> > account l "assets"-> Account assets with 0 txns and $-1 balance-> > accountsmatching ["ch"] l-> accountsmatching ["ch"] l-> [Account assets:bank:checking with 4 txns and $0 balance]-> > subaccounts l (account l "assets")-> subaccounts l (account l "assets")-> [Account assets:bank with 0 txns and $1 balance,Account assets:cash...-> > head $ transactions l-> 2008/01/01 income assets:bank:checking $1 RegularPosting-> > accounttree 2 l-> Node {rootLabel = Account top with 0 txns and 0 balance, subForest = [...-> > accounttreeat l (account l "assets")-> Just (Node {rootLabel = Account assets with 0 txns and $-1 balance, ...-> > datespan l -- disabled-> DateSpan (Just 2008-01-01) (Just 2009-01-01)-> > rawdatespan l-> DateSpan (Just 2008-01-01) (Just 2009-01-01)-> > ledgeramounts l-> [$1,$-1,$1,$-1,$1,$-1,$1,$1,$-2,$1,$-1]-> > commodities l-> [Commodity {symbol = "$", side = L, spaced = False, comma = False, ...----}--module Ledger.Ledger-where-import qualified Data.Map as Map-import Data.Map (findWithDefault, fromList)-import Ledger.Utils-import Ledger.Types-import Ledger.Account (nullacct)-import Ledger.AccountName-import Ledger.Journal-import Ledger.Posting---instance Show Ledger where-    show l = printf "Ledger with %d transactions, %d accounts\n%s"-             (length (jtxns $ journal l) +-              length (jmodifiertxns $ journal l) +-              length (jperiodictxns $ journal l))-             (length $ accountnames l)-             (showtree $ accountnametree l)--nullledger :: Ledger-nullledger = Ledger{-      journal = nulljournal,-      accountnametree = nullaccountnametree,-      accountmap = fromList []-    }---- | Convert a journal to a more efficient cached ledger, described above.-cacheLedger :: Journal -> Ledger-cacheLedger j = nullledger{journal=j,accountnametree=ant,accountmap=amap}-    where (ant, amap) = crunchJournal j---- | Add (or recalculate) the cached journal info in a ledger.-cacheLedger' :: Ledger -> CachedLedger-cacheLedger' l = l{accountnametree=ant,accountmap=amap}-    where (ant, amap) = crunchJournal $ journal l---- | Like cacheLedger, but filtering the journal first.-cacheLedger'' filterspec l@Ledger{journal=j} = l{journal=j',accountnametree=ant,accountmap=amap}-    where (ant, amap) = crunchJournal j'-          j' = filterJournalPostings filterspec{depth=Nothing} j--type CachedLedger = Ledger---- | List a ledger's account names.-ledgerAccountNames :: Ledger -> [AccountName]-ledgerAccountNames = drop 1 . flatten . accountnametree---- | Get the named account from a (cached) ledger.--- If the ledger has not been cached (with crunchJournal or--- cacheLedger'), this returns the null account.-ledgerAccount :: Ledger -> AccountName -> Account-ledgerAccount l a = findWithDefault nullacct a $ accountmap l---- | List a ledger's accounts, in tree order-ledgerAccounts :: Ledger -> [Account]-ledgerAccounts = drop 1 . flatten . ledgerAccountTree 9999---- | List a ledger's top-level accounts, in tree order-ledgerTopAccounts :: Ledger -> [Account]-ledgerTopAccounts = map root . branches . ledgerAccountTree 9999---- | Accounts in ledger whose name matches the pattern, in tree order.-ledgerAccountsMatching :: [String] -> Ledger -> [Account]-ledgerAccountsMatching pats = filter (matchpats pats . aname) . accounts---- | List a ledger account's immediate subaccounts-ledgerSubAccounts :: Ledger -> Account -> [Account]-ledgerSubAccounts l Account{aname=a} = -    map (ledgerAccount l) $ filter (`isSubAccountNameOf` a) $ accountnames l---- | List a ledger's postings, in the order parsed.-ledgerPostings :: Ledger -> [Posting]-ledgerPostings = journalPostings . journal---- | Get a ledger's tree of accounts to the specified depth.-ledgerAccountTree :: Int -> Ledger -> Tree Account-ledgerAccountTree depth l = treemap (ledgerAccount l) $ treeprune depth $ accountnametree l---- | Get a ledger's tree of accounts rooted at the specified account.-ledgerAccountTreeAt :: Ledger -> Account -> Maybe (Tree Account)-ledgerAccountTreeAt l acct = subtreeat acct $ ledgerAccountTree 9999 l---- | The (fully specified) date span containing all the ledger's (filtered) transactions,--- or DateSpan Nothing Nothing if there are none.-ledgerDateSpan :: Ledger -> DateSpan-ledgerDateSpan = postingsDateSpan . ledgerPostings---- | Convenience aliases.-accountnames :: Ledger -> [AccountName]-accountnames = ledgerAccountNames--account :: Ledger -> AccountName -> Account-account = ledgerAccount--accounts :: Ledger -> [Account]-accounts = ledgerAccounts--topaccounts :: Ledger -> [Account]-topaccounts = ledgerTopAccounts--accountsmatching :: [String] -> Ledger -> [Account]-accountsmatching = ledgerAccountsMatching--subaccounts :: Ledger -> Account -> [Account]-subaccounts = ledgerSubAccounts--postings :: Ledger -> [Posting]-postings = ledgerPostings--commodities :: Ledger -> [Commodity]-commodities = nub . journalCommodities . journal--accounttree :: Int -> Ledger -> Tree Account-accounttree = ledgerAccountTree--accounttreeat :: Ledger -> Account -> Maybe (Tree Account)-accounttreeat = ledgerAccountTreeAt---- datespan :: Ledger -> DateSpan--- datespan = ledgerDateSpan--rawdatespan :: Ledger -> DateSpan-rawdatespan = journalDateSpan . journal--ledgeramounts :: Ledger -> [MixedAmount]-ledgeramounts = journalAmounts . journal
− Ledger/Parse.hs
@@ -1,732 +0,0 @@-{-# LANGUAGE CPP #-}-{-|--Parsers for standard ledger and timelog files.--Here is the ledger grammar from the ledger 2.5 manual:--@-The ledger file format is quite simple, but also very flexible. It supports-many options, though typically the user can ignore most of them. They are-summarized below.  The initial character of each line determines what the-line means, and how it should be interpreted. Allowable initial characters-are:--NUMBER      A line beginning with a number denotes an entry. It may be followed by any-            number of lines, each beginning with whitespace, to denote the entry’s account-            transactions. The format of the first line is:--                    DATE[=EDATE] [*|!] [(CODE)] DESC--            If ‘*’ appears after the date (with optional effective date), it indicates the entry-            is “cleared”, which can mean whatever the user wants it t omean. If ‘!’ appears-            after the date, it indicates d the entry is “pending”; i.e., tentatively cleared from-            the user’s point of view, but not yet actually cleared. If a ‘CODE’ appears in-            parentheses, it may be used to indicate a check number, or the type of the-            transaction. Following these is the payee, or a description of the transaction.-            The format of each following transaction is:--                      ACCOUNT     AMOUNT    [; NOTE]--            The ‘ACCOUNT’ may be surrounded by parentheses if it is a virtual-            transactions, or square brackets if it is a virtual transactions that must-            balance. The ‘AMOUNT’ can be followed by a per-unit transaction cost,-            by specifying ‘ AMOUNT’, or a complete transaction cost with ‘\@ AMOUNT’.-            Lastly, the ‘NOTE’ may specify an actual and/or effective date for the-            transaction by using the syntax ‘[ACTUAL_DATE]’ or ‘[=EFFECTIVE_DATE]’ or-            ‘[ACTUAL_DATE=EFFECtIVE_DATE]’.--=           An automated entry. A value expression must appear after the equal sign.-            After this initial line there should be a set of one or more transactions, just as-            if it were normal entry. If the amounts of the transactions have no commodity,-            they will be applied as modifiers to whichever real transaction is matched by-            the value expression.- -~           A period entry. A period expression must appear after the tilde.-            After this initial line there should be a set of one or more transactions, just as-            if it were normal entry.--!           A line beginning with an exclamation mark denotes a command directive. It-            must be immediately followed by the command word. The supported commands-            are:--           ‘!include’-                        Include the stated ledger file.-           ‘!account’-                        The account name is given is taken to be the parent of all transac--                        tions that follow, until ‘!end’ is seen.-           ‘!end’       Ends an account block.- -;          A line beginning with a colon indicates a comment, and is ignored.- -Y          If a line begins with a capital Y, it denotes the year used for all subsequent-           entries that give a date without a year. The year should appear immediately-           after the Y, for example: ‘Y2004’. This is useful at the beginning of a file, to-           specify the year for that file. If all entries specify a year, however, this command-           has no effect.-           - -P          Specifies a historical price for a commodity. These are usually found in a pricing-           history file (see the ‘-Q’ option). The syntax is:--                  P DATE SYMBOL PRICE--N SYMBOL   Indicates that pricing information is to be ignored for a given symbol, nor will-           quotes ever be downloaded for that symbol. Useful with a home currency, such-           as the dollar ($). It is recommended that these pricing options be set in the price-           database file, which defaults to ‘~/.pricedb’. The syntax for this command is:--                  N SYMBOL--        -D AMOUNT   Specifies the default commodity to use, by specifying an amount in the expected-           format. The entry command will use this commodity as the default when none-           other can be determined. This command may be used multiple times, to set-           the default flags for different commodities; whichever is seen last is used as the-           default commodity. For example, to set US dollars as the default commodity,-           while also setting the thousands flag and decimal flag for that commodity, use:--                  D $1,000.00--C AMOUNT1 = AMOUNT2-           Specifies a commodity conversion, where the first amount is given to be equiv--           alent to the second amount. The first amount should use the decimal precision-           desired during reporting:--                  C 1.00 Kb = 1024 bytes--i, o, b, h-           These four relate to timeclock support, which permits ledger to read timelog-           files. See the timeclock’s documentation for more info on the syntax of its-           timelog files.-@--Here is the timelog grammar from timeclock.el 2.6:--@-A timelog contains data in the form of a single entry per line.-Each entry has the form:--  CODE YYYY/MM/DD HH:MM:SS [COMMENT]--CODE is one of: b, h, i, o or O.  COMMENT is optional when the code is-i, o or O.  The meanings of the codes are:--  b  Set the current time balance, or \"time debt\".  Useful when-     archiving old log data, when a debt must be carried forward.-     The COMMENT here is the number of seconds of debt.--  h  Set the required working time for the given day.  This must-     be the first entry for that day.  The COMMENT in this case is-     the number of hours in this workday.  Floating point amounts-     are allowed.--  i  Clock in.  The COMMENT in this case should be the name of the-     project worked on.--  o  Clock out.  COMMENT is unnecessary, but can be used to provide-     a description of how the period went, for example.--  O  Final clock out.  Whatever project was being worked on, it is-     now finished.  Useful for creating summary reports.-@--Example:--@-i 2007/03/10 12:26:00 hledger-o 2007/03/10 17:26:02-@---}--module Ledger.Parse-where-import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError, catchError)-import Text.ParserCombinators.Parsec-import Text.ParserCombinators.Parsec.Char-import Text.ParserCombinators.Parsec.Combinator-import System.Directory-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding (readFile, putStr, putStrLn, print, getContents)-import System.IO.UTF8-#endif-import Ledger.Utils-import Ledger.Types-import Ledger.Dates-import Ledger.AccountName (accountNameFromComponents,accountNameComponents)-import Ledger.Amount-import Ledger.Transaction-import Ledger.Posting-import Ledger.Journal-import Ledger.Commodity (dollars,dollar,unknown)-import System.FilePath(takeDirectory,combine)----- | A JournalUpdate is some transformation of a "Journal". It can do I/O--- or raise an error.-type JournalUpdate = ErrorT String IO (Journal -> Journal)---- | Some context kept during parsing.-data LedgerFileCtx = Ctx {-      ctxYear     :: !(Maybe Integer)  -- ^ the default year most recently specified with Y-    , ctxCommod   :: !(Maybe String)   -- ^ I don't know-    , ctxAccount  :: ![String]         -- ^ the current stack of parent accounts specified by !account-    } deriving (Read, Show)--emptyCtx :: LedgerFileCtx-emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] }--setYear :: Integer -> GenParser tok LedgerFileCtx ()-setYear y = updateState (\ctx -> ctx{ctxYear=Just y})--getYear :: GenParser tok LedgerFileCtx (Maybe Integer)-getYear = liftM ctxYear getState--pushParentAccount :: String -> GenParser tok LedgerFileCtx ()-pushParentAccount parent = updateState addParentAccount-    where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }-          normalize = (++ ":") --popParentAccount :: GenParser tok LedgerFileCtx ()-popParentAccount = do ctx0 <- getState-                      case ctxAccount ctx0 of-                        [] -> unexpected "End of account block with no beginning"-                        (_:rest) -> setState $ ctx0 { ctxAccount = rest }--getParentAccount :: GenParser tok LedgerFileCtx String-getParentAccount = liftM (concat . reverse . ctxAccount) getState--expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath-expandPath pos fp = liftM mkRelative (expandHome fp)-  where-    mkRelative = combine (takeDirectory (sourceName pos))-    expandHome inname | "~/" `isPrefixOf` inname = do homedir <- liftIO getHomeDirectory-                                                      return $ homedir ++ drop 1 inname-                      | otherwise                = return inname---- let's get to it---- | Parses a ledger file or timelog file to a "Journal", or gives an--- error.  Requires the current (local) time to calculate any unfinished--- timelog sessions, we pass it in for repeatability.-parseLedgerFile :: LocalTime -> FilePath -> ErrorT String IO Journal-parseLedgerFile t "-" = liftIO getContents >>= parseLedger t "-"-parseLedgerFile t f   = liftIO (readFile f) >>= parseLedger t f---- | Like parseLedgerFile, but parses a string. A file path is still--- provided to save in the resulting journal.-parseLedger :: LocalTime -> FilePath -> String -> ErrorT String IO Journal-parseLedger reftime inname intxt =-  case runParser ledgerFile emptyCtx inname intxt of-    Right m  -> liftM (journalConvertTimeLog reftime) $ m `ap` return nulljournal-    Left err -> throwError $ show err -- XXX raises an uncaught exception if we have a parsec user error, eg from many ?---- parsers---- | Top-level journal parser. Returns a single composite, I/O performing,--- error-raising "JournalUpdate" which can be applied to an empty journal--- to get the final result.-ledgerFile :: GenParser Char LedgerFileCtx JournalUpdate-ledgerFile = do items <- many ledgerItem-                eof-                return $ liftM (foldr (.) id) $ sequence items-    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-      ledgerItem = choice [ ledgerExclamationDirective-                          , liftM (return . addTransaction) ledgerTransaction-                          , liftM (return . addModifierTransaction) ledgerModifierTransaction-                          , liftM (return . addPeriodicTransaction) ledgerPeriodicTransaction-                          , liftM (return . addHistoricalPrice) ledgerHistoricalPrice-                          , ledgerDefaultYear-                          , ledgerIgnoredPriceCommodity-                          , ledgerTagDirective-                          , ledgerEndTagDirective-                          , emptyLine >> return (return id)-                          , liftM (return . addTimeLogEntry)  timelogentry-                          ]--emptyLine :: GenParser Char st ()-emptyLine = do many spacenonewline-               optional $ (char ';' <?> "comment") >> many (noneOf "\n")-               newline-               return ()--ledgercomment :: GenParser Char st String-ledgercomment = do-  many1 $ char ';'-  many spacenonewline-  many (noneOf "\n")-  <?> "comment"--ledgercommentline :: GenParser Char st String-ledgercommentline = do-  many spacenonewline-  s <- ledgercomment-  optional newline-  eof-  return s-  <?> "comment"--ledgerExclamationDirective :: GenParser Char LedgerFileCtx JournalUpdate-ledgerExclamationDirective = do-  char '!' <?> "directive"-  directive <- many nonspace-  case directive of-    "include" -> ledgerInclude-    "account" -> ledgerAccountBegin-    "end"     -> ledgerAccountEnd-    _         -> mzero--ledgerInclude :: GenParser Char LedgerFileCtx JournalUpdate-ledgerInclude = do many1 spacenonewline-                   filename <- restofline-                   outerState <- getState-                   outerPos <- getPosition-                   let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"-                   return $ do contents <- expandPath outerPos filename >>= readFileE outerPos-                               case runParser ledgerFile outerState filename contents of-                                 Right l   -> l `catchError` (throwError . (inIncluded ++))-                                 Left perr -> throwError $ inIncluded ++ show perr-    where readFileE outerPos filename = ErrorT $ liftM Right (readFile filename) `catch` leftError-              where leftError err = return $ Left $ currentPos ++ whileReading ++ show err-                    currentPos = show outerPos-                    whileReading = " reading " ++ show filename ++ ":\n"--ledgerAccountBegin :: GenParser Char LedgerFileCtx JournalUpdate-ledgerAccountBegin = do many1 spacenonewline-                        parent <- ledgeraccountname-                        newline-                        pushParentAccount parent-                        return $ return id--ledgerAccountEnd :: GenParser Char LedgerFileCtx JournalUpdate-ledgerAccountEnd = popParentAccount >> return (return id)--ledgerModifierTransaction :: GenParser Char LedgerFileCtx ModifierTransaction-ledgerModifierTransaction = do-  char '=' <?> "modifier transaction"-  many spacenonewline-  valueexpr <- restofline-  postings <- ledgerpostings-  return $ ModifierTransaction valueexpr postings--ledgerPeriodicTransaction :: GenParser Char LedgerFileCtx PeriodicTransaction-ledgerPeriodicTransaction = do-  char '~' <?> "periodic transaction"-  many spacenonewline-  periodexpr <- restofline-  postings <- ledgerpostings-  return $ PeriodicTransaction periodexpr postings--ledgerHistoricalPrice :: GenParser Char LedgerFileCtx HistoricalPrice-ledgerHistoricalPrice = do-  char 'P' <?> "historical price"-  many spacenonewline-  date <- try (do {LocalTime d _ <- ledgerdatetime; return d}) <|> ledgerdate -- a time is ignored-  many1 spacenonewline-  symbol <- commoditysymbol-  many spacenonewline-  price <- someamount-  restofline-  return $ HistoricalPrice date symbol price--ledgerIgnoredPriceCommodity :: GenParser Char LedgerFileCtx JournalUpdate-ledgerIgnoredPriceCommodity = do-  char 'N' <?> "ignored-price commodity"-  many1 spacenonewline-  commoditysymbol-  restofline-  return $ return id--ledgerDefaultCommodity :: GenParser Char LedgerFileCtx JournalUpdate-ledgerDefaultCommodity = do-  char 'D' <?> "default commodity"-  many1 spacenonewline-  someamount-  restofline-  return $ return id--ledgerCommodityConversion :: GenParser Char LedgerFileCtx JournalUpdate-ledgerCommodityConversion = do-  char 'C' <?> "commodity conversion"-  many1 spacenonewline-  someamount-  many spacenonewline-  char '='-  many spacenonewline-  someamount-  restofline-  return $ return id--ledgerTagDirective :: GenParser Char LedgerFileCtx JournalUpdate-ledgerTagDirective = do-  string "tag" <?> "tag directive"-  many1 spacenonewline-  _ <- many1 nonspace-  restofline-  return $ return id--ledgerEndTagDirective :: GenParser Char LedgerFileCtx JournalUpdate-ledgerEndTagDirective = do-  string "end tag" <?> "end tag directive"-  restofline-  return $ return id---- like ledgerAccountBegin, updates the LedgerFileCtx-ledgerDefaultYear :: GenParser Char LedgerFileCtx JournalUpdate-ledgerDefaultYear = do-  char 'Y' <?> "default year"-  many spacenonewline-  y <- many1 digit-  let y' = read y-  guard (y' >= 1000)-  setYear y'-  return $ return id---- | Try to parse a ledger entry. If we successfully parse an entry,--- check it can be balanced, and fail if not.-ledgerTransaction :: GenParser Char LedgerFileCtx Transaction-ledgerTransaction = do-  date <- ledgerdate <?> "transaction"-  edate <- try (ledgereffectivedate date <?> "effective date") <|> return Nothing-  status <- ledgerstatus-  code <- ledgercode-  description <- many1 spacenonewline >> liftM rstrip (many (noneOf ";\n") <?> "description")-  comment <- ledgercomment <|> return ""-  restofline-  postings <- ledgerpostings-  let t = txnTieKnot $ Transaction date edate status code description comment postings ""-  case balanceTransaction t of-    Right t' -> return t'-    Left err -> fail err--ledgerdate :: GenParser Char LedgerFileCtx Day-ledgerdate = (try ledgerfulldate <|> ledgerpartialdate) <?> "full or partial date"--ledgerfulldate :: GenParser Char LedgerFileCtx Day-ledgerfulldate = do-  (y,m,d) <- ymd-  return $ fromGregorian (read y) (read m) (read d)---- | Match a partial M/D date in a ledger, and also require that a default--- year directive was previously encountered.-ledgerpartialdate :: GenParser Char LedgerFileCtx Day-ledgerpartialdate = do-  (_,m,d) <- md-  y <- getYear-  when (y==Nothing) $ fail "partial date found, but no default year specified"-  return $ fromGregorian (fromJust y) (read m) (read d)--ledgerdatetime :: GenParser Char LedgerFileCtx LocalTime-ledgerdatetime = do -  day <- ledgerdate-  many1 spacenonewline-  h <- many1 digit-  char ':'-  m <- many1 digit-  s <- optionMaybe $ do-      char ':'-      many1 digit-  let tod = TimeOfDay (read h) (read m) (maybe 0 (fromIntegral.read) s)-  return $ LocalTime day tod--ledgereffectivedate :: Day -> GenParser Char LedgerFileCtx (Maybe Day)-ledgereffectivedate actualdate = do-  char '='-  -- kludgy way to use actual date for default year-  let withDefaultYear d p = do-        y <- getYear-        let (y',_,_) = toGregorian d in setYear y'-        r <- p-        when (isJust y) $ setYear $ fromJust y-        return r-  edate <- withDefaultYear actualdate ledgerdate-  return $ Just edate--ledgerstatus :: GenParser Char st Bool-ledgerstatus = try (do { many1 spacenonewline; char '*' <?> "status"; return True } ) <|> return False--ledgercode :: GenParser Char st String-ledgercode = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""--ledgerpostings :: GenParser Char LedgerFileCtx [Posting]-ledgerpostings = do-  -- complicated to handle intermixed comment lines.. please make me better.-  ctx <- getState-  let parses p = isRight . parseWithCtx ctx p-  ls <- many1 $ try linebeginningwithspaces-  let ls' = filter (not . (ledgercommentline `parses`)) ls-  guard (not $ null ls')-  return $ map (fromparse . parseWithCtx ctx ledgerposting) ls'-  <?> "postings"--linebeginningwithspaces :: GenParser Char st String-linebeginningwithspaces = do-  sp <- many1 spacenonewline-  c <- nonspace-  cs <- restofline-  return $ sp ++ (c:cs) ++ "\n"--ledgerposting :: GenParser Char LedgerFileCtx Posting-ledgerposting = do-  many1 spacenonewline-  status <- ledgerstatus-  account <- transactionaccountname-  let (ptype, account') = (postingTypeFromAccountName account, unbracket account)-  amount <- postingamount-  many spacenonewline-  comment <- ledgercomment <|> return ""-  newline-  return (Posting status account' amount comment ptype Nothing)---- qualify with the parent account from parsing context-transactionaccountname :: GenParser Char LedgerFileCtx AccountName-transactionaccountname = liftM2 (++) getParentAccount ledgeraccountname---- | Parse an account name. Account names may have single spaces inside--- 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.-ledgeraccountname :: GenParser Char st AccountName-ledgeraccountname = do-    a <- many1 (nonspace <|> singlespace)-    let a' = striptrailingspace a-    when (accountNameFromComponents (accountNameComponents a') /= a')-         (fail $ "accountname seems ill-formed: "++a')-    return a'-    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---- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace---     <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"---- | Parse an amount, with an optional left or right currency symbol and--- optional price.-postingamount :: GenParser Char st MixedAmount-postingamount =-  try (do-        many1 spacenonewline-        someamount <|> return missingamt-      ) <|> return missingamt--someamount :: GenParser Char st MixedAmount-someamount = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount --leftsymbolamount :: GenParser Char st MixedAmount-leftsymbolamount = do-  sym <- commoditysymbol -  sp <- many spacenonewline-  (q,p,comma) <- amountquantity-  pri <- priceamount-  let c = Commodity {symbol=sym,side=L,spaced=not $ null sp,comma=comma,precision=p}-  return $ Mixed [Amount c q pri]-  <?> "left-symbol amount"--rightsymbolamount :: GenParser Char st MixedAmount-rightsymbolamount = do-  (q,p,comma) <- amountquantity-  sp <- many spacenonewline-  sym <- commoditysymbol-  pri <- priceamount-  let c = Commodity {symbol=sym,side=R,spaced=not $ null sp,comma=comma,precision=p}-  return $ Mixed [Amount c q pri]-  <?> "right-symbol amount"--nosymbolamount :: GenParser Char st MixedAmount-nosymbolamount = do-  (q,p,comma) <- amountquantity-  pri <- priceamount-  let c = Commodity {symbol="",side=L,spaced=False,comma=comma,precision=p}-  return $ Mixed [Amount c q pri]-  <?> "no-symbol amount"--commoditysymbol :: GenParser Char st String-commoditysymbol = (quotedcommoditysymbol <|>-                   many1 (noneOf "0123456789-.@;\n \"")-                  ) <?> "commodity symbol"--quotedcommoditysymbol :: GenParser Char st String-quotedcommoditysymbol = do-  char '"'-  s <- many1 $ noneOf "-.@;\n \""-  char '"'-  return s--priceamount :: GenParser Char st (Maybe MixedAmount)-priceamount =-    try (do-          many spacenonewline-          char '@'-          many spacenonewline-          a <- someamount-          return $ Just a-          ) <|> return Nothing---- gawd.. trying to parse a ledger number without error:---- | Parse a ledger-style numeric quantity and also return the number of--- digits to the right of the decimal point and whether thousands are--- separated by comma.-amountquantity :: GenParser Char st (Double, Int, Bool)-amountquantity = do-  sign <- optionMaybe $ string "-"-  (intwithcommas,frac) <- numberparts-  let comma = ',' `elem` intwithcommas-  let precision = length frac-  -- read the actual value. We expect this read to never fail.-  let int = filter (/= ',') intwithcommas-  let int' = if null int then "0" else int-  let frac' = if null frac then "0" else frac-  let sign' = fromMaybe "" sign-  let quantity = read $ sign'++int'++"."++frac'-  return (quantity, precision, comma)-  <?> "commodity quantity"---- | parse the two strings of digits before and after a possible decimal--- point.  The integer part may contain commas, or either part may be--- empty, or there may be no point.-numberparts :: GenParser Char st (String,String)-numberparts = numberpartsstartingwithdigit <|> numberpartsstartingwithpoint--numberpartsstartingwithdigit :: GenParser Char st (String,String)-numberpartsstartingwithdigit = do-  let digitorcomma = digit <|> char ','-  first <- digit-  rest <- many digitorcomma-  frac <- try (do {char '.'; many digit}) <|> return ""-  return (first:rest,frac)-                     -numberpartsstartingwithpoint :: GenParser Char st (String,String)-numberpartsstartingwithpoint = do-  char '.'-  frac <- many1 digit-  return ("",frac)-                     ---- | Parse a timelog entry.-timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry-timelogentry = do-  code <- oneOf "bhioO"-  many1 spacenonewline-  datetime <- ledgerdatetime-  comment <- optionMaybe (many1 spacenonewline >> liftM2 (++) getParentAccount restofline)-  return $ TimeLogEntry (read [code]) datetime (fromMaybe "" comment)----- | Parse a hledger display expression, which is a simple date test like--- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.-datedisplayexpr :: GenParser Char st (Posting -> Bool)-datedisplayexpr = do-  char 'd'-  op <- compareop-  char '['-  (y,m,d) <- smartdate-  char ']'-  let date    = parsedate $ printf "%04s/%02s/%02s" y m d-      test op = return $ (`op` date) . postingDate-  case op of-    "<"  -> test (<)-    "<=" -> test (<=)-    "="  -> test (==)-    "==" -> test (==)-    ">=" -> test (>=)-    ">"  -> test (>)-    _    -> mzero--compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]---tests_Parse = TestList [--   "ledgerTransaction" ~: do-    assertParseEqual (parseWithCtx emptyCtx ledgerTransaction entry1_str) entry1-    assertBool "ledgerTransaction should not parse just a date"-                   $ isLeft $ parseWithCtx emptyCtx ledgerTransaction "2009/1/1\n"-    assertBool "ledgerTransaction should require some postings"-                   $ isLeft $ parseWithCtx emptyCtx ledgerTransaction "2009/1/1 a\n"-    let t = parseWithCtx emptyCtx ledgerTransaction "2009/1/1 a ;comment\n b 1\n"-    assertBool "ledgerTransaction should not include a comment in the description"-                   $ either (const False) ((== "a") . tdescription) t--  ,"ledgerModifierTransaction" ~: do-     assertParse (parseWithCtx emptyCtx ledgerModifierTransaction "= (some value expr)\n some:postings  1\n")--  ,"ledgerPeriodicTransaction" ~: do-     assertParse (parseWithCtx emptyCtx ledgerPeriodicTransaction "~ (some period expr)\n some:postings  1\n")--  ,"ledgerExclamationDirective" ~: do-     assertParse (parseWithCtx emptyCtx ledgerExclamationDirective "!include /some/file.x\n")-     assertParse (parseWithCtx emptyCtx ledgerExclamationDirective "!account some:account\n")-     assertParse (parseWithCtx emptyCtx (ledgerExclamationDirective >> ledgerExclamationDirective) "!account a\n!end\n")--  ,"ledgercommentline" ~: do-     assertParse (parseWithCtx emptyCtx ledgercommentline "; some comment \n")-     assertParse (parseWithCtx emptyCtx ledgercommentline " \t; x\n")-     assertParse (parseWithCtx emptyCtx ledgercommentline ";x")--  ,"ledgerDefaultYear" ~: do-     assertParse (parseWithCtx emptyCtx ledgerDefaultYear "Y 2010\n")-     assertParse (parseWithCtx emptyCtx ledgerDefaultYear "Y 10001\n")--  ,"ledgerHistoricalPrice" ~:-    assertParseEqual (parseWithCtx emptyCtx ledgerHistoricalPrice "P 2004/05/01 XYZ $55.00\n") (HistoricalPrice (parsedate "2004/05/01") "XYZ" $ Mixed [dollars 55])--  ,"ledgerIgnoredPriceCommodity" ~: do-     assertParse (parseWithCtx emptyCtx ledgerIgnoredPriceCommodity "N $\n")--  ,"ledgerDefaultCommodity" ~: do-     assertParse (parseWithCtx emptyCtx ledgerDefaultCommodity "D $1,000.0\n")--  ,"ledgerCommodityConversion" ~: do-     assertParse (parseWithCtx emptyCtx ledgerCommodityConversion "C 1h = $50.00\n")--  ,"ledgerTagDirective" ~: do-     assertParse (parseWithCtx emptyCtx ledgerTagDirective "tag foo \n")--  ,"ledgerEndTagDirective" ~: do-     assertParse (parseWithCtx emptyCtx ledgerEndTagDirective "end tag \n")--  ,"ledgeraccountname" ~: do-    assertBool "ledgeraccountname parses a normal accountname" (isRight $ parsewith ledgeraccountname "a:b:c")-    assertBool "ledgeraccountname rejects an empty inner component" (isLeft $ parsewith ledgeraccountname "a::c")-    assertBool "ledgeraccountname rejects an empty leading component" (isLeft $ parsewith ledgeraccountname ":b:c")-    assertBool "ledgeraccountname rejects an empty trailing component" (isLeft $ parsewith ledgeraccountname "a:b:")-- ,"ledgerposting" ~: do-    assertParseEqual (parseWithCtx emptyCtx ledgerposting "  expenses:food:dining  $10.00\n") -                     (Posting False "expenses:food:dining" (Mixed [dollars 10]) "" RegularPosting Nothing)-    assertBool "ledgerposting parses a quoted commodity with numbers"-                   (isRight $ parseWithCtx emptyCtx ledgerposting "  a  1 \"DE123\"\n")--  ,"someamount" ~: do-     let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity-         assertMixedAmountParse parseresult mixedamount =-             (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)-     assertMixedAmountParse (parsewith someamount "1 @ $2")-                            (Mixed [Amount unknown 1 (Just $ Mixed [Amount dollar{precision=0} 2 Nothing])])--  ,"postingamount" ~: do-    assertParseEqual (parseWithCtx emptyCtx postingamount " $47.18") (Mixed [dollars 47.18])-    assertParseEqual (parseWithCtx emptyCtx postingamount " $1.")-                (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,comma=False,precision=0} 1 Nothing])-- ]--entry1_str = unlines- ["2007/01/28 coopportunity"- ,"    expenses:food:groceries                   $47.18"- ,"    assets:checking                          $-47.18"- ,""- ]--entry1 =-    txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""-     [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing, -      Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting Nothing] ""--
− Ledger/Posting.hs
@@ -1,94 +0,0 @@-{-|--A 'Posting' represents a 'MixedAmount' being added to or subtracted from a-single 'Account'.  Each 'Transaction' contains two or more postings which-should add up to 0. Postings also reference their parent transaction, so-we can get a date or description for a posting (from the transaction).---}--module Ledger.Posting-where-import Ledger.Utils-import Ledger.Types-import Ledger.Amount-import Ledger.AccountName-import Ledger.Dates (nulldate)---instance Show Posting where show = showPosting--nullposting = Posting False "" nullmixedamt "" RegularPosting Nothing--showPosting :: Posting -> String-showPosting (Posting{paccount=a,pamount=amt,pcomment=com,ptype=t}) =-    concatTopPadded [showaccountname a ++ " ", showamount amt, comment]-    where-      ledger3ishlayout = False-      acctnamewidth = if ledger3ishlayout then 25 else 22-      showaccountname = printf ("%-"++(show acctnamewidth)++"s") . bracket . elideAccountName width-      (bracket,width) = case t of-                          BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)-                          VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)-                          _ -> (id,acctnamewidth)-      showamount = padleft 12 . showMixedAmountOrZero-      comment = if null com then "" else "  ; " ++ com--- XXX refactor-showPostingForRegister (Posting{paccount=a,pamount=amt,ptype=t}) =-    concatTopPadded [showaccountname a ++ " ", showamount amt]-    where-      ledger3ishlayout = False-      acctnamewidth = if ledger3ishlayout then 25 else 22-      showaccountname = printf ("%-"++(show acctnamewidth)++"s") . bracket . elideAccountName width-      (bracket,width) = case t of-                          BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)-                          VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)-                          _ -> (id,acctnamewidth)-      showamount = padleft 12 . showMixedAmountOrZeroWithoutPrice--isReal :: Posting -> Bool-isReal p = ptype p == RegularPosting--isVirtual :: Posting -> Bool-isVirtual p = ptype p == VirtualPosting--isBalancedVirtual :: Posting -> Bool-isBalancedVirtual p = ptype p == BalancedVirtualPosting--hasAmount :: Posting -> Bool-hasAmount = (/= missingamt) . pamount--postingTypeFromAccountName a-    | head a == '[' && last a == ']' = BalancedVirtualPosting-    | head a == '(' && last a == ')' = VirtualPosting-    | otherwise = RegularPosting--accountNamesFromPostings :: [Posting] -> [AccountName]-accountNamesFromPostings = nub . map paccount--sumPostings :: [Posting] -> MixedAmount-sumPostings = sumMixedAmountsPreservingHighestPrecision . map pamount--postingDate :: Posting -> Day-postingDate p = maybe nulldate tdate $ ptransaction p--postingCleared :: Posting -> Bool-postingCleared p = maybe False tstatus $ ptransaction p---- | Does this posting fall within the given date span ?-isPostingInDateSpan :: DateSpan -> Posting -> Bool-isPostingInDateSpan (DateSpan Nothing Nothing)   _ = True-isPostingInDateSpan (DateSpan Nothing (Just e))  p = postingDate p < e-isPostingInDateSpan (DateSpan (Just b) Nothing)  p = postingDate p >= b-isPostingInDateSpan (DateSpan (Just b) (Just e)) p = d >= b && d < e where d = postingDate p--isEmptyPosting :: Posting -> Bool-isEmptyPosting = isZeroMixedAmount . pamount---- | Get the minimal date span which contains all the postings, or--- DateSpan Nothing Nothing if there are none.-postingsDateSpan :: [Posting] -> DateSpan-postingsDateSpan [] = DateSpan Nothing Nothing-postingsDateSpan ps = DateSpan (Just $ postingDate $ head ps') (Just $ addDays 1 $ postingDate $ last ps')-    where ps' = sortBy (comparing postingDate) ps-
− Ledger/TimeLog.hs
@@ -1,90 +0,0 @@-{-|--A 'TimeLogEntry' is a clock-in, clock-out, or other directive in a timelog-file (see timeclock.el or the command-line version). These can be-converted to 'Transactions' and queried like a ledger.---}--module Ledger.TimeLog-where-import Ledger.Utils-import Ledger.Types-import Ledger.Dates-import Ledger.Commodity-import Ledger.Transaction--instance Show TimeLogEntry where -    show t = printf "%s %s %s" (show $ tlcode t) (show $ tldatetime t) (tlcomment t)--instance Show TimeLogCode where -    show SetBalance = "b"-    show SetRequiredHours = "h"-    show In = "i"-    show Out = "o"-    show FinalOut = "O"--instance Read TimeLogCode where -    readsPrec _ ('b' : xs) = [(SetBalance, xs)]-    readsPrec _ ('h' : xs) = [(SetRequiredHours, xs)]-    readsPrec _ ('i' : xs) = [(In, xs)]-    readsPrec _ ('o' : xs) = [(Out, xs)]-    readsPrec _ ('O' : xs) = [(FinalOut, xs)]-    readsPrec _ _ = []---- | Convert time log entries to ledger transactions. When there is no--- clockout, add one with the provided current time. Sessions crossing--- midnight are split into days to give accurate per-day totals.-entriesFromTimeLogEntries :: LocalTime -> [TimeLogEntry] -> [Transaction]-entriesFromTimeLogEntries _ [] = []-entriesFromTimeLogEntries now [i]-    | odate > idate = entryFromTimeLogInOut i o' : entriesFromTimeLogEntries now [i',o]-    | otherwise = [entryFromTimeLogInOut i o]-    where-      o = TimeLogEntry Out end ""-      end = if itime > now then itime else now-      (itime,otime) = (tldatetime i,tldatetime o)-      (idate,odate) = (localDay itime,localDay otime)-      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}-      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}-entriesFromTimeLogEntries now (i:o:rest)-    | odate > idate = entryFromTimeLogInOut i o' : entriesFromTimeLogEntries now (i':o:rest)-    | otherwise = entryFromTimeLogInOut i o : entriesFromTimeLogEntries now rest-    where-      (itime,otime) = (tldatetime i,tldatetime o)-      (idate,odate) = (localDay itime,localDay otime)-      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}-      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}---- | Convert a timelog clockin and clockout entry to an equivalent ledger--- entry, representing the time expenditure. Note this entry is  not balanced,--- since we omit the \"assets:time\" transaction for simpler output.-entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction-entryFromTimeLogInOut i o-    | otime >= itime = t-    | otherwise = -        error $ "clock-out time less than clock-in time in:\n" ++ showTransaction t-    where-      t = Transaction {-            tdate         = idate,-            teffectivedate = Nothing,-            tstatus       = True,-            tcode         = "",-            tdescription  = showtime itod ++ "-" ++ showtime otod,-            tcomment      = "",-            tpostings = ps,-            tpreceding_comment_lines=""-          }-      showtime = take 5 . show-      acctname = tlcomment i-      itime    = tldatetime i-      otime    = tldatetime o-      itod     = localTimeOfDay itime-      otod     = localTimeOfDay otime-      idate    = localDay itime-      hrs      = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc-      amount   = Mixed [hours hrs]-      ps       = [Posting{pstatus=False,paccount=acctname,pamount=amount,-                          pcomment="",ptype=RegularPosting,ptransaction=Just t}-                 --,Posting "assets:time" (-amount) "" RegularPosting-                 ]
− Ledger/Transaction.hs
@@ -1,250 +0,0 @@-{-|--A 'Transaction' represents a single balanced entry in the ledger file. It-normally contains two or more balanced 'Posting's.---}--module Ledger.Transaction-where-import Ledger.Utils-import Ledger.Types-import Ledger.Dates-import Ledger.Posting-import Ledger.Amount-import Ledger.Commodity (dollars, dollar, unknown)--instance Show Transaction where show = showTransactionUnelided--instance Show ModifierTransaction where -    show t = "= " ++ mtvalueexpr t ++ "\n" ++ unlines (map show (mtpostings t))--instance Show PeriodicTransaction where -    show t = "~ " ++ ptperiodicexpr t ++ "\n" ++ unlines (map show (ptpostings t))--nulltransaction :: Transaction-nulltransaction = Transaction {-                    tdate=nulldate,-                    teffectivedate=Nothing, -                    tstatus=False, -                    tcode="", -                    tdescription="", -                    tcomment="",-                    tpostings=[],-                    tpreceding_comment_lines=""-                  }--{-|-Show a ledger entry, formatted for the print command. ledger 2.x's-standard format looks like this:--@-yyyy/mm/dd[ *][ CODE] description.........          [  ; comment...............]-    account name 1.....................  ...$amount1[  ; comment...............]-    account name 2.....................  ..$-amount1[  ; comment...............]--pcodewidth    = no limit -- 10          -- mimicking ledger layout.-pdescwidth    = no limit -- 20          -- I don't remember what these mean,-pacctwidth    = 35 minimum, no maximum  -- they were important at the time.-pamtwidth     = 11-pcommentwidth = no limit -- 22-@--}-showTransaction :: Transaction -> String-showTransaction = showTransaction' True False--showTransactionUnelided :: Transaction -> String-showTransactionUnelided = showTransaction' False False--showTransactionForPrint :: Bool -> Transaction -> String-showTransactionForPrint effective = showTransaction' False effective--showTransaction' :: Bool -> Bool -> Transaction -> String-showTransaction' elide effective t =-    unlines $ [description] ++ showpostings (tpostings t) ++ [""]-    where-      description = concat [date, status, code, desc, comment]-      date | effective = showdate $ fromMaybe (tdate t) $ teffectivedate t-           | otherwise = showdate (tdate t) ++ maybe "" showedate (teffectivedate t)-      status = if tstatus t then " *" else ""-      code = if length (tcode t) > 0 then printf " (%s)" $ tcode t else ""-      desc = ' ' : tdescription t-      comment = if null com then "" else "  ; " ++ com where com = tcomment t-      showdate = printf "%-10s" . showDate-      showedate = printf "=%s" . showdate-      showpostings ps-          | elide && length ps > 1 && isTransactionBalanced t-              = map showposting (init ps) ++ [showpostingnoamt (last ps)]-          | otherwise = map showposting ps-          where-            showposting p = showacct p ++ "  " ++ showamount (pamount p) ++ showcomment (pcomment p)-            showpostingnoamt p = rstrip $ showacct p ++ "              " ++ showcomment (pcomment p)-            showacct p = "    " ++ showstatus p ++ printf (printf "%%-%ds" w) (showAccountName Nothing (ptype p) (paccount p))-            w = maximum $ map (length . paccount) ps-            showamount = printf "%12s" . showMixedAmountOrZero-            showcomment s = if null s then "" else "  ; "++s-            showstatus p = if pstatus p then "* " else ""---- | Show an account name, clipped to the given width if any, and--- appropriately bracketed/parenthesised for the given posting type.-showAccountName :: Maybe Int -> PostingType -> AccountName -> String-showAccountName w = fmt-    where-      fmt RegularPosting = take w'-      fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse-      fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse-      w' = fromMaybe 999999 w-      parenthesise s = "("++s++")"-      bracket s = "["++s++"]"--realPostings :: Transaction -> [Posting]-realPostings = filter isReal . tpostings--virtualPostings :: Transaction -> [Posting]-virtualPostings = filter isVirtual . tpostings--balancedVirtualPostings :: Transaction -> [Posting]-balancedVirtualPostings = filter isBalancedVirtual . tpostings---- | Get the sums of a transaction's real, virtual, and balanced virtual postings.-transactionPostingBalances :: Transaction -> (MixedAmount,MixedAmount,MixedAmount)-transactionPostingBalances t = (sumPostings $ realPostings t-                               ,sumPostings $ virtualPostings t-                               ,sumPostings $ balancedVirtualPostings t)---- | Is this transaction balanced ? A balanced transaction's real--- (non-virtual) postings sum to 0, and any balanced virtual postings--- also sum to 0.-isTransactionBalanced :: Transaction -> Bool-isTransactionBalanced t = isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum-    where (rsum, _, bvsum) = transactionPostingBalances t---- | Ensure that this entry is balanced, possibly auto-filling a missing--- amount first. We can auto-fill if there is just one non-virtual--- transaction without an amount. The auto-filled balance will be--- converted to cost basis if possible. If the entry can not be balanced,--- return an error message instead.-balanceTransaction :: Transaction -> Either String Transaction-balanceTransaction t@Transaction{tpostings=ps}-    | length missingamounts' > 1 = Left $ printerr "could not balance this transaction (too many missing amounts)"-    | not $ isTransactionBalanced t' = Left $ printerr $ nonzerobalanceerror t'-    | otherwise = Right t'-    where-      (withamounts, missingamounts) = partition hasAmount $ filter isReal ps-      (_, missingamounts') = partition hasAmount ps-      t' = t{tpostings=ps'}-      ps' | length missingamounts == 1 = map balance ps-          | otherwise = ps-          where -            balance p | isReal p && not (hasAmount p) = p{pamount = costOfMixedAmount (-otherstotal)}-                      | otherwise = p-                      where otherstotal = sum $ map pamount withamounts-      printerr s = intercalate "\n" [s, showTransactionUnelided t]--nonzerobalanceerror :: Transaction -> String-nonzerobalanceerror t = printf "could not balance this transaction (%s%s%s)" rmsg sep bvmsg-    where-      (rsum, _, bvsum) = transactionPostingBalances t-      rmsg | isReallyZeroMixedAmountCost rsum = ""-           | otherwise = "real postings are off by " ++ show rsum-      bvmsg | isReallyZeroMixedAmountCost bvsum = ""-            | otherwise = "balanced virtual postings are off by " ++ show bvsum-      sep = if not (null rmsg) && not (null bvmsg) then "; " else ""---- | Convert the primary date to either the actual or effective date.-ledgerTransactionWithDate :: WhichDate -> Transaction -> Transaction-ledgerTransactionWithDate ActualDate t = t-ledgerTransactionWithDate EffectiveDate t = txnTieKnot t{tdate=fromMaybe (tdate t) (teffectivedate t)}-    ---- | Ensure a transaction's postings refer back to it.-txnTieKnot :: Transaction -> Transaction-txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (settxn t) ps}---- | Set a posting's parent transaction.-settxn :: Transaction -> Posting -> Posting-settxn t p = p{ptransaction=Just t}--tests_Transaction = TestList [-  "showTransaction" ~: do-     assertEqual "show a balanced transaction, eliding last amount"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries        $47.18"-        ,"    assets:checking"-        ,""-        ])-       (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""-                [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting (Just t)-                ,Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting (Just t)-                ] ""-        in showTransaction t)--  ,"showTransaction" ~: do-     assertEqual "show a balanced transaction, no eliding"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries        $47.18"-        ,"    assets:checking               $-47.18"-        ,""-        ])-       (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""-                [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting (Just t)-                ,Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting (Just t)-                ] ""-        in showTransactionUnelided t)--     -- document some cases that arise in debug/testing:-  ,"showTransaction" ~: do-     assertEqual "show an unbalanced transaction, should not elide"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries        $47.18"-        ,"    assets:checking               $-47.19"-        ,""-        ])-       (showTransaction-        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""-         [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing-         ,Posting False "assets:checking" (Mixed [dollars (-47.19)]) "" RegularPosting Nothing-         ] ""))--  ,"showTransaction" ~: do-     assertEqual "show an unbalanced transaction with one posting, should not elide"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries        $47.18"-        ,""-        ])-       (showTransaction-        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""-         [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing-         ] ""))--  ,"showTransaction" ~: do-     assertEqual "show a transaction with one posting and a missing amount"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries              "-        ,""-        ])-       (showTransaction-        (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" ""-         [Posting False "expenses:food:groceries" missingamt "" RegularPosting Nothing-         ] ""))--  ,"showTransaction" ~: do-     assertEqual "show a transaction with a priced commodityless amount"-       (unlines-        ["2010/01/01 x"-        ,"    a        1 @ $2"-        ,"    b              "-        ,""-        ])-       (showTransaction-        (txnTieKnot $ Transaction (parsedate "2010/01/01") Nothing False "" "x" ""-         [Posting False "a" (Mixed [Amount unknown 1 (Just $ Mixed [Amount dollar{precision=0} 2 Nothing])]) "" RegularPosting Nothing-         ,Posting False "b" missingamt "" RegularPosting Nothing-         ] ""))--  ]
− Ledger/Types.hs
@@ -1,153 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-|--Most data types are defined here to avoid import cycles.-Here is an overview of the hledger data model:--> Ledger              -- hledger's ledger is a journal file plus cached/derived data->  Journal            -- a representation of the journal file, containing..->   [Transaction]     -- ..journal transactions, which have date, status, code, description and..->    [Posting]        -- ..two or more account postings (account name and amount)->  Tree AccountName   -- all account names as a tree->  Map AccountName Account -- a map from account name to account info (postings and balances)--For more detailed documentation on each type, see the corresponding modules.--Terminology has been in flux:--  - ledger 2 had entries containing transactions.--  - hledger 0.4 had Entrys containing RawTransactions, which were flattened to Transactions.--  - ledger 3 has transactions containing postings.--  - hledger 0.5 had LedgerTransactions containing Postings, which were flattened to Transactions.--  - hledger 0.8 has Transactions containing Postings, and no flattened type.---}--module Ledger.Types -where-import Ledger.Utils-import qualified Data.Map as Map-import System.Time (ClockTime)-import Data.Typeable (Typeable)---type SmartDate = (String,String,String)--data WhichDate = ActualDate | EffectiveDate deriving (Eq,Show)--data DateSpan = DateSpan (Maybe Day) (Maybe Day) deriving (Eq,Show,Ord)--data Interval = NoInterval | Daily | Weekly | Monthly | Quarterly | Yearly -                deriving (Eq,Show,Ord)--type AccountName = String--data Side = L | R deriving (Eq,Show,Ord) --data Commodity = Commodity {-      symbol :: String,  -- ^ the commodity's symbol-      -- display preferences for amounts of this commodity-      side :: Side,      -- ^ should the symbol appear on the left or the right-      spaced :: Bool,    -- ^ should there be a space between symbol and quantity-      comma :: Bool,     -- ^ should thousands be comma-separated-      precision :: Int   -- ^ number of decimal places to display-    } deriving (Eq,Show,Ord)--data Amount = Amount {-      commodity :: Commodity,-      quantity :: Double,-      price :: Maybe MixedAmount  -- ^ unit price/conversion rate for this amount at posting time-    } deriving (Eq)--newtype MixedAmount = Mixed [Amount] deriving (Eq)--data PostingType = RegularPosting | VirtualPosting | BalancedVirtualPosting-                   deriving (Eq,Show)--data Posting = Posting {-      pstatus :: Bool,-      paccount :: AccountName,-      pamount :: MixedAmount,-      pcomment :: String,-      ptype :: PostingType,-      ptransaction :: Maybe Transaction  -- ^ this posting's parent transaction (co-recursive types).-                                        -- Tying this knot gets tedious, Maybe makes it easier/optional.-    } deriving (Eq)--data Transaction = Transaction {-      tdate :: Day,-      teffectivedate :: Maybe Day,-      tstatus :: Bool,  -- XXX tcleared ?-      tcode :: String,-      tdescription :: String,-      tcomment :: String,-      tpostings :: [Posting],-      tpreceding_comment_lines :: String-    } deriving (Eq)--data ModifierTransaction = ModifierTransaction {-      mtvalueexpr :: String,-      mtpostings :: [Posting]-    } deriving (Eq)--data PeriodicTransaction = PeriodicTransaction {-      ptperiodicexpr :: String,-      ptpostings :: [Posting]-    } deriving (Eq)--data TimeLogCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord) --data TimeLogEntry = TimeLogEntry {-      tlcode :: TimeLogCode,-      tldatetime :: LocalTime,-      tlcomment :: String-    } deriving (Eq,Ord)--data HistoricalPrice = HistoricalPrice {-      hdate :: Day,-      hsymbol :: String,-      hamount :: MixedAmount-    } deriving (Eq) -- & Show (in Amount.hs)--data Journal = Journal {-      jmodifiertxns :: [ModifierTransaction],-      jperiodictxns :: [PeriodicTransaction],-      jtxns :: [Transaction],-      open_timelog_entries :: [TimeLogEntry],-      historical_prices :: [HistoricalPrice],-      final_comment_lines :: String,-      filepath :: FilePath,-      filereadtime :: ClockTime,-      jtext :: String-    } deriving (Eq)--data Account = Account {-      aname :: AccountName,-      apostings :: [Posting],    -- ^ transactions in this account-      abalance :: MixedAmount    -- ^ sum of transactions in this account and subaccounts-    }--data Ledger = Ledger {-      journal :: Journal,-      accountnametree :: Tree AccountName,-      accountmap :: Map.Map AccountName Account-    } deriving Typeable---- | A generic, pure specification of how to filter transactions/postings.--- This exists to keep app-specific options out of the hledger library.-data FilterSpec = FilterSpec {-     datespan  :: DateSpan   -- ^ only include if in this date span-    ,cleared   :: Maybe Bool -- ^ only include if cleared\/uncleared\/don't care-    ,real      :: Bool       -- ^ only include if real\/don't care-    ,empty     :: Bool       -- ^ include if empty (ie amount is zero)-    ,costbasis :: Bool       -- ^ convert all amounts to cost basis-    ,acctpats  :: [String]   -- ^ only include if matching these account patterns-    ,descpats  :: [String]   -- ^ only include if matching these description patterns-    ,whichdate :: WhichDate  -- ^ which dates to use (actual or effective)-    ,depth     :: Maybe Int-    } deriving (Show)-
− Ledger/Utils.hs
@@ -1,321 +0,0 @@-{-# LANGUAGE CPP #-}-{-|--Provide standard imports and utilities which are useful everywhere, or-needed low in the module hierarchy. This is the bottom of the dependency graph.---}--module Ledger.Utils (-module Data.Char,-module Control.Monad,-module Data.List,---module Data.Map,-module Data.Maybe,-module Data.Ord,-module Data.Tree,-module Data.Time.Clock,-module Data.Time.Calendar,-module Data.Time.LocalTime,-module Debug.Trace,-module Ledger.Utils,-module Text.Printf,-module Text.RegexPR,-module Test.HUnit,-)-where-import Data.Char-import Control.Exception-import Control.Monad-import Data.List---import qualified Data.Map as Map-import Data.Maybe-import Data.Ord-import Data.Tree-import Data.Time.Clock-import Data.Time.Calendar-import Data.Time.LocalTime-import Debug.Trace-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding (readFile,putStr,print)-import System.IO.UTF8-#endif-import Test.HUnit-import Text.Printf-import Text.RegexPR-import Text.ParserCombinators.Parsec----- strings--lowercase = map toLower-uppercase = map toUpper--strip = lstrip . rstrip-lstrip = dropws-rstrip = reverse . dropws . reverse-dropws = dropWhile (`elem` " \t")--elideLeft width s =-    if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s--elideRight width s =-    if length s > width then take (width - 2) s ++ ".." else s--underline :: String -> String-underline s = s' ++ replicate (length s) '-' ++ "\n"-    where s'-            | last s == '\n' = s-            | otherwise = s ++ "\n"--unbracket :: String -> String-unbracket s-    | (head s == '[' && last s == ']') || (head s == '(' && last s == ')') = init $ tail s-    | otherwise = s---- | Join multi-line strings as side-by-side rectangular strings of the same height, top-padded.-concatTopPadded :: [String] -> String-concatTopPadded strs = intercalate "\n" $ map concat $ transpose padded-    where-      lss = map lines strs-      h = maximum $ map length lss-      ypad ls = replicate (difforzero h (length ls)) "" ++ ls-      xpad ls = map (padleft w) ls where w | null ls = 0-                                           | otherwise = maximum $ map length ls-      padded = map (xpad . ypad) lss---- | Join multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.-concatBottomPadded :: [String] -> String-concatBottomPadded strs = intercalate "\n" $ map concat $ transpose padded-    where-      lss = map lines strs-      h = maximum $ map length lss-      ypad ls = ls ++ replicate (difforzero h (length ls)) ""-      xpad ls = map (padleft w) ls where w | null ls = 0-                                           | otherwise = maximum $ map length ls-      padded = map (xpad . ypad) lss---- | Compose strings vertically and right-aligned.-vConcatRightAligned :: [String] -> String-vConcatRightAligned ss = intercalate "\n" $ map showfixedwidth ss-    where-      showfixedwidth = printf (printf "%%%ds" width)-      width = maximum $ map length ss---- | Convert a multi-line string to a rectangular string top-padded to the specified height.-padtop :: Int -> String -> String-padtop h s = intercalate "\n" xpadded-    where-      ls = lines s-      sh = length ls-      sw | null ls = 0-         | otherwise = maximum $ map length ls-      ypadded = replicate (difforzero h sh) "" ++ ls-      xpadded = map (padleft sw) ypadded---- | Convert a multi-line string to a rectangular string bottom-padded to the specified height.-padbottom :: Int -> String -> String-padbottom h s = intercalate "\n" xpadded-    where-      ls = lines s-      sh = length ls-      sw | null ls = 0-         | otherwise = maximum $ map length ls-      ypadded = ls ++ replicate (difforzero h sh) ""-      xpadded = map (padleft sw) ypadded---- | Convert a multi-line string to a rectangular string left-padded to the specified width.-padleft :: Int -> String -> String-padleft w "" = concat $ replicate w " "-padleft w s = intercalate "\n" $ map (printf (printf "%%%ds" w)) $ lines s---- | Convert a multi-line string to a rectangular string right-padded to the specified width.-padright :: Int -> String -> String-padright w "" = concat $ replicate w " "-padright w s = intercalate "\n" $ map (printf (printf "%%-%ds" w)) $ lines s---- | Clip a multi-line string to the specified width and height from the top left.-cliptopleft :: Int -> Int -> String -> String-cliptopleft w h = intercalate "\n" . take h . map (take w) . lines---- | Clip and pad a multi-line string to fill the specified width and height.-fitto :: Int -> Int -> String -> String-fitto w h s = intercalate "\n" $ take h $ rows ++ repeat blankline-    where-      rows = map (fit w) $ lines s-      fit w = take w . (++ repeat ' ')-      blankline = replicate w ' '---- math--difforzero :: (Num a, Ord a) => a -> a -> a-difforzero a b = maximum [(a - b), 0]---- regexps--containsRegex :: String -> String -> Bool-containsRegex r s = case matchRegexPR ("(?i)"++r) s of-                      Just _ -> True-                      _ -> False----- 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'---- trees--root = rootLabel-subs = subForest-branches = subForest---- | List just the leaf nodes of a tree-leaves :: Tree a -> [a]-leaves (Node v []) = [v]-leaves (Node _ branches) = concatMap leaves branches---- | get the sub-tree rooted at the first (left-most, depth-first) occurrence--- of the specified node value-subtreeat :: Eq a => a -> Tree a -> Maybe (Tree a)-subtreeat v t-    | root t == v = Just t-    | otherwise = subtreeinforest v $ subs t---- | get the sub-tree for the specified node value in the first tree in--- forest in which it occurs.-subtreeinforest :: Eq a => a -> [Tree a] -> Maybe (Tree a)-subtreeinforest _ [] = Nothing-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) []-treeprune d t = Node (root t) (map (treeprune $ d-1) $ branches t)---- | apply f to all tree nodes-treemap :: (a -> b) -> Tree a -> Tree b-treemap f t = Node (f $ root t) (map (treemap f) $ branches t)---- | remove all subtrees whose nodes do not fulfill predicate-treefilter :: (a -> Bool) -> Tree a -> Tree a-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. --- treedropall -- do this repeatedly.---- | show a compact ascii representation of a tree-showtree :: Show a => Tree a -> String-showtree = unlines . filter (containsRegex "[^ \\|]") . lines . drawTree . treemap show---- | show a compact ascii representation of a forest-showforest :: Show a => Forest a -> String-showforest = concatMap showtree---- debugging---- | trace (print on stdout at runtime) a showable expression--- (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 newline and a label prepended-ltrace :: Show a => String -> a -> a-ltrace l a = trace (l ++ ": " ++ show a) a---- | trace an expression using a custom show function-tracewith f e = trace (f e) e---- parsing--parsewith :: Parser a -> String -> Either ParseError a-parsewith p = parse p ""--parseWithCtx :: b -> GenParser Char b a -> String -> Either ParseError a-parseWithCtx ctx p = runParser p ctx ""--fromparse :: Either ParseError a -> a-fromparse = either parseerror id--parseerror e = error $ showParseError e--showParseError e = "parse error at " ++ show e--showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e)--nonspace :: GenParser Char st Char-nonspace = satisfy (not . isSpace)--spacenonewline :: GenParser Char st Char-spacenonewline = satisfy (`elem` " \v\f\t")--restofline :: GenParser Char st String-restofline = anyChar `manyTill` newline---- time--getCurrentLocalTime :: IO LocalTime-getCurrentLocalTime = do-  t <- getCurrentTime-  tz <- getCurrentTimeZone-  return $ utcToLocalTime tz t---- testing---- | Get a Test's label, or the empty string.-tname :: Test -> String-tname (TestLabel n _) = n-tname _ = ""---- | Flatten a Test containing TestLists into a list of single tests.-tflatten :: Test -> [Test]-tflatten (TestLabel _ t@(TestList _)) = tflatten t-tflatten (TestList ts) = concatMap tflatten ts-tflatten t = [t]---- | Filter TestLists in a Test, recursively, preserving the structure.-tfilter :: (Test -> Bool) -> Test -> Test-tfilter p (TestLabel l ts) = TestLabel l (tfilter p ts)-tfilter p (TestList ts) = TestList $ filter (any p . tflatten) $ map (tfilter p) ts-tfilter _ t = t---- | Simple way to assert something is some expected value, with no label.-is :: (Eq a, Show a) => a -> a -> Assertion-a `is` e = assertEqual "" e a---- | Assert a parse result is successful, printing the parse error on failure.-assertParse :: (Either ParseError a) -> Assertion-assertParse parse = either (assertFailure.show) (const (return ())) parse---- | Assert a parse result is some expected value, printing the parse error on failure.-assertParseEqual :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion-assertParseEqual parse expected = either (assertFailure.show) (`is` expected) parse--printParseError :: (Show a) => a -> IO ()-printParseError e = do putStr "parse error at "; print e----- misc--isLeft :: Either a b -> Bool-isLeft (Left _) = True-isLeft _        = False--isRight :: Either a b -> Bool-isRight = not . isLeft--strictReadFile :: FilePath -> IO String-strictReadFile f = readFile f >>= \s -> Control.Exception.evaluate (length s) >> return s
hledger-lib.cabal view
@@ -1,5 +1,5 @@ name:           hledger-lib-version:        0.9+version: 0.10 category:       Finance synopsis:       Core types and utilities for working with hledger (or c++ ledger) data. description:@@ -26,21 +26,21 @@  library   exposed-modules:-                  Ledger-                  Ledger.Account-                  Ledger.AccountName-                  Ledger.Amount-                  Ledger.Commodity-                  Ledger.Dates-                  Ledger.IO-                  Ledger.Transaction-                  Ledger.Journal-                  Ledger.Ledger-                  Ledger.Posting-                  Ledger.Parse-                  Ledger.TimeLog-                  Ledger.Types-                  Ledger.Utils+                  Hledger.Data+                  Hledger.Data.Account+                  Hledger.Data.AccountName+                  Hledger.Data.Amount+                  Hledger.Data.Commodity+                  Hledger.Data.Dates+                  Hledger.Data.IO+                  Hledger.Data.Transaction+                  Hledger.Data.Journal+                  Hledger.Data.Ledger+                  Hledger.Data.Posting+                  Hledger.Data.Parse+                  Hledger.Data.TimeLog+                  Hledger.Data.Types+                  Hledger.Data.Utils   Build-Depends:                   base >= 3 && < 5                  ,containers@@ -51,6 +51,7 @@                  ,old-time                  ,parsec                  ,regexpr >= 0.5.1+                 ,safe >= 0.2                  ,time                  ,utf8-string >= 0.3                  ,HUnit