packages feed

hledger-lib 0.27.1 → 1.0

raw patch · 57 files changed

+8596/−2910 lines, 57 filesdep +Globdep +data-defaultdep +doctestdep ~parsecPVP ok

version bump matches the API change (PVP)

Dependencies added: Glob, data-default, doctest, ghc-prim, megaparsec, semigroups, text

Dependency ranges changed: parsec

API changes (from Hackage documentation)

- Hledger.Data.Dates: tests_Hledger_Data_Dates :: Test
- Hledger.Data.Journal: addTimeLogEntry :: TimeLogEntry -> Journal -> Journal
- Hledger.Data.Journal: canonicalStyles :: [Amount] -> Map Commodity AmountStyle
- Hledger.Data.Journal: journalCanonicaliseAmounts :: Journal -> Journal
- Hledger.Data.Journal: nullctx :: JournalContext
- Hledger.Data.TimeLog: entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction
- Hledger.Data.TimeLog: instance GHC.Read.Read Hledger.Data.Types.TimeLogCode
- Hledger.Data.TimeLog: instance GHC.Show.Show Hledger.Data.Types.TimeLogCode
- Hledger.Data.TimeLog: instance GHC.Show.Show Hledger.Data.Types.TimeLogEntry
- Hledger.Data.TimeLog: tests_Hledger_Data_TimeLog :: Test
- Hledger.Data.TimeLog: timeLogEntriesToTransactions :: LocalTime -> [TimeLogEntry] -> [Transaction]
- Hledger.Data.Types: Ctx :: !(Maybe Year) -> !(Maybe (Commodity, AmountStyle)) -> ![AccountName] -> ![AccountAlias] -> !Integer -> JournalContext
- Hledger.Data.Types: TimeLogEntry :: GenericSourcePos -> TimeLogCode -> LocalTime -> String -> String -> TimeLogEntry
- Hledger.Data.Types: [ctxAccount] :: JournalContext -> ![AccountName]
- Hledger.Data.Types: [ctxAliases] :: JournalContext -> ![AccountAlias]
- Hledger.Data.Types: [ctxDefaultCommodityAndStyle] :: JournalContext -> !(Maybe (Commodity, AmountStyle))
- Hledger.Data.Types: [ctxTransactionIndex] :: JournalContext -> !Integer
- Hledger.Data.Types: [ctxYear] :: JournalContext -> !(Maybe Year)
- Hledger.Data.Types: [filereadtime] :: Journal -> ClockTime
- Hledger.Data.Types: [files] :: Journal -> [(FilePath, String)]
- Hledger.Data.Types: [final_comment_lines] :: Journal -> String
- Hledger.Data.Types: [jContext] :: Journal -> JournalContext
- Hledger.Data.Types: [jcommoditystyles] :: Journal -> Map Commodity AmountStyle
- Hledger.Data.Types: [open_timelog_entries] :: Journal -> [TimeLogEntry]
- Hledger.Data.Types: data JournalContext
- Hledger.Data.Types: data TimeLogCode
- Hledger.Data.Types: data TimeLogEntry
- Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.JournalContext
- Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.TimeLogCode
- Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.TimeLogEntry
- Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.JournalContext
- Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.TimeLogCode
- Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.TimeLogEntry
- Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.JournalContext
- Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.TimeLogCode
- Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.TimeLogEntry
- Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.TimeLogCode
- Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0AccountAlias
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Amount
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0ClearedStatus
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0ClockTime
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0DateSpan
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0DigitGroupStyle
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0GenericSourcePos
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Journal
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0JournalContext
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0MarketPrice
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0MixedAmount
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0ModifierTransaction
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0PeriodicTransaction
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Posting
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0PostingType
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Price
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Side
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_0Transaction
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1AccountAlias
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1ClearedStatus
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1PostingType
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1Price
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1Side
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_1TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_2ClearedStatus
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_2Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_2PostingType
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_2Price
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_2TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_3Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_3TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_4Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_4TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_5Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_6Interval
- Hledger.Data.Types: instance GHC.Generics.Constructor Hledger.Data.Types.C1_7Interval
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1AccountAlias
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Amount
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1ClearedStatus
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1ClockTime
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1DateSpan
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1DigitGroupStyle
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1GenericSourcePos
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Interval
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Journal
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1JournalContext
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1MarketPrice
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1MixedAmount
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1ModifierTransaction
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1PeriodicTransaction
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Posting
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1PostingType
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Price
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Side
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Datatype Hledger.Data.Types.D1Transaction
- Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.JournalContext
- Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.TimeLogCode
- Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0Amount
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0JournalContext
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0MarketPrice
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0ModifierTransaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0PeriodicTransaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_0Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_10Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1Amount
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1JournalContext
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1MarketPrice
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1ModifierTransaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1PeriodicTransaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_1Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2Amount
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2JournalContext
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2MarketPrice
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_2Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3Amount
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3JournalContext
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_3Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_4AmountStyle
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_4Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_4JournalContext
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_4Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_4TimeLogEntry
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_4Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_5Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_5Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_5Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_6Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_6Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_6Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_7Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_7Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_7Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_8Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_8Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_8Transaction
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_9Journal
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_9Posting
- Hledger.Data.Types: instance GHC.Generics.Selector Hledger.Data.Types.S1_0_9Transaction
- Hledger.Data.Types: instance GHC.Read.Read Hledger.Data.Types.JournalContext
- Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.JournalContext
- Hledger.Data.Types: numberRepresentation :: String
- Hledger.Data.Types: type Commodity = String
- Hledger.Data.Types: type JournalUpdate = ExceptT String IO (Journal -> Journal)
- Hledger.Read: accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName
- Hledger.Read: amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
- Hledger.Read: amountp' :: String -> Amount
- Hledger.Read: codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
- Hledger.Read: mamountp' :: String -> MixedAmount
- Hledger.Read: numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
- Hledger.Read.JournalReader: accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName
- Hledger.Read.JournalReader: amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
- Hledger.Read.JournalReader: amountp' :: String -> Amount
- Hledger.Read.JournalReader: codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
- Hledger.Read.JournalReader: mamountp' :: String -> MixedAmount
- Hledger.Read.JournalReader: numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
- Hledger.Read.TimelogReader: reader :: Reader
- Hledger.Read.TimelogReader: tests_Hledger_Read_TimelogReader :: Test
- Hledger.Reports.BalanceReport: type RenderableAccountName = (AccountName, AccountName, Int)
- Hledger.Reports.ReportOptions: CumulativeBalance :: BalanceType
- Hledger.Reports.ReportOptions: PeriodBalance :: BalanceType
- Hledger.Reports.ReportOptions: [begin_] :: ReportOpts -> Maybe Day
- Hledger.Reports.ReportOptions: [cleared_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [daily_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [end_] :: ReportOpts -> Maybe Day
- Hledger.Reports.ReportOptions: [monthly_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [pending_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [quarterly_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [uncleared_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [weekly_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: [yearly_] :: ReportOpts -> Bool
- Hledger.Reports.ReportOptions: clearedValueFromOpts :: ReportOpts -> Maybe ClearedStatus
- Hledger.Reports.ReportOptions: dateSpanFromOpts :: Day -> ReportOpts -> DateSpan
- Hledger.Reports.ReportOptions: instance System.Console.CmdArgs.Default.Default Hledger.Reports.ReportOptions.AccountListMode
- Hledger.Reports.ReportOptions: instance System.Console.CmdArgs.Default.Default Hledger.Reports.ReportOptions.BalanceType
- Hledger.Reports.ReportOptions: instance System.Console.CmdArgs.Default.Default Hledger.Reports.ReportOptions.ReportOpts
- Hledger.Reports.ReportOptions: intervalFromOpts :: ReportOpts -> Interval
- Hledger.Utils.Debug: dbgshow :: Show a => Int -> String -> a -> a
- Hledger.Utils.Debug: dbgtrace :: String -> a -> a
- Hledger.Utils.Debug: ltrace :: Show a => String -> a -> a
- Hledger.Utils.Debug: mtrace :: (Monad m, Show a) => a -> m a
- Hledger.Utils.Debug: strace :: Show a => a -> a
- Hledger.Utils.Parse: parseWithCtx :: Stream s m t => u -> ParsecT s u m a -> s -> m (Either ParseError a)
- Hledger.Utils.String: escapeDoubleQuotes :: String -> String
- Hledger.Utils.String: escapeSingleQuotes :: String -> String
- Hledger.Utils.String: isDoubleQuoted :: [Char] -> Bool
- Hledger.Utils.String: isSingleQuoted :: [Char] -> Bool
- Hledger.Utils.String: quoteIfSpaced :: String -> String
- Hledger.Utils.String: stripquotes :: String -> String
+ Hledger.Data.AccountName: acctsep :: Text
+ Hledger.Data.Journal: canonicalStyleFrom :: [AmountStyle] -> AmountStyle
+ Hledger.Data.Journal: commodityStylesFromAmounts :: [Amount] -> Map CommoditySymbol AmountStyle
+ Hledger.Data.Journal: filterTransactionPostings :: Query -> Transaction -> Transaction
+ Hledger.Data.Journal: instance GHC.Base.Monoid Hledger.Data.Types.Journal
+ Hledger.Data.Journal: journalApplyCommodityStyles :: Journal -> Journal
+ Hledger.Data.Journal: journalCheckBalanceAssertions :: Journal -> Either String Journal
+ Hledger.Data.Journal: journalNumberAndTieTransactions :: Journal -> Journal
+ Hledger.Data.Journal: journalUntieTransactions :: Transaction -> Transaction
+ Hledger.Data.Period: dateSpanAsPeriod :: DateSpan -> Period
+ Hledger.Data.Period: firstMonthOfQuarter :: Num a => a -> a
+ Hledger.Data.Period: isLastDayOfMonth :: (Num a, Num a1, Eq a, Eq a1) => Integer -> a1 -> a -> Bool
+ Hledger.Data.Period: mondayBefore :: Day -> Day
+ Hledger.Data.Period: periodAsDateSpan :: Period -> DateSpan
+ Hledger.Data.Period: periodEnd :: Period -> Maybe Day
+ Hledger.Data.Period: periodGrow :: Period -> Period
+ Hledger.Data.Period: periodNext :: Period -> Period
+ Hledger.Data.Period: periodNextIn :: DateSpan -> Period -> Period
+ Hledger.Data.Period: periodPrevious :: Period -> Period
+ Hledger.Data.Period: periodPreviousIn :: DateSpan -> Period -> Period
+ Hledger.Data.Period: periodShrink :: Day -> Period -> Period
+ Hledger.Data.Period: periodStart :: Period -> Maybe Day
+ Hledger.Data.Period: quarterContainingMonth :: Integral a => a -> a
+ Hledger.Data.Period: showPeriod :: Period -> String
+ Hledger.Data.Period: simplifyPeriod :: Period -> Period
+ Hledger.Data.Period: startOfFirstWeekInMonth :: Integer -> Int -> Day
+ Hledger.Data.Period: yearMonthContainingWeekStarting :: Day -> (Integer, Int)
+ Hledger.Data.Timeclock: entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction
+ Hledger.Data.Timeclock: instance GHC.Read.Read Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Timeclock: instance GHC.Show.Show Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Timeclock: instance GHC.Show.Show Hledger.Data.Types.TimeclockEntry
+ Hledger.Data.Timeclock: tests_Hledger_Data_Timeclock :: Test
+ Hledger.Data.Timeclock: timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]
+ Hledger.Data.Transaction: showPostingLine :: Posting -> String
+ Hledger.Data.Transaction: txnUntieKnot :: Transaction -> Transaction
+ Hledger.Data.Types: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Commodity
+ Hledger.Data.Types: DayPeriod :: Day -> Period
+ Hledger.Data.Types: MonthPeriod :: Year -> Month -> Period
+ Hledger.Data.Types: PeriodAll :: Period
+ Hledger.Data.Types: PeriodBetween :: Day -> Day -> Period
+ Hledger.Data.Types: PeriodFrom :: Day -> Period
+ Hledger.Data.Types: PeriodTo :: Day -> Period
+ Hledger.Data.Types: QuarterPeriod :: Year -> Quarter -> Period
+ Hledger.Data.Types: TimeclockEntry :: GenericSourcePos -> TimeclockCode -> LocalTime -> AccountName -> Text -> TimeclockEntry
+ Hledger.Data.Types: WeekPeriod :: Day -> Period
+ Hledger.Data.Types: YearPeriod :: Year -> Period
+ Hledger.Data.Types: [cformat] :: Commodity -> Maybe AmountStyle
+ Hledger.Data.Types: [csymbol] :: Commodity -> CommoditySymbol
+ Hledger.Data.Types: [jaccounts] :: Journal -> [AccountName]
+ Hledger.Data.Types: [jcommodities] :: Journal -> Map CommoditySymbol Commodity
+ Hledger.Data.Types: [jfiles] :: Journal -> [(FilePath, Text)]
+ Hledger.Data.Types: [jfinalcommentlines] :: Journal -> Text
+ Hledger.Data.Types: [jinferredcommodities] :: Journal -> Map CommoditySymbol AmountStyle
+ Hledger.Data.Types: [jlastreadtime] :: Journal -> ClockTime
+ Hledger.Data.Types: [jparsealiases] :: Journal -> [AccountAlias]
+ Hledger.Data.Types: [jparsedefaultcommodity] :: Journal -> Maybe (CommoditySymbol, AmountStyle)
+ Hledger.Data.Types: [jparsedefaultyear] :: Journal -> Maybe Year
+ Hledger.Data.Types: [jparseparentaccounts] :: Journal -> [AccountName]
+ Hledger.Data.Types: [jparsetimeclockentries] :: Journal -> [TimeclockEntry]
+ Hledger.Data.Types: data Commodity
+ Hledger.Data.Types: data Period
+ Hledger.Data.Types: data TimeclockCode
+ Hledger.Data.Types: data TimeclockEntry
+ Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.Commodity
+ Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.TimeclockEntry
+ Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.Account
+ Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.Commodity
+ Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.Period
+ Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.TimeclockEntry
+ Hledger.Data.Types: instance Data.Default.Class.Default Hledger.Data.Types.Interval
+ Hledger.Data.Types: instance Data.Default.Class.Default Hledger.Data.Types.Period
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.Commodity
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.Period
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.TimeclockEntry
+ Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.Period
+ Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Types: instance GHC.Classes.Ord Hledger.Data.Types.TimeclockEntry
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.Account
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.Commodity
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.Period
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.TimeclockCode
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.TimeclockEntry
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.Commodity
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.Period
+ Hledger.Data.Types: type CommoditySymbol = Text
+ Hledger.Data.Types: type Month = Int
+ Hledger.Data.Types: type MonthDay = Int
+ Hledger.Data.Types: type MonthWeek = Int
+ Hledger.Data.Types: type ParsedJournal = Journal
+ Hledger.Data.Types: type Quarter = Int
+ Hledger.Data.Types: type TagName = Text
+ Hledger.Data.Types: type TagValue = Text
+ Hledger.Data.Types: type WeekDay = Int
+ Hledger.Data.Types: type YearDay = Int
+ Hledger.Data.Types: type YearWeek = Int
+ Hledger.Query: queryIsEmpty :: Query -> Bool
+ Hledger.Query: queryIsReal :: Query -> Bool
+ Hledger.Query: queryIsStatus :: Query -> Bool
+ Hledger.Read: readFormatNames :: [StorageFormat]
+ Hledger.Read.Common: accountnamep :: TextParser m AccountName
+ Hledger.Read.Common: addAccountAlias :: MonadState Journal m => AccountAlias -> m ()
+ Hledger.Read.Common: amountp :: Monad m => JournalStateParser m Amount
+ Hledger.Read.Common: amountp' :: String -> Amount
+ Hledger.Read.Common: bracketeddatetagsp :: Maybe Day -> ErroringJournalParser [(TagName, Day)]
+ Hledger.Read.Common: clearAccountAliases :: MonadState Journal m => m ()
+ Hledger.Read.Common: codep :: TextParser m String
+ Hledger.Read.Common: commentStartingWithp :: [Char] -> ErroringJournalParser Text
+ Hledger.Read.Common: commentTags :: Text -> [Tag]
+ Hledger.Read.Common: commentchars :: [Char]
+ Hledger.Read.Common: commentp :: ErroringJournalParser Text
+ Hledger.Read.Common: commoditysymbolp :: TextParser m CommoditySymbol
+ Hledger.Read.Common: datep :: JournalStateParser m Day
+ Hledger.Read.Common: datetagp :: Maybe Day -> ErroringJournalParser (TagName, Day)
+ Hledger.Read.Common: datetimep :: ErroringJournalParser LocalTime
+ Hledger.Read.Common: descriptionp :: ErroringJournalParser String
+ Hledger.Read.Common: emptyorcommentlinep :: ErroringJournalParser ()
+ Hledger.Read.Common: fixedlotpricep :: ErroringJournalParser (Maybe Amount)
+ Hledger.Read.Common: followingcommentandtagsp :: Maybe Day -> ErroringJournalParser (Text, [Tag], Maybe Day, Maybe Day)
+ Hledger.Read.Common: followingcommentp :: ErroringJournalParser Text
+ Hledger.Read.Common: genericSourcePos :: SourcePos -> GenericSourcePos
+ Hledger.Read.Common: getAccountAliases :: MonadState Journal m => m [AccountAlias]
+ Hledger.Read.Common: getDefaultCommodityAndStyle :: JournalStateParser m (Maybe (CommoditySymbol, AmountStyle))
+ Hledger.Read.Common: getParentAccount :: ErroringJournalParser AccountName
+ Hledger.Read.Common: getYear :: JournalStateParser m (Maybe Year)
+ Hledger.Read.Common: journalAddFile :: (FilePath, Text) -> Journal -> Journal
+ Hledger.Read.Common: leftsymbolamountp :: Monad m => JournalStateParser m Amount
+ Hledger.Read.Common: mamountp' :: String -> MixedAmount
+ Hledger.Read.Common: modifiedaccountnamep :: ErroringJournalParser AccountName
+ Hledger.Read.Common: multilinecommentp :: ErroringJournalParser ()
+ Hledger.Read.Common: nontagp :: Parser String
+ Hledger.Read.Common: nosymbolamountp :: Monad m => JournalStateParser m Amount
+ Hledger.Read.Common: numberp :: TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
+ Hledger.Read.Common: parseAndFinaliseJournal :: ErroringJournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+ Hledger.Read.Common: parseAndFinaliseJournal' :: JournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+ Hledger.Read.Common: parserErrorAt :: SourcePos -> String -> ErroringJournalParser a
+ Hledger.Read.Common: partialbalanceassertionp :: ErroringJournalParser (Maybe MixedAmount)
+ Hledger.Read.Common: popParentAccount :: ErroringJournalParser ()
+ Hledger.Read.Common: postingdatesp :: Maybe Day -> ErroringJournalParser [(TagName, Day)]
+ Hledger.Read.Common: priceamountp :: Monad m => JournalStateParser m Price
+ Hledger.Read.Common: pushAccount :: AccountName -> ErroringJournalParser ()
+ Hledger.Read.Common: pushParentAccount :: AccountName -> ErroringJournalParser ()
+ Hledger.Read.Common: quotedcommoditysymbolp :: TextParser m CommoditySymbol
+ Hledger.Read.Common: rejp :: ErroringJournalParser a -> Text -> IO (Either String a)
+ Hledger.Read.Common: rightsymbolamountp :: Monad m => JournalStateParser m Amount
+ Hledger.Read.Common: rjp :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char Dec) a)
+ Hledger.Read.Common: rtp :: TextParser Identity a -> Text -> Either (ParseError Char Dec) a
+ Hledger.Read.Common: runErroringJournalParser :: ErroringJournalParser a -> Text -> IO (Either String a)
+ Hledger.Read.Common: runJournalParser :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char Dec) a)
+ Hledger.Read.Common: runTextParser :: TextParser Identity a -> Text -> Either (ParseError Char Dec) a
+ Hledger.Read.Common: secondarydatep :: Day -> ErroringJournalParser Day
+ Hledger.Read.Common: semicoloncommentp :: ErroringJournalParser Text
+ Hledger.Read.Common: setDefaultCommodityAndStyle :: (CommoditySymbol, AmountStyle) -> ErroringJournalParser ()
+ Hledger.Read.Common: setYear :: Year -> JournalStateParser m ()
+ Hledger.Read.Common: signp :: TextParser m String
+ Hledger.Read.Common: simplecommoditysymbolp :: TextParser m CommoditySymbol
+ Hledger.Read.Common: spaceandamountormissingp :: ErroringJournalParser MixedAmount
+ Hledger.Read.Common: statusp :: TextParser m ClearedStatus
+ Hledger.Read.Common: tagnamep :: Parser Text
+ Hledger.Read.Common: tagp :: Parser Tag
+ Hledger.Read.Common: tagsp :: Parser [Tag]
+ Hledger.Read.Common: tagvaluep :: TextParser m Text
+ Hledger.Read.CsvReader: instance Text.Megaparsec.Error.ShowErrorComponent GHC.Base.String
+ Hledger.Read.JournalReader: datep :: JournalStateParser m Day
+ Hledger.Read.JournalReader: rejp :: ErroringJournalParser a -> Text -> IO (Either String a)
+ Hledger.Read.JournalReader: rjp :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char Dec) a)
+ Hledger.Read.JournalReader: runErroringJournalParser :: ErroringJournalParser a -> Text -> IO (Either String a)
+ Hledger.Read.JournalReader: runJournalParser :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char Dec) a)
+ Hledger.Read.TimeclockReader: reader :: Reader
+ Hledger.Read.TimeclockReader: tests_Hledger_Read_TimeclockReader :: Test
+ Hledger.Read.TimeclockReader: timeclockfilep :: ErroringJournalParser ParsedJournal
+ Hledger.Read.TimedotReader: reader :: Reader
+ Hledger.Read.TimedotReader: tests_Hledger_Read_TimedotReader :: Test
+ Hledger.Read.TimedotReader: timedotfilep :: ErroringJournalParser ParsedJournal
+ Hledger.Reports.MultiBalanceReports: singleBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
+ Hledger.Reports.ReportOptions: CumulativeChange :: BalanceType
+ Hledger.Reports.ReportOptions: PeriodChange :: BalanceType
+ Hledger.Reports.ReportOptions: [clearedstatus_] :: ReportOpts -> Maybe ClearedStatus
+ Hledger.Reports.ReportOptions: [interval_] :: ReportOpts -> Interval
+ Hledger.Reports.ReportOptions: instance Data.Default.Class.Default GHC.Types.Bool
+ Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.AccountListMode
+ Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.BalanceType
+ Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.ReportOpts
+ Hledger.Reports.TransactionsReports: transactionRegisterDate :: Query -> Query -> Transaction -> Day
+ Hledger.Utils: getCurrentZonedTime :: IO ZonedTime
+ Hledger.Utils: readFileAnyLineEnding :: FilePath -> IO Text
+ Hledger.Utils: readFileOrStdinAnyLineEnding :: String -> IO Text
+ Hledger.Utils.Debug: dbglog :: Show a => String -> a -> a
+ Hledger.Utils.Debug: pprint :: Show a => a -> IO ()
+ Hledger.Utils.Parse: choiceInState :: [StateT s (ParsecT Dec Text m) a] -> StateT s (ParsecT Dec Text m) a
+ Hledger.Utils.Parse: parseWithState :: Monad m => st -> StateT st (ParsecT Dec Text m) a -> Text -> m (Either (ParseError Char Dec) a)
+ Hledger.Utils.Parse: parseWithState' :: (Stream s, ErrorComponent e) => st -> StateT st (ParsecT e s Identity) a -> s -> (Either (ParseError (Token s) e) a)
+ Hledger.Utils.Parse: parsewithString :: Parsec e String a -> String -> Either (ParseError Char e) a
+ Hledger.Utils.Parse: type ErroringJournalParser a = StateT Journal (ParsecT Dec Text (ExceptT String IO)) a
+ Hledger.Utils.Parse: type JournalParser a = StateT Journal (ParsecT Dec Text Identity) a
+ Hledger.Utils.Parse: type JournalStateParser m a = StateT Journal (ParsecT Dec Text m) a
+ Hledger.Utils.Parse: type TextParser m a = ParsecT Dec Text m a
+ Hledger.Utils.String: charWidth :: Char -> Int
+ Hledger.Utils.Text: difforzero :: (Num a, Ord a) => a -> a -> a
+ Hledger.Utils.Text: escapeDoubleQuotes :: Text -> Text
+ Hledger.Utils.Text: escapeSingleQuotes :: Text -> Text
+ Hledger.Utils.Text: fitText :: Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text
+ Hledger.Utils.Text: isDoubleQuoted :: Text -> Bool
+ Hledger.Utils.Text: isSingleQuoted :: Text -> Bool
+ Hledger.Utils.Text: quoteIfSpaced :: Text -> Text
+ Hledger.Utils.Text: quotechars :: [Char]
+ Hledger.Utils.Text: stripquotes :: Text -> Text
+ Hledger.Utils.Text: textConcatTopPadded :: [Text] -> Text
+ Hledger.Utils.Text: textElideRight :: Int -> Text -> Text
+ Hledger.Utils.Text: textPadLeftWide :: Int -> Text -> Text
+ Hledger.Utils.Text: textPadRightWide :: Int -> Text -> Text
+ Hledger.Utils.Text: textTakeWidth :: Int -> Text -> Text
+ Hledger.Utils.Text: textUnbracket :: Text -> Text
+ Hledger.Utils.Text: textWidth :: Text -> Int
+ Hledger.Utils.Text: textlstrip :: Text -> Text
+ Hledger.Utils.Text: textrstrip :: Text -> Text
+ Hledger.Utils.Text: textstrip :: Text -> Text
+ Hledger.Utils.Text: whitespacechars :: [Char]
- Hledger.Data.AccountName: accountLeafName :: AccountName -> String
+ Hledger.Data.AccountName: accountLeafName :: AccountName -> Text
- Hledger.Data.AccountName: accountNameComponents :: AccountName -> [String]
+ Hledger.Data.AccountName: accountNameComponents :: AccountName -> [Text]
- Hledger.Data.AccountName: accountNameFromComponents :: [String] -> AccountName
+ Hledger.Data.AccountName: accountNameFromComponents :: [Text] -> AccountName
- Hledger.Data.AccountName: accountNameToAccountOnlyRegex :: String -> String
+ Hledger.Data.AccountName: accountNameToAccountOnlyRegex :: AccountName -> Regexp
- Hledger.Data.AccountName: accountNameToAccountRegex :: String -> String
+ Hledger.Data.AccountName: accountNameToAccountRegex :: AccountName -> Regexp
- Hledger.Data.AccountName: accountRegexToAccountName :: String -> String
+ Hledger.Data.AccountName: accountRegexToAccountName :: Regexp -> AccountName
- Hledger.Data.AccountName: accountSummarisedName :: AccountName -> String
+ Hledger.Data.AccountName: accountSummarisedName :: AccountName -> Text
- Hledger.Data.AccountName: nullaccountnametree :: Tree [Char]
+ Hledger.Data.AccountName: nullaccountnametree :: IsString a => Tree a
- Hledger.Data.Amount: amountWithCommodity :: Commodity -> Amount -> Amount
+ Hledger.Data.Amount: amountWithCommodity :: CommoditySymbol -> Amount -> Amount
- Hledger.Data.Amount: canonicaliseAmount :: Map Commodity AmountStyle -> Amount -> Amount
+ Hledger.Data.Amount: canonicaliseAmount :: Map CommoditySymbol AmountStyle -> Amount -> Amount
- Hledger.Data.Amount: canonicaliseMixedAmount :: Map Commodity AmountStyle -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: canonicaliseMixedAmount :: Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
- Hledger.Data.Amount: filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount
+ Hledger.Data.Amount: filterMixedAmountByCommodity :: CommoditySymbol -> MixedAmount -> MixedAmount
- Hledger.Data.Commodity: comm :: String -> Commodity
+ Hledger.Data.Commodity: comm :: String -> CommoditySymbol
- Hledger.Data.Commodity: commoditysymbols :: [([Char], [Char])]
+ Hledger.Data.Commodity: commoditysymbols :: [(String, CommoditySymbol)]
- Hledger.Data.Commodity: conversionRate :: Commodity -> Commodity -> Double
+ Hledger.Data.Commodity: conversionRate :: CommoditySymbol -> CommoditySymbol -> Double
- Hledger.Data.Commodity: nonsimplecommoditychars :: String
+ Hledger.Data.Commodity: nonsimplecommoditychars :: [Char]
- Hledger.Data.Commodity: quoteCommoditySymbolIfNeeded :: [Char] -> [Char]
+ Hledger.Data.Commodity: quoteCommoditySymbolIfNeeded :: Text -> Text
- Hledger.Data.Dates: datesepchar :: Stream [Char] m Char => ParsecT [Char] st m Char
+ Hledger.Data.Dates: datesepchar :: TextParser m Char
- Hledger.Data.Dates: fixSmartDateStr :: Day -> String -> String
+ Hledger.Data.Dates: fixSmartDateStr :: Day -> Text -> String
- Hledger.Data.Dates: fixSmartDateStrEither :: Day -> String -> Either ParseError String
+ Hledger.Data.Dates: fixSmartDateStrEither :: Day -> Text -> Either (ParseError Char Dec) String
- Hledger.Data.Dates: fixSmartDateStrEither' :: Day -> String -> Either ParseError Day
+ Hledger.Data.Dates: fixSmartDateStrEither' :: Day -> Text -> Either (ParseError Char Dec) Day
- Hledger.Data.Dates: maybePeriod :: Day -> String -> Maybe (Interval, DateSpan)
+ Hledger.Data.Dates: maybePeriod :: Day -> Text -> Maybe (Interval, DateSpan)
- Hledger.Data.Dates: parsePeriodExpr :: Day -> String -> Either ParseError (Interval, DateSpan)
+ Hledger.Data.Dates: parsePeriodExpr :: Day -> Text -> Either (ParseError Char Dec) (Interval, DateSpan)
- Hledger.Data.Dates: smartdate :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+ Hledger.Data.Dates: smartdate :: Parser SmartDate
- Hledger.Data.Journal: journalDescriptions :: Journal -> [String]
+ Hledger.Data.Journal: journalDescriptions :: Journal -> [Text]
- Hledger.Data.Journal: journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Bool -> Journal -> Either String Journal
+ Hledger.Data.Journal: journalFinalise :: ClockTime -> FilePath -> Text -> Bool -> ParsedJournal -> Either String Journal
- Hledger.Data.Ledger: ledgerCommodities :: Ledger -> [Commodity]
+ Hledger.Data.Ledger: ledgerCommodities :: Ledger -> [CommoditySymbol]
- Hledger.Data.Posting: showComment :: String -> String
+ Hledger.Data.Posting: showComment :: Text -> String
- Hledger.Data.Transaction: balanceTransaction :: Maybe (Map Commodity AmountStyle) -> Transaction -> Either String Transaction
+ Hledger.Data.Transaction: balanceTransaction :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Either String Transaction
- Hledger.Data.Transaction: isTransactionBalanced :: Maybe (Map Commodity AmountStyle) -> Transaction -> Bool
+ Hledger.Data.Transaction: isTransactionBalanced :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Bool
- Hledger.Data.Types: Amount :: Commodity -> Quantity -> Price -> AmountStyle -> Amount
+ Hledger.Data.Types: Amount :: CommoditySymbol -> Quantity -> Price -> AmountStyle -> Amount
- Hledger.Data.Types: FinalOut :: TimeLogCode
+ Hledger.Data.Types: FinalOut :: TimeclockCode
- Hledger.Data.Types: In :: TimeLogCode
+ Hledger.Data.Types: In :: TimeclockCode
- Hledger.Data.Types: Journal :: [ModifierTransaction] -> [PeriodicTransaction] -> [Transaction] -> [TimeLogEntry] -> [MarketPrice] -> String -> JournalContext -> [(FilePath, String)] -> ClockTime -> Map Commodity AmountStyle -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [AccountName] -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [MarketPrice] -> [ModifierTransaction] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
- Hledger.Data.Types: MarketPrice :: Day -> Commodity -> Amount -> MarketPrice
+ Hledger.Data.Types: MarketPrice :: Day -> CommoditySymbol -> Amount -> MarketPrice
- Hledger.Data.Types: ModifierTransaction :: String -> [Posting] -> ModifierTransaction
+ Hledger.Data.Types: ModifierTransaction :: Text -> [Posting] -> ModifierTransaction
- Hledger.Data.Types: Out :: TimeLogCode
+ Hledger.Data.Types: Out :: TimeclockCode
- Hledger.Data.Types: PeriodicTransaction :: String -> [Posting] -> PeriodicTransaction
+ Hledger.Data.Types: PeriodicTransaction :: Text -> [Posting] -> PeriodicTransaction
- Hledger.Data.Types: Posting :: Maybe Day -> Maybe Day -> ClearedStatus -> AccountName -> MixedAmount -> String -> PostingType -> [Tag] -> Maybe MixedAmount -> Maybe Transaction -> Posting
+ Hledger.Data.Types: Posting :: Maybe Day -> Maybe Day -> ClearedStatus -> AccountName -> MixedAmount -> Text -> PostingType -> [Tag] -> Maybe MixedAmount -> Maybe Transaction -> Posting
- Hledger.Data.Types: Reader :: StorageFormat -> (FilePath -> String -> Bool) -> (Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal) -> Reader
+ Hledger.Data.Types: Reader :: StorageFormat -> (FilePath -> Text -> Bool) -> (Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal) -> Reader
- Hledger.Data.Types: SetBalance :: TimeLogCode
+ Hledger.Data.Types: SetBalance :: TimeclockCode
- Hledger.Data.Types: SetRequiredHours :: TimeLogCode
+ Hledger.Data.Types: SetRequiredHours :: TimeclockCode
- Hledger.Data.Types: Transaction :: Integer -> GenericSourcePos -> Day -> Maybe Day -> ClearedStatus -> String -> String -> String -> [Tag] -> [Posting] -> String -> Transaction
+ Hledger.Data.Types: Transaction :: Integer -> GenericSourcePos -> Day -> Maybe Day -> ClearedStatus -> Text -> Text -> Text -> [Tag] -> [Posting] -> Text -> Transaction
- Hledger.Data.Types: [acommodity] :: Amount -> Commodity
+ Hledger.Data.Types: [acommodity] :: Amount -> CommoditySymbol
- Hledger.Data.Types: [mpcommodity] :: MarketPrice -> Commodity
+ Hledger.Data.Types: [mpcommodity] :: MarketPrice -> CommoditySymbol
- Hledger.Data.Types: [mtvalueexpr] :: ModifierTransaction -> String
+ Hledger.Data.Types: [mtvalueexpr] :: ModifierTransaction -> Text
- Hledger.Data.Types: [pcomment] :: Posting -> String
+ Hledger.Data.Types: [pcomment] :: Posting -> Text
- Hledger.Data.Types: [ptperiodicexpr] :: PeriodicTransaction -> String
+ Hledger.Data.Types: [ptperiodicexpr] :: PeriodicTransaction -> Text
- Hledger.Data.Types: [rDetector] :: Reader -> FilePath -> String -> Bool
+ Hledger.Data.Types: [rDetector] :: Reader -> FilePath -> Text -> Bool
- Hledger.Data.Types: [rParser] :: Reader -> Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal
+ Hledger.Data.Types: [rParser] :: Reader -> Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal
- Hledger.Data.Types: [tcode] :: Transaction -> String
+ Hledger.Data.Types: [tcode] :: Transaction -> Text
- Hledger.Data.Types: [tcomment] :: Transaction -> String
+ Hledger.Data.Types: [tcomment] :: Transaction -> Text
- Hledger.Data.Types: [tdescription] :: Transaction -> String
+ Hledger.Data.Types: [tdescription] :: Transaction -> Text
- Hledger.Data.Types: [tlaccount] :: TimeLogEntry -> String
+ Hledger.Data.Types: [tlaccount] :: TimeclockEntry -> AccountName
- Hledger.Data.Types: [tlcode] :: TimeLogEntry -> TimeLogCode
+ Hledger.Data.Types: [tlcode] :: TimeclockEntry -> TimeclockCode
- Hledger.Data.Types: [tldatetime] :: TimeLogEntry -> LocalTime
+ Hledger.Data.Types: [tldatetime] :: TimeclockEntry -> LocalTime
- Hledger.Data.Types: [tldescription] :: TimeLogEntry -> String
+ Hledger.Data.Types: [tldescription] :: TimeclockEntry -> Text
- Hledger.Data.Types: [tlsourcepos] :: TimeLogEntry -> GenericSourcePos
+ Hledger.Data.Types: [tlsourcepos] :: TimeclockEntry -> GenericSourcePos
- Hledger.Data.Types: [tpreceding_comment_lines] :: Transaction -> String
+ Hledger.Data.Types: [tpreceding_comment_lines] :: Transaction -> Text
- Hledger.Data.Types: type AccountName = String
+ Hledger.Data.Types: type AccountName = Text
- Hledger.Data.Types: type Tag = (String, String) A tag name and (possibly empty) value.
+ Hledger.Data.Types: type Tag = (TagName, TagValue) A tag name and (possibly empty) value.
- Hledger.Query: parseQuery :: Day -> String -> (Query, [QueryOpt])
+ Hledger.Query: parseQuery :: Day -> Text -> (Query, [QueryOpt])
- Hledger.Query: words'' :: [String] -> String -> [String]
+ Hledger.Query: words'' :: [Text] -> Text -> [Text]
- Hledger.Read: accountaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
+ Hledger.Read: accountaliasp :: TextParser m AccountAlias
- Hledger.Read: postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting
+ Hledger.Read: postingp :: Maybe Day -> ErroringJournalParser Posting
- Hledger.Read: readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal)
+ Hledger.Read: readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal)
- Hledger.Read: readJournal' :: String -> IO Journal
+ Hledger.Read: readJournal' :: Text -> IO Journal
- Hledger.Read.JournalReader: accountaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
+ Hledger.Read.JournalReader: accountaliasp :: TextParser m AccountAlias
- Hledger.Read.JournalReader: datetimep :: Stream [Char] m Char => ParsecT [Char] JournalContext m LocalTime
+ Hledger.Read.JournalReader: datetimep :: ErroringJournalParser LocalTime
- Hledger.Read.JournalReader: defaultyeardirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
+ Hledger.Read.JournalReader: defaultyeardirectivep :: ErroringJournalParser ()
- Hledger.Read.JournalReader: directivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
+ Hledger.Read.JournalReader: directivep :: ErroringJournalParser ()
- Hledger.Read.JournalReader: emptyorcommentlinep :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
+ Hledger.Read.JournalReader: emptyorcommentlinep :: ErroringJournalParser ()
- Hledger.Read.JournalReader: followingcommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
+ Hledger.Read.JournalReader: followingcommentp :: ErroringJournalParser Text
- Hledger.Read.JournalReader: getParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
+ Hledger.Read.JournalReader: getParentAccount :: ErroringJournalParser AccountName
- Hledger.Read.JournalReader: journalp :: ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate, JournalContext)
+ Hledger.Read.JournalReader: journalp :: ErroringJournalParser ParsedJournal
- Hledger.Read.JournalReader: marketpricedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) MarketPrice
+ Hledger.Read.JournalReader: marketpricedirectivep :: ErroringJournalParser MarketPrice
- Hledger.Read.JournalReader: modifiedaccountnamep :: Stream [Char] m Char => ParsecT [Char] JournalContext m AccountName
+ Hledger.Read.JournalReader: modifiedaccountnamep :: ErroringJournalParser AccountName
- Hledger.Read.JournalReader: parseAndFinaliseJournal :: (ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate, JournalContext)) -> Bool -> FilePath -> String -> ExceptT String IO Journal
+ Hledger.Read.JournalReader: parseAndFinaliseJournal :: ErroringJournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal
- Hledger.Read.JournalReader: postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting
+ Hledger.Read.JournalReader: postingp :: Maybe Day -> ErroringJournalParser Posting
- Hledger.Read.JournalReader: statusp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ClearedStatus
+ Hledger.Read.JournalReader: statusp :: TextParser m ClearedStatus
- Hledger.Reports.BalanceReport: type BalanceReportItem = (RenderableAccountName, MixedAmount)
+ Hledger.Reports.BalanceReport: type BalanceReportItem = (AccountName, AccountName, Int, MixedAmount)
- Hledger.Reports.MultiBalanceReports: MultiBalanceReport :: ([DateSpan], [MultiBalanceReportRow], MultiBalanceTotalsRow) -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReports: MultiBalanceReport :: ([DateSpan], [MultiBalanceReportRow], MultiBalanceReportTotals) -> MultiBalanceReport
- Hledger.Reports.MultiBalanceReports: type MultiBalanceReportRow = (RenderableAccountName, [MixedAmount], MixedAmount, MixedAmount)
+ Hledger.Reports.MultiBalanceReports: type MultiBalanceReportRow = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
- Hledger.Reports.ReportOptions: ReportOpts :: Maybe Day -> Maybe Day -> Maybe (Interval, DateSpan) -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe DisplayExp -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FormatStr -> String -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> Maybe ClearedStatus -> Bool -> Maybe Int -> Maybe DisplayExp -> Bool -> Bool -> Bool -> Bool -> Maybe FormatStr -> String -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> ReportOpts
- Hledger.Reports.ReportOptions: [period_] :: ReportOpts -> Maybe (Interval, DateSpan)
+ Hledger.Reports.ReportOptions: [period_] :: ReportOpts -> Period
- Hledger.Reports.TransactionsReports: transactionsReportByCommodity :: TransactionsReport -> [(Commodity, TransactionsReport)]
+ Hledger.Reports.TransactionsReports: transactionsReportByCommodity :: TransactionsReport -> [(CommoditySymbol, TransactionsReport)]
- Hledger.Reports.TransactionsReports: triAmount :: (t, t1, t2, t3, t4, t5) -> t4
+ Hledger.Reports.TransactionsReports: triAmount :: (t4, t3, t2, t1, t5, t) -> t5
- Hledger.Reports.TransactionsReports: triBalance :: (t, t1, t2, t3, t4, t5) -> t5
+ Hledger.Reports.TransactionsReports: triBalance :: (t4, t3, t2, t1, t, t5) -> t5
- Hledger.Reports.TransactionsReports: triCommodityAmount :: Commodity -> (t, t1, t2, t3, MixedAmount, t4) -> MixedAmount
+ Hledger.Reports.TransactionsReports: triCommodityAmount :: CommoditySymbol -> (t4, t3, t2, t1, MixedAmount, t) -> MixedAmount
- Hledger.Reports.TransactionsReports: triCommodityBalance :: Commodity -> (t, t1, t2, t3, t4, MixedAmount) -> MixedAmount
+ Hledger.Reports.TransactionsReports: triCommodityBalance :: CommoditySymbol -> (t4, t3, t2, t1, t, MixedAmount) -> MixedAmount
- Hledger.Reports.TransactionsReports: triDate :: (t, Transaction, t1, t2, t3, t4) -> Day
+ Hledger.Reports.TransactionsReports: triDate :: (t4, Transaction, t3, t2, t1, t) -> Day
- Hledger.Reports.TransactionsReports: triOrigTransaction :: (t, t1, t2, t3, t4, t5) -> t
+ Hledger.Reports.TransactionsReports: triOrigTransaction :: (t5, t4, t3, t2, t1, t) -> t5
- Hledger.Utils: expandPath :: MonadIO m => FilePath -> FilePath -> m FilePath
+ Hledger.Utils: expandPath :: FilePath -> FilePath -> IO FilePath
- Hledger.Utils: fifth5 :: (t, t1, t2, t3, t4) -> t4
+ Hledger.Utils: fifth5 :: (t3, t2, t1, t, t4) -> t4
- Hledger.Utils: fifth6 :: (t, t1, t2, t3, t4, t5) -> t4
+ Hledger.Utils: fifth6 :: (t4, t3, t2, t1, t5, t) -> t5
- Hledger.Utils: first3 :: (t, t1, t2) -> t
+ Hledger.Utils: first3 :: (t2, t1, t) -> t2
- Hledger.Utils: first4 :: (t, t1, t2, t3) -> t
+ Hledger.Utils: first4 :: (t3, t2, t1, t) -> t3
- Hledger.Utils: first5 :: (t, t1, t2, t3, t4) -> t
+ Hledger.Utils: first5 :: (t4, t3, t2, t1, t) -> t4
- Hledger.Utils: first6 :: (t, t1, t2, t3, t4, t5) -> t
+ Hledger.Utils: first6 :: (t5, t4, t3, t2, t1, t) -> t5
- Hledger.Utils: fourth4 :: (t, t1, t2, t3) -> t3
+ Hledger.Utils: fourth4 :: (t2, t1, t, t3) -> t3
- Hledger.Utils: fourth5 :: (t, t1, t2, t3, t4) -> t3
+ Hledger.Utils: fourth5 :: (t3, t2, t1, t4, t) -> t4
- Hledger.Utils: fourth6 :: (t, t1, t2, t3, t4, t5) -> t3
+ Hledger.Utils: fourth6 :: (t4, t3, t2, t5, t1, t) -> t5
- Hledger.Utils: readFile' :: FilePath -> IO String
+ Hledger.Utils: readFile' :: FilePath -> IO Text
- Hledger.Utils: second3 :: (t, t1, t2) -> t1
+ Hledger.Utils: second3 :: (t1, t2, t) -> t2
- Hledger.Utils: second4 :: (t, t1, t2, t3) -> t1
+ Hledger.Utils: second4 :: (t2, t3, t1, t) -> t3
- Hledger.Utils: second5 :: (t, t1, t2, t3, t4) -> t1
+ Hledger.Utils: second5 :: (t3, t4, t2, t1, t) -> t4
- Hledger.Utils: second6 :: (t, t1, t2, t3, t4, t5) -> t1
+ Hledger.Utils: second6 :: (t4, t5, t3, t2, t1, t) -> t5
- Hledger.Utils: sixth6 :: (t, t1, t2, t3, t4, t5) -> t5
+ Hledger.Utils: sixth6 :: (t4, t3, t2, t1, t, t5) -> t5
- Hledger.Utils: third3 :: (t, t1, t2) -> t2
+ Hledger.Utils: third3 :: (t1, t, t2) -> t2
- Hledger.Utils: third4 :: (t, t1, t2, t3) -> t2
+ Hledger.Utils: third4 :: (t2, t1, t3, t) -> t3
- Hledger.Utils: third5 :: (t, t1, t2, t3, t4) -> t2
+ Hledger.Utils: third5 :: (t3, t2, t4, t1, t) -> t4
- Hledger.Utils: third6 :: (t, t1, t2, t3, t4, t5) -> t2
+ Hledger.Utils: third6 :: (t4, t3, t5, t2, t1, t) -> t5
- Hledger.Utils.Debug: dbg1IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg1IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg2IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg2IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg3IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg3IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg4IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg4IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg5IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg5IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg6IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg6IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg7IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg7IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg8IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg8IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbg9IO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbg9IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: dbgIO :: Show a => String -> a -> IO ()
+ Hledger.Utils.Debug: dbgIO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger.Utils.Debug: pdbg :: Stream [Char] m t => Int -> String -> ParsecT [Char] st m ()
+ Hledger.Utils.Debug: pdbg :: Int -> String -> TextParser m ()
- Hledger.Utils.Debug: ptrace :: Stream [Char] m t => String -> ParsecT [Char] st m ()
+ Hledger.Utils.Debug: ptrace :: String -> TextParser m ()
- Hledger.Utils.Debug: tracePrettyAtIO :: Show a => Int -> String -> a -> IO ()
+ Hledger.Utils.Debug: tracePrettyAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()
- Hledger.Utils.Parse: choice' :: Stream s m t => [ParsecT s u m a] -> ParsecT s u m a
+ Hledger.Utils.Parse: choice' :: [TextParser m a] -> TextParser m a
- Hledger.Utils.Parse: eolof :: (Stream [Char] m Char) => ParsecT [Char] st m ()
+ Hledger.Utils.Parse: eolof :: TextParser m ()
- Hledger.Utils.Parse: fromparse :: Either ParseError a -> a
+ Hledger.Utils.Parse: fromparse :: (Show t, Show e) => Either (ParseError t e) a -> a
- Hledger.Utils.Parse: nonspace :: (Stream [Char] m Char) => ParsecT [Char] st m Char
+ Hledger.Utils.Parse: nonspace :: TextParser m Char
- Hledger.Utils.Parse: parseerror :: ParseError -> a
+ Hledger.Utils.Parse: parseerror :: (Show t, Show e) => ParseError t e -> a
- Hledger.Utils.Parse: parsewith :: Parsec [Char] () a -> String -> Either ParseError a
+ Hledger.Utils.Parse: parsewith :: Parsec e Text a -> Text -> Either (ParseError Char e) a
- Hledger.Utils.Parse: restofline :: (Stream [Char] m Char) => ParsecT [Char] st m String
+ Hledger.Utils.Parse: restofline :: TextParser m String
- Hledger.Utils.Parse: showDateParseError :: ParseError -> String
+ Hledger.Utils.Parse: showDateParseError :: (Show t, Show e) => ParseError t e -> String
- Hledger.Utils.Parse: showParseError :: ParseError -> String
+ Hledger.Utils.Parse: showParseError :: (Show t, Show e) => ParseError t e -> String
- Hledger.Utils.Parse: spacenonewline :: (Stream [Char] m Char) => ParsecT [Char] st m Char
+ Hledger.Utils.Parse: spacenonewline :: (Stream s, Char ~ Token s) => ParsecT Dec s m Char
- Hledger.Utils.Test: assertParse :: (Either ParseError a) -> Assertion
+ Hledger.Utils.Test: assertParse :: (Show t, Show e) => (Either (ParseError t e) a) -> Assertion
- Hledger.Utils.Test: assertParseEqual :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion
+ Hledger.Utils.Test: assertParseEqual :: (Show a, Eq a, Show t, Show e) => (Either (ParseError t e) a) -> a -> Assertion
- Hledger.Utils.Test: assertParseFailure :: (Either ParseError a) -> Assertion
+ Hledger.Utils.Test: assertParseFailure :: (Either (ParseError t e) a) -> Assertion

Files

CHANGES view
@@ -1,9 +1,154 @@-API-ish changes in hledger-lib.-For user-visible changes, see hledger's change log.+API-ish changes in the hledger-lib package.+(For user-visible changes, see hledger's change log.) -0.27.1 (2016/05/27) -- GHC 8 compatibility+# 1.0 (2016/10/26)++## timedot format++-   new "timedot" format for retroactive/approximate time logging.++    Timedot is a plain text format for logging dated, categorised+    quantities (eg time), supported by hledger.  It is convenient+    for approximate and retroactive time logging, eg when the+    real-time clock-in/out required with a timeclock file is too+    precise or too interruptive.  It can be formatted like a bar+    chart, making clear at a glance where time was spent.++## timeclock format++-   renamed "timelog" format to "timeclock", matching the emacs package++-   sessions can no longer span file boundaries (unclocked-out++    sessions will be auto-closed at the end of the file).++-   transaction ids now count up rather than down (#394)++-   timeclock files no longer support default year directives++-   removed old code for appending timeclock transactions to journal transactions.++    A holdover from the days when both were allowed in one file.++## csv format++-   fix empty field assignment parsing, rule parse errors after megaparsec port (#407) (Hans-Peter Deifel)++## journal format++-   journal files can now include timeclock or timedot files (#320)++    (but not yet CSV files).++-   fixed an issue with ordering of same-date transactions included from other files++-   the "commodity" directive and "format" subdirective are now supported, allowing++    full control of commodity style (#295) The commodity directive's+    format subdirective can now be used to override the inferred+    style for a commodity, eg to increase or decrease the+    precision. This is at least a good workaround for #295.++-   Ledger-style "apply account"/"end apply account" directives are now used to set a default parent account.++-   the Ledger-style "account" directive is now accepted (and ignored).++-   bracketed posting dates are more robust (#304)++    Bracketed posting dates were fragile; they worked only if you+    wrote full 10-character dates. Also some semantics were a bit+    unclear. Now they should be robust, and have been documented+    more clearly. This is a legacy undocumented Ledger syntax, but+    it improves compatibility and might be preferable to the more+    verbose "date:" tags if you write posting dates often (as I do).+    Internally, bracketed posting dates are no longer considered to+    be tags.  Journal comment, tag, and posting date parsers have+    been reworked, all with doctests.++-   balance assertion failure messages are clearer++-   with --debug=2, more detail about balance assertions is shown.++## misc++-   file parsers have been ported from Parsec to Megaparsec \o/ (#289, #366) (Alexey Shmalko, Moritz Kiefer)++-   most hledger types have been converted from String to Text, reducing memory usage by 30%+ on large files and giving a slight speed increase++-   file parsers have been simplified for easier troubleshooting (#275).++    The journal/timeclock/timedot parsers, instead of constructing+    opaque journal update functions which are later applied to build+    the journal, now construct the journal directly by modifying the+    parser state. This is easier to understand and debug. It also+    rules out the possibility of journal updates being a space+    leak. (They weren't, in fact this change increased memory usage+    slightly, but that has been addressed in other ways).  The+    ParsedJournal type alias has been added to distinguish+    "being-parsed" journals and "finalised" journals.++-   file format detection is more robust.++    The Journal, Timelog and Timedot readers' detectors now check+    each line in the sample data, not just the first one. I think the+    sample data is only about 30 chars right now, but even so this+    fixed a format detection issue I was seeing. +    Also, we now always try parsing stdin as journal format (not just sometimes).++-   all file formats now produce transaction ids, not just journal (#394)++-   git clone of the hledger repo on windows now works (#345)++-   added missing benchmark file (#342)++-   our stack.yaml files are more compatible across stack versions (#300)++-   use newer file-embed to fix ghci working directory dependence (<https://github.com/snoyberg/file-embed/issues/18>)++-   report more accurate dates in account transaction report when postings have their own dates++    (affects hledger-ui and hledger-web registers).+    The newly-named "transaction register date" is the date to be+    displayed for that transaction in a transaction register, for+    some current account and filter query.  It is either the+    transaction date from the journal ("transaction general date"),+    or if postings to the current account and matched by the+    register's filter query have their own dates, the earliest of+    those posting dates.++-   simplify account transactions report's running total.++    The account transactions report used for hledger-ui and -web+    registers now gives either the "period total" or "historical+    total", depending strictly on the --historical flag. It doesn't+    try to indicate whether the historical total is the accurate+    historical balance (which depends on the user's report query).++-   reloading a file now preserves the effect of options, query arguments etc.++-   reloading a journal should now reload all included files as well.++-   the Hledger.Read.\* modules have been reorganised for better reuse.++    Hledger.Read.Utils has been renamed Hledger.Read.Common+    and holds low-level parsers & utilities; high-level read+    utilities are now in Hledger.Read.++-   clarify amount display style canonicalisation code and terminology a bit.++    Individual amounts still have styles; from these we derive+    the standard "commodity styles". In user docs, we might call+    these "commodity formats" since they can be controlled by the+    "format" subdirective in journal files.++-   Journal is now a monoid++-   expandPath now throws a proper IO error++-   more unit tests, start using doctest++   0.27 (2015/10/30)
Hledger.hs view
@@ -1,19 +1,15 @@ module Hledger (-                module Hledger.Data-               ,module Hledger.Query-               ,module Hledger.Read-               ,module Hledger.Reports-               ,module Hledger.Utils-               ,tests_Hledger+  module X+ ,tests_Hledger ) where-import Test.HUnit+import           Test.HUnit -import Hledger.Data-import Hledger.Query-import Hledger.Read hiding (samplejournal)-import Hledger.Reports-import Hledger.Utils+import           Hledger.Data    as X+import           Hledger.Query   as X+import           Hledger.Read    as X hiding (samplejournal)+import           Hledger.Reports as X+import           Hledger.Utils   as X  tests_Hledger = TestList     [
Hledger/Data.hs view
@@ -15,10 +15,11 @@                module Hledger.Data.Dates,                module Hledger.Data.Journal,                module Hledger.Data.Ledger,+               module Hledger.Data.Period,                module Hledger.Data.Posting,                module Hledger.Data.RawOptions,                module Hledger.Data.StringFormat,-               module Hledger.Data.TimeLog,+               module Hledger.Data.Timeclock,                module Hledger.Data.Transaction,                module Hledger.Data.Types,                tests_Hledger_Data@@ -33,10 +34,11 @@ import Hledger.Data.Dates import Hledger.Data.Journal import Hledger.Data.Ledger+import Hledger.Data.Period import Hledger.Data.Posting import Hledger.Data.RawOptions import Hledger.Data.StringFormat-import Hledger.Data.TimeLog+import Hledger.Data.Timeclock import Hledger.Data.Transaction import Hledger.Data.Types @@ -47,13 +49,12 @@     ,tests_Hledger_Data_AccountName     ,tests_Hledger_Data_Amount     ,tests_Hledger_Data_Commodity-    ,tests_Hledger_Data_Dates     ,tests_Hledger_Data_Journal     ,tests_Hledger_Data_Ledger     ,tests_Hledger_Data_Posting     -- ,tests_Hledger_Data_RawOptions     -- ,tests_Hledger_Data_StringFormat-    ,tests_Hledger_Data_TimeLog+    ,tests_Hledger_Data_Timeclock     ,tests_Hledger_Data_Transaction     -- ,tests_Hledger_Data_Types     ]
Hledger/Data/Account.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, StandaloneDeriving #-}+{-# LANGUAGE RecordWildCards, StandaloneDeriving, OverloadedStrings #-} {-|  @@ -52,9 +52,12 @@   , aboring = False   } --- | Derive 1. an account tree and 2. their balances from a list of postings.--- (ledger's core feature). The accounts are returned in a list, but--- retain their tree structure; the first one is the root of the tree.+-- | Derive 1. an account tree and 2. each account's total exclusive+-- and inclusive changes from a list of postings.+-- This is the core of the balance command (and of *ledger).+-- The accounts are returned as a list in flattened tree order,+-- and also reference each other as a tree.+-- (The first account is the root of the tree.) accountsFromPostings :: [Posting] -> [Account] accountsFromPostings ps =   let
Hledger/Data/AccountName.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE NoMonomorphismRestriction#-}+{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-|  'AccountName's are strings like @assets:cash:petty@, with multiple@@ -10,7 +10,9 @@ module Hledger.Data.AccountName where import Data.List-import Data.List.Split (splitOn)+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T import Data.Tree import Test.HUnit import Text.Printf@@ -19,24 +21,29 @@ import Hledger.Utils  ---- change to use a different separator for nested accounts+acctsepchar :: Char acctsepchar = ':' -accountNameComponents :: AccountName -> [String]-accountNameComponents = splitAtElement acctsepchar+acctsep :: Text+acctsep = T.pack [acctsepchar] -accountNameFromComponents :: [String] -> AccountName-accountNameFromComponents = concat . intersperse [acctsepchar]+-- accountNameComponents :: AccountName -> [String]+-- accountNameComponents = splitAtElement acctsepchar -accountLeafName :: AccountName -> String+accountNameComponents :: AccountName -> [Text]+accountNameComponents = T.splitOn acctsep++accountNameFromComponents :: [Text] -> AccountName+accountNameFromComponents = T.intercalate acctsep++accountLeafName :: AccountName -> Text accountLeafName = last . accountNameComponents  -- | Truncate all account name components but the last to two characters.-accountSummarisedName :: AccountName -> String+accountSummarisedName :: AccountName -> Text accountSummarisedName a   --   length cs > 1 = take 2 (head cs) ++ ":" ++ a'-  | length cs > 1 = intercalate ":" (map (take 2) $ init cs) ++ ":" ++ a'+  | length cs > 1 = (T.intercalate ":" (map (T.take 2) $ init cs)) <> ":" <> a'   | otherwise     = a'     where       cs = accountNameComponents a@@ -44,7 +51,7 @@  accountNameLevel :: AccountName -> Int accountNameLevel "" = 0-accountNameLevel a = length (filter (==acctsepchar) a) + 1+accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1  accountNameDrop :: Int -> AccountName -> AccountName accountNameDrop n = accountNameFromComponents . drop n . accountNameComponents@@ -72,7 +79,7 @@  -- | Is the first account a parent or other ancestor of (and not the same as) the second ? isAccountNamePrefixOf :: AccountName -> AccountName -> Bool-isAccountNamePrefixOf = isPrefixOf . (++ [acctsepchar])+isAccountNamePrefixOf = T.isPrefixOf . (<> acctsep)  isSubAccountNameOf :: AccountName -> AccountName -> Bool s `isSubAccountNameOf` p =@@ -113,22 +120,22 @@ elideAccountName :: Int -> AccountName -> AccountName elideAccountName width s   -- XXX special case for transactions register's multi-account pseudo-names-  | " (split)" `isSuffixOf` s =+  | " (split)" `T.isSuffixOf` s =     let-      names = splitOn ", " $ take (length s - 8) s+      names = T.splitOn ", " $ T.take (T.length s - 8) s       widthpername = (max 0 (width - 8 - 2 * (max 1 (length names) - 1))) `div` length names     in-     fitString Nothing (Just width) True False $-     (++" (split)") $-     intercalate ", " $+     fitText Nothing (Just width) True False $+     (<>" (split)") $+     T.intercalate ", " $      [accountNameFromComponents $ elideparts widthpername [] $ accountNameComponents s' | s' <- names]   | otherwise =-    fitString Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s+    fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s       where-        elideparts :: Int -> [String] -> [String] -> [String]+        elideparts :: Int -> [Text] -> [Text] -> [Text]         elideparts width done ss-          | strWidth (accountNameFromComponents $ done++ss) <= width = done++ss-          | length ss > 1 = elideparts width (done++[takeWidth 2 $ head ss]) (tail ss)+          | textWidth (accountNameFromComponents $ done++ss) <= width = done++ss+          | length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss)           | otherwise = done++ss  -- | Keep only the first n components of an account name, where n@@ -143,18 +150,18 @@ clipOrEllipsifyAccountName n = accountNameFromComponents . take n . accountNameComponents  -- | Convert an account name to a regular expression matching it and its subaccounts.-accountNameToAccountRegex :: String -> String+accountNameToAccountRegex :: AccountName -> Regexp accountNameToAccountRegex "" = ""-accountNameToAccountRegex a = printf "^%s(:|$)" a+accountNameToAccountRegex a = printf "^%s(:|$)" (T.unpack a)  -- | Convert an account name to a regular expression matching it but not its subaccounts.-accountNameToAccountOnlyRegex :: String -> String+accountNameToAccountOnlyRegex :: AccountName -> Regexp accountNameToAccountOnlyRegex "" = ""-accountNameToAccountOnlyRegex a = printf "^%s$" a+accountNameToAccountOnlyRegex a = printf "^%s$"  $ T.unpack a -- XXX pack  -- | Convert an exact account-matching regular expression to a plain account name.-accountRegexToAccountName :: String -> String-accountRegexToAccountName = regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1"+accountRegexToAccountName :: Regexp -> AccountName+accountRegexToAccountName = T.pack . regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1" -- XXX pack  -- | Does this string look like an exact account-matching regular expression ? isAccountRegex  :: String -> Bool
Hledger/Data/Amount.hs view
@@ -1,7 +1,6 @@-{-# LANGUAGE CPP, StandaloneDeriving, RecordWildCards #-} {-| A simple 'Amount' is some quantity of money, shares, or anything else.-It has a (possibly null) 'Commodity' and a numeric quantity:+It has a (possibly null) 'CommoditySymbol' and a numeric quantity:  @   $1@@ -41,6 +40,8 @@  -} +{-# LANGUAGE CPP, StandaloneDeriving, RecordWildCards, OverloadedStrings #-}+ module Hledger.Data.Amount (   -- * Amount   amount,@@ -101,15 +102,13 @@ ) where  import Data.Char (isDigit)-#ifdef DOUBLE-roundTo = flip const-#else import Data.Decimal (roundTo)-#endif import Data.Function (on) import Data.List import Data.Map (findWithDefault) import Data.Maybe+-- import Data.Text (Text)+import qualified Data.Text as T import Test.HUnit import Text.Printf import qualified Data.Map as M@@ -180,7 +179,7 @@  -- | Convert an amount to the specified commodity, ignoring and discarding -- any assigned prices and assuming an exchange rate of 1.-amountWithCommodity :: Commodity -> Amount -> Amount+amountWithCommodity :: CommoditySymbol -> Amount -> Amount amountWithCommodity c a = a{acommodity=c, aprice=NoPrice}  -- | Convert an amount to the commodity of its assigned price, if any.  Notes:@@ -211,16 +210,8 @@                | otherwise     = (null . filter (`elem` digits) . showAmountWithoutPriceOrCommodity) a  -- | 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 Amount{aquantity=q} = iszero q-  where-   iszero =-#ifdef DOUBLE-    null . filter (`elem` digits) . printf ("%."++show zeroprecision++"f") where zeroprecision = 8-#else-    (==0)-#endif+isReallyZeroAmount Amount{aquantity=q} = q == 0  -- | Get the string representation of an amount, based on its commodity's -- display settings except using the specified precision.@@ -270,14 +261,14 @@ showAmountHelper _ Amount{acommodity="AUTO"} = "" showAmountHelper showzerocommodity a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) =     case ascommodityside of-      L -> printf "%s%s%s%s" c' space quantity' price-      R -> printf "%s%s%s%s" quantity' space c' price+      L -> printf "%s%s%s%s" (T.unpack c') space quantity' price+      R -> printf "%s%s%s%s" quantity' space (T.unpack c') price     where       quantity = showamountquantity a       displayingzero = null $ filter (`elem` digits) $ quantity       (quantity',c') | displayingzero && not showzerocommodity = ("0","")                      | otherwise = (quantity, quoteCommoditySymbolIfNeeded c)-      space = if (not (null c') && ascommodityspaced) then " " else "" :: String+      space = if (not (T.null c') && ascommodityspaced) then " " else "" :: String       price = showPrice p  -- | Like showAmount, but show a zero amount's commodity if it has one.@@ -292,15 +283,9 @@     where       -- isint n = fromIntegral (round n) == n       qstr -- p == maxprecision && isint q = printf "%d" (round q::Integer)-#ifdef DOUBLE-        | p == maxprecisionwithpoint = printf "%f" q-        | p == maxprecision          = chopdotzero $ printf "%f" q-        | otherwise                  = printf ("%."++show p++"f") q-#else         | p == maxprecisionwithpoint = show q         | p == maxprecision          = chopdotzero $ show q         | otherwise                  = show $ roundTo (fromIntegral p) q-#endif  -- | Replace a number string's decimal point with the specified character, -- and add the specified digit group separators. The last digit group will@@ -340,7 +325,7 @@  -- like journalCanonicaliseAmounts -- | Canonicalise an amount's display style using the provided commodity style map.-canonicaliseAmount :: M.Map Commodity AmountStyle -> Amount -> Amount+canonicaliseAmount :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount canonicaliseAmount styles a@Amount{acommodity=c, astyle=s} = a{astyle=s'}     where       s' = findWithDefault s c styles@@ -449,9 +434,9 @@ sumSimilarAmountsUsingFirstPrice [] = nullamt sumSimilarAmountsUsingFirstPrice as = (sum as){aprice=aprice $ head as} --- | Sum same-commodity amounts. If there were different prices, set--- the price to a special marker indicating "various". Only used as a--- rendering helper.+-- -- | Sum same-commodity amounts. If there were different prices, set+-- -- the price to a special marker indicating "various". Only used as a+-- -- rendering helper. -- sumSimilarAmountsNotingPriceDifference :: [Amount] -> Amount -- sumSimilarAmountsNotingPriceDifference [] = nullamt -- sumSimilarAmountsNotingPriceDifference as = undefined@@ -468,7 +453,7 @@ -- with the specified commodity and the quantity of that commodity -- found in the original. NB if Amount's quantity is zero it will be -- discarded next time the MixedAmount gets normalised.-filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount+filterMixedAmountByCommodity :: CommoditySymbol -> MixedAmount -> MixedAmount filterMixedAmountByCommodity c (Mixed as) = Mixed as'   where     as' = case filter ((==c) . acommodity) as of@@ -580,7 +565,7 @@       stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}  -- | Canonicalise a mixed amount's display styles using the provided commodity style map.-canonicaliseMixedAmount :: M.Map Commodity AmountStyle -> MixedAmount -> MixedAmount+canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as  -------------------------------------------------------------------------------
Hledger/Data/Commodity.hs view
@@ -6,10 +6,16 @@ are thousands separated by comma, significant decimal places and so on.  -}++{-# LANGUAGE OverloadedStrings #-}+ module Hledger.Data.Commodity where import Data.List import Data.Maybe (fromMaybe)+import Data.Monoid+-- import Data.Text (Text)+import qualified Data.Text as T import Test.HUnit -- import qualified Data.Map as M @@ -18,9 +24,9 @@   -- characters that may not be used in a non-quoted commodity symbol-nonsimplecommoditychars = "0123456789-+.@;\n \"{}=" :: String+nonsimplecommoditychars = "0123456789-+.@;\n \"{}=" :: [Char] -quoteCommoditySymbolIfNeeded s | any (`elem` nonsimplecommoditychars) s = "\"" ++ s ++ "\""+quoteCommoditySymbolIfNeeded s | any (`elem` nonsimplecommoditychars) (T.unpack s) = "\"" <> s <> "\""                                | otherwise = s  commodity = ""@@ -42,18 +48,18 @@   ]  -- | Look up one of the sample commodities' symbol by name.-comm :: String -> Commodity+comm :: String -> CommoditySymbol comm name = snd $ fromMaybe               (error' "commodity lookup failed")               (find (\n -> fst n == name) commoditysymbols)  -- | Find the conversion rate between two commodities. Currently returns 1.-conversionRate :: Commodity -> Commodity -> Double+conversionRate :: CommoditySymbol -> CommoditySymbol -> Double conversionRate _ _ = 1  -- -- | Convert a list of commodities to a map from commodity symbols to -- -- unique, display-preference-canonicalised commodities.--- canonicaliseCommodities :: [Commodity] -> Map.Map String Commodity+-- canonicaliseCommodities :: [CommoditySymbol] -> Map.Map String CommoditySymbol -- canonicaliseCommodities cs = --     Map.fromList [(s,firstc{precision=maxp}) | s <- symbols, --                   let cs = commoditymap ! s,
Hledger/Data/Dates.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE NoMonoLocalBinds #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE OverloadedStrings #-} {-|  Date parsing and utilities for hledger.@@ -19,6 +21,8 @@ An 'Interval' is ledger's \"reporting interval\" - weekly, monthly, quarterly, etc. +'Period' will probably replace DateSpan in due course.+ -}  -- XXX fromGregorian silently clips bad dates, use fromGregorianValid instead ?@@ -38,7 +42,6 @@   prevday,   parsePeriodExpr,   nulldatespan,-  tests_Hledger_Data_Dates,   failIfInvalidYear,   failIfInvalidMonth,   failIfInvalidDay,@@ -69,6 +72,8 @@ import Control.Monad import Data.List.Compat import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T #if MIN_VERSION_time(1,5,0) import Data.Time.Format hiding (months) #else@@ -77,82 +82,30 @@ #endif import Data.Time.Calendar import Data.Time.Calendar.OrdinalDate-import Data.Time.Calendar.WeekDate import Data.Time.Clock import Data.Time.LocalTime import Safe (headMay, lastMay, readMay)-import Test.HUnit-import Text.Parsec+import Text.Megaparsec+import Text.Megaparsec.Text import Text.Printf  import Hledger.Data.Types+import Hledger.Data.Period import Hledger.Utils   -- Help ppShow parse and line-wrap DateSpans better in debug output. instance Show DateSpan where-    show (DateSpan s1 s2) = "DateSpan \"" ++ show s1 ++ "\" \"" ++ show s2 ++ "\""+    show s = "DateSpan " ++ showDateSpan s+    -- show s = "DateSpan \"" ++ showDateSpan s ++ "\"" -- quotes to help pretty-show  showDate :: Day -> String showDate = formatTime defaultTimeLocale "%0C%y/%m/%d" --- XXX review for more boundary crossing issues -- | Render a datespan as a display string, abbreviating into a -- compact form if possible.-showDateSpan ds@(DateSpan (Just from) (Just to)) =-  case (toGregorian from, toGregorian to) of-    -- special cases we can abbreviate:-    -- a year, YYYY-    ((fy,1,1), (ty,1,1))   | fy+1==ty           -> formatTime defaultTimeLocale "%0C%y" from-    -- a half, YYYYhN-    ((fy,1,1), (ty,7,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yh1" from-    ((fy,7,1), (ty,1,1))   | fy+1==ty           -> formatTime defaultTimeLocale "%0C%yh2" from-    -- a quarter, YYYYqN-    ((fy,1,1), (ty,4,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yq1" from-    ((fy,4,1), (ty,7,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yq2" from-    ((fy,7,1), (ty,10,1))  | fy==ty             -> formatTime defaultTimeLocale "%0C%yq3" from-    ((fy,10,1), (ty,1,1))  | fy+1==ty           -> formatTime defaultTimeLocale "%0C%yq4" from-    -- a month, YYYY/MM-    ((fy,fm,1), (ty,tm,1)) | fy==ty && fm+1==tm -> formatTime defaultTimeLocale "%0C%y/%m" from-    ((fy,12,1), (ty,1,1))  | fy+1==ty           -> formatTime defaultTimeLocale "%0C%y/%m" from-    -- a week (two successive mondays),-    -- YYYYwN ("week N of year YYYY")-    -- _ | let ((fy,fw,fd), (ty,tw,td)) = (toWeekDate from, toWeekDate to) in fy==ty && fw+1==tw && fd==1 && td==1-    --                                             -> formatTime defaultTimeLocale "%0f%gw%V" from-    -- YYYY/MM/DDwN ("week N, starting on YYYY/MM/DD")-    _ | let ((fy,fw,fd), (ty,tw,td)) = (toWeekDate from, toWeekDate (addDays (-1) to)) in fy==ty && fw==tw && fd==1 && td==7-                                                -> formatTime defaultTimeLocale "%0C%y/%m/%dw%V" from-    -- a day, YYYY/MM/DDd (d suffix is to distinguish from a regular date in register)-    ((fy,fm,fd), (ty,tm,td)) | fy==ty && fm==tm && fd+1==td -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from-    -- crossing a year boundary-    ((fy,fm,fd), (ty,tm,td)) | fy+1==ty && fm==12 && tm==1 && fd==31 && td==1 -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from-    -- crossing a month boundary XXX wrongly shows LEAPYEAR/2/28-LEAPYEAR/3/1 as LEAPYEAR/2/28-    ((fy,fm,fd), (ty,tm,td)) | fy==ty && fm+1==tm && fd `elem` fromMaybe [] (lookup fm lastdayofmonth) && td==1 -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from-    -- otherwise, YYYY/MM/DD-YYYY/MM/DD-    _                                           -> showDateSpan' ds-  where lastdayofmonth = [(1,[31])-                         ,(2,[28,29])-                         ,(3,[31])-                         ,(4,[30])-                         ,(5,[31])-                         ,(6,[30])-                         ,(7,[31])-                         ,(8,[31])-                         ,(9,[30])-                         ,(10,[31])-                         ,(11,[30])-                         ,(12,[31])-                         ]--showDateSpan ds = showDateSpan' ds---- | Render a datespan as a display string.-showDateSpan' (DateSpan from to) =-  concat-    [maybe "" showDate from-    ,"-"-    ,maybe "" (showDate . prevday) to-    ]+showDateSpan :: DateSpan -> String+showDateSpan = showPeriod . dateSpanAsPeriod  -- | Get the current local date. getCurrentDay :: IO Day@@ -189,6 +142,32 @@  -- | Split a DateSpan into one or more consecutive whole spans of the specified length which enclose it. -- If no interval is specified, the original span is returned.+--+-- ==== Examples:+-- >>> let t i d1 d2 = splitSpan i $ mkdatespan d1 d2+-- >>> t NoInterval "2008/01/01" "2009/01/01"+-- [DateSpan 2008]+-- >>> t (Quarters 1) "2008/01/01" "2009/01/01"+-- [DateSpan 2008q1,DateSpan 2008q2,DateSpan 2008q3,DateSpan 2008q4]+-- >>> splitSpan (Quarters 1) nulldatespan+-- [DateSpan -]+-- >>> t (Days 1) "2008/01/01" "2008/01/01"  -- an empty datespan+-- [DateSpan 2008/01/01-2007/12/31]+-- >>> t (Quarters 1) "2008/01/01" "2008/01/01"+-- [DateSpan 2008/01/01-2007/12/31]+-- >>> t (Months 1) "2008/01/01" "2008/04/01"+-- [DateSpan 2008/01,DateSpan 2008/02,DateSpan 2008/03]+-- >>> t (Months 2) "2008/01/01" "2008/04/01"+-- [DateSpan 2008/01/01-2008/02/29,DateSpan 2008/03/01-2008/04/30]+-- >>> t (Weeks 1) "2008/01/01" "2008/01/15"+-- [DateSpan 2007/12/31w01,DateSpan 2008/01/07w02,DateSpan 2008/01/14w03]+-- >>> t (Weeks 2) "2008/01/01" "2008/01/15"+-- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]+-- >>> t (DayOfMonth 2) "2008/01/01" "2008/04/01"+-- [DateSpan 2008/01/02-2008/02/01,DateSpan 2008/02/02-2008/03/01,DateSpan 2008/03/02-2008/04/01]+-- >>> t (DayOfWeek 2) "2011/01/01" "2011/01/15"+-- [DateSpan 2011/01/04-2011/01/10,DateSpan 2011/01/11-2011/01/17]+-- splitSpan :: Interval -> DateSpan -> [DateSpan] splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing] splitSpan NoInterval     s = [s]@@ -271,10 +250,10 @@  -- | Parse a period expression to an Interval and overall DateSpan using -- the provided reference date, or return a parse error.-parsePeriodExpr :: Day -> String -> Either ParseError (Interval, DateSpan)+parsePeriodExpr :: Day -> Text -> Either (ParseError Char Dec) (Interval, DateSpan) parsePeriodExpr refdate = parsewith (periodexpr refdate <* eof) -maybePeriod :: Day -> String -> Maybe (Interval,DateSpan)+maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan) maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate  -- | Show a DateSpan as a human-readable pseudo-period-expression string.@@ -327,22 +306,93 @@  -- | 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 :: Day -> Text -> String fixSmartDateStr d s = either                        (\e->error' $ printf "could not parse date %s %s" (show s) (show e))                        id-                       $ fixSmartDateStrEither d s+                       $ (fixSmartDateStrEither d s :: Either (ParseError Char Dec) String)  -- | A safe version of fixSmartDateStr.-fixSmartDateStrEither :: Day -> String -> Either ParseError String+fixSmartDateStrEither :: Day -> Text -> Either (ParseError Char Dec) String fixSmartDateStrEither d = either Left (Right . showDate) . fixSmartDateStrEither' d -fixSmartDateStrEither' :: Day -> String -> Either ParseError Day-fixSmartDateStrEither' d s = case parsewith smartdateonly (lowercase s) of+fixSmartDateStrEither' :: Day -> Text -> Either (ParseError Char Dec) Day+fixSmartDateStrEither' d s = case parsewith smartdateonly (T.toLower s) of                                Right sd -> Right $ fixSmartDate d sd                                Left e -> Left e  -- | Convert a SmartDate to an absolute date using the provided reference date.+--+-- ==== Examples:+-- >>> let t = fixSmartDateStr (parsedate "2008/11/26")+-- >>> t "0000-01-01"+-- "0000/01/01"+-- >>> t "1999-12-02"+-- "1999/12/02"+-- >>> t "1999.12.02"+-- "1999/12/02"+-- >>> t "1999/3/2"+-- "1999/03/02"+-- >>> t "19990302"+-- "1999/03/02"+-- >>> t "2008/2"+-- "2008/02/01"+-- >>> t "0020/2"+-- "0020/02/01"+-- >>> t "1000"+-- "1000/01/01"+-- >>> t "4/2"+-- "2008/04/02"+-- >>> t "2"+-- "2008/11/02"+-- >>> t "January"+-- "2008/01/01"+-- >>> t "feb"+-- "2008/02/01"+-- >>> t "today"+-- "2008/11/26"+-- >>> t "yesterday"+-- "2008/11/25"+-- >>> t "tomorrow"+-- "2008/11/27"+-- >>> t "this day"+-- "2008/11/26"+-- >>> t "last day"+-- "2008/11/25"+-- >>> t "next day"+-- "2008/11/27"+-- >>> t "this week"  -- last monday+-- "2008/11/24"+-- >>> t "last week"  -- previous monday+-- "2008/11/17"+-- >>> t "next week"  -- next monday+-- "2008/12/01"+-- >>> t "this month"+-- "2008/11/01"+-- >>> t "last month"+-- "2008/10/01"+-- >>> t "next month"+-- "2008/12/01"+-- >>> t "this quarter"+-- "2008/10/01"+-- >>> t "last quarter"+-- "2008/07/01"+-- >>> t "next quarter"+-- "2009/01/01"+-- >>> t "this year"+-- "2008/01/01"+-- >>> t "last year"+-- "2007/01/01"+-- >>> t "next year"+-- "2009/01/01"+--+-- t "last wed"+-- "2008/11/19"+-- t "next friday"+-- "2008/11/28"+-- t "next january"+-- "2009/01/01"+-- fixSmartDate :: Day -> SmartDate -> Day fixSmartDate refdate sdate = fix sdate     where@@ -448,7 +498,10 @@ -- parsedatetime s = fromMaybe (error' $ "could not parse timestamp \"" ++ s ++ "\"") --                             (parsedatetimeM s) --- | Parse a date string to a time type, or raise an error.+-- | Parse a YYYY-MM-DD or YYYY/MM/DD date string to a Day, or raise an error. For testing/debugging.+--+-- >>> parsedate "2008/02/03"+-- 2008-02-03 parsedate :: String -> Day parsedate s =  fromMaybe (error' $ "could not parse date \"" ++ s ++ "\"")                          (parsedateM s)@@ -471,8 +524,8 @@  -- | 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+_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,@@ -490,22 +543,23 @@ Returns a SmartDate, to be converted to a full date later (see fixSmartDate). Assumes any text in the parse stream has been lowercased. -}-smartdate :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+smartdate :: Parser SmartDate smartdate = do   -- XXX maybe obscures date errors ? see ledgerdate   (y,m,d) <- choice' [yyyymmdd, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing]   return (y,m,d)  -- | Like smartdate, but there must be nothing other than whitespace after the date.-smartdateonly :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+smartdateonly :: Parser SmartDate smartdateonly = do   d <- smartdate   many spacenonewline   eof   return d +datesepchars :: [Char] datesepchars = "/-."-datesepchar :: Stream [Char] m Char => ParsecT [Char] st m Char+datesepchar :: TextParser m Char datesepchar = oneOf datesepchars  validYear, validMonth, validDay :: String -> Bool@@ -518,54 +572,54 @@ failIfInvalidMonth s = unless (validMonth s) $ fail $ "bad month number: " ++ s failIfInvalidDay s   = unless (validDay s)   $ fail $ "bad day number: " ++ s -yyyymmdd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+yyyymmdd :: Parser SmartDate yyyymmdd = do-  y <- count 4 digit-  m <- count 2 digit+  y <- count 4 digitChar+  m <- count 2 digitChar   failIfInvalidMonth m-  d <- count 2 digit+  d <- count 2 digitChar   failIfInvalidDay d   return (y,m,d) -ymd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+ymd :: Parser SmartDate ymd = do-  y <- many1 digit+  y <- some digitChar   failIfInvalidYear y   sep <- datesepchar-  m <- many1 digit+  m <- some digitChar   failIfInvalidMonth m   char sep-  d <- many1 digit+  d <- some digitChar   failIfInvalidDay d   return $ (y,m,d) -ym :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+ym :: Parser SmartDate ym = do-  y <- many1 digit+  y <- some digitChar   failIfInvalidYear y   datesepchar-  m <- many1 digit+  m <- some digitChar   failIfInvalidMonth m   return (y,m,"") -y :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+y :: Parser SmartDate y = do-  y <- many1 digit+  y <- some digitChar   failIfInvalidYear y   return (y,"","") -d :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+d :: Parser SmartDate d = do-  d <- many1 digit+  d <- some digitChar   failIfInvalidDay d   return ("","",d) -md :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+md :: Parser SmartDate md = do-  m <- many1 digit+  m <- some digitChar   failIfInvalidMonth m   datesepchar-  d <- many1 digit+  d <- some digitChar   failIfInvalidDay d   return ("",m,d) @@ -578,24 +632,24 @@ monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs -month :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+month :: Parser SmartDate month = do   m <- choice $ map (try . string) months   let i = monthIndex m   return ("",show i,"") -mon :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+mon :: Parser SmartDate mon = do   m <- choice $ map (try . string) monthabbrevs   let i = monIndex m   return ("",show i,"") -today,yesterday,tomorrow :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+today,yesterday,tomorrow :: Parser SmartDate today     = string "today"     >> return ("","","today") yesterday = string "yesterday" >> return ("","","yesterday") tomorrow  = string "tomorrow"  >> return ("","","tomorrow") -lastthisnextthing :: Stream [Char] m Char => ParsecT [Char] st m SmartDate+lastthisnextthing :: Parser SmartDate lastthisnextthing = do   r <- choice [         string "last"@@ -615,7 +669,19 @@    return ("",r,p) -periodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan)+-- |+-- >>> let p = parsewith (periodexpr (parsedate "2008/11/26")) :: T.Text -> Either (ParseError Char Dec) (Interval, DateSpan)+-- >>> p "from aug to oct"+-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)+-- >>> p "aug to oct"+-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)+-- >>> p "every 3 days in aug"+-- Right (Days 3,DateSpan 2008/08)+-- >>> p "daily from aug"+-- Right (Days 1,DateSpan 2008/08/01-)+-- >>> p "every week to 2009"+-- Right (Weeks 1,DateSpan -2008/12/31)+periodexpr :: Day -> Parser (Interval, DateSpan) periodexpr rdate = choice $ map try [                     intervalanddateperiodexpr rdate,                     intervalperiodexpr,@@ -623,7 +689,7 @@                     (return (NoInterval,DateSpan Nothing Nothing))                    ] -intervalanddateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan)+intervalanddateperiodexpr :: Day -> Parser (Interval, DateSpan) intervalanddateperiodexpr rdate = do   many spacenonewline   i <- reportinginterval@@ -631,20 +697,20 @@   s <- periodexprdatespan rdate   return (i,s) -intervalperiodexpr :: Stream [Char] m Char => ParsecT [Char] st m (Interval, DateSpan)+intervalperiodexpr :: Parser (Interval, DateSpan) intervalperiodexpr = do   many spacenonewline   i <- reportinginterval   return (i, DateSpan Nothing Nothing) -dateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan)+dateperiodexpr :: Day -> Parser (Interval, DateSpan) dateperiodexpr rdate = do   many spacenonewline   s <- periodexprdatespan rdate   return (NoInterval, s)  -- Parse a reporting interval.-reportinginterval :: Stream [Char] m Char => ParsecT [Char] st m Interval+reportinginterval :: Parser Interval reportinginterval = choice' [                        tryinterval "day"     "daily"     Days,                        tryinterval "week"    "weekly"    Weeks,@@ -657,7 +723,7 @@                           return $ Months 2,                        do string "every"                           many spacenonewline-                          n <- fmap read $ many1 digit+                          n <- fmap read $ some digitChar                           thsuffix                           many spacenonewline                           string "day"@@ -668,7 +734,7 @@                           return $ DayOfWeek n,                        do string "every"                           many spacenonewline-                          n <- fmap read $ many1 digit+                          n <- fmap read $ some digitChar                           thsuffix                           many spacenonewline                           string "day"@@ -684,7 +750,7 @@       thsuffix = choice' $ map string ["st","nd","rd","th"]        -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".-      tryinterval :: Stream [Char] m Char => String -> String -> (Int -> Interval) -> ParsecT [Char] st m Interval+      tryinterval :: String -> String -> (Int -> Interval) -> Parser Interval       tryinterval singular compact intcons =           choice' [            do string compact@@ -695,14 +761,14 @@               return $ intcons 1,            do string "every"               many spacenonewline-              n <- fmap read $ many1 digit+              n <- fmap read $ some digitChar               many spacenonewline               string plural               return $ intcons n            ]           where plural = singular ++ "s" -periodexprdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan+periodexprdatespan :: Day -> Parser DateSpan periodexprdatespan rdate = choice $ map try [                             doubledatespan rdate,                             fromdatespan rdate,@@ -710,7 +776,7 @@                             justdatespan rdate                            ] -doubledatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan+doubledatespan :: Day -> Parser DateSpan doubledatespan rdate = do   optional (string "from" >> many spacenonewline)   b <- smartdate@@ -719,7 +785,7 @@   e <- smartdate   return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e) -fromdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan+fromdatespan :: Day -> Parser DateSpan fromdatespan rdate = do   b <- choice [     do@@ -733,13 +799,13 @@     ]   return $ DateSpan (Just $ fixSmartDate rdate b) Nothing -todatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan+todatespan :: Day -> Parser DateSpan todatespan rdate = do   choice [string "to", string "-"] >> many spacenonewline   e <- smartdate   return $ DateSpan Nothing (Just $ fixSmartDate rdate e) -justdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan+justdatespan :: Day -> Parser DateSpan justdatespan rdate = do   optional (string "in" >> many spacenonewline)   d <- smartdate@@ -754,103 +820,4 @@ nulldatespan = DateSpan Nothing Nothing  nulldate :: Day-nulldate = parsedate "0000/01/01"--tests_Hledger_Data_Dates = TestList- [--   "parsedate" ~: do-    let date1 = parsedate "2008/11/26"-    parsedate "2008/02/03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1-    parsedate "2008-02-03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1--  ,"period expressions" ~: do-    let todaysdate = parsedate "2008/11/26"-    let str `gives` result = show (parsewith (periodexpr todaysdate) str) `is` ("Right " ++ result)-    "from aug to oct"           `gives` "(NoInterval,DateSpan \"Just 2008-08-01\" \"Just 2008-10-01\")"-    "aug to oct"                `gives` "(NoInterval,DateSpan \"Just 2008-08-01\" \"Just 2008-10-01\")"-    "every 3 days in aug"       `gives` "(Days 3,DateSpan \"Just 2008-08-01\" \"Just 2008-09-01\")"-    "daily from aug"            `gives` "(Days 1,DateSpan \"Just 2008-08-01\" \"Nothing\")"-    "every week to 2009"        `gives` "(Weeks 1,DateSpan \"Nothing\" \"Just 2009-01-01\")"--  ,"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"]-    (Quarters 1,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"-     ]-    (Quarters 1,nulldatespan) `gives`-     [nulldatespan]-    (Days 1,mkdatespan "2008/01/01" "2008/01/01") `gives`-     [mkdatespan "2008/01/01" "2008/01/01"]-    (Quarters 1,mkdatespan "2008/01/01" "2008/01/01") `gives`-     [mkdatespan "2008/01/01" "2008/01/01"]-    (Months 1,mkdatespan "2008/01/01" "2008/04/01") `gives`-     [mkdatespan "2008/01/01" "2008/02/01"-     ,mkdatespan "2008/02/01" "2008/03/01"-     ,mkdatespan "2008/03/01" "2008/04/01"-     ]-    (Months 2,mkdatespan "2008/01/01" "2008/04/01") `gives`-     [mkdatespan "2008/01/01" "2008/03/01"-     ,mkdatespan "2008/03/01" "2008/05/01"-     ]-    (Weeks 1,mkdatespan "2008/01/01" "2008/01/15") `gives`-     [mkdatespan "2007/12/31" "2008/01/07"-     ,mkdatespan "2008/01/07" "2008/01/14"-     ,mkdatespan "2008/01/14" "2008/01/21"-     ]-    (Weeks 2,mkdatespan "2008/01/01" "2008/01/15") `gives`-     [mkdatespan "2007/12/31" "2008/01/14"-     ,mkdatespan "2008/01/14" "2008/01/28"-     ]-    (DayOfMonth 2,mkdatespan "2008/01/01" "2008/04/01") `gives`-     [mkdatespan "2008/01/02" "2008/02/02"-     ,mkdatespan "2008/02/02" "2008/03/02"-     ,mkdatespan "2008/03/02" "2008/04/02"-     ]-    (DayOfWeek 2,mkdatespan "2011/01/01" "2011/01/15") `gives`-     [mkdatespan "2011/01/04" "2011/01/11"-     ,mkdatespan "2011/01/11" "2011/01/18"-     ]--  ,"fixSmartDateStr" ~: do-    let gives = is . fixSmartDateStr (parsedate "2008/11/26")-    "0000-01-01"   `gives` "0000/01/01"-    "1999-12-02"   `gives` "1999/12/02"-    "1999.12.02"   `gives` "1999/12/02"-    "1999/3/2"     `gives` "1999/03/02"-    "19990302"     `gives` "1999/03/02"-    "2008/2"       `gives` "2008/02/01"-    "0020/2"       `gives` "0020/02/01"-    "1000"         `gives` "1000/01/01"-    "4/2"          `gives` "2008/04/02"-    "2"            `gives` "2008/11/02"-    "January"      `gives` "2008/01/01"-    "feb"          `gives` "2008/02/01"-    "today"        `gives` "2008/11/26"-    "yesterday"    `gives` "2008/11/25"-    "tomorrow"     `gives` "2008/11/27"-    "this day"     `gives` "2008/11/26"-    "last day"     `gives` "2008/11/25"-    "next day"     `gives` "2008/11/27"-    "this week"    `gives` "2008/11/24" -- last monday-    "last week"    `gives` "2008/11/17" -- previous monday-    "next week"    `gives` "2008/12/01" -- next monday-    "this month"   `gives` "2008/11/01"-    "last month"   `gives` "2008/10/01"-    "next month"   `gives` "2008/12/01"-    "this quarter" `gives` "2008/10/01"-    "last quarter" `gives` "2008/07/01"-    "next quarter" `gives` "2009/01/01"-    "this year"    `gives` "2008/01/01"-    "last year"    `gives` "2007/01/01"-    "next year"    `gives` "2009/01/01"---     "last wed"     `gives` "2008/11/19"---     "next friday"  `gives` "2008/11/28"---     "next january" `gives` "2009/01/01"-- ]+nulldate = fromGregorian 0 1 1
Hledger/Data/Journal.hs view
@@ -1,4 +1,4 @@--- {-# LANGUAGE CPP #-}+{-# LANGUAGE StandaloneDeriving, OverloadedStrings #-} {-|  A 'Journal' is a set of transactions, plus optional related data.  This is@@ -12,11 +12,11 @@   addMarketPrice,   addModifierTransaction,   addPeriodicTransaction,-  addTimeLogEntry,   addTransaction,   journalApplyAliases,   journalBalanceTransactions,-  journalCanonicaliseAmounts,+  journalApplyCommodityStyles,+  commodityStylesFromAmounts,   journalConvertAmountsToCost,   journalFinalise,   -- * Filtering@@ -24,6 +24,7 @@   filterJournalPostings,   filterJournalAmounts,   filterTransactionAmounts,+  filterTransactionPostings,   filterPostingAmount,   -- * Querying   journalAccountNames,@@ -49,10 +50,12 @@   journalEquityAccountQuery,   journalCashAccountQuery,   -- * Misc-  canonicalStyles,+  canonicalStyleFrom,   matchpats,-  nullctx,   nulljournal,+  journalCheckBalanceAssertions,+  journalNumberAndTieTransactions,+  journalUntieTransactions,   -- * Tests   samplejournal,   tests_Hledger_Data_Journal,@@ -62,12 +65,13 @@ import Data.List -- import Data.Map (findWithDefault) import Data.Maybe+import Data.Monoid import Data.Ord-import Safe (headMay)+import Data.Text (Text)+import qualified Data.Text as T+import Safe (headMay, headDef) import Data.Time.Calendar-import Data.Time.LocalTime import Data.Tree-import Safe (headDef) import System.Time (ClockTime(TOD)) import Test.HUnit import Text.Printf@@ -81,10 +85,14 @@ import Hledger.Data.Dates import Hledger.Data.Transaction import Hledger.Data.Posting-import Hledger.Data.TimeLog import Hledger.Query  +-- try to make Journal ppShow-compatible+-- instance Show ClockTime where+--   show t = "<ClockTime>"+-- deriving instance Show Journal+ instance Show Journal where   show j     | debugLevel < 3 = printf "Journal %s with %d transactions, %d accounts"@@ -107,7 +115,7 @@               length (jperiodictxns j))              (length accounts)              (show accounts)-             (show $ jcommoditystyles j)+             (show $ jinferredcommodities j)              -- ++ (show $ journalTransactions l)              where accounts = filter (/= "root") $ flatten $ journalAccountNameTree j @@ -116,37 +124,73 @@ --                      ,show (jtxns j) --                      ,show (jmodifiertxns j) --                      ,show (jperiodictxns j)---                      ,show $ open_timelog_entries j+--                      ,show $ jparsetimeclockentries j --                      ,show $ jmarketprices j---                      ,show $ final_comment_lines j---                      ,show $ jContext j---                      ,show $ map fst $ files j+--                      ,show $ jfinalcommentlines j+--                      ,show $ jparsestate j+--                      ,show $ map fst $ jfiles j --                      ] -nulljournal :: Journal-nulljournal = Journal { jmodifiertxns = []-                      , jperiodictxns = []-                      , jtxns = []-                      , open_timelog_entries = []-                      , jmarketprices = []-                      , final_comment_lines = []-                      , jContext = nullctx-                      , files = []-                      , filereadtime = TOD 0 0-                      , jcommoditystyles = M.fromList []-                      }+-- The monoid instance for Journal is useful for two situations.+-- +-- 1. concatenating finalised journals, eg with multiple -f options:+-- FIRST <> SECOND. The second's list fields are appended to the+-- first's, map fields are combined, transaction counts are summed,+-- the parse state of the second is kept.+-- +-- 2. merging a child parsed journal, eg with the include directive:+-- CHILD <> PARENT. A parsed journal's data is in reverse order, so+-- this gives what we want.+--+instance Monoid Journal where+  mempty = nulljournal+  mappend j1 j2 = Journal {+     jparsedefaultyear          = jparsedefaultyear          j2+    ,jparsedefaultcommodity     = jparsedefaultcommodity     j2+    ,jparseparentaccounts       = jparseparentaccounts       j2+    ,jparsealiases              = jparsealiases              j2+    -- ,jparsetransactioncount     = jparsetransactioncount     j1 +  jparsetransactioncount     j2+    ,jparsetimeclockentries = jparsetimeclockentries j1 <> jparsetimeclockentries j2+    ,jaccounts                  = jaccounts                  j1 <> jaccounts                  j2+    ,jcommodities               = jcommodities               j1 <> jcommodities               j2+    ,jinferredcommodities       = jinferredcommodities       j1 <> jinferredcommodities       j2+    ,jmarketprices              = jmarketprices              j1 <> jmarketprices              j2+    ,jmodifiertxns              = jmodifiertxns              j1 <> jmodifiertxns              j2+    ,jperiodictxns              = jperiodictxns              j1 <> jperiodictxns              j2+    ,jtxns                      = jtxns                      j1 <> jtxns                      j2+    ,jfinalcommentlines         = jfinalcommentlines         j2+    ,jfiles                     = jfiles                     j1 <> jfiles                     j2+    ,jlastreadtime              = max (jlastreadtime j1) (jlastreadtime j2)+    } -nullctx :: JournalContext-nullctx = Ctx{ctxYear=Nothing, ctxDefaultCommodityAndStyle=Nothing, ctxAccount=[], ctxAliases=[], ctxTransactionIndex=0}+nulljournal :: Journal+nulljournal = Journal {+   jparsedefaultyear          = Nothing+  ,jparsedefaultcommodity     = Nothing+  ,jparseparentaccounts       = []+  ,jparsealiases              = []+  -- ,jparsetransactioncount     = 0+  ,jparsetimeclockentries = []+  ,jaccounts                  = []+  ,jcommodities               = M.fromList []+  ,jinferredcommodities       = M.fromList []+  ,jmarketprices              = []+  ,jmodifiertxns              = []+  ,jperiodictxns              = []+  ,jtxns                      = []+  ,jfinalcommentlines         = ""+  ,jfiles                     = []+  ,jlastreadtime              = TOD 0 0+  }  journalFilePath :: Journal -> FilePath journalFilePath = fst . mainfile  journalFilePaths :: Journal -> [FilePath]-journalFilePaths = map fst . files+journalFilePaths = map fst . jfiles -mainfile :: Journal -> (FilePath, String)-mainfile = headDef ("", "") . files+mainfile :: Journal -> (FilePath, Text)+mainfile = headDef ("", "") . jfiles  addTransaction :: Transaction -> Journal -> Journal addTransaction t j = j { jtxns = t : jtxns j }@@ -160,9 +204,6 @@ addMarketPrice :: MarketPrice -> Journal -> Journal addMarketPrice h j = j { jmarketprices = h : jmarketprices j } -addTimeLogEntry :: TimeLogEntry -> Journal -> Journal-addTimeLogEntry tle j = j { open_timelog_entries = tle : open_timelog_entries j }- -- | Get the transaction with this index (its 1-based position in the input stream), if any. journalTransactionAt :: Journal -> Integer -> Maybe Transaction journalTransactionAt Journal{jtxns=ts} i =@@ -172,13 +213,13 @@ -- | Get the transaction that appeared immediately after this one in the input stream, if any. journalNextTransaction :: Journal -> Transaction -> Maybe Transaction journalNextTransaction j t = journalTransactionAt j (tindex t + 1)-  + -- | Get the transaction that appeared immediately before this one in the input stream, if any. journalPrevTransaction :: Journal -> Transaction -> Maybe Transaction journalPrevTransaction j t = journalTransactionAt j (tindex t - 1)-  + -- | Unique transaction descriptions used in this journal.-journalDescriptions :: Journal -> [String]+journalDescriptions :: Journal -> [Text] journalDescriptions = nub . sort . map tdescription . jtxns  -- | All postings from this journal's transactions, in order.@@ -258,9 +299,7 @@ -- | Keep only postings matching the query expression. -- This can leave unbalanced transactions. filterJournalPostings :: Query -> Journal -> Journal-filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map filtertransactionpostings ts}-    where-      filtertransactionpostings t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps}+filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionPostings q) ts}  -- | Within each posting's amount, keep only the parts matching the query. -- This can leave unbalanced transactions.@@ -276,6 +315,10 @@ filterPostingAmount :: Query -> Posting -> Posting filterPostingAmount q p@Posting{pamount=Mixed as} = p{pamount=Mixed $ filter (q `matchesAmount`) as} +filterTransactionPostings :: Query -> Transaction -> Transaction+filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps}++ {- ------------------------------------------------------------------------------- -- filtering V1@@ -414,25 +457,40 @@       dotransaction t@Transaction{tpostings=ps} = t{tpostings=map doposting ps}       doposting p@Posting{paccount=a} = p{paccount= accountNameApplyAliases aliases a} --- | Do post-parse processing on a journal to make it ready for use: check--- all transactions balance, canonicalise amount formats, close any open--- timelog entries, maybe check balance assertions and so on.-journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Bool -> Journal -> Either String Journal-journalFinalise tclock tlocal path txt ctx assrt j@Journal{files=fs} = do-  (journalBalanceTransactions $-    journalCanonicaliseAmounts $-    journalCloseTimeLogEntries tlocal $-    j{ files=(path,txt):fs-     , filereadtime=tclock-     , jContext=ctx-     , jtxns=reverse $ jtxns j -- NOTE: see addTransaction-     , jmodifiertxns=reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction-     , jperiodictxns=reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction-     , jmarketprices=reverse $ jmarketprices j -- NOTE: see addMarketPrice-     , open_timelog_entries=reverse $ open_timelog_entries j -- NOTE: see addTimeLogEntry-     })+-- | Do post-parse processing on a parsed journal to make it ready for+-- use.  Reverse parsed data to normal order, canonicalise amount+-- formats, check/ensure that transactions are balanced, and maybe+-- check balance assertions.+journalFinalise :: ClockTime -> FilePath -> Text -> Bool -> ParsedJournal -> Either String Journal+journalFinalise t path txt assrt j@Journal{jfiles=fs} = do+  (journalNumberAndTieTransactions <$>+    (journalBalanceTransactions $+    journalApplyCommodityStyles $+    j{ jfiles        = (path,txt) : reverse fs+     , jlastreadtime = t+     , jtxns         = reverse $ jtxns j -- NOTE: see addTransaction+     , jmodifiertxns = reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction+     , jperiodictxns = reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction+     , jmarketprices = reverse $ jmarketprices j -- NOTE: see addMarketPrice+     }))   >>= if assrt then journalCheckBalanceAssertions else return +journalNumberAndTieTransactions = journalTieTransactions . journalNumberTransactions++-- | Number (set the tindex field) this journal's transactions, counting upward from 1.+journalNumberTransactions :: Journal -> Journal+journalNumberTransactions j@Journal{jtxns=ts} = j{jtxns=map (\(i,t) -> t{tindex=i}) $ zip [1..] ts}++-- | Tie the knot in all of this journal's transactions, ensuring their postings+-- refer to them. This should be done last, after any other transaction-modifying operations.+journalTieTransactions :: Journal -> Journal+journalTieTransactions j@Journal{jtxns=ts} = j{jtxns=map txnTieKnot ts}++-- | Untie all transaction-posting knots in this journal, so that eg+-- recursiveSize and GHCI's :sprint can work on it.+journalUntieTransactions :: Transaction -> Transaction+journalUntieTransactions t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}+ -- | Check any balance assertions in the journal and return an error -- message if any of them fail. journalCheckBalanceAssertions :: Journal -> Either String Journal@@ -462,26 +520,47 @@ checkBalanceAssertion :: ([String],MixedAmount) -> [Posting] -> ([String],MixedAmount) checkBalanceAssertion (errs,startbal) ps   | null ps = (errs,startbal)-  | isNothing assertion = (errs,startbal)-  | -- bal' /= assertedbal  -- MixedAmount's Eq instance currently gets confused by different precisions-    not $ isReallyZeroMixedAmount (bal - assertedbal) = (errs++[err], fullbal)-  | otherwise = (errs,fullbal)+  | isNothing $ pbalanceassertion p = (errs,startbal)+  | iswrong = (errs++[err], finalfullbal)+  | otherwise = (errs,finalfullbal)   where     p = last ps-    assertion = pbalanceassertion p-    Just assertedbal = dbg2 "assertedbal" assertion-    assertedcomm = dbg2 "assertedcomm" $ maybe "" acommodity $ headMay $ amounts assertedbal-    fullbal = dbg2 "fullbal" $ sum $ [dbg2 "startbal" startbal] ++ map pamount ps-    singlebal = dbg2 "singlebal" $ filterMixedAmount (\a -> acommodity a == assertedcomm) fullbal-    bal = singlebal -- check single-commodity balance like Ledger; maybe add == FULLBAL later-    err = printf "Balance assertion failed for account %s on %s\n%sAfter posting:\n   %s\nexpected balance in commodity \"%s\" is %s, calculated balance was %s."-                 (paccount p)-                 (show $ postingDate p)-                 (maybe "" (("In transaction:\n"++).show) $ ptransaction p)-                 (show p)+    Just assertedbal = pbalanceassertion p+    assertedcomm = maybe "" acommodity $ headMay $ amounts assertedbal+    finalfullbal = sum $ [startbal] ++ map pamount (dbg2 "ps" ps)+    finalsinglebal = filterMixedAmount (\a -> acommodity a == assertedcomm) finalfullbal+    actualbal = finalsinglebal -- just check the single-commodity balance, like Ledger; maybe add ==FULLBAL later+    iswrong = dbg2 debugmsg $+      not (isReallyZeroMixedAmount (actualbal - assertedbal))+      -- bal' /= assertedbal  -- MixedAmount's Eq instance currently gets confused by different precisions+      where+        debugmsg = "assertions: on " ++ show (postingDate p) ++ " balance of " ++ show assertedcomm+                    ++ " in " ++ T.unpack (paccount p) ++ " should be " ++ show assertedbal+    diff = assertedbal - actualbal+    diffplus | isNegativeMixedAmount diff == Just False = "+"+             | otherwise = ""+    err = printf (unlines [+                      "balance assertion error%s",+                      "after posting:",+                      "%s",+                      "balance assertion details:",+                      "date:       %s",+                      "account:    %s",+                      "commodity:  %s",+                      "calculated: %s",+                      "asserted:   %s (difference: %s)"+                      ])+                 (case ptransaction p of+                    Nothing -> ":" -- shouldn't happen+                    Just t ->  printf " in \"%s\" (line %d, column %d):\nin transaction:\n%s" f l c (chomp $ show t) :: String+                      where GenericSourcePos f l c = tsourcepos t)+                 (showPostingLine p)+                 (showDate $ postingDate p)+                 (T.unpack $ paccount p) -- XXX pack                  assertedcomm+                 (showMixedAmount finalsinglebal)                  (showMixedAmount assertedbal)-                 (showMixedAmount singlebal)+                 (diffplus ++ showMixedAmount diff)  -- Given a sequence of postings to a single account, split it into -- sub-sequences consisting of ordinary postings followed by a single@@ -496,55 +575,77 @@  -- | Fill in any missing amounts and check that all journal transactions -- balance, or return an error message. This is done after parsing all--- amounts and working out the canonical commodities, since balancing+-- amounts and applying canonical commodity styles, since balancing -- depends on display precision. Reports only the first error encountered. journalBalanceTransactions :: Journal -> Either String Journal-journalBalanceTransactions j@Journal{jtxns=ts, jcommoditystyles=ss} =-  case sequence $ map balance ts of Right ts' -> Right j{jtxns=map txnTieKnot ts'}+journalBalanceTransactions j@Journal{jtxns=ts, jinferredcommodities=ss} =+  case sequence $ map balance ts of Right ts' -> Right j{jtxns=ts'}                                     Left e    -> Left e       where balance = balanceTransaction (Just ss) --- | Convert all the journal's posting amounts (and historical price--- amounts, but currently not transaction price 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 posting amounts in that commodity.-journalCanonicaliseAmounts :: Journal -> Journal-journalCanonicaliseAmounts j@Journal{jtxns=ts, jmarketprices=mps} = j''+-- | Choose and apply a consistent display format to the posting+-- amounts in each commodity. Each commodity's format is specified by+-- a commodity format directive, or otherwise inferred from posting+-- amounts as in hledger < 0.28.+journalApplyCommodityStyles :: Journal -> Journal+journalApplyCommodityStyles j@Journal{jtxns=ts, jmarketprices=mps} = j''     where+      j' = journalInferCommodityStyles j       j'' = j'{jtxns=map fixtransaction ts, jmarketprices=map fixmarketprice mps}-      j' = j{jcommoditystyles = canonicalStyles $ dbg8 "journalAmounts" $ journalAmounts j}       fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps}       fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a}       fixmarketprice mp@MarketPrice{mpamount=a} = mp{mpamount=fixamount a}       fixmixedamount (Mixed as) = Mixed $ map fixamount as       fixamount a@Amount{acommodity=c} = a{astyle=journalCommodityStyle j' c} --- | Given a list of amounts in parse order, build a map from commodities--- to canonical display styles for amounts in that commodity.-canonicalStyles :: [Amount] -> M.Map Commodity AmountStyle-canonicalStyles amts = M.fromList commstyles+-- | Get this journal's standard display style for the given+-- commodity.  That is the style defined by the last corresponding+-- commodity format directive if any, otherwise the style inferred+-- from the posting amounts (or in some cases, price amounts) in this+-- commodity if any, otherwise the default style.+journalCommodityStyle :: Journal -> CommoditySymbol -> AmountStyle+journalCommodityStyle j c =+  headDef amountstyle{asprecision=2} $+  catMaybes [+     M.lookup c (jcommodities j) >>= cformat+    ,M.lookup c $ jinferredcommodities j+    ]++-- | Infer a display format for each commodity based on the amounts parsed.+-- "hledger... will use the format of the first posting amount in the+-- commodity, and the highest precision of all posting amounts in the commodity."+journalInferCommodityStyles :: Journal -> Journal+journalInferCommodityStyles j =+  j{jinferredcommodities =+        commodityStylesFromAmounts $+        dbg8 "journalChooseCommmodityStyles using amounts" $ journalAmounts j}++-- | Given a list of amounts in parse order, build a map from their commodity names+-- to standard commodity display formats.+commodityStylesFromAmounts :: [Amount] -> M.Map CommoditySymbol AmountStyle+commodityStylesFromAmounts amts = M.fromList commstyles   where     samecomm = \a1 a2 -> acommodity a1 == acommodity a2     commamts = [(acommodity $ head as, as) | as <- groupBy samecomm $ sortBy (comparing acommodity) amts]     commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts] --- Given an ordered list of amount styles for a commodity, build a canonical style.+-- | Given an ordered list of amount styles, choose a canonical style.+-- That is: the style of the first, and the+-- maximum precision of all. canonicalStyleFrom :: [AmountStyle] -> AmountStyle canonicalStyleFrom [] = amountstyle canonicalStyleFrom ss@(first:_) =   first{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}   where-    -- precision is the maximum of all precisions seen+    mgrps = maybe Nothing Just $ headMay $ catMaybes $ map asdigitgroups ss+    -- precision is maximum of all precisions     prec = maximum $ map asprecision ss-    -- find the first decimal point and the first digit group style seen,-    -- or use defaults.     mdec  = Just $ headDef '.' $ catMaybes $ map asdecimalpoint ss-    mgrps = maybe Nothing Just $ headMay $ catMaybes $ map asdigitgroups ss---- | Get this journal's canonical amount style for the given commodity, or the null style.-journalCommodityStyle :: Journal -> Commodity -> AmountStyle-journalCommodityStyle j c = M.findWithDefault amountstyle c $ jcommoditystyles j+    -- precision is that of first amount with a decimal point+    -- (mdec, prec) =+    --   case filter (isJust . asdecimalpoint) ss of+    --   (s:_) -> (asdecimalpoint s, asprecision s)+    --   []    -> (Just '.', 0)  -- -- | Apply this journal's historical price records to unpriced amounts where possible. -- journalApplyMarketPrices :: Journal -> Journal@@ -560,41 +661,36 @@  -- -- | 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.--- journalMarketPriceFor :: Journal -> Day -> Commodity -> Maybe MixedAmount--- journalMarketPriceFor j d Commodity{symbol=s} = do+-- journalMarketPriceFor :: Journal -> Day -> CommoditySymbol -> Maybe MixedAmount+-- journalMarketPriceFor j d CommoditySymbol{symbol=s} = do --   let ps = reverse $ filter ((<= d).mpdate) $ filter ((s==).hsymbol) $ sortBy (comparing mpdate) $ jmarketprices j --   case ps of (MarketPrice{mpamount=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-      -- similar to journalCanonicaliseAmounts+      -- similar to journalApplyCommodityStyles       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 = canonicaliseAmount (jcommoditystyles j) . costOfAmount+      fixamount = canonicaliseAmount (jinferredcommodities j) . costOfAmount  -- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol.--- journalCanonicalCommodities :: Journal -> M.Map String Commodity+-- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol -- journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j  -- -- | Get all this journal's amounts' commodities, in the order parsed.--- journalAmountCommodities :: Journal -> [Commodity]+-- journalAmountCommodities :: Journal -> [CommoditySymbol] -- journalAmountCommodities = map acommodity . concatMap amounts . journalAmounts  -- -- | Get all this journal's amount and price commodities, in the order parsed.--- journalAmountAndPriceCommodities :: Journal -> [Commodity]+-- journalAmountAndPriceCommodities :: Journal -> [CommoditySymbol] -- journalAmountAndPriceCommodities = concatMap amountCommodities . concatMap amounts . journalAmounts  -- -- | Get this amount's commodity and any commodities referenced in its price.--- amountCommodities :: Amount -> [Commodity]+-- amountCommodities :: Amount -> [CommoditySymbol] -- amountCommodities Amount{acommodity=c,aprice=p} = --     case p of Nothing -> [c] --               Just (UnitPrice ma)  -> c:(concatMap amountCommodities $ amounts ma)
Hledger/Data/Ledger.hs view
@@ -10,6 +10,8 @@ module Hledger.Data.Ledger where import qualified Data.Map as M+-- import Data.Text (Text)+import qualified Data.Text as T import Safe (headDef) import Test.HUnit import Text.Printf@@ -37,7 +39,7 @@  -- | Filter a journal's transactions with the given query, then derive -- a ledger containing the chart of accounts and balances. If the--- query includes a depth limit, that will affect the this ledger's+-- query includes a depth limit, that will affect the ledger's -- journal but not the ledger's account tree. ledgerFromJournal :: Query -> Journal -> Ledger ledgerFromJournal q j = nullledger{ljournal=j'', laccounts=as}@@ -72,7 +74,7 @@  -- | Accounts in ledger whose name matches the pattern, in tree order. ledgerAccountsMatching :: [String] -> Ledger -> [Account]-ledgerAccountsMatching pats = filter (matchpats pats . aname) . laccounts+ledgerAccountsMatching pats = filter (matchpats pats . T.unpack . aname) . laccounts -- XXX pack  -- | List a ledger's postings, in the order parsed. ledgerPostings :: Ledger -> [Posting]@@ -84,8 +86,8 @@ ledgerDateSpan = postingsDateSpan . ledgerPostings  -- | All commodities used in this ledger.-ledgerCommodities :: Ledger -> [Commodity]-ledgerCommodities = M.keys . jcommoditystyles . ljournal+ledgerCommodities :: Ledger -> [CommoditySymbol]+ledgerCommodities = M.keys . jinferredcommodities . ljournal   tests_ledgerFromJournal = [
+ Hledger/Data/Period.hs view
@@ -0,0 +1,262 @@+{-|++Manipulate the time periods typically used for reports with Period,+a richer abstraction than DateSpan. See also Types and Dates.++-}++module Hledger.Data.Period+where++import Data.Time.Calendar+import Data.Time.Calendar.MonthDay+import Data.Time.Calendar.OrdinalDate+import Data.Time.Calendar.WeekDate+import Data.Time.Format+import Text.Printf++import Hledger.Data.Types++-- | Convert Periods to DateSpans.+--+-- >>> periodAsDateSpan (MonthPeriod 2000 1) == DateSpan (Just $ fromGregorian 2000 1 1) (Just $ fromGregorian 2000 2 1)+-- True+periodAsDateSpan :: Period -> DateSpan+periodAsDateSpan (DayPeriod d) = DateSpan (Just d) (Just $ addDays 1 d)+periodAsDateSpan (WeekPeriod b) = DateSpan (Just b) (Just $ addDays 7 b)+periodAsDateSpan (MonthPeriod y m) = DateSpan (Just $ fromGregorian y m 1) (Just $ fromGregorian y' m' 1)+  where+    (y',m') | m==12     = (y+1,1)+            | otherwise = (y,m+1)+periodAsDateSpan (QuarterPeriod y q) = DateSpan (Just $ fromGregorian y m 1) (Just $ fromGregorian y' m' 1)+  where+    (y', q') | q==4      = (y+1,1)+             | otherwise = (y,q+1)+    quarterAsMonth q = (q-1) * 3 + 1+    m  = quarterAsMonth q+    m' = quarterAsMonth q'+periodAsDateSpan (YearPeriod y) = DateSpan (Just $ fromGregorian y 1 1) (Just $ fromGregorian (y+1) 1 1)+periodAsDateSpan (PeriodBetween b e) = DateSpan (Just b) (Just e)+periodAsDateSpan (PeriodFrom b) = DateSpan (Just b) Nothing+periodAsDateSpan (PeriodTo e) = DateSpan Nothing (Just e)+periodAsDateSpan (PeriodAll) = DateSpan Nothing Nothing++-- | Convert DateSpans to Periods.+--+-- >>> dateSpanAsPeriod $ DateSpan (Just $ fromGregorian 2000 1 1) (Just $ fromGregorian 2000 2 1)+-- MonthPeriod 2000 1+dateSpanAsPeriod :: DateSpan -> Period+dateSpanAsPeriod (DateSpan (Just b) (Just e)) = simplifyPeriod $ PeriodBetween b e+dateSpanAsPeriod (DateSpan (Just b) Nothing) = PeriodFrom b+dateSpanAsPeriod (DateSpan Nothing (Just e)) = PeriodTo e+dateSpanAsPeriod (DateSpan Nothing Nothing) = PeriodAll++-- | Convert PeriodBetweens to a more abstract period where possible.+--+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 1 1 1) (fromGregorian 2 1 1)+-- YearPeriod 1+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 10 1) (fromGregorian 2001 1 1)+-- QuarterPeriod 2000 4+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 1) (fromGregorian 2000 3 1)+-- MonthPeriod 2000 2+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2016 7 25) (fromGregorian 2016 8 1)+-- WeekPeriod 2016-07-25+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 1 1) (fromGregorian 2000 1 2)+-- DayPeriod 2000-01-01+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 28) (fromGregorian 2000 3 1)+-- PeriodBetween 2000-02-28 2000-03-01+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 29) (fromGregorian 2000 3 1)+-- DayPeriod 2000-02-29+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 12 31) (fromGregorian 2001 1 1)+-- DayPeriod 2000-12-31+--+simplifyPeriod :: Period -> Period+simplifyPeriod (PeriodBetween b e) =+  case (toGregorian b, toGregorian e) of+    -- a year+    ((by,1,1), (ey,1,1))   | by+1==ey           -> YearPeriod by+    -- a half-year+    -- ((by,1,1), (ey,7,1))   | by==ey             ->+    -- ((by,7,1), (ey,1,1))   | by+1==ey           ->+    -- a quarter+    ((by,1,1), (ey,4,1))   | by==ey             -> QuarterPeriod by 1+    ((by,4,1), (ey,7,1))   | by==ey             -> QuarterPeriod by 2+    ((by,7,1), (ey,10,1))  | by==ey             -> QuarterPeriod by 3+    ((by,10,1), (ey,1,1))  | by+1==ey           -> QuarterPeriod by 4+    -- a month+    ((by,bm,1), (ey,em,1)) | by==ey && bm+1==em -> MonthPeriod by bm+    ((by,12,1), (ey,1,1))  | by+1==ey           -> MonthPeriod by 12+    -- a week (two successive mondays),+    -- YYYYwN ("week N of year YYYY")+    -- _ | let ((by,bw,bd), (ey,ew,ed)) = (toWeekDate from, toWeekDate to) in by==ey && fw+1==tw && bd==1 && ed==1 ->+    -- a week starting on a monday+    _ | let ((by,bw,bd), (ey,ew,ed)) = (toWeekDate b, toWeekDate (addDays (-1) e))+        in by==ey && bw==ew && bd==1 && ed==7   -> WeekPeriod b+    -- a day+    ((by,bm,bd), (ey,em,ed)) |+        (by==ey && bm==em && bd+1==ed) ||+        (by+1==ey && bm==12 && em==1 && bd==31 && ed==1) || -- crossing a year boundary+        (by==ey && bm+1==em && isLastDayOfMonth by bm bd && ed==1) -- crossing a month boundary+         -> DayPeriod b+    _ -> PeriodBetween b e+simplifyPeriod p = p++isLastDayOfMonth y m d =+  case m of+    1 -> d==31+    2 | isLeapYear y -> d==29+      | otherwise    -> d==28+    3 -> d==31+    4 -> d==30+    5 -> d==31+    6 -> d==30+    7 -> d==31+    8 -> d==31+    9 -> d==30+    10 -> d==31+    11 -> d==30+    12 -> d==31+    _ -> False++-- | Render a period as a compact display string suitable for user output.+--+-- >>> showPeriod (WeekPeriod (fromGregorian 2016 7 25))+-- "2016/07/25w30"+showPeriod (DayPeriod b)       = formatTime defaultTimeLocale "%0C%y/%m/%d" b     -- DATE+showPeriod (WeekPeriod b)      = formatTime defaultTimeLocale "%0C%y/%m/%dw%V" b  -- STARTDATEwYEARWEEK+showPeriod (MonthPeriod y m)   = printf "%04d/%02d" y m                           -- YYYY/MM+showPeriod (QuarterPeriod y q) = printf "%04dq%d" y q                             -- YYYYqN+showPeriod (YearPeriod y)      = printf "%04d" y                                  -- YYYY+showPeriod (PeriodBetween b e) = formatTime defaultTimeLocale "%0C%y/%m/%d" b+                                 ++ formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e) -- STARTDATE-INCLUSIVEENDDATE+showPeriod (PeriodFrom b)      = formatTime defaultTimeLocale "%0C%y/%m/%d-" b                   -- STARTDATE-+showPeriod (PeriodTo e)        = formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e)    -- -INCLUSIVEENDDATE+showPeriod PeriodAll           = "-"++periodStart :: Period -> Maybe Day+periodStart p = mb+  where+    DateSpan mb _ = periodAsDateSpan p++periodEnd :: Period -> Maybe Day+periodEnd p = me+  where+    DateSpan _ me = periodAsDateSpan p++-- | Move a period to the following period of same duration.+periodNext :: Period -> Period+periodNext (DayPeriod b) = DayPeriod (addDays 1 b)+periodNext (WeekPeriod b) = WeekPeriod (addDays 7 b)+periodNext (MonthPeriod y 12) = MonthPeriod (y+1) 1+periodNext (MonthPeriod y m) = MonthPeriod y (m+1)+periodNext (QuarterPeriod y 4) = QuarterPeriod (y+1) 1+periodNext (QuarterPeriod y q) = QuarterPeriod y (q+1)+periodNext (YearPeriod y) = YearPeriod (y+1)+periodNext p = p++-- | Move a period to the preceding period of same duration.+periodPrevious :: Period -> Period+periodPrevious (DayPeriod b) = DayPeriod (addDays (-1) b)+periodPrevious (WeekPeriod b) = WeekPeriod (addDays (-7) b)+periodPrevious (MonthPeriod y 1) = MonthPeriod (y-1) 12+periodPrevious (MonthPeriod y m) = MonthPeriod y (m-1)+periodPrevious (QuarterPeriod y 1) = QuarterPeriod (y-1) 4+periodPrevious (QuarterPeriod y q) = QuarterPeriod y (q-1)+periodPrevious (YearPeriod y) = YearPeriod (y-1)+periodPrevious p = p++-- | Move a period to the following period of same duration, staying within enclosing dates.+periodNextIn :: DateSpan -> Period -> Period+periodNextIn (DateSpan _ (Just e)) p =+  case mb of+    Just b -> if b < e then p' else p+    _      -> p+  where+    p' = periodNext p+    mb = periodStart p'+periodNextIn _ p = periodNext p++-- | Move a period to the preceding period of same duration, staying within enclosing dates.+periodPreviousIn :: DateSpan -> Period -> Period+periodPreviousIn (DateSpan (Just b) _) p =+  case me of+    Just e -> if e > b then p' else p+    _      -> p+  where+    p' = periodPrevious p+    me = periodEnd p'+periodPreviousIn _ p = periodPrevious p++-- | Enlarge a standard period to the next larger enclosing standard period, if there is one.+-- Eg, a day becomes the enclosing week.+-- A week becomes whichever month the week's thursday falls into.+-- A year becomes all (unlimited).+-- Growing an unlimited period, or a non-standard period (arbitrary dates) has no effect.+periodGrow :: Period -> Period+periodGrow (DayPeriod b) = WeekPeriod $ mondayBefore b+periodGrow (WeekPeriod b) = MonthPeriod y m+  where (y,m) = yearMonthContainingWeekStarting b+periodGrow (MonthPeriod y m) = QuarterPeriod y (quarterContainingMonth m)+periodGrow (QuarterPeriod y _) = YearPeriod y+periodGrow (YearPeriod _) = PeriodAll+periodGrow p = p++-- | Shrink a period to the next smaller standard period inside it,+-- choosing the subperiod which contains today's date if possible,+-- otherwise the first subperiod. It goes like this:+-- unbounded periods and nonstandard periods (between two arbitrary dates) ->+-- current year ->+-- current quarter if it's in selected year, otherwise first quarter of selected year ->+-- current month if it's in selected quarter, otherwise first month of selected quarter ->+-- current week if it's in selected month, otherwise first week of selected month ->+-- today if it's in selected week, otherwise first day of selected week,+--  unless that's in previous month, in which case first day of month containing selected week.+-- Shrinking a day has no effect.+periodShrink :: Day -> Period -> Period+periodShrink _     p@(DayPeriod _) = p+periodShrink today (WeekPeriod b)+  | today >= b && diffDays today b < 7 = DayPeriod today+  | m /= weekmonth                     = DayPeriod $ fromGregorian weekyear weekmonth 1+  | otherwise                          = DayPeriod b+  where+    (_,m,_) = toGregorian b+    (weekyear,weekmonth) = yearMonthContainingWeekStarting b+periodShrink today (MonthPeriod y m)+  | (y',m') == (y,m) = WeekPeriod $ mondayBefore today+  | otherwise        = WeekPeriod $ startOfFirstWeekInMonth y m+  where (y',m',_) = toGregorian today+periodShrink today (QuarterPeriod y q)+  | quarterContainingMonth thismonth == q = MonthPeriod y thismonth+  | otherwise                             = MonthPeriod y (firstMonthOfQuarter q)+  where (_,thismonth,_) = toGregorian today+periodShrink today (YearPeriod y)+  | y == thisyear = QuarterPeriod y thisquarter+  | otherwise     = QuarterPeriod y 1+  where+    (thisyear,thismonth,_) = toGregorian today+    thisquarter = quarterContainingMonth thismonth+periodShrink today _ = YearPeriod y+  where (y,_,_) = toGregorian today++mondayBefore d = addDays (fromIntegral (1 - wd)) d+  where+    (_,_,wd) = toWeekDate d++yearMonthContainingWeekStarting weekstart = (y,m)+  where+    thu = addDays 3 weekstart+    (y,yd) = toOrdinalDate thu+    (m,_) = dayOfYearToMonthAndDay (isLeapYear y) yd++quarterContainingMonth m = (m-1) `div` 3 + 1++firstMonthOfQuarter q = (q-1)*3 + 1++startOfFirstWeekInMonth y m+  | monthstartday <= 4 = mon+  | otherwise          = addDays 7 mon  -- month starts with a fri/sat/sun+  where+    monthstart = fromGregorian y m 1+    mon = mondayBefore monthstart+    (_,_,monthstartday) = toWeekDate monthstart+
Hledger/Data/Posting.hs view
@@ -7,6 +7,8 @@  -} +{-# LANGUAGE OverloadedStrings #-}+ module Hledger.Data.Posting (   -- * Posting   nullposting,@@ -50,7 +52,10 @@ import Data.List import Data.Maybe import Data.MemoUgly (memo)+import Data.Monoid import Data.Ord+import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar import Safe import Test.HUnit@@ -89,7 +94,7 @@     where       ledger3ishlayout = False       acctnamewidth = if ledger3ishlayout then 25 else 22-      showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . elideAccountName width+      showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . T.unpack . elideAccountName width       (bracket,width) = case t of                           BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)                           VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)@@ -97,8 +102,8 @@       showamount = padLeftWide 12 . showMixedAmount  -showComment :: String -> String-showComment s = if null s then "" else "  ;" ++ s+showComment :: Text -> String+showComment t = if T.null t then "" else "  ;" ++ T.unpack t  isReal :: Posting -> Bool isReal p = ptype p == RegularPosting@@ -192,32 +197,32 @@  accountNamePostingType :: AccountName -> PostingType accountNamePostingType a-    | null a = RegularPosting-    | head a == '[' && last a == ']' = BalancedVirtualPosting-    | head a == '(' && last a == ')' = VirtualPosting+    | T.null a = RegularPosting+    | T.head a == '[' && T.last a == ']' = BalancedVirtualPosting+    | T.head a == '(' && T.last a == ')' = VirtualPosting     | otherwise = RegularPosting  accountNameWithoutPostingType :: AccountName -> AccountName accountNameWithoutPostingType a = case accountNamePostingType a of-                                    BalancedVirtualPosting -> init $ tail a-                                    VirtualPosting -> init $ tail a+                                    BalancedVirtualPosting -> T.init $ T.tail a+                                    VirtualPosting -> T.init $ T.tail a                                     RegularPosting -> a  accountNameWithPostingType :: PostingType -> AccountName -> AccountName-accountNameWithPostingType BalancedVirtualPosting a = "["++accountNameWithoutPostingType a++"]"-accountNameWithPostingType VirtualPosting a = "("++accountNameWithoutPostingType a++")"+accountNameWithPostingType BalancedVirtualPosting a = "["<>accountNameWithoutPostingType a<>"]"+accountNameWithPostingType VirtualPosting a = "("<>accountNameWithoutPostingType a<>")" accountNameWithPostingType RegularPosting a = accountNameWithoutPostingType a  -- | Prefix one account name to another, preserving posting type -- indicators like concatAccountNames. joinAccountNames :: AccountName -> AccountName -> AccountName-joinAccountNames a b = concatAccountNames $ filter (not . null) [a,b]+joinAccountNames a b = concatAccountNames $ filter (not . T.null) [a,b]  -- | Join account names into one. If any of them has () or [] posting type -- indicators, these (the first type encountered) will also be applied to -- the resulting account name. concatAccountNames :: [AccountName] -> AccountName-concatAccountNames as = accountNameWithPostingType t $ intercalate ":" $ map accountNameWithoutPostingType as+concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map accountNameWithoutPostingType as     where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as  -- | Rewrite an account name using all matching aliases from the given list, in sequence.@@ -241,9 +246,9 @@  aliasReplace :: AccountAlias -> AccountName -> AccountName aliasReplace (BasicAlias old new) a-  | old `isAccountNamePrefixOf` a || old == a = new ++ drop (length old) a+  | old `isAccountNamePrefixOf` a || old == a = new <> T.drop (T.length old) a   | otherwise = a-aliasReplace (RegexAlias re repl) a = regexReplaceCIMemo re repl a+aliasReplace (RegexAlias re repl) a = T.pack $ regexReplaceCIMemo re repl $ T.unpack a -- XXX   tests_Hledger_Data_Posting = TestList [
Hledger/Data/RawOptions.hs view
@@ -23,6 +23,7 @@ where  import Data.Maybe+import qualified Data.Text as T import Safe  import Hledger.Utils@@ -32,7 +33,7 @@ type RawOpts = [(String,String)]  setopt :: String -> String -> RawOpts -> RawOpts-setopt name val = (++ [(name, quoteIfNeeded val)])+setopt name val = (++ [(name, quoteIfNeeded $ val)])  setboolopt :: String -> RawOpts -> RawOpts setboolopt name = (++ [(name,"")])@@ -45,7 +46,7 @@ boolopt = inRawOpts  maybestringopt :: String -> RawOpts -> Maybe String-maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name . reverse+maybestringopt name = maybe Nothing (Just . T.unpack . stripquotes . T.pack) . lookup name . reverse  stringopt :: String -> RawOpts -> String stringopt name = fromMaybe "" . maybestringopt name
Hledger/Data/StringFormat.hs view
@@ -2,7 +2,7 @@ -- hledger's report item fields. The formats are used by -- report-specific renderers like renderBalanceReportItem. -{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleContexts, TypeFamilies #-}  module Hledger.Data.StringFormat (           parseStringFormat@@ -19,7 +19,8 @@ import Data.Char (isPrint) import Data.Maybe import Test.HUnit-import Text.Parsec+import Text.Megaparsec+import Text.Megaparsec.String  import Hledger.Utils.String (formatString) @@ -79,15 +80,15 @@  -- | Parse a string format specification, or return a parse error. parseStringFormat :: String -> Either String StringFormat-parseStringFormat input = case (runParser (stringformatp <* eof) () "(unknown)") input of+parseStringFormat input = case (runParser (stringformatp <* eof) "(unknown)") input of     Left y -> Left $ show y     Right x -> Right x  defaultStringFormatStyle = BottomAligned -stringformatp :: Stream [Char] m Char => ParsecT [Char] st m StringFormat+stringformatp :: Parser StringFormat stringformatp = do-  alignspec <- optionMaybe (try $ char '%' >> oneOf "^_,")+  alignspec <- optional (try $ char '%' >> oneOf "^_,")   let constructor =         case alignspec of           Just '^' -> TopAligned@@ -96,24 +97,24 @@           _        -> defaultStringFormatStyle   constructor <$> many componentp -componentp :: Stream [Char] m Char => ParsecT [Char] st m StringFormatComponent+componentp :: Parser StringFormatComponent componentp = formatliteralp <|> formatfieldp -formatliteralp :: Stream [Char] m Char => ParsecT [Char] st m StringFormatComponent+formatliteralp :: Parser StringFormatComponent formatliteralp = do-    s <- many1 c+    s <- some c     return $ FormatLiteral s     where       isPrintableButNotPercentage x = isPrint x && (not $ x == '%')       c =     (satisfy isPrintableButNotPercentage <?> "printable character")           <|> try (string "%%" >> return '%') -formatfieldp :: Stream [Char] m Char => ParsecT [Char] st m StringFormatComponent+formatfieldp :: Parser StringFormatComponent formatfieldp = do     char '%'-    leftJustified <- optionMaybe (char '-')-    minWidth <- optionMaybe (many1 $ digit)-    maxWidth <- optionMaybe (do char '.'; many1 $ digit) -- TODO: Can this be (char '1') *> (many1 digit)+    leftJustified <- optional (char '-')+    minWidth <- optional (some $ digitChar)+    maxWidth <- optional (do char '.'; some $ digitChar) -- TODO: Can this be (char '1') *> (some digitChar)     char '('     f <- fieldp     char ')'@@ -123,14 +124,14 @@         Just text -> Just m where ((m,_):_) = readDec text         _ -> Nothing -fieldp :: Stream [Char] m Char => ParsecT [Char] st m ReportItemField+fieldp :: Parser ReportItemField fieldp = do         try (string "account" >> return AccountField)     <|> try (string "depth_spacer" >> return DepthSpacerField)     <|> try (string "date" >> return DescriptionField)     <|> try (string "description" >> return DescriptionField)     <|> try (string "total" >> return TotalField)-    <|> try (many1 digit >>= (\s -> return $ FieldNo $ read s))+    <|> try (some digitChar >>= (\s -> return $ FieldNo $ read s))  ---------------------------------------------------------------------- 
− Hledger/Data/TimeLog.hs
@@ -1,142 +0,0 @@-{-# LANGUAGE CPP #-}-{-|--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.Maybe-import Data.Time.Calendar-import Data.Time.Clock-import Data.Time.Format-import Data.Time.LocalTime-#if !(MIN_VERSION_time(1,5,0))-import System.Locale (defaultTimeLocale)-#endif-import Test.HUnit-import Text.Printf--import Hledger.Utils-import Hledger.Data.Types-import Hledger.Data.Dates-import Hledger.Data.Amount-import Hledger.Data.Posting-import Hledger.Data.Transaction--instance Show TimeLogEntry where-    show t = printf "%s %s %s  %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription 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 journal 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 (tlsourcepos i) 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 journal--- transaction, 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 {-            tindex       = 0,-            tsourcepos   = tlsourcepos i,-            tdate        = idate,-            tdate2       = Nothing,-            tstatus      = Cleared,-            tcode        = "",-            tdescription = desc,-            tcomment     = "",-            ttags        = [],-            tpostings    = ps,-            tpreceding_comment_lines=""-          }-      itime    = tldatetime i-      otime    = tldatetime o-      itod     = localTimeOfDay itime-      otod     = localTimeOfDay otime-      idate    = localDay itime-      desc     | null (tldescription i) = showtime itod ++ "-" ++ showtime otod-               | otherwise              = tldescription i-      showtime = take 5 . show-      hours    = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc-      acctname = tlaccount i-      amount   = Mixed [hrs hours]-      ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]---tests_Hledger_Data_TimeLog = TestList [--   "timeLogEntriesToTransactions" ~: do-     today <- getCurrentDay-     now' <- getCurrentTime-     tz <- getCurrentTimeZone-     let now = utcToLocalTime tz now'-         nowstr = showtime now-         yesterday = prevday today-         clockin = TimeLogEntry nullsourcepos In-         mktime d = LocalTime d . fromMaybe midnight .-#if MIN_VERSION_time(1,5,0)-                    parseTimeM True defaultTimeLocale "%H:%M:%S"-#else-                    parseTime defaultTimeLocale "%H:%M:%S"-#endif-         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/Timeclock.hs view
@@ -0,0 +1,145 @@+{-|++A 'TimeclockEntry' is a clock-in, clock-out, or other directive in a timeclock+file (see timeclock.el or the command-line version). These can be+converted to 'Transactions' and queried like a ledger.++-}++{-# LANGUAGE CPP, OverloadedStrings #-}++module Hledger.Data.Timeclock+where+import Data.Maybe+-- import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Calendar+import Data.Time.Clock+import Data.Time.Format+import Data.Time.LocalTime+#if !(MIN_VERSION_time(1,5,0))+import System.Locale (defaultTimeLocale)+#endif+import Test.HUnit+import Text.Printf++import Hledger.Utils+import Hledger.Data.Types+import Hledger.Data.Dates+import Hledger.Data.Amount+import Hledger.Data.Posting+import Hledger.Data.Transaction++instance Show TimeclockEntry where+    show t = printf "%s %s %s  %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription t)++instance Show TimeclockCode where+    show SetBalance = "b"+    show SetRequiredHours = "h"+    show In = "i"+    show Out = "o"+    show FinalOut = "O"++instance Read TimeclockCode 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 journal 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.+timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]+timeclockEntriesToTransactions _ [] = []+timeclockEntriesToTransactions now [i]+    | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now [i',o]+    | otherwise = [entryFromTimeclockInOut i o]+    where+      o = TimeclockEntry (tlsourcepos i) 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}}+timeclockEntriesToTransactions now (i:o:rest)+    | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now (i':o:rest)+    | otherwise = entryFromTimeclockInOut i o : timeclockEntriesToTransactions 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 timeclock clockin and clockout entry to an equivalent journal+-- transaction, representing the time expenditure. Note this entry is  not balanced,+-- since we omit the \"assets:time\" transaction for simpler output.+entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction+entryFromTimeclockInOut i o+    | otime >= itime = t+    | otherwise =+        error' $ "clock-out time less than clock-in time in:\n" ++ showTransaction t+    where+      t = Transaction {+            tindex       = 0,+            tsourcepos   = tlsourcepos i,+            tdate        = idate,+            tdate2       = Nothing,+            tstatus      = Cleared,+            tcode        = "",+            tdescription = desc,+            tcomment     = "",+            ttags        = [],+            tpostings    = ps,+            tpreceding_comment_lines=""+          }+      itime    = tldatetime i+      otime    = tldatetime o+      itod     = localTimeOfDay itime+      otod     = localTimeOfDay otime+      idate    = localDay itime+      desc     | T.null (tldescription i) = T.pack $ showtime itod ++ "-" ++ showtime otod+               | otherwise                = tldescription i+      showtime = take 5 . show+      hours    = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc+      acctname = tlaccount i+      amount   = Mixed [hrs hours]+      ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]+++tests_Hledger_Data_Timeclock = TestList [++   "timeclockEntriesToTransactions" ~: do+     today <- getCurrentDay+     now' <- getCurrentTime+     tz <- getCurrentTimeZone+     let now = utcToLocalTime tz now'+         nowstr = showtime now+         yesterday = prevday today+         clockin = TimeclockEntry nullsourcepos In+         mktime d = LocalTime d . fromMaybe midnight .+#if MIN_VERSION_time(1,5,0)+                    parseTimeM True defaultTimeLocale "%H:%M:%S"+#else+                    parseTime defaultTimeLocale "%H:%M:%S"+#endif+         showtime = formatTime defaultTimeLocale "%H:%M"+         assertEntriesGiveStrings name es ss = assertEqual name ss (map (T.unpack . tdescription) $ timeclockEntriesToTransactions 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
@@ -7,12 +7,14 @@  -} +{-# LANGUAGE OverloadedStrings #-}+ module Hledger.Data.Transaction (   -- * Transaction   nullsourcepos,   nulltransaction,   txnTieKnot,-  -- settxn,+  txnUntieKnot,   -- * operations   showAccountName,   hasRealPostings,@@ -31,12 +33,15 @@   showTransaction,   showTransactionUnelided,   showTransactionUnelidedOneLineAmounts,+  showPostingLine,   -- * misc.   tests_Hledger_Data_Transaction ) where import Data.List import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar import Test.HUnit import Text.Printf@@ -51,10 +56,10 @@ instance Show Transaction where show = showTransactionUnelided  instance Show ModifierTransaction where-    show t = "= " ++ mtvalueexpr t ++ "\n" ++ unlines (map show (mtpostings t))+    show t = "= " ++ T.unpack (mtvalueexpr t) ++ "\n" ++ unlines (map show (mtpostings t))  instance Show PeriodicTransaction where-    show t = "~ " ++ ptperiodicexpr t ++ "\n" ++ unlines (map show (ptpostings t))+    show t = "~ " ++ T.unpack (ptperiodicexpr t) ++ "\n" ++ unlines (map show (ptpostings t))  nullsourcepos :: GenericSourcePos nullsourcepos = GenericSourcePos "" 1 1@@ -145,16 +150,16 @@       status | tstatus t == Cleared = " *"              | tstatus t == Pending = " !"              | otherwise            = ""-      code = if length (tcode t) > 0 then printf " (%s)" $ tcode t else ""-      desc = if null d then "" else " " ++ d where d = tdescription t+      code = if T.length (tcode t) > 0 then printf " (%s)" $ T.unpack $ tcode t else ""+      desc = if null d then "" else " " ++ d where d = T.unpack $ tdescription t       (samelinecomment, newlinecomments) =         case renderCommentLines (tcomment t) of []   -> ("",[])                                                 c:cs -> (c,cs)  -- Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.-renderCommentLines :: String -> [String]-renderCommentLines s  = case lines s of ("":ls) -> "":map commentprefix ls-                                        ls      -> map commentprefix ls+renderCommentLines :: Text -> [String]+renderCommentLines t  = case lines $ T.unpack t of ("":ls) -> "":map commentprefix ls+                                                   ls      -> map commentprefix ls     where       commentprefix = indent . ("; "++) @@ -187,7 +192,7 @@         showstatus p ++ fitString (Just acctwidth) Nothing False True (showAccountName Nothing (ptype p) (paccount p))         where           showstatus p = if pstatus p == Cleared then "* " else ""-          acctwidth = maximum $ map (strWidth . paccount) ps+          acctwidth = maximum $ map (textWidth . paccount) ps      -- currently prices are considered part of the amount string when right-aligning amounts     amount@@ -201,6 +206,15 @@       case renderCommentLines (pcomment p) of []   -> ("",[])                                               c:cs -> (c,cs) ++-- used in balance assertion error+showPostingLine p =+  indent $+  if pstatus p == Cleared then "* " else "" +++  showAccountName Nothing (ptype p) (paccount p) +++  "    " +++  showMixedAmountOneLine (pamount p)+ tests_postingAsLines = [    "postingAsLines" ~: do     let p `gives` ls = assertEqual "" ls (postingAsLines False False [p] p)@@ -229,13 +243,17 @@ 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+      fmt RegularPosting = take w' . T.unpack+      fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse . T.unpack+      fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse . T.unpack       w' = fromMaybe 999999 w-      parenthesise s = "("++s++")"-      bracket s = "["++s++"]" +parenthesise :: String -> String+parenthesise s = "("++s++")"++bracket :: String -> String+bracket s = "["++s++"]"+ hasRealPostings :: Transaction -> Bool hasRealPostings = not . null . realPostings @@ -260,7 +278,7 @@ -- | 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 :: Maybe (Map.Map Commodity AmountStyle) -> Transaction -> Bool+isTransactionBalanced :: Maybe (Map.Map CommoditySymbol AmountStyle) -> Transaction -> Bool isTransactionBalanced styles t =     -- isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum     isZeroMixedAmount rsum' && isZeroMixedAmount bvsum'@@ -274,7 +292,7 @@ -- amount or conversion price(s), or return an error message. -- Balancing is affected by commodity display precisions, so those can -- (optionally) be provided.-balanceTransaction :: Maybe (Map.Map Commodity AmountStyle) -> Transaction -> Either String Transaction+balanceTransaction :: Maybe (Map.Map CommoditySymbol AmountStyle) -> Transaction -> Either String Transaction balanceTransaction styles t =   case inferBalancingAmount t of     Left err -> Left err@@ -402,11 +420,16 @@ -- | Ensure a transaction's postings refer back to it, so that eg -- relatedPostings works right. txnTieKnot :: Transaction -> Transaction-txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (settxn t) ps}+txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (postingSetTransaction t) ps} +-- | Ensure a transaction's postings do not refer back to it, so that eg+-- recursiveSize and GHCI's :sprint work right.+txnUntieKnot :: Transaction -> Transaction+txnUntieKnot t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}+ -- | Set a posting's parent transaction.-settxn :: Transaction -> Posting -> Posting-settxn t p = p{ptransaction=Just t}+postingSetTransaction :: Transaction -> Posting -> Posting+postingSetTransaction t p = p{ptransaction=Just t}  tests_Hledger_Data_Transaction = TestList $ concat [   tests_postingAsLines,
Hledger/Data/Types.hs view
@@ -24,11 +24,12 @@ import Control.DeepSeq (NFData) import Control.Monad.Except (ExceptT) import Data.Data-#ifndef DOUBLE import Data.Decimal+import Data.Default import Text.Blaze (ToMarkup(..))-#endif import qualified Data.Map as M+import Data.Text (Text)+-- import qualified Data.Text as T import Data.Time.Calendar import Data.Time.LocalTime import System.Time (ClockTime(..))@@ -44,15 +45,61 @@  instance NFData DateSpan -data Interval = NoInterval-              | Days Int | Weeks Int | Months Int | Quarters Int | Years Int-              | DayOfMonth Int | DayOfWeek Int-              -- WeekOfYear Int | MonthOfYear Int | QuarterOfYear Int-                deriving (Eq,Show,Ord,Data,Generic,Typeable)+-- synonyms for various date-related scalars+type Year = Integer+type Month = Int     -- 1-12+type Quarter = Int   -- 1-4+type YearWeek = Int  -- 1-52+type MonthWeek = Int -- 1-5+type YearDay = Int   -- 1-366+type MonthDay = Int  -- 1-31+type WeekDay = Int   -- 1-7 +-- Typical report periods (spans of time), both finite and open-ended.+-- A richer abstraction than DateSpan.+data Period =+    DayPeriod Day+  | WeekPeriod Day+  | MonthPeriod Year Month+  | QuarterPeriod Year Quarter+  | YearPeriod Year+  | PeriodBetween Day Day+  | PeriodFrom Day+  | PeriodTo Day+  | PeriodAll+  deriving (Eq,Ord,Show,Data,Generic,Typeable)++instance Default Period where def = PeriodAll++---- Typical report period/subperiod durations, from a day to a year.+--data Duration =+--    DayLong+--   WeekLong+--   MonthLong+--   QuarterLong+--   YearLong+--  deriving (Eq,Ord,Show,Data,Generic,Typeable)++-- Ways in which a period can be divided into subperiods.+data Interval =+    NoInterval+  | Days Int+  | Weeks Int+  | Months Int+  | Quarters Int+  | Years Int+  | DayOfMonth Int+  | DayOfWeek Int+  -- WeekOfYear Int+  -- MonthOfYear Int+  -- QuarterOfYear Int+  deriving (Eq,Show,Ord,Data,Generic,Typeable)++instance Default Interval where def = NoInterval+ instance NFData Interval -type AccountName = String+type AccountName = Text  data AccountAlias = BasicAlias AccountName AccountName                   | RegexAlias Regexp Replacement@@ -64,15 +111,7 @@  instance NFData Side -type Commodity = String---- | The basic numeric type used in amounts. Different implementations--- can be selected via cabal flag for testing and benchmarking purposes.-numberRepresentation :: String-#ifdef DOUBLE-type Quantity = Double-numberRepresentation = "Double"-#else+-- | The basic numeric type used in amounts. type Quantity = Decimal deriving instance Data (Quantity) -- The following is for hledger-web, and requires blaze-markup.@@ -80,8 +119,6 @@ instance ToMarkup (Quantity)  where    toMarkup = toMarkup . show-numberRepresentation = "Decimal"-#endif  -- | An amount's price (none, per unit, or total) in another commodity. -- Note the price should be a positive number, although this is not enforced.@@ -91,11 +128,11 @@  -- | Display style for an amount. data AmountStyle = AmountStyle {-      ascommodityside :: Side,       -- ^ does the symbol appear on the left or the right ?-      ascommodityspaced :: Bool,     -- ^ space between symbol and quantity ?-      asprecision :: Int,            -- ^ number of digits displayed after the decimal point-      asdecimalpoint :: Maybe Char,  -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"-      asdigitgroups :: Maybe DigitGroupStyle -- ^ style for displaying digit groups, if any+      ascommodityside   :: Side,                 -- ^ does the symbol appear on the left or the right ?+      ascommodityspaced :: Bool,                 -- ^ space between symbol and quantity ?+      asprecision       :: Int,                  -- ^ number of digits displayed after the decimal point+      asdecimalpoint    :: Maybe Char,           -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"+      asdigitgroups     :: Maybe DigitGroupStyle -- ^ style for displaying digit groups, if any } deriving (Eq,Ord,Read,Show,Typeable,Data,Generic)  instance NFData AmountStyle@@ -111,11 +148,20 @@  instance NFData DigitGroupStyle +type CommoditySymbol = Text++data Commodity = Commodity {+  csymbol :: CommoditySymbol,+  cformat :: Maybe AmountStyle+  } deriving (Show,Eq,Data,Generic) --,Ord,Typeable,Data,Generic)++instance NFData Commodity+ data Amount = Amount {-      acommodity :: Commodity,-      aquantity :: Quantity,-      aprice :: Price,                -- ^ the (fixed) price for this amount, if any-      astyle :: AmountStyle+      acommodity :: CommoditySymbol,+      aquantity  :: Quantity,+      aprice     :: Price,           -- ^ the (fixed) price for this amount, if any+      astyle     :: AmountStyle     } deriving (Eq,Ord,Typeable,Data,Generic)  instance NFData Amount@@ -129,30 +175,32 @@  instance NFData PostingType -type Tag = (String, String)  -- ^ A tag name and (possibly empty) value.+type TagName = Text+type TagValue = Text+type Tag = (TagName, TagValue)  -- ^ A tag name and (possibly empty) value.  data ClearedStatus = Uncleared | Pending | Cleared-                   deriving (Eq,Ord,Typeable,Data,Generic)+  deriving (Eq,Ord,Typeable,Data,Generic)  instance NFData ClearedStatus -instance Show ClearedStatus where -- custom show-  show Uncleared = ""             -- a bad idea-  show Pending   = "!"            -- don't do it+instance Show ClearedStatus where -- custom show.. bad idea.. don't do it..+  show Uncleared = ""+  show Pending   = "!"   show Cleared   = "*"  data Posting = Posting {-      pdate :: Maybe Day,  -- ^ this posting's date, if different from the transaction's-      pdate2 :: Maybe Day,  -- ^ this posting's secondary date, if different from the transaction's-      pstatus :: ClearedStatus,-      paccount :: AccountName,-      pamount :: MixedAmount,-      pcomment :: String, -- ^ this posting's comment lines, as a single non-indented multi-line string-      ptype :: PostingType,-      ptags :: [Tag], -- ^ tag names and values, extracted from the comment-      pbalanceassertion :: Maybe MixedAmount,  -- ^ optional: the expected balance in the account after this posting-      ptransaction :: Maybe Transaction    -- ^ this posting's parent transaction (co-recursive types).-                                           -- Tying this knot gets tedious, Maybe makes it easier/optional.+      pdate             :: Maybe Day,         -- ^ this posting's date, if different from the transaction's+      pdate2            :: Maybe Day,         -- ^ this posting's secondary date, if different from the transaction's+      pstatus           :: ClearedStatus,+      paccount          :: AccountName,+      pamount           :: MixedAmount,+      pcomment          :: Text,              -- ^ this posting's comment lines, as a single non-indented multi-line string+      ptype             :: PostingType,+      ptags             :: [Tag],             -- ^ tag names and values, extracted from the comment+      pbalanceassertion :: Maybe MixedAmount, -- ^ optional: the expected balance in the account after this posting+      ptransaction      :: Maybe Transaction  -- ^ this posting's parent transaction (co-recursive types).+                                              -- Tying this knot gets tedious, Maybe makes it easier/optional.     } deriving (Typeable,Data,Generic)  instance NFData Posting@@ -170,102 +218,99 @@ instance NFData GenericSourcePos  data Transaction = Transaction {-      tindex :: Integer, -- ^ this transaction's 1-based position in the input stream, or 0 when not available-      tsourcepos :: GenericSourcePos,-      tdate :: Day,-      tdate2 :: Maybe Day,-      tstatus :: ClearedStatus,-      tcode :: String,-      tdescription :: String,-      tcomment :: String, -- ^ this transaction's comment lines, as a single non-indented multi-line string-      ttags :: [Tag], -- ^ tag names and values, extracted from the comment-      tpostings :: [Posting],            -- ^ this transaction's postings-      tpreceding_comment_lines :: String -- ^ any comment lines immediately preceding this transaction+      tindex                   :: Integer,   -- ^ this transaction's 1-based position in the input stream, or 0 when not available+      tsourcepos               :: GenericSourcePos,+      tdate                    :: Day,+      tdate2                   :: Maybe Day,+      tstatus                  :: ClearedStatus,+      tcode                    :: Text,+      tdescription             :: Text,+      tcomment                 :: Text,      -- ^ this transaction's comment lines, as a single non-indented multi-line string+      ttags                    :: [Tag],     -- ^ tag names and values, extracted from the comment+      tpostings                :: [Posting], -- ^ this transaction's postings+      tpreceding_comment_lines :: Text       -- ^ any comment lines immediately preceding this transaction     } deriving (Eq,Typeable,Data,Generic)  instance NFData Transaction  data ModifierTransaction = ModifierTransaction {-      mtvalueexpr :: String,-      mtpostings :: [Posting]+      mtvalueexpr :: Text,+      mtpostings  :: [Posting]     } deriving (Eq,Typeable,Data,Generic)  instance NFData ModifierTransaction  data PeriodicTransaction = PeriodicTransaction {-      ptperiodicexpr :: String,-      ptpostings :: [Posting]+      ptperiodicexpr :: Text,+      ptpostings     :: [Posting]     } deriving (Eq,Typeable,Data,Generic)  instance NFData PeriodicTransaction -data TimeLogCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data,Generic)+data TimeclockCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data,Generic) -instance NFData TimeLogCode+instance NFData TimeclockCode -data TimeLogEntry = TimeLogEntry {-      tlsourcepos :: GenericSourcePos,-      tlcode :: TimeLogCode,-      tldatetime :: LocalTime,-      tlaccount :: String,-      tldescription :: String+data TimeclockEntry = TimeclockEntry {+      tlsourcepos   :: GenericSourcePos,+      tlcode        :: TimeclockCode,+      tldatetime    :: LocalTime,+      tlaccount     :: AccountName,+      tldescription :: Text     } deriving (Eq,Ord,Typeable,Data,Generic) -instance NFData TimeLogEntry+instance NFData TimeclockEntry  data MarketPrice = MarketPrice {-      mpdate :: Day,-      mpcommodity :: Commodity,-      mpamount :: Amount+      mpdate      :: Day,+      mpcommodity :: CommoditySymbol,+      mpamount    :: Amount     } deriving (Eq,Ord,Typeable,Data,Generic) -- & Show (in Amount.hs)  instance NFData MarketPrice -type Year = Integer---- | A journal "context" is some data which can change in the course of--- parsing a journal. An example is the default year, which changes when a--- Y directive is encountered.  At the end of parsing, the final context--- is saved for later use by eg the add command.-data JournalContext = Ctx {-      ctxYear      :: !(Maybe Year)      -- ^ the default year most recently specified with Y-    , ctxDefaultCommodityAndStyle :: !(Maybe (Commodity,AmountStyle)) -- ^ the default commodity and amount style most recently specified with D-    , ctxAccount   :: ![AccountName]     -- ^ the current stack of parent accounts/account name components-                                        --   specified with "account" directive(s). Concatenated, these-                                        --   are the account prefix prepended to parsed account names.-    , ctxAliases   :: ![AccountAlias]   -- ^ the current list of account name aliases in effect-    , ctxTransactionIndex   :: !Integer -- ^ the number of transactions read so far-    } deriving (Read, Show, Eq, Data, Typeable, Generic)--instance NFData JournalContext+-- | A Journal, containing transactions and various other things.+-- The basic data model for hledger.+--+-- This is used during parsing (as the type alias ParsedJournal), and+-- then finalised/validated for use as a Journal. Some extra+-- parsing-related fields are included for convenience, at least for+-- now. In a ParsedJournal these are updated as parsing proceeds, in a+-- Journal they represent the final state at end of parsing (used eg+-- by the add command).+--+data Journal = Journal {+  -- parsing-related data+   jparsedefaultyear      :: Maybe Year                            -- ^ the current default year, specified by the most recent Y directive (or current date)+  ,jparsedefaultcommodity :: Maybe (CommoditySymbol,AmountStyle)   -- ^ the current default commodity and its format, specified by the most recent D directive+  ,jparseparentaccounts   :: [AccountName]                         -- ^ the current stack of parent account names, specified by apply account directives+  ,jparsealiases          :: [AccountAlias]                        -- ^ the current account name aliases in effect, specified by alias directives (& options ?)+  -- ,jparsetransactioncount :: Integer                               -- ^ the current count of transactions parsed so far (only journal format txns, currently)+  ,jparsetimeclockentries :: [TimeclockEntry]                   -- ^ timeclock sessions which have not been clocked out+  -- principal data+  ,jaccounts              :: [AccountName]                         -- ^ accounts that have been declared by account directives+  ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives+  ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts+  ,jmarketprices          :: [MarketPrice]+  ,jmodifiertxns          :: [ModifierTransaction]+  ,jperiodictxns          :: [PeriodicTransaction]+  ,jtxns                  :: [Transaction]+  ,jfinalcommentlines     :: Text                                   -- ^ any final trailing comments in the (main) journal file+  ,jfiles                 :: [(FilePath, Text)]                     -- ^ the file path and raw text of the main and+                                                                    --   any included journal files. The main file is first,+                                                                    --   followed by any included files in the order encountered.+  ,jlastreadtime          :: ClockTime                              -- ^ when this journal was last read from its file(s)+  } deriving (Eq, Typeable, Data, Generic)  deriving instance Data (ClockTime) deriving instance Typeable (ClockTime) deriving instance Generic (ClockTime)- instance NFData ClockTime--data Journal = Journal {-      jmodifiertxns :: [ModifierTransaction],-      jperiodictxns :: [PeriodicTransaction],-      jtxns :: [Transaction],-      open_timelog_entries :: [TimeLogEntry],-      jmarketprices :: [MarketPrice],-      final_comment_lines :: String,        -- ^ any trailing comments from the journal file-      jContext :: JournalContext,           -- ^ the context (parse state) at the end of parsing-      files :: [(FilePath, String)],        -- ^ the file path and raw text of the main and-                                            -- any included journal files. The main file is-                                            -- first followed by any included files in the-                                            -- order encountered.-      filereadtime :: ClockTime,            -- ^ when this journal was last read from its file(s)-      jcommoditystyles :: M.Map Commodity AmountStyle  -- ^ how to display amounts in each commodity-    } deriving (Eq, Typeable, Data, Generic)- instance NFData Journal --- | A JournalUpdate is some transformation of a Journal. It can do I/O or--- raise an exception.-type JournalUpdate = ExceptT String IO (Journal -> Journal)+-- | A journal in the process of being parsed, not yet finalised.+-- The data is partial, and list fields are in reverse order.+type ParsedJournal = Journal  -- | The id of a data format understood by hledger, eg @journal@ or @csv@. -- The --output-format option selects one of these for output.@@ -277,9 +322,9 @@      -- name of the format this reader handles      rFormat   :: StorageFormat      -- quickly check if this reader can probably handle the given file path and file content-    ,rDetector :: FilePath -> String -> Bool+    ,rDetector :: FilePath -> Text -> Bool      -- parse the given string, using the given parse rules file if any, returning a journal or error aware of the given file path-    ,rParser   :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal+    ,rParser   :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal     }  instance Show Reader where show r = rFormat r ++ " reader"@@ -287,22 +332,22 @@ -- | An account, with name, balances and links to parent/subaccounts -- which let you walk up or down the account tree. data Account = Account {-  aname :: AccountName,     -- ^ this account's full name-  aebalance :: MixedAmount, -- ^ this account's balance, excluding subaccounts-  asubs :: [Account],       -- ^ sub-accounts-  anumpostings :: Int,      -- ^ number of postings to this account-  -- derived from the above:-  aibalance :: MixedAmount, -- ^ this account's balance, including subaccounts-  aparent :: Maybe Account, -- ^ parent account-  aboring :: Bool           -- ^ used in the accounts report to label elidable parents-  }+  aname                     :: AccountName,   -- ^ this account's full name+  aebalance                 :: MixedAmount,   -- ^ this account's balance, excluding subaccounts+  asubs                     :: [Account],     -- ^ sub-accounts+  anumpostings              :: Int,           -- ^ number of postings to this account+  -- derived from the above :+  aibalance                 :: MixedAmount,   -- ^ this account's balance, including subaccounts+  aparent                   :: Maybe Account, -- ^ parent account+  aboring                   :: Bool           -- ^ used in the accounts report to label elidable parents+  } deriving (Typeable, Data, Generic)  -- | A Ledger has the journal it derives from, and the accounts -- derived from that. Accounts are accessible both list-wise and -- tree-wise, since each one knows its parent and subs; the first -- account is the root of the tree and always exists. data Ledger = Ledger {-  ljournal :: Journal,+  ljournal  :: Journal,   laccounts :: [Account] } 
Hledger/Query.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveDataTypeable #-} {-|  A general query system for matching things (accounts, postings,@@ -6,6 +5,8 @@  -} +{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ViewPatterns #-}+ module Hledger.Query (   -- * Query and QueryOpt   Query(..),@@ -23,6 +24,9 @@   queryIsDateOrDate2,   queryIsStartDateOnly,   queryIsSym,+  queryIsReal,+  queryIsStatus,+  queryIsEmpty,   queryStartDate,   queryEndDate,   queryDateSpan,@@ -45,13 +49,16 @@ import Data.Either import Data.List import Data.Maybe+import Data.Monoid ((<>))+-- import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar import Safe (readDef, headDef) import Test.HUnit--- import Text.ParserCombinators.Parsec-import Text.Parsec hiding (Empty)+import Text.Megaparsec+import Text.Megaparsec.Text -import Hledger.Utils+import Hledger.Utils hiding (words') import Hledger.Data.Types import Hledger.Data.AccountName import Hledger.Data.Amount (amount, nullamt, usd)@@ -149,7 +156,7 @@ -- 1. multiple account patterns are OR'd together -- 2. multiple description patterns are OR'd together -- 3. then all terms are AND'd together-parseQuery :: Day -> String -> (Query,[QueryOpt])+parseQuery :: Day -> T.Text -> (Query,[QueryOpt]) parseQuery d s = (q, opts)   where     terms = words'' prefixes s@@ -173,21 +180,27 @@ -- | Quote-and-prefix-aware version of words - don't split on spaces which -- are inside quotes, including quotes which may have one of the specified -- prefixes in front, and maybe an additional not: prefix in front of that.-words'' :: [String] -> String -> [String]+words'' :: [T.Text] -> T.Text -> [T.Text] words'' prefixes = fromparse . parsewith maybeprefixedquotedphrases -- XXX     where-      maybeprefixedquotedphrases = choice' [prefixedQuotedPattern, singleQuotedPattern, doubleQuotedPattern, pattern] `sepBy` many1 spacenonewline+      maybeprefixedquotedphrases :: Parser [T.Text]+      maybeprefixedquotedphrases = choice' [prefixedQuotedPattern, singleQuotedPattern, doubleQuotedPattern, pattern] `sepBy` some spacenonewline+      prefixedQuotedPattern :: Parser T.Text       prefixedQuotedPattern = do-        not' <- fromMaybe "" `fmap` (optionMaybe $ string "not:")+        not' <- fromMaybe "" `fmap` (optional $ string "not:")         let allowednexts | null not' = prefixes                          | otherwise = prefixes ++ [""]-        next <- choice' $ map string allowednexts-        let prefix = not' ++ next+        next <- fmap T.pack $ choice' $ map (string . T.unpack) allowednexts+        let prefix :: T.Text+            prefix = T.pack not' <> next         p <- singleQuotedPattern <|> doubleQuotedPattern-        return $ prefix ++ stripquotes p-      singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf "'") >>= return . stripquotes-      doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf "\"") >>= return . stripquotes-      pattern = many (noneOf " \n\r")+        return $ prefix <> stripquotes p+      singleQuotedPattern :: Parser T.Text+      singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf ("'" :: [Char])) >>= return . stripquotes . T.pack+      doubleQuotedPattern :: Parser T.Text+      doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf ("\"" :: [Char])) >>= return . stripquotes . T.pack+      pattern :: Parser T.Text+      pattern = fmap T.pack $ many (noneOf (" \n\r" :: [Char]))  tests_words'' = [    "words''" ~: do@@ -204,7 +217,8 @@  -- XXX -- keep synced with patterns below, excluding "not"-prefixes = map (++":") [+prefixes :: [T.Text]+prefixes = map (<>":") [      "inacctonly"     ,"inacct"     ,"amt"@@ -221,6 +235,7 @@     ,"tag"     ] +defaultprefix :: T.Text defaultprefix = "acct"  -- -- | Parse the query string as a boolean tree of match patterns.@@ -235,36 +250,37 @@  -- | Parse a single query term as either a query or a query option, -- or raise an error if it has invalid syntax.-parseQueryTerm :: Day -> String -> Either Query QueryOpt-parseQueryTerm _ ('i':'n':'a':'c':'c':'t':'o':'n':'l':'y':':':s) = Right $ QueryOptInAcctOnly s-parseQueryTerm _ ('i':'n':'a':'c':'c':'t':':':s) = Right $ QueryOptInAcct s-parseQueryTerm d ('n':'o':'t':':':s) = case parseQueryTerm d s of-                                       Left m  -> Left $ Not m-                                       Right _ -> Left Any -- not:somequeryoption will be ignored-parseQueryTerm _ ('c':'o':'d':'e':':':s) = Left $ Code s-parseQueryTerm _ ('d':'e':'s':'c':':':s) = Left $ Desc s-parseQueryTerm _ ('a':'c':'c':'t':':':s) = Left $ Acct s-parseQueryTerm d ('d':'a':'t':'e':'2':':':s) =-        case parsePeriodExpr d s of Left e         -> error' $ "\"date2:"++s++"\" gave a "++showDateParseError e+parseQueryTerm :: Day -> T.Text -> Either Query QueryOpt+parseQueryTerm _ (T.stripPrefix "inacctonly:" -> Just s) = Right $ QueryOptInAcctOnly s+parseQueryTerm _ (T.stripPrefix "inacct:" -> Just s) = Right $ QueryOptInAcct s+parseQueryTerm d (T.stripPrefix "not:" -> Just s) =+  case parseQueryTerm d s of+    Left m -> Left $ Not m+    Right _ -> Left Any -- not:somequeryoption will be ignored+parseQueryTerm _ (T.stripPrefix "code:" -> Just s) = Left $ Code $ T.unpack s+parseQueryTerm _ (T.stripPrefix "desc:" -> Just s) = Left $ Desc $ T.unpack s+parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left $ Acct $ T.unpack s+parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =+        case parsePeriodExpr d s of Left e         -> error' $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e                                     Right (_,span) -> Left $ Date2 span-parseQueryTerm d ('d':'a':'t':'e':':':s) =-        case parsePeriodExpr d s of Left e         -> error' $ "\"date:"++s++"\" gave a "++showDateParseError e+parseQueryTerm d (T.stripPrefix "date:" -> Just s) =+        case parsePeriodExpr d s of Left e         -> error' $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e                                     Right (_,span) -> Left $ Date span-parseQueryTerm _ ('s':'t':'a':'t':'u':'s':':':s) = -        case parseStatus s of Left e   -> error' $ "\"status:"++s++"\" gave a parse error: " ++ e+parseQueryTerm _ (T.stripPrefix "status:" -> Just s) =+        case parseStatus s of Left e   -> error' $ "\"status:"++T.unpack s++"\" gave a parse error: " ++ e                               Right st -> Left $ Status st-parseQueryTerm _ ('r':'e':'a':'l':':':s) = Left $ Real $ parseBool s || null s-parseQueryTerm _ ('a':'m':'t':':':s) = Left $ Amt ord q where (ord, q) = parseAmountQueryTerm s-parseQueryTerm _ ('e':'m':'p':'t':'y':':':s) = Left $ Empty $ parseBool s-parseQueryTerm _ ('d':'e':'p':'t':'h':':':s)+parseQueryTerm _ (T.stripPrefix "real:" -> Just s) = Left $ Real $ parseBool s || T.null s+parseQueryTerm _ (T.stripPrefix "amt:" -> Just s) = Left $ Amt ord q where (ord, q) = parseAmountQueryTerm s+parseQueryTerm _ (T.stripPrefix "empty:" -> Just s) = Left $ Empty $ parseBool s+parseQueryTerm _ (T.stripPrefix "depth:" -> Just s)   | n >= 0    = Left $ Depth n   | otherwise = error' "depth: should have a positive number"-  where n = readDef 0 s+  where n = readDef 0 (T.unpack s) -parseQueryTerm _ ('c':'u':'r':':':s) = Left $ Sym s -- support cur: as an alias-parseQueryTerm _ ('t':'a':'g':':':s) = Left $ Tag n v where (n,v) = parseTag s+parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Left $ Sym (T.unpack s) -- support cur: as an alias+parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = Left $ Tag n v where (n,v) = parseTag s parseQueryTerm _ "" = Left $ Any-parseQueryTerm d s = parseQueryTerm d $ defaultprefix++":"++s+parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s  tests_parseQueryTerm = [   "parseQueryTerm" ~: do@@ -293,35 +309,40 @@  deriving (Show,Eq,Data,Typeable)  -- can fail-parseAmountQueryTerm :: String -> (OrdPlus, Quantity)+parseAmountQueryTerm :: T.Text -> (OrdPlus, Quantity) parseAmountQueryTerm s' =   case s' of     -- feel free to do this a smarter way     ""              -> err-    '<':'+':s       -> (Lt, readDef err s)-    '<':'=':'+':s   -> (LtEq, readDef err s)-    '>':'+':s       -> (Gt, readDef err s)-    '>':'=':'+':s   -> (GtEq, readDef err s)-    '=':'+':s       -> (Eq, readDef err s)-    '+':s           -> (Eq, readDef err s)-    '<':'-':s       -> (Lt, negate $ readDef err s)-    '<':'=':'-':s   -> (LtEq, negate $ readDef err s)-    '>':'-':s       -> (Gt, negate $ readDef err s)-    '>':'=':'-':s   -> (GtEq, negate $ readDef err s)-    '=':'-':s       -> (Eq, negate $ readDef err s)-    '-':s           -> (Eq, negate $ readDef err s)-    '<':'=':s       -> let n = readDef err s in case n of 0 -> (LtEq, 0)-                                                          _ -> (AbsLtEq, n)-    '<':s           -> let n = readDef err s in case n of 0 -> (Lt, 0)-                                                          _ -> (AbsLt, n)-    '>':'=':s       -> let n = readDef err s in case n of 0 -> (GtEq, 0)-                                                          _ -> (AbsGtEq, n)-    '>':s           -> let n = readDef err s in case n of 0 -> (Gt, 0)-                                                          _ -> (AbsGt, n)-    '=':s           -> (AbsEq, readDef err s)-    s               -> (AbsEq, readDef err s)+    (T.stripPrefix "<+" -> Just s)  -> (Lt, readDef err (T.unpack s))+    (T.stripPrefix "<=+" -> Just s) -> (LtEq, readDef err (T.unpack s))+    (T.stripPrefix ">+" -> Just s)  -> (Gt, readDef err (T.unpack s))+    (T.stripPrefix ">=+" -> Just s) -> (GtEq, readDef err (T.unpack s))+    (T.stripPrefix "=+" -> Just s)  -> (Eq, readDef err (T.unpack s))+    (T.stripPrefix "+" -> Just s)   -> (Eq, readDef err (T.unpack s))+    (T.stripPrefix "<-" -> Just s)  -> (Lt, negate $ readDef err (T.unpack s))+    (T.stripPrefix "<=-" -> Just s) -> (LtEq, negate $ readDef err (T.unpack s))+    (T.stripPrefix ">-" -> Just s)  -> (Gt, negate $ readDef err (T.unpack s))+    (T.stripPrefix ">=-" -> Just s) -> (GtEq, negate $ readDef err (T.unpack s))+    (T.stripPrefix "=-" -> Just s)  -> (Eq, negate $ readDef err (T.unpack s))+    (T.stripPrefix "-" -> Just s)   -> (Eq, negate $ readDef err (T.unpack s))+    (T.stripPrefix "<=" -> Just s)  -> let n = readDef err (T.unpack s) in+                                         case n of+                                           0 -> (LtEq, 0)+                                           _ -> (AbsLtEq, n)+    (T.stripPrefix "<" -> Just s)   -> let n = readDef err (T.unpack s) in+                                         case n of 0 -> (Lt, 0)+                                                   _ -> (AbsLt, n)+    (T.stripPrefix ">=" -> Just s)  -> let n = readDef err (T.unpack s) in+                                         case n of 0 -> (GtEq, 0)+                                                   _ -> (AbsGtEq, n)+    (T.stripPrefix ">" -> Just s)   -> let n = readDef err (T.unpack s) in+                                         case n of 0 -> (Gt, 0)+                                                   _ -> (AbsGt, n)+    (T.stripPrefix "=" -> Just s)           -> (AbsEq, readDef err (T.unpack s))+    s               -> (AbsEq, readDef err (T.unpack s))   where-    err = error' $ "could not parse as '=', '<', or '>' (optional) followed by a (optionally signed) numeric quantity: " ++ s'+    err = error' $ "could not parse as '=', '<', or '>' (optional) followed by a (optionally signed) numeric quantity: " ++ T.unpack s'  tests_parseAmountQueryTerm = [   "parseAmountQueryTerm" ~: do@@ -335,13 +356,13 @@     "-0.23" `gives` (Eq,(-0.23))   ] -parseTag :: String -> (Regexp, Maybe Regexp)-parseTag s | '=' `elem` s = (n, Just $ tail v)-           | otherwise    = (s, Nothing)-           where (n,v) = break (=='=') s+parseTag :: T.Text -> (Regexp, Maybe Regexp)+parseTag s | "=" `T.isInfixOf` s = (T.unpack n, Just $ tail $ T.unpack v)+           | otherwise    = (T.unpack s, Nothing)+           where (n,v) = T.break (=='=') s  -- | Parse the value part of a "status:" query, or return an error.-parseStatus :: String -> Either String ClearedStatus+parseStatus :: T.Text -> Either String ClearedStatus parseStatus s | s `elem` ["*","1"] = Right Cleared               | s `elem` ["!"]     = Right Pending               | s `elem` ["","0"]  = Right Uncleared@@ -349,10 +370,10 @@  -- | Parse the boolean value part of a "status:" query. "1" means true, -- anything else will be parsed as false without error.-parseBool :: String -> Bool+parseBool :: T.Text -> Bool parseBool s = s `elem` truestrings -truestrings :: [String]+truestrings :: [T.Text] truestrings = ["1"]  simplifyQuery :: Query -> Query@@ -395,7 +416,7 @@ same (a:as) = all (a==) as  -- | Remove query terms (or whole sub-expressions) not matching the given--- predicate from this query.  XXX Semantics not yet clear.+-- predicate from this query.  XXX Semantics not completely clear. filterQuery :: (Query -> Bool) -> Query -> Query filterQuery p = simplifyQuery . filterQuery' p @@ -452,6 +473,18 @@ queryIsSym (Sym _) = True queryIsSym _ = False +queryIsReal :: Query -> Bool+queryIsReal (Real _) = True+queryIsReal _ = False++queryIsStatus :: Query -> Bool+queryIsStatus (Status _) = True+queryIsStatus _ = False++queryIsEmpty :: Query -> Bool+queryIsEmpty (Empty _) = True+queryIsEmpty _ = False+ -- | Does this query specify a start date and nothing else (that would -- filter postings prior to the date) ? -- When the flag is true, look for a starting secondary date instead.@@ -557,8 +590,8 @@ -- Just looks at the first query option. inAccountQuery :: [QueryOpt] -> Maybe Query inAccountQuery [] = Nothing-inAccountQuery (QueryOptInAcctOnly a:_) = Just $ Acct $ accountNameToAccountOnlyRegex a-inAccountQuery (QueryOptInAcct a:_) = Just $ Acct $ accountNameToAccountRegex a+inAccountQuery (QueryOptInAcctOnly a : _) = Just $ Acct $ accountNameToAccountOnlyRegex a+inAccountQuery (QueryOptInAcct a     : _) = Just $ Acct $ accountNameToAccountRegex a  -- -- | Convert a query to its inverse. -- negateQuery :: Query -> Query@@ -573,7 +606,7 @@ matchesAccount (Not m) a = not $ matchesAccount m a matchesAccount (Or ms) a = any (`matchesAccount` a) ms matchesAccount (And ms) a = all (`matchesAccount` a) ms-matchesAccount (Acct r) a = regexMatchesCI r a+matchesAccount (Acct r) a = regexMatchesCI r (T.unpack a) -- XXX pack matchesAccount (Depth d) a = accountNameLevel a <= d matchesAccount (Tag _ _) _ = False matchesAccount _ _ = True@@ -605,7 +638,7 @@ matchesAmount (And qs) a = all (`matchesAmount` a) qs -- matchesAmount (Amt ord n) a = compareAmount ord n a-matchesAmount (Sym r) a = regexMatchesCI ("^" ++ r ++ "$") $ acommodity a+matchesAmount (Sym r) a = regexMatchesCI ("^" ++ r ++ "$") $ T.unpack $ acommodity a -- matchesAmount _ _ = True @@ -632,9 +665,9 @@ matchesPosting (None) _ = False matchesPosting (Or qs) p = any (`matchesPosting` p) qs matchesPosting (And qs) p = all (`matchesPosting` p) qs-matchesPosting (Code r) p = regexMatchesCI r $ maybe "" tcode $ ptransaction p-matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" tdescription $ ptransaction p-matchesPosting (Acct r) p = regexMatchesCI r $ paccount p+matchesPosting (Code r) p = regexMatchesCI r $ maybe "" (T.unpack . tcode) $ ptransaction p+matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" (T.unpack . tdescription) $ ptransaction p+matchesPosting (Acct r) p = regexMatchesCI r $ T.unpack $ paccount p -- XXX pack matchesPosting (Date span) p = span `spanContainsDate` postingDate p matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p matchesPosting (Status Uncleared) p = postingStatus p /= Cleared@@ -647,7 +680,7 @@ -- matchesPosting (Empty False) Posting{pamount=a} = True -- matchesPosting (Empty True) Posting{pamount=a} = isZeroMixedAmount a matchesPosting (Empty _) _ = True-matchesPosting (Sym r) Posting{pamount=Mixed as} = any (regexMatchesCI $ "^" ++ r ++ "$") $ map acommodity as+matchesPosting (Sym r) Posting{pamount=Mixed as} = any (regexMatchesCI $ "^" ++ r ++ "$") $ map (T.unpack . acommodity) as matchesPosting (Tag n v) p = not $ null $ matchedTags n v $ postingAllTags p -- matchesPosting _ _ = False @@ -690,8 +723,8 @@ matchesTransaction (None) _ = False matchesTransaction (Or qs) t = any (`matchesTransaction` t) qs matchesTransaction (And qs) t = all (`matchesTransaction` t) qs-matchesTransaction (Code r) t = regexMatchesCI r $ tcode t-matchesTransaction (Desc r) t = regexMatchesCI r $ tdescription t+matchesTransaction (Code r) t = regexMatchesCI r $ T.unpack $ tcode t+matchesTransaction (Desc r) t = regexMatchesCI r $ T.unpack $ tdescription t matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction (Date span) t = spanContainsDate span $ tdate t matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t@@ -723,8 +756,8 @@ matchedTags :: Regexp -> Maybe Regexp -> [Tag] -> [Tag] matchedTags namepat valuepat tags = filter (match namepat valuepat) tags   where-    match npat Nothing     (n,_) = regexMatchesCI npat n-    match npat (Just vpat) (n,v) = regexMatchesCI npat n && regexMatchesCI vpat v+    match npat Nothing     (n,_) = regexMatchesCI npat (T.unpack n) -- XXX+    match npat (Just vpat) (n,v) = regexMatchesCI npat (T.unpack n) && regexMatchesCI vpat (T.unpack v)  -- tests 
Hledger/Read.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE ScopedTypeVariables #-} {-|  This is the entry point to hledger's reading system, which can read@@ -8,7 +7,12 @@  -} -module Hledger.Read (+{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}++module Hledger.Read+  (+       module Hledger.Read.Common,+       readFormatNames,        -- * Journal reading API        defaultJournalPath,        defaultJournal,@@ -20,12 +24,12 @@        ensureJournalFileExists,        -- * Parsers used elsewhere        postingp,-       accountnamep,-       amountp,-       amountp',-       mamountp',-       numberp,-       codep,+       -- accountnamep,+       -- amountp,+       -- amountp',+       -- mamountp',+       -- numberp,+       -- codep,        accountaliasp,        -- * Tests        samplejournal,@@ -36,115 +40,54 @@ import Control.Monad.Except import Data.List import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T import System.Directory (doesFileExist, getHomeDirectory) import System.Environment (getEnv) import System.Exit (exitFailure) import System.FilePath ((</>))-import System.IO (IOMode(..), openFile, stdin, stderr, hSetNewlineMode, universalNewlineMode)+import System.IO (stderr) import Test.HUnit import Text.Printf  import Hledger.Data.Dates (getCurrentDay) import Hledger.Data.Types-import Hledger.Data.Journal (nullctx)+import Hledger.Read.Common import Hledger.Read.JournalReader as JournalReader-import Hledger.Read.TimelogReader as TimelogReader+import Hledger.Read.TimedotReader as TimedotReader+import Hledger.Read.TimeclockReader as TimeclockReader import Hledger.Read.CsvReader as CsvReader import Hledger.Utils import Prelude hiding (getContents, writeFile)-import Hledger.Utils.UTF8IOCompat (hGetContents, writeFile)+import Hledger.Utils.UTF8IOCompat (writeFile)  -journalEnvVar           = "LEDGER_FILE"-journalEnvVar2          = "LEDGER"-journalDefaultFilename  = ".hledger.journal"- -- The available data file readers, each one handling a particular data -- format. The first is also used as the default for unknown formats. readers :: [Reader] readers = [   JournalReader.reader- ,TimelogReader.reader+ ,TimeclockReader.reader+ ,TimedotReader.reader  ,CsvReader.reader  ] --- | All the data formats we can read.--- formats = map rFormat readers---- | Get the default journal file path specified by the environment.--- Like ledger, we look first for the LEDGER_FILE environment--- variable, and if that does not exist, for the legacy LEDGER--- environment variable. If neither is set, or the value is blank,--- return the hard-coded default, which is @.hledger.journal@ in the--- users's home directory (or in the current directory, if we cannot--- determine a home directory).-defaultJournalPath :: IO String-defaultJournalPath = do-  s <- envJournalPath-  if null s then defaultJournalPath else return s-    where-      envJournalPath =-        getEnv journalEnvVar-         `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2-                                            `C.catch` (\(_::C.IOException) -> return ""))-      defaultJournalPath = do-                  home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")-                  return $ home </> journalDefaultFilename---- | Read the default journal file specified by the environment, or raise an error.-defaultJournal :: IO Journal-defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return---- | Read a journal from the given string, trying all known formats, or simply throw an error.-readJournal' :: String -> IO Journal-readJournal' s = readJournal Nothing Nothing True Nothing s >>= either error' return--tests_readJournal' = [-  "readJournal' parses sample journal" ~: do-     _ <- samplejournal-     assertBool "" True- ]--+readFormatNames :: [StorageFormat]+readFormatNames = map rFormat readers --- | Read a journal from this string, trying whatever readers seem appropriate:------ - if a format is specified, try that reader only------ - or if one or more readers recognises the file path and data, try those------ - otherwise, try them all.------ A CSV conversion rules file may also be specified for use by the CSV reader.--- Also there is a flag specifying whether to check or ignore balance assertions in the journal.-readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal)-readJournal format rulesfile assrt path s =-  tryReaders $ readersFor (format, path, s)-  where-    -- try each reader in turn, returning the error of the first if all fail-    tryReaders :: [Reader] -> IO (Either String Journal)-    tryReaders = firstSuccessOrBestError []-      where-        firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal)-        firstSuccessOrBestError [] []        = return $ Left "no readers found"-        firstSuccessOrBestError errs (r:rs) = do-          dbg1IO "trying reader" (rFormat r)-          result <- (runExceptT . (rParser r) rulesfile assrt path') s-          dbg1IO "reader result" $ either id show result-          case result of Right j -> return $ Right j                       -- success!-                         Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying-        firstSuccessOrBestError (e:_) []    = return $ Left e              -- none left, return first error-        path' = fromMaybe "(string)" path+journalEnvVar           = "LEDGER_FILE"+journalEnvVar2          = "LEDGER"+journalDefaultFilename  = ".hledger.journal"  -- | Which readers are worth trying for this (possibly unspecified) format, filepath, and data ?-readersFor :: (Maybe StorageFormat, Maybe FilePath, String) -> [Reader]-readersFor (format,path,s) =-    dbg1 ("possible readers for "++show (format,path,elideRight 30 s)) $+readersFor :: (Maybe StorageFormat, Maybe FilePath, Text) -> [Reader]+readersFor (format,path,t) =+    dbg1 ("possible readers for "++show (format,path,textElideRight 30 t)) $     case format of      Just f  -> case readerForStorageFormat f of Just r  -> [r]                                                  Nothing -> []      Nothing -> case path of Nothing  -> readers-                             Just p   -> case readersForPathAndData (p,s) of [] -> readers+                             Just p   -> case readersForPathAndData (p,t) of [] -> readers                                                                              rs -> rs  -- | Find the (first) reader which can handle the given format, if any.@@ -155,36 +98,65 @@       rs = filter ((s==).rFormat) readers :: [Reader]  -- | Find the readers which think they can handle the given file path and data, if any.-readersForPathAndData :: (FilePath,String) -> [Reader]-readersForPathAndData (f,s) = filter (\r -> (rDetector r) f s) readers+readersForPathAndData :: (FilePath,Text) -> [Reader]+readersForPathAndData (f,t) = filter (\r -> dbg1 ("try "++rFormat r++" format") $ (rDetector r) f t) readers +-- try each reader in turn, returning the error of the first if all fail+tryReaders :: [Reader] -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal)+tryReaders readers mrulesfile assrt path t = firstSuccessOrBestError [] readers+  where+    firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal)+    firstSuccessOrBestError [] []        = return $ Left "no readers found"+    firstSuccessOrBestError errs (r:rs) = do+      dbg1IO "trying reader" (rFormat r)+      result <- (runExceptT . (rParser r) mrulesfile assrt path') t+      dbg1IO "reader result" $ either id show result+      case result of Right j -> return $ Right j                       -- success!+                     Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying+    firstSuccessOrBestError (e:_) []    = return $ Left e              -- none left, return first error+    path' = fromMaybe "(string)" path++-- | Read a journal from this string, trying whatever readers seem appropriate:+--+-- - if a format is specified, try that reader only+--+-- - or if one or more readers recognises the file path and data, try those+--+-- - otherwise, try them all.+--+-- A CSV conversion rules file may also be specified for use by the CSV reader.+-- Also there is a flag specifying whether to check or ignore balance assertions in the journal.+readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal)+readJournal mformat mrulesfile assrt mpath t = tryReaders (readersFor (mformat, mpath, t)) mrulesfile assrt mpath t++-- | Call readJournalFile on each specified file path, and combine the+-- resulting journals into one. If there are any errors, the first is+-- returned, otherwise they are combined per Journal's monoid instance+-- (concatenated, basically). Parse context (eg directives & aliases)+-- is not maintained across file boundaries, it resets at the start of+-- each file (though the rfinal parse state saved in the resulting+-- journal is the combination of parse states from all files).+readJournalFiles :: Maybe StorageFormat -> Maybe FilePath -> Bool -> [FilePath] -> IO (Either String Journal)+readJournalFiles mformat mrulesfile assrt fs = do+  (either Left (Right . mconcat) . sequence)+    <$> mapM (readJournalFile mformat mrulesfile assrt) fs+ -- | Read a Journal from this file (or stdin if the filename is -) or give -- an error message, using the specified data format or trying all known -- formats. A CSV conversion rules file may be specified for better -- conversion of that format. Also there is a flag specifying whether -- to check or ignore balance assertions in the journal. readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> Bool -> FilePath -> IO (Either String Journal)-readJournalFile format rulesfile assrt f = readJournalFiles format rulesfile assrt [f]--readJournalFiles :: Maybe StorageFormat -> Maybe FilePath -> Bool -> [FilePath] -> IO (Either String Journal)-readJournalFiles format rulesfile assrt f = do-  contents <- fmap concat $ mapM readFileAnyNewline f-  readJournal format rulesfile assrt (listToMaybe f) contents- where-  readFileAnyNewline f = do-    requireJournalFileExists f-    h <- fileHandle f-    hSetNewlineMode h universalNewlineMode-    hGetContents h-  fileHandle "-" = return stdin-  fileHandle f = openFile f ReadMode+readJournalFile mformat mrulesfile assrt f = do+  requireJournalFileExists f+  readFileOrStdinAnyLineEnding f >>= readJournal mformat mrulesfile assrt (Just f)  -- | If the specified journal file does not exist, give a helpful error and quit. requireJournalFileExists :: FilePath -> IO () requireJournalFileExists "-" = return () requireJournalFileExists f = do   exists <- doesFileExist f-  when (not exists) $ do+  when (not exists) $ do  -- XXX might not be a journal file     hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f     hPrintf stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"     hPrintf stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n"@@ -206,9 +178,43 @@   d <- getCurrentDay   return $ printf "; journal created %s by hledger\n" (show d) +-- | Get the default journal file path specified by the environment.+-- Like ledger, we look first for the LEDGER_FILE environment+-- variable, and if that does not exist, for the legacy LEDGER+-- environment variable. If neither is set, or the value is blank,+-- return the hard-coded default, which is @.hledger.journal@ in the+-- users's home directory (or in the current directory, if we cannot+-- determine a home directory).+defaultJournalPath :: IO String+defaultJournalPath = do+  s <- envJournalPath+  if null s then defaultJournalPath else return s+    where+      envJournalPath =+        getEnv journalEnvVar+         `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2+                                            `C.catch` (\(_::C.IOException) -> return ""))+      defaultJournalPath = do+                  home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")+                  return $ home </> journalDefaultFilename++-- | Read the default journal file specified by the environment, or raise an error.+defaultJournal :: IO Journal+defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return++-- | Read a journal from the given text, trying all known formats, or simply throw an error.+readJournal' :: Text -> IO Journal+readJournal' t = readJournal Nothing Nothing True Nothing t >>= either error' return++tests_readJournal' = [+  "readJournal' parses sample journal" ~: do+     _ <- samplejournal+     assertBool "" True+ ]+ -- tests -samplejournal = readJournal' $ unlines+samplejournal = readJournal' $ T.unlines  ["2008/01/01 income"  ,"    assets:bank:checking  $1"  ,"    income:salary"@@ -240,11 +246,12 @@   tests_readJournal'   ++ [    tests_Hledger_Read_JournalReader,-   tests_Hledger_Read_TimelogReader,+   tests_Hledger_Read_TimeclockReader,+   tests_Hledger_Read_TimedotReader,    tests_Hledger_Read_CsvReader,     "journal" ~: do-    r <- runExceptT $ parseWithCtx nullctx JournalReader.journalp ""+    r <- runExceptT $ parseWithState mempty JournalReader.journalp ""     assertBool "journalp should parse an empty file" (isRight $ r)     jE <- readJournal Nothing Nothing True Nothing "" -- don't know how to get it from journal     either error' (assertBool "journalp parsing an empty file should give an empty journal" . null . jtxns) jE
+ Hledger/Read/Common.hs view
@@ -0,0 +1,828 @@+--- * doc+-- Lines beginning "--- *" are collapsible orgstruct nodes. Emacs users,+-- (add-hook 'haskell-mode-hook+--   (lambda () (set-variable 'orgstruct-heading-prefix-regexp "--- " t))+--   'orgstruct-mode)+-- and press TAB on nodes to expand/collapse.++{-|++Some common parsers and helpers used by several readers.+Some of these might belong in Hledger.Read.JournalReader or Hledger.Read.++-}++--- * module+{-# LANGUAGE CPP, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-}++module Hledger.Read.Common+where+--- * imports+import Prelude ()+import Prelude.Compat hiding (readFile)+import Control.Monad.Compat+import Control.Monad.Except (ExceptT(..), runExceptT, throwError) --, catchError)+import Control.Monad.State.Strict+import Data.Char (isNumber)+import Data.Functor.Identity+import Data.List.Compat+import Data.List.NonEmpty (NonEmpty(..))+import Data.List.Split (wordsBy)+import Data.Maybe+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Calendar+import Data.Time.LocalTime+import Safe+import System.Time (getClockTime)+import Text.Megaparsec hiding (parse,State)+import Text.Megaparsec.Text++import Hledger.Data+import Hledger.Utils++-- $setup++--- * parsing utils++-- | Run a string parser with no state in the identity monad.+runTextParser, rtp :: TextParser Identity a -> Text -> Either (ParseError Char Dec) a+runTextParser p t =  runParser p "" t+rtp = runTextParser++-- | Run a journal parser with a null journal-parsing state.+runJournalParser, rjp :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char Dec) a)+runJournalParser p t = runParserT p "" t+rjp = runJournalParser++-- | Run an error-raising journal parser with a null journal-parsing state.+runErroringJournalParser, rejp :: ErroringJournalParser a -> Text -> IO (Either String a)+runErroringJournalParser p t =+  runExceptT $+  runJournalParser (evalStateT p mempty)+                   t >>=+  either (throwError . parseErrorPretty) return+rejp = runErroringJournalParser++genericSourcePos :: SourcePos -> GenericSourcePos+genericSourcePos p = GenericSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p) (fromIntegral . unPos $ sourceColumn p)++-- | Given a parsec ParsedJournal parser, file path and data string,+-- parse and post-process a ready-to-use Journal, or give an error.+parseAndFinaliseJournal :: ErroringJournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal+parseAndFinaliseJournal parser assrt f txt = do+  t <- liftIO getClockTime+  y <- liftIO getCurrentYear+  ep <- runParserT (evalStateT parser nulljournal {jparsedefaultyear=Just y}) f txt+  case ep of+    Right pj -> case journalFinalise t f txt assrt pj of+                        Right j -> return j+                        Left e  -> throwError e+    Left e   -> throwError $ parseErrorPretty e++parseAndFinaliseJournal' :: JournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal+parseAndFinaliseJournal' parser assrt f txt = do+  t <- liftIO getClockTime+  y <- liftIO getCurrentYear+  let ep = runParser (evalStateT parser nulljournal {jparsedefaultyear=Just y}) f txt+  case ep of+    Right pj -> case journalFinalise t f txt assrt pj of+                        Right j -> return j+                        Left e  -> throwError e+    Left e   -> throwError $ parseErrorPretty e++setYear :: Year -> JournalStateParser m ()+setYear y = modify' (\j -> j{jparsedefaultyear=Just y})++getYear :: JournalStateParser m (Maybe Year)+getYear = fmap jparsedefaultyear get++setDefaultCommodityAndStyle :: (CommoditySymbol,AmountStyle) -> ErroringJournalParser ()+setDefaultCommodityAndStyle cs = modify' (\j -> j{jparsedefaultcommodity=Just cs})++getDefaultCommodityAndStyle :: JournalStateParser m (Maybe (CommoditySymbol,AmountStyle))+getDefaultCommodityAndStyle = jparsedefaultcommodity `fmap` get++pushAccount :: AccountName -> ErroringJournalParser ()+pushAccount acct = modify' (\j -> j{jaccounts = acct : jaccounts j})++pushParentAccount :: AccountName -> ErroringJournalParser ()+pushParentAccount acct = modify' (\j -> j{jparseparentaccounts = acct : jparseparentaccounts j})++popParentAccount :: ErroringJournalParser ()+popParentAccount = do+  j <- get+  case jparseparentaccounts j of+    []       -> unexpected (Tokens ('E' :| "nd of apply account block with no beginning"))+    (_:rest) -> put j{jparseparentaccounts=rest}++getParentAccount :: ErroringJournalParser AccountName+getParentAccount = fmap (concatAccountNames . reverse . jparseparentaccounts) get++addAccountAlias :: MonadState Journal m => AccountAlias -> m ()+addAccountAlias a = modify' (\(j@Journal{..}) -> j{jparsealiases=a:jparsealiases})++getAccountAliases :: MonadState Journal m => m [AccountAlias]+getAccountAliases = fmap jparsealiases get++clearAccountAliases :: MonadState Journal m => m ()+clearAccountAliases = modify' (\(j@Journal{..}) -> j{jparsealiases=[]})++-- getTransactionCount :: MonadState Journal m =>  m Integer+-- getTransactionCount = fmap jparsetransactioncount get+--+-- setTransactionCount :: MonadState Journal m => Integer -> m ()+-- setTransactionCount i = modify' (\j -> j{jparsetransactioncount=i})+--+-- -- | Increment the transaction index by one and return the new value.+-- incrementTransactionCount :: MonadState Journal m => m Integer+-- incrementTransactionCount = do+--   modify' (\j -> j{jparsetransactioncount=jparsetransactioncount j + 1})+--   getTransactionCount++journalAddFile :: (FilePath,Text) -> Journal -> Journal+journalAddFile f j@Journal{jfiles=fs} = j{jfiles=fs++[f]}+  -- append, unlike the other fields, even though we do a final reverse,+  -- to compensate for additional reversal due to including/monoid-concatting++-- -- | Terminate parsing entirely, returning the given error message+-- -- with the current parse position prepended.+-- parserError :: String -> ErroringJournalParser a+-- parserError s = do+--   pos <- getPosition+--   parserErrorAt pos s++-- | Terminate parsing entirely, returning the given error message+-- with the given parse position prepended.+parserErrorAt :: SourcePos -> String -> ErroringJournalParser a+parserErrorAt pos s = throwError $ sourcePosPretty pos ++ ":\n" ++ s++--- * parsers+--- ** transaction bits++statusp :: TextParser m ClearedStatus+statusp =+  choice'+    [ many spacenonewline >> char '*' >> return Cleared+    , many spacenonewline >> char '!' >> return Pending+    , return Uncleared+    ]+    <?> "cleared status"++codep :: TextParser m String+codep = try (do { some spacenonewline; char '(' <?> "codep"; anyChar `manyTill` char ')' } ) <|> return ""++descriptionp :: ErroringJournalParser String+descriptionp = many (noneOf (";\n" :: [Char]))++--- ** dates++-- | Parse a date in YYYY/MM/DD format.+-- Hyphen (-) and period (.) are also allowed as separators.+-- The year may be omitted if a default year has been set.+-- Leading zeroes may be omitted.+datep :: JournalStateParser m Day+datep = do+  -- hacky: try to ensure precise errors for invalid dates+  -- XXX reported error position is not too good+  -- pos <- genericSourcePos <$> getPosition+  datestr <- do+    c <- digitChar+    cs <- lift $ many $ choice' [digitChar, datesepchar]+    return $ c:cs+  let sepchars = nub $ sort $ filter (`elem` datesepchars) datestr+  when (length sepchars /= 1) $ fail $ "bad date, different separators used: " ++ datestr+  let dateparts = wordsBy (`elem` datesepchars) datestr+  currentyear <- getYear+  [y,m,d] <- case (dateparts,currentyear) of+              ([m,d],Just y)  -> return [show y,m,d]+              ([_,_],Nothing) -> fail $ "partial date "++datestr++" found, but the current year is unknown"+              ([y,m,d],_)     -> return [y,m,d]+              _               -> fail $ "bad date: " ++ datestr+  let maybedate = fromGregorianValid (read y) (read m) (read d)+  case maybedate of+    Nothing   -> fail $ "bad date: " ++ datestr+    Just date -> return date+  <?> "full or partial date"++-- | Parse a date and time in YYYY/MM/DD HH:MM[:SS][+-ZZZZ] format.+-- Hyphen (-) and period (.) are also allowed as date separators.+-- The year may be omitted if a default year has been set.+-- Seconds are optional.+-- The timezone is optional and ignored (the time is always interpreted as a local time).+-- Leading zeroes may be omitted (except in a timezone).+datetimep :: ErroringJournalParser LocalTime+datetimep = do+  day <- datep+  lift $ some spacenonewline+  h <- some digitChar+  let h' = read h+  guard $ h' >= 0 && h' <= 23+  char ':'+  m <- some digitChar+  let m' = read m+  guard $ m' >= 0 && m' <= 59+  s <- optional $ char ':' >> some digitChar+  let s' = case s of Just sstr -> read sstr+                     Nothing   -> 0+  guard $ s' >= 0 && s' <= 59+  {- tz <- -}+  optional $ do+                   plusminus <- oneOf ("-+" :: [Char])+                   d1 <- digitChar+                   d2 <- digitChar+                   d3 <- digitChar+                   d4 <- digitChar+                   return $ plusminus:d1:d2:d3:d4:""+  -- ltz <- liftIO $ getCurrentTimeZone+  -- let tz' = maybe ltz (fromMaybe ltz . parseTime defaultTimeLocale "%z") tz+  -- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')+  return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')++secondarydatep :: Day -> ErroringJournalParser Day+secondarydatep primarydate = do+  char '='+  -- kludgy way to use primary 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 -- XXX+        -- mapM setYear <$> y+        return r+  withDefaultYear primarydate datep++-- |+-- >> parsewith twoorthreepartdatestringp "2016/01/2"+-- Right "2016/01/2"+-- twoorthreepartdatestringp = do+--   n1 <- some digitChar+--   c <- datesepchar+--   n2 <- some digitChar+--   mn3 <- optional $ char c >> some digitChar+--   return $ n1 ++ c:n2 ++ maybe "" (c:) mn3++--- ** account names++-- | Parse an account name, then apply any parent account prefix and/or account aliases currently in effect.+modifiedaccountnamep :: ErroringJournalParser AccountName+modifiedaccountnamep = do+  parent <- getParentAccount+  aliases <- getAccountAliases+  a <- lift accountnamep+  return $+    accountNameApplyAliases aliases $+     -- XXX accountNameApplyAliasesMemo ? doesn't seem to make a difference+    joinAccountNames parent+    a++-- | Parse an account name. Account names start with a non-space, may+-- have single spaces inside them, and are terminated by two or more+-- spaces (or end of input). Also they have one or more components of+-- at least one character, separated by the account separator char.+-- (This parser will also consume one following space, if present.)+accountnamep :: TextParser m AccountName+accountnamep = do+    astr <- do+      c <- nonspace+      cs <- striptrailingspace <$> many (nonspace <|> singlespace)+      return $ c:cs+    let a = T.pack astr+    when (accountNameFromComponents (accountNameComponents a) /= a)+         (fail $ "account name seems ill-formed: "++astr)+    return a+    where+      singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})+      striptrailingspace "" = ""+      striptrailingspace s  = if last s == ' ' then init s else s++-- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace+--     <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"++--- ** amounts++-- | Parse whitespace then an amount, with an optional left or right+-- currency symbol and optional price, or return the special+-- "missing" marker amount.+spaceandamountormissingp :: ErroringJournalParser MixedAmount+spaceandamountormissingp =+  try (do+        lift $ some spacenonewline+        (Mixed . (:[])) `fmap` amountp <|> return missingmixedamt+      ) <|> return missingmixedamt++#ifdef TESTS+assertParseEqual' :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion+assertParseEqual' parse expected = either (assertFailure.show) (`is'` expected) parse++is' :: (Eq a, Show a) => a -> a -> Assertion+a `is'` e = assertEqual e a++test_spaceandamountormissingp = do+    assertParseEqual' (parseWithState mempty spaceandamountormissingp " $47.18") (Mixed [usd 47.18])+    assertParseEqual' (parseWithState mempty spaceandamountormissingp "$47.18") missingmixedamt+    assertParseEqual' (parseWithState mempty spaceandamountormissingp " ") missingmixedamt+    assertParseEqual' (parseWithState mempty spaceandamountormissingp "") missingmixedamt+#endif++-- | Parse a single-commodity amount, with optional symbol on the left or+-- right, optional unit or total price, and optional (ignored)+-- ledger-style balance assertion or fixed lot price declaration.+amountp :: Monad m => JournalStateParser m Amount+amountp = try leftsymbolamountp <|> try rightsymbolamountp <|> nosymbolamountp++#ifdef TESTS+test_amountp = do+    assertParseEqual' (parseWithState mempty amountp "$47.18") (usd 47.18)+    assertParseEqual' (parseWithState mempty amountp "$1.") (usd 1 `withPrecision` 0)+  -- ,"amount with unit price" ~: do+    assertParseEqual'+     (parseWithState mempty amountp "$10 @ €0.5")+     (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1))+  -- ,"amount with total price" ~: do+    assertParseEqual'+     (parseWithState mempty amountp "$10 @@ €5")+     (usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0))+#endif++-- | Parse an amount from a string, or get an error.+amountp' :: String -> Amount+amountp' s =+  case runParser (evalStateT (amountp <* eof) mempty) "" (T.pack s) of+    Right amt -> amt+    Left err  -> error' $ show err -- XXX should throwError++-- | Parse a mixed amount from a string, or get an error.+mamountp' :: String -> MixedAmount+mamountp' = Mixed . (:[]) . amountp'++signp :: TextParser m String+signp = do+  sign <- optional $ oneOf ("+-" :: [Char])+  return $ case sign of Just '-' -> "-"+                        _        -> ""++leftsymbolamountp :: Monad m => JournalStateParser m Amount+leftsymbolamountp = do+  sign <- lift signp+  c <- lift commoditysymbolp+  sp <- lift $ many spacenonewline+  (q,prec,mdec,mgrps) <- lift numberp+  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}+  p <- priceamountp+  let applysign = if sign=="-" then negate else id+  return $ applysign $ Amount c q p s+  <?> "left-symbol amount"++rightsymbolamountp :: Monad m => JournalStateParser m Amount+rightsymbolamountp = do+  (q,prec,mdec,mgrps) <- lift numberp+  sp <- lift $ many spacenonewline+  c <- lift commoditysymbolp+  p <- priceamountp+  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}+  return $ Amount c q p s+  <?> "right-symbol amount"++nosymbolamountp :: Monad m => JournalStateParser m Amount+nosymbolamountp = do+  (q,prec,mdec,mgrps) <- lift numberp+  p <- priceamountp+  -- apply the most recently seen default commodity and style to this commodityless amount+  defcs <- getDefaultCommodityAndStyle+  let (c,s) = case defcs of+        Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})+        Nothing          -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})+  return $ Amount c q p s+  <?> "no-symbol amount"++commoditysymbolp :: TextParser m CommoditySymbol+commoditysymbolp = (quotedcommoditysymbolp <|> simplecommoditysymbolp) <?> "commodity symbol"++quotedcommoditysymbolp :: TextParser m CommoditySymbol+quotedcommoditysymbolp = do+  char '"'+  s <- some $ noneOf (";\n\"" :: [Char])+  char '"'+  return $ T.pack s++simplecommoditysymbolp :: TextParser m CommoditySymbol+simplecommoditysymbolp = T.pack <$> some (noneOf nonsimplecommoditychars)++priceamountp :: Monad m => JournalStateParser m Price+priceamountp =+    try (do+          lift (many spacenonewline)+          char '@'+          try (do+                char '@'+                lift (many spacenonewline)+                a <- amountp -- XXX can parse more prices ad infinitum, shouldn't+                return $ TotalPrice a)+           <|> (do+            lift (many spacenonewline)+            a <- amountp -- XXX can parse more prices ad infinitum, shouldn't+            return $ UnitPrice a))+         <|> return NoPrice++partialbalanceassertionp :: ErroringJournalParser (Maybe MixedAmount)+partialbalanceassertionp =+    try (do+          lift (many spacenonewline)+          char '='+          lift (many spacenonewline)+          a <- amountp -- XXX should restrict to a simple amount+          return $ Just $ Mixed [a])+         <|> return Nothing++-- balanceassertion :: Monad m => TextParser m (Maybe MixedAmount)+-- balanceassertion =+--     try (do+--           lift (many spacenonewline)+--           string "=="+--           lift (many spacenonewline)+--           a <- amountp -- XXX should restrict to a simple amount+--           return $ Just $ Mixed [a])+--          <|> return Nothing++-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices+fixedlotpricep :: ErroringJournalParser (Maybe Amount)+fixedlotpricep =+    try (do+          lift (many spacenonewline)+          char '{'+          lift (many spacenonewline)+          char '='+          lift (many spacenonewline)+          a <- amountp -- XXX should restrict to a simple amount+          lift (many spacenonewline)+          char '}'+          return $ Just a)+         <|> return Nothing++-- | Parse a string representation of a number for its value and display+-- attributes.+--+-- Some international number formats are accepted, eg either period or comma+-- may be used for the decimal point, and the other of these may be used for+-- separating digit groups in the integer part. See+-- http://en.wikipedia.org/wiki/Decimal_separator for more examples.+--+-- This returns: the parsed numeric value, the precision (number of digits+-- seen following the decimal point), the decimal point character used if any,+-- and the digit group style if any.+--+numberp :: TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)+numberp = do+  -- a number is an optional sign followed by a sequence of digits possibly+  -- interspersed with periods, commas, or both+  -- ptrace "numberp"+  sign <- signp+  parts <- some $ choice' [some digitChar, some $ char ',', some $ char '.']+  dbg8 "numberp parsed" (sign,parts) `seq` return ()++  -- check the number is well-formed and identify the decimal point and digit+  -- group separator characters used, if any+  let (numparts, puncparts) = partition numeric parts+      (ok, mdecimalpoint, mseparator) =+          case (numparts, puncparts) of+            ([],_)     -> (False, Nothing, Nothing)  -- no digits, not ok+            (_,[])     -> (True, Nothing, Nothing)   -- digits with no punctuation, ok+            (_,[[d]])  -> (True, Just d, Nothing)    -- just a single punctuation of length 1, assume it's a decimal point+            (_,[_])    -> (False, Nothing, Nothing)  -- a single punctuation of some other length, not ok+            (_,_:_:_)  ->                                       -- two or more punctuations+              let (s:ss, d) = (init puncparts, last puncparts)  -- the leftmost is a separator and the rightmost may be a decimal point+              in if any ((/=1).length) puncparts               -- adjacent punctuation chars, not ok+                    || any (s/=) ss                            -- separator chars vary, not ok+                    || head parts == s                        -- number begins with a separator char, not ok+                 then (False, Nothing, Nothing)+                 else if s == d+                      then (True, Nothing, Just $ head s)       -- just one kind of punctuation - must be separators+                      else (True, Just $ head d, Just $ head s) -- separator(s) and a decimal point+  unless ok $ fail $ "number seems ill-formed: "++concat parts++  -- get the digit group sizes and digit group style if any+  let (intparts',fracparts') = span ((/= mdecimalpoint) . Just . head) parts+      (intparts, fracpart) = (filter numeric intparts', filter numeric fracparts')+      groupsizes = reverse $ case map length intparts of+                               (a:b:cs) | a < b -> b:cs+                               gs               -> gs+      mgrps = (`DigitGroups` groupsizes) <$> mseparator++  -- put the parts back together without digit group separators, get the precision and parse the value+  let int = concat $ "":intparts+      frac = concat $ "":fracpart+      precision = length frac+      int' = if null int then "0" else int+      frac' = if null frac then "0" else frac+      quantity = read $ sign++int'++"."++frac' -- this read should never fail++  return $ dbg8 "numberp quantity,precision,mdecimalpoint,mgrps" (quantity,precision,mdecimalpoint,mgrps)+  <?> "numberp"+  where+    numeric = isNumber . headDef '_'++-- test_numberp = do+--       let s `is` n = assertParseEqual (parseWithState mempty numberp s) n+--           assertFails = assertBool . isLeft . parseWithState mempty numberp+--       assertFails ""+--       "0"          `is` (0, 0, '.', ',', [])+--       "1"          `is` (1, 0, '.', ',', [])+--       "1.1"        `is` (1.1, 1, '.', ',', [])+--       "1,000.1"    `is` (1000.1, 1, '.', ',', [3])+--       "1.00.000,1" `is` (100000.1, 1, ',', '.', [3,2])+--       "1,000,000"  `is` (1000000, 0, '.', ',', [3,3])+--       "1."         `is` (1,   0, '.', ',', [])+--       "1,"         `is` (1,   0, ',', '.', [])+--       ".1"         `is` (0.1, 1, '.', ',', [])+--       ",1"         `is` (0.1, 1, ',', '.', [])+--       assertFails "1,000.000,1"+--       assertFails "1.000,000.1"+--       assertFails "1,000.000.1"+--       assertFails "1,,1"+--       assertFails "1..1"+--       assertFails ".1,"+--       assertFails ",1."++--- ** comments++multilinecommentp :: ErroringJournalParser ()+multilinecommentp = do+  string "comment" >> lift (many spacenonewline) >> newline+  go+  where+    go = try (eof <|> (string "end comment" >> newline >> return ()))+         <|> (anyLine >> go)+    anyLine = anyChar `manyTill` newline++emptyorcommentlinep :: ErroringJournalParser ()+emptyorcommentlinep = do+  lift (many spacenonewline) >> (commentp <|> (lift (many spacenonewline) >> newline >> return ""))+  return ()++-- | Parse a possibly multi-line comment following a semicolon.+followingcommentp :: ErroringJournalParser Text+followingcommentp =+  -- ptrace "followingcommentp"+  do samelinecomment <- lift (many spacenonewline) >> (try semicoloncommentp <|> (newline >> return ""))+     newlinecomments <- many (try (lift (some spacenonewline) >> semicoloncommentp))+     return $ T.unlines $ samelinecomment:newlinecomments++-- | Parse a possibly multi-line comment following a semicolon, and+-- any tags and/or posting dates within it. Posting dates can be+-- expressed with "date"/"date2" tags and/or bracketed dates.  The+-- dates are parsed in full here so that errors are reported in the+-- right position. Missing years can be inferred if a default date is+-- provided.+--+-- >>> rejp (followingcommentandtagsp (Just $ fromGregorian 2000 1 2)) "; a:b, date:3/4, [=5/6]"+-- Right ("a:b, date:3/4, [=5/6]\n",[("a","b"),("date","3/4")],Just 2000-03-04,Just 2000-05-06)+--+-- Year unspecified and no default provided -> unknown year error, at correct position:+-- >>> rejp (followingcommentandtagsp Nothing) "  ;    xxx   date:3/4\n  ; second line"+-- Left ...1:22...partial date 3/4 found, but the current year is unknown...+--+-- Date tag value contains trailing text - forgot the comma, confused:+-- the syntaxes ?  We'll accept the leading date anyway+-- >>> rejp (followingcommentandtagsp (Just $ fromGregorian 2000 1 2)) "; date:3/4=5/6"+-- Right ("date:3/4=5/6\n",[("date","3/4=5/6")],Just 2000-03-04,Nothing)+--+followingcommentandtagsp :: Maybe Day -> ErroringJournalParser (Text, [Tag], Maybe Day, Maybe Day)+followingcommentandtagsp mdefdate = do+  -- pdbg 0 "followingcommentandtagsp"++  -- Parse a single or multi-line comment, starting on this line or the next one.+  -- Save the starting position and preserve all whitespace for the subsequent re-parsing,+  -- to get good error positions.+  startpos <- getPosition+  commentandwhitespace :: String <- do+    let semicoloncommentp' = (:) <$> char ';' <*> anyChar `manyTill` eolof+    sp1 <- lift (many spacenonewline)+    l1  <- try (lift semicoloncommentp') <|> (newline >> return "")+    ls  <- lift . many $ try ((++) <$> some spacenonewline <*> semicoloncommentp')+    return $ unlines $ (sp1 ++ l1) : ls+  let comment = T.pack $ unlines $ map (lstrip . dropWhile (==';') . strip) $ lines commentandwhitespace+  -- pdbg 0 $ "commentws:"++show commentandwhitespace+  -- pdbg 0 $ "comment:"++show comment++  -- Reparse the comment for any tags.+  tags <- case runTextParser (setPosition startpos >> tagsp) $ T.pack commentandwhitespace of+            Right ts -> return ts+            Left e   -> throwError $ parseErrorPretty e+  -- pdbg 0 $ "tags: "++show tags++  -- Reparse the comment for any posting dates. Use the transaction date for defaults, if provided.+  epdates <- liftIO $ rejp (setPosition startpos >> postingdatesp mdefdate) $ T.pack commentandwhitespace+  pdates <- case epdates of+              Right ds -> return ds+              Left e   -> throwError e+  -- pdbg 0 $ "pdates: "++show pdates+  let mdate  = headMay $ map snd $ filter ((=="date").fst)  pdates+      mdate2 = headMay $ map snd $ filter ((=="date2").fst) pdates++  return (comment, tags, mdate, mdate2)++commentp :: ErroringJournalParser Text+commentp = commentStartingWithp commentchars++commentchars :: [Char]+commentchars = "#;*"++semicoloncommentp :: ErroringJournalParser Text+semicoloncommentp = commentStartingWithp ";"++commentStartingWithp :: [Char] -> ErroringJournalParser Text+commentStartingWithp cs = do+  -- ptrace "commentStartingWith"+  oneOf cs+  lift (many spacenonewline)+  l <- anyChar `manyTill` (lift eolof)+  optional newline+  return $ T.pack l++--- ** tags++-- | Extract any tags (name:value ended by comma or newline) embedded in a string.+--+-- >>> commentTags "a b:, c:c d:d, e"+-- [("b",""),("c","c d:d")]+--+-- >>> commentTags "a [1/1/1] [1/1] [1], [=1/1/1] [=1/1] [=1] [1/1=1/1/1] [1=1/1/1] b:c"+-- [("b","c")]+--+-- --[("date","1/1/1"),("date","1/1"),("date2","1/1/1"),("date2","1/1"),("date","1/1"),("date2","1/1/1"),("date","1"),("date2","1/1/1")]+--+-- >>> commentTags "\na b:, \nd:e, f"+-- [("b",""),("d","e")]+--+commentTags :: Text -> [Tag]+commentTags s =+  case runTextParser tagsp s of+    Right r -> r+    Left _  -> [] -- shouldn't happen++-- | Parse all tags found in a string.+tagsp :: Parser [Tag]+tagsp = -- do+  -- pdbg 0 $ "tagsp"+  many (try (nontagp >> tagp))++-- | Parse everything up till the first tag.+--+-- >>> rtp nontagp "\na b:, \nd:e, f"+-- Right "\na "+nontagp :: Parser String+nontagp = -- do+  -- pdbg 0 "nontagp"+  -- anyChar `manyTill` (lookAhead (try (tagorbracketeddatetagsp Nothing >> return ()) <|> eof))+  anyChar `manyTill` lookAhead (try (void tagp) <|> eof)+  -- XXX costly ?++-- | Tags begin with a colon-suffixed tag name (a word beginning with+-- a letter) and are followed by a tag value (any text up to a comma+-- or newline, whitespace-stripped).+--+-- >>> rtp tagp "a:b b , c AuxDate: 4/2"+-- Right ("a","b b")+--+tagp :: Parser Tag+tagp = do+  -- pdbg 0 "tagp"+  n <- tagnamep+  v <- tagvaluep+  return (n,v)++-- |+-- >>> rtp tagnamep "a:"+-- Right "a"+tagnamep :: Parser Text+tagnamep = -- do+  -- pdbg 0 "tagnamep"+  T.pack <$> some (noneOf (": \t\n" :: [Char])) <* char ':'++tagvaluep :: TextParser m Text+tagvaluep = do+  -- ptrace "tagvalue"+  v <- anyChar `manyTill` (void (try (char ',')) <|> eolof)+  return $ T.pack $ strip $ reverse $ dropWhile (==',') $ reverse $ strip v++--- ** posting dates++-- | Parse all posting dates found in a string. Posting dates can be+-- expressed with date/date2 tags and/or bracketed dates.  The dates+-- are parsed fully to give useful errors. Missing years can be+-- inferred only if a default date is provided.+--+postingdatesp :: Maybe Day -> ErroringJournalParser [(TagName,Day)]+postingdatesp mdefdate = do+  -- pdbg 0 $ "postingdatesp"+  let p = ((:[]) <$> datetagp mdefdate) <|> bracketeddatetagsp mdefdate+      nonp =+         many (notFollowedBy p >> anyChar)+         -- anyChar `manyTill` (lookAhead (try (p >> return ()) <|> eof))+  concat <$> many (try (nonp >> p))++--- ** date tags++-- | Date tags are tags with name "date" or "date2". Their value is+-- parsed as a date, using the provided default date if any for+-- inferring a missing year if needed. Any error in date parsing is+-- reported and terminates parsing.+--+-- >>> rejp (datetagp Nothing) "date: 2000/1/2 "+-- Right ("date",2000-01-02)+--+-- >>> rejp (datetagp (Just $ fromGregorian 2001 2 3)) "date2:3/4"+-- Right ("date2",2001-03-04)+--+-- >>> rejp (datetagp Nothing) "date:  3/4"+-- Left ...1:9...partial date 3/4 found, but the current year is unknown...+--+datetagp :: Maybe Day -> ErroringJournalParser (TagName,Day)+datetagp mdefdate = do+  -- pdbg 0 "datetagp"+  string "date"+  n <- T.pack . fromMaybe "" <$> optional (string "2")+  char ':'+  startpos <- getPosition+  v <- lift tagvaluep+  -- re-parse value as a date.+  j <- get+  let ep :: Either (ParseError Char Dec) Day+      ep = parseWithState'+             j{jparsedefaultyear=first3.toGregorian <$> mdefdate}+             -- The value extends to a comma, newline, or end of file.+             -- It seems like ignoring any extra stuff following a date+             -- gives better errors here.+             (do+                 setPosition startpos+                 datep) -- <* eof)+             v+  case ep+    of Left e  -> throwError $ parseErrorPretty e+       Right d -> return ("date"<>n, d)++--- ** bracketed dates++-- tagorbracketeddatetagsp :: Monad m => Maybe Day -> TextParser u m [Tag]+-- tagorbracketeddatetagsp mdefdate =+--   bracketeddatetagsp mdefdate <|> ((:[]) <$> tagp)++-- | Parse Ledger-style bracketed posting dates ([DATE=DATE2]), as+-- "date" and/or "date2" tags. Anything that looks like an attempt at+-- this (a square-bracketed sequence of 0123456789/-.= containing at+-- least one digit and one date separator) is also parsed, and will+-- throw an appropriate error.+--+-- The dates are parsed in full here so that errors are reported in+-- the right position. A missing year in DATE can be inferred if a+-- default date is provided. A missing year in DATE2 will be inferred+-- from DATE.+--+-- >>> rejp (bracketeddatetagsp Nothing) "[2016/1/2=3/4]"+-- Right [("date",2016-01-02),("date2",2016-03-04)]+--+-- >>> rejp (bracketeddatetagsp Nothing) "[1]"+-- Left ...not a bracketed date...+--+-- >>> rejp (bracketeddatetagsp Nothing) "[2016/1/32]"+-- Left ...1:11:...bad date: 2016/1/32...+--+-- >>> rejp (bracketeddatetagsp Nothing) "[1/31]"+-- Left ...1:6:...partial date 1/31 found, but the current year is unknown...+--+-- >>> rejp (bracketeddatetagsp Nothing) "[0123456789/-.=/-.=]"+-- Left ...1:15:...bad date, different separators...+--+bracketeddatetagsp :: Maybe Day -> ErroringJournalParser [(TagName, Day)]+bracketeddatetagsp mdefdate = do+  -- pdbg 0 "bracketeddatetagsp"+  char '['+  startpos <- getPosition+  let digits = "0123456789"+  s <- some (oneOf $ '=':digits++datesepchars)+  char ']'+  unless (any (`elem` s) digits && any (`elem` datesepchars) s) $+    fail "not a bracketed date"++  -- looks sufficiently like a bracketed date, now we+  -- re-parse as dates and throw any errors+  j <- get+  let ep :: Either (ParseError Char Dec) (Maybe Day, Maybe Day)+      ep = parseWithState'+             j{jparsedefaultyear=first3.toGregorian <$> mdefdate}+             (do+               setPosition startpos+               md1 <- optional datep+               maybe (return ()) (setYear.first3.toGregorian) md1+               md2 <- optional $ char '=' >> datep+               eof+               return (md1,md2)+             )+             (T.pack s)+  case ep+    of Left e          -> throwError $ parseErrorPretty e+       Right (md1,md2) -> return $ catMaybes+         [("date",) <$> md1, ("date2",) <$> md2]+
Hledger/Read/CsvReader.hs view
@@ -6,6 +6,11 @@ -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}  module Hledger.Read.CsvReader (   -- * Reader@@ -25,11 +30,15 @@ import Control.Exception hiding (try) import Control.Monad import Control.Monad.Except+import Control.Monad.State.Strict (StateT, get, modify', evalStateT) -- import Test.HUnit import Data.Char (toLower, isDigit, isSpace) import Data.List.Compat import Data.Maybe import Data.Ord+import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar (Day) #if MIN_VERSION_time(1,5,0) import Data.Time.Format (parseTimeM, defaultTimeLocale)@@ -41,17 +50,17 @@ import System.Directory (doesFileExist) import System.FilePath import System.IO (stderr)-import Test.HUnit+import Test.HUnit hiding (State) import Text.CSV (parseCSV, CSV)-import Text.Parsec hiding (parse)-import Text.Parsec.Pos-import Text.Parsec.Error+import Text.Megaparsec hiding (parse, State)+import Text.Megaparsec.Text+import qualified Text.Parsec as Parsec import Text.Printf (hPrintf,printf)  import Hledger.Data import Hledger.Utils.UTF8IOCompat (getContents) import Hledger.Utils-import Hledger.Read.JournalReader (amountp, statusp, genericSourcePos)+import Hledger.Read.Common (amountp, statusp, genericSourcePos)   reader :: Reader@@ -61,18 +70,19 @@ format = "csv"  -- | Does the given file path and data look like it might be CSV ?-detect :: FilePath -> String -> Bool-detect f s+detect :: FilePath -> Text -> Bool+detect f t   | f /= "-"  = takeExtension f == '.':format  -- from a file: yes if the extension is .csv-  | otherwise = length (filter (==',') s) >= 2 -- from stdin: yes if there are two or more commas+  | otherwise = T.length (T.filter (==',') t) >= 2 -- from stdin: yes if there are two or more commas  -- | Parse and post-process a "Journal" from CSV data, or give an error. -- XXX currently ignores the string and reads from the file path-parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal-parse rulesfile _ f s = do-  r <- liftIO $ readJournalFromCsv rulesfile f s+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal+parse rulesfile _ f t = do+  r <- liftIO $ readJournalFromCsv rulesfile f t   case r of Left e -> throwError e-            Right j -> return j+            Right j -> return $ journalNumberAndTieTransactions j+-- XXX does not use parseAndFinaliseJournal like the other readers  -- | Read a Journal from the given CSV data (and filename, used for error -- messages), or return an error. Proceed as follows:@@ -85,7 +95,7 @@ -- 4. parse the rules file -- 5. convert the CSV records to a journal using the rules -- @-readJournalFromCsv :: Maybe FilePath -> FilePath -> String -> IO (Either String Journal)+readJournalFromCsv :: Maybe FilePath -> FilePath -> Text -> IO (Either String Journal) readJournalFromCsv Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin" readJournalFromCsv mrulesfile csvfile csvdata =  handle (\e -> return $ Left $ show (e :: IOException)) $ do@@ -100,7 +110,7 @@   rules_ <- liftIO $ runExceptT $ parseRulesFile rulesfile   let rules = case rules_ of               Right (t::CsvRules) -> t-              Left err -> throwerr $ show err+              Left err -> throwerr err   dbg2IO "rules" rules    -- apply skip directive@@ -115,7 +125,7 @@   records <- (either throwerr id .               dbg2 "validateCsv" . validateCsv skip .               dbg2 "parseCsv")-             `fmap` parseCsv parsecfilename csvdata+             `fmap` parseCsv parsecfilename (T.unpack csvdata)   dbg1IO "first 3 csv records" $ take 3 records    -- identify header lines@@ -124,7 +134,12 @@    -- convert to transactions and return as a journal   let txns = snd $ mapAccumL-                     (\pos r -> (pos, transactionFromCsvRecord (incSourceLine pos 1) rules r))+                     (\pos r -> (pos,+                                 transactionFromCsvRecord+                                   (let SourcePos name line col =  pos in+                                    SourcePos name (unsafePos $ unPos line + 1) col)+                                   rules+                                    r))                      (initialPos parsecfilename) records    -- heuristic: if the records appear to have been in reverse date order,@@ -134,14 +149,14 @@             | otherwise = txns   return $ Right nulljournal{jtxns=sortBy (comparing tdate) txns'} -parseCsv :: FilePath -> String -> IO (Either ParseError CSV)+parseCsv :: FilePath -> String -> IO (Either Parsec.ParseError CSV) parseCsv path csvdata =   case path of     "-" -> liftM (parseCSV "(stdin)") getContents     _   -> return $ parseCSV path csvdata  -- | Return the cleaned up and validated CSV data, or an error.-validateCsv :: Int -> Either ParseError CSV -> Either String [CsvRecord]+validateCsv :: Int -> Either Parsec.ParseError CSV -> Either String [CsvRecord] validateCsv _ (Left e) = Left $ show e validateCsv numhdrlines (Right rs) = validate $ drop numhdrlines $ filternulls rs   where@@ -296,6 +311,8 @@   rconditionalblocks :: [ConditionalBlock] } deriving (Show, Eq) +type CsvRulesParser a = StateT CsvRules Parser a+ type DirectiveName    = String type CsvFieldName     = String type CsvFieldIndex    = Int@@ -339,39 +356,42 @@ getDirective :: DirectiveName -> CsvRules -> Maybe FieldTemplate getDirective directivename = lookup directivename . rdirectives +instance ShowErrorComponent String where+  showErrorComponent = id  parseRulesFile :: FilePath -> ExceptT String IO CsvRules parseRulesFile f = do   s <- liftIO $ (readFile' f >>= expandIncludes (takeDirectory f))   let rules = parseCsvRules f s   case rules of-    Left e -> ExceptT $ return $ Left $ show e+    Left e -> ExceptT $ return $ Left $ parseErrorPretty e     Right r -> do                r_ <- liftIO $ runExceptT $ validateRules r                ExceptT $ case r_ of-                 Left e -> return $ Left $ show $ toParseError e+                 Left e -> return $ Left $ parseErrorPretty $ toParseError e                  Right r -> return $ Right r   where-    toParseError s = newErrorMessage (Message s) (initialPos "")+    toParseError :: forall s. Ord s => s -> ParseError Char s+    toParseError s = (mempty :: ParseError Char s) { errorCustom = S.singleton s}  -- | Pre-parse csv rules to interpolate included files, recursively. -- This is a cheap hack to avoid rewriting the existing parser.-expandIncludes :: FilePath -> String -> IO String+expandIncludes :: FilePath -> T.Text -> IO T.Text expandIncludes basedir content = do-  let (ls,rest) = break (isPrefixOf "include") $ lines content+  let (ls,rest) = break (T.isPrefixOf "include") $ T.lines content   case rest of-    [] -> return $ unlines ls-    (('i':'n':'c':'l':'u':'d':'e':f):ls') -> do-      let f'       = basedir </> dropWhile isSpace f+    [] -> return $ T.unlines ls+    ((T.stripPrefix "include" -> Just f):ls') -> do+      let f'       = basedir </> dropWhile isSpace (T.unpack f)           basedir' = takeDirectory f'-      included <- readFile f' >>= expandIncludes basedir'-      return $ unlines [unlines ls, included, unlines ls']-    ls' -> return $ unlines $ ls ++ ls'   -- should never get here+      included <- readFile' f' >>= expandIncludes basedir'+      return $ T.unlines [T.unlines ls, included, T.unlines ls']+    ls' -> return $ T.unlines $ ls ++ ls'   -- should never get here -parseCsvRules :: FilePath -> String -> Either ParseError CsvRules+parseCsvRules :: FilePath -> T.Text -> Either (ParseError Char Dec) CsvRules -- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s parseCsvRules rulesfile s =-  runParser rulesp rules rulesfile s+  runParser (evalStateT rulesp rules) rulesfile s  -- | Return the validated rules, or an error. validateRules :: CsvRules -> ExceptT String IO CsvRules@@ -389,42 +409,42 @@  -- parsers -rulesp :: Stream [Char] m t => ParsecT [Char] CsvRules m CsvRules+rulesp :: CsvRulesParser CsvRules rulesp = do-  many $ choice'-    [blankorcommentlinep                                                    <?> "blank or comment line"-    ,(directivep        >>= modifyState . addDirective)                     <?> "directive"-    ,(fieldnamelistp    >>= modifyState . setIndexesAndAssignmentsFromList) <?> "field name list"-    ,(fieldassignmentp  >>= modifyState . addAssignment)                    <?> "field assignment"-    ,(conditionalblockp >>= modifyState . addConditionalBlock)              <?> "conditional block"+  many $ choiceInState+    [blankorcommentlinep                                                <?> "blank or comment line"+    ,(directivep        >>= modify' . addDirective)                     <?> "directive"+    ,(fieldnamelistp    >>= modify' . setIndexesAndAssignmentsFromList) <?> "field name list"+    ,(fieldassignmentp  >>= modify' . addAssignment)                    <?> "field assignment"+    ,(conditionalblockp >>= modify' . addConditionalBlock)              <?> "conditional block"     ]   eof-  r <- getState+  r <- get   return r{rdirectives=reverse $ rdirectives r           ,rassignments=reverse $ rassignments r           ,rconditionalblocks=reverse $ rconditionalblocks r           } -blankorcommentlinep :: Stream [Char] m t => ParsecT [Char] CsvRules m ()-blankorcommentlinep = pdbg 3 "trying blankorcommentlinep" >> choice' [blanklinep, commentlinep]+blankorcommentlinep :: CsvRulesParser ()+blankorcommentlinep = lift (pdbg 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep] -blanklinep :: Stream [Char] m t => ParsecT [Char] CsvRules m ()-blanklinep = many spacenonewline >> newline >> return () <?> "blank line"+blanklinep :: CsvRulesParser ()+blanklinep = lift (many spacenonewline) >> newline >> return () <?> "blank line" -commentlinep :: Stream [Char] m t => ParsecT [Char] CsvRules m ()-commentlinep = many spacenonewline >> commentcharp >> restofline >> return () <?> "comment line"+commentlinep :: CsvRulesParser ()+commentlinep = lift (many spacenonewline) >> commentcharp >> lift restofline >> return () <?> "comment line" -commentcharp :: Stream [Char] m t => ParsecT [Char] CsvRules m Char-commentcharp = oneOf ";#*"+commentcharp :: CsvRulesParser Char+commentcharp = oneOf (";#*" :: [Char]) -directivep :: Stream [Char] m t => ParsecT [Char] CsvRules m (DirectiveName, String)-directivep = do-  pdbg 3 "trying directive"-  d <- choice' $ map string directives-  v <- (((char ':' >> many spacenonewline) <|> many1 spacenonewline) >> directivevalp)-       <|> (optional (char ':') >> many spacenonewline >> eolof >> return "")+directivep :: CsvRulesParser (DirectiveName, String)+directivep = (do+  lift $ pdbg 3 "trying directive"+  d <- choiceInState $ map string directives+  v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp)+       <|> (optional (char ':') >> lift (many spacenonewline) >> lift eolof >> return "")   return (d,v)-  <?> "directive"+  ) <?> "directive"  directives =   ["date-format"@@ -436,46 +456,46 @@    -- ,"base-currency"   ] -directivevalp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]-directivevalp = anyChar `manyTill` eolof+directivevalp :: CsvRulesParser String+directivevalp = anyChar `manyTill` lift eolof -fieldnamelistp :: Stream [Char] m t => ParsecT [Char] CsvRules m [CsvFieldName]+fieldnamelistp :: CsvRulesParser [CsvFieldName] fieldnamelistp = (do-  pdbg 3 "trying fieldnamelist"+  lift $ pdbg 3 "trying fieldnamelist"   string "fields"   optional $ char ':'-  many1 spacenonewline-  let separator = many spacenonewline >> char ',' >> many spacenonewline-  f <- fromMaybe "" <$> optionMaybe fieldnamep-  fs <- many1 $ (separator >> fromMaybe "" <$> optionMaybe fieldnamep)-  restofline+  lift (some spacenonewline)+  let separator = lift (many spacenonewline) >> char ',' >> lift (many spacenonewline)+  f <- fromMaybe "" <$> optional fieldnamep+  fs <- some $ (separator >> fromMaybe "" <$> optional fieldnamep)+  lift restofline   return $ map (map toLower) $ f:fs   ) <?> "field name list" -fieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]+fieldnamep :: CsvRulesParser String fieldnamep = quotedfieldnamep <|> barefieldnamep -quotedfieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]+quotedfieldnamep :: CsvRulesParser String quotedfieldnamep = do   char '"'-  f <- many1 $ noneOf "\"\n:;#~"+  f <- some $ noneOf ("\"\n:;#~" :: [Char])   char '"'   return f -barefieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]-barefieldnamep = many1 $ noneOf " \t\n,;#~"+barefieldnamep :: CsvRulesParser String+barefieldnamep = some $ noneOf (" \t\n,;#~" :: [Char]) -fieldassignmentp :: Stream [Char] m t => ParsecT [Char] CsvRules m (JournalFieldName, FieldTemplate)+fieldassignmentp :: CsvRulesParser (JournalFieldName, FieldTemplate) fieldassignmentp = do-  pdbg 3 "trying fieldassignment"+  lift $ pdbg 3 "trying fieldassignmentp"   f <- journalfieldnamep   assignmentseparatorp   v <- fieldvalp   return (f,v)   <?> "field assignment" -journalfieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]-journalfieldnamep = pdbg 2 "trying journalfieldnamep" >> choice' (map string journalfieldnames)+journalfieldnamep :: CsvRulesParser String+journalfieldnamep = lift (pdbg 2 "trying journalfieldnamep") >> choiceInState (map string journalfieldnames)  journalfieldnames =   [-- pseudo fields:@@ -494,74 +514,74 @@   ,"comment"   ] -assignmentseparatorp :: Stream [Char] m t => ParsecT [Char] CsvRules m ()+assignmentseparatorp :: CsvRulesParser () assignmentseparatorp = do-  pdbg 3 "trying assignmentseparatorp"+  lift $ pdbg 3 "trying assignmentseparatorp"   choice [-    -- try (many spacenonewline >> oneOf ":="),-    try (many spacenonewline >> char ':'),-    space+    -- try (lift (many spacenonewline) >> oneOf ":="),+    try (lift (many spacenonewline) >> char ':'),+    spaceChar     ]-  _ <- many spacenonewline+  _ <- lift (many spacenonewline)   return () -fieldvalp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]+fieldvalp :: CsvRulesParser String fieldvalp = do-  pdbg 2 "trying fieldval"-  anyChar `manyTill` eolof+  lift $ pdbg 2 "trying fieldvalp"+  anyChar `manyTill` lift eolof -conditionalblockp :: Stream [Char] m t => ParsecT [Char] CsvRules m ConditionalBlock+conditionalblockp :: CsvRulesParser ConditionalBlock conditionalblockp = do-  pdbg 3 "trying conditionalblockp"-  string "if" >> many spacenonewline >> optional newline-  ms <- many1 recordmatcherp-  as <- many (many1 spacenonewline >> fieldassignmentp)+  lift $ pdbg 3 "trying conditionalblockp"+  string "if" >> lift (many spacenonewline) >> optional newline+  ms <- some recordmatcherp+  as <- many (lift (some spacenonewline) >> fieldassignmentp)   when (null as) $     fail "start of conditional block found, but no assignment rules afterward\n(assignment rules in a conditional block should be indented)\n"   return (ms, as)   <?> "conditional block" -recordmatcherp :: Stream [Char] m t => ParsecT [Char] CsvRules m [[Char]]+recordmatcherp :: CsvRulesParser [String] recordmatcherp = do-  pdbg 2 "trying recordmatcherp"+  lift $ pdbg 2 "trying recordmatcherp"   -- pos <- currentPos-  _  <- optional (matchoperatorp >> many spacenonewline >> optional newline)+  _  <- optional (matchoperatorp >> lift (many spacenonewline) >> optional newline)   ps <- patternsp   when (null ps) $     fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"   return ps   <?> "record matcher" -matchoperatorp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]-matchoperatorp = choice' $ map string+matchoperatorp :: CsvRulesParser String+matchoperatorp = choiceInState $ map string   ["~"   -- ,"!~"   -- ,"="   -- ,"!="   ] -patternsp :: Stream [Char] m t => ParsecT [Char] CsvRules m [[Char]]+patternsp :: CsvRulesParser [String] patternsp = do-  pdbg 3 "trying patternsp"+  lift $ pdbg 3 "trying patternsp"   ps <- many regexp   return ps -regexp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]+regexp :: CsvRulesParser String regexp = do-  pdbg 3 "trying regexp"+  lift $ pdbg 3 "trying regexp"   notFollowedBy matchoperatorp-  c <- nonspace-  cs <- anyChar `manyTill` eolof+  c <- lift nonspace+  cs <- anyChar `manyTill` lift eolof   return $ strip $ c:cs  -- fieldmatcher = do --   pdbg 2 "trying fieldmatcher"---   f <- fromMaybe "all" `fmap` (optionMaybe $ do+--   f <- fromMaybe "all" `fmap` (optional $ do --          f' <- fieldname---          many spacenonewline+--          lift (many spacenonewline) --          return f') --   char '~'---   many spacenonewline+--   lift (many spacenonewline) --   ps <- patterns --   let r = "(" ++ intercalate "|" ps ++ ")" --   return (f,r)@@ -605,7 +625,9 @@     status      =       case mfieldtemplate "status" of         Nothing  -> Uncleared-        Just str -> either statuserror id $ runParser (statusp <* eof) nullctx "" $ render str+        Just str -> either statuserror id .+                    runParser (statusp <* eof) "" .+                    T.pack $ render str           where             statuserror err = error' $ unlines               ["error: could not parse \""++str++"\" as a cleared status (should be *, ! or empty)"@@ -617,7 +639,7 @@     precomment  = maybe "" render $ mfieldtemplate "precomment"     currency    = maybe (fromMaybe "" mdefaultcurrency) render $ mfieldtemplate "currency"     amountstr   = (currency++) $ negateIfParenthesised $ getAmountStr rules record-    amount      = either amounterror (Mixed . (:[])) $ runParser (amountp <* eof) nullctx "" amountstr+    amount      = either amounterror (Mixed . (:[])) $ runParser (evalStateT (amountp <* eof) mempty) "" $ T.pack amountstr     amounterror err = error' $ unlines       ["error: could not parse \""++amountstr++"\" as an amount"       ,showRecord record@@ -638,8 +660,8 @@     defaccount2 = case isNegativeMixedAmount amount2 of                    Just True -> "income:unknown"                    _         -> "expenses:unknown"-    account1    = maybe "" render (mfieldtemplate "account1") `or` defaccount1-    account2    = maybe "" render (mfieldtemplate "account2") `or` defaccount2+    account1    = T.pack $ maybe "" render (mfieldtemplate "account1") `or` defaccount1+    account2    = T.pack $ maybe "" render (mfieldtemplate "account2") `or` defaccount2      -- build the transaction     t = nulltransaction{@@ -647,10 +669,10 @@       tdate                    = date',       tdate2                   = mdate2',       tstatus                  = status,-      tcode                    = code,-      tdescription             = description,-      tcomment                 = comment,-      tpreceding_comment_lines = precomment,+      tcode                    = T.pack code,+      tdescription             = T.pack description,+      tcomment                 = T.pack comment,+      tpreceding_comment_lines = T.pack precomment,       tpostings                =         [posting {paccount=account2, pamount=amount2, ptransaction=Just t}         ,posting {paccount=account1, pamount=amount1, ptransaction=Just t}@@ -780,20 +802,23 @@     assertParseEqual (parseCsvRules "unknown" "") rules    -- ,"convert rules parsing: accountrule" ~: do-  --    assertParseEqual (parseWithCtx rules accountrule "A\na\n") -- leading blank line required+  --    assertParseEqual (parseWithState rules accountrule "A\na\n") -- leading blank line required   --                ([("A",Nothing)], "a")    ,"convert rules parsing: trailing comments" ~: do-     assertParse (parseWithCtx rules rulesp "skip\n# \n#\n")+     assertParse (parseWithState' rules rulesp "skip\n# \n#\n")    ,"convert rules parsing: trailing blank lines" ~: do-     assertParse (parseWithCtx rules rulesp "skip\n\n  \n")+     assertParse (parseWithState' rules rulesp "skip\n\n  \n") +  ,"convert rules parsing: empty field value" ~: do+     assertParse (parseWithState' rules rulesp "account1 \nif foo\n  account2 foo\n")+   -- not supported   -- ,"convert rules parsing: no final newline" ~: do-  --    assertParse (parseWithCtx rules csvrulesfile "A\na")-  --    assertParse (parseWithCtx rules csvrulesfile "A\na\n# \n#")-  --    assertParse (parseWithCtx rules csvrulesfile "A\na\n\n  ")+  --    assertParse (parseWithState rules csvrulesfile "A\na")+  --    assertParse (parseWithState rules csvrulesfile "A\na\n# \n#")+  --    assertParse (parseWithState rules csvrulesfile "A\na\n\n  ")                   -- (rules{                  --   -- dateField=Maybe FieldPosition,
Hledger/Read/JournalReader.hs view
@@ -1,1201 +1,722 @@--- {-# OPTIONS_GHC -F -pgmF htfpp #-}-{-# LANGUAGE CPP, RecordWildCards, NoMonoLocalBinds, ScopedTypeVariables #-}-{-# LANGUAGE FlexibleContexts #-}-{-|--A reader for hledger's journal file format-(<http://hledger.org/MANUAL.html#the-journal-file>).  hledger's journal-format is a compatible subset of c++ ledger's-(<http://ledger-cli.org/3.0/doc/ledger3.html#Journal-Format>), so this-reader should handle many ledger files as well. Example:--@-2012\/3\/24 gift-    expenses:gifts  $10-    assets:cash-@---}--module Hledger.Read.JournalReader (-  -- * Reader-  reader,-  -- * Parsers used elsewhere-  parseAndFinaliseJournal,-  genericSourcePos,-  getParentAccount,-  journalp,-  directivep,-  defaultyeardirectivep,-  marketpricedirectivep,-  datetimep,-  codep,-  accountnamep,-  modifiedaccountnamep,-  postingp,-  amountp,-  amountp',-  mamountp',-  numberp,-  statusp,-  emptyorcommentlinep,-  followingcommentp,-  accountaliasp-  -- * Tests-  ,tests_Hledger_Read_JournalReader-#ifdef TESTS-  -- disabled by default, HTF not available on windows-  ,htf_thisModulesTests-  ,htf_Hledger_Read_JournalReader_importedTests-#endif-)-where-import Prelude ()-import Prelude.Compat hiding (readFile)-import qualified Control.Exception as C-import Control.Monad.Compat-import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError, catchError)-import Data.Char (isNumber)-import Data.List.Compat-import Data.List.Split (wordsBy)-import Data.Maybe-import Data.Time.Calendar-import Data.Time.LocalTime-import Safe (headDef, lastDef)-import Test.HUnit-#ifdef TESTS-import Test.Framework-import Text.Parsec.Error-#endif-import Text.Parsec hiding (parse)-import Text.Printf-import System.FilePath-import System.Time (getClockTime)--import Hledger.Data-import Hledger.Utils----- standard reader exports--reader :: Reader-reader = Reader format detect parse--format :: String-format = "journal"---- | Does the given file path and data look like it might be hledger's journal format ?-detect :: FilePath -> String -> Bool-detect f s-  | f /= "-"  = takeExtension f `elem` ['.':format, ".j"]  -- from a file: yes if the extension is .journal or .j-  -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented)-  | otherwise = regexMatches "^[0-9]+.*\n[ \t]+" s---- | Parse and post-process a "Journal" from hledger's journal file--- format, or give an error.-parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal-parse _ = parseAndFinaliseJournal journalp---- parsing utils--genericSourcePos :: SourcePos -> GenericSourcePos-genericSourcePos p = GenericSourcePos (sourceName p) (sourceLine p) (sourceColumn p)---- | Flatten a list of JournalUpdate's (journal-transforming--- monadic actions which can do IO or raise an exception) into a--- single equivalent action.-combineJournalUpdates :: [JournalUpdate] -> JournalUpdate-combineJournalUpdates us = foldl' (flip (.)) id <$> sequence us--- XXX may be contributing to excessive stack use---- cf http://neilmitchell.blogspot.co.uk/2015/09/detecting-space-leaks.html--- $ ./devprof +RTS -K576K -xc--- *** Exception (reporting due to +RTS -xc): (THUNK_STATIC), stack trace: ---   Hledger.Read.JournalReader.combineJournalUpdates.\,---   called from Hledger.Read.JournalReader.combineJournalUpdates,---   called from Hledger.Read.JournalReader.fixedlotprice,---   called from Hledger.Read.JournalReader.partialbalanceassertion,---   called from Hledger.Read.JournalReader.getDefaultCommodityAndStyle,---   called from Hledger.Read.JournalReader.priceamount,---   called from Hledger.Read.JournalReader.nosymbolamount,---   called from Hledger.Read.JournalReader.numberp,---   called from Hledger.Read.JournalReader.rightsymbolamount,---   called from Hledger.Read.JournalReader.simplecommoditysymbol,---   called from Hledger.Read.JournalReader.quotedcommoditysymbol,---   called from Hledger.Read.JournalReader.commoditysymbol,---   called from Hledger.Read.JournalReader.signp,---   called from Hledger.Read.JournalReader.leftsymbolamount,---   called from Hledger.Read.JournalReader.amountp,---   called from Hledger.Read.JournalReader.spaceandamountormissing,---   called from Hledger.Read.JournalReader.accountnamep.singlespace,---   called from Hledger.Utils.Parse.nonspace,---   called from Hledger.Read.JournalReader.accountnamep,---   called from Hledger.Read.JournalReader.getAccountAliases,---   called from Hledger.Read.JournalReader.getParentAccount,---   called from Hledger.Read.JournalReader.modifiedaccountnamep,---   called from Hledger.Read.JournalReader.postingp,---   called from Hledger.Read.JournalReader.postings,---   called from Hledger.Read.JournalReader.commentStartingWith,---   called from Hledger.Read.JournalReader.semicoloncomment,---   called from Hledger.Read.JournalReader.followingcommentp,---   called from Hledger.Read.JournalReader.descriptionp,---   called from Hledger.Read.JournalReader.codep,---   called from Hledger.Read.JournalReader.statusp,---   called from Hledger.Utils.Parse.spacenonewline,---   called from Hledger.Read.JournalReader.secondarydatep,---   called from Hledger.Data.Dates.datesepchar,---   called from Hledger.Read.JournalReader.datep,---   called from Hledger.Read.JournalReader.transaction,---   called from Hledger.Utils.Parse.choice',---   called from Hledger.Read.JournalReader.directive,---   called from Hledger.Read.JournalReader.emptyorcommentlinep,---   called from Hledger.Read.JournalReader.multilinecommentp,---   called from Hledger.Read.JournalReader.journal.journalItem,---   called from Hledger.Read.JournalReader.journal,---   called from Hledger.Read.JournalReader.parseJournalWith,---   called from Hledger.Read.readJournal.tryReaders.firstSuccessOrBestError,---   called from Hledger.Read.readJournal.tryReaders,---   called from Hledger.Read.readJournal,---   called from Main.main,---   called from Main.CAF--- Stack space overflow: current size 33568 bytes.---- | Given a JournalUpdate-generating parsec parser, file path and data string,--- parse and post-process a Journal so that it's ready to use, or give an error.-parseAndFinaliseJournal ::-  (ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate,JournalContext))-  -> Bool -> FilePath -> String -> ExceptT String IO Journal-parseAndFinaliseJournal parser assrt f s = do-  tc <- liftIO getClockTime-  tl <- liftIO getCurrentLocalTime-  y <- liftIO getCurrentYear-  r <- runParserT parser nullctx{ctxYear=Just y} f s-  case r of-    Right (updates,ctx) -> do-                           j <- ap updates (return nulljournal)-                           case journalFinalise tc tl f s ctx assrt j of-                             Right j'  -> return j'-                             Left estr -> throwError estr-    Left e -> throwError $ show e--setYear :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()-setYear y = modifyState (\ctx -> ctx{ctxYear=Just y})--getYear :: Stream [Char] m Char => ParsecT s JournalContext m (Maybe Integer)-getYear = liftM ctxYear getState--setDefaultCommodityAndStyle :: Stream [Char] m Char => (Commodity,AmountStyle) -> ParsecT [Char] JournalContext m ()-setDefaultCommodityAndStyle cs = modifyState (\ctx -> ctx{ctxDefaultCommodityAndStyle=Just cs})--getDefaultCommodityAndStyle :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe (Commodity,AmountStyle))-getDefaultCommodityAndStyle = ctxDefaultCommodityAndStyle `fmap` getState--pushParentAccount :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m ()-pushParentAccount parent = modifyState addParentAccount-    where addParentAccount ctx0 = ctx0 { ctxAccount = parent : ctxAccount ctx0 }--popParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()-popParentAccount = do ctx0 <- getState-                      case ctxAccount ctx0 of-                        [] -> unexpected "End of account block with no beginning"-                        (_:rest) -> setState $ ctx0 { ctxAccount = rest }--getParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m String-getParentAccount = liftM (concatAccountNames . reverse . ctxAccount) getState--addAccountAlias :: Stream [Char] m Char => AccountAlias -> ParsecT [Char] JournalContext m ()-addAccountAlias a = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=a:ctxAliases})--getAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m [AccountAlias]-getAccountAliases = liftM ctxAliases getState--clearAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()-clearAccountAliases = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=[]})--getIndex :: Stream [Char] m Char => ParsecT s JournalContext m Integer-getIndex = liftM ctxTransactionIndex getState--setIndex :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()-setIndex i = modifyState (\ctx -> ctx{ctxTransactionIndex=i})---- parsers---- | Top-level journal parser. Returns a single composite, I/O performing,--- error-raising "JournalUpdate" (and final "JournalContext") which can be--- applied to an empty journal to get the final result.-journalp :: ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate,JournalContext)-journalp = do-  journalupdates <- many journalItem-  eof-  finalctx <- getState-  return $ (combineJournalUpdates journalupdates, finalctx)-    where-      -- As all journal line types can be distinguished by the first-      -- character, excepting transactions versus empty (blank or-      -- comment-only) lines, can use choice w/o try-      journalItem = choice [ directivep-                           , liftM (return . addTransaction) transactionp-                           , liftM (return . addModifierTransaction) modifiertransactionp-                           , liftM (return . addPeriodicTransaction) periodictransactionp-                           , liftM (return . addMarketPrice) marketpricedirectivep-                           , emptyorcommentlinep >> return (return id)-                           , multilinecommentp >> return (return id)-                           ] <?> "journal transaction or directive"---- cf http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives-directivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-directivep = do-  optional $ char '!'-  choice' [-    includedirectivep-   ,aliasdirectivep-   ,endaliasesdirectivep-   ,accountdirectivep-   ,enddirectivep-   ,tagdirectivep-   ,endtagdirectivep-   ,defaultyeardirectivep-   ,defaultcommoditydirectivep-   ,commodityconversiondirectivep-   ,ignoredpricecommoditydirectivep-   ]-  <?> "directive"--includedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-includedirectivep = do-  string "include"-  many1 spacenonewline-  filename <- restofline-  outerState <- getState-  outerPos <- getPosition-  let curdir = takeDirectory (sourceName outerPos)-  let (u::ExceptT String IO (Journal -> Journal, JournalContext)) = do-       filepath <- expandPath curdir filename-       txt <- readFileOrError outerPos filepath-       let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"-       r <- runParserT journalp outerState filepath txt-       case r of-         Right (ju, ctx) -> do-                            u <- combineJournalUpdates [ return $ journalAddFile (filepath,txt)-                                                       , ju-                                                       ] `catchError` (throwError . (inIncluded ++))-                            return (u, ctx)-         Left err -> throwError $ inIncluded ++ show err-       where readFileOrError pos fp =-                ExceptT $ liftM Right (readFile' fp) `C.catch`-                  \e -> return $ Left $ printf "%s reading %s:\n%s" (show pos) fp (show (e::C.IOException))-  r <- liftIO $ runExceptT u-  case r of-    Left err -> return $ throwError err-    Right (ju, _finalparsectx) -> return $ ExceptT $ return $ Right ju--journalAddFile :: (FilePath,String) -> Journal -> Journal-journalAddFile f j@Journal{files=fs} = j{files=fs++[f]}- -- NOTE: first encountered file to left, to avoid a reverse--accountdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-accountdirectivep = do-  string "account"-  many1 spacenonewline-  parent <- accountnamep-  newline-  pushParentAccount parent-  -- return $ return id-  return $ ExceptT $ return $ Right id--enddirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-enddirectivep = do-  string "end"-  popParentAccount-  -- return (return id)-  return $ ExceptT $ return $ Right id--aliasdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-aliasdirectivep = do-  string "alias"-  many1 spacenonewline-  alias <- accountaliasp-  addAccountAlias alias-  return $ return id--accountaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias-accountaliasp = regexaliasp <|> basicaliasp--basicaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias-basicaliasp = do-  -- pdbg 0 "basicaliasp"-  old <- rstrip <$> (many1 $ noneOf "=")-  char '='-  many spacenonewline-  new <- rstrip <$> anyChar `manyTill` eolof  -- don't require a final newline, good for cli options-  return $ BasicAlias old new--regexaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias-regexaliasp = do-  -- pdbg 0 "regexaliasp"-  char '/'-  re <- many1 $ noneOf "/\n\r" -- paranoid: don't try to read past line end-  char '/'-  many spacenonewline-  char '='-  many spacenonewline-  repl <- rstrip <$> anyChar `manyTill` eolof-  return $ RegexAlias re repl--endaliasesdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-endaliasesdirectivep = do-  string "end aliases"-  clearAccountAliases-  return (return id)--tagdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-tagdirectivep = do-  string "tag" <?> "tag directive"-  many1 spacenonewline-  _ <- many1 nonspace-  restofline-  return $ return id--endtagdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-endtagdirectivep = do-  (string "end tag" <|> string "pop") <?> "end tag or pop directive"-  restofline-  return $ return id--defaultyeardirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-defaultyeardirectivep = do-  char 'Y' <?> "default year"-  many spacenonewline-  y <- many1 digit-  let y' = read y-  failIfInvalidYear y-  setYear y'-  return $ return id--defaultcommoditydirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-defaultcommoditydirectivep = do-  char 'D' <?> "default commodity"-  many1 spacenonewline-  Amount{..} <- amountp-  setDefaultCommodityAndStyle (acommodity, astyle)-  restofline-  return $ return id--marketpricedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) MarketPrice-marketpricedirectivep = do-  char 'P' <?> "market price"-  many spacenonewline-  date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored-  many1 spacenonewline-  symbol <- commoditysymbolp-  many spacenonewline-  price <- amountp-  restofline-  return $ MarketPrice date symbol price--ignoredpricecommoditydirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-ignoredpricecommoditydirectivep = do-  char 'N' <?> "ignored-price commodity"-  many1 spacenonewline-  commoditysymbolp-  restofline-  return $ return id--commodityconversiondirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate-commodityconversiondirectivep = do-  char 'C' <?> "commodity conversion"-  many1 spacenonewline-  amountp-  many spacenonewline-  char '='-  many spacenonewline-  amountp-  restofline-  return $ return id--modifiertransactionp :: ParsecT [Char] JournalContext (ExceptT String IO) ModifierTransaction-modifiertransactionp = do-  char '=' <?> "modifier transaction"-  many spacenonewline-  valueexpr <- restofline-  postings <- postingsp-  return $ ModifierTransaction valueexpr postings--periodictransactionp :: ParsecT [Char] JournalContext (ExceptT String IO) PeriodicTransaction-periodictransactionp = do-  char '~' <?> "periodic transaction"-  many spacenonewline-  periodexpr <- restofline-  postings <- postingsp-  return $ PeriodicTransaction periodexpr postings---- | Parse a (possibly unbalanced) transaction.-transactionp :: ParsecT [Char] JournalContext (ExceptT String IO) Transaction-transactionp = do-  -- ptrace "transactionp"-  sourcepos <- genericSourcePos <$> getPosition-  date <- datep <?> "transaction"-  edate <- optionMaybe (secondarydatep date) <?> "secondary date"-  lookAhead (spacenonewline <|> newline) <?> "whitespace or newline"-  status <- statusp <?> "cleared status"-  code <- codep <?> "transaction code"-  description <- descriptionp >>= return . strip-  comment <- try followingcommentp <|> (newline >> return "")-  let tags = tagsInComment comment-  postings <- postingsp-  i' <- (+1) <$> getIndex-  setIndex i'-  return $ txnTieKnot $ Transaction i' sourcepos date edate status code description comment tags postings ""--descriptionp = many (noneOf ";\n")--#ifdef TESTS-test_transactionp = do-    let s `gives` t = do-                        let p = parseWithCtx nullctx transactionp s-                        assertBool $ isRight p-                        let Right t2 = p-                            -- same f = assertEqual (f t) (f t2)-                        assertEqual (tdate t) (tdate t2)-                        assertEqual (tdate2 t) (tdate2 t2)-                        assertEqual (tstatus t) (tstatus t2)-                        assertEqual (tcode t) (tcode t2)-                        assertEqual (tdescription t) (tdescription t2)-                        assertEqual (tcomment t) (tcomment t2)-                        assertEqual (ttags t) (ttags t2)-                        assertEqual (tpreceding_comment_lines t) (tpreceding_comment_lines t2)-                        assertEqual (show $ tpostings t) (show $ tpostings t2)-    -- "0000/01/01\n\n" `gives` nulltransaction-    unlines [-      "2012/05/14=2012/05/15 (code) desc  ; tcomment1",-      "    ; tcomment2",-      "    ; ttag1: val1",-      "    * a         $1.00  ; pcomment1",-      "    ; pcomment2",-      "    ; ptag1: val1",-      "    ; ptag2: val2"-      ]-     `gives`-     nulltransaction{-      tdate=parsedate "2012/05/14",-      tdate2=Just $ parsedate "2012/05/15",-      tstatus=Uncleared,-      tcode="code",-      tdescription="desc",-      tcomment=" tcomment1\n tcomment2\n ttag1: val1\n",-      ttags=[("ttag1","val1")],-      tpostings=[-        nullposting{-          pstatus=Cleared,-          paccount="a",-          pamount=Mixed [usd 1],-          pcomment=" pcomment1\n pcomment2\n ptag1: val1\n  ptag2: val2\n",-          ptype=RegularPosting,-          ptags=[("ptag1","val1"),("ptag2","val2")],-          ptransaction=Nothing-          }-        ],-      tpreceding_comment_lines=""-      }-    unlines [-      "2015/1/1",-      ]-     `gives`-     nulltransaction{-      tdate=parsedate "2015/01/01",-      }--    assertRight $ parseWithCtx nullctx transactionp $ unlines-      ["2007/01/28 coopportunity"-      ,"    expenses:food:groceries                   $47.18"-      ,"    assets:checking                          $-47.18"-      ,""-      ]--    -- transactionp should not parse just a date-    assertLeft $ parseWithCtx nullctx transactionp "2009/1/1\n"--    -- transactionp should not parse just a date and description-    assertLeft $ parseWithCtx nullctx transactionp "2009/1/1 a\n"--    -- transactionp should not parse a following comment as part of the description-    let p = parseWithCtx nullctx transactionp "2009/1/1 a ;comment\n b 1\n"-    assertRight p-    assertEqual "a" (let Right p' = p in tdescription p')--    -- parse transaction with following whitespace line-    assertRight $ parseWithCtx nullctx transactionp $ unlines-        ["2012/1/1"-        ,"  a  1"-        ,"  b"-        ," "-        ]--    let p = parseWithCtx nullctx transactionp $ unlines-             ["2009/1/1 x  ; transaction comment"-             ," a  1  ; posting 1 comment"-             ," ; posting 1 comment 2"-             ," b"-             ," ; posting 2 comment"-             ]-    assertRight p-    assertEqual 2 (let Right t = p in length $ tpostings t)-#endif---- | Parse a date in YYYY/MM/DD format.--- Hyphen (-) and period (.) are also allowed as separators.--- The year may be omitted if a default year has been set.--- Leading zeroes may be omitted.-datep :: Stream [Char] m t => ParsecT [Char] JournalContext m Day-datep = do-  -- hacky: try to ensure precise errors for invalid dates-  -- XXX reported error position is not too good-  -- pos <- genericSourcePos <$> getPosition-  datestr <- do-    c <- digit-    cs <- many $ choice' [digit, datesepchar]-    return $ c:cs-  let sepchars = nub $ sort $ filter (`elem` datesepchars) datestr-  when (length sepchars /= 1) $ fail $ "bad date, different separators used: " ++ datestr-  let dateparts = wordsBy (`elem` datesepchars) datestr-  currentyear <- getYear-  [y,m,d] <- case (dateparts,currentyear) of-              ([m,d],Just y)  -> return [show y,m,d]-              ([_,_],Nothing) -> fail $ "partial date "++datestr++" found, but the current year is unknown"-              ([y,m,d],_)     -> return [y,m,d]-              _               -> fail $ "bad date: " ++ datestr-  let maybedate = fromGregorianValid (read y) (read m) (read d)-  case maybedate of-    Nothing   -> fail $ "bad date: " ++ datestr-    Just date -> return date-  <?> "full or partial date"---- | Parse a date and time in YYYY/MM/DD HH:MM[:SS][+-ZZZZ] format.--- Hyphen (-) and period (.) are also allowed as date separators.--- The year may be omitted if a default year has been set.--- Seconds are optional.--- The timezone is optional and ignored (the time is always interpreted as a local time).--- Leading zeroes may be omitted (except in a timezone).-datetimep :: Stream [Char] m Char => ParsecT [Char] JournalContext m LocalTime-datetimep = do-  day <- datep-  many1 spacenonewline-  h <- many1 digit-  let h' = read h-  guard $ h' >= 0 && h' <= 23-  char ':'-  m <- many1 digit-  let m' = read m-  guard $ m' >= 0 && m' <= 59-  s <- optionMaybe $ char ':' >> many1 digit-  let s' = case s of Just sstr -> read sstr-                     Nothing   -> 0-  guard $ s' >= 0 && s' <= 59-  {- tz <- -}-  optionMaybe $ do-                   plusminus <- oneOf "-+"-                   d1 <- digit-                   d2 <- digit-                   d3 <- digit-                   d4 <- digit-                   return $ plusminus:d1:d2:d3:d4:""-  -- ltz <- liftIO $ getCurrentTimeZone-  -- let tz' = maybe ltz (fromMaybe ltz . parseTime defaultTimeLocale "%z") tz-  -- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')-  return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')--secondarydatep :: Stream [Char] m Char => Day -> ParsecT [Char] JournalContext m Day-secondarydatep primarydate = do-  char '='-  -- kludgy way to use primary date for default year-  let withDefaultYear d p = do-        y <- getYear-        let (y',_,_) = toGregorian d in setYear y'-        r <- p-        when (isJust y) $ setYear $ fromJust y-        return r-  edate <- withDefaultYear primarydate datep-  return edate--statusp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ClearedStatus-statusp =-  choice'-    [ many spacenonewline >> char '*' >> return Cleared-    , many spacenonewline >> char '!' >> return Pending-    , return Uncleared-    ]-    <?> "cleared status"--codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String-codep = try (do { many1 spacenonewline; char '(' <?> "codep"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""---- Parse the following whitespace-beginning lines as postings, posting tags, and/or comments.-postingsp :: Stream [Char] m Char => ParsecT [Char] JournalContext m [Posting]-postingsp = many (try postingp) <?> "postings"---- linebeginningwithspaces :: Stream [Char] m Char => ParsecT [Char] JournalContext m String--- linebeginningwithspaces = do---   sp <- many1 spacenonewline---   c <- nonspace---   cs <- restofline---   return $ sp ++ (c:cs) ++ "\n"--postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting-postingp = do-  many1 spacenonewline-  status <- statusp-  many spacenonewline-  account <- modifiedaccountnamep-  let (ptype, account') = (accountNamePostingType account, unbracket account)-  amount <- spaceandamountormissingp-  massertion <- partialbalanceassertionp-  _ <- fixedlotpricep-  many spacenonewline-  ctx <- getState-  comment <- try followingcommentp <|> (newline >> return "")-  let tags = tagsInComment comment-  -- oh boy-  date <- case dateValueFromTags tags of-        Nothing -> return Nothing-        Just v -> case runParser (datep <* eof) ctx "" v of-                    Right d -> return $ Just d-                    Left err -> parserFail $ show err-  date2 <- case date2ValueFromTags tags of-        Nothing -> return Nothing-        Just v -> case runParser (datep <* eof) ctx "" v of-                    Right d -> return $ Just d-                    Left err -> parserFail $ show err-  return posting-   { pdate=date-   , pdate2=date2-   , pstatus=status-   , paccount=account'-   , pamount=amount-   , pcomment=comment-   , ptype=ptype-   , ptags=tags-   , pbalanceassertion=massertion-   }--#ifdef TESTS-test_postingp = do-    let s `gives` ep = do-                         let parse = parseWithCtx nullctx postingp s-                         assertBool -- "postingp parser"-                           $ isRight parse-                         let Right ap = parse-                             same f = assertEqual (f ep) (f ap)-                         same pdate-                         same pstatus-                         same paccount-                         same pamount-                         same pcomment-                         same ptype-                         same ptags-                         same ptransaction-    "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n" `gives`-      posting{paccount="expenses:food:dining", pamount=Mixed [usd 10], pcomment=" a: a a \n b: b b \n", ptags=[("a","a a"), ("b","b b")]}--    " a  1 ; [2012/11/28]\n" `gives`-      ("a" `post` num 1){pcomment=" [2012/11/28]\n"-                        ,ptags=[("date","2012/11/28")]-                        ,pdate=parsedateM "2012/11/28"}--    " a  1 ; a:a, [=2012/11/28]\n" `gives`-      ("a" `post` num 1){pcomment=" a:a, [=2012/11/28]\n"-                        ,ptags=[("a","a"), ("date2","2012/11/28")]-                        ,pdate=Nothing}--    " a  1 ; a:a\n  ; [2012/11/28=2012/11/29],b:b\n" `gives`-      ("a" `post` num 1){pcomment=" a:a\n [2012/11/28=2012/11/29],b:b\n"-                        ,ptags=[("a","a"), ("date","2012/11/28"), ("date2","2012/11/29"), ("b","b")]-                        ,pdate=parsedateM "2012/11/28"}--    assertBool -- "postingp parses a quoted commodity with numbers"-      (isRight $ parseWithCtx nullctx postingp "  a  1 \"DE123\"\n")--  -- ,"postingp parses balance assertions and fixed lot prices" ~: do-    assertBool (isRight $ parseWithCtx nullctx postingp "  a  1 \"DE123\" =$1 { =2.2 EUR} \n")--    -- let parse = parseWithCtx nullctx postingp " a\n ;next-line comment\n"-    -- assertRight parse-    -- let Right p = parse-    -- assertEqual "next-line comment\n" (pcomment p)-    -- assertEqual (Just nullmixedamt) (pbalanceassertion p)-#endif---- | Parse an account name, then apply any parent account prefix and/or account aliases currently in effect.-modifiedaccountnamep :: Stream [Char] m Char => ParsecT [Char] JournalContext m AccountName-modifiedaccountnamep = do-  parent <- getParentAccount-  aliases <- getAccountAliases-  a <- accountnamep-  return $-    accountNameApplyAliases aliases $-     -- XXX accountNameApplyAliasesMemo ? doesn't seem to make a difference-    joinAccountNames parent-    a---- | Parse an account name. Account names start with a non-space, may--- have single spaces inside them, and are terminated by two or more--- spaces (or end of input). Also they have one or more components of--- at least one character, separated by the account separator char.--- (This parser will also consume one following space, if present.)-accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName-accountnamep = do-    a <- do-      c <- nonspace-      cs <- striptrailingspace <$> many (nonspace <|> singlespace)-      return $ c:cs-    when (accountNameFromComponents (accountNameComponents a) /= a)-         (fail $ "account name seems ill-formed: "++a)-    return a-    where-      singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})-      striptrailingspace "" = ""-      striptrailingspace s  = if last s == ' ' then init s else s---- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace---     <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"---- | Parse whitespace then an amount, with an optional left or right--- currency symbol and optional price, or return the special--- "missing" marker amount.-spaceandamountormissingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m MixedAmount-spaceandamountormissingp =-  try (do-        many1 spacenonewline-        (Mixed . (:[])) `fmap` amountp <|> return missingmixedamt-      ) <|> return missingmixedamt--#ifdef TESTS-assertParseEqual' :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion-assertParseEqual' parse expected = either (assertFailure.show) (`is'` expected) parse--is' :: (Eq a, Show a) => a -> a -> Assertion-a `is'` e = assertEqual e a--test_spaceandamountormissingp = do-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp " $47.18") (Mixed [usd 47.18])-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp "$47.18") missingmixedamt-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp " ") missingmixedamt-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp "") missingmixedamt-#endif---- | Parse a single-commodity amount, with optional symbol on the left or--- right, optional unit or total price, and optional (ignored)--- ledger-style balance assertion or fixed lot price declaration.-amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount-amountp = try leftsymbolamountp <|> try rightsymbolamountp <|> nosymbolamountp--#ifdef TESTS-test_amountp = do-    assertParseEqual' (parseWithCtx nullctx amountp "$47.18") (usd 47.18)-    assertParseEqual' (parseWithCtx nullctx amountp "$1.") (usd 1 `withPrecision` 0)-  -- ,"amount with unit price" ~: do-    assertParseEqual'-     (parseWithCtx nullctx amountp "$10 @ €0.5")-     (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1))-  -- ,"amount with total price" ~: do-    assertParseEqual'-     (parseWithCtx nullctx amountp "$10 @@ €5")-     (usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0))-#endif---- | Parse an amount from a string, or get an error.-amountp' :: String -> Amount-amountp' s =-  case runParser (amountp <* eof) nullctx "" s of-    Right t -> t-    Left err -> error' $ show err---- | Parse a mixed amount from a string, or get an error.-mamountp' :: String -> MixedAmount-mamountp' = Mixed . (:[]) . amountp'--signp :: Stream [Char] m t => ParsecT [Char] JournalContext m String-signp = do-  sign <- optionMaybe $ oneOf "+-"-  return $ case sign of Just '-' -> "-"-                        _        -> ""--leftsymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount-leftsymbolamountp = do-  sign <- signp-  c <- commoditysymbolp-  sp <- many spacenonewline-  (q,prec,mdec,mgrps) <- numberp-  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}-  p <- priceamountp-  let applysign = if sign=="-" then negate else id-  return $ applysign $ Amount c q p s-  <?> "left-symbol amount"--rightsymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount-rightsymbolamountp = do-  (q,prec,mdec,mgrps) <- numberp-  sp <- many spacenonewline-  c <- commoditysymbolp-  p <- priceamountp-  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}-  return $ Amount c q p s-  <?> "right-symbol amount"--nosymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount-nosymbolamountp = do-  (q,prec,mdec,mgrps) <- numberp-  p <- priceamountp-  -- apply the most recently seen default commodity and style to this commodityless amount-  defcs <- getDefaultCommodityAndStyle-  let (c,s) = case defcs of-        Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})-        Nothing          -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})-  return $ Amount c q p s-  <?> "no-symbol amount"--commoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String-commoditysymbolp = (quotedcommoditysymbolp <|> simplecommoditysymbolp) <?> "commodity symbol"--quotedcommoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String-quotedcommoditysymbolp = do-  char '"'-  s <- many1 $ noneOf ";\n\""-  char '"'-  return s--simplecommoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String-simplecommoditysymbolp = many1 (noneOf nonsimplecommoditychars)--priceamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Price-priceamountp =-    try (do-          many spacenonewline-          char '@'-          try (do-                char '@'-                many spacenonewline-                a <- amountp -- XXX can parse more prices ad infinitum, shouldn't-                return $ TotalPrice a)-           <|> (do-            many spacenonewline-            a <- amountp -- XXX can parse more prices ad infinitum, shouldn't-            return $ UnitPrice a))-         <|> return NoPrice--partialbalanceassertionp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Maybe MixedAmount)-partialbalanceassertionp =-    try (do-          many spacenonewline-          char '='-          many spacenonewline-          a <- amountp -- XXX should restrict to a simple amount-          return $ Just $ Mixed [a])-         <|> return Nothing---- balanceassertion :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe MixedAmount)--- balanceassertion =---     try (do---           many spacenonewline---           string "=="---           many spacenonewline---           a <- amountp -- XXX should restrict to a simple amount---           return $ Just $ Mixed [a])---          <|> return Nothing---- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices-fixedlotpricep :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe Amount)-fixedlotpricep =-    try (do-          many spacenonewline-          char '{'-          many spacenonewline-          char '='-          many spacenonewline-          a <- amountp -- XXX should restrict to a simple amount-          many spacenonewline-          char '}'-          return $ Just a)-         <|> return Nothing---- | Parse a string representation of a number for its value and display--- attributes.------ Some international number formats are accepted, eg either period or comma--- may be used for the decimal point, and the other of these may be used for--- separating digit groups in the integer part. See--- http://en.wikipedia.org/wiki/Decimal_separator for more examples.------ This returns: the parsed numeric value, the precision (number of digits--- seen following the decimal point), the decimal point character used if any,--- and the digit group style if any.----numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)-numberp = do-  -- a number is an optional sign followed by a sequence of digits possibly-  -- interspersed with periods, commas, or both-  -- ptrace "numberp"-  sign <- signp-  parts <- many1 $ choice' [many1 digit, many1 $ char ',', many1 $ char '.']-  dbg8 "numberp parsed" (sign,parts) `seq` return ()--  -- check the number is well-formed and identify the decimal point and digit-  -- group separator characters used, if any-  let (numparts, puncparts) = partition numeric parts-      (ok, mdecimalpoint, mseparator) =-          case (numparts, puncparts) of-            ([],_)     -> (False, Nothing, Nothing)  -- no digits, not ok-            (_,[])     -> (True, Nothing, Nothing)   -- digits with no punctuation, ok-            (_,[[d]])  -> (True, Just d, Nothing)    -- just a single punctuation of length 1, assume it's a decimal point-            (_,[_])    -> (False, Nothing, Nothing)  -- a single punctuation of some other length, not ok-            (_,_:_:_)  ->                                       -- two or more punctuations-              let (s:ss, d) = (init puncparts, last puncparts)  -- the leftmost is a separator and the rightmost may be a decimal point-              in if (any ((/=1).length) puncparts               -- adjacent punctuation chars, not ok-                     || any (s/=) ss                            -- separator chars vary, not ok-                     || head parts == s)                        -- number begins with a separator char, not ok-                 then (False, Nothing, Nothing)-                 else if s == d-                      then (True, Nothing, Just $ head s)       -- just one kind of punctuation - must be separators-                      else (True, Just $ head d, Just $ head s) -- separator(s) and a decimal point-  when (not ok) (fail $ "number seems ill-formed: "++concat parts)--  -- get the digit group sizes and digit group style if any-  let (intparts',fracparts') = span ((/= mdecimalpoint) . Just . head) parts-      (intparts, fracpart) = (filter numeric intparts', filter numeric fracparts')-      groupsizes = reverse $ case map length intparts of-                               (a:b:cs) | a < b -> b:cs-                               gs               -> gs-      mgrps = maybe Nothing (Just . (`DigitGroups` groupsizes)) $ mseparator--  -- put the parts back together without digit group separators, get the precision and parse the value-  let int = concat $ "":intparts-      frac = concat $ "":fracpart-      precision = length frac-      int' = if null int then "0" else int-      frac' = if null frac then "0" else frac-      quantity = read $ sign++int'++"."++frac' -- this read should never fail--  return $ dbg8 "numberp quantity,precision,mdecimalpoint,mgrps" (quantity,precision,mdecimalpoint,mgrps)-  <?> "numberp"-  where-    numeric = isNumber . headDef '_'---- test_numberp = do---       let s `is` n = assertParseEqual (parseWithCtx nullctx numberp s) n---           assertFails = assertBool . isLeft . parseWithCtx nullctx numberp---       assertFails ""---       "0"          `is` (0, 0, '.', ',', [])---       "1"          `is` (1, 0, '.', ',', [])---       "1.1"        `is` (1.1, 1, '.', ',', [])---       "1,000.1"    `is` (1000.1, 1, '.', ',', [3])---       "1.00.000,1" `is` (100000.1, 1, ',', '.', [3,2])---       "1,000,000"  `is` (1000000, 0, '.', ',', [3,3])---       "1."         `is` (1,   0, '.', ',', [])---       "1,"         `is` (1,   0, ',', '.', [])---       ".1"         `is` (0.1, 1, '.', ',', [])---       ",1"         `is` (0.1, 1, ',', '.', [])---       assertFails "1,000.000,1"---       assertFails "1.000,000.1"---       assertFails "1,000.000.1"---       assertFails "1,,1"---       assertFails "1..1"---       assertFails ".1,"---       assertFails ",1."---- comment parsers--multilinecommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()-multilinecommentp = do-  string "comment" >> many spacenonewline >> newline-  go-  where-    go = try (eof <|> (string "end comment" >> newline >> return ()))-         <|> (anyLine >> go)-    anyLine = anyChar `manyTill` newline--emptyorcommentlinep :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()-emptyorcommentlinep = do-  many spacenonewline >> (commentp <|> (many spacenonewline >> newline >> return ""))-  return ()--followingcommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String-followingcommentp =-  -- ptrace "followingcommentp"-  do samelinecomment <- many spacenonewline >> (try semicoloncommentp <|> (newline >> return ""))-     newlinecomments <- many (try (many1 spacenonewline >> semicoloncommentp))-     return $ unlines $ samelinecomment:newlinecomments--commentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String-commentp = commentStartingWithp commentchars--commentchars :: [Char]-commentchars = "#;*"--semicoloncommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String-semicoloncommentp = commentStartingWithp ";"--commentStartingWithp :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m String-commentStartingWithp cs = do-  -- ptrace "commentStartingWith"-  oneOf cs-  many spacenonewline-  l <- anyChar `manyTill` eolof-  optional newline-  return l--tagsInComment :: String -> [Tag]-tagsInComment c = concatMap tagsInCommentLine $ lines c'-  where-    c' = ledgerDateSyntaxToTags c--tagsInCommentLine :: String -> [Tag]-tagsInCommentLine = catMaybes . map maybetag . map strip . splitAtElement ','-  where-    maybetag s = case runParser (tagp <* eof) nullctx "" s of-                  Right t -> Just t-                  Left _ -> Nothing--tagp = do-  -- ptrace "tag"-  n <- tagnamep-  v <- tagvaluep-  return (n,v)--tagnamep = do-  -- ptrace "tagname"-  n <- many1 $ noneOf ": \t"-  char ':'-  return n--tagvaluep = do-  -- ptrace "tagvalue"-  v <- anyChar `manyTill` ((char ',' >> return ()) <|> eolof)-  return $ strip $ reverse $ dropWhile (==',') $ reverse $ strip v--ledgerDateSyntaxToTags :: String -> String-ledgerDateSyntaxToTags = regexReplaceBy "\\[[-.\\/0-9=]+\\]" replace-  where-    replace ('[':s) | lastDef ' ' s == ']' = replace' $ init s-    replace s = s--    replace' s | isdate s = datetag s-    replace' ('=':s) | isdate s = date2tag s-    replace' s | last s =='=' && isdate (init s) = datetag (init s)-    replace' s | length ds == 2 && isdate d1 && isdate d1 = datetag d1 ++ date2tag d2-      where-        ds = splitAtElement '=' s-        d1 = headDef "" ds-        d2 = lastDef "" ds-    replace' s = s--    isdate = isJust . parsedateM-    datetag s = "date:"++s++", "-    date2tag s = "date2:"++s++", "--#ifdef TESTS-test_ledgerDateSyntaxToTags = do-     assertEqual "date2:2012/11/28, " $ ledgerDateSyntaxToTags "[=2012/11/28]"-#endif--dateValueFromTags, date2ValueFromTags :: [Tag] -> Maybe String-dateValueFromTags  ts = maybe Nothing (Just . snd) $ find ((=="date") . fst) ts-date2ValueFromTags ts = maybe Nothing (Just . snd) $ find ((=="date2") . fst) ts---tests_Hledger_Read_JournalReader = TestList $ concat [-    -- test_numberp- ]--{- old hunit tests--tests_Hledger_Read_JournalReader = TestList $ concat [-    test_numberp,-    test_amountp,-    test_spaceandamountormissingp,-    test_tagcomment,-    test_inlinecomment,-    test_comments,-    test_ledgerDateSyntaxToTags,-    test_postingp,-    test_transactionp,-    [-   "modifiertransactionp" ~: do-     assertParse (parseWithCtx nullctx modifiertransactionp "= (some value expr)\n some:postings  1\n")--  ,"periodictransactionp" ~: do-     assertParse (parseWithCtx nullctx periodictransactionp "~ (some period expr)\n some:postings  1\n")--  ,"directivep" ~: do-     assertParse (parseWithCtx nullctx directivep "!include /some/file.x\n")-     assertParse (parseWithCtx nullctx directivep "account some:account\n")-     assertParse (parseWithCtx nullctx (directivep >> directivep) "!account a\nend\n")--  ,"comment" ~: do-     assertParse (parseWithCtx nullctx comment "; some comment \n")-     assertParse (parseWithCtx nullctx comment " \t; x\n")-     assertParse (parseWithCtx nullctx comment "#x")--  ,"datep" ~: do-     assertParse (parseWithCtx nullctx datep "2011/1/1")-     assertParseFailure (parseWithCtx nullctx datep "1/1")-     assertParse (parseWithCtx nullctx{ctxYear=Just 2011} datep "1/1")--  ,"datetimep" ~: do-      let p = do {t <- datetimep; eof; return t}-          bad = assertParseFailure . parseWithCtx nullctx p-          good = assertParse . parseWithCtx nullctx p-      bad "2011/1/1"-      bad "2011/1/1 24:00:00"-      bad "2011/1/1 00:60:00"-      bad "2011/1/1 00:00:60"-      good "2011/1/1 00:00"-      good "2011/1/1 23:59:59"-      good "2011/1/1 3:5:7"-      -- timezone is parsed but ignored-      let startofday = LocalTime (fromGregorian 2011 1 1) (TimeOfDay 0 0 (fromIntegral 0))-      assertParseEqual (parseWithCtx nullctx p "2011/1/1 00:00-0800") startofday-      assertParseEqual (parseWithCtx nullctx p "2011/1/1 00:00+1234") startofday--  ,"defaultyeardirectivep" ~: do-     assertParse (parseWithCtx nullctx defaultyeardirectivep "Y 2010\n")-     assertParse (parseWithCtx nullctx defaultyeardirectivep "Y 10001\n")--  ,"marketpricedirectivep" ~:-    assertParseEqual (parseWithCtx nullctx marketpricedirectivep "P 2004/05/01 XYZ $55.00\n") (MarketPrice (parsedate "2004/05/01") "XYZ" $ usd 55)--  ,"ignoredpricecommoditydirectivep" ~: do-     assertParse (parseWithCtx nullctx ignoredpricecommoditydirectivep "N $\n")--  ,"defaultcommoditydirectivep" ~: do-     assertParse (parseWithCtx nullctx defaultcommoditydirectivep "D $1,000.0\n")--  ,"commodityconversiondirectivep" ~: do-     assertParse (parseWithCtx nullctx commodityconversiondirectivep "C 1h = $50.00\n")--  ,"tagdirectivep" ~: do-     assertParse (parseWithCtx nullctx tagdirectivep "tag foo \n")--  ,"endtagdirectivep" ~: do-     assertParse (parseWithCtx nullctx endtagdirectivep "end tag \n")-     assertParse (parseWithCtx nullctx endtagdirectivep "pop \n")--  ,"accountnamep" ~: do-    assertBool "accountnamep parses a normal account name" (isRight $ parsewith accountnamep "a:b:c")-    assertBool "accountnamep rejects an empty inner component" (isLeft $ parsewith accountnamep "a::c")-    assertBool "accountnamep rejects an empty leading component" (isLeft $ parsewith accountnamep ":b:c")-    assertBool "accountnamep rejects an empty trailing component" (isLeft $ parsewith accountnamep "a:b:")--  ,"leftsymbolamountp" ~: do-    assertParseEqual (parseWithCtx nullctx leftsymbolamountp "$1")  (usd 1 `withPrecision` 0)-    assertParseEqual (parseWithCtx nullctx leftsymbolamountp "$-1") (usd (-1) `withPrecision` 0)-    assertParseEqual (parseWithCtx nullctx leftsymbolamountp "-$1") (usd (-1) `withPrecision` 0)--  ,"amount" ~: do-     let -- | compare a parse result with an expected amount, showing the debug representation for clarity-         assertAmountParse parseresult amount =-             (either (const "parse error") showAmountDebug parseresult) ~?= (showAmountDebug amount)-     assertAmountParse (parseWithCtx nullctx amountp "1 @ $2")-       (num 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0))-- ]]--}-+--- * doc+-- Lines beginning "--- *" are collapsible orgstruct nodes. Emacs users,+-- (add-hook 'haskell-mode-hook+--   (lambda () (set-variable 'orgstruct-heading-prefix-regexp "--- " t))+--   'orgstruct-mode)+-- and press TAB on nodes to expand/collapse.++{-|++A reader for hledger's journal file format+(<http://hledger.org/MANUAL.html#the-journal-file>).  hledger's journal+format is a compatible subset of c++ ledger's+(<http://ledger-cli.org/3.0/doc/ledger3.html#Journal-Format>), so this+reader should handle many ledger files as well. Example:++@+2012\/3\/24 gift+    expenses:gifts  $10+    assets:cash+@++Journal format supports the include directive which can read files in+other formats, so the other file format readers need to be importable+here.  Some low-level journal syntax parsers which those readers also+use are therefore defined separately in Hledger.Read.Common, avoiding+import cycles.++-}++--- * module++{-# LANGUAGE CPP, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-}++module Hledger.Read.JournalReader (+--- * exports++  -- * Reader+  reader,++  -- * Parsing utils+  genericSourcePos,+  parseAndFinaliseJournal,+  runJournalParser,+  rjp,+  runErroringJournalParser,+  rejp,++  -- * Parsers used elsewhere+  getParentAccount,+  journalp,+  directivep,+  defaultyeardirectivep,+  marketpricedirectivep,+  datetimep,+  datep,+  -- codep,+  -- accountnamep,+  modifiedaccountnamep,+  postingp,+  -- amountp,+  -- amountp',+  -- mamountp',+  -- numberp,+  statusp,+  emptyorcommentlinep,+  followingcommentp,+  accountaliasp++  -- * Tests+  ,tests_Hledger_Read_JournalReader++)+where+--- * imports+import Prelude ()+import Prelude.Compat hiding (readFile)+import qualified Control.Exception as C+import Control.Monad+import Control.Monad.Except (ExceptT(..), runExceptT, throwError)+import Control.Monad.State.Strict+import qualified Data.Map.Strict as M+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Calendar+import Data.Time.LocalTime+import Safe+import Test.HUnit+#ifdef TESTS+import Test.Framework+import Text.Megaparsec.Error+#endif+import Text.Megaparsec hiding (parse)+import Text.Printf+import System.FilePath++import Hledger.Data+import Hledger.Read.Common+import Hledger.Read.TimeclockReader (timeclockfilep)+import Hledger.Read.TimedotReader (timedotfilep)+import Hledger.Utils++-- $setup+-- >>> :set -XOverloadedStrings++--- * reader++reader :: Reader+reader = Reader format detect parse++format :: String+format = "journal"++-- | Does the given file path and data look like it might be hledger's journal format ?+detect :: FilePath -> Text -> Bool+detect f _t+  | f /= "-"  = takeExtension f `elem` ['.':format, ".j"] -- from a known file name: yes if the extension is this format's name or .j+  | otherwise = True                                      -- from stdin: yes, always attempt to parse stdin as journal data+  -- otherwise = regexMatches "(^|\n)[0-9]+.*\n[ \t]+" $ T.unpack t   -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented)++-- | Parse and post-process a "Journal" from hledger's journal file+-- format, or give an error.+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal+parse _ = parseAndFinaliseJournal journalp++--- * parsers+--- ** journal++-- | A journal parser. Accumulates and returns a "ParsedJournal",+-- which should be finalised/validated before use.+--+-- >>> rejp (journalp <* eof) "2015/1/1\n a  0\n"+-- Right Journal  with 1 transactions, 1 accounts+--+journalp :: ErroringJournalParser ParsedJournal+journalp = do+  many addJournalItemP+  eof+  get++-- | A side-effecting parser; parses any kind of journal item+-- and updates the parse state accordingly.+addJournalItemP :: ErroringJournalParser ()+addJournalItemP =+  -- all journal line types can be distinguished by the first+  -- character, can use choice without backtracking+  choice [+      directivep+    , transactionp          >>= modify' . addTransaction+    , modifiertransactionp  >>= modify' . addModifierTransaction+    , periodictransactionp  >>= modify' . addPeriodicTransaction+    , marketpricedirectivep >>= modify' . addMarketPrice+    , void emptyorcommentlinep+    , void multilinecommentp+    ] <?> "transaction or directive"++--- ** directives++-- | Parse any journal directive and update the parse state accordingly.+-- Cf http://hledger.org/manual.html#directives,+-- http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives+directivep :: ErroringJournalParser ()+directivep = (do+  optional $ char '!'+  choiceInState [+    includedirectivep+   ,aliasdirectivep+   ,endaliasesdirectivep+   ,accountdirectivep+   ,applyaccountdirectivep+   ,commoditydirectivep+   ,endapplyaccountdirectivep+   ,tagdirectivep+   ,endtagdirectivep+   ,defaultyeardirectivep+   ,defaultcommoditydirectivep+   ,commodityconversiondirectivep+   ,ignoredpricecommoditydirectivep+   ]+  ) <?> "directive"++includedirectivep :: ErroringJournalParser ()+includedirectivep = do+  string "include"+  lift (some spacenonewline)+  filename  <- lift restofline+  parentpos <- getPosition+  parentj   <- get+  let childj = newJournalWithParseStateFrom parentj+  (ej :: Either String ParsedJournal) <-+    liftIO $ runExceptT $ do+      let curdir = takeDirectory (sourceName parentpos)+      filepath <- expandPath curdir filename `orRethrowIOError` (show parentpos ++ " locating " ++ filename)+      txt      <- readFileAnyLineEnding filepath `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)+      (ej1::Either (ParseError Char Dec) ParsedJournal) <-+        runParserT+           (evalStateT+              (choiceInState+                 [journalp+                 ,timeclockfilep+                 ,timedotfilep+                 -- can't include a csv file yet, that reader is special+                 ])+              childj)+           filepath txt+      either+        (throwError+          . ((show parentpos ++ " in included file " ++ show filename ++ ":\n") ++)+          . show)+        (return . journalAddFile (filepath, txt))+        ej1+  case ej of+    Left e       -> throwError e+    Right childj -> modify' (\parentj -> childj <> parentj)+    -- discard child's parse info, prepend its (reversed) list data, combine other fields++newJournalWithParseStateFrom :: Journal -> Journal+newJournalWithParseStateFrom j = mempty{+   jparsedefaultyear      = jparsedefaultyear j+  ,jparsedefaultcommodity = jparsedefaultcommodity j+  ,jparseparentaccounts   = jparseparentaccounts j+  ,jparsealiases          = jparsealiases j+  -- ,jparsetransactioncount = jparsetransactioncount j+  ,jparsetimeclockentries = jparsetimeclockentries j+  }++-- | Lift an IO action into the exception monad, rethrowing any IO+-- error with the given message prepended.+orRethrowIOError :: IO a -> String -> ExceptT String IO a+orRethrowIOError io msg =+  ExceptT $+    (Right <$> io)+    `C.catch` \(e::C.IOException) -> return $ Left $ printf "%s:\n%s" msg (show e)++accountdirectivep :: ErroringJournalParser ()+accountdirectivep = do+  string "account"+  lift (some spacenonewline)+  acct <- lift accountnamep+  newline+  _ <- many indentedlinep+  modify' (\j -> j{jaccounts = acct : jaccounts j})++indentedlinep = lift (some spacenonewline) >> (rstrip <$> lift restofline)++-- | Parse a one-line or multi-line commodity directive.+--+-- >>> Right _ <- rejp commoditydirectivep "commodity $1.00"+-- >>> Right _ <- rejp commoditydirectivep "commodity $\n  format $1.00"+-- >>> Right _ <- rejp commoditydirectivep "commodity $\n\n" -- a commodity with no format+-- >>> Right _ <- rejp commoditydirectivep "commodity $1.00\n  format $1.00" -- both, what happens ?+commoditydirectivep :: ErroringJournalParser ()+commoditydirectivep = try commoditydirectiveonelinep <|> commoditydirectivemultilinep++-- | Parse a one-line commodity directive.+--+-- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00"+-- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00 ; blah\n"+commoditydirectiveonelinep :: ErroringJournalParser ()+commoditydirectiveonelinep = do+  string "commodity"+  lift (some spacenonewline)+  Amount{acommodity,astyle} <- amountp+  lift (many spacenonewline)+  _ <- followingcommentp <|> (lift eolof >> return "")+  let comm = Commodity{csymbol=acommodity, cformat=Just astyle}+  modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})++-- | Parse a multi-line commodity directive, containing 0 or more format subdirectives.+--+-- >>> Right _ <- rejp commoditydirectivemultilinep "commodity $ ; blah \n  format $1.00 ; blah"+commoditydirectivemultilinep :: ErroringJournalParser ()+commoditydirectivemultilinep = do+  string "commodity"+  lift (some spacenonewline)+  sym <- lift commoditysymbolp+  _ <- followingcommentp <|> (lift eolof >> return "")+  mformat <- lastMay <$> many (indented $ formatdirectivep sym)+  let comm = Commodity{csymbol=sym, cformat=mformat}+  modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j})+  where+    indented = (lift (some spacenonewline) >>)++-- | Parse a format (sub)directive, throwing a parse error if its+-- symbol does not match the one given.+formatdirectivep :: CommoditySymbol -> ErroringJournalParser AmountStyle+formatdirectivep expectedsym = do+  string "format"+  lift (some spacenonewline)+  pos <- getPosition+  Amount{acommodity,astyle} <- amountp+  _ <- followingcommentp <|> (lift eolof >> return "")+  if acommodity==expectedsym+    then return astyle+    else parserErrorAt pos $+         printf "commodity directive symbol \"%s\" and format directive symbol \"%s\" should be the same" expectedsym acommodity++applyaccountdirectivep :: ErroringJournalParser ()+applyaccountdirectivep = do+  string "apply" >> lift (some spacenonewline) >> string "account"+  lift (some spacenonewline)+  parent <- lift accountnamep+  newline+  pushParentAccount parent++endapplyaccountdirectivep :: ErroringJournalParser ()+endapplyaccountdirectivep = do+  string "end" >> lift (some spacenonewline) >> string "apply" >> lift (some spacenonewline) >> string "account"+  popParentAccount++aliasdirectivep :: ErroringJournalParser ()+aliasdirectivep = do+  string "alias"+  lift (some spacenonewline)+  alias <- lift accountaliasp+  addAccountAlias alias++accountaliasp :: TextParser m AccountAlias+accountaliasp = regexaliasp <|> basicaliasp++basicaliasp :: TextParser m AccountAlias+basicaliasp = do+  -- pdbg 0 "basicaliasp"+  old <- rstrip <$> (some $ noneOf ("=" :: [Char]))+  char '='+  many spacenonewline+  new <- rstrip <$> anyChar `manyTill` eolof  -- don't require a final newline, good for cli options+  return $ BasicAlias (T.pack old) (T.pack new)++regexaliasp :: TextParser m AccountAlias+regexaliasp = do+  -- pdbg 0 "regexaliasp"+  char '/'+  re <- some $ noneOf ("/\n\r" :: [Char]) -- paranoid: don't try to read past line end+  char '/'+  many spacenonewline+  char '='+  many spacenonewline+  repl <- rstrip <$> anyChar `manyTill` eolof+  return $ RegexAlias re repl++endaliasesdirectivep :: ErroringJournalParser ()+endaliasesdirectivep = do+  string "end aliases"+  clearAccountAliases++tagdirectivep :: ErroringJournalParser ()+tagdirectivep = do+  string "tag" <?> "tag directive"+  lift (some spacenonewline)+  _ <- lift $ some nonspace+  lift restofline+  return ()++endtagdirectivep :: ErroringJournalParser ()+endtagdirectivep = do+  (string "end tag" <|> string "pop") <?> "end tag or pop directive"+  lift restofline+  return ()++defaultyeardirectivep :: ErroringJournalParser ()+defaultyeardirectivep = do+  char 'Y' <?> "default year"+  lift (many spacenonewline)+  y <- some digitChar+  let y' = read y+  failIfInvalidYear y+  setYear y'++defaultcommoditydirectivep :: ErroringJournalParser ()+defaultcommoditydirectivep = do+  char 'D' <?> "default commodity"+  lift (some spacenonewline)+  Amount{..} <- amountp+  lift restofline+  setDefaultCommodityAndStyle (acommodity, astyle)++marketpricedirectivep :: ErroringJournalParser MarketPrice+marketpricedirectivep = do+  char 'P' <?> "market price"+  lift (many spacenonewline)+  date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored+  lift (some spacenonewline)+  symbol <- lift commoditysymbolp+  lift (many spacenonewline)+  price <- amountp+  lift restofline+  return $ MarketPrice date symbol price++ignoredpricecommoditydirectivep :: ErroringJournalParser ()+ignoredpricecommoditydirectivep = do+  char 'N' <?> "ignored-price commodity"+  lift (some spacenonewline)+  lift commoditysymbolp+  lift restofline+  return ()++commodityconversiondirectivep :: ErroringJournalParser ()+commodityconversiondirectivep = do+  char 'C' <?> "commodity conversion"+  lift (some spacenonewline)+  amountp+  lift (many spacenonewline)+  char '='+  lift (many spacenonewline)+  amountp+  lift restofline+  return ()++--- ** transactions++modifiertransactionp :: ErroringJournalParser ModifierTransaction+modifiertransactionp = do+  char '=' <?> "modifier transaction"+  lift (many spacenonewline)+  valueexpr <- T.pack <$> lift restofline+  postings <- postingsp Nothing+  return $ ModifierTransaction valueexpr postings++periodictransactionp :: ErroringJournalParser PeriodicTransaction+periodictransactionp = do+  char '~' <?> "periodic transaction"+  lift (many spacenonewline)+  periodexpr <- T.pack <$> lift restofline+  postings <- postingsp Nothing+  return $ PeriodicTransaction periodexpr postings++-- | Parse a (possibly unbalanced) transaction.+transactionp :: ErroringJournalParser Transaction+transactionp = do+  -- ptrace "transactionp"+  sourcepos <- genericSourcePos <$> getPosition+  date <- datep <?> "transaction"+  edate <- optional (secondarydatep date) <?> "secondary date"+  lookAhead (lift spacenonewline <|> newline) <?> "whitespace or newline"+  status <- lift statusp <?> "cleared status"+  code <- T.pack <$> lift codep <?> "transaction code"+  description <- T.pack . strip <$> descriptionp+  comment <- try followingcommentp <|> (newline >> return "")+  let tags = commentTags comment+  postings <- postingsp (Just date)+  return $ txnTieKnot $ Transaction 0 sourcepos date edate status code description comment tags postings ""++#ifdef TESTS+test_transactionp = do+    let s `gives` t = do+                        let p = parseWithState mempty transactionp s+                        assertBool $ isRight p+                        let Right t2 = p+                            -- same f = assertEqual (f t) (f t2)+                        assertEqual (tdate t) (tdate t2)+                        assertEqual (tdate2 t) (tdate2 t2)+                        assertEqual (tstatus t) (tstatus t2)+                        assertEqual (tcode t) (tcode t2)+                        assertEqual (tdescription t) (tdescription t2)+                        assertEqual (tcomment t) (tcomment t2)+                        assertEqual (ttags t) (ttags t2)+                        assertEqual (tpreceding_comment_lines t) (tpreceding_comment_lines t2)+                        assertEqual (show $ tpostings t) (show $ tpostings t2)+    -- "0000/01/01\n\n" `gives` nulltransaction+    unlines [+      "2012/05/14=2012/05/15 (code) desc  ; tcomment1",+      "    ; tcomment2",+      "    ; ttag1: val1",+      "    * a         $1.00  ; pcomment1",+      "    ; pcomment2",+      "    ; ptag1: val1",+      "    ; ptag2: val2"+      ]+     `gives`+     nulltransaction{+      tdate=parsedate "2012/05/14",+      tdate2=Just $ parsedate "2012/05/15",+      tstatus=Uncleared,+      tcode="code",+      tdescription="desc",+      tcomment=" tcomment1\n tcomment2\n ttag1: val1\n",+      ttags=[("ttag1","val1")],+      tpostings=[+        nullposting{+          pstatus=Cleared,+          paccount="a",+          pamount=Mixed [usd 1],+          pcomment=" pcomment1\n pcomment2\n ptag1: val1\n  ptag2: val2\n",+          ptype=RegularPosting,+          ptags=[("ptag1","val1"),("ptag2","val2")],+          ptransaction=Nothing+          }+        ],+      tpreceding_comment_lines=""+      }+    unlines [+      "2015/1/1",+      ]+     `gives`+     nulltransaction{+      tdate=parsedate "2015/01/01",+      }++    assertRight $ parseWithState mempty transactionp $ unlines+      ["2007/01/28 coopportunity"+      ,"    expenses:food:groceries                   $47.18"+      ,"    assets:checking                          $-47.18"+      ,""+      ]++    -- transactionp should not parse just a date+    assertLeft $ parseWithState mempty transactionp "2009/1/1\n"++    -- transactionp should not parse just a date and description+    assertLeft $ parseWithState mempty transactionp "2009/1/1 a\n"++    -- transactionp should not parse a following comment as part of the description+    let p = parseWithState mempty transactionp "2009/1/1 a ;comment\n b 1\n"+    assertRight p+    assertEqual "a" (let Right p' = p in tdescription p')++    -- parse transaction with following whitespace line+    assertRight $ parseWithState mempty transactionp $ unlines+        ["2012/1/1"+        ,"  a  1"+        ,"  b"+        ," "+        ]++    let p = parseWithState mempty transactionp $ unlines+             ["2009/1/1 x  ; transaction comment"+             ," a  1  ; posting 1 comment"+             ," ; posting 1 comment 2"+             ," b"+             ," ; posting 2 comment"+             ]+    assertRight p+    assertEqual 2 (let Right t = p in length $ tpostings t)+#endif++--- ** postings++-- Parse the following whitespace-beginning lines as postings, posting+-- tags, and/or comments (inferring year, if needed, from the given date).+postingsp :: Maybe Day -> ErroringJournalParser [Posting]+postingsp mdate = many (try $ postingp mdate) <?> "postings"++-- linebeginningwithspaces :: Monad m => JournalParser m String+-- linebeginningwithspaces = do+--   sp <- lift (some spacenonewline)+--   c <- nonspace+--   cs <- lift restofline+--   return $ sp ++ (c:cs) ++ "\n"++postingp :: Maybe Day -> ErroringJournalParser Posting+postingp mtdate = do+  -- pdbg 0 "postingp"+  lift (some spacenonewline)+  status <- lift statusp+  lift (many spacenonewline)+  account <- modifiedaccountnamep+  let (ptype, account') = (accountNamePostingType account, textUnbracket account)+  amount <- spaceandamountormissingp+  massertion <- partialbalanceassertionp+  _ <- fixedlotpricep+  lift (many spacenonewline)+  (comment,tags,mdate,mdate2) <-+    try (followingcommentandtagsp mtdate) <|> (newline >> return ("",[],Nothing,Nothing))+  return posting+   { pdate=mdate+   , pdate2=mdate2+   , pstatus=status+   , paccount=account'+   , pamount=amount+   , pcomment=comment+   , ptype=ptype+   , ptags=tags+   , pbalanceassertion=massertion+   }++#ifdef TESTS+test_postingp = do+    let s `gives` ep = do+                         let parse = parseWithState mempty (postingp Nothing) s+                         assertBool -- "postingp parser"+                           $ isRight parse+                         let Right ap = parse+                             same f = assertEqual (f ep) (f ap)+                         same pdate+                         same pstatus+                         same paccount+                         same pamount+                         same pcomment+                         same ptype+                         same ptags+                         same ptransaction+    "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n" `gives`+      posting{paccount="expenses:food:dining", pamount=Mixed [usd 10], pcomment=" a: a a \n b: b b \n", ptags=[("a","a a"), ("b","b b")]}++    " a  1 ; [2012/11/28]\n" `gives`+      ("a" `post` num 1){pcomment=" [2012/11/28]\n"+                        ,ptags=[("date","2012/11/28")]+                        ,pdate=parsedateM "2012/11/28"}++    " a  1 ; a:a, [=2012/11/28]\n" `gives`+      ("a" `post` num 1){pcomment=" a:a, [=2012/11/28]\n"+                        ,ptags=[("a","a"), ("date2","2012/11/28")]+                        ,pdate=Nothing}++    " a  1 ; a:a\n  ; [2012/11/28=2012/11/29],b:b\n" `gives`+      ("a" `post` num 1){pcomment=" a:a\n [2012/11/28=2012/11/29],b:b\n"+                        ,ptags=[("a","a"), ("date","2012/11/28"), ("date2","2012/11/29"), ("b","b")]+                        ,pdate=parsedateM "2012/11/28"}++    assertBool -- "postingp parses a quoted commodity with numbers"+      (isRight $ parseWithState mempty (postingp Nothing) "  a  1 \"DE123\"\n")++  -- ,"postingp parses balance assertions and fixed lot prices" ~: do+    assertBool (isRight $ parseWithState mempty (postingp Nothing) "  a  1 \"DE123\" =$1 { =2.2 EUR} \n")++    -- let parse = parseWithState mempty postingp " a\n ;next-line comment\n"+    -- assertRight parse+    -- let Right p = parse+    -- assertEqual "next-line comment\n" (pcomment p)+    -- assertEqual (Just nullmixedamt) (pbalanceassertion p)+#endif++--- * more tests++tests_Hledger_Read_JournalReader = TestList $ concat [+    -- test_numberp+ ]++{- old hunit tests++tests_Hledger_Read_JournalReader = TestList $ concat [+    test_numberp,+    test_amountp,+    test_spaceandamountormissingp,+    test_tagcomment,+    test_inlinecomment,+    test_comments,+    test_ledgerDateSyntaxToTags,+    test_postingp,+    test_transactionp,+    [+   "modifiertransactionp" ~: do+     assertParse (parseWithState mempty modifiertransactionp "= (some value expr)\n some:postings  1\n")++  ,"periodictransactionp" ~: do+     assertParse (parseWithState mempty periodictransactionp "~ (some period expr)\n some:postings  1\n")++  ,"directivep" ~: do+     assertParse (parseWithState mempty directivep "!include /some/file.x\n")+     assertParse (parseWithState mempty directivep "account some:account\n")+     assertParse (parseWithState mempty (directivep >> directivep) "!account a\nend\n")++  ,"comment" ~: do+     assertParse (parseWithState mempty comment "; some comment \n")+     assertParse (parseWithState mempty comment " \t; x\n")+     assertParse (parseWithState mempty comment "#x")++  ,"datep" ~: do+     assertParse (parseWithState mempty datep "2011/1/1")+     assertParseFailure (parseWithState mempty datep "1/1")+     assertParse (parseWithState mempty{jpsYear=Just 2011} datep "1/1")++  ,"datetimep" ~: do+      let p = do {t <- datetimep; eof; return t}+          bad = assertParseFailure . parseWithState mempty p+          good = assertParse . parseWithState mempty p+      bad "2011/1/1"+      bad "2011/1/1 24:00:00"+      bad "2011/1/1 00:60:00"+      bad "2011/1/1 00:00:60"+      good "2011/1/1 00:00"+      good "2011/1/1 23:59:59"+      good "2011/1/1 3:5:7"+      -- timezone is parsed but ignored+      let startofday = LocalTime (fromGregorian 2011 1 1) (TimeOfDay 0 0 (fromIntegral 0))+      assertParseEqual (parseWithState mempty p "2011/1/1 00:00-0800") startofday+      assertParseEqual (parseWithState mempty p "2011/1/1 00:00+1234") startofday++  ,"defaultyeardirectivep" ~: do+     assertParse (parseWithState mempty defaultyeardirectivep "Y 2010\n")+     assertParse (parseWithState mempty defaultyeardirectivep "Y 10001\n")++  ,"marketpricedirectivep" ~:+    assertParseEqual (parseWithState mempty marketpricedirectivep "P 2004/05/01 XYZ $55.00\n") (MarketPrice (parsedate "2004/05/01") "XYZ" $ usd 55)++  ,"ignoredpricecommoditydirectivep" ~: do+     assertParse (parseWithState mempty ignoredpricecommoditydirectivep "N $\n")++  ,"defaultcommoditydirectivep" ~: do+     assertParse (parseWithState mempty defaultcommoditydirectivep "D $1,000.0\n")++  ,"commodityconversiondirectivep" ~: do+     assertParse (parseWithState mempty commodityconversiondirectivep "C 1h = $50.00\n")++  ,"tagdirectivep" ~: do+     assertParse (parseWithState mempty tagdirectivep "tag foo \n")++  ,"endtagdirectivep" ~: do+     assertParse (parseWithState mempty endtagdirectivep "end tag \n")+     assertParse (parseWithState mempty endtagdirectivep "pop \n")++  ,"accountnamep" ~: do+    assertBool "accountnamep parses a normal account name" (isRight $ parsewith accountnamep "a:b:c")+    assertBool "accountnamep rejects an empty inner component" (isLeft $ parsewith accountnamep "a::c")+    assertBool "accountnamep rejects an empty leading component" (isLeft $ parsewith accountnamep ":b:c")+    assertBool "accountnamep rejects an empty trailing component" (isLeft $ parsewith accountnamep "a:b:")++  ,"leftsymbolamountp" ~: do+    assertParseEqual (parseWithState mempty leftsymbolamountp "$1")  (usd 1 `withPrecision` 0)+    assertParseEqual (parseWithState mempty leftsymbolamountp "$-1") (usd (-1) `withPrecision` 0)+    assertParseEqual (parseWithState mempty leftsymbolamountp "-$1") (usd (-1) `withPrecision` 0)++  ,"amount" ~: do+     let -- | compare a parse result with an expected amount, showing the debug representation for clarity+         assertAmountParse parseresult amount =+             (either (const "parse error") showAmountDebug parseresult) ~?= (showAmountDebug amount)+     assertAmountParse (parseWithState mempty amountp "1 @ $2")+       (num 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0))++ ]]+-}
+ Hledger/Read/TimeclockReader.hs view
@@ -0,0 +1,125 @@+{-|++A reader for the timeclock file format generated by timeclock.el+(<http://www.emacswiki.org/emacs/TimeClock>). Example:++@+i 2007\/03\/10 12:26:00 hledger+o 2007\/03\/10 17:26:02+@++From timeclock.el 2.6:++@+A timeclock 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.+@++-}++{-# LANGUAGE OverloadedStrings #-}++module Hledger.Read.TimeclockReader (+  -- * Reader+  reader,+  -- * Misc other exports+  timeclockfilep,+  -- * Tests+  tests_Hledger_Read_TimeclockReader+)+where+import           Prelude ()+import           Prelude.Compat+import           Control.Monad+import           Control.Monad.Except (ExceptT)+import           Control.Monad.State.Strict+import           Data.Maybe (fromMaybe)+import           Data.Text (Text)+import qualified Data.Text as T+import           Test.HUnit+import           Text.Megaparsec hiding (parse)+import           System.FilePath++import           Hledger.Data+-- XXX too much reuse ?+import           Hledger.Read.Common+import           Hledger.Utils+++reader :: Reader+reader = Reader format detect parse++format :: String+format = "timeclock"++-- | Does the given file path and data look like it might be timeclock.el's timeclock format ?+detect :: FilePath -> Text -> Bool+detect f t+  | f /= "-"  = takeExtension f == '.':format -- from a known file name: yes if the extension is this format's name+  | otherwise = regexMatches "(^|\n)[io] " $ T.unpack t  -- from stdin: yes if any line starts with "i " or "o "++-- | Parse and post-process a "Journal" from timeclock.el's timeclock+-- format, saving the provided file path and the current time, or give an+-- error.+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal+parse _ = parseAndFinaliseJournal timeclockfilep++timeclockfilep :: ErroringJournalParser ParsedJournal+timeclockfilep = do many timeclockitemp+                    eof+                    j@Journal{jparsetimeclockentries=es} <- get+                    -- Convert timeclock entries in this journal to transactions, closing any unfinished sessions.+                    -- Doing this here rather than in journalFinalise means timeclock sessions can't span file boundaries,+                    -- but it simplifies code above.+                    now <- liftIO getCurrentLocalTime+                    -- entries have been parsed in reverse order. timeclockEntriesToTransactions+                    -- expects them to be in normal order, then we must reverse again since+                    -- journalFinalise expects them in reverse order+                    let j' = j{jtxns = reverse $ timeclockEntriesToTransactions now $ reverse es, jparsetimeclockentries = []}+                    return j'+    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+      timeclockitemp = choice [ +                            void emptyorcommentlinep+                          , timeclockentryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j})+                          ] <?> "timeclock entry, or default year or historical price directive"++-- | Parse a timeclock entry.+timeclockentryp :: ErroringJournalParser TimeclockEntry+timeclockentryp = do+  sourcepos <- genericSourcePos <$> lift getPosition+  code <- oneOf ("bhioO" :: [Char])+  lift (some spacenonewline)+  datetime <- datetimep+  account <- fromMaybe "" <$> optional (lift (some spacenonewline) >> modifiedaccountnamep)+  description <- T.pack . fromMaybe "" <$> lift (optional (some spacenonewline >> restofline))+  return $ TimeclockEntry sourcepos (read [code]) datetime account description++tests_Hledger_Read_TimeclockReader = TestList [+ ]+
+ Hledger/Read/TimedotReader.hs view
@@ -0,0 +1,159 @@+{-|++A reader for the "timedot" file format.+Example:++@+#DATE+#ACCT DOTS  # Each dot represents 15m, spaces are ignored++# on 2/1, 1h was spent on FOSS haskell work, 0.25h on research, etc.+2/1+fos.haskell   .... ..+biz.research  .+inc.client1   .... .... .... .... .... ....++2/2+biz.research  .+inc.client1   .... .... ..++@++-}++{-# LANGUAGE OverloadedStrings #-}++module Hledger.Read.TimedotReader (+  -- * Reader+  reader,+  -- * Misc other exports+  timedotfilep,+  -- * Tests+  tests_Hledger_Read_TimedotReader+)+where+import Prelude ()+import Prelude.Compat+import Control.Monad+import Control.Monad.Except (ExceptT)+import Control.Monad.State.Strict+import Data.Char (isSpace)+import Data.List (foldl')+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T+import Test.HUnit+import Text.Megaparsec hiding (parse)+import System.FilePath++import Hledger.Data+import Hledger.Read.Common+import Hledger.Utils hiding (ptrace)++-- easier to toggle this here sometimes+-- import qualified Hledger.Utils (ptrace)+-- ptrace = Hledger.Utils.ptrace+ptrace = return++reader :: Reader+reader = Reader format detect parse++format :: String+format = "timedot"++-- | Does the given file path and data look like it might contain this format ?+detect :: FilePath -> Text -> Bool+detect f t+  | f /= "-"  = takeExtension f == '.':format -- from a file: yes if the extension matches the format name+  | otherwise = regexMatches "(^|\n)[0-9]" $ T.unpack t  -- from stdin: yes if we can see a possible timedot day entry (digits in column 0)++-- | Parse and post-process a "Journal" from the timedot format, or give an error.+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal+parse _ = parseAndFinaliseJournal timedotfilep++timedotfilep :: ErroringJournalParser ParsedJournal+timedotfilep = do many timedotfileitemp+                  eof+                  get+    where+      timedotfileitemp :: ErroringJournalParser ()+      timedotfileitemp = do+        ptrace "timedotfileitemp"+        choice [+          void emptyorcommentlinep+         ,timedotdayp >>= \ts -> modify' (addTransactions ts)+         ] <?> "timedot day entry, or default year or comment line or blank line"++addTransactions :: [Transaction] -> Journal -> Journal+addTransactions ts j = foldl' (flip ($)) j (map addTransaction ts)++-- | Parse timedot day entries to zero or more time transactions for that day.+-- @+-- 2/1+-- fos.haskell  .... ..+-- biz.research .+-- inc.client1  .... .... .... .... .... ....+-- @+timedotdayp :: ErroringJournalParser [Transaction]+timedotdayp = do+  ptrace " timedotdayp"+  d <- datep <* lift eolof+  es <- catMaybes <$> many (const Nothing <$> try emptyorcommentlinep <|>+                            Just <$> (notFollowedBy datep >> timedotentryp))+  return $ map (\t -> t{tdate=d}) es -- <$> many timedotentryp++-- | Parse a single timedot entry to one (dateless) transaction.+-- @+-- fos.haskell  .... ..+-- @+timedotentryp :: ErroringJournalParser Transaction+timedotentryp = do+  ptrace "  timedotentryp"+  pos <- genericSourcePos <$> getPosition+  lift (many spacenonewline)+  a <- modifiedaccountnamep+  lift (many spacenonewline)+  hours <-+    try (followingcommentp >> return 0)+    <|> (timedotdurationp <*+         (try followingcommentp <|> (newline >> return "")))+  let t = nulltransaction{+        tsourcepos = pos,+        tstatus    = Cleared,+        tpostings  = [+          nullposting{paccount=a+                     ,pamount=Mixed [setAmountPrecision 2 $ num hours]  -- don't assume hours; do set precision to 2+                     ,ptype=VirtualPosting+                     ,ptransaction=Just t+                     }+          ]+        }+  return t++timedotdurationp :: ErroringJournalParser Quantity+timedotdurationp = try timedotnumberp <|> timedotdotsp++-- | Parse a duration written as a decimal number of hours (optionally followed by the letter h).+-- @+-- 1.5h+-- @+timedotnumberp :: ErroringJournalParser Quantity+timedotnumberp = do+   (q, _, _, _) <- lift numberp+   lift (many spacenonewline)+   optional $ char 'h'+   lift (many spacenonewline)+   return q++-- | Parse a quantity written as a line of dots, each representing 0.25.+-- @+-- .... ..+-- @+timedotdotsp :: ErroringJournalParser Quantity+timedotdotsp = do+  dots <- filter (not.isSpace) <$> many (oneOf (". " :: [Char]))+  return $ (/4) $ fromIntegral $ length dots++tests_Hledger_Read_TimedotReader = TestList [+ ]+
− Hledger/Read/TimelogReader.hs
@@ -1,116 +0,0 @@-{-|--A reader for the timelog file format generated by timeclock.el-(<http://www.emacswiki.org/emacs/TimeClock>). Example:--@-i 2007\/03\/10 12:26:00 hledger-o 2007\/03\/10 17:26:02-@--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.-@---}--module Hledger.Read.TimelogReader (-  -- * Reader-  reader,-  -- * Tests-  tests_Hledger_Read_TimelogReader-)-where-import Prelude ()-import Prelude.Compat-import Control.Monad (liftM)-import Control.Monad.Except (ExceptT)-import Data.List (isPrefixOf, foldl')-import Data.Maybe (fromMaybe)-import Test.HUnit-import Text.Parsec hiding (parse)-import System.FilePath--import Hledger.Data--- XXX too much reuse ?-import Hledger.Read.JournalReader (-  directivep, marketpricedirectivep, defaultyeardirectivep, emptyorcommentlinep, datetimep,-  parseAndFinaliseJournal, modifiedaccountnamep, genericSourcePos-  )-import Hledger.Utils---reader :: Reader-reader = Reader format detect parse--format :: String-format = "timelog"---- | Does the given file path and data look like it might be timeclock.el's timelog format ?-detect :: FilePath -> String -> Bool-detect f s-  | f /= "-"  = takeExtension f == '.':format               -- from a file: yes if the extension is .timelog-  | otherwise = "i " `isPrefixOf` s || "o " `isPrefixOf` s  -- from stdin: yes if it starts with "i " or "o "---- | Parse and post-process a "Journal" from timeclock.el's timelog--- format, saving the provided file path and the current time, or give an--- error.-parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal-parse _ = parseAndFinaliseJournal timelogfilep--timelogfilep :: ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate, JournalContext)-timelogfilep = do items <- many timelogitemp-                  eof-                  ctx <- getState-                  return (liftM (foldl' (\acc new x -> new (acc x)) id) $ sequence items, ctx)-    where-      -- As all ledger line types can be distinguished by the first-      -- character, excepting transactions versus empty (blank or-      -- comment-only) lines, can use choice w/o try-      timelogitemp = choice [ directivep-                          , liftM (return . addMarketPrice) marketpricedirectivep-                          , defaultyeardirectivep-                          , emptyorcommentlinep >> return (return id)-                          , liftM (return . addTimeLogEntry)  timelogentryp-                          ] <?> "timelog entry, or default year or historical price directive"---- | Parse a timelog entry.-timelogentryp :: ParsecT [Char] JournalContext (ExceptT String IO) TimeLogEntry-timelogentryp = do-  sourcepos <- genericSourcePos <$> getPosition-  code <- oneOf "bhioO"-  many1 spacenonewline-  datetime <- datetimep-  account <- fromMaybe "" <$> optionMaybe (many1 spacenonewline >> modifiedaccountnamep)-  description <- fromMaybe "" <$> optionMaybe (many1 spacenonewline >> restofline)-  return $ TimeLogEntry sourcepos (read [code]) datetime account description--tests_Hledger_Read_TimelogReader = TestList [- ]-
Hledger/Reports.hs view
@@ -15,7 +15,7 @@   module Hledger.Reports.TransactionsReports,   module Hledger.Reports.BalanceReport,   module Hledger.Reports.MultiBalanceReports,-  module Hledger.Reports.BalanceHistoryReport,+--   module Hledger.Reports.BalanceHistoryReport,    -- * Tests   tests_Hledger_Reports@@ -30,7 +30,7 @@ import Hledger.Reports.TransactionsReports import Hledger.Reports.BalanceReport import Hledger.Reports.MultiBalanceReports-import Hledger.Reports.BalanceHistoryReport+-- import Hledger.Reports.BalanceHistoryReport  tests_Hledger_Reports :: Test tests_Hledger_Reports = TestList $
Hledger/Reports/BalanceHistoryReport.hs view
@@ -4,6 +4,7 @@ Account balance history report.  -}+-- XXX not used  module Hledger.Reports.BalanceHistoryReport (   accountBalanceHistory
Hledger/Reports/BalanceReport.hs view
@@ -1,14 +1,21 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, ScopedTypeVariables #-} {-|  Balance report, used by the balance command.  -} ++++++++{-# LANGUAGE FlexibleInstances, ScopedTypeVariables, OverloadedStrings #-}+ module Hledger.Reports.BalanceReport (   BalanceReport,   BalanceReportItem,-  RenderableAccountName,   balanceReport,   balanceReportValue,   mixedAmountValue,@@ -31,24 +38,31 @@ import Hledger.Reports.ReportOptions  ++++++ -- | A simple single-column balance report. It has: ----- 1. a list of rows, each containing a renderable account name and a corresponding amount+-- 1. a list of items, one per account, each containing: ----- 2. the final total of the amounts-type BalanceReport = ([BalanceReportItem], MixedAmount)-type BalanceReportItem = (RenderableAccountName, MixedAmount)---- | A renderable account name includes some additional hints for rendering accounts in a balance report.--- It has:+--   * the full account name ----- * The full account name+--   * the Ledger-style elided short account name+--     (the leaf account name, prefixed by any boring parents immediately above);+--     or with --flat, the full account name again ----- * The ledger-style short elided account name (the leaf name, prefixed by any boring parents immediately above)+--   * the number of indentation steps for rendering a Ledger-style account tree,+--     taking into account elided boring parents, --no-elide and --flat ----- * The number of indentation steps to use when rendering a ledger-style account tree---   (normally the 0-based depth of this account excluding boring parents, or 0 with --flat).-type RenderableAccountName = (AccountName, AccountName, Int)+--   * an amount+--+-- 2. the total of all amounts+--+type BalanceReport = ([BalanceReportItem], MixedAmount)+type BalanceReportItem = (AccountName, AccountName, Int, MixedAmount)  -- | When true (the default), this makes balance --flat reports and their implementation clearer. -- Single/multi-col balance reports currently aren't all correct if this is false.@@ -61,7 +75,7 @@  -- | Generate a simple balance report, containing the matched accounts and -- their balances (change of balance) during the specified period.--- This is like periodBalanceReport with a single column (but more mature,+-- This is like PeriodChangeReport with a single column (but more mature, -- eg this can do hierarchical display). balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport balanceReport opts q j = (items, total)@@ -91,10 +105,10 @@             prunezeros  = if empty_ opts then id else fromMaybe nullacct . pruneAccounts (isZeroMixedAmount . balance)             markboring  = if no_elide_ opts then id else markBoringParentAccounts       items = dbg1 "items" $ map (balanceReportItem opts q) accts'-      total | not (flat_ opts) = dbg1 "total" $ sum [amt | ((_,_,indent),amt) <- items, indent == 0]+      total | not (flat_ opts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0]             | otherwise        = dbg1 "total" $                                  if flatShowsExclusiveBalance-                                 then sum $ map snd items+                                 then sum $ map fourth4 items                                  else sum $ map aebalance $ clipAccountsAndAggregate 1 accts'  -- | In an account tree with zero-balance leaves removed, mark the@@ -108,13 +122,13 @@  balanceReportItem :: ReportOpts -> Query -> Account -> BalanceReportItem balanceReportItem opts q a-  | flat_ opts = ((name, name,       0),      (if flatShowsExclusiveBalance then aebalance else aibalance) a)-  | otherwise  = ((name, elidedname, indent), aibalance a)+  | flat_ opts = (name, name,       0,      (if flatShowsExclusiveBalance then aebalance else aibalance) a)+  | otherwise  = (name, elidedname, indent, aibalance a)   where     name | queryDepth q > 0 = aname a          | otherwise        = "..."     elidedname = accountNameFromComponents (adjacentboringparentnames ++ [accountLeafName name])-    adjacentboringparentnames = reverse $ map (accountLeafName.aname) $ takeWhile aboring $ parents+    adjacentboringparentnames = reverse $ map (accountLeafName.aname) $ takeWhile aboring parents     indent = length $ filter (not.aboring) parents     -- parents exclude the tree's root node     parents = case parentAccounts a of [] -> []@@ -123,7 +137,7 @@ -- -- the above using the newer multi balance report code: -- balanceReport' opts q j = (items, total) --   where---     MultiBalanceReport (_,mbrrows,mbrtotals) = periodBalanceReport opts q j+--     MultiBalanceReport (_,mbrrows,mbrtotals) = PeriodChangeReport opts q j --     items = [(a,a',n, headDef 0 bs) | ((a,a',n), bs) <- mbrrows] --     total = headDef 0 mbrtotals @@ -134,14 +148,15 @@ balanceReportValue j d r = r'   where     (items,total) = r-    r' = ([(n, mixedAmountValue j d a) |(n,a) <- items], mixedAmountValue j d total)+    r' = dbg8 "balanceReportValue"+         ([(n, n', i, mixedAmountValue j d a) |(n,n',i,a) <- items], mixedAmountValue j d total)  mixedAmountValue :: Journal -> Day -> MixedAmount -> MixedAmount mixedAmountValue j d (Mixed as) = Mixed $ map (amountValue j d) as  -- | Find the market value of this amount on the given date, in it's--- default valuation commodity, based on historical prices. If no--- default valuation commodity can be found, the amount is left+-- default valuation commodity, based on recorded market prices.+-- If no default valuation commodity can be found, the amount is left -- unchanged. amountValue :: Journal -> Day -> Amount -> Amount amountValue j d a =@@ -154,20 +169,27 @@ -- | Find the market value, if known, of one unit of this commodity on -- the given date, in the commodity in which it has most recently been -- market-priced (ie the commodity mentioned in the most recent--- applicable historical price directive before this date).-commodityValue :: Journal -> Day -> Commodity -> Maybe Amount+-- applicable market price directive before this date).+commodityValue :: Journal -> Day -> CommoditySymbol -> Maybe Amount commodityValue j d c     | null applicableprices = Nothing     | otherwise             = Just $ mpamount $ last applicableprices   where     applicableprices = [p | p <- sort $ jmarketprices j, mpcommodity p == c, mpdate p <= d] +++++++ tests_balanceReport =   let     (opts,journal) `gives` r = do       let (eitems, etotal) = r           (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal-          showw (acct,amt) = (acct, showMixedAmountDebug amt)+          showw (acct,acct',indent,amt) = (acct, acct', indent, showMixedAmountDebug amt)       assertEqual "items" (map showw eitems) (map showw aitems)       assertEqual "total" (showMixedAmountDebug etotal) (showMixedAmountDebug atotal)     usd0 = usd 0@@ -179,36 +201,36 @@   ,"balanceReport with no args on sample journal" ~: do    (defreportopts, samplejournal) `gives`     ([-      (("assets","assets",0), mamountp' "$-1.00")-     ,(("assets:bank:saving","bank:saving",1), mamountp' "$1.00")-     ,(("assets:cash","cash",1), mamountp' "$-2.00")-     ,(("expenses","expenses",0), mamountp' "$2.00")-     ,(("expenses:food","food",1), mamountp' "$1.00")-     ,(("expenses:supplies","supplies",1), mamountp' "$1.00")-     ,(("income","income",0), mamountp' "$-2.00")-     ,(("income:gifts","gifts",1), mamountp' "$-1.00")-     ,(("income:salary","salary",1), mamountp' "$-1.00")-     ,(("liabilities:debts","liabilities:debts",0), mamountp' "$1.00")+      ("assets","assets",0, mamountp' "$-1.00")+     ,("assets:bank:saving","bank:saving",1, mamountp' "$1.00")+     ,("assets:cash","cash",1, mamountp' "$-2.00")+     ,("expenses","expenses",0, mamountp' "$2.00")+     ,("expenses:food","food",1, mamountp' "$1.00")+     ,("expenses:supplies","supplies",1, mamountp' "$1.00")+     ,("income","income",0, mamountp' "$-2.00")+     ,("income:gifts","gifts",1, mamountp' "$-1.00")+     ,("income:salary","salary",1, mamountp' "$-1.00")+     ,("liabilities:debts","liabilities:debts",0, mamountp' "$1.00")      ],      Mixed [usd0])    ,"balanceReport with --depth=N" ~: do    (defreportopts{depth_=Just 1}, samplejournal) `gives`     ([-      (("assets",      "assets",      0), mamountp' "$-1.00")-     ,(("expenses",    "expenses",    0), mamountp'  "$2.00")-     ,(("income",      "income",      0), mamountp' "$-2.00")-     ,(("liabilities", "liabilities", 0), mamountp'  "$1.00")+      ("assets",      "assets",      0, mamountp' "$-1.00")+     ,("expenses",    "expenses",    0, mamountp'  "$2.00")+     ,("income",      "income",      0, mamountp' "$-2.00")+     ,("liabilities", "liabilities", 0, mamountp'  "$1.00")      ],      Mixed [usd0])    ,"balanceReport with depth:N" ~: do    (defreportopts{query_="depth:1"}, samplejournal) `gives`     ([-      (("assets",      "assets",      0), mamountp' "$-1.00")-     ,(("expenses",    "expenses",    0), mamountp'  "$2.00")-     ,(("income",      "income",      0), mamountp' "$-2.00")-     ,(("liabilities", "liabilities", 0), mamountp'  "$1.00")+      ("assets",      "assets",      0, mamountp' "$-1.00")+     ,("expenses",    "expenses",    0, mamountp'  "$2.00")+     ,("income",      "income",      0, mamountp' "$-2.00")+     ,("liabilities", "liabilities", 0, mamountp'  "$1.00")      ],      Mixed [usd0]) @@ -218,32 +240,32 @@      Mixed [nullamt])    (defreportopts{query_="date2:'in 2009'"}, samplejournal2) `gives`     ([-      (("assets:bank:checking","assets:bank:checking",0),mamountp' "$1.00")-     ,(("income:salary","income:salary",0),mamountp' "$-1.00")+      ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")+     ,("income:salary","income:salary",0,mamountp' "$-1.00")      ],      Mixed [usd0])    ,"balanceReport with desc:" ~: do    (defreportopts{query_="desc:income"}, samplejournal) `gives`     ([-      (("assets:bank:checking","assets:bank:checking",0),mamountp' "$1.00")-     ,(("income:salary","income:salary",0), mamountp' "$-1.00")+      ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")+     ,("income:salary","income:salary",0, mamountp' "$-1.00")      ],      Mixed [usd0])    ,"balanceReport with not:desc:" ~: do    (defreportopts{query_="not:desc:income"}, samplejournal) `gives`     ([-      (("assets","assets",0), mamountp' "$-2.00")-     ,(("assets:bank","bank",1), Mixed [usd0])-     ,(("assets:bank:checking","checking",2),mamountp' "$-1.00")-     ,(("assets:bank:saving","saving",2), mamountp' "$1.00")-     ,(("assets:cash","cash",1), mamountp' "$-2.00")-     ,(("expenses","expenses",0), mamountp' "$2.00")-     ,(("expenses:food","food",1), mamountp' "$1.00")-     ,(("expenses:supplies","supplies",1), mamountp' "$1.00")-     ,(("income:gifts","income:gifts",0), mamountp' "$-1.00")-     ,(("liabilities:debts","liabilities:debts",0), mamountp' "$1.00")+      ("assets","assets",0, mamountp' "$-2.00")+     ,("assets:bank","bank",1, Mixed [usd0])+     ,("assets:bank:checking","checking",2,mamountp' "$-1.00")+     ,("assets:bank:saving","saving",2, mamountp' "$1.00")+     ,("assets:cash","cash",1, mamountp' "$-2.00")+     ,("expenses","expenses",0, mamountp' "$2.00")+     ,("expenses:food","food",1, mamountp' "$1.00")+     ,("expenses:supplies","supplies",1, mamountp' "$1.00")+     ,("income:gifts","income:gifts",0, mamountp' "$-1.00")+     ,("liabilities:debts","liabilities:debts",0, mamountp' "$1.00")      ],      Mixed [usd0]) @@ -364,27 +386,28 @@ -}  ] -Right samplejournal2 = journalBalanceTransactions $-         nulljournal-         {jtxns = [-           txnTieKnot $ Transaction {-             tindex=0,-             tsourcepos=nullsourcepos,-             tdate=parsedate "2008/01/01",-             tdate2=Just $ parsedate "2009/01/01",-             tstatus=Uncleared,-             tcode="",-             tdescription="income",-             tcomment="",-             ttags=[],-             tpostings=-                 [posting {paccount="assets:bank:checking", pamount=Mixed [usd 1]}-                 ,posting {paccount="income:salary", pamount=missingmixedamt}-                 ],-             tpreceding_comment_lines=""-           }-          ]-         }+Right samplejournal2 =+  journalBalanceTransactions+    nulljournal{+      jtxns = [+        txnTieKnot Transaction{+          tindex=0,+          tsourcepos=nullsourcepos,+          tdate=parsedate "2008/01/01",+          tdate2=Just $ parsedate "2009/01/01",+          tstatus=Uncleared,+          tcode="",+          tdescription="income",+          tcomment="",+          ttags=[],+          tpostings=+            [posting {paccount="assets:bank:checking", pamount=Mixed [usd 1]}+            ,posting {paccount="income:salary", pamount=missingmixedamt}+            ],+          tpreceding_comment_lines=""+        }+      ]+    }  -- tests_isInterestingIndented = [ --   "isInterestingIndented" ~: do@@ -395,5 +418,5 @@ --  ]  tests_Hledger_Reports_BalanceReport :: Test-tests_Hledger_Reports_BalanceReport = TestList $-    tests_balanceReport+tests_Hledger_Reports_BalanceReport = TestList+  tests_balanceReport
Hledger/Reports/MultiBalanceReports.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, ScopedTypeVariables #-}+{-# LANGUAGE FlexibleInstances, ScopedTypeVariables #-} {-|  Multi-column balance reports, used by the balance command.@@ -9,7 +9,8 @@   MultiBalanceReport(..),   MultiBalanceReportRow,   multiBalanceReport,-  multiBalanceReportValue+  multiBalanceReportValue,+  singleBalanceReport    -- -- * Tests   -- tests_Hledger_Reports_MultiBalanceReport@@ -32,32 +33,34 @@  -- | A multi balance report is a balance report with one or more columns. It has: ----- 1. a list of each column's date span+-- 1. a list of each column's period (date span) ----- 2. a list of rows, each containing a renderable account name and the amounts to show in each column+-- 2. a list of row items, each containing: ----- 3. a list of each column's final total+--   * the full account name ----- The meaning of the amounts depends on the type of multi balance--- report, of which there are three: periodic, cumulative and historical--- (see 'BalanceType' and "Hledger.Cli.Balance").-newtype MultiBalanceReport = MultiBalanceReport ([DateSpan]-                                                ,[MultiBalanceReportRow]-                                                ,MultiBalanceTotalsRow-                                                )---- | A row in a multi balance report has+--   * the leaf account name ----- * An account name, with rendering hints+--   * the account's depth ----- * A list of amounts to be shown in each of the report's columns.+--   * the amounts to show in each column ----- * The total of the row amounts.+--   * the total of the row's amounts ----- * The average of the row amounts.-type MultiBalanceReportRow = (RenderableAccountName, [MixedAmount], MixedAmount, MixedAmount)--type MultiBalanceTotalsRow = ([MixedAmount], MixedAmount, MixedAmount)+--   * the average of the row's amounts+--+-- 3. the column totals and the overall total and average+--+-- The meaning of the amounts depends on the type of multi balance+-- report, of which there are three: periodic, cumulative and historical+-- (see 'BalanceType' and "Hledger.Cli.Balance").+newtype MultiBalanceReport =+  MultiBalanceReport ([DateSpan]+                     ,[MultiBalanceReportRow]+                     ,MultiBalanceReportTotals+                     )+type MultiBalanceReportRow    = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)+type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount)  instance Show MultiBalanceReport where     -- use ppShow to break long lists onto multiple lines@@ -69,6 +72,20 @@ -- type alias just to remind us which AccountNames might be depth-clipped, below. type ClippedAccountName = AccountName +-- | Generates a single column BalanceReport like balanceReport, but uses+-- multiBalanceReport, so supports --historical. Does not support+-- boring parent eliding yet.+singleBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport+singleBalanceReport opts q j = (rows', total)+  where+    MultiBalanceReport (_, rows, (totals, _, _)) = multiBalanceReport opts q j+    rows' = [(a+             ,if tree_ opts then a' else a   -- BalanceReport expects full account name here with --flat+             ,if tree_ opts then d-1 else 0  -- BalanceReport uses 0-based account depths+             , headDef nullmixedamt amts     -- 0 columns is illegal, should not happen, return zeroes if it does+             ) | (a,a',d, amts, _, _) <- rows]+    total = headDef nullmixedamt totals+ -- | Generate a multicolumn balance report for the matched accounts, -- showing the change of balance, accumulated balance, or historical balance -- in each of the specified periods.@@ -84,7 +101,7 @@       precedingq = dbg1 "precedingq" $ And [datelessq, dateqcons $ DateSpan Nothing (spanStart reportspan)]       requestedspan  = dbg1 "requestedspan"  $ queryDateSpan (date2_ opts) q                              -- span specified by -b/-e/-p options and query args       requestedspan' = dbg1 "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan (date2_ opts) j  -- if open-ended, close it using the journal's end dates-      intervalspans  = dbg1 "intervalspans"  $ splitSpan (intervalFromOpts opts) requestedspan'           -- interval spans enclosing it+      intervalspans  = dbg1 "intervalspans"  $ splitSpan (interval_ opts) requestedspan'           -- interval spans enclosing it       reportspan     = dbg1 "reportspan"     $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) -- the requested span enlarged to a whole number of intervals                                                        (maybe Nothing spanEnd   $ lastMay intervalspans)       newdatesq = dbg1 "newdateq" $ dateqcons reportspan@@ -97,15 +114,15 @@           filterJournalPostings reportq $        -- remove postings not matched by (adjusted) query           journalSelectingAmountFromOpts opts j -      displayspans = dbg1 "displayspans" $ splitSpan (intervalFromOpts opts) displayspan+      displayspans = dbg1 "displayspans" $ splitSpan (interval_ opts) displayspan         where           displayspan-            | empty_ opts = dbg1 "displayspan (-E)" $ reportspan                                -- all the requested intervals+            | empty_ opts = dbg1 "displayspan (-E)" reportspan                                -- all the requested intervals             | otherwise   = dbg1 "displayspan"      $ requestedspan `spanIntersect` matchedspan -- exclude leading/trailing empty intervals           matchedspan = dbg1 "matchedspan" $ postingsDateSpan' (whichDateFromOpts opts) ps        psPerSpan :: [[Posting]] =-          dbg1 "psPerSpan" $+          dbg1 "psPerSpan"           [filter (isPostingInDateSpan' (whichDateFromOpts opts) s) ps | s <- displayspans]        postedAcctBalChangesPerSpan :: [[(ClippedAccountName, MixedAmount)]] =@@ -125,7 +142,7 @@       postedAccts :: [AccountName] = dbg1 "postedAccts" $ sort $ accountNamesFromPostings ps        -- starting balances and accounts from transactions before the report start date-      startacctbals = dbg1 "startacctbals" $ map (\((a,_,_),b) -> (a,b)) startbalanceitems+      startacctbals = dbg1 "startacctbals" $ map (\(a,_,_,b) -> (a,b)) startbalanceitems           where             (startbalanceitems,_) = dbg1 "starting balance report" $ balanceReport opts' precedingq j                                     where@@ -141,22 +158,22 @@           if empty_ opts then nub $ sort $ startAccts ++ postedAccts else postedAccts        acctBalChangesPerSpan :: [[(ClippedAccountName, MixedAmount)]] =-          dbg1 "acctBalChangesPerSpan" $+          dbg1 "acctBalChangesPerSpan"           [sortBy (comparing fst) $ unionBy (\(a,_) (a',_) -> a == a') postedacctbals zeroes            | postedacctbals <- postedAcctBalChangesPerSpan]           where zeroes = [(a, nullmixedamt) | a <- displayedAccts]        acctBalChanges :: [(ClippedAccountName, [MixedAmount])] =-          dbg1 "acctBalChanges" $+          dbg1 "acctBalChanges"           [(a, map snd abs) | abs@((a,_):_) <- transpose acctBalChangesPerSpan] -- never null, or used when null...        items :: [MultiBalanceReportRow] =-          dbg1 "items" $-          [((a, accountLeafName a, accountNameLevel a), displayedBals, rowtot, rowavg)+          dbg1 "items"+          [(a, accountLeafName a, accountNameLevel a, displayedBals, rowtot, rowavg)            | (a,changes) <- acctBalChanges            , let displayedBals = case balancetype_ opts of                                   HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor a) changes-                                  CumulativeBalance -> drop 1 $ scanl (+) nullmixedamt changes+                                  CumulativeChange -> drop 1 $ scanl (+) nullmixedamt changes                                   _                 -> changes            , let rowtot = sum displayedBals            , let rowavg = averageMixedAmounts displayedBals@@ -167,13 +184,13 @@           -- dbg1 "totals" $           map sum balsbycol           where-            balsbycol = transpose [bs | ((a,_,_),bs,_,_) <- items, not (tree_ opts) || a `elem` highestlevelaccts]+            balsbycol = transpose [bs | (a,_,_,bs,_,_) <- items, not (tree_ opts) || a `elem` highestlevelaccts]             highestlevelaccts     =-                dbg1 "highestlevelaccts" $+                dbg1 "highestlevelaccts"                 [a | a <- displayedAccts, not $ any (`elem` displayedAccts) $ init $ expandAccountName a] -      totalsrow :: MultiBalanceTotalsRow =-          dbg1 "totalsrow" $+      totalsrow :: MultiBalanceReportTotals =+          dbg1 "totalsrow"           (totals, sum totals, averageMixedAmounts totals)        dbg1 s = let p = "multiBalanceReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in this function's debug output@@ -188,7 +205,7 @@     MultiBalanceReport (spans, rows, (coltotals, rowtotaltotal, rowavgtotal)) = r     r' = MultiBalanceReport          (spans,-          [(n, map convert rowamts, convert rowtotal, convert rowavg) | (n, rowamts, rowtotal, rowavg) <- rows],+          [(acct, acct', depth, map convert rowamts, convert rowtotal, convert rowavg) | (acct, acct', depth, rowamts, rowtotal, rowavg) <- rows],           (map convert coltotals, convert rowtotaltotal, convert rowavgtotal))     convert = mixedAmountValue j d 
Hledger/Reports/PostingsReport.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections #-}+{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections, OverloadedStrings #-} {-|  Postings report, used by the register command.@@ -19,6 +19,8 @@ import Data.List import Data.Maybe import Data.Ord (comparing)+-- import Data.Text (Text)+import qualified Data.Text as T import Data.Time.Calendar import Safe (headMay, lastMay) import Test.HUnit@@ -45,7 +47,10 @@                                         -- the interval.                           ,Maybe String -- The posting's transaction's description, if this is the first posting in the transaction.                           ,Posting      -- The posting, possibly with the account name depth-clipped.-                          ,MixedAmount  -- The running total after this posting (or with --average, the running average).+                          ,MixedAmount  -- The running total after this posting, or with --average,+                                        -- the running average posting amount. With --historical,+                                        -- postings before the report start date are included in+                                        -- the running total/average.                           )  -- | Select postings from the journal and add running balance and other@@ -53,51 +58,84 @@ postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport postingsReport opts q j = (totallabel, items)     where-      -- figure out adjusted queries & spans like multiBalanceReport-      symq = dbg1 "symq"   $ filterQuery queryIsSym $ dbg1 "requested q" q+      reportspan = adjustReportDates opts q j+      whichdate = whichDateFromOpts opts       depth = queryDepth q-      depthless = filterQuery (not . queryIsDepth)-      datelessq = filterQuery (not . queryIsDateOrDate2) q-      -- XXX date:/date2:/--date2 handling is not robust, combinations of these can confuse it-      dateq = filterQuery queryIsDateOrDate2 q-      (dateqcons,pdate) | queryIsDate2 dateq || (queryIsDate dateq && date2_ opts) = (Date2, postingDate2)-                        | otherwise = (Date, postingDate)-      requestedspan  = dbg1 "requestedspan"  $ queryDateSpan' q   -- span specified by -b/-e/-p options and query args-      requestedspan' = dbg1 "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan ({-date2_ opts-} False) j  -- if open-ended, close it using the journal's end dates-      intervalspans  = dbg1 "intervalspans"  $ splitSpan (intervalFromOpts opts) requestedspan' -- interval spans enclosing it-      reportstart    = dbg1 "reportstart"    $ maybe Nothing spanStart $ headMay intervalspans-      reportend      = dbg1 "reportend"      $ maybe Nothing spanEnd   $ lastMay intervalspans-      reportspan     = dbg1 "reportspan"     $ DateSpan reportstart reportend  -- the requested span enlarged to a whole number of intervals-      beforestartq   = dbg1 "beforestartq"   $ dateqcons $ DateSpan Nothing reportstart-      beforeendq     = dbg1 "beforeendq"     $ dateqcons $ DateSpan Nothing reportend-      reportq        = dbg1 "reportq"        $ depthless $ And [datelessq, beforeendq] -- user's query with no start date, end date on an interval boundary and no depth limit -      pstoend =-          dbg1 "ps4" $ sortBy (comparing pdate) $                                  -- sort postings by date (or date2)-          dbg1 "ps3" $ map (filterPostingAmount symq) $                            -- remove amount parts which the query's cur: terms would exclude-          dbg1 "ps2" $ (if related_ opts then concatMap relatedPostings else id) $ -- with -r, replace each with its sibling postings-          dbg1 "ps1" $ filter (reportq `matchesPosting`) $                         -- filter postings by the query, including before the report start date, ignoring depth-                      journalPostings $ journalSelectingAmountFromOpts opts j-      (precedingps, reportps) = dbg1 "precedingps, reportps" $ span (beforestartq `matchesPosting`) pstoend+      -- postings to be included in the report, and similarly-matched postings before the report start date+      (precedingps, reportps) = matchedPostingsBeforeAndDuring opts q j reportspan -      showempty = empty_ opts || average_ opts-      -- displayexpr = display_ opts  -- XXX-      interval = intervalFromOpts opts -- XXX+      -- postings or pseudo postings to be displayed+      displayps | interval == NoInterval = map (,Nothing) reportps+                | otherwise              = summarisePostingsByInterval interval whichdate depth showempty reportspan reportps+        where+          interval = interval_ opts -- XXX+          showempty = empty_ opts || average_ opts -      whichdate = whichDateFromOpts opts-      itemps | interval == NoInterval = map (,Nothing) reportps-             | otherwise              = summarisePostingsByInterval interval whichdate depth showempty reportspan reportps-      items = dbg1 "items" $ postingsReportItems itemps (nullposting,Nothing) whichdate depth startbal runningcalc 1+      -- posting report items ready for display+      items = dbg1 "postingsReport items" $ postingsReportItems displayps (nullposting,Nothing) whichdate depth startbal runningcalc startnum         where-          startbal = if balancetype_ opts == HistoricalBalance then sumPostings precedingps else 0+          historical = balancetype_ opts == HistoricalBalance+          precedingsum = sumPostings precedingps+          precedingavg | null precedingps = 0+                       | otherwise        = precedingsum `divideMixedAmount` (fromIntegral $ length precedingps)+          startbal | average_ opts = if historical then precedingavg else 0+                   | otherwise     = if historical then precedingsum else 0+          startnum = if historical then length precedingps + 1 else 1           runningcalc | average_ opts = \i avg amt -> avg + (amt - avg) `divideMixedAmount` (fromIntegral i) -- running average                       | otherwise     = \_ bal amt -> bal + amt                                              -- running total -      dbg1 s = let p = "postingsReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in debug output-      -- dbg1 = const id  -- exclude from debug output- totallabel = "Total" +-- | Adjust report start/end dates to more useful ones based on+-- journal data and report intervals. Ie:+-- 1. If the start date is unspecified, use the earliest date in the journal (if any)+-- 2. If the end date is unspecified, use the latest date in the journal (if any)+-- 3. If a report interval is specified, enlarge the dates to enclose whole intervals+adjustReportDates :: ReportOpts -> Query -> Journal -> DateSpan+adjustReportDates opts q j = reportspan+  where+    -- see also multiBalanceReport+    requestedspan       = dbg1 "requestedspan"       $ queryDateSpan' q                                       -- span specified by -b/-e/-p options and query args+    journalspan         = dbg1 "journalspan"         $ dates `spanUnion` date2s                               -- earliest and latest dates (or date2s) in the journal+      where+        dates  = journalDateSpan False j+        date2s = journalDateSpan True  j+    requestedspanclosed = dbg1 "requestedspanclosed" $ requestedspan `spanDefaultsFrom` journalspan           -- if open-ended, close it using the journal's dates (if any)+    intervalspans       = dbg1 "intervalspans"       $ splitSpan (interval_ opts) requestedspanclosed  -- get the whole intervals enclosing that+    mreportstart        = dbg1 "reportstart"         $ maybe Nothing spanStart $ headMay intervalspans        -- start of the first interval, or open ended+    mreportend          = dbg1 "reportend"           $ maybe Nothing spanEnd   $ lastMay intervalspans        -- end of the last interval, or open ended+    reportspan          = dbg1 "reportspan"          $ DateSpan mreportstart mreportend                       -- the requested span enlarged to whole intervals if possible++-- | Find postings matching a given query, within a given date span,+-- and also any similarly-matched postings before that date span.+-- Date restrictions and depth restrictions in the query are ignored.+-- A helper for the postings report.+matchedPostingsBeforeAndDuring :: ReportOpts -> Query -> Journal -> DateSpan -> ([Posting],[Posting])+matchedPostingsBeforeAndDuring opts q j (DateSpan mstart mend) =+  dbg1 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps+  where+    beforestartq = dbg1 "beforestartq" $ dateqtype $ DateSpan Nothing mstart+    beforeandduringps =+      dbg1 "ps4" $ sortBy (comparing sortdate) $                               -- sort postings by date or date2+      dbg1 "ps3" $ map (filterPostingAmount symq) $                            -- remove amount parts which the query's cur: terms would exclude+      dbg1 "ps2" $ (if related_ opts then concatMap relatedPostings else id) $ -- with -r, replace each with its sibling postings+      dbg1 "ps1" $ filter (beforeandduringq `matchesPosting`) $                -- filter postings by the query, with no start date or depth limit+                  journalPostings $ journalSelectingAmountFromOpts opts j+      where+        beforeandduringq = dbg1 "beforeandduringq" $ And [depthless $ dateless q, beforeendq]+          where+            depthless  = filterQuery (not . queryIsDepth)+            dateless   = filterQuery (not . queryIsDateOrDate2)+            beforeendq = dateqtype $ DateSpan Nothing mend+        sortdate = if date2_ opts then postingDate2 else postingDate+        symq = dbg1 "symq" $ filterQuery queryIsSym q+    dateqtype+      | queryIsDate2 dateq || (queryIsDate dateq && date2_ opts) = Date2+      | otherwise = Date+      where+        dateq = dbg1 "dateq" $ filterQuery queryIsDateOrDate2 $ dbg1 "q" q  -- XXX confused by multiple date:/date2: ?+ -- | Generate postings report line items from a list of postings or (with -- non-Nothing dates attached) summary postings. postingsReportItems :: [(Posting,Maybe Day)] -> (Posting,Maybe Day) -> WhichDate -> Int -> MixedAmount -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) -> Int -> [PostingsReportItem]@@ -127,7 +165,7 @@   where     date = case wd of PrimaryDate   -> postingDate p                       SecondaryDate -> postingDate2 p-    desc = maybe "" tdescription $ ptransaction p+    desc = T.unpack $ maybe "" tdescription $ ptransaction p  -- | Convert a list of postings into summary postings, one per interval, -- aggregated to the specified depth if any.@@ -148,7 +186,7 @@ -- interval's end date attached with a tuple. type SummaryPosting = (Posting, Maybe Day) --- | Given a date span (representing a reporting interval) and a list of+-- | Given a date span (representing a report interval) and a list of -- postings within it, aggregate the postings into one summary posting per -- account. --@@ -233,8 +271,8 @@     -- with query and/or command-line options    assertEqual "" 11 (length $ snd $ postingsReport defreportopts Any samplejournal)-   assertEqual ""  9 (length $ snd $ postingsReport defreportopts{monthly_=True} Any samplejournal)-   assertEqual "" 19 (length $ snd $ postingsReport defreportopts{monthly_=True, empty_=True} Any samplejournal)+   assertEqual ""  9 (length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal)+   assertEqual "" 19 (length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal)    assertEqual ""  4 (length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal)     -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
Hledger/Reports/ReportOptions.hs view
@@ -15,9 +15,6 @@   checkReportOpts,   flat_,   tree_,-  dateSpanFromOpts,-  intervalFromOpts,-  clearedValueFromOpts,   whichDateFromOpts,   journalSelectingAmountFromOpts,   queryFromOpts,@@ -34,9 +31,12 @@ #if !MIN_VERSION_base(4,8,0) import Data.Functor.Compat ((<$>)) #endif+import Data.Maybe+import qualified Data.Text as T import Data.Typeable (Typeable) import Data.Time.Calendar-import System.Console.CmdArgs.Default  -- some additional default stuff+import Data.Default+import Safe import Test.HUnit  import Hledger.Data@@ -46,13 +46,15 @@  type FormatStr = String --- | Which balance is being shown in a multi-column balance report.-data BalanceType = PeriodBalance     -- ^ The change of balance in each period.-                 | CumulativeBalance -- ^ The accumulated balance at each period's end, starting from zero at the report start date.-                 | HistoricalBalance -- ^ The historical balance at each period's end, starting from the account balances at the report start date.+-- | Which "balance" is being shown in a balance report.+data BalanceType = PeriodChange      -- ^ The change of balance in each period.+                 | CumulativeChange  -- ^ The accumulated change across multiple periods.+                 | HistoricalBalance -- ^ The historical ending balance, including the effect of+                                     --   all postings before the report period. Unless altered by,+                                     --   a query, this is what you would see on a bank statement.   deriving (Eq,Show,Data,Typeable) -instance Default BalanceType where def = PeriodBalance+instance Default BalanceType where def = PeriodChange  -- | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ? data AccountListMode = ALDefault | ALTree | ALFlat deriving (Eq, Show, Data, Typeable)@@ -63,12 +65,9 @@ -- corresponding to hledger's command-line options and query language -- arguments. Used in hledger-lib and above. data ReportOpts = ReportOpts {-     begin_          :: Maybe Day-    ,end_            :: Maybe Day-    ,period_         :: Maybe (Interval,DateSpan)-    ,cleared_        :: Bool-    ,pending_        :: Bool-    ,uncleared_      :: Bool+     period_         :: Period+    ,interval_       :: Interval+    ,clearedstatus_  :: Maybe ClearedStatus     ,cost_           :: Bool     ,depth_          :: Maybe Int     ,display_        :: Maybe DisplayExp@@ -76,19 +75,14 @@     ,empty_          :: Bool     ,no_elide_       :: Bool     ,real_           :: Bool-    ,daily_          :: Bool-    ,weekly_         :: Bool-    ,monthly_        :: Bool-    ,quarterly_      :: Bool-    ,yearly_         :: Bool     ,format_         :: Maybe FormatStr     ,query_          :: String -- all arguments, as a string-    -- register+    -- register only     ,average_        :: Bool     ,related_        :: Bool-    -- balance+    -- balance only     ,balancetype_    :: BalanceType-    ,accountlistmode_  :: AccountListMode+    ,accountlistmode_ :: AccountListMode     ,drop_           :: Int     ,row_total_      :: Bool     ,no_total_       :: Bool@@ -96,6 +90,7 @@  } deriving (Show, Data, Typeable)  instance Default ReportOpts where def = defreportopts+instance Default Bool where def = False  defreportopts :: ReportOpts defreportopts = ReportOpts@@ -119,50 +114,50 @@     def     def     def-    def-    def-    def-    def-    def-    def-    def-    def  rawOptsToReportOpts :: RawOpts -> IO ReportOpts rawOptsToReportOpts rawopts = checkReportOpts <$> do   d <- getCurrentDay+  let rawopts' = checkRawOpts rawopts   return defreportopts{-     begin_       = maybesmartdateopt d "begin" rawopts-    ,end_         = maybesmartdateopt d "end" rawopts-    ,period_      = maybeperiodopt d rawopts-    ,cleared_     = boolopt "cleared" rawopts-    ,pending_     = boolopt "pending" rawopts-    ,uncleared_   = boolopt "uncleared" rawopts-    ,cost_        = boolopt "cost" rawopts-    ,depth_       = maybeintopt "depth" rawopts-    ,display_     = maybedisplayopt d rawopts-    ,date2_       = boolopt "date2" rawopts-    ,empty_       = boolopt "empty" rawopts-    ,no_elide_    = boolopt "no-elide" rawopts-    ,real_        = boolopt "real" rawopts-    ,daily_       = boolopt "daily" rawopts-    ,weekly_      = boolopt "weekly" rawopts-    ,monthly_     = boolopt "monthly" rawopts-    ,quarterly_   = boolopt "quarterly" rawopts-    ,yearly_      = boolopt "yearly" rawopts-    ,format_      = maybestringopt "format" rawopts -- XXX move to CliOpts or move validation from Cli.CliOptions to here-    ,query_       = unwords $ listofstringopt "args" rawopts -- doesn't handle an arg like "" right-    ,average_     = boolopt "average" rawopts-    ,related_     = boolopt "related" rawopts-    ,balancetype_ = balancetypeopt rawopts-    ,accountlistmode_ = accountlistmodeopt rawopts-    ,drop_        = intopt "drop" rawopts-    ,row_total_   = boolopt "row-total" rawopts-    ,no_total_    = boolopt "no-total" rawopts-    ,value_       = boolopt "value" rawopts+     period_      = periodFromRawOpts d rawopts'+    ,interval_    = intervalFromRawOpts rawopts'+    ,clearedstatus_ = clearedStatusFromRawOpts rawopts'+    ,cost_        = boolopt "cost" rawopts'+    ,depth_       = maybeintopt "depth" rawopts'+    ,display_     = maybedisplayopt d rawopts'+    ,date2_       = boolopt "date2" rawopts'+    ,empty_       = boolopt "empty" rawopts'+    ,no_elide_    = boolopt "no-elide" rawopts'+    ,real_        = boolopt "real" rawopts'+    ,format_      = maybestringopt "format" rawopts' -- XXX move to CliOpts or move validation from Cli.CliOptions to here+    ,query_       = unwords $ listofstringopt "args" rawopts' -- doesn't handle an arg like "" right+    ,average_     = boolopt "average" rawopts'+    ,related_     = boolopt "related" rawopts'+    ,balancetype_ = balancetypeopt rawopts'+    ,accountlistmode_ = accountlistmodeopt rawopts'+    ,drop_        = intopt "drop" rawopts'+    ,row_total_   = boolopt "row-total" rawopts'+    ,no_total_    = boolopt "no-total" rawopts'+    ,value_       = boolopt "value" rawopts'     } --- | Do extra validation of opts, raising an error if there is trouble.+-- | Do extra validation of raw option values, raising an error if there's a problem.+checkRawOpts :: RawOpts -> RawOpts+checkRawOpts rawopts+-- our standard behaviour is to accept conflicting options actually,+-- using the last one - more forgiving for overriding command-line aliases+--   | countopts ["change","cumulative","historical"] > 1+--     = optserror "please specify at most one of --change, --cumulative, --historical"+--   | countopts ["flat","tree"] > 1+--     = optserror "please specify at most one of --flat, --tree"+--   | countopts ["daily","weekly","monthly","quarterly","yearly"] > 1+--     = optserror "please specify at most one of --daily, "+  | otherwise = rawopts+--   where+--     countopts = length . filter (`boolopt` rawopts)++-- | Do extra validation of report options, raising an error if there's a problem. checkReportOpts :: ReportOpts -> ReportOpts checkReportOpts ropts@ReportOpts{..} =   either optserror (const ropts) $ do@@ -178,24 +173,93 @@     _          -> ALDefault  balancetypeopt :: RawOpts -> BalanceType-balancetypeopt rawopts-    | length [o | o <- ["cumulative","historical"], isset o] > 1-                         = optserror "please specify at most one of --cumulative and --historical"-    | isset "cumulative" = CumulativeBalance-    | isset "historical" = HistoricalBalance-    | otherwise          = PeriodBalance-    where-      isset = flip boolopt rawopts+balancetypeopt rawopts =+  case reverse $ filter (`elem` ["change","cumulative","historical"]) $ map fst rawopts of+    ("historical":_) -> HistoricalBalance+    ("cumulative":_) -> CumulativeChange+    _                -> PeriodChange -maybesmartdateopt :: Day -> String -> RawOpts -> Maybe Day-maybesmartdateopt d name rawopts =-        case maybestringopt name rawopts of-          Nothing -> Nothing-          Just s -> either-                    (\e -> optserror $ "could not parse "++name++" date: "++show e)-                    Just-                    $ fixSmartDateStrEither' d s+-- Get the period specified by the intersection of -b/--begin, -e/--end and/or+-- -p/--period options, using the given date to interpret relative date expressions.+periodFromRawOpts :: Day -> RawOpts -> Period+periodFromRawOpts d rawopts =+  case (mearliestb, mlateste) of+    (Nothing, Nothing) -> PeriodAll+    (Just b, Nothing)  -> PeriodFrom b+    (Nothing, Just e)  -> PeriodTo e+    (Just b, Just e)   -> simplifyPeriod $+                          PeriodBetween b e+  where+    mearliestb = case beginDatesFromRawOpts d rawopts of+                   [] -> Nothing+                   bs -> Just $ minimum bs+    mlateste   = case endDatesFromRawOpts d rawopts of+                   [] -> Nothing+                   es -> Just $ maximum es +-- Get all begin dates specified by -b/--begin or -p/--period options, in order,+-- using the given date to interpret relative date expressions.+beginDatesFromRawOpts :: Day -> RawOpts -> [Day]+beginDatesFromRawOpts d = catMaybes . map (begindatefromrawopt d)+  where+    begindatefromrawopt d (n,v)+      | n == "begin" =+          either (\e -> optserror $ "could not parse "++n++" date: "++show e) Just $+          fixSmartDateStrEither' d (T.pack v)+      | n == "period" =+        case+          either (\e -> optserror $ "could not parse period option: "++show e) id $+          parsePeriodExpr d (stripquotes $ T.pack v)+        of+          (_, DateSpan (Just b) _) -> Just b+          _                        -> Nothing+      | otherwise = Nothing++-- Get all end dates specified by -e/--end or -p/--period options, in order,+-- using the given date to interpret relative date expressions.+endDatesFromRawOpts :: Day -> RawOpts -> [Day]+endDatesFromRawOpts d = catMaybes . map (enddatefromrawopt d)+  where+    enddatefromrawopt d (n,v)+      | n == "end" =+          either (\e -> optserror $ "could not parse "++n++" date: "++show e) Just $+          fixSmartDateStrEither' d (T.pack v)+      | n == "period" =+        case+          either (\e -> optserror $ "could not parse period option: "++show e) id $+          parsePeriodExpr d (stripquotes $ T.pack v)+        of+          (_, DateSpan _ (Just e)) -> Just e+          _                        -> Nothing+      | otherwise = Nothing++-- | Get the report interval, if any, specified by the last of -p/--period,+-- -D/--daily, -W/--weekly, -M/--monthly etc. options.+intervalFromRawOpts :: RawOpts -> Interval+intervalFromRawOpts = lastDef NoInterval . catMaybes . map intervalfromrawopt+  where+    intervalfromrawopt (n,v)+      | n == "period" =+          either (\e -> optserror $ "could not parse period option: "++show e) (Just . fst) $+          parsePeriodExpr nulldate (stripquotes $ T.pack v) -- reference date does not affect the interval+      | n == "daily"     = Just $ Days 1+      | n == "weekly"    = Just $ Weeks 1+      | n == "monthly"   = Just $ Months 1+      | n == "quarterly" = Just $ Quarters 1+      | n == "yearly"    = Just $ Years 1+      | otherwise = Nothing++-- | Get the cleared status, if any, specified by the last of -C/--cleared,+-- --pending, -U/--uncleared options.+clearedStatusFromRawOpts :: RawOpts -> Maybe ClearedStatus+clearedStatusFromRawOpts = lastMay . catMaybes . map clearedstatusfromrawopt+  where+    clearedstatusfromrawopt (n,_)+      | n == "cleared"   = Just Cleared+      | n == "pending"   = Just Pending+      | n == "uncleared" = Just Uncleared+      | otherwise        = Nothing+ type DisplayExp = String  maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp@@ -203,17 +267,20 @@     maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts     where       fixbracketeddatestr "" = ""-      fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"+      fixbracketeddatestr s = "[" ++ fixSmartDateStr d (T.pack $ init $ tail s) ++ "]" -maybeperiodopt :: Day -> RawOpts -> Maybe (Interval,DateSpan)-maybeperiodopt d rawopts =-    case maybestringopt "period" rawopts of-      Nothing -> Nothing-      Just s -> either-                (\e -> optserror $ "could not parse period option: "++show e)-                Just-                $ parsePeriodExpr d s+-- | Select the Transaction date accessor based on --date2.+transactionDateFn :: ReportOpts -> (Transaction -> Day)+transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate +-- | Select the Posting date accessor based on --date2.+postingDateFn :: ReportOpts -> (Posting -> Day)+postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate++-- | Report which date we will report on based on --date2.+whichDateFromOpts :: ReportOpts -> WhichDate+whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate+ -- | Legacy-compatible convenience aliases for accountlistmode_. tree_ :: ReportOpts -> Bool tree_ = (==ALTree) . accountlistmode_@@ -221,51 +288,9 @@ flat_ :: ReportOpts -> Bool flat_ = (==ALFlat) . accountlistmode_ --- | Figure out the date span we should report on, based on any--- begin/end/period options provided. A period option will cause begin and--- end options to be ignored.-dateSpanFromOpts :: Day -> ReportOpts -> DateSpan-dateSpanFromOpts _ ReportOpts{..} =-    case period_ of Just (_,span) -> span-                    Nothing -> DateSpan begin_ end_---- | Figure out the reporting interval, if any, specified by the options.--- --period overrides --daily overrides --weekly overrides --monthly etc.-intervalFromOpts :: ReportOpts -> Interval-intervalFromOpts ReportOpts{..} =-    case period_ of-      Just (interval,_) -> interval-      Nothing -> i-          where i | daily_ = Days 1-                  | weekly_ = Weeks 1-                  | monthly_ = Months 1-                  | quarterly_ = Quarters 1-                  | yearly_ = Years 1-                  | otherwise =  NoInterval---- | Get a maybe boolean representing the last cleared/uncleared option if any.-clearedValueFromOpts :: ReportOpts -> Maybe ClearedStatus-clearedValueFromOpts ReportOpts{..} | cleared_   = Just Cleared-                                    | pending_   = Just Pending-                                    | uncleared_ = Just Uncleared-                                    | otherwise  = Nothing- -- depthFromOpts :: ReportOpts -> Int -- depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts) --- | Report which date we will report on based on --date2.-whichDateFromOpts :: ReportOpts -> WhichDate-whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate---- | Select the Transaction date accessor based on --date2.-transactionDateFn :: ReportOpts -> (Transaction -> Day)-transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate---- | Select the Posting date accessor based on --date2.-postingDateFn :: ReportOpts -> (Posting -> Day)-postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate-- -- | Convert this journal's postings' amounts to the cost basis amounts if -- specified by options. journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal@@ -275,25 +300,25 @@  -- | Convert report options and arguments to a query. queryFromOpts :: Day -> ReportOpts -> Query-queryFromOpts d opts@ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]+queryFromOpts d ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]   where     flagsq = And $-              [(if date2_ then Date2 else Date) $ dateSpanFromOpts d opts]+              [(if date2_ then Date2 else Date) $ periodAsDateSpan period_]               ++ (if real_ then [Real True] else [])               ++ (if empty_ then [Empty True] else []) -- ?-              ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))+              ++ (maybe [] ((:[]) . Status) clearedstatus_)               ++ (maybe [] ((:[]) . Depth) depth_)-    argsq = fst $ parseQuery d query_+    argsq = fst $ parseQuery d (T.pack query_)  -- | Convert report options to a query, ignoring any non-flag command line arguments. queryFromOptsOnly :: Day -> ReportOpts -> Query-queryFromOptsOnly d opts@ReportOpts{..} = simplifyQuery flagsq+queryFromOptsOnly _d ReportOpts{..} = simplifyQuery flagsq   where     flagsq = And $-              [(if date2_ then Date2 else Date) $ dateSpanFromOpts d opts]+              [(if date2_ then Date2 else Date) $ periodAsDateSpan period_]               ++ (if real_ then [Real True] else [])               ++ (if empty_ then [Empty True] else []) -- ?-              ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))+              ++ (maybe [] ((:[]) . Status) clearedstatus_)               ++ (maybe [] ((:[]) . Depth) depth_)  tests_queryFromOpts :: [Test]@@ -303,7 +328,7 @@   assertEqual "" (Acct "a") (queryFromOpts nulldate defreportopts{query_="a"})   assertEqual "" (Desc "a a") (queryFromOpts nulldate defreportopts{query_="desc:'a a'"})   assertEqual "" (Date $ mkdatespan "2012/01/01" "2013/01/01")-                 (queryFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")+                 (queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01")                                                       ,query_="date:'to 2013'"                                                       })   assertEqual "" (Date2 $ mkdatespan "2012/01/01" "2013/01/01")@@ -317,14 +342,14 @@ queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts   where     flagsqopts = []-    argsqopts = snd $ parseQuery d query_+    argsqopts = snd $ parseQuery d (T.pack query_)  tests_queryOptsFromOpts :: [Test] tests_queryOptsFromOpts = [  "queryOptsFromOpts" ~: do   assertEqual "" [] (queryOptsFromOpts nulldate defreportopts)   assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{query_="a"})-  assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")+  assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01")                                                              ,query_="date:'to 2013'"                                                              })  ]
Hledger/Reports/TransactionsReports.hs view
@@ -21,7 +21,8 @@   triCommodityBalance,   journalTransactionsReport,   accountTransactionsReport,-  transactionsReportByCommodity+  transactionsReportByCommodity,+  transactionRegisterDate    -- -- * Tests   -- tests_Hledger_Reports_TransactionsReports@@ -30,11 +31,15 @@  import Data.List import Data.Ord+-- import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Calendar -- import Test.HUnit  import Hledger.Data import Hledger.Query import Hledger.Reports.ReportOptions+import Hledger.Utils.Debug   -- | A transactions report includes a list of transactions@@ -48,11 +53,13 @@                           ,[TransactionsReportItem] -- line items, one per transaction                           ) type TransactionsReportItem = (Transaction -- the original journal transaction, unmodified-                              ,Transaction -- the transaction as seen from a particular account+                              ,Transaction -- the transaction as seen from a particular account, with postings maybe filtered                               ,Bool        -- is this a split, ie more than one other account posting                               ,String      -- a display string describing the other account(s), if any-                              ,MixedAmount -- the amount posted to the current account(s) (or total amount posted)-                              ,MixedAmount -- the running balance for the current account(s) after this transaction+                              ,MixedAmount -- the amount posted to the current account(s) by the filtered postings (or total amount posted)+                              ,MixedAmount -- the running total of item amounts, starting from zero;+                                           -- or with --historical, the running total including items+                                           -- (matched by the report query) preceding the report period                               )  triOrigTransaction (torig,_,_,_,_,_) = torig@@ -80,20 +87,40 @@  -- | An account transactions report represents transactions affecting -- a particular account (or possibly several accounts, but we don't--- use that). It is used by hledger-web's (and hledger-ui's) account--- register view, where we want to show one row per journal--- transaction, with:+-- use that). It is used eg by hledger-ui's and hledger-web's account+-- register view, where we want to show one row per transaction, in+-- the context of the current account. Report items consist of: ----- - the total increase/decrease to the current account+-- - the transaction, unmodified ----- - the names of the other account(s) posted to/from+-- - the transaction as seen in the context of the current account and query,+--   which means: ----- - transaction dates, adjusted to the date of the earliest posting to---   the current account if those postings have their own dates+--   - the transaction date is set to the "transaction context date",+--     which can be different from the transaction's general date:+--     if postings to the current account (and matched by the report query)+--     have their own dates, it's the earliest of these dates. ----- Currently, reporting intervals are not supported, and report items--- are most recent first.+--   - the transaction's postings are filtered, excluding any which are not+--     matched by the report query --+-- - a text description of the other account(s) posted to/from+--+-- - a flag indicating whether there's more than one other account involved+--+-- - the total increase/decrease to the current account+--+-- - the report transactions' running total after this transaction;+--   or if historical balance is requested (-H), the historical running total.+--   The historical running total includes transactions from before the+--   report start date if one is specified, filtered by the report query.+--   The historical running total may or may not be the account's historical+--   running balance, depending on the report query.+--+-- Items are sorted by transaction register date (the earliest date the transaction+-- posts to the current account), most recent first.+-- Reporting intervals are currently ignored.+-- type AccountTransactionsReport =   (String                          -- label for the balance column, eg "balance" or "total"   ,[AccountTransactionsReportItem] -- line items, one per transaction@@ -101,94 +128,98 @@  type AccountTransactionsReportItem =   (-   Transaction -- the original journal transaction-  ,Transaction -- the adjusted account transaction-  ,Bool        -- is this a split, ie with more than one posting to other account(s)+   Transaction -- the transaction, unmodified+  ,Transaction -- the transaction, as seen from the current account+  ,Bool        -- is this a split (more than one posting to other accounts) ?   ,String      -- a display string describing the other account(s), if any   ,MixedAmount -- the amount posted to the current account(s) (or total amount posted)-  ,MixedAmount -- the running balance for the current account(s) after this transaction+  ,MixedAmount -- the register's running total or the current account(s)'s historical balance, after this transaction   )  accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport-accountTransactionsReport opts j q thisacctquery = (label, items)- where-     -- transactions with excluded currencies removed-     ts1 = jtxns $-           filterJournalAmounts (filterQuery queryIsSym q) $-           journalSelectingAmountFromOpts opts j-     -- affecting this account-     ts2 = filter (matchesTransaction thisacctquery) ts1-     -- with dates adjusted for account transactions report-     ts3 = map (setTransactionDateToPostingDate q thisacctquery) ts2-     -- and sorted-     ts = sortBy (comparing tdate) ts3--     -- starting balance: if we are filtering by a start date and nothing else,-     -- the sum of postings to this account before that date; otherwise zero.-     (startbal,label) | queryIsNull q                        = (nullmixedamt,        balancelabel)-                      | queryIsStartDateOnly (date2_ opts) q = (sumPostings priorps, balancelabel)-                      | otherwise                            = (nullmixedamt,        totallabel)-                      where-                        priorps = -- ltrace "priorps" $-                                  filter (matchesPosting-                                          (-- ltrace "priormatcher" $-                                           And [thisacctquery, tostartdatequery]))-                                         $ transactionsPostings ts-                        tostartdatequery = Date (DateSpan Nothing startdate)-                        startdate = queryStartDate (date2_ opts) q+accountTransactionsReport opts j reportq thisacctq = (label, items)+  where+    -- a depth limit does not affect the account transactions report+    -- seems unnecessary for some reason XXX+    reportq' = -- filterQuery (not . queryIsDepth)+               reportq+    -- get all transactions, with amounts converted to cost basis if -B+    ts1 = jtxns $ journalSelectingAmountFromOpts opts j+    -- apply any cur:SYM filters in reportq'+    symq  = filterQuery queryIsSym reportq'+    ts2 = (if queryIsNull symq then id else map (filterTransactionAmounts symq)) ts1+    -- keep just the transactions affecting this account (via possibly realness or status-filtered postings)+    realq = filterQuery queryIsReal reportq'+    statusq = filterQuery queryIsStatus reportq'+    ts3 = filter (matchesTransaction thisacctq . filterTransactionPostings (And [realq, statusq])) ts2+    -- sort by the transaction's register date, for accurate starting balance+    ts = sortBy (comparing (transactionRegisterDate reportq' thisacctq)) ts3 -     items = reverse $ -- see also registerChartHtml-             accountTransactionsReportItems q thisacctquery startbal negate ts+    (startbal,label)+      | balancetype_ opts == HistoricalBalance = (sumPostings priorps, balancelabel)+      | otherwise                              = (nullmixedamt,        totallabel)+      where+        priorps = dbg1 "priorps" $+                  filter (matchesPosting+                          (dbg1 "priorq" $+                           And [thisacctq, tostartdateq, datelessreportq]))+                         $ transactionsPostings ts+        tostartdateq =+          case mstartdate of+            Just _  -> Date (DateSpan Nothing mstartdate)+            Nothing -> None  -- no start date specified, there are no prior postings+        mstartdate = queryStartDate (date2_ opts) reportq'+        datelessreportq = filterQuery (not . queryIsDateOrDate2) reportq' --- | Adjust a transaction's date to the earliest date of postings to a--- particular account, if any, after filtering with a certain query.-setTransactionDateToPostingDate :: Query -> Query -> Transaction -> Transaction-setTransactionDateToPostingDate query thisacctquery t = t'-  where-    queryps = tpostings $ filterTransactionPostings query t-    thisacctps = filter (matchesPosting thisacctquery) queryps-    t' = case thisacctps of-          [] -> t-          _  -> t{tdate=d}-            where-              d | null ds   = tdate t-                | otherwise = minimum ds-              ds = map postingDate thisacctps-                   -- no opts here, don't even bother with that date/date2 rigmarole+    items = reverse $ -- see also registerChartHtml+            accountTransactionsReportItems reportq' thisacctq startbal negate ts -totallabel = "Running Total"-balancelabel = "Historical Balance"+totallabel = "Period Total"+balancelabel = "Historical Total"  -- | Generate transactions report items from a list of transactions,--- using the provided query and current account queries, starting--- balance, sign-setting function and balance-summing function. With a--- "this account" query of None, this can be used the for the--- journalTransactionsReport also.+-- using the provided user-specified report query, a query specifying+-- which account to use as the focus, a starting balance, a sign-setting+-- function and a balance-summing function. Or with a None current account+-- query, this can also be used for the journalTransactionsReport. accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [TransactionsReportItem] accountTransactionsReportItems _ _ _ _ [] = []-accountTransactionsReportItems query thisacctquery bal signfn (torig:ts) =-    -- This is used for both accountTransactionsReport and journalTransactionsReport,-    -- which makes it a bit overcomplicated+accountTransactionsReportItems reportq thisacctq bal signfn (torig:ts) =     case i of Just i' -> i':is               Nothing -> is+    -- 201403: This is used for both accountTransactionsReport and journalTransactionsReport, which makes it a bit overcomplicated+    -- 201407: I've lost my grip on this, let's just hope for the best+    -- 201606: we now calculate change and balance from filtered postings, check this still works well for all callers XXX     where-      -- XXX I've lost my grip on this, let's just hope for the best-      origps = tpostings torig-      tacct@Transaction{tpostings=queryps} = filterTransactionPostings query torig-      (thisacctps, otheracctps) = partition (matchesPosting thisacctquery) origps-      amt = negate $ sum $ map pamount thisacctps-      numotheraccts = length $ nub $ map paccount otheracctps-      otheracctstr | thisacctquery == None = summarisePostingAccounts origps-                   | numotheraccts == 0    = summarisePostingAccounts thisacctps-                   | otherwise             = summarisePostingAccounts otheracctps-      (i,bal') = case queryps of-           [] -> (Nothing,bal)+      tfiltered@Transaction{tpostings=reportps} = filterTransactionPostings reportq torig+      tacct = tfiltered{tdate=transactionRegisterDate reportq thisacctq tfiltered}+      (i,bal') = case reportps of+           [] -> (Nothing,bal)  -- no matched postings in this transaction, skip it            _  -> (Just (torig, tacct, numotheraccts > 1, otheracctstr, a, b), b)                  where-                  a = signfn amt+                  (thisacctps, otheracctps) = partition (matchesPosting thisacctq) reportps+                  numotheraccts = length $ nub $ map paccount otheracctps+                  otheracctstr | thisacctq == None  = summarisePostingAccounts reportps     -- no current account ? summarise all matched postings+                               | numotheraccts == 0 = summarisePostingAccounts thisacctps   -- only postings to current account ? summarise those+                               | otherwise          = summarisePostingAccounts otheracctps  -- summarise matched postings to other account(s)+                  a = signfn $ negate $ sum $ map pamount thisacctps                   b = bal + a-      is = accountTransactionsReportItems query thisacctquery bal' signfn ts+      is = accountTransactionsReportItems reportq thisacctq bal' signfn ts +-- | What is the transaction's date in the context of a particular account+-- (specified with a query) and report query, as in an account register ?+-- It's normally the transaction's general date, but if any posting(s)+-- matched by the report query and affecting the matched account(s) have+-- their own earlier dates, it's the earliest of these dates.+-- Secondary transaction/posting dates are ignored.+transactionRegisterDate :: Query -> Query -> Transaction -> Day+transactionRegisterDate reportq thisacctq t+  | null thisacctps = tdate t+  | otherwise       = minimum $ map postingDate thisacctps+  where+    reportps   = tpostings $ filterTransactionPostings reportq t+    thisacctps = filter (matchesPosting thisacctq) reportps+ -- -- | Generate a short readable summary of some postings, like -- -- "from (negatives) to (positives)". -- summarisePostings :: [Posting] -> String@@ -204,20 +235,17 @@ -- To reduce noise, if there are both real and virtual postings, show only the real ones. summarisePostingAccounts :: [Posting] -> String summarisePostingAccounts ps =-  (intercalate ", " . map accountSummarisedName . nub . map paccount) displayps+  (intercalate ", " . map (T.unpack . accountSummarisedName) . nub . map paccount) displayps -- XXX pack   where     realps = filter isReal ps     displayps | null realps = ps               | otherwise   = realps -filterTransactionPostings :: Query -> Transaction -> Transaction-filterTransactionPostings m t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps}- -------------------------------------------------------------------------------  -- | Split a transactions report whose items may involve several commodities, -- into one or more single-commodity transactions reports.-transactionsReportByCommodity :: TransactionsReport -> [(Commodity, TransactionsReport)]+transactionsReportByCommodity :: TransactionsReport -> [(CommoditySymbol, TransactionsReport)] transactionsReportByCommodity tr =   [(c, filterTransactionsReportByCommodity c tr) | c <- transactionsReportCommodities tr]   where@@ -227,7 +255,7 @@ -- Remove transaction report items and item amount (and running -- balance amount) components that don't involve the specified -- commodity. Other item fields such as the transaction are left unchanged.-filterTransactionsReportByCommodity :: Commodity -> TransactionsReport -> TransactionsReport+filterTransactionsReportByCommodity :: CommoditySymbol -> TransactionsReport -> TransactionsReport filterTransactionsReportByCommodity c (label,items) =   (label, fixTransactionsReportItemBalances $ concat [filterTransactionsReportItemByCommodity c i | i <- items])   where
Hledger/Utils.hs view
@@ -22,6 +22,7 @@                           module Hledger.Utils.Parse,                           module Hledger.Utils.Regex,                           module Hledger.Utils.String,+                          module Hledger.Utils.Text,                           module Hledger.Utils.Test,                           module Hledger.Utils.Tree,                           -- Debug.Trace.trace,@@ -32,13 +33,16 @@                           ) where import Control.Monad (liftM)-import Control.Monad.IO.Class (MonadIO, liftIO) -- import Data.Char -- import Data.List -- import Data.Maybe -- import Data.PPrint+import Data.Text (Text)+import qualified Data.Text.IO as T import Data.Time.Clock import Data.Time.LocalTime+-- import Data.Text (Text)+-- import qualified Data.Text as T import System.Directory (getHomeDirectory) import System.FilePath((</>), isRelative) import System.IO@@ -49,6 +53,7 @@ import Hledger.Utils.Parse import Hledger.Utils.Regex import Hledger.Utils.String+import Hledger.Utils.Text import Hledger.Utils.Test import Hledger.Utils.Tree -- import Prelude hiding (readFile,writeFile,appendFile,getContents,putStr,putStrLn)@@ -92,6 +97,8 @@     split es = let (first,rest) = break (x==) es                in first : splitAtElement x rest +-- text+ -- time  getCurrentLocalTime :: IO LocalTime@@ -100,6 +107,12 @@   tz <- getCurrentTimeZone   return $ utcToLocalTime tz t +getCurrentZonedTime :: IO ZonedTime+getCurrentZonedTime = do+  t <- getCurrentTime+  tz <- getCurrentTimeZone+  return $ utcToZonedTime tz t+ -- misc  isLeft :: Either a b -> Bool@@ -115,25 +128,44 @@  -- | Convert a possibly relative, possibly tilde-containing file path to an absolute one, -- given the current directory. ~username is not supported. Leave "-" unchanged.-expandPath :: MonadIO m => FilePath -> FilePath -> m FilePath -- general type sig for use in reader parsers+-- Can raise an error.+expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers expandPath _ "-" = return "-" expandPath curdir p = (if isRelative p then (curdir </>) else id) `liftM` expandPath' p   where-    expandPath' ('~':'/':p)  = liftIO $ (</> p) `fmap` getHomeDirectory-    expandPath' ('~':'\\':p) = liftIO $ (</> p) `fmap` getHomeDirectory-    expandPath' ('~':_)      = error' "~USERNAME in paths is not supported"+    expandPath' ('~':'/':p)  = (</> p) <$> getHomeDirectory+    expandPath' ('~':'\\':p) = (</> p) <$> getHomeDirectory+    expandPath' ('~':_)      = ioError $ userError "~USERNAME in paths is not supported"     expandPath' p            = return p  firstJust ms = case dropWhile (==Nothing) ms of     [] -> Nothing     (md:_) -> md --- | Read a file in universal newline mode, handling whatever newline convention it may contain.-readFile' :: FilePath -> IO String+-- | Read a file in universal newline mode, handling any of the usual line ending conventions.+readFile' :: FilePath -> IO Text readFile' name =  do   h <- openFile name ReadMode   hSetNewlineMode h universalNewlineMode-  hGetContents h+  T.hGetContents h++-- | Read a file in universal newline mode, handling any of the usual line ending conventions.+readFileAnyLineEnding :: FilePath -> IO Text+readFileAnyLineEnding path =  do+  h <- openFile path ReadMode+  hSetNewlineMode h universalNewlineMode+  T.hGetContents h++-- | Read the given file, or standard input if the path is "-", using+-- universal newline mode.+readFileOrStdinAnyLineEnding :: String -> IO Text+readFileOrStdinAnyLineEnding f = do+  h <- fileHandle f+  hSetNewlineMode h universalNewlineMode+  T.hGetContents h+  where+    fileHandle "-" = return stdin+    fileHandle f = openFile f ReadMode  -- | Total version of maximum, for integral types, giving 0 for an empty list. maximum' :: Integral a => [a] -> a
Hledger/Utils/Debug.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP, FlexibleContexts #-}+{-# LANGUAGE CPP, FlexibleContexts, TypeFamilies #-} -- | Debugging helpers  -- more:@@ -16,50 +16,42 @@ ) where -import Control.Monad (when)-import Data.List-import Debug.Trace-import Safe (readDef)-import System.Environment (getArgs)-import System.Exit-import System.IO.Unsafe (unsafePerformIO)-import Text.Parsec-import Text.Printf+import           Control.Monad (when)+import           Control.Monad.IO.Class+import           Data.List hiding (uncons)+import qualified Data.Text as T+import           Debug.Trace+import           Hledger.Utils.Parse+import           Safe (readDef)+import           System.Environment (getArgs)+import           System.Exit+import           System.IO.Unsafe (unsafePerformIO)+import           Text.Megaparsec+import           Text.Printf  #if __GLASGOW_HASKELL__ >= 704-import Text.Show.Pretty (ppShow)+import           Text.Show.Pretty (ppShow) #else -- the required pretty-show version requires GHC >= 7.4 ppShow :: Show a => a -> String ppShow = show #endif ---- | Trace (print on stdout at runtime) a showable value.--- (for easily tracing in the middle of a complex expression)-strace :: Show a => a -> a-strace a = trace (show a) a---- | Labelled trace - like strace, with a label prepended.-ltrace :: Show a => String -> a -> a-ltrace l a = trace (l ++ ": " ++ show a) a---- | Monadic trace - like strace, but works as a standalone line in a monad.-mtrace :: (Monad m, Show a) => a -> m a-mtrace a = strace a `seq` return a+pprint :: Show a => a -> IO ()+pprint = putStrLn . ppShow --- | Custom trace - like strace, with a custom show function.+-- | Trace (print to stderr) a showable value using a custom show function. traceWith :: (a -> String) -> a -> a-traceWith f e = trace (f e) e+traceWith f a = trace (f a) a  -- | Parsec trace - show the current parsec position and next input, -- and the provided label if it's non-null.-ptrace :: Stream [Char] m t => String -> ParsecT [Char] st m ()+ptrace :: String -> TextParser m () ptrace msg = do   pos <- getPosition-  next <- take peeklength `fmap` getInput+  next <- (T.take peeklength) `fmap` getInput   let (l,c) = (sourceLine pos, sourceColumn pos)-      s  = printf "at line %2d col %2d: %s" l c (show next) :: String+      s  = printf "at line %2d col %2d: %s" (unPos l) (unPos c) (show next) :: String       s' = printf ("%-"++show (peeklength+30)++"s") s ++ " " ++ msg   trace s' $ return ()   where@@ -70,8 +62,10 @@ -- @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to -- a higher value (note: not @--debug N@ for some reason).  This uses -- unsafePerformIO and can be accessed from anywhere and before normal--- command-line processing. After command-line processing, it is also--- available as the @debug_@ field of 'Hledger.Cli.CliOptions.CliOpts'.+-- command-line processing. When running with :main in GHCI, you must+-- touch and reload this module to see the effect of a new --debug option.+-- After command-line processing, it is also available as the @debug_@+-- field of 'Hledger.Cli.CliOptions.CliOpts'. -- {-# OPTIONS_GHC -fno-cse #-}  -- {-# NOINLINE debugLevel #-} debugLevel :: Int@@ -121,34 +115,34 @@  -- | Convenience aliases for tracePrettyAtIO. -- Like dbg, but convenient to insert in an IO monad.-dbgIO :: Show a => String -> a -> IO ()+dbgIO :: (MonadIO m, Show a) => String -> a -> m () dbgIO = tracePrettyAtIO 0 -dbg1IO :: Show a => String -> a -> IO ()+dbg1IO :: (MonadIO m, Show a) => String -> a -> m () dbg1IO = tracePrettyAtIO 1 -dbg2IO :: Show a => String -> a -> IO ()+dbg2IO :: (MonadIO m, Show a) => String -> a -> m () dbg2IO = tracePrettyAtIO 2 -dbg3IO :: Show a => String -> a -> IO ()+dbg3IO :: (MonadIO m, Show a) => String -> a -> m () dbg3IO = tracePrettyAtIO 3 -dbg4IO :: Show a => String -> a -> IO ()+dbg4IO :: (MonadIO m, Show a) => String -> a -> m () dbg4IO = tracePrettyAtIO 4 -dbg5IO :: Show a => String -> a -> IO ()+dbg5IO :: (MonadIO m, Show a) => String -> a -> m () dbg5IO = tracePrettyAtIO 5 -dbg6IO :: Show a => String -> a -> IO ()+dbg6IO :: (MonadIO m, Show a) => String -> a -> m () dbg6IO = tracePrettyAtIO 6 -dbg7IO :: Show a => String -> a -> IO ()+dbg7IO :: (MonadIO m, Show a) => String -> a -> m () dbg7IO = tracePrettyAtIO 7 -dbg8IO :: Show a => String -> a -> IO ()+dbg8IO :: (MonadIO m, Show a) => String -> a -> m () dbg8IO = tracePrettyAtIO 8 -dbg9IO :: Show a => String -> a -> IO ()+dbg9IO :: (MonadIO m, Show a) => String -> a -> m () dbg9IO = tracePrettyAtIO 9  -- | Pretty-print a message and a showable value to the console if the debug level is at or above the specified level.@@ -156,11 +150,9 @@ tracePrettyAt :: Show a => Int -> String -> a -> a tracePrettyAt lvl = dbgppshow lvl -tracePrettyAtIO :: Show a => Int -> String -> a -> IO ()-tracePrettyAtIO lvl lbl x = tracePrettyAt lvl lbl x `seq` return ()---- XXX--- Could not deduce (a ~ ())+-- tracePrettyAtM :: (Monad m, Show a) => Int -> String -> a -> m a+-- tracePrettyAtM lvl lbl x = tracePrettyAt lvl lbl x `seq` return x+-- XXX Could not deduce (a ~ ()) -- from the context (Show a) --   bound by the type signature for --              dbgM :: Show a => String -> a -> IO ()@@ -170,25 +162,25 @@ --       at hledger/Hledger/Cli/Main.hs:200:13 -- Expected type: String -> a -> IO () --   Actual type: String -> a -> IO a------ tracePrettyAtM :: (Monad m, Show a) => Int -> String -> a -> m a--- tracePrettyAtM lvl lbl x = tracePrettyAt lvl lbl x `seq` return x +tracePrettyAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()+tracePrettyAtIO lvl lbl x = liftIO $ tracePrettyAt lvl lbl x `seq` return ()+ -- | print this string to the console before evaluating the expression,--- if the global debug level is non-zero.  Uses unsafePerformIO.-dbgtrace :: String -> a -> a-dbgtrace-    | debugLevel > 0 = trace-    | otherwise      = flip const+-- if the global debug level is at or above the specified level.  Uses unsafePerformIO.+-- dbgtrace :: Int -> String -> a -> a+-- dbgtrace level+--     | debugLevel >= level = trace+--     | otherwise           = flip const  -- | Print a showable value to the console, with a message, if the -- debug level is at or above the specified level (uses -- unsafePerformIO). -- Values are displayed with show, all on one line, which is hard to read.-dbgshow :: Show a => Int -> String -> a -> a-dbgshow level-    | debugLevel >= level = ltrace-    | otherwise           = flip const+-- dbgshow :: Show a => Int -> String -> a -> a+-- dbgshow level+--     | debugLevel >= level = ltrace+--     | otherwise           = flip const  -- | Print a showable value to the console, with a message, if the -- debug level is at or above the specified level (uses@@ -218,7 +210,6 @@ --                               return a --     | otherwise           = a - -- | Like dbg, then exit the program. Uses unsafePerformIO. dbgExit :: Show a => String -> a -> a dbgExit msg = const (unsafePerformIO exitFailure) . dbg msg@@ -227,7 +218,14 @@ -- input) to the console when the debug level is at or above -- this level. Uses unsafePerformIO. -- pdbgAt :: GenParser m => Float -> String -> m ()-pdbg :: Stream [Char] m t => Int -> String -> ParsecT [Char] st m ()+pdbg :: Int -> String -> TextParser m () pdbg level msg = when (level <= debugLevel) $ ptrace msg -+-- | Like dbg, but writes the output to "debug.log" in the current directory.+-- Uses unsafePerformIO. Can fail due to log file contention if called too quickly+-- ("*** Exception: debug.log: openFile: resource busy (file is locked)").+dbglog :: Show a => String -> a -> a+dbglog label a =+  (unsafePerformIO $+    appendFile "debug.log" $ label ++ ": " ++ ppShow a ++ "\n")+  `seq` a
Hledger/Utils/Parse.hs view
@@ -1,45 +1,71 @@-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleContexts, TypeFamilies #-} module Hledger.Utils.Parse where +import Control.Monad.Except import Data.Char import Data.List-import Text.Parsec+import Data.Text (Text)+import Text.Megaparsec hiding (State)+import Data.Functor.Identity (Identity(..)) import Text.Printf +import Control.Monad.State.Strict (StateT, evalStateT)++import Hledger.Data.Types import Hledger.Utils.UTF8IOCompat (error') +-- | A parser of strict text with generic user state, monad and return type.+type TextParser m a = ParsecT Dec Text m a++type JournalStateParser m a = StateT Journal (ParsecT Dec Text m) a++type JournalParser a = StateT Journal (ParsecT Dec Text Identity) a++-- | A journal parser that runs in IO and can throw an error mid-parse.+type ErroringJournalParser a = StateT Journal (ParsecT Dec Text (ExceptT String IO)) a+ -- | Backtracking choice, use this when alternatives share a prefix. -- Consumes no input if all choices fail.-choice' :: Stream s m t => [ParsecT s u m a] -> ParsecT s u m a-choice' = choice . map Text.Parsec.try+choice' :: [TextParser m a] -> TextParser m a+choice' = choice . map Text.Megaparsec.try -parsewith :: Parsec [Char] () a -> String -> Either ParseError a-parsewith p = runParser p () ""+-- | Backtracking choice, use this when alternatives share a prefix.+-- Consumes no input if all choices fail.+choiceInState :: [StateT s (ParsecT Dec Text m) a] -> StateT s (ParsecT Dec Text m) a+choiceInState = choice . map Text.Megaparsec.try -parseWithCtx :: Stream s m t => u -> ParsecT s u m a -> s -> m (Either ParseError a)-parseWithCtx ctx p = runParserT p ctx ""+parsewith :: Parsec e Text a -> Text -> Either (ParseError Char e) a+parsewith p = runParser p "" -fromparse :: Either ParseError a -> a+parsewithString :: Parsec e String a -> String -> Either (ParseError Char e) a+parsewithString p = runParser p ""++parseWithState :: Monad m => st -> StateT st (ParsecT Dec Text m) a -> Text -> m (Either (ParseError Char Dec) a)+parseWithState ctx p s = runParserT (evalStateT p ctx) "" s++parseWithState' :: (Stream s, ErrorComponent e) => st -> StateT st (ParsecT e s Identity) a -> s -> (Either (ParseError (Token s) e) a)+parseWithState' ctx p s = runParser (evalStateT p ctx) "" s++fromparse :: (Show t, Show e) => Either (ParseError t e) a -> a fromparse = either parseerror id -parseerror :: ParseError -> a+parseerror :: (Show t, Show e) => ParseError t e -> a parseerror e = error' $ showParseError e -showParseError :: ParseError -> String+showParseError :: (Show t, Show e) => ParseError t e -> String showParseError e = "parse error at " ++ show e -showDateParseError :: ParseError -> String+showDateParseError :: (Show t, Show e) => ParseError t e -> String showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e) -nonspace :: (Stream [Char] m Char) => ParsecT [Char] st m Char+nonspace :: TextParser m Char nonspace = satisfy (not . isSpace) -spacenonewline :: (Stream [Char] m Char) => ParsecT [Char] st m Char+spacenonewline :: (Stream s, Char ~ Token s) => ParsecT Dec s m Char spacenonewline = satisfy (`elem` " \v\f\t") -restofline :: (Stream [Char] m Char) => ParsecT [Char] st m String+restofline :: TextParser m String restofline = anyChar `manyTill` newline -eolof :: (Stream [Char] m Char) => ParsecT [Char] st m ()+eolof :: TextParser m () eolof = (newline >> return ()) <|> eof-
Hledger/Utils/String.hs view
@@ -8,19 +8,13 @@  stripbrackets,  unbracket,  -- quoting- quoteIfSpaced,  quoteIfNeeded,  singleQuoteIfNeeded,  -- quotechars,  -- whitespacechars,- escapeDoubleQuotes,- escapeSingleQuotes,  escapeQuotes,  words',  unwords',- stripquotes,- isSingleQuoted,- isDoubleQuoted,  -- * single-line layout  strip,  lstrip,@@ -42,6 +36,7 @@  cliptopleft,  fitto,  -- * wide-character-aware layout+ charWidth,  strWidth,  takeWidth,  fitString,@@ -53,7 +48,7 @@  import Data.Char import Data.List-import Text.Parsec+import Text.Megaparsec import Text.Printf (printf)  import Hledger.Utils.Parse@@ -106,20 +101,11 @@             | last s == '\n' = s             | otherwise = s ++ "\n" --- | Wrap a string in double quotes, and \-prefix any embedded single--- quotes, if it contains whitespace and is not already single- or--- double-quoted.-quoteIfSpaced :: String -> String-quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s-                | not $ any (`elem` s) whitespacechars = s-                | otherwise = "'"++escapeSingleQuotes s++"'"- -- | Double-quote this string if it contains whitespace, single quotes -- or double-quotes, escaping the quotes as needed. quoteIfNeeded :: String -> String quoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = "\"" ++ escapeDoubleQuotes s ++ "\""                 | otherwise = s- -- | Single-quote this string if it contains whitespace or double-quotes. -- No good for strings containing single quotes. singleQuoteIfNeeded :: String -> String@@ -133,9 +119,6 @@ escapeDoubleQuotes :: String -> String escapeDoubleQuotes = regexReplace "\"" "\"" -escapeSingleQuotes :: String -> String-escapeSingleQuotes = regexReplace "'" "\'"- escapeQuotes :: String -> String escapeQuotes = regexReplace "([\"'])" "\\1" @@ -143,9 +126,9 @@ -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails. words' :: String -> [String] words' "" = []-words' s  = map stripquotes $ fromparse $ parsewith p s+words' s  = map stripquotes $ fromparse $ parsewithString p s     where-      p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` many1 spacenonewline+      p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` some spacenonewline              -- eof              return ss       pattern = many (noneOf whitespacechars)
Hledger/Utils/Test.hs view
@@ -1,7 +1,7 @@ module Hledger.Utils.Test where  import Test.HUnit-import Text.Parsec+import Text.Megaparsec  -- | Get a Test's label, or the empty string. testName :: Test -> String@@ -25,15 +25,16 @@ a `is` e = assertEqual "" e a  -- | Assert a parse result is successful, printing the parse error on failure.-assertParse :: (Either ParseError a) -> Assertion+assertParse :: (Show t, Show e) => (Either (ParseError t e) a) -> Assertion assertParse parse = either (assertFailure.show) (const (return ())) parse + -- | Assert a parse result is successful, printing the parse error on failure.-assertParseFailure :: (Either ParseError a) -> Assertion+assertParseFailure :: (Either (ParseError t e) a) -> Assertion assertParseFailure parse = either (const $ return ()) (const $ assertFailure "parse should not have succeeded") 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 :: (Show a, Eq a, Show t, Show e) => (Either (ParseError t e) a) -> a -> Assertion assertParseEqual parse expected = either (assertFailure.show) (`is` expected) parse  printParseError :: (Show a) => a -> IO ()
+ Hledger/Utils/Text.hs view
@@ -0,0 +1,414 @@+-- | Text formatting helpers, ported from String as needed.+-- There may be better alternatives out there.++{-# LANGUAGE OverloadedStrings #-}++module Hledger.Utils.Text+ --  (+ -- -- * misc+ -- lowercase,+ -- uppercase,+ -- underline,+ -- stripbrackets,+ -- unbracket,+ -- -- quoting+ -- quoteIfSpaced,+ -- quoteIfNeeded,+ -- singleQuoteIfNeeded,+ -- -- quotechars,+ -- -- whitespacechars,+ -- escapeDoubleQuotes,+ -- escapeSingleQuotes,+ -- escapeQuotes,+ -- words',+ -- unwords',+ -- stripquotes,+ -- isSingleQuoted,+ -- isDoubleQuoted,+ -- -- * single-line layout+ -- strip,+ -- lstrip,+ -- rstrip,+ -- chomp,+ -- elideLeft,+ -- elideRight,+ -- formatString,+ -- -- * multi-line layout+ -- concatTopPadded,+ -- concatBottomPadded,+ -- concatOneLine,+ -- vConcatLeftAligned,+ -- vConcatRightAligned,+ -- padtop,+ -- padbottom,+ -- padleft,+ -- padright,+ -- cliptopleft,+ -- fitto,+ -- -- * wide-character-aware layout+ -- strWidth,+ -- textTakeWidth,+ -- fitString,+ -- fitStringMulti,+ -- padLeftWide,+ -- padRightWide+ -- )+where++-- import Data.Char+import Data.List+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+-- import Text.Parsec+-- import Text.Printf (printf)++-- import Hledger.Utils.Parse+-- import Hledger.Utils.Regex+import Hledger.Utils.String (charWidth)++-- lowercase, uppercase :: String -> String+-- lowercase = map toLower+-- uppercase = map toUpper++-- | Remove leading and trailing whitespace.+textstrip :: Text -> Text+textstrip = textlstrip . textrstrip++-- | Remove leading whitespace.+textlstrip :: Text -> Text+textlstrip = T.dropWhile (`elem` (" \t" :: String)) :: Text -> Text -- XXX isSpace ?++-- | Remove trailing whitespace.+textrstrip = T.reverse . textlstrip . T.reverse+textrstrip :: Text -> Text++-- -- | Remove trailing newlines/carriage returns.+-- chomp :: String -> String+-- chomp = reverse . dropWhile (`elem` "\r\n") . reverse++-- stripbrackets :: String -> String+-- stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse :: String -> String++-- elideLeft :: Int -> String -> String+-- elideLeft width s =+--     if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s++textElideRight :: Int -> Text -> Text+textElideRight width t =+    if T.length t > width then T.take (width - 2) t <> ".." else t++-- -- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it.+-- -- Works on multi-line strings too (but will rewrite non-unix line endings).+-- formatString :: Bool -> Maybe Int -> Maybe Int -> String -> String+-- formatString leftJustified minwidth maxwidth s = intercalate "\n" $ map (printf fmt) $ lines s+--     where+--       justify = if leftJustified then "-" else ""+--       minwidth' = maybe "" show minwidth+--       maxwidth' = maybe "" (("."++).show) maxwidth+--       fmt = "%" ++ justify ++ minwidth' ++ maxwidth' ++ "s"++-- underline :: String -> String+-- underline s = s' ++ replicate (length s) '-' ++ "\n"+--     where s'+--             | last s == '\n' = s+--             | otherwise = s ++ "\n"++-- | Wrap a string in double quotes, and \-prefix any embedded single+-- quotes, if it contains whitespace and is not already single- or+-- double-quoted.+quoteIfSpaced :: T.Text -> T.Text+quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s+                | not $ any (`elem` (T.unpack s)) whitespacechars = s+                | otherwise = "'"<>escapeSingleQuotes s<>"'"++-- -- | Wrap a string in double quotes, and \-prefix any embedded single+-- -- quotes, if it contains whitespace and is not already single- or+-- -- double-quoted.+-- quoteIfSpaced :: String -> String+-- quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s+--                 | not $ any (`elem` s) whitespacechars = s+--                 | otherwise = "'"++escapeSingleQuotes s++"'"++-- -- | Double-quote this string if it contains whitespace, single quotes+-- -- or double-quotes, escaping the quotes as needed.+-- quoteIfNeeded :: T.Text -> T.Text+-- quoteIfNeeded s | any (`elem` T.unpack s) (quotechars++whitespacechars) = "\"" <> escapeDoubleQuotes s <> "\""+--                 | otherwise = s++-- -- | Single-quote this string if it contains whitespace or double-quotes.+-- -- No good for strings containing single quotes.+-- singleQuoteIfNeeded :: String -> String+-- singleQuoteIfNeeded s | any (`elem` s) whitespacechars = "'"++s++"'"+--                       | otherwise = s++quotechars, whitespacechars :: [Char]+quotechars      = "'\""+whitespacechars = " \t\n\r"++escapeDoubleQuotes :: T.Text -> T.Text+escapeDoubleQuotes = T.replace "\"" "\""++escapeSingleQuotes :: T.Text -> T.Text+escapeSingleQuotes = T.replace "'" "\'"++-- escapeQuotes :: String -> String+-- escapeQuotes = regexReplace "([\"'])" "\\1"++-- -- | Quote-aware version of words - don't split on spaces which are inside quotes.+-- -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.+-- words' :: String -> [String]+-- words' "" = []+-- words' s  = map stripquotes $ fromparse $ parsewith p s+--     where+--       p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` many1 spacenonewline+--              -- eof+--              return ss+--       pattern = many (noneOf whitespacechars)+--       singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf "'")+--       doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf "\"")++-- -- | Quote-aware version of unwords - single-quote strings which contain whitespace+-- unwords' :: [Text] -> Text+-- unwords' = T.unwords . map quoteIfNeeded++-- | Strip one matching pair of single or double quotes on the ends of a string.+stripquotes :: Text -> Text+stripquotes s = if isSingleQuoted s || isDoubleQuoted s then T.init $ T.tail s else s++isSingleQuoted :: Text -> Bool+isSingleQuoted s =+  T.length (T.take 2 s) == 2 && T.head s == '\'' && T.last s == '\''++isDoubleQuoted :: Text -> Bool+isDoubleQuoted s =+  T.length (T.take 2 s) == 2 && T.head s == '"' && T.last s == '"'++textUnbracket :: Text -> Text+textUnbracket s+    | (T.head s == '[' && T.last s == ']') || (T.head s == '(' && T.last s == ')') = T.init $ T.tail s+    | otherwise = s++-- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded.+-- Treats wide characters as double width.+textConcatTopPadded :: [Text] -> Text+textConcatTopPadded ts = T.intercalate "\n" $ map T.concat $ transpose padded+    where+      lss = map T.lines ts :: [[Text]]+      h = maximum $ map length lss+      ypad ls = replicate (difforzero h (length ls)) "" ++ ls+      xpad ls = map (textPadLeftWide w) ls+        where w | null ls = 0+                | otherwise = maximum $ map textWidth ls+      padded = map (xpad . ypad) lss :: [[Text]]++-- -- | Join several multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.+-- -- Treats wide characters as double width.+-- 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 (padRightWide w) ls where w | null ls = 0+--                                                 | otherwise = maximum $ map strWidth ls+--       padded = map (xpad . ypad) lss+++-- -- | Join multi-line strings horizontally, after compressing each of+-- -- them to a single line with a comma and space between each original line.+-- concatOneLine :: [String] -> String+-- concatOneLine strs = concat $ map ((intercalate ", ").lines) strs++-- -- | Join strings vertically, left-aligned and right-padded.+-- vConcatLeftAligned :: [String] -> String+-- vConcatLeftAligned ss = intercalate "\n" $ map showfixedwidth ss+--     where+--       showfixedwidth = printf (printf "%%-%ds" width)+--       width = maximum $ map length ss++-- -- | Join strings vertically, right-aligned and left-padded.+-- 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++difforzero :: (Num a, Ord a) => a -> a -> a+difforzero a b = maximum [(a - b), 0]++-- -- | Convert a multi-line string to a rectangular string left-padded to the specified width.+-- -- Treats wide characters as double 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.+-- -- Treats wide characters as double 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 ' '++-- -- Functions below treat wide (eg CJK) characters as double-width.++-- | General-purpose wide-char-aware single-line text layout function.+-- It can left- or right-pad a short string to a minimum width.+-- It can left- or right-clip a long string to a maximum width, optionally inserting an ellipsis (the third argument).+-- It clips and pads on the right when the fourth argument is true, otherwise on the left.+-- It treats wide characters as double width.+fitText :: Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text+fitText mminwidth mmaxwidth ellipsify rightside s = (clip . pad) s+  where+    clip :: Text -> Text+    clip s =+      case mmaxwidth of+        Just w+          | textWidth s > w ->+            case rightside of+              True  -> textTakeWidth (w - T.length ellipsis) s <> ellipsis+              False -> ellipsis <> T.reverse (textTakeWidth (w - T.length ellipsis) $ T.reverse s)+          | otherwise -> s+          where+            ellipsis = if ellipsify then ".." else ""+        Nothing -> s+    pad :: Text -> Text+    pad s =+      case mminwidth of+        Just w+          | sw < w ->+            case rightside of+              True  -> s <> T.replicate (w - sw) " "+              False -> T.replicate (w - sw) " " <> s+          | otherwise -> s+        Nothing -> s+      where sw = textWidth s++-- -- | A version of fitString that works on multi-line strings,+-- -- separate for now to avoid breakage.+-- -- This will rewrite any line endings to unix newlines.+-- fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String+-- fitStringMulti mminwidth mmaxwidth ellipsify rightside s =+--   (intercalate "\n" . map (fitString mminwidth mmaxwidth ellipsify rightside) . lines) s++-- | Left-pad a text to the specified width.+-- Treats wide characters as double width.+-- Works on multi-line texts too (but will rewrite non-unix line endings).+textPadLeftWide :: Int -> Text -> Text+textPadLeftWide w "" = T.replicate w " "+textPadLeftWide w s  = T.intercalate "\n" $ map (fitText (Just w) Nothing False False) $ T.lines s+-- XXX not yet replaceable by+-- padLeftWide w = fitStringMulti (Just w) Nothing False False++-- | Right-pad a string to the specified width.+-- Treats wide characters as double width.+-- Works on multi-line strings too (but will rewrite non-unix line endings).+textPadRightWide :: Int -> Text -> Text+textPadRightWide w "" = T.replicate w " "+textPadRightWide w s  = T.intercalate "\n" $ map (fitText (Just w) Nothing False True) $ T.lines s+-- XXX not yet replaceable by+-- padRightWide w = fitStringMulti (Just w) Nothing False True++-- | Double-width-character-aware string truncation. Take as many+-- characters as possible from a string without exceeding the+-- specified width. Eg textTakeWidth 3 "りんご" = "り".+textTakeWidth :: Int -> Text -> Text+textTakeWidth _ ""     = ""+textTakeWidth 0 _      = ""+textTakeWidth w t | not (T.null t),+                let c = T.head t,+                let cw = charWidth c,+                cw <= w+                = T.cons c $ textTakeWidth (w-cw) (T.tail t)+              | otherwise = ""++-- -- from Pandoc (copyright John MacFarlane, GPL)+-- -- see also http://unicode.org/reports/tr11/#Description++-- | Calculate the designated render width of a string, taking into+-- account wide characters and line breaks (the longest line within a+-- multi-line string determines the width ).+textWidth :: Text -> Int+textWidth "" = 0+textWidth s = maximum $ map (T.foldr (\a b -> charWidth a + b) 0) $ T.lines s++-- -- | Get the designated render width of a character: 0 for a combining+-- -- character, 1 for a regular character, 2 for a wide character.+-- -- (Wide characters are rendered as exactly double width in apps and+-- -- fonts that support it.) (From Pandoc.)+-- charWidth :: Char -> Int+-- charWidth c =+--   case c of+--       _ | c <  '\x0300'                    -> 1+--         | c >= '\x0300' && c <= '\x036F'   -> 0  -- combining+--         | c >= '\x0370' && c <= '\x10FC'   -> 1+--         | c >= '\x1100' && c <= '\x115F'   -> 2+--         | c >= '\x1160' && c <= '\x11A2'   -> 1+--         | c >= '\x11A3' && c <= '\x11A7'   -> 2+--         | c >= '\x11A8' && c <= '\x11F9'   -> 1+--         | c >= '\x11FA' && c <= '\x11FF'   -> 2+--         | c >= '\x1200' && c <= '\x2328'   -> 1+--         | c >= '\x2329' && c <= '\x232A'   -> 2+--         | c >= '\x232B' && c <= '\x2E31'   -> 1+--         | c >= '\x2E80' && c <= '\x303E'   -> 2+--         | c == '\x303F'                    -> 1+--         | c >= '\x3041' && c <= '\x3247'   -> 2+--         | c >= '\x3248' && c <= '\x324F'   -> 1 -- ambiguous+--         | c >= '\x3250' && c <= '\x4DBF'   -> 2+--         | c >= '\x4DC0' && c <= '\x4DFF'   -> 1+--         | c >= '\x4E00' && c <= '\xA4C6'   -> 2+--         | c >= '\xA4D0' && c <= '\xA95F'   -> 1+--         | c >= '\xA960' && c <= '\xA97C'   -> 2+--         | c >= '\xA980' && c <= '\xABF9'   -> 1+--         | c >= '\xAC00' && c <= '\xD7FB'   -> 2+--         | c >= '\xD800' && c <= '\xDFFF'   -> 1+--         | c >= '\xE000' && c <= '\xF8FF'   -> 1 -- ambiguous+--         | c >= '\xF900' && c <= '\xFAFF'   -> 2+--         | c >= '\xFB00' && c <= '\xFDFD'   -> 1+--         | c >= '\xFE00' && c <= '\xFE0F'   -> 1 -- ambiguous+--         | c >= '\xFE10' && c <= '\xFE19'   -> 2+--         | c >= '\xFE20' && c <= '\xFE26'   -> 1+--         | c >= '\xFE30' && c <= '\xFE6B'   -> 2+--         | c >= '\xFE70' && c <= '\xFEFF'   -> 1+--         | c >= '\xFF01' && c <= '\xFF60'   -> 2+--         | c >= '\xFF61' && c <= '\x16A38'  -> 1+--         | c >= '\x1B000' && c <= '\x1B001' -> 2+--         | c >= '\x1D000' && c <= '\x1F1FF' -> 1+--         | c >= '\x1F200' && c <= '\x1F251' -> 2+--         | c >= '\x1F300' && c <= '\x1F773' -> 1+--         | c >= '\x20000' && c <= '\x3FFFD' -> 2+--         | otherwise                        -> 1+
Hledger/Utils/Tree.hs view
@@ -4,7 +4,7 @@ import Data.List (foldl') import qualified Data.Map as M import Data.Tree--- import Text.Parsec+-- import Text.Megaparsec -- import Text.Printf  import Hledger.Utils.Regex
+ README view
@@ -0,0 +1,3 @@+A reusable library containing hledger's core functionality.+This is used by most hledger* packages for common data parsing,+command line option handling, reporting etc.
+ doc/hledger_csv.5 view
@@ -0,0 +1,239 @@++.TH "hledger_csv" "5" "October 2016" "hledger 1.0" "hledger User Manuals"++++.SH NAME+.PP+CSV \- how hledger reads CSV data, and the CSV rules file format+.SH DESCRIPTION+.PP+hledger can read CSV files, converting each CSV record into a journal+entry (transaction), if you provide some conversion hints in a "rules+file".+This file should be named like the CSV file with an additional+\f[C]\&.rules\f[] suffix (eg: \f[C]mybank.csv.rules\f[]); or, you can+specify the file with \f[C]\-\-rules\-file\ PATH\f[].+hledger will create it if necessary, with some default rules which+you\[aq]ll need to adjust.+At minimum, the rules file must specify the \f[C]date\f[] and+\f[C]amount\f[] fields.+For an example, see How to read CSV files.+.PP+To learn about \f[I]exporting\f[] CSV, see CSV output.+.SH CSV RULES+.PP+The following six kinds of rule can appear in the rules file, in any+order.+Blank lines and lines beginning with \f[C]#\f[] or \f[C];\f[] are+ignored.+.SS skip+.PP+\f[C]skip\f[]\f[I]\f[C]N\f[]\f[]+.PP+Skip this number of CSV records at the beginning.+You\[aq]ll need this whenever your CSV data contains header lines.+Eg:+.IP+.nf+\f[C]+#\ ignore\ the\ first\ CSV\ line+skip\ 1+\f[]+.fi+.SS date\-format+.PP+\f[C]date\-format\f[]\f[I]\f[C]DATEFMT\f[]\f[]+.PP+When your CSV date fields are not formatted like \f[C]YYYY/MM/DD\f[] (or+\f[C]YYYY\-MM\-DD\f[] or \f[C]YYYY.MM.DD\f[]), you\[aq]ll need to+specify the format.+DATEFMT is a strptime\-like date parsing pattern, which must parse the+date field values completely.+Examples:+.IP+.nf+\f[C]+#\ for\ dates\ like\ "6/11/2013":+date\-format\ %\-d/%\-m/%Y+\f[]+.fi+.IP+.nf+\f[C]+#\ for\ dates\ like\ "11/06/2013":+date\-format\ %m/%d/%Y+\f[]+.fi+.IP+.nf+\f[C]+#\ for\ dates\ like\ "2013\-Nov\-06":+date\-format\ %Y\-%h\-%d+\f[]+.fi+.IP+.nf+\f[C]+#\ for\ dates\ like\ "11/6/2013\ 11:32\ PM":+date\-format\ %\-m/%\-d/%Y\ %l:%M\ %p+\f[]+.fi+.SS field list+.PP+\f[C]fields\f[]\f[I]\f[C]FIELDNAME1\f[]\f[],+\f[I]\f[C]FIELDNAME2\f[]\f[]...+.PP+This (a) names the CSV fields, in order (names may not contain+whitespace; uninteresting names may be left blank), and (b) assigns them+to journal entry fields if you use any of these standard field names:+\f[C]date\f[], \f[C]date2\f[], \f[C]status\f[], \f[C]code\f[],+\f[C]description\f[], \f[C]comment\f[], \f[C]account1\f[],+\f[C]account2\f[], \f[C]amount\f[], \f[C]amount\-in\f[],+\f[C]amount\-out\f[], \f[C]currency\f[].+Eg:+.IP+.nf+\f[C]+#\ use\ the\ 1st,\ 2nd\ and\ 4th\ CSV\ fields\ as\ the\ entry\[aq]s\ date,\ description\ and\ amount,+#\ and\ give\ the\ 7th\ and\ 8th\ fields\ meaningful\ names\ for\ later\ reference:+#+#\ CSV\ field:+#\ \ \ \ \ \ 1\ \ \ \ \ 2\ \ \ \ \ \ \ \ \ \ \ \ 3\ 4\ \ \ \ \ \ \ 5\ 6\ 7\ \ \ \ \ \ \ \ \ \ 8+#\ entry\ field:+fields\ date,\ description,\ ,\ amount,\ ,\ ,\ somefield,\ anotherfield+\f[]+.fi+.SS field assignment+.PP+\f[I]\f[C]ENTRYFIELDNAME\f[]\f[] \f[I]\f[C]FIELDVALUE\f[]\f[]+.PP+This sets a journal entry field (one of the standard names above) to the+given text value, which can include CSV field values interpolated by+name (\f[C]%CSVFIELDNAME\f[]) or 1\-based position (\f[C]%N\f[]).+ Eg:+.IP+.nf+\f[C]+#\ set\ the\ amount\ to\ the\ 4th\ CSV\ field\ with\ "USD\ "\ prepended+amount\ USD\ %4+\f[]+.fi+.IP+.nf+\f[C]+#\ combine\ three\ fields\ to\ make\ a\ comment\ (containing\ two\ tags)+comment\ note:\ %somefield\ \-\ %anotherfield,\ date:\ %1+\f[]+.fi+.PP+Field assignments can be used instead of or in addition to a field list.+.SS conditional block+.PP+\f[C]if\f[] \f[I]\f[C]PATTERN\f[]\f[]+.PD 0+.P+.PD+\ \ \ \ \f[I]\f[C]FIELDASSIGNMENTS\f[]\f[]...+.PP+\f[C]if\f[]+.PD 0+.P+.PD+\f[I]\f[C]PATTERN\f[]\f[]+.PD 0+.P+.PD+\f[I]\f[C]PATTERN\f[]\f[]...+.PD 0+.P+.PD+\ \ \ \ \f[I]\f[C]FIELDASSIGNMENTS\f[]\f[]...+.PP+This applies one or more field assignments, only to those CSV records+matched by one of the PATTERNs.+The patterns are case\-insensitive regular expressions which match+anywhere within the whole CSV record (it\[aq]s not yet possible to match+within a specific field).+When there are multiple patterns they can be written on separate lines,+unindented.+The field assignments are on separate lines indented by at least one+space.+Examples:+.IP+.nf+\f[C]+#\ if\ the\ CSV\ record\ contains\ "groceries",\ set\ account2\ to\ "expenses:groceries"+if\ groceries+\ account2\ expenses:groceries+\f[]+.fi+.IP+.nf+\f[C]+#\ if\ the\ CSV\ record\ contains\ any\ of\ these\ patterns,\ set\ account2\ and\ comment\ as\ shown+if+monthly\ service\ fee+atm\ transaction\ fee+banking\ thru\ software+\ account2\ expenses:business:banking+\ comment\ \ XXX\ deductible\ ?\ check\ it+\f[]+.fi+.SS include+.PP+\f[C]include\f[]\f[I]\f[C]RULESFILE\f[]\f[]+.PP+Include another rules file at this point.+\f[C]RULESFILE\f[] is either an absolute file path or a path relative to+the current file\[aq]s directory.+Eg:+.IP+.nf+\f[C]+#\ rules\ reused\ with\ several\ CSV\ files+include\ common.rules+\f[]+.fi+.SH TIPS+.PP+Each generated journal entry will have two postings, to+\f[C]account1\f[] and \f[C]account2\f[] respectively.+Currently it\[aq]s not possible to generate entries with more than two+postings.+.PP+If the CSV has debit/credit amounts in separate fields, assign to the+\f[C]amount\-in\f[] and \f[C]amount\-out\f[] pseudo fields instead of+\f[C]amount\f[].+.PP+If the CSV has the currency in a separate field, assign that to the+\f[C]currency\f[] pseudo field which will be automatically prepended to+the amount.+(Or you can do the same thing with a field assignment.)+.PP+If an amount value is parenthesised, it will be de\-parenthesised and+sign\-flipped automatically.+.PP+The generated journal entries will be sorted by date.+The original order of same\-day entries will be preserved, usually.+++.SH "REPORTING BUGS"+Report bugs at http://bugs.hledger.org+(or on the #hledger IRC channel or hledger mail list)++.SH AUTHORS+Simon Michael <simon@joyful.com> and contributors++.SH COPYRIGHT++Copyright (C) 2007-2016 Simon Michael.+.br+Released under GNU GPL v3 or later.++.SH SEE ALSO+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),+ledger(1)++http://hledger.org
+ doc/hledger_csv.5.info view
@@ -0,0 +1,231 @@+This is hledger-lib/doc/hledger_csv.5.info, produced by makeinfo+version 4.8 from stdin.+++File: hledger_csv.5.info,  Node: Top,  Up: (dir)++hledger_csv(5) hledger 1.0+**************************++hledger can read CSV files, converting each CSV record into a journal+entry (transaction), if you provide some conversion hints in a "rules+file". This file should be named like the CSV file with an additional+`.rules' suffix (eg: `mybank.csv.rules'); or, you can specify the file+with `--rules-file PATH'. hledger will create it if necessary, with+some default rules which you'll need to adjust. At minimum, the rules+file must specify the `date' and `amount' fields. For an example, see+How to read CSV files.++   To learn about _exporting_ CSV, see CSV output.++* Menu:++* CSV RULES::+* TIPS::+++File: hledger_csv.5.info,  Node: CSV RULES,  Next: TIPS,  Prev: Top,  Up: Top++1 CSV RULES+***********++The following six kinds of rule can appear in the rules file, in any+order. Blank lines and lines beginning with `#' or `;' are ignored.++* Menu:++* skip::+* date-format::+* field list::+* field assignment::+* conditional block::+* include::+++File: hledger_csv.5.info,  Node: skip,  Next: date-format,  Up: CSV RULES++1.1 skip+========++`skip'_`N'_++   Skip this number of CSV records at the beginning. You'll need this+whenever your CSV data contains header lines. Eg:+++# ignore the first CSV line+skip 1+++File: hledger_csv.5.info,  Node: date-format,  Next: field list,  Prev: skip,  Up: CSV RULES++1.2 date-format+===============++`date-format'_`DATEFMT'_++   When your CSV date fields are not formatted like `YYYY/MM/DD' (or+`YYYY-MM-DD' or `YYYY.MM.DD'), you'll need to specify the format.+DATEFMT is a strptime-like date parsing pattern, which must parse the+date field values completely. Examples:+++# for dates like "6/11/2013":+date-format %-d/%-m/%Y+++# for dates like "11/06/2013":+date-format %m/%d/%Y+++# for dates like "2013-Nov-06":+date-format %Y-%h-%d+++# for dates like "11/6/2013 11:32 PM":+date-format %-m/%-d/%Y %l:%M %p+++File: hledger_csv.5.info,  Node: field list,  Next: field assignment,  Prev: date-format,  Up: CSV RULES++1.3 field list+==============++`fields'_`FIELDNAME1'_, _`FIELDNAME2'_...++   This (a) names the CSV fields, in order (names may not contain+whitespace; uninteresting names may be left blank), and (b) assigns them+to journal entry fields if you use any of these standard field names:+`date', `date2', `status', `code', `description', `comment',+`account1', `account2', `amount', `amount-in', `amount-out',+`currency'. Eg:+++# use the 1st, 2nd and 4th CSV fields as the entry's date, description and amount,+# and give the 7th and 8th fields meaningful names for later reference:+#+# CSV field:+#      1     2            3 4       5 6 7          8+# entry field:+fields date, description, , amount, , , somefield, anotherfield+++File: hledger_csv.5.info,  Node: field assignment,  Next: conditional block,  Prev: field list,  Up: CSV RULES++1.4 field assignment+====================++_`ENTRYFIELDNAME'_ _`FIELDVALUE'_++   This sets a journal entry field (one of the standard names above) to+the given text value, which can include CSV field values interpolated by+name (`%CSVFIELDNAME') or 1-based position (`%N'). Eg:+++# set the amount to the 4th CSV field with "USD " prepended+amount USD %4+++# combine three fields to make a comment (containing two tags)+comment note: %somefield - %anotherfield, date: %1++   Field assignments can be used instead of or in addition to a field+list.+++File: hledger_csv.5.info,  Node: conditional block,  Next: include,  Prev: field assignment,  Up: CSV RULES++1.5 conditional block+=====================++`if' _`PATTERN'_+_`FIELDASSIGNMENTS'_...++   `if'+_`PATTERN'_+_`PATTERN'_...+_`FIELDASSIGNMENTS'_...++   This applies one or more field assignments, only to those CSV records+matched by one of the PATTERNs. The patterns are case-insensitive+regular expressions which match anywhere within the whole CSV record+(it's not yet possible to match within a specific field). When there are+multiple patterns they can be written on separate lines, unindented. The+field assignments are on separate lines indented by at least one space.+Examples:+++# if the CSV record contains "groceries", set account2 to "expenses:groceries"+if groceries+ account2 expenses:groceries+++# if the CSV record contains any of these patterns, set account2 and comment as shown+if+monthly service fee+atm transaction fee+banking thru software+ account2 expenses:business:banking+ comment  XXX deductible ? check it+++File: hledger_csv.5.info,  Node: include,  Prev: conditional block,  Up: CSV RULES++1.6 include+===========++`include'_`RULESFILE'_++   Include another rules file at this point. `RULESFILE' is either an+absolute file path or a path relative to the current file's directory.+Eg:+++# rules reused with several CSV files+include common.rules+++File: hledger_csv.5.info,  Node: TIPS,  Prev: CSV RULES,  Up: Top++2 TIPS+******++Each generated journal entry will have two postings, to `account1' and+`account2' respectively. Currently it's not possible to generate+entries with more than two postings.++   If the CSV has debit/credit amounts in separate fields, assign to the+`amount-in' and `amount-out' pseudo fields instead of `amount'.++   If the CSV has the currency in a separate field, assign that to the+`currency' pseudo field which will be automatically prepended to the+amount. (Or you can do the same thing with a field assignment.)++   If an amount value is parenthesised, it will be de-parenthesised and+sign-flipped automatically.++   The generated journal entries will be sorted by date. The original+order of same-day entries will be preserved, usually.++++Tag Table:+Node: Top90+Node: CSV RULES795+Ref: #csv-rules901+Node: skip1144+Ref: #skip1240+Node: date-format1411+Ref: #date-format1540+Node: field list2049+Ref: #field-list2188+Node: field assignment2883+Ref: #field-assignment3040+Node: conditional block3545+Ref: #conditional-block3701+Node: include4588+Ref: #include4699+Node: TIPS4930+Ref: #tips5014++End Tag Table
+ doc/hledger_csv.5.txt view
@@ -0,0 +1,169 @@++hledger_csv(5)               hledger User Manuals               hledger_csv(5)++++NAME+       CSV - how hledger reads CSV data, and the CSV rules file format++DESCRIPTION+       hledger  can  read CSV files, converting each CSV record into a journal+       entry (transaction), if you provide some conversion hints in  a  "rules+       file".   This file should be named like the CSV file with an additional+       .rules suffix (eg: mybank.csv.rules); or, you can specify the file with+       --rules-file PATH.   hledger  will  create  it  if necessary, with some+       default rules which you'll need to adjust.  At minimum, the rules  file+       must  specify  the  date and amount fields.  For an example, see How to+       read CSV files.++       To learn about exporting CSV, see CSV output.++CSV RULES+       The following six kinds of rule can appear in the rules  file,  in  any+       order.  Blank lines and lines beginning with # or ; are ignored.++   skip+       skipN++       Skip  this  number  of  CSV records at the beginning.  You'll need this+       whenever your CSV data contains header lines.  Eg:++              # ignore the first CSV line+              skip 1++   date-format+       date-formatDATEFMT++       When your CSV  date  fields  are  not  formatted  like  YYYY/MM/DD  (or+       YYYY-MM-DD  or YYYY.MM.DD), you'll need to specify the format.  DATEFMT+       is a strptime-like date parsing pattern,  which  must  parse  the  date+       field values completely.  Examples:++              # for dates like "6/11/2013":+              date-format %-d/%-m/%Y++              # for dates like "11/06/2013":+              date-format %m/%d/%Y++              # for dates like "2013-Nov-06":+              date-format %Y-%h-%d++              # for dates like "11/6/2013 11:32 PM":+              date-format %-m/%-d/%Y %l:%M %p++   field list+       fieldsFIELDNAME1, FIELDNAME2...++       This  (a)  names the CSV fields, in order (names may not contain white-+       space; uninteresting names may be left blank), and (b) assigns them  to+       journal  entry  fields  if  you  use any of these standard field names:+       date, date2, status, code, description,  comment,  account1,  account2,+       amount, amount-in, amount-out, currency.  Eg:++              # use the 1st, 2nd and 4th CSV fields as the entry's date, description and amount,+              # and give the 7th and 8th fields meaningful names for later reference:+              #+              # CSV field:+              #      1     2            3 4       5 6 7          8+              # entry field:+              fields date, description, , amount, , , somefield, anotherfield++   field assignment+       ENTRYFIELDNAME FIELDVALUE++       This  sets  a  journal entry field (one of the standard names above) to+       the given text value, which can include CSV field  values  interpolated+       by name (%CSVFIELDNAME) or 1-based position (%N).+        Eg:++              # set the amount to the 4th CSV field with "USD " prepended+              amount USD %4++              # combine three fields to make a comment (containing two tags)+              comment note: %somefield - %anotherfield, date: %1++       Field  assignments  can  be  used  instead of or in addition to a field+       list.++   conditional block+       if PATTERN+           FIELDASSIGNMENTS...++       if+       PATTERN+       PATTERN...+           FIELDASSIGNMENTS...++       This applies one or more field assignments, only to those  CSV  records+       matched by one of the PATTERNs.  The patterns are case-insensitive reg-+       ular expressions which match anywhere within the whole CSV record (it's+       not  yet  possible  to  match within a specific field).  When there are+       multiple patterns they can be written on  separate  lines,  unindented.+       The  field  assignments  are on separate lines indented by at least one+       space.  Examples:++              # if the CSV record contains "groceries", set account2 to "expenses:groceries"+              if groceries+               account2 expenses:groceries++              # if the CSV record contains any of these patterns, set account2 and comment as shown+              if+              monthly service fee+              atm transaction fee+              banking thru software+               account2 expenses:business:banking+               comment  XXX deductible ? check it++   include+       includeRULESFILE++       Include another rules file at this point.  RULESFILE is either an abso-+       lute file path or a path relative to the current file's directory.  Eg:++              # rules reused with several CSV files+              include common.rules++TIPS+       Each generated journal entry will have two postings,  to  account1  and+       account2 respectively.  Currently it's not possible to generate entries+       with more than two postings.++       If the CSV has debit/credit amounts in separate fields, assign  to  the+       amount-in and amount-out pseudo fields instead of amount.++       If  the  CSV  has  the currency in a separate field, assign that to the+       currency pseudo field which will  be  automatically  prepended  to  the+       amount.  (Or you can do the same thing with a field assignment.)++       If  an  amount  value is parenthesised, it will be de-parenthesised and+       sign-flipped automatically.++       The generated journal entries will be sorted  by  date.   The  original+       order of same-day entries will be preserved, usually.++++REPORTING BUGS+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel+       or hledger mail list)+++AUTHORS+       Simon Michael <simon@joyful.com> and contributors+++COPYRIGHT+       Copyright (C) 2007-2016 Simon Michael.+       Released under GNU GPL v3 or later.+++SEE ALSO+       hledger(1),     hledger-ui(1),     hledger-web(1),      hledger-api(1),+       hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_time-+       dot(5), ledger(1)++       http://hledger.org++++hledger 1.0                      October 2016                   hledger_csv(5)
+ doc/hledger_journal.5 view
@@ -0,0 +1,926 @@+.\"t++.TH "hledger_journal" "5" "October 2016" "hledger 1.0" "hledger User Manuals"++++.SH NAME+.PP+Journal \- hledger\[aq]s default file format, representing a General+Journal+.SH DESCRIPTION+.PP+hledger\[aq]s usual data source is a plain text file containing journal+entries in hledger journal format.+This file represents a standard accounting general journal.+I use file names ending in \f[C]\&.journal\f[], but that\[aq]s not+required.+The journal file contains a number of transaction entries, each+describing a transfer of money (or any commodity) between two or more+named accounts, in a simple format readable by both hledger and humans.+.PP+hledger\[aq]s journal format is a compatible subset, mostly, of+ledger\[aq]s journal format, so hledger can work with compatible ledger+journal files as well.+It\[aq]s safe, and encouraged, to run both hledger and ledger on the+same journal file, eg to validate the results you\[aq]re getting.+.PP+You can use hledger without learning any more about this file; just use+the add or web commands to create and update it.+Many users, though, also edit the journal file directly with a text+editor, perhaps assisted by the helper modes for emacs or vim.+.PP+Here\[aq]s an example:+.IP+.nf+\f[C]+;\ A\ sample\ journal\ file.\ This\ is\ a\ comment.++2008/01/01\ income\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ;\ <\-\ transaction\[aq]s\ first\ line\ starts\ in\ column\ 0,\ contains\ date\ and\ description+\ \ \ \ assets:bank:checking\ \ $1\ \ \ \ ;\ <\-\ posting\ lines\ start\ with\ whitespace,\ each\ contains\ an\ account\ name+\ \ \ \ income:salary\ \ \ \ \ \ \ \ $\-1\ \ \ \ ;\ \ \ \ followed\ by\ at\ least\ two\ spaces\ and\ an\ amount++2008/06/01\ gift+\ \ \ \ assets:bank:checking\ \ $1\ \ \ \ ;\ <\-\ at\ least\ two\ postings\ in\ a\ transaction+\ \ \ \ income:gifts\ \ \ \ \ \ \ \ \ $\-1\ \ \ \ ;\ <\-\ their\ amounts\ must\ balance\ to\ 0++2008/06/02\ save+\ \ \ \ assets:bank:saving\ \ \ \ $1+\ \ \ \ assets:bank:checking\ \ \ \ \ \ \ \ ;\ <\-\ one\ amount\ may\ be\ omitted;\ here\ $\-1\ is\ inferred++2008/06/03\ eat\ &\ shop\ \ \ \ \ \ \ \ \ \ \ ;\ <\-\ description\ can\ be\ anything+\ \ \ \ expenses:food\ \ \ \ \ \ \ \ \ $1+\ \ \ \ expenses:supplies\ \ \ \ \ $1\ \ \ \ ;\ <\-\ this\ transaction\ debits\ two\ expense\ accounts+\ \ \ \ assets:cash\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ;\ <\-\ $\-2\ inferred++2008/12/31\ *\ pay\ off\ \ \ \ \ \ \ \ \ \ \ \ ;\ <\-\ an\ optional\ *\ or\ !\ after\ the\ date\ means\ "cleared"\ (or\ anything\ you\ want)+\ \ \ \ liabilities:debts\ \ \ \ \ $1+\ \ \ \ assets:bank:checking+\f[]+.fi+.SH FILE FORMAT+.SS Transactions+.PP+Transactions are represented by journal entries.+Each begins with a simple date in column 0, followed by three optional+fields with spaces between them:+.IP \[bu] 2+a status flag, which can be empty or \f[C]!\f[] or \f[C]*\f[] (meaning+"uncleared", "pending" and "cleared", or whatever you want)+.IP \[bu] 2+a transaction code (eg a check number),+.IP \[bu] 2+and/or a description+.PP+then some number of postings, of some amount to some account.+Each posting is on its own line, consisting of:+.IP \[bu] 2+indentation of one or more spaces (or tabs)+.IP \[bu] 2+optionally, a \f[C]!\f[] or \f[C]*\f[] status flag followed by a space+.IP \[bu] 2+an account name, optionally containing single spaces+.IP \[bu] 2+optionally, two or more spaces or tabs followed by an amount+.PP+Usually there are two or more postings, though one or none is also+possible.+The posting amounts within a transaction must always balance, ie add up+to 0.+Optionally one amount can be left blank, in which case it will be+inferred.+.SS Dates+.SS Simple dates+.PP+Within a journal file, transaction dates use Y/M/D (or Y\-M\-D or Y.M.D)+Leading zeros are optional.+The year may be omitted, in which case it will be inferred from the+context \- the current transaction, the default year set with a default+year directive, or the current date when the command is run.+Some examples: \f[C]2010/01/31\f[], \f[C]1/31\f[],+\f[C]2010\-01\-31\f[], \f[C]2010.1.31\f[].+.SS Secondary dates+.PP+Real\-life transactions sometimes involve more than one date \- eg the+date you write a cheque, and the date it clears in your bank.+When you want to model this, eg for more accurate balances, you can+specify individual posting dates, which I recommend.+Or, you can use the secondary dates (aka auxiliary/effective dates)+feature, supported for compatibility with Ledger.+.PP+A secondary date can be written after the primary date, separated by an+equals sign.+The primary date, on the left, is used by default; the secondary date,+on the right, is used when the \f[C]\-\-date2\f[] flag is specified+(\f[C]\-\-aux\-date\f[] or \f[C]\-\-effective\f[] also work).+.PP+The meaning of secondary dates is up to you, but it\[aq]s best to follow+a consistent rule.+Eg write the bank\[aq]s clearing date as primary, and when needed, the+date the transaction was initiated as secondary.+.PP+Here\[aq]s an example.+Note that a secondary date will use the year of the primary date if+unspecified.+.IP+.nf+\f[C]+2010/2/23=2/19\ movie\ ticket+\ \ expenses:cinema\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $10+\ \ assets:checking+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ register\ checking+2010/02/23\ movie\ ticket\ \ \ \ \ \ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10\ \ \ \ \ \ \ \ \ $\-10+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ register\ checking\ \-\-date2+2010/02/19\ movie\ ticket\ \ \ \ \ \ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10\ \ \ \ \ \ \ \ \ $\-10+\f[]+.fi+.PP+Secondary dates require some effort; you must use them consistently in+your journal entries and remember whether to use or not use the+\f[C]\-\-date2\f[] flag for your reports.+They are included in hledger for Ledger compatibility, but posting dates+are a more powerful and less confusing alternative.+.SS Posting dates+.PP+You can give individual postings a different date from their parent+transaction, by adding a posting comment containing a tag (see below)+like \f[C]date:DATE\f[].+This is probably the best way to control posting dates precisely.+Eg in this example the expense should appear in May reports, and the+deduction from checking should be reported on 6/1 for easy bank+reconciliation:+.IP+.nf+\f[C]+2015/5/30+\ \ \ \ expenses:food\ \ \ \ \ $10\ \ \ ;\ food\ purchased\ on\ saturday\ 5/30+\ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ ;\ bank\ cleared\ it\ on\ monday,\ date:6/1+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ \-f\ t.j\ register\ food+2015/05/30\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ expenses:food\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $10\ \ \ \ \ \ \ \ \ \ \ $10+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ \-f\ t.j\ register\ checking+2015/06/01\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10\ \ \ \ \ \ \ \ \ \ $\-10+\f[]+.fi+.PP+DATE should be a simple date; if the year is not specified it will use+the year of the transaction\[aq]s date.+You can set the secondary date similarly, with \f[C]date2:DATE2\f[].+The \f[C]date:\f[] or \f[C]date2:\f[] tags must have a valid simple date+value if they are present, eg a \f[C]date:\f[] tag with no value is not+allowed.+.PP+Ledger\[aq]s earlier, more compact bracketed date syntax is also+supported: \f[C][DATE]\f[], \f[C][DATE=DATE2]\f[] or \f[C][=DATE2]\f[].+hledger will attempt to parse any square\-bracketed sequence of the+\f[C]0123456789/\-.=\f[] characters in this way.+With this syntax, DATE infers its year from the transaction and DATE2+infers its year from DATE.+.SS Account names+.PP+Account names typically have several parts separated by a full colon,+from which hledger derives a hierarchical chart of accounts.+They can be anything you like, but in finance there are traditionally+five top\-level accounts: \f[C]assets\f[], \f[C]liabilities\f[],+\f[C]income\f[], \f[C]expenses\f[], and \f[C]equity\f[].+.PP+Account names may contain single spaces, eg:+\f[C]assets:accounts\ receivable\f[].+Because of this, they must always be followed by \f[B]two or more+spaces\f[] (or newline).+.PP+Account names can be aliased.+.SS Amounts+.PP+After the account name, there is usually an amount.+Important: between account name and amount, there must be \f[B]two or+more spaces\f[].+.PP+Amounts consist of a number and (usually) a currency symbol or commodity+name.+Some examples:+.PP+\f[C]2.00001\f[]+.PD 0+.P+.PD+\f[C]$1\f[]+.PD 0+.P+.PD+\f[C]4000\ AAPL\f[]+.PD 0+.P+.PD+\f[C]3\ "green\ apples"\f[]+.PD 0+.P+.PD+\f[C]\-$1,000,000.00\f[]+.PD 0+.P+.PD+\f[C]INR\ 9,99,99,999.00\f[]+.PD 0+.P+.PD+\f[C]EUR\ \-2.000.000,00\f[]+.PP+As you can see, the amount format is somewhat flexible:+.IP \[bu] 2+amounts are a number (the "quantity") and optionally a currency+symbol/commodity name (the "commodity").+.IP \[bu] 2+the commodity is a symbol, word, or double\-quoted phrase, on the left+or right, with or without a separating space+.IP \[bu] 2+negative amounts with a commodity on the left can have the minus sign+before or after it+.IP \[bu] 2+digit groups (thousands, or any other grouping) can be separated by+commas (in which case period is used for decimal point) or periods (in+which case comma is used for decimal point)+.PP+You can use any of these variations when recording data, but when+hledger displays amounts, it will choose a consistent format for each+commodity.+(Except for price amounts, which are always formatted as written).+The display format is chosen as follows:+.IP \[bu] 2+if there is a commodity directive specifying the format, that is used+.IP \[bu] 2+otherwise the format is inferred from the first posting amount in that+commodity in the journal, and the precision (number of decimal places)+will be the maximum from all posting amounts in that commmodity+.IP \[bu] 2+or if there are no such amounts in the journal, a default format is used+(like \f[C]$1000.00\f[]).+.PP+Price amounts and amounts in D directives usually don\[aq]t affect+amount format inference, but in some situations they can do so+indirectly.+(Eg when D\[aq]s default commodity is applied to a commodity\-less+amount, or when an amountless posting is balanced using a price\[aq]s+commodity, or when \-V is used.) If you find this causing problems, set+the desired format with a commodity directive.+.SS Virtual Postings+.PP+When you parenthesise the account name in a posting, we call that a+\f[I]virtual posting\f[], which means:+.IP \[bu] 2+it is ignored when checking that the transaction is balanced+.IP \[bu] 2+it is excluded from reports when the \f[C]\-\-real/\-R\f[] flag is used,+or the \f[C]real:1\f[] query.+.PP+You could use this, eg, to set an account\[aq]s opening balance without+needing to use the \f[C]equity:opening\ balances\f[] account:+.IP+.nf+\f[C]+1/1\ special\ unbalanced\ posting\ to\ set\ initial\ balance+\ \ (assets:checking)\ \ \ $1000+\f[]+.fi+.PP+When the account name is bracketed, we call it a \f[I]balanced virtual+posting\f[].+This is like an ordinary virtual posting except the balanced virtual+postings in a transaction must balance to 0, like the real postings (but+separately from them).+Balanced virtual postings are also excluded by \f[C]\-\-real/\-R\f[] or+\f[C]real:1\f[].+.IP+.nf+\f[C]+1/1\ buy\ food\ with\ cash,\ and\ update\ some\ budget\-tracking\ subaccounts\ elsewhere+\ \ expenses:food\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $10+\ \ assets:cash\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10+\ \ [assets:checking:available]\ \ \ \ \ $10+\ \ [assets:checking:budget:food]\ \ $\-10+\f[]+.fi+.PP+Virtual postings have some legitimate uses, but those are few.+You can usually find an equivalent journal entry using real postings,+which is more correct and provides better error checking.+.SS Balance Assertions+.PP+hledger supports ledger\-style balance assertions in journal files.+These look like \f[C]=EXPECTEDBALANCE\f[] following a posting\[aq]s+amount.+Eg in this example we assert the expected dollar balance in accounts a+and b after each posting:+.IP+.nf+\f[C]+2013/1/1+\ \ a\ \ \ $1\ \ =$1+\ \ b\ \ \ \ \ \ \ =$\-1++2013/1/2+\ \ a\ \ \ $1\ \ =$2+\ \ b\ \ $\-1\ \ =$\-2+\f[]+.fi+.PP+After reading a journal file, hledger will check all balance assertions+and report an error if any of them fail.+Balance assertions can protect you from, eg, inadvertently disrupting+reconciled balances while cleaning up old entries.+You can disable them temporarily with the+\f[C]\-\-ignore\-assertions\f[] flag, which can be useful for+troubleshooting or for reading Ledger files.+.SS Assertions and ordering+.PP+hledger sorts an account\[aq]s postings and assertions first by date and+then (for postings on the same day) by parse order.+Note this is different from Ledger, which sorts assertions only by parse+order.+(Also, Ledger assertions do not see the accumulated effect of repeated+postings to the same account within a transaction.)+.PP+So, hledger balance assertions keep working if you reorder+differently\-dated transactions within the journal.+But if you reorder same\-dated transactions or postings, assertions+might break and require updating.+This order dependence does bring an advantage: precise control over the+order of postings and assertions within a day, so you can assert+intra\-day balances.+.PP+With included files, things are a little more complicated.+Including preserves the ordering of postings and assertions.+If you have multiple postings to an account on the same day, split+across different files, and you also want to assert the account\[aq]s+balance on the same day, you\[aq]ll have to put the assertion in the+right file.+.SS Assertions and commodities+.PP+The asserted balance must be a simple single\-commodity amount, and in+fact the assertion checks only this commodity\[aq]s balance within the+(possibly multi\-commodity) account balance.+We could call this a partial balance assertion.+This is compatible with Ledger, and makes it possible to make assertions+about accounts containing multiple commodities.+.PP+To assert each commodity\[aq]s balance in such a multi\-commodity+account, you can add multiple postings (with amount 0 if necessary).+But note that no matter how many assertions you add, you can\[aq]t be+sure the account does not contain some unexpected commodity.+(We\[aq]ll add support for this kind of total balance assertion if+there\[aq]s demand.)+.SS Assertions and subaccounts+.PP+Balance assertions do not count the balance from subaccounts; they check+the posted account\[aq]s exclusive balance.+For example:+.IP+.nf+\f[C]+1/1+\ \ checking:fund\ \ \ 1\ =\ 1\ \ ;\ post\ to\ this\ subaccount,\ its\ balance\ is\ now\ 1+\ \ checking\ \ \ \ \ \ \ \ 1\ =\ 1\ \ ;\ post\ to\ the\ parent\ account,\ its\ exclusive\ balance\ is\ now\ 1+\ \ equity+\f[]+.fi+.PP+The balance report\[aq]s flat mode shows these exclusive balances more+clearly:+.IP+.nf+\f[C]+$\ hledger\ bal\ checking\ \-\-flat+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 1\ \ checking+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 1\ \ checking:fund+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 2+\f[]+.fi+.SS Assertions and virtual postings+.PP+Balance assertions are checked against all postings, both real and+virtual.+They are not affected by the \f[C]\-\-real/\-R\f[] flag or+\f[C]real:\f[] query.+.SS Prices+.SS Transaction prices+.PP+When recording a transaction, you can also record an amount\[aq]s price+in another commodity.+This documents the exchange rate, cost (of a purchase), or selling price+(of a sale) that was in effect within this particular transaction (or+more precisely, within the particular posting).+These transaction prices are fixed, and do not change.+.PP+Such priced amounts can be displayed in their transaction price\[aq]s+commodity, by using the \f[C]\-\-cost/\-B\f[] flag (B for "cost Basis"),+supported by most hledger commands.+.PP+There are three ways to specify a transaction price:+.IP "1." 3+Write the unit price (aka exchange rate), as \f[C]\@\ UNITPRICE\f[]+after the amount:+.RS 4+.IP+.nf+\f[C]+2009/1/1+\ \ assets:foreign\ currency\ \ \ €100\ \@\ $1.35\ \ ;\ one\ hundred\ euros\ at\ $1.35\ each+\ \ assets:cash+\f[]+.fi+.RE+.IP "2." 3+Or write the total price, as \f[C]\@\@\ TOTALPRICE\f[] after the amount:+.RS 4+.IP+.nf+\f[C]+2009/1/1+\ \ assets:foreign\ currency\ \ \ €100\ \@\@\ $135\ \ ;\ one\ hundred\ euros\ at\ $135\ for\ the\ lot+\ \ assets:cash+\f[]+.fi+.RE+.IP "3." 3+Or let hledger infer the price so as to balance the transaction.+To permit this, you must fully specify all posting amounts, and their+sum must have a non\-zero amount in exactly two commodities:+.RS 4+.IP+.nf+\f[C]+2009/1/1+\ \ assets:foreign\ currency\ \ \ €100\ \ \ \ \ \ \ \ \ \ ;\ one\ hundred\ euros+\ \ assets:cash\ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-135\ \ \ \ \ \ \ \ \ \ ;\ exchanged\ for\ $135+\f[]+.fi+.RE+.PP+With any of the above examples we get:+.IP+.nf+\f[C]+$\ hledger\ print\ \-B+2009/01/01+\ \ \ \ assets:foreign\ currency\ \ \ \ \ \ \ $135.00+\ \ \ \ assets:cash\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-135.00+\f[]+.fi+.PP+Example use for transaction prices: recording the effective conversion+rate of purchases made in a foreign currency.+.SS Market prices+.PP+Market prices are not tied to a particular transaction; they represent+historical exchange rates between two commodities, usually from some+public market which publishes such rates.+.PP+When market prices are known, the \f[C]\-V/\-\-value\f[] option will use+them to convert reported amounts to their market value as of the report+end date.+This option is currently available only with the balance command.+.PP+You record market prices (Ledger calls them historical prices) with a P+directive, in the journal or perhaps in a separate included file.+Market price directives have the format:+.IP+.nf+\f[C]+P\ DATE\ COMMODITYSYMBOL\ UNITPRICE+\f[]+.fi+.PP+For example, the following directives say that the euro\[aq]s exchange+rate was 1.35 US dollars during 2009, and $1.40 from 2010 onward (and+unknown before 2009).+.IP+.nf+\f[C]+P\ 2009/1/1\ €\ $1.35+P\ 2010/1/1\ €\ $1.40+\f[]+.fi+.PP+Example use for market prices: tracking the value of stocks.+.SS Comments+.PP+Lines in the journal beginning with a semicolon (\f[C];\f[]) or hash+(\f[C]#\f[]) or asterisk (\f[C]*\f[]) are comments, and will be ignored.+(Asterisk comments make it easy to treat your journal like an org\-mode+outline in emacs.)+.PP+Also, anything between \f[C]comment\f[] and \f[C]end\ comment\f[]+directives is a (multi\-line) comment.+If there is no \f[C]end\ comment\f[], the comment extends to the end of+the file.+.PP+You can attach comments to a transaction by writing them after the+description and/or indented on the following lines (before the+postings).+Similarly, you can attach comments to an individual posting by writing+them after the amount and/or indented on the following lines.+.PP+Some examples:+.IP+.nf+\f[C]+#\ a\ journal\ comment++;\ also\ a\ journal\ comment++comment+This\ is\ a\ multiline\ comment,+which\ continues\ until\ a\ line+where\ the\ "end\ comment"\ string+appears\ on\ its\ own.+end\ comment++2012/5/14\ something\ \ ;\ a\ transaction\ comment+\ \ \ \ ;\ the\ transaction\ comment,\ continued+\ \ \ \ posting1\ \ 1\ \ ;\ a\ comment\ for\ posting\ 1+\ \ \ \ posting2+\ \ \ \ ;\ a\ comment\ for\ posting\ 2+\ \ \ \ ;\ another\ comment\ line\ for\ posting\ 2+;\ a\ journal\ comment\ (because\ not\ indented)+\f[]+.fi+.SS Tags+.PP+A \f[I]tag\f[] is a word followed by a full colon inside a transaction+or posting comment.+You can write multiple tags, comma separated.+Eg: \f[C];\ a\ comment\ containing\ sometag:,\ anothertag:\f[].+You can search for tags with the \f[C]tag:\f[] query.+.PP+A tag can also have a value, which is any text between the colon and the+next comma or newline, excluding leading/trailing whitespace.+(So hledger tag values can not contain commas or newlines).+.PP+Tags in a transaction comment affect the transaction and all of its+postings, while tags in a posting comment affect only that posting.+For example, the following transaction has three tags (A, TAG2,+third\-tag) and the posting has four (A, TAG2, third\-tag,+posting\-tag):+.IP+.nf+\f[C]+1/1\ a\ transaction\ \ ;\ A:,\ TAG2:+\ \ \ \ ;\ third\-tag:\ a\ third\ transaction\ tag,\ this\ time\ with\ a\ value+\ \ \ \ (a)\ \ $1\ \ ;\ posting\-tag:+\f[]+.fi+.PP+Tags are like Ledger\[aq]s metadata feature, except hledger\[aq]s tag+values are simple strings.+.SS Directives+.SS Account aliases+.PP+You can define aliases which rewrite your account names (after reading+the journal, before generating reports).+hledger\[aq]s account aliases can be useful for:+.IP \[bu] 2+expanding shorthand account names to their full form, allowing easier+data entry and a less verbose journal+.IP \[bu] 2+adapting old journals to your current chart of accounts+.IP \[bu] 2+experimenting with new account organisations, like a new hierarchy or+combining two accounts into one+.IP \[bu] 2+customising reports+.PP+See also How to use account aliases.+.SS Basic aliases+.PP+To set an account alias, use the \f[C]alias\f[] directive in your+journal file.+This affects all subsequent journal entries in the current file or its+included files.+The spaces around the = are optional:+.IP+.nf+\f[C]+alias\ OLD\ =\ NEW+\f[]+.fi+.PP+Or, you can use the \f[C]\-\-alias\ \[aq]OLD=NEW\[aq]\f[] option on the+command line.+This affects all entries.+It\[aq]s useful for trying out aliases interactively.+.PP+OLD and NEW are full account names.+hledger will replace any occurrence of the old account name with the new+one.+Subaccounts are also affected.+Eg:+.IP+.nf+\f[C]+alias\ checking\ =\ assets:bank:wells\ fargo:checking+#\ rewrites\ "checking"\ to\ "assets:bank:wells\ fargo:checking",\ or\ "checking:a"\ to\ "assets:bank:wells\ fargo:checking:a"+\f[]+.fi+.SS Regex aliases+.PP+There is also a more powerful variant that uses a regular expression,+indicated by the forward slashes.+(This was the default behaviour in hledger 0.24\-0.25):+.IP+.nf+\f[C]+alias\ /REGEX/\ =\ REPLACEMENT+\f[]+.fi+.PP+or \f[C]\-\-alias\ \[aq]/REGEX/=REPLACEMENT\[aq]\f[].+.PP+REGEX is a case\-insensitive regular expression.+Anywhere it matches inside an account name, the matched part will be+replaced by REPLACEMENT.+If REGEX contains parenthesised match groups, these can be referenced by+the usual numeric backreferences in REPLACEMENT.+Note, currently regular expression aliases may cause noticeable+slow\-downs.+(And if you use Ledger on your hledger file, they will be ignored.) Eg:+.IP+.nf+\f[C]+alias\ /^(.+):bank:([^:]+)(.*)/\ =\ \\1:\\2\ \\3+#\ rewrites\ "assets:bank:wells\ fargo:checking"\ to\ \ "assets:wells\ fargo\ checking"+\f[]+.fi+.SS Multiple aliases+.PP+You can define as many aliases as you like using directives or+command\-line options.+Aliases are recursive \- each alias sees the result of applying previous+ones.+(This is different from Ledger, where aliases are non\-recursive by+default).+Aliases are applied in the following order:+.IP "1." 3+alias directives, most recently seen first (recent directives take+precedence over earlier ones; directives not yet seen are ignored)+.IP "2." 3+alias options, in the order they appear on the command line+.SS end aliases+.PP+You can clear (forget) all currently defined aliases with the+\f[C]end\ aliases\f[] directive:+.IP+.nf+\f[C]+end\ aliases+\f[]+.fi+.SS account directive+.PP+The \f[C]account\f[] directive predefines account names, as in Ledger+and Beancount.+This may be useful for your own documentation; hledger doesn\[aq]t make+use of it yet.+.IP+.nf+\f[C]+;\ account\ ACCT+;\ \ \ OPTIONAL\ COMMENTS/TAGS...++account\ assets:bank:checking+\ a\ comment+\ acct\-no:12345++account\ expenses:food++;\ etc.+\f[]+.fi+.SS apply account directive+.PP+You can specify a parent account which will be prepended to all accounts+within a section of the journal.+Use the \f[C]apply\ account\f[] and \f[C]end\ apply\ account\f[]+directives like so:+.IP+.nf+\f[C]+apply\ account\ home++2010/1/1+\ \ \ \ food\ \ \ \ $10+\ \ \ \ cash++end\ apply\ account+\f[]+.fi+.PP+which is equivalent to:+.IP+.nf+\f[C]+2010/01/01+\ \ \ \ home:food\ \ \ \ \ \ \ \ \ \ \ $10+\ \ \ \ home:cash\ \ \ \ \ \ \ \ \ \ $\-10+\f[]+.fi+.PP+If \f[C]end\ apply\ account\f[] is omitted, the effect lasts to the end+of the file.+Included files are also affected, eg:+.IP+.nf+\f[C]+apply\ account\ business+include\ biz.journal+end\ apply\ account+apply\ account\ personal+include\ personal.journal+\f[]+.fi+.PP+Prior to hledger 1.0, legacy \f[C]account\f[] and \f[C]end\f[] spellings+were also supported.+.SS Multi\-line comments+.PP+A line containing just \f[C]comment\f[] starts a multi\-line comment,+and a line containing just \f[C]end\ comment\f[] ends it.+See comments.+.SS commodity directive+.PP+The \f[C]commodity\f[] directive predefines commodities (currently this+is just informational), and also it may define the display format for+amounts in this commodity (overriding the automatically inferred+format).+.PP+It may be written on a single line, like this:+.IP+.nf+\f[C]+;\ commodity\ EXAMPLEAMOUNT++;\ display\ AAAA\ amounts\ with\ the\ symbol\ on\ the\ right,\ space\-separated,+;\ using\ period\ as\ decimal\ point,\ with\ four\ decimal\ places,\ and+;\ separating\ thousands\ with\ comma.+commodity\ 1,000.0000\ AAAA+\f[]+.fi+.PP+or on multiple lines, using the "format" subdirective.+In this case the commodity symbol appears twice and should be the same+in both places:+.IP+.nf+\f[C]+;\ commodity\ SYMBOL+;\ \ \ format\ EXAMPLEAMOUNT++;\ display\ indian\ rupees\ with\ currency\ name\ on\ the\ left,+;\ thousands,\ lakhs\ and\ crores\ comma\-separated,+;\ period\ as\ decimal\ point,\ and\ two\ decimal\ places.+commodity\ INR+\ \ format\ INR\ 9,99,99,999.00+\f[]+.fi+.SS Default commodity+.PP+The D directive sets a default commodity (and display format), to be+used for amounts without a commodity symbol (ie, plain numbers).+(Note this differs from Ledger\[aq]s default commodity directive.) The+commodity and display format will be applied to all subsequent+commodity\-less amounts, or until the next D directive.+.IP+.nf+\f[C]+#\ commodity\-less\ amounts\ should\ be\ treated\ as\ dollars+#\ (and\ displayed\ with\ symbol\ on\ the\ left,\ thousands\ separators\ and\ two\ decimal\ places)+D\ $1,000.00++1/1+\ \ a\ \ \ \ \ 5\ \ \ \ #\ <\-\ commodity\-less\ amount,\ becomes\ $1+\ \ b+\f[]+.fi+.SS Default year+.PP+You can set a default year to be used for subsequent dates which+don\[aq]t specify a year.+This is a line beginning with \f[C]Y\f[] followed by the year.+Eg:+.IP+.nf+\f[C]+Y2009\ \ \ \ \ \ ;\ set\ default\ year\ to\ 2009++12/15\ \ \ \ \ \ ;\ equivalent\ to\ 2009/12/15+\ \ expenses\ \ 1+\ \ assets++Y2010\ \ \ \ \ \ ;\ change\ default\ year\ to\ 2010++2009/1/30\ \ ;\ specifies\ the\ year,\ not\ affected+\ \ expenses\ \ 1+\ \ assets++1/31\ \ \ \ \ \ \ ;\ equivalent\ to\ 2010/1/31+\ \ expenses\ \ 1+\ \ assets+\f[]+.fi+.SS Including other files+.PP+You can pull in the content of additional journal files by writing an+include directive, like this:+.IP+.nf+\f[C]+include\ path/to/file.journal+\f[]+.fi+.PP+If the path does not begin with a slash, it is relative to the current+file.+Glob patterns (\f[C]*\f[]) are not currently supported.+.PP+The \f[C]include\f[] directive can only be used in journal files.+It can include journal, timeclock or timedot files, but not CSV files.+.SH EDITOR SUPPORT+.PP+Add\-on modes exist for various text editors, to make working with+journal files easier.+They add colour, navigation aids and helpful commands.+For hledger users who edit the journal file directly (the majority),+using one of these modes is quite recommended.+.PP+These were written with Ledger in mind, but also work with hledger+files:+.PP+.TS+tab(@);+lw(16.5n) lw(51.5n).+T{+Emacs+T}@T{+http://www.ledger\-cli.org/3.0/doc/ledger\-mode.html+T}+T{+Vim+T}@T{+https://github.com/ledger/ledger/wiki/Getting\-started+T}+T{+Sublime Text+T}@T{+https://github.com/ledger/ledger/wiki/Using\-Sublime\-Text+T}+T{+Textmate+T}@T{+https://github.com/ledger/ledger/wiki/Using\-TextMate\-2+T}+T{+Text Wrangler \ +T}@T{+https://github.com/ledger/ledger/wiki/Editing\-Ledger\-files\-with\-TextWrangler+T}+.TE+++.SH "REPORTING BUGS"+Report bugs at http://bugs.hledger.org+(or on the #hledger IRC channel or hledger mail list)++.SH AUTHORS+Simon Michael <simon@joyful.com> and contributors++.SH COPYRIGHT++Copyright (C) 2007-2016 Simon Michael.+.br+Released under GNU GPL v3 or later.++.SH SEE ALSO+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),+ledger(1)++http://hledger.org
+ doc/hledger_journal.5.info view
@@ -0,0 +1,995 @@+This is hledger-lib/doc/hledger_journal.5.info, produced by makeinfo+version 4.8 from stdin.+++File: hledger_journal.5.info,  Node: Top,  Up: (dir)++hledger_journal(5) hledger 1.0+******************************++hledger's usual data source is a plain text file containing journal+entries in hledger journal format. This file represents a standard+accounting general journal. I use file names ending in `.journal', but+that's not required. The journal file contains a number of transaction+entries, each describing a transfer of money (or any commodity) between+two or more named accounts, in a simple format readable by both hledger+and humans.++   hledger's journal format is a compatible subset, mostly, of ledger's+journal format, so hledger can work with compatible ledger journal files+as well. It's safe, and encouraged, to run both hledger and ledger on+the same journal file, eg to validate the results you're getting.++   You can use hledger without learning any more about this file; just+use the add or web commands to create and update it. Many users, though,+also edit the journal file directly with a text editor, perhaps assisted+by the helper modes for emacs or vim.++   Here's an example:+++; A sample journal file. This is a comment.++2008/01/01 income               ; <- transaction's first line starts in column 0, contains date and description+    assets:bank:checking  $1    ; <- posting lines start with whitespace, each contains an account name+    income:salary        $-1    ;    followed by at least two spaces and an amount++2008/06/01 gift+    assets:bank:checking  $1    ; <- at least two postings in a transaction+    income:gifts         $-1    ; <- their amounts must balance to 0++2008/06/02 save+    assets:bank:saving    $1+    assets:bank:checking        ; <- one amount may be omitted; here $-1 is inferred++2008/06/03 eat & shop           ; <- description can be anything+    expenses:food         $1+    expenses:supplies     $1    ; <- this transaction debits two expense accounts+    assets:cash                 ; <- $-2 inferred++2008/12/31 * pay off            ; <- an optional * or ! after the date means "cleared" (or anything you want)+    liabilities:debts     $1+    assets:bank:checking++* Menu:++* FILE FORMAT::+* EDITOR SUPPORT::+++File: hledger_journal.5.info,  Node: FILE FORMAT,  Next: EDITOR SUPPORT,  Prev: Top,  Up: Top++1 FILE FORMAT+*************++* Menu:++* Transactions::+* Dates::+* Account names::+* Amounts::+* Virtual Postings::+* Balance Assertions::+* Prices::+* Comments::+* Tags::+* Directives::+++File: hledger_journal.5.info,  Node: Transactions,  Next: Dates,  Up: FILE FORMAT++1.1 Transactions+================++Transactions are represented by journal entries. Each begins with a+simple date in column 0, followed by three optional fields with spaces+between them:++   * a status flag, which can be empty or `!' or `*' (meaning+     "uncleared", "pending" and "cleared", or whatever you want)++   * a transaction code (eg a check number),++   * and/or a description++   then some number of postings, of some amount to some account. Each+posting is on its own line, consisting of:++   * indentation of one or more spaces (or tabs)++   * optionally, a `!' or `*' status flag followed by a space++   * an account name, optionally containing single spaces++   * optionally, two or more spaces or tabs followed by an amount++   Usually there are two or more postings, though one or none is also+possible. The posting amounts within a transaction must always balance,+ie add up to 0. Optionally one amount can be left blank, in which case+it will be inferred.+++File: hledger_journal.5.info,  Node: Dates,  Next: Account names,  Prev: Transactions,  Up: FILE FORMAT++1.2 Dates+=========++* Menu:++* Simple dates::+* Secondary dates::+* Posting dates::+++File: hledger_journal.5.info,  Node: Simple dates,  Next: Secondary dates,  Up: Dates++1.2.1 Simple dates+------------------++Within a journal file, transaction dates use Y/M/D (or Y-M-D or Y.M.D)+Leading zeros are optional. The year may be omitted, in which case it+will be inferred from the context - the current transaction, the default+year set with a default year directive, or the current date when the+command is run. Some examples: `2010/01/31', `1/31', `2010-01-31',+`2010.1.31'.+++File: hledger_journal.5.info,  Node: Secondary dates,  Next: Posting dates,  Prev: Simple dates,  Up: Dates++1.2.2 Secondary dates+---------------------++Real-life transactions sometimes involve more than one date - eg the+date you write a cheque, and the date it clears in your bank. When you+want to model this, eg for more accurate balances, you can specify+individual posting dates, which I recommend. Or, you can use the+secondary dates (aka auxiliary/effective dates) feature, supported for+compatibility with Ledger.++   A secondary date can be written after the primary date, separated by+an equals sign. The primary date, on the left, is used by default; the+secondary date, on the right, is used when the `--date2' flag is+specified (`--aux-date' or `--effective' also work).++   The meaning of secondary dates is up to you, but it's best to follow+a consistent rule. Eg write the bank's clearing date as primary, and+when needed, the date the transaction was initiated as secondary.++   Here's an example. Note that a secondary date will use the year of+the primary date if unspecified.+++2010/2/23=2/19 movie ticket+  expenses:cinema                   $10+  assets:checking+++$ hledger register checking+2010/02/23 movie ticket         assets:checking                $-10         $-10+++$ hledger register checking --date2+2010/02/19 movie ticket         assets:checking                $-10         $-10++   Secondary dates require some effort; you must use them consistently+in your journal entries and remember whether to use or not use the+`--date2' flag for your reports. They are included in hledger for+Ledger compatibility, but posting dates are a more powerful and less+confusing alternative.+++File: hledger_journal.5.info,  Node: Posting dates,  Prev: Secondary dates,  Up: Dates++1.2.3 Posting dates+-------------------++You can give individual postings a different date from their parent+transaction, by adding a posting comment containing a tag (see below)+like `date:DATE'. This is probably the best way to control posting+dates precisely. Eg in this example the expense should appear in May+reports, and the deduction from checking should be reported on 6/1 for+easy bank reconciliation:+++2015/5/30+    expenses:food     $10   ; food purchased on saturday 5/30+    assets:checking         ; bank cleared it on monday, date:6/1+++$ hledger -f t.j register food+2015/05/30                      expenses:food                  $10           $10+++$ hledger -f t.j register checking+2015/06/01                      assets:checking               $-10          $-10++   DATE should be a simple date; if the year is not specified it will+use the year of the transaction's date. You can set the secondary date+similarly, with `date2:DATE2'. The `date:' or `date2:' tags must have a+valid simple date value if they are present, eg a `date:' tag with no+value is not allowed.++   Ledger's earlier, more compact bracketed date syntax is also+supported: `[DATE]', `[DATE=DATE2]' or `[=DATE2]'. hledger will attempt+to parse any square-bracketed sequence of the `0123456789/-.='+characters in this way. With this syntax, DATE infers its year from the+transaction and DATE2 infers its year from DATE.+++File: hledger_journal.5.info,  Node: Account names,  Next: Amounts,  Prev: Dates,  Up: FILE FORMAT++1.3 Account names+=================++Account names typically have several parts separated by a full colon,+from which hledger derives a hierarchical chart of accounts. They can be+anything you like, but in finance there are traditionally five top-level+accounts: `assets', `liabilities', `income', `expenses', and `equity'.++   Account names may contain single spaces, eg: `assets:accounts+receivable'. Because of this, they must always be followed by *two or+more spaces* (or newline).++   Account names can be aliased.+++File: hledger_journal.5.info,  Node: Amounts,  Next: Virtual Postings,  Prev: Account names,  Up: FILE FORMAT++1.4 Amounts+===========++After the account name, there is usually an amount. Important: between+account name and amount, there must be *two or more spaces*.++   Amounts consist of a number and (usually) a currency symbol or+commodity name. Some examples:++   `2.00001'+`$1'+`4000 AAPL'+`3 "green apples"'+`-$1,000,000.00'+`INR 9,99,99,999.00'+`EUR -2.000.000,00'++   As you can see, the amount format is somewhat flexible:++   * amounts are a number (the "quantity") and optionally a currency+     symbol/commodity name (the "commodity").++   * the commodity is a symbol, word, or double-quoted phrase, on the+     left or right, with or without a separating space++   * negative amounts with a commodity on the left can have the minus+     sign before or after it++   * digit groups (thousands, or any other grouping) can be separated by+     commas (in which case period is used for decimal point) or periods+     (in which case comma is used for decimal point)++   You can use any of these variations when recording data, but when+hledger displays amounts, it will choose a consistent format for each+commodity. (Except for price amounts, which are always formatted as+written). The display format is chosen as follows:++   * if there is a commodity directive specifying the format, that is+     used++   * otherwise the format is inferred from the first posting amount in+     that commodity in the journal, and the precision (number of+     decimal places) will be the maximum from all posting amounts in+     that commmodity++   * or if there are no such amounts in the journal, a default format+     is used (like `$1000.00').++   Price amounts and amounts in D directives usually don't affect amount+format inference, but in some situations they can do so indirectly. (Eg+when D's default commodity is applied to a commodity-less amount, or+when an amountless posting is balanced using a price's commodity, or+when -V is used.) If you find this causing problems, set the desired+format with a commodity directive.+++File: hledger_journal.5.info,  Node: Virtual Postings,  Next: Balance Assertions,  Prev: Amounts,  Up: FILE FORMAT++1.5 Virtual Postings+====================++When you parenthesise the account name in a posting, we call that a+_virtual posting_, which means:++   * it is ignored when checking that the transaction is balanced++   * it is excluded from reports when the `--real/-R' flag is used, or+     the `real:1' query.++   You could use this, eg, to set an account's opening balance without+needing to use the `equity:opening balances' account:+++1/1 special unbalanced posting to set initial balance+  (assets:checking)   $1000++   When the account name is bracketed, we call it a _balanced virtual+posting_. This is like an ordinary virtual posting except the balanced+virtual postings in a transaction must balance to 0, like the real+postings (but separately from them). Balanced virtual postings are also+excluded by `--real/-R' or `real:1'.+++1/1 buy food with cash, and update some budget-tracking subaccounts elsewhere+  expenses:food                   $10+  assets:cash                    $-10+  [assets:checking:available]     $10+  [assets:checking:budget:food]  $-10++   Virtual postings have some legitimate uses, but those are few. You+can usually find an equivalent journal entry using real postings, which+is more correct and provides better error checking.+++File: hledger_journal.5.info,  Node: Balance Assertions,  Next: Prices,  Prev: Virtual Postings,  Up: FILE FORMAT++1.6 Balance Assertions+======================++hledger supports ledger-style balance assertions in journal files. These+look like `=EXPECTEDBALANCE' following a posting's amount. Eg in this+example we assert the expected dollar balance in accounts a and b after+each posting:+++2013/1/1+  a   $1  =$1+  b       =$-1++2013/1/2+  a   $1  =$2+  b  $-1  =$-2++   After reading a journal file, hledger will check all balance+assertions and report an error if any of them fail. Balance assertions+can protect you from, eg, inadvertently disrupting reconciled balances+while cleaning up old entries. You can disable them temporarily with the+`--ignore-assertions' flag, which can be useful for troubleshooting or+for reading Ledger files.++* Menu:++* Assertions and ordering::+* Assertions and commodities::+* Assertions and subaccounts::+* Assertions and virtual postings::+++File: hledger_journal.5.info,  Node: Assertions and ordering,  Next: Assertions and commodities,  Up: Balance Assertions++1.6.1 Assertions and ordering+-----------------------------++hledger sorts an account's postings and assertions first by date and+then (for postings on the same day) by parse order. Note this is+different from Ledger, which sorts assertions only by parse order.+(Also, Ledger assertions do not see the accumulated effect of repeated+postings to the same account within a transaction.)++   So, hledger balance assertions keep working if you reorder+differently-dated transactions within the journal. But if you reorder+same-dated transactions or postings, assertions might break and require+updating. This order dependence does bring an advantage: precise control+over the order of postings and assertions within a day, so you can+assert intra-day balances.++   With included files, things are a little more complicated. Including+preserves the ordering of postings and assertions. If you have multiple+postings to an account on the same day, split across different files,+and you also want to assert the account's balance on the same day,+you'll have to put the assertion in the right file.+++File: hledger_journal.5.info,  Node: Assertions and commodities,  Next: Assertions and subaccounts,  Prev: Assertions and ordering,  Up: Balance Assertions++1.6.2 Assertions and commodities+--------------------------------++The asserted balance must be a simple single-commodity amount, and in+fact the assertion checks only this commodity's balance within the+(possibly multi-commodity) account balance. We could call this a partial+balance assertion. This is compatible with Ledger, and makes it possible+to make assertions about accounts containing multiple commodities.++   To assert each commodity's balance in such a multi-commodity account,+you can add multiple postings (with amount 0 if necessary). But note+that no matter how many assertions you add, you can't be sure the+account does not contain some unexpected commodity. (We'll add support+for this kind of total balance assertion if there's demand.)+++File: hledger_journal.5.info,  Node: Assertions and subaccounts,  Next: Assertions and virtual postings,  Prev: Assertions and commodities,  Up: Balance Assertions++1.6.3 Assertions and subaccounts+--------------------------------++Balance assertions do not count the balance from subaccounts; they check+the posted account's exclusive balance. For example:+++1/1+  checking:fund   1 = 1  ; post to this subaccount, its balance is now 1+  checking        1 = 1  ; post to the parent account, its exclusive balance is now 1+  equity++   The balance report's flat mode shows these exclusive balances more+clearly:+++$ hledger bal checking --flat+                   1  checking+                   1  checking:fund+--------------------+                   2+++File: hledger_journal.5.info,  Node: Assertions and virtual postings,  Prev: Assertions and subaccounts,  Up: Balance Assertions++1.6.4 Assertions and virtual postings+-------------------------------------++Balance assertions are checked against all postings, both real and+virtual. They are not affected by the `--real/-R' flag or `real:' query.+++File: hledger_journal.5.info,  Node: Prices,  Next: Comments,  Prev: Balance Assertions,  Up: FILE FORMAT++1.7 Prices+==========++* Menu:++* Transaction prices::+* Market prices::+++File: hledger_journal.5.info,  Node: Transaction prices,  Next: Market prices,  Up: Prices++1.7.1 Transaction prices+------------------------++When recording a transaction, you can also record an amount's price in+another commodity. This documents the exchange rate, cost (of a+purchase), or selling price (of a sale) that was in effect within this+particular transaction (or more precisely, within the particular+posting). These transaction prices are fixed, and do not change.++   Such priced amounts can be displayed in their transaction price's+commodity, by using the `--cost/-B' flag (B for "cost Basis"),+supported by most hledger commands.++   There are three ways to specify a transaction price:++  1. Write the unit price (aka exchange rate), as `@ UNITPRICE' after+     the amount:+++     2009/1/1+       assets:foreign currency   €100 @ $1.35  ; one hundred euros at $1.35 each+       assets:cash++  2. Or write the total price, as `@@ TOTALPRICE' after the amount:+++     2009/1/1+       assets:foreign currency   €100 @@ $135  ; one hundred euros at $135 for the lot+       assets:cash++  3. Or let hledger infer the price so as to balance the transaction. To+     permit this, you must fully specify all posting amounts, and their+     sum must have a non-zero amount in exactly two commodities:+++     2009/1/1+       assets:foreign currency   €100          ; one hundred euros+       assets:cash              $-135          ; exchanged for $135+++   With any of the above examples we get:+++$ hledger print -B+2009/01/01+    assets:foreign currency       $135.00+    assets:cash                  $-135.00++   Example use for transaction prices: recording the effective+conversion rate of purchases made in a foreign currency.+++File: hledger_journal.5.info,  Node: Market prices,  Prev: Transaction prices,  Up: Prices++1.7.2 Market prices+-------------------++Market prices are not tied to a particular transaction; they represent+historical exchange rates between two commodities, usually from some+public market which publishes such rates.++   When market prices are known, the `-V/--value' option will use them+to convert reported amounts to their market value as of the report end+date. This option is currently available only with the balance command.++   You record market prices (Ledger calls them historical prices) with+a P directive, in the journal or perhaps in a separate included file.+Market price directives have the format:+++P DATE COMMODITYSYMBOL UNITPRICE++   For example, the following directives say that the euro's exchange+rate was 1.35 US dollars during 2009, and $1.40 from 2010 onward (and+unknown before 2009).+++P 2009/1/1 € $1.35+P 2010/1/1 € $1.40++   Example use for market prices: tracking the value of stocks.+++File: hledger_journal.5.info,  Node: Comments,  Next: Tags,  Prev: Prices,  Up: FILE FORMAT++1.8 Comments+============++Lines in the journal beginning with a semicolon (`;') or hash (`#') or+asterisk (`*') are comments, and will be ignored.  (Asterisk comments+make it easy to treat your journal like an org-mode outline in emacs.)++   Also, anything between `comment' and `end comment' directives is a+(multi-line) comment. If there is no `end comment', the comment extends+to the end of the file.++   You can attach comments to a transaction by writing them after the+description and/or indented on the following lines (before the+postings). Similarly, you can attach comments to an individual posting+by writing them after the amount and/or indented on the following lines.++   Some examples:+++# a journal comment++; also a journal comment++comment+This is a multiline comment,+which continues until a line+where the "end comment" string+appears on its own.+end comment++2012/5/14 something  ; a transaction comment+    ; the transaction comment, continued+    posting1  1  ; a comment for posting 1+    posting2+    ; a comment for posting 2+    ; another comment line for posting 2+; a journal comment (because not indented)+++File: hledger_journal.5.info,  Node: Tags,  Next: Directives,  Prev: Comments,  Up: FILE FORMAT++1.9 Tags+========++A _tag_ is a word followed by a full colon inside a transaction or+posting comment. You can write multiple tags, comma separated. Eg: `; a+comment containing sometag:, anothertag:'. You can search for tags with+the `tag:' query.++   A tag can also have a value, which is any text between the colon and+the next comma or newline, excluding leading/trailing whitespace. (So+hledger tag values can not contain commas or newlines).++   Tags in a transaction comment affect the transaction and all of its+postings, while tags in a posting comment affect only that posting. For+example, the following transaction has three tags (A, TAG2, third-tag)+and the posting has four (A, TAG2, third-tag, posting-tag):+++1/1 a transaction  ; A:, TAG2:+    ; third-tag: a third transaction tag, this time with a value+    (a)  $1  ; posting-tag:++   Tags are like Ledger's metadata feature, except hledger's tag values+are simple strings.+++File: hledger_journal.5.info,  Node: Directives,  Prev: Tags,  Up: FILE FORMAT++1.10 Directives+===============++* Menu:++* Account aliases::+* account directive::+* apply account directive::+* Multi-line comments::+* commodity directive::+* Default commodity::+* Default year::+* Including other files::+++File: hledger_journal.5.info,  Node: Account aliases,  Next: account directive,  Up: Directives++1.10.1 Account aliases+----------------------++You can define aliases which rewrite your account names (after reading+the journal, before generating reports). hledger's account aliases can+be useful for:++   * expanding shorthand account names to their full form, allowing+     easier data entry and a less verbose journal++   * adapting old journals to your current chart of accounts++   * experimenting with new account organisations, like a new hierarchy+     or combining two accounts into one++   * customising reports++   See also How to use account aliases.++* Menu:++* Basic aliases::+* Regex aliases::+* Multiple aliases::+* end aliases::+++File: hledger_journal.5.info,  Node: Basic aliases,  Next: Regex aliases,  Up: Account aliases++1.10.1.1 Basic aliases+......................++To set an account alias, use the `alias' directive in your journal+file. This affects all subsequent journal entries in the current file or+its included files. The spaces around the = are optional:+++alias OLD = NEW++   Or, you can use the `--alias 'OLD=NEW'' option on the command line.+This affects all entries. It's useful for trying out aliases+interactively.++   OLD and NEW are full account names. hledger will replace any+occurrence of the old account name with the new one. Subaccounts are+also affected.  Eg:+++alias checking = assets:bank:wells fargo:checking+# rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"+++File: hledger_journal.5.info,  Node: Regex aliases,  Next: Multiple aliases,  Prev: Basic aliases,  Up: Account aliases++1.10.1.2 Regex aliases+......................++There is also a more powerful variant that uses a regular expression,+indicated by the forward slashes. (This was the default behaviour in+hledger 0.24-0.25):+++alias /REGEX/ = REPLACEMENT++   or `--alias '/REGEX/=REPLACEMENT''.++   REGEX is a case-insensitive regular expression. Anywhere it matches+inside an account name, the matched part will be replaced by+REPLACEMENT. If REGEX contains parenthesised match groups, these can be+referenced by the usual numeric backreferences in REPLACEMENT. Note,+currently regular expression aliases may cause noticeable slow-downs.+(And if you use Ledger on your hledger file, they will be ignored.) Eg:+++alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3+# rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking"+++File: hledger_journal.5.info,  Node: Multiple aliases,  Next: end aliases,  Prev: Regex aliases,  Up: Account aliases++1.10.1.3 Multiple aliases+.........................++You can define as many aliases as you like using directives or+command-line options. Aliases are recursive - each alias sees the result+of applying previous ones. (This is different from Ledger, where aliases+are non-recursive by default). Aliases are applied in the following+order:++  1. alias directives, most recently seen first (recent directives take+     precedence over earlier ones; directives not yet seen are ignored)++  2. alias options, in the order they appear on the command line+++File: hledger_journal.5.info,  Node: end aliases,  Prev: Multiple aliases,  Up: Account aliases++1.10.1.4 end aliases+....................++You can clear (forget) all currently defined aliases with the `end+aliases' directive:+++end aliases+++File: hledger_journal.5.info,  Node: account directive,  Next: apply account directive,  Prev: Account aliases,  Up: Directives++1.10.2 account directive+------------------------++The `account' directive predefines account names, as in Ledger and+Beancount. This may be useful for your own documentation; hledger+doesn't make use of it yet.+++; account ACCT+;   OPTIONAL COMMENTS/TAGS...++account assets:bank:checking+ a comment+ acct-no:12345++account expenses:food++; etc.+++File: hledger_journal.5.info,  Node: apply account directive,  Next: Multi-line comments,  Prev: account directive,  Up: Directives++1.10.3 apply account directive+------------------------------++You can specify a parent account which will be prepended to all accounts+within a section of the journal. Use the `apply account' and `end apply+account' directives like so:+++apply account home++2010/1/1+    food    $10+    cash++end apply account++   which is equivalent to:+++2010/01/01+    home:food           $10+    home:cash          $-10++   If `end apply account' is omitted, the effect lasts to the end of+the file. Included files are also affected, eg:+++apply account business+include biz.journal+end apply account+apply account personal+include personal.journal++   Prior to hledger 1.0, legacy `account' and `end' spellings were also+supported.+++File: hledger_journal.5.info,  Node: Multi-line comments,  Next: commodity directive,  Prev: apply account directive,  Up: Directives++1.10.4 Multi-line comments+--------------------------++A line containing just `comment' starts a multi-line comment, and a+line containing just `end comment' ends it. See comments.+++File: hledger_journal.5.info,  Node: commodity directive,  Next: Default commodity,  Prev: Multi-line comments,  Up: Directives++1.10.5 commodity directive+--------------------------++The `commodity' directive predefines commodities (currently this is+just informational), and also it may define the display format for+amounts in this commodity (overriding the automatically inferred+format).++   It may be written on a single line, like this:+++; commodity EXAMPLEAMOUNT++; display AAAA amounts with the symbol on the right, space-separated,+; using period as decimal point, with four decimal places, and+; separating thousands with comma.+commodity 1,000.0000 AAAA++   or on multiple lines, using the "format" subdirective. In this case+the commodity symbol appears twice and should be the same in both+places:+++; commodity SYMBOL+;   format EXAMPLEAMOUNT++; display indian rupees with currency name on the left,+; thousands, lakhs and crores comma-separated,+; period as decimal point, and two decimal places.+commodity INR+  format INR 9,99,99,999.00+++File: hledger_journal.5.info,  Node: Default commodity,  Next: Default year,  Prev: commodity directive,  Up: Directives++1.10.6 Default commodity+------------------------++The D directive sets a default commodity (and display format), to be+used for amounts without a commodity symbol (ie, plain numbers). (Note+this differs from Ledger's default commodity directive.) The commodity+and display format will be applied to all subsequent commodity-less+amounts, or until the next D directive.+++# commodity-less amounts should be treated as dollars+# (and displayed with symbol on the left, thousands separators and two decimal places)+D $1,000.00++1/1+  a     5    # <- commodity-less amount, becomes $1+  b+++File: hledger_journal.5.info,  Node: Default year,  Next: Including other files,  Prev: Default commodity,  Up: Directives++1.10.7 Default year+-------------------++You can set a default year to be used for subsequent dates which don't+specify a year. This is a line beginning with `Y' followed by the year.+Eg:+++Y2009      ; set default year to 2009++12/15      ; equivalent to 2009/12/15+  expenses  1+  assets++Y2010      ; change default year to 2010++2009/1/30  ; specifies the year, not affected+  expenses  1+  assets++1/31       ; equivalent to 2010/1/31+  expenses  1+  assets+++File: hledger_journal.5.info,  Node: Including other files,  Prev: Default year,  Up: Directives++1.10.8 Including other files+----------------------------++You can pull in the content of additional journal files by writing an+include directive, like this:+++include path/to/file.journal++   If the path does not begin with a slash, it is relative to the+current file. Glob patterns (`*') are not currently supported.++   The `include' directive can only be used in journal files. It can+include journal, timeclock or timedot files, but not CSV files.+++File: hledger_journal.5.info,  Node: EDITOR SUPPORT,  Prev: FILE FORMAT,  Up: Top++2 EDITOR SUPPORT+****************++Add-on modes exist for various text editors, to make working with+journal files easier. They add colour, navigation aids and helpful+commands. For hledger users who edit the journal file directly (the+majority), using one of these modes is quite recommended.++   These were written with Ledger in mind, but also work with hledger+files:++Emacs             http://www.ledger-cli.org/3.0/doc/ledger-mode.html+Vim               https://github.com/ledger/ledger/wiki/Getting-started+Sublime Text      https://github.com/ledger/ledger/wiki/Using-Sublime-Text+Textmate          https://github.com/ledger/ledger/wiki/Using-TextMate-2+Text Wrangler     https://github.com/ledger/ledger/wiki/Editing-Ledger-files-with-TextWrangler++++Tag Table:+Node: Top94+Node: FILE FORMAT2284+Ref: #file-format2410+Node: Transactions2569+Ref: #transactions2689+Node: Dates3632+Ref: #dates3760+Node: Simple dates3825+Ref: #simple-dates3953+Node: Secondary dates4317+Ref: #secondary-dates4473+Node: Posting dates6033+Ref: #posting-dates6164+Node: Account names7535+Ref: #account-names7674+Node: Amounts8159+Ref: #amounts8297+Node: Virtual Postings10295+Ref: #virtual-postings10456+Node: Balance Assertions11676+Ref: #balance-assertions11840+Node: Assertions and ordering12662+Ref: #assertions-and-ordering12847+Node: Assertions and commodities13878+Ref: #assertions-and-commodities14104+Node: Assertions and subaccounts14796+Ref: #assertions-and-subaccounts15030+Node: Assertions and virtual postings15552+Ref: #assertions-and-virtual-postings15761+Node: Prices15902+Ref: #prices16034+Node: Transaction prices16085+Ref: #transaction-prices16230+Node: Market prices17837+Ref: #market-prices17972+Node: Comments18860+Ref: #comments18982+Node: Tags20094+Ref: #tags20212+Node: Directives21135+Ref: #directives21250+Node: Account aliases21443+Ref: #account-aliases21589+Node: Basic aliases22191+Ref: #basic-aliases22336+Node: Regex aliases23024+Ref: #regex-aliases23194+Node: Multiple aliases23964+Ref: #multiple-aliases24138+Node: end aliases24634+Ref: #end-aliases24776+Node: account directive24878+Ref: #account-directive25060+Node: apply account directive25356+Ref: #apply-account-directive25554+Node: Multi-line comments26214+Ref: #multi-line-comments26406+Node: commodity directive26533+Ref: #commodity-directive26719+Node: Default commodity27592+Ref: #default-commodity27767+Node: Default year28303+Ref: #default-year28470+Node: Including other files28893+Ref: #including-other-files29052+Node: EDITOR SUPPORT29448+Ref: #editor-support29568++End Tag Table
+ doc/hledger_journal.5.txt view
@@ -0,0 +1,700 @@++hledger_journal(5)           hledger User Manuals           hledger_journal(5)++++NAME+       Journal - hledger's default file format, representing a General Journal++DESCRIPTION+       hledger's usual data source is a plain  text  file  containing  journal+       entries  in  hledger  journal  format.  This file represents a standard+       accounting general journal.  I use file names ending in  .journal,  but+       that's not required.  The journal file contains a number of transaction+       entries, each describing a transfer of money (or any commodity) between+       two or more named accounts, in a simple format readable by both hledger+       and humans.++       hledger's journal format is a compatible subset,  mostly,  of  ledger's+       journal  format,  so  hledger  can  work with compatible ledger journal+       files as well.  It's safe, and encouraged,  to  run  both  hledger  and+       ledger on the same journal file, eg to validate the results you're get-+       ting.++       You can use hledger without learning any more about this file; just use+       the  add  or web commands to create and update it.  Many users, though,+       also edit the  journal  file  directly  with  a  text  editor,  perhaps+       assisted by the helper modes for emacs or vim.++       Here's an example:++              ; A sample journal file. This is a comment.++              2008/01/01 income               ; <- transaction's first line starts in column 0, contains date and description+                  assets:bank:checking  $1    ; <- posting lines start with whitespace, each contains an account name+                  income:salary        $-1    ;    followed by at least two spaces and an amount++              2008/06/01 gift+                  assets:bank:checking  $1    ; <- at least two postings in a transaction+                  income:gifts         $-1    ; <- their amounts must balance to 0++              2008/06/02 save+                  assets:bank:saving    $1+                  assets:bank:checking        ; <- one amount may be omitted; here $-1 is inferred++              2008/06/03 eat & shop           ; <- description can be anything+                  expenses:food         $1+                  expenses:supplies     $1    ; <- this transaction debits two expense accounts+                  assets:cash                 ; <- $-2 inferred++              2008/12/31 * pay off            ; <- an optional * or ! after the date means "cleared" (or anything you want)+                  liabilities:debts     $1+                  assets:bank:checking++FILE FORMAT+   Transactions+       Transactions  are  represented  by journal entries.  Each begins with a+       simple date in column 0, followed by three optional fields with  spaces+       between them:++       o a  status  flag,  which  can be empty or ! or * (meaning "uncleared",+         "pending" and "cleared", or whatever you want)++       o a transaction code (eg a check number),++       o and/or a description++       then some number of postings, of some amount  to  some  account.   Each+       posting is on its own line, consisting of:++       o indentation of one or more spaces (or tabs)++       o optionally, a ! or * status flag followed by a space++       o an account name, optionally containing single spaces++       o optionally, two or more spaces or tabs followed by an amount++       Usually there are two or more postings, though one or none is also pos-+       sible.  The posting amounts within a transaction must  always  balance,+       ie add up to 0.  Optionally one amount can be left blank, in which case+       it will be inferred.++   Dates+   Simple dates+       Within a journal file, transaction dates use Y/M/D (or Y-M-D or  Y.M.D)+       Leading  zeros are optional.  The year may be omitted, in which case it+       will be inferred from  the  context  -  the  current  transaction,  the+       default  year  set  with  a default year directive, or the current date+       when the command is run.  Some examples: 2010/01/31, 1/31,  2010-01-31,+       2010.1.31.++   Secondary dates+       Real-life  transactions  sometimes  involve more than one date - eg the+       date you write a cheque, and the date it clears in your bank.  When you+       want  to  model  this,  eg  for more accurate balances, you can specify+       individual posting dates, which I recommend.  Or, you can use the  sec-+       ondary  dates  (aka  auxiliary/effective  dates) feature, supported for+       compatibility with Ledger.++       A secondary date can be written after the primary date, separated by an+       equals  sign.   The  primary date, on the left, is used by default; the+       secondary date, on the right, is used when the --date2 flag  is  speci-+       fied (--aux-date or --effective also work).++       The  meaning of secondary dates is up to you, but it's best to follow a+       consistent rule.  Eg write the bank's clearing  date  as  primary,  and+       when needed, the date the transaction was initiated as secondary.++       Here's an example.  Note that a secondary date will use the year of the+       primary date if unspecified.++              2010/2/23=2/19 movie ticket+                expenses:cinema                   $10+                assets:checking++              $ hledger register checking+              2010/02/23 movie ticket         assets:checking                $-10         $-10++              $ hledger register checking --date2+              2010/02/19 movie ticket         assets:checking                $-10         $-10++       Secondary dates require some effort; you must use them consistently  in+       your journal entries and remember whether to use or not use the --date2+       flag for your reports.  They are included in hledger for Ledger compat-+       ibility,  but  posting  dates  are  a  more powerful and less confusing+       alternative.++   Posting dates+       You can give individual postings a different  date  from  their  parent+       transaction,  by  adding a posting comment containing a tag (see below)+       like date:DATE.  This is probably the best way to control posting dates+       precisely.   Eg  in  this  example  the  expense  should  appear in May+       reports, and the deduction from checking should be reported on 6/1  for+       easy bank reconciliation:++              2015/5/30+                  expenses:food     $10   ; food purchased on saturday 5/30+                  assets:checking         ; bank cleared it on monday, date:6/1++              $ hledger -f t.j register food+              2015/05/30                      expenses:food                  $10           $10++              $ hledger -f t.j register checking+              2015/06/01                      assets:checking               $-10          $-10++       DATE  should be a simple date; if the year is not specified it will use+       the year of the transaction's date.  You can  set  the  secondary  date+       similarly,  with  date2:DATE2.   The  date:  or date2: tags must have a+       valid simple date value if they are present, eg a  date:  tag  with  no+       value is not allowed.++       Ledger's earlier, more compact bracketed date syntax is also supported:+       [DATE], [DATE=DATE2] or [=DATE2].  hledger will attempt  to  parse  any+       square-bracketed sequence of the 0123456789/-.= characters in this way.+       With this syntax, DATE infers its year from the transaction  and  DATE2+       infers its year from DATE.++   Account names+       Account  names  typically have several parts separated by a full colon,+       from which hledger derives a hierarchical chart of accounts.  They  can+       be  anything  you  like,  but  in  finance there are traditionally five+       top-level accounts: assets, liabilities, income, expenses, and  equity.++       Account  names  may  contain single spaces, eg: assets:accounts receiv-+       able.  Because of this, they must always be followed  by  two  or  more+       spaces (or newline).++       Account names can be aliased.++   Amounts+       After the account name, there is usually an amount.  Important: between+       account name and amount, there must be two or more spaces.++       Amounts consist of a number and (usually) a currency symbol or  commod-+       ity name.  Some examples:++       2.00001+       $1+       4000 AAPL+       3 "green apples"+       -$1,000,000.00+       INR 9,99,99,999.00+       EUR -2.000.000,00++       As you can see, the amount format is somewhat flexible:++       o amounts  are a number (the "quantity") and optionally a currency sym-+         bol/commodity name (the "commodity").++       o the commodity is a symbol, word, or double-quoted phrase, on the left+         or right, with or without a separating space++       o negative amounts with a commodity on the left can have the minus sign+         before or after it++       o digit groups (thousands, or any other grouping) can be  separated  by+         commas  (in  which  case period is used for decimal point) or periods+         (in which case comma is used for decimal point)++       You can use any of these  variations  when  recording  data,  but  when+       hledger  displays  amounts, it will choose a consistent format for each+       commodity.  (Except for price amounts, which are  always  formatted  as+       written).  The display format is chosen as follows:++       o if there is a commodity directive specifying the format, that is used++       o otherwise the format is inferred from the  first  posting  amount  in+         that  commodity  in the journal, and the precision (number of decimal+         places) will be the maximum from all posting amounts in that commmod-+         ity++       o or  if  there are no such amounts in the journal, a default format is+         used (like $1000.00).++       Price amounts and amounts in D directives usually don't  affect  amount+       format  inference,  but  in  some situations they can do so indirectly.+       (Eg when D's default commodity is applied to a  commodity-less  amount,+       or when an amountless posting is balanced using a price's commodity, or+       when -V is used.) If you find this causing problems,  set  the  desired+       format with a commodity directive.++   Virtual Postings+       When  you  parenthesise  the  account name in a posting, we call that a+       virtual posting, which means:++       o it is ignored when checking that the transaction is balanced++       o it is excluded from reports when the --real/-R flag is used,  or  the+         real:1 query.++       You  could  use  this,  eg, to set an account's opening balance without+       needing to use the equity:opening balances account:++              1/1 special unbalanced posting to set initial balance+                (assets:checking)   $1000++       When the account name is bracketed, we call it a balanced virtual post-+       ing.  This is like an ordinary virtual posting except the balanced vir-+       tual postings in a transaction must balance to 0, like the  real  post-+       ings  (but  separately  from them).  Balanced virtual postings are also+       excluded by --real/-R or real:1.++              1/1 buy food with cash, and update some budget-tracking subaccounts elsewhere+                expenses:food                   $10+                assets:cash                    $-10+                [assets:checking:available]     $10+                [assets:checking:budget:food]  $-10++       Virtual postings have some legitimate uses, but those are few.  You can+       usually  find an equivalent journal entry using real postings, which is+       more correct and provides better error checking.++   Balance Assertions+       hledger supports ledger-style  balance  assertions  in  journal  files.+       These  look  like =EXPECTEDBALANCE following a posting's amount.  Eg in+       this example we assert the expected dollar balance in accounts a and  b+       after each posting:++              2013/1/1+                a   $1  =$1+                b       =$-1++              2013/1/2+                a   $1  =$2+                b  $-1  =$-2++       After reading a journal file, hledger will check all balance assertions+       and report an error if any of them fail.  Balance assertions  can  pro-+       tect  you  from, eg, inadvertently disrupting reconciled balances while+       cleaning up old entries.  You can disable  them  temporarily  with  the+       --ignore-assertions  flag,  which  can be useful for troubleshooting or+       for reading Ledger files.++   Assertions and ordering+       hledger sorts an account's postings and assertions first  by  date  and+       then  (for postings on the same day) by parse order.  Note this is dif-+       ferent from Ledger, which sorts assertions only by parse order.  (Also,+       Ledger  assertions  do not see the accumulated effect of repeated post-+       ings to the same account within a transaction.)++       So, hledger balance assertions keep  working  if  you  reorder  differ-+       ently-dated  transactions  within  the  journal.   But  if  you reorder+       same-dated transactions or postings, assertions might break and require+       updating.   This order dependence does bring an advantage: precise con-+       trol over the order of postings and assertions within a day, so you can+       assert intra-day balances.++       With  included  files, things are a little more complicated.  Including+       preserves the ordering of postings and assertions.  If you have  multi-+       ple  postings  to  an  account  on the same day, split across different+       files, and you also want to assert the account's balance  on  the  same+       day, you'll have to put the assertion in the right file.++   Assertions and commodities+       The  asserted  balance must be a simple single-commodity amount, and in+       fact the assertion checks only  this  commodity's  balance  within  the+       (possibly  multi-commodity) account balance.  We could call this a par-+       tial balance assertion.  This is compatible with Ledger, and  makes  it+       possible to make assertions about accounts containing multiple commodi-+       ties.++       To assert each commodity's balance in such a  multi-commodity  account,+       you  can  add multiple postings (with amount 0 if necessary).  But note+       that no matter how many assertions you  add,  you  can't  be  sure  the+       account does not contain some unexpected commodity.  (We'll add support+       for this kind of total balance assertion if there's demand.)++   Assertions and subaccounts+       Balance assertions do not count  the  balance  from  subaccounts;  they+       check the posted account's exclusive balance.  For example:++              1/1+                checking:fund   1 = 1  ; post to this subaccount, its balance is now 1+                checking        1 = 1  ; post to the parent account, its exclusive balance is now 1+                equity++       The  balance  report's  flat  mode  shows these exclusive balances more+       clearly:++              $ hledger bal checking --flat+                                 1  checking+                                 1  checking:fund+              --------------------+                                 2++   Assertions and virtual postings+       Balance assertions are checked against all postings, both real and vir-+       tual.  They are not affected by the --real/-R flag or real: query.++   Prices+   Transaction prices+       When  recording a transaction, you can also record an amount's price in+       another commodity.  This documents the exchange rate, cost (of  a  pur-+       chase),  or  selling  price  (of a sale) that was in effect within this+       particular transaction (or more precisely, within the particular  post-+       ing).  These transaction prices are fixed, and do not change.++       Such  priced amounts can be displayed in their transaction price's com-+       modity, by using the --cost/-B flag (B for "cost Basis"), supported  by+       most hledger commands.++       There are three ways to specify a transaction price:++       1. Write  the  unit price (aka exchange rate), as @ UNITPRICE after the+          amount:++                  2009/1/1+                    assets:foreign currency   100 @ $1.35  ; one hundred euros at $1.35 each+                    assets:cash++       2. Or write the total price, as @@ TOTALPRICE after the amount:++                  2009/1/1+                    assets:foreign currency   100 @@ $135  ; one hundred euros at $135 for the lot+                    assets:cash++       3. Or let hledger infer the price so as to balance the transaction.  To+          permit  this,  you must fully specify all posting amounts, and their+          sum must have a non-zero amount in exactly two commodities:++                  2009/1/1+                    assets:foreign currency   100          ; one hundred euros+                    assets:cash              $-135          ; exchanged for $135++       With any of the above examples we get:++              $ hledger print -B+              2009/01/01+                  assets:foreign currency       $135.00+                  assets:cash                  $-135.00++       Example use for transaction prices: recording the effective  conversion+       rate of purchases made in a foreign currency.++   Market prices+       Market  prices are not tied to a particular transaction; they represent+       historical exchange rates between two commodities,  usually  from  some+       public market which publishes such rates.++       When  market  prices  are known, the -V/--value option will use them to+       convert reported amounts to their market value as  of  the  report  end+       date.   This  option  is currently available only with the balance com-+       mand.++       You record market prices (Ledger calls them historical prices) with a P+       directive, in the journal or perhaps in a separate included file.  Mar-+       ket price directives have the format:++              P DATE COMMODITYSYMBOL UNITPRICE++       For example, the following directives say that the euro's exchange rate+       was  1.35  US  dollars  during  2009,  and  $1.40 from 2010 onward (and+       unknown before 2009).++              P 2009/1/1  $1.35+              P 2010/1/1  $1.40++       Example use for market prices: tracking the value of stocks.++   Comments+       Lines in the journal beginning with a semicolon  (;)  or  hash  (#)  or+       asterisk  (*)  are  comments,  and will be ignored.  (Asterisk comments+       make it easy to treat your journal like an org-mode outline in  emacs.)++       Also,   anything  between  comment  and  end comment  directives  is  a+       (multi-line) comment.  If there is no end comment, the comment  extends+       to the end of the file.++       You  can  attach  comments  to  a transaction by writing them after the+       description and/or indented on the following lines  (before  the  post-+       ings).   Similarly, you can attach comments to an individual posting by+       writing them after the amount and/or indented on the following lines.++       Some examples:++              # a journal comment++              ; also a journal comment++              comment+              This is a multiline comment,+              which continues until a line+              where the "end comment" string+              appears on its own.+              end comment++              2012/5/14 something  ; a transaction comment+                  ; the transaction comment, continued+                  posting1  1  ; a comment for posting 1+                  posting2+                  ; a comment for posting 2+                  ; another comment line for posting 2+              ; a journal comment (because not indented)++   Tags+       A tag is a word followed by a full colon inside a transaction or  post-+       ing  comment.   You  can  write  multiple  tags,  comma separated.  Eg:+       ; a comment containing sometag:, anothertag:.  You can search for  tags+       with the tag: query.++       A  tag  can  also have a value, which is any text between the colon and+       the next comma or newline, excluding leading/trailing whitespace.   (So+       hledger tag values can not contain commas or newlines).++       Tags  in  a  transaction  comment affect the transaction and all of its+       postings, while tags in a posting comment  affect  only  that  posting.+       For  example,  the  following  transaction  has  three  tags  (A, TAG2,+       third-tag) and the posting has four (A, TAG2, third-tag, posting-tag):++              1/1 a transaction  ; A:, TAG2:+                  ; third-tag: a third transaction tag, this time with a value+                  (a)  $1  ; posting-tag:++       Tags are like Ledger's metadata feature, except  hledger's  tag  values+       are simple strings.++   Directives+   Account aliases+       You  can define aliases which rewrite your account names (after reading+       the journal, before generating reports).  hledger's account aliases can+       be useful for:++       o expanding shorthand account names to their full form, allowing easier+         data entry and a less verbose journal++       o adapting old journals to your current chart of accounts++       o experimenting with new account organisations, like a new hierarchy or+         combining two accounts into one++       o customising reports++       See also How to use account aliases.++   Basic aliases+       To  set an account alias, use the alias directive in your journal file.+       This affects all subsequent journal entries in the current file or  its+       included files.  The spaces around the = are optional:++              alias OLD = NEW++       Or, you can use the --alias 'OLD=NEW' option on the command line.  This+       affects all entries.  It's useful for trying out aliases interactively.++       OLD  and  NEW  are full account names.  hledger will replace any occur-+       rence of the old account name with the new one.  Subaccounts  are  also+       affected.  Eg:++              alias checking = assets:bank:wells fargo:checking+              # rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"++   Regex aliases+       There  is  also a more powerful variant that uses a regular expression,+       indicated by the forward slashes.  (This was the default  behaviour  in+       hledger 0.24-0.25):++              alias /REGEX/ = REPLACEMENT++       or --alias '/REGEX/=REPLACEMENT'.++       REGEX  is  a  case-insensitive regular expression.  Anywhere it matches+       inside an account name, the matched part will be replaced  by  REPLACE-+       MENT.   If REGEX contains parenthesised match groups, these can be ref-+       erenced by the usual numeric backreferences in REPLACEMENT.  Note, cur-+       rently  regular  expression  aliases  may  cause noticeable slow-downs.+       (And if you use Ledger on your hledger file, they will be ignored.) Eg:++              alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3+              # rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking"++   Multiple aliases+       You  can  define  as  many aliases as you like using directives or com-+       mand-line options.  Aliases are recursive - each alias sees the  result+       of  applying  previous  ones.   (This  is  different from Ledger, where+       aliases are non-recursive by default).  Aliases are applied in the fol-+       lowing order:++       1. alias  directives,  most recently seen first (recent directives take+          precedence over earlier ones; directives not yet seen are ignored)++       2. alias options, in the order they appear on the command line++   end aliases+       You  can  clear  (forget)  all  currently  defined  aliases  with   the+       end aliases directive:++              end aliases++   account directive+       The  account directive predefines account names, as in Ledger and Bean-+       count.  This may be useful for your own documentation; hledger  doesn't+       make use of it yet.++              ; account ACCT+              ;   OPTIONAL COMMENTS/TAGS...++              account assets:bank:checking+               a comment+               acct-no:12345++              account expenses:food++              ; etc.++   apply account directive+       You  can  specify  a  parent  account  which  will  be prepended to all+       accounts within a section of the journal.  Use  the  apply account  and+       end apply account directives like so:++              apply account home++              2010/1/1+                  food    $10+                  cash++              end apply account++       which is equivalent to:++              2010/01/01+                  home:food           $10+                  home:cash          $-10++       If  end apply account  is  omitted,  the effect lasts to the end of the+       file.  Included files are also affected, eg:++              apply account business+              include biz.journal+              end apply account+              apply account personal+              include personal.journal++       Prior to hledger 1.0, legacy account and end spellings were  also  sup-+       ported.++   Multi-line comments+       A  line containing just comment starts a multi-line comment, and a line+       containing just end comment ends it.  See comments.++   commodity directive+       The commodity directive predefines commodities (currently this is  just+       informational),  and  also it may define the display format for amounts+       in this commodity (overriding the automatically inferred format).++       It may be written on a single line, like this:++              ; commodity EXAMPLEAMOUNT++              ; display AAAA amounts with the symbol on the right, space-separated,+              ; using period as decimal point, with four decimal places, and+              ; separating thousands with comma.+              commodity 1,000.0000 AAAA++       or on multiple lines, using the "format" subdirective.   In  this  case+       the  commodity  symbol  appears  twice  and  should be the same in both+       places:++              ; commodity SYMBOL+              ;   format EXAMPLEAMOUNT++              ; display indian rupees with currency name on the left,+              ; thousands, lakhs and crores comma-separated,+              ; period as decimal point, and two decimal places.+              commodity INR+                format INR 9,99,99,999.00++   Default commodity+       The D directive sets a default commodity (and display  format),  to  be+       used for amounts without a commodity symbol (ie, plain numbers).  (Note+       this differs from Ledger's default commodity directive.) The  commodity+       and  display  format  will  be applied to all subsequent commodity-less+       amounts, or until the next D directive.++              # commodity-less amounts should be treated as dollars+              # (and displayed with symbol on the left, thousands separators and two decimal places)+              D $1,000.00++              1/1+                a     5    # <- commodity-less amount, becomes $1+                b++   Default year+       You can set a default year to be used for subsequent dates which  don't+       specify  a year.  This is a line beginning with Y followed by the year.+       Eg:++              Y2009      ; set default year to 2009++              12/15      ; equivalent to 2009/12/15+                expenses  1+                assets++              Y2010      ; change default year to 2010++              2009/1/30  ; specifies the year, not affected+                expenses  1+                assets++              1/31       ; equivalent to 2010/1/31+                expenses  1+                assets++   Including other files+       You can pull in the content of additional journal files by  writing  an+       include directive, like this:++              include path/to/file.journal++       If  the path does not begin with a slash, it is relative to the current+       file.  Glob patterns (*) are not currently supported.++       The include directive can only  be  used  in  journal  files.   It  can+       include journal, timeclock or timedot files, but not CSV files.++EDITOR SUPPORT+       Add-on modes exist for various text editors, to make working with jour-+       nal files easier.  They add colour, navigation aids  and  helpful  com-+       mands.   For  hledger  users  who  edit  the journal file directly (the+       majority), using one of these modes is quite recommended.++       These were written with Ledger in mind,  but  also  work  with  hledger+       files:+++       Emacs              http://www.ledger-cli.org/3.0/doc/ledger-mode.html+       Vim                https://github.com/ledger/ledger/wiki/Get-+                          ting-started+       Sublime Text       https://github.com/ledger/ledger/wiki/Using-Sub-+                          lime-Text+       Textmate           https://github.com/ledger/ledger/wiki/Using-Text-+                          Mate-2+       Text Wrangler      https://github.com/ledger/ledger/wiki/Edit-+                          ing-Ledger-files-with-TextWrangler++++REPORTING BUGS+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel+       or hledger mail list)+++AUTHORS+       Simon Michael <simon@joyful.com> and contributors+++COPYRIGHT+       Copyright (C) 2007-2016 Simon Michael.+       Released under GNU GPL v3 or later.+++SEE ALSO+       hledger(1),     hledger-ui(1),     hledger-web(1),      hledger-api(1),+       hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_time-+       dot(5), ledger(1)++       http://hledger.org++++hledger 1.0                      October 2016               hledger_journal(5)
+ doc/hledger_timeclock.5 view
@@ -0,0 +1,100 @@++.TH "hledger_timeclock" "5" "October 2016" "hledger 1.0" "hledger User Manuals"++++.SH NAME+.PP+Timeclock \- the time logging format of timeclock.el, as read by hledger+.SH DESCRIPTION+.PP+hledger can read timeclock files.+As with Ledger, these are (a subset of) timeclock.el\[aq]s format,+containing clock\-in and clock\-out entries as in the example below.+The date is a simple date.+The time format is HH:MM[:SS][+\-ZZZZ].+Seconds and timezone are optional.+The timezone, if present, must be four digits and is ignored (currently+the time is always interpreted as a local time).+.IP+.nf+\f[C]+i\ 2015/03/30\ 09:00:00\ some:account\ name\ \ optional\ description\ after\ two\ spaces+o\ 2015/03/30\ 09:20:00+i\ 2015/03/31\ 22:21:45\ another\ account+o\ 2015/04/01\ 02:00:34+\f[]+.fi+.PP+hledger treats each clock\-in/clock\-out pair as a transaction posting+some number of hours to an account.+Or if the session spans more than one day, it is split into several+transactions, one for each day.+For the above time log, \f[C]hledger\ print\f[] generates these journal+entries:+.IP+.nf+\f[C]+$\ hledger\ \-f\ t.timeclock\ print+2015/03/30\ *\ optional\ description\ after\ two\ spaces+\ \ \ \ (some:account\ name)\ \ \ \ \ \ \ \ \ 0.33h++2015/03/31\ *\ 22:21\-23:59+\ \ \ \ (another\ account)\ \ \ \ \ \ \ \ \ 1.64h++2015/04/01\ *\ 00:00\-02:00+\ \ \ \ (another\ account)\ \ \ \ \ \ \ \ \ 2.01h+\f[]+.fi+.PP+Here is a sample.timeclock to download and some queries to try:+.IP+.nf+\f[C]+$\ hledger\ \-f\ sample.timeclock\ balance\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ current\ time\ balances+$\ hledger\ \-f\ sample.timeclock\ register\ \-p\ 2009/3\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ sessions\ in\ march\ 2009+$\ hledger\ \-f\ sample.timeclock\ register\ \-p\ weekly\ \-\-depth\ 1\ \-\-empty\ \ #\ time\ summary\ by\ week+\f[]+.fi+.PP+To generate time logs, ie to clock in and clock out, you could:+.IP \[bu] 2+use emacs and the built\-in timeclock.el, or the extended+timeclock\-x.el and perhaps the extras in ledgerutils.el+.IP \[bu] 2+at the command line, use these bash aliases:+.RS 2+.IP+.nf+\f[C]+alias\ ti="echo\ i\ `date\ \[aq]+%Y\-%m\-%d\ %H:%M:%S\[aq]`\ \\$*\ >>$TIMELOG"+alias\ to="echo\ o\ `date\ \[aq]+%Y\-%m\-%d\ %H:%M:%S\[aq]`\ >>$TIMELOG"+\f[]+.fi+.RE+.IP \[bu] 2+or use the old \f[C]ti\f[] and \f[C]to\f[] scripts in the ledger 2.x+repository.+These rely on a "timeclock" executable which I think is just the ledger+2 executable renamed.+++.SH "REPORTING BUGS"+Report bugs at http://bugs.hledger.org+(or on the #hledger IRC channel or hledger mail list)++.SH AUTHORS+Simon Michael <simon@joyful.com> and contributors++.SH COPYRIGHT++Copyright (C) 2007-2016 Simon Michael.+.br+Released under GNU GPL v3 or later.++.SH SEE ALSO+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),+ledger(1)++http://hledger.org
+ doc/hledger_timeclock.5.info view
@@ -0,0 +1,67 @@+This is hledger-lib/doc/hledger_timeclock.5.info, produced by makeinfo+version 4.8 from stdin.+++File: hledger_timeclock.5.info,  Node: Top,  Up: (dir)++hledger_timeclock(5) hledger 1.0+********************************++hledger can read timeclock files. As with Ledger, these are (a subset+of) timeclock.el's format, containing clock-in and clock-out entries as+in the example below. The date is a simple date. The time format is+HH:MM[:SS][+-ZZZZ]. Seconds and timezone are optional. The timezone, if+present, must be four digits and is ignored (currently the time is+always interpreted as a local time).+++i 2015/03/30 09:00:00 some:account name  optional description after two spaces+o 2015/03/30 09:20:00+i 2015/03/31 22:21:45 another account+o 2015/04/01 02:00:34++   hledger treats each clock-in/clock-out pair as a transaction posting+some number of hours to an account. Or if the session spans more than+one day, it is split into several transactions, one for each day. For+the above time log, `hledger print' generates these journal entries:+++$ hledger -f t.timeclock print+2015/03/30 * optional description after two spaces+    (some:account name)         0.33h++2015/03/31 * 22:21-23:59+    (another account)         1.64h++2015/04/01 * 00:00-02:00+    (another account)         2.01h++   Here is a sample.timeclock to download and some queries to try:+++$ hledger -f sample.timeclock balance                               # current time balances+$ hledger -f sample.timeclock register -p 2009/3                    # sessions in march 2009+$ hledger -f sample.timeclock register -p weekly --depth 1 --empty  # time summary by week++   To generate time logs, ie to clock in and clock out, you could:++   * use emacs and the built-in timeclock.el, or the extended+     timeclock-x.el and perhaps the extras in ledgerutils.el++   * at the command line, use these bash aliases:+++     alias ti="echo i `date '+%Y-%m-%d %H:%M:%S'` \$* >>$TIMELOG"+     alias to="echo o `date '+%Y-%m-%d %H:%M:%S'` >>$TIMELOG"++   * or use the old `ti' and `to' scripts in the ledger 2.x repository.+     These rely on a "timeclock" executable which I think is just the+     ledger 2 executable renamed.+++++Tag Table:+Node: Top96++End Tag Table
+ doc/hledger_timeclock.5.txt view
@@ -0,0 +1,82 @@++hledger_timeclock(5)         hledger User Manuals         hledger_timeclock(5)++++NAME+       Timeclock - the time logging format of timeclock.el, as read by hledger++DESCRIPTION+       hledger can read timeclock files.  As with Ledger, these are (a  subset+       of) timeclock.el's format, containing clock-in and clock-out entries as+       in the example below.  The date is a simple date.  The time  format  is+       HH:MM[:SS][+-ZZZZ].   Seconds and timezone are optional.  The timezone,+       if present, must be four digits and is ignored (currently the  time  is+       always interpreted as a local time).++              i 2015/03/30 09:00:00 some:account name  optional description after two spaces+              o 2015/03/30 09:20:00+              i 2015/03/31 22:21:45 another account+              o 2015/04/01 02:00:34++       hledger  treats  each  clock-in/clock-out pair as a transaction posting+       some number of hours to an account.  Or if the session spans more  than+       one  day, it is split into several transactions, one for each day.  For+       the above time log, hledger print generates these journal entries:++              $ hledger -f t.timeclock print+              2015/03/30 * optional description after two spaces+                  (some:account name)         0.33h++              2015/03/31 * 22:21-23:59+                  (another account)         1.64h++              2015/04/01 * 00:00-02:00+                  (another account)         2.01h++       Here is a sample.timeclock to download and some queries to try:++              $ hledger -f sample.timeclock balance                               # current time balances+              $ hledger -f sample.timeclock register -p 2009/3                    # sessions in march 2009+              $ hledger -f sample.timeclock register -p weekly --depth 1 --empty  # time summary by week++       To generate time logs, ie to clock in and clock out, you could:++       o use emacs and  the  built-in  timeclock.el,  or  the  extended  time-+         clock-x.el and perhaps the extras in ledgerutils.el++       o at the command line, use these bash aliases:++                alias ti="echo i `date '+%Y-%m-%d %H:%M:%S'` \$* >>$TIMELOG"+                alias to="echo o `date '+%Y-%m-%d %H:%M:%S'` >>$TIMELOG"++       o or use the old ti and to scripts in the ledger 2.x repository.  These+         rely on a "timeclock" executable which I think is just the  ledger  2+         executable renamed.++++REPORTING BUGS+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel+       or hledger mail list)+++AUTHORS+       Simon Michael <simon@joyful.com> and contributors+++COPYRIGHT+       Copyright (C) 2007-2016 Simon Michael.+       Released under GNU GPL v3 or later.+++SEE ALSO+       hledger(1),     hledger-ui(1),     hledger-web(1),      hledger-api(1),+       hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_time-+       dot(5), ledger(1)++       http://hledger.org++++hledger 1.0                      October 2016             hledger_timeclock(5)
+ doc/hledger_timedot.5 view
@@ -0,0 +1,144 @@++.TH "hledger_timedot" "5" "October 2016" "hledger 1.0" "hledger User Manuals"++++.SH NAME+.PP+Timedot \- hledger\[aq]s human\-friendly time logging format+.SH DESCRIPTION+.PP+Timedot is a plain text format for logging dated, categorised quantities+(eg time), supported by hledger.+It is convenient for approximate and retroactive time logging, eg when+the real\-time clock\-in/out required with a timeclock file is too+precise or too interruptive.+It can be formatted like a bar chart, making clear at a glance where+time was spent.+.PP+Though called "timedot", the format does not specify the commodity being+logged, so could represent other dated, quantifiable things.+Eg you could record a single\-entry journal of financial transactions,+perhaps slightly more conveniently than with hledger_journal(5) format.+.SH FILE FORMAT+.PP+A timedot file contains a series of day entries.+A day entry begins with a date, and is followed by category/quantity+pairs, one per line.+Dates are hledger\-style simple dates (see hledger_journal(5)).+Categories are hledger\-style account names, optionally indented.+There must be at least two spaces between the category and the quantity.+Quantities can be written in two ways:+.IP "1." 3+a series of dots (period characters).+Each dot represents "a quarter" \- eg, a quarter hour.+Spaces can be used to group dots into hours, for easier counting.+.IP "2." 3+a number (integer or decimal), representing "units" \- eg, hours.+A good alternative when dots are cumbersome.+(A number also can record negative quantities.)+.PP+Blank lines and lines beginning with #, ; or * are ignored.+An example:+.IP+.nf+\f[C]+#\ on\ this\ day,\ 6h\ was\ spent\ on\ client\ work,\ 1.5h\ on\ haskell\ FOSS\ work,\ etc.+2016/2/1+inc:client1\ \ \ ....\ ....\ ....\ ....\ ....\ ....+fos:haskell\ \ \ ....\ ..\ +biz:research\ \ .++2016/2/2+inc:client1\ \ \ ....\ ....+biz:research\ \ .+\f[]+.fi+.PP+Or with numbers:+.IP+.nf+\f[C]+2016/2/3+inc:client1\ \ \ 4+fos:hledger\ \ \ 3+biz:research\ \ 1+\f[]+.fi+.PP+Reporting:+.IP+.nf+\f[C]+$\ hledger\ \-f\ t.timedot\ print\ date:2016/2/2+2016/02/02\ *+\ \ \ \ (inc:client1)\ \ \ \ \ \ \ \ \ \ 2.00++2016/02/02\ *+\ \ \ \ (biz:research)\ \ \ \ \ \ \ \ \ \ 0.25+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ \-f\ t.timedot\ bal\ \-\-daily\ \-\-tree+Balance\ changes\ in\ 2016/02/01\-2016/02/03:++\ \ \ \ \ \ \ \ \ \ \ \ ||\ \ 2016/02/01d\ \ 2016/02/02d\ \ 2016/02/03d\ +============++========================================+\ biz\ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 1.00\ +\ \ \ research\ ||\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 1.00\ +\ fos\ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 1.50\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ 3.00\ +\ \ \ haskell\ \ ||\ \ \ \ \ \ \ \ \ 1.50\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ \ \ \ 0\ +\ \ \ hledger\ \ ||\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ 3.00\ +\ inc\ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 6.00\ \ \ \ \ \ \ \ \ 2.00\ \ \ \ \ \ \ \ \ 4.00\ +\ \ \ client1\ \ ||\ \ \ \ \ \ \ \ \ 6.00\ \ \ \ \ \ \ \ \ 2.00\ \ \ \ \ \ \ \ \ 4.00\ +\-\-\-\-\-\-\-\-\-\-\-\-++\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\ \ \ \ \ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 7.75\ \ \ \ \ \ \ \ \ 2.25\ \ \ \ \ \ \ \ \ 8.00\ +\f[]+.fi+.PP+I prefer to use period for separating account components.+We can make this work with an account alias:+.IP+.nf+\f[C]+2016/2/4+fos.hledger.timedot\ \ 4+fos.ledger\ \ \ \ \ \ \ \ \ \ \ ..+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ \-f\ t.timedot\ \-\-alias\ /\\\\./=:\ bal\ date:2016/2/4+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4.50\ \ fos+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4.00\ \ \ \ hledger:timedot+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 0.50\ \ \ \ ledger+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4.50+\f[]+.fi+.PP+Here is a sample.timedot.+++.SH "REPORTING BUGS"+Report bugs at http://bugs.hledger.org+(or on the #hledger IRC channel or hledger mail list)++.SH AUTHORS+Simon Michael <simon@joyful.com> and contributors++.SH COPYRIGHT++Copyright (C) 2007-2016 Simon Michael.+.br+Released under GNU GPL v3 or later.++.SH SEE ALSO+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),+ledger(1)++http://hledger.org
+ doc/hledger_timedot.5.info view
@@ -0,0 +1,121 @@+This is hledger-lib/doc/hledger_timedot.5.info, produced by makeinfo+version 4.8 from stdin.+++File: hledger_timedot.5.info,  Node: Top,  Up: (dir)++hledger_timedot(5) hledger 1.0+******************************++Timedot is a plain text format for logging dated, categorised quantities+(eg time), supported by hledger. It is convenient for approximate and+retroactive time logging, eg when the real-time clock-in/out required+with a timeclock file is too precise or too interruptive. It can be+formatted like a bar chart, making clear at a glance where time was+spent.++   Though called "timedot", the format does not specify the commodity+being logged, so could represent other dated, quantifiable things. Eg+you could record a single-entry journal of financial transactions,+perhaps slightly more conveniently than with hledger_journal(5) format.++* Menu:++* FILE FORMAT::+++File: hledger_timedot.5.info,  Node: FILE FORMAT,  Prev: Top,  Up: Top++1 FILE FORMAT+*************++A timedot file contains a series of day entries. A day entry begins with+a date, and is followed by category/quantity pairs, one per line. Dates+are hledger-style simple dates (see hledger_journal(5)). Categories are+hledger-style account names, optionally indented. There must be at least+two spaces between the category and the quantity. Quantities can be+written in two ways:++  1. a series of dots (period characters). Each dot represents "a+     quarter" - eg, a quarter hour. Spaces can be used to group dots+     into hours, for easier counting.++  2. a number (integer or decimal), representing "units" - eg, hours. A+     good alternative when dots are cumbersome. (A number also can+     record negative quantities.)+++   Blank lines and lines beginning with #, ; or * are ignored. An+example:+++# on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.+2016/2/1+inc:client1   .... .... .... .... .... ....+fos:haskell   .... ..+biz:research  .++2016/2/2+inc:client1   .... ....+biz:research  .++   Or with numbers:+++2016/2/3+inc:client1   4+fos:hledger   3+biz:research  1++   Reporting:+++$ hledger -f t.timedot print date:2016/2/2+2016/02/02 *+    (inc:client1)          2.00++2016/02/02 *+    (biz:research)          0.25+++$ hledger -f t.timedot bal --daily --tree+Balance changes in 2016/02/01-2016/02/03:++            ||  2016/02/01d  2016/02/02d  2016/02/03d+============++========================================+ biz        ||         0.25         0.25         1.00+   research ||         0.25         0.25         1.00+ fos        ||         1.50            0         3.00+   haskell  ||         1.50            0            0+   hledger  ||            0            0         3.00+ inc        ||         6.00         2.00         4.00+   client1  ||         6.00         2.00         4.00+------------++----------------------------------------+            ||         7.75         2.25         8.00++   I prefer to use period for separating account components. We can make+this work with an account alias:+++2016/2/4+fos.hledger.timedot  4+fos.ledger           ..+++$ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4+                4.50  fos+                4.00    hledger:timedot+                0.50    ledger+--------------------+                4.50++   Here is a sample.timedot.++++Tag Table:+Node: Top94+Node: FILE FORMAT876+Ref: #file-format979++End Tag Table
+ doc/hledger_timedot.5.txt view
@@ -0,0 +1,123 @@++hledger_timedot(5)           hledger User Manuals           hledger_timedot(5)++++NAME+       Timedot - hledger's human-friendly time logging format++DESCRIPTION+       Timedot  is  a plain text format for logging dated, categorised quanti-+       ties (eg time), supported by hledger.  It is convenient for approximate+       and  retroactive  time  logging,  eg  when  the  real-time clock-in/out+       required with a timeclock file is too precise or too interruptive.   It+       can  be formatted like a bar chart, making clear at a glance where time+       was spent.++       Though called "timedot", the format  does  not  specify  the  commodity+       being  logged, so could represent other dated, quantifiable things.  Eg+       you could record a single-entry journal of financial transactions, per-+       haps slightly more conveniently than with hledger_journal(5) format.++FILE FORMAT+       A  timedot  file  contains a series of day entries.  A day entry begins+       with a date, and is followed by category/quantity pairs, one per  line.+       Dates  are  hledger-style simple dates (see hledger_journal(5)).  Cate-+       gories are hledger-style account  names,  optionally  indented.   There+       must  be  at  least  two  spaces between the category and the quantity.+       Quantities can be written in two ways:++       1. a series of dots (period characters).  Each dot represents "a  quar-+          ter"  -  eg,  a quarter hour.  Spaces can be used to group dots into+          hours, for easier counting.++       2. a number (integer or decimal), representing "units" - eg, hours.   A+          good  alternative  when  dots  are  cumbersome.   (A number also can+          record negative quantities.)++       Blank lines and lines beginning with #, ; or * are ignored.   An  exam-+       ple:++              # on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.+              2016/2/1+              inc:client1   .... .... .... .... .... ....+              fos:haskell   .... ..+              biz:research  .++              2016/2/2+              inc:client1   .... ....+              biz:research  .++       Or with numbers:++              2016/2/3+              inc:client1   4+              fos:hledger   3+              biz:research  1++       Reporting:++              $ hledger -f t.timedot print date:2016/2/2+              2016/02/02 *+                  (inc:client1)          2.00++              2016/02/02 *+                  (biz:research)          0.25++              $ hledger -f t.timedot bal --daily --tree+              Balance changes in 2016/02/01-2016/02/03:++                          ||  2016/02/01d  2016/02/02d  2016/02/03d+              ============++========================================+               biz        ||         0.25         0.25         1.00+                 research ||         0.25         0.25         1.00+               fos        ||         1.50            0         3.00+                 haskell  ||         1.50            0            0+                 hledger  ||            0            0         3.00+               inc        ||         6.00         2.00         4.00+                 client1  ||         6.00         2.00         4.00+              ------------++----------------------------------------+                          ||         7.75         2.25         8.00++       I  prefer to use period for separating account components.  We can make+       this work with an account alias:++              2016/2/4+              fos.hledger.timedot  4+              fos.ledger           ..++              $ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4+                              4.50  fos+                              4.00    hledger:timedot+                              0.50    ledger+              --------------------+                              4.50++       Here is a sample.timedot.++++REPORTING BUGS+       Report bugs at http://bugs.hledger.org (or on the #hledger IRC  channel+       or hledger mail list)+++AUTHORS+       Simon Michael <simon@joyful.com> and contributors+++COPYRIGHT+       Copyright (C) 2007-2016 Simon Michael.+       Released under GNU GPL v3 or later.+++SEE ALSO+       hledger(1),      hledger-ui(1),     hledger-web(1),     hledger-api(1),+       hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_time-+       dot(5), ledger(1)++       http://hledger.org++++hledger 1.0                      October 2016               hledger_timedot(5)
hledger-lib.cabal view
@@ -1,20 +1,20 @@--- This file has been generated from package.yaml by hpack version 0.5.4.+-- This file has been generated from package.yaml by hpack version 0.14.0. -- -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        0.27.1+version:        1.0 stability:      stable category:       Finance synopsis:       Core data types, parsers and functionality for the hledger accounting tools-description:    -    This is a reusable library containing hledger's core functionality.-    hledger is a cross-platform program for tracking money, time, or-    any other commodity, using double-entry accounting and a simple,-    editable file format. It is inspired by and largely compatible-    with ledger(1).  hledger provides command-line, curses and web-    interfaces, and aims to be a reliable, practical tool for daily-    use.+description:    This is a reusable library containing hledger's core functionality.+                .+                hledger is a cross-platform program for tracking money, time, or+                any other commodity, using double-entry accounting and a simple,+                editable file format. It is inspired by and largely compatible+                with ledger(1).  hledger provides command-line, curses and web+                interfaces, and aims to be a reliable, practical tool for daily+                use. license:        GPL license-file:   LICENSE author:         Simon Michael <simon@joyful.com>@@ -23,65 +23,76 @@ bug-reports:    http://bugs.hledger.org cabal-version:  >= 1.10 build-type:     Simple-tested-with:    GHC==7.6.3, GHC==7.8.4, GHC==7.10.3, GHC==8.0.1+tested-with:    GHC==7.10.3, GHC==8.0  extra-source-files:-    CHANGES +    CHANGES+    README +data-files:+    doc/hledger_csv.5+    doc/hledger_csv.5.info+    doc/hledger_csv.5.txt+    doc/hledger_journal.5+    doc/hledger_journal.5.info+    doc/hledger_journal.5.txt+    doc/hledger_timeclock.5+    doc/hledger_timeclock.5.info+    doc/hledger_timeclock.5.txt+    doc/hledger_timedot.5+    doc/hledger_timedot.5.info+    doc/hledger_timedot.5.txt+ source-repository head   type: git   location: https://github.com/simonmichael/hledger -flag double-  manual: True+flag oldtime+  description: If building with time < 1.5, also depend on old-locale. Set automatically by cabal.+  manual: False   default: False-  description:-    Use old Double number representation (instead of Decimal), for testing/benchmarking. -flag old-locale-  default: False-  description: -    A compatibility flag, set automatically by cabal.-    If false then depend on time >= 1.5, -    if true then depend on time < 1.5 together with old-locale.- library-  if flag(double)-    cpp-options: -DDOUBLE   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans   build-depends:-      base >= 4.3 && < 5-    , base-compat >= 0.8.1+      base >=4.3 && <5+    , base-compat >=0.8.1     , array-    , blaze-markup >= 0.5.1+    , blaze-markup >=0.5.1     , bytestring-    , cmdargs >= 0.10 && < 0.11+    , cmdargs >=0.10 && <0.11     , containers     , csv+    , data-default >=0.5     , Decimal     , deepseq     , directory     , filepath+    , megaparsec >=5 && < 5.1     , mtl     , mtl-compat     , old-time-    , parsec >= 3+    , pretty-show >=1.6.4     , regex-tdfa-    , safe >= 0.2-    , split >= 0.1 && < 0.3-    , transformers >= 0.2 && < 0.6+    , safe >=0.2+    , split >=0.1 && <0.3+    , text >=1.2 && <1.3+    , transformers >=0.2 && <0.6     , uglymemo-    , utf8-string >= 0.3.5 && < 1.1+    , utf8-string >=0.3.5 && <1.1     , HUnit--  if impl(ghc >= 7.4)-    build-depends: pretty-show >= 1.6.4--  if flag(old-locale)-    build-depends: time < 1.5, old-locale+    , parsec+    , semigroups+  if impl(ghc <7.6)+    build-depends:+        ghc-prim+  if flag(oldtime)+    build-depends:+        time <1.5+      , old-locale   else-    build-depends: time >= 1.5-+    build-depends:+        time >=1.5   exposed-modules:       Hledger       Hledger.Data@@ -92,17 +103,20 @@       Hledger.Data.Dates       Hledger.Data.Journal       Hledger.Data.Ledger+      Hledger.Data.Period       Hledger.Data.StringFormat       Hledger.Data.Posting       Hledger.Data.RawOptions-      Hledger.Data.TimeLog+      Hledger.Data.Timeclock       Hledger.Data.Transaction       Hledger.Data.Types       Hledger.Query       Hledger.Read+      Hledger.Read.Common       Hledger.Read.CsvReader       Hledger.Read.JournalReader-      Hledger.Read.TimelogReader+      Hledger.Read.TimedotReader+      Hledger.Read.TimeclockReader       Hledger.Reports       Hledger.Reports.ReportOptions       Hledger.Reports.BalanceHistoryReport@@ -117,50 +131,97 @@       Hledger.Utils.Regex       Hledger.Utils.String       Hledger.Utils.Test+      Hledger.Utils.Text       Hledger.Utils.Tree       Hledger.Utils.UTF8IOCompat+  other-modules:+      Paths_hledger_lib   default-language: Haskell2010 -test-suite tests+test-suite doctests   type: exitcode-stdio-1.0-  main-is: suite.hs   hs-source-dirs:       tests   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans+  main-is: doctests.hs   build-depends:-      base >= 4.3 && < 5-    , base-compat >= 0.8.1+      base >=4.3 && <5+    , base-compat >=0.8.1     , array-    , blaze-markup >= 0.5.1+    , blaze-markup >=0.5.1     , bytestring-    , cmdargs >= 0.10 && < 0.11+    , cmdargs >=0.10 && <0.11     , containers     , csv+    , data-default >=0.5     , Decimal     , deepseq     , directory     , filepath+    , megaparsec >=5 && < 5.1     , mtl     , mtl-compat     , old-time-    , parsec >= 3+    , pretty-show >=1.6.4     , regex-tdfa-    , safe >= 0.2-    , split >= 0.1 && < 0.3-    , transformers >= 0.2 && < 0.6+    , safe >=0.2+    , split >=0.1 && <0.3+    , text >=1.2 && <1.3+    , transformers >=0.2 && <0.6     , uglymemo-    , utf8-string >= 0.3.5 && < 1.1+    , utf8-string >=0.3.5 && <1.1     , HUnit+    , doctest >=0.8+    , Glob >=0.7+  if impl(ghc <7.6)+    build-depends:+        ghc-prim+  default-language: Haskell2010++test-suite hunittests+  type: exitcode-stdio-1.0+  main-is: hunittests.hs+  hs-source-dirs:+      tests+  ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans+  build-depends:+      base >=4.3 && <5+    , base-compat >=0.8.1+    , array+    , blaze-markup >=0.5.1+    , bytestring+    , cmdargs >=0.10 && <0.11+    , containers+    , csv+    , data-default >=0.5+    , Decimal+    , deepseq+    , directory+    , filepath+    , megaparsec >=5 && < 5.1+    , mtl+    , mtl-compat+    , old-time+    , pretty-show >=1.6.4+    , regex-tdfa+    , safe >=0.2+    , split >=0.1 && <0.3+    , text >=1.2 && <1.3+    , transformers >=0.2 && <0.6+    , uglymemo+    , utf8-string >=0.3.5 && <1.1+    , HUnit     , hledger-lib     , test-framework     , test-framework-hunit--  if impl(ghc >= 7.4)-    build-depends: pretty-show >= 1.6.4--  if flag(old-locale)-    build-depends: time < 1.5, old-locale+  if impl(ghc <7.6)+    build-depends:+        ghc-prim+  if flag(oldtime)+    build-depends:+        time <1.5+      , old-locale   else-    build-depends: time >= 1.5-+    build-depends:+        time >=1.5   default-language: Haskell2010
+ tests/doctests.hs view
@@ -0,0 +1,9 @@+{-# LANGUAGE PackageImports #-}++import Data.List+import "Glob" System.FilePath.Glob+import Test.DocTest++main = do+  fs <- ("Hledger.hs" :) . filter (not . isInfixOf "/.") <$> glob "Hledger/**/*.hs"+  doctest fs
+ tests/hunittests.hs view
@@ -0,0 +1,6 @@+import Hledger (tests_Hledger)+import Test.Framework.Providers.HUnit (hUnitTestToTests)+import Test.Framework.Runners.Console (defaultMain)++main :: IO ()+main = defaultMain $ hUnitTestToTests tests_Hledger
− tests/suite.hs
@@ -1,6 +0,0 @@-import Hledger (tests_Hledger)-import Test.Framework.Providers.HUnit (hUnitTestToTests)-import Test.Framework.Runners.Console (defaultMain)--main :: IO ()-main = defaultMain $ hUnitTestToTests tests_Hledger