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 +149/−4
- Hledger.hs +8/−12
- Hledger/Data.hs +5/−4
- Hledger/Data/Account.hs +7/−4
- Hledger/Data/AccountName.hs +35/−28
- Hledger/Data/Amount.hs +16/−31
- Hledger/Data/Commodity.hs +11/−5
- Hledger/Data/Dates.hs +176/−209
- Hledger/Data/Journal.hs +206/−110
- Hledger/Data/Ledger.hs +6/−4
- Hledger/Data/Period.hs +262/−0
- Hledger/Data/Posting.hs +19/−14
- Hledger/Data/RawOptions.hs +3/−2
- Hledger/Data/StringFormat.hs +15/−14
- Hledger/Data/TimeLog.hs +0/−142
- Hledger/Data/Timeclock.hs +145/−0
- Hledger/Data/Transaction.hs +42/−19
- Hledger/Data/Types.hs +166/−121
- Hledger/Query.hs +115/−82
- Hledger/Read.hs +114/−107
- Hledger/Read/Common.hs +828/−0
- Hledger/Read/CsvReader.hs +139/−114
- Hledger/Read/JournalReader.hs +722/−1201
- Hledger/Read/TimeclockReader.hs +125/−0
- Hledger/Read/TimedotReader.hs +159/−0
- Hledger/Read/TimelogReader.hs +0/−116
- Hledger/Reports.hs +2/−2
- Hledger/Reports/BalanceHistoryReport.hs +1/−0
- Hledger/Reports/BalanceReport.hs +105/−82
- Hledger/Reports/MultiBalanceReports.hs +53/−36
- Hledger/Reports/PostingsReport.hs +79/−41
- Hledger/Reports/ReportOptions.hs +162/−137
- Hledger/Reports/TransactionsReports.hs +116/−88
- Hledger/Utils.hs +40/−8
- Hledger/Utils/Debug.hs +59/−61
- Hledger/Utils/Parse.hs +43/−17
- Hledger/Utils/String.hs +4/−21
- Hledger/Utils/Test.hs +5/−4
- Hledger/Utils/Text.hs +414/−0
- Hledger/Utils/Tree.hs +1/−1
- README +3/−0
- doc/hledger_csv.5 +239/−0
- doc/hledger_csv.5.info +231/−0
- doc/hledger_csv.5.txt +169/−0
- doc/hledger_journal.5 +926/−0
- doc/hledger_journal.5.info +995/−0
- doc/hledger_journal.5.txt +700/−0
- doc/hledger_timeclock.5 +100/−0
- doc/hledger_timeclock.5.info +67/−0
- doc/hledger_timeclock.5.txt +82/−0
- doc/hledger_timedot.5 +144/−0
- doc/hledger_timedot.5.info +121/−0
- doc/hledger_timedot.5.txt +123/−0
- hledger-lib.cabal +124/−63
- tests/doctests.hs +9/−0
- tests/hunittests.hs +6/−0
- tests/suite.hs +0/−6
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