packages feed

hledger-lib 1.10 → 1.11

raw patch · 59 files changed

+3437/−3065 lines, 59 filesdep +call-stackdep +cassavadep +cassava-megaparsecdep −HUnitdep −csvdep −test-frameworkdep ~Globdep ~doctestPVP ok

version bump matches the API change (PVP)

Dependencies added: call-stack, cassava, cassava-megaparsec

Dependencies removed: HUnit, csv, test-framework, test-framework-hunit

Dependency ranges changed: Glob, doctest

API changes (from Hackage documentation)

- Hledger: (@@) :: Amount -> Amount -> Amount
- Hledger: ALDefault :: AccountListMode
- Hledger: ALFlat :: AccountListMode
- Hledger: ALTree :: AccountListMode
- Hledger: Account :: AccountName -> Maybe AccountCode -> MixedAmount -> [Account] -> Int -> MixedAmount -> Maybe Account -> Bool -> Account
- Hledger: AccountField :: ReportItemField
- Hledger: Acct :: Regexp -> Query
- Hledger: Amount :: CommoditySymbol -> Quantity -> Price -> AmountStyle -> Bool -> Amount
- Hledger: AmountStyle :: Side -> Bool -> !Int -> Maybe Char -> Maybe DigitGroupStyle -> AmountStyle
- Hledger: Amt :: OrdPlus -> Quantity -> Query
- Hledger: And :: [Query] -> Query
- Hledger: Any :: Query
- Hledger: BalancedVirtualPosting :: PostingType
- Hledger: BasicAlias :: AccountName -> AccountName -> AccountAlias
- Hledger: Black :: Color
- Hledger: Blue :: Color
- Hledger: BottomAligned :: [StringFormatComponent] -> StringFormat
- Hledger: Cleared :: Status
- Hledger: Code :: Regexp -> Query
- Hledger: Commodity :: CommoditySymbol -> Maybe AmountStyle -> Commodity
- Hledger: CumulativeChange :: BalanceType
- Hledger: Cyan :: Color
- Hledger: Date :: DateSpan -> Query
- Hledger: Date2 :: DateSpan -> Query
- Hledger: DateSpan :: (Maybe Day) -> (Maybe Day) -> DateSpan
- Hledger: DayOfMonth :: Int -> Interval
- Hledger: DayOfWeek :: Int -> Interval
- Hledger: DayOfYear :: Int -> Int -> Interval
- Hledger: DayPeriod :: Day -> Period
- Hledger: Days :: Int -> Interval
- Hledger: DefaultDateField :: ReportItemField
- Hledger: Depth :: Int -> Query
- Hledger: DepthSpacerField :: ReportItemField
- Hledger: Desc :: Regexp -> Query
- Hledger: DescriptionField :: ReportItemField
- Hledger: DigitGroups :: Char -> [Int] -> DigitGroupStyle
- Hledger: Dull :: ColorIntensity
- Hledger: Empty :: Bool -> Query
- Hledger: FieldNo :: Int -> ReportItemField
- Hledger: FinalOut :: TimeclockCode
- Hledger: FormatField :: Bool -> (Maybe Int) -> (Maybe Int) -> ReportItemField -> StringFormatComponent
- Hledger: FormatLiteral :: String -> StringFormatComponent
- Hledger: GenericSourcePos :: FilePath -> Int -> Int -> GenericSourcePos
- Hledger: Green :: Color
- Hledger: HistoricalBalance :: BalanceType
- Hledger: In :: TimeclockCode
- Hledger: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> Bool -> String -> Bool -> InputOpts
- Hledger: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(AccountName, Maybe AccountCode)] -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [MarketPrice] -> [ModifierTransaction] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
- Hledger: JournalSourcePos :: FilePath -> (Int, Int) -> GenericSourcePos
- Hledger: L :: Side
- Hledger: Ledger :: Journal -> [Account] -> Ledger
- Hledger: Magenta :: Color
- Hledger: MarketPrice :: Day -> CommoditySymbol -> Amount -> MarketPrice
- Hledger: Mixed :: [Amount] -> MixedAmount
- Hledger: ModifierTransaction :: Text -> [Posting] -> ModifierTransaction
- Hledger: MonthPeriod :: Year -> Month -> Period
- Hledger: Months :: Int -> Interval
- Hledger: MultiBalanceReport :: ([DateSpan], [MultiBalanceReportRow], MultiBalanceReportTotals) -> MultiBalanceReport
- Hledger: NoInterval :: Interval
- Hledger: NoPrice :: Price
- Hledger: None :: Query
- Hledger: NormallyNegative :: NormalSign
- Hledger: NormallyPositive :: NormalSign
- Hledger: Not :: Query -> Query
- Hledger: OneLine :: [StringFormatComponent] -> StringFormat
- Hledger: Or :: [Query] -> Query
- Hledger: Out :: TimeclockCode
- Hledger: Pending :: Status
- Hledger: PeriodAll :: Period
- Hledger: PeriodBetween :: Day -> Day -> Period
- Hledger: PeriodChange :: BalanceType
- Hledger: PeriodFrom :: Day -> Period
- Hledger: PeriodTo :: Day -> Period
- Hledger: PeriodicReport :: ([DateSpan], [PeriodicReportRow a], PeriodicReportRow a) -> PeriodicReport a
- Hledger: PeriodicTransaction :: Text -> Interval -> DateSpan -> Status -> Text -> Text -> Text -> [Tag] -> [Posting] -> PeriodicTransaction
- Hledger: Posting :: Maybe Day -> Maybe Day -> Status -> AccountName -> MixedAmount -> Text -> PostingType -> [Tag] -> BalanceAssertion -> Maybe Transaction -> Maybe Posting -> Posting
- Hledger: PrimaryDate :: WhichDate
- Hledger: QuarterPeriod :: Year -> Quarter -> Period
- Hledger: Quarters :: Int -> Interval
- Hledger: QueryOptInAcct :: AccountName -> QueryOpt
- Hledger: QueryOptInAcctOnly :: AccountName -> QueryOpt
- Hledger: R :: Side
- Hledger: Reader :: StorageFormat -> [String] -> InputOpts -> FilePath -> Text -> ExceptT String IO Journal -> Bool -> Reader
- Hledger: Real :: Bool -> Query
- Hledger: Red :: Color
- Hledger: RegexAlias :: Regexp -> Replacement -> AccountAlias
- Hledger: RegularPosting :: PostingType
- Hledger: ReportOpts :: Period -> Interval -> [Status] -> Bool -> Maybe Int -> Maybe DisplayExp -> Bool -> Bool -> Bool -> Bool -> Maybe FormatStr -> String -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> ReportOpts
- Hledger: SecondaryDate :: WhichDate
- Hledger: SetBalance :: TimeclockCode
- Hledger: SetRequiredHours :: TimeclockCode
- Hledger: StatusQ :: Status -> Query
- Hledger: Sym :: Regexp -> Query
- Hledger: T :: (Map a (FastTree a)) -> FastTree a
- Hledger: Tag :: Regexp -> (Maybe Regexp) -> Query
- Hledger: TimeclockEntry :: GenericSourcePos -> TimeclockCode -> LocalTime -> AccountName -> Text -> TimeclockEntry
- Hledger: TopAligned :: [StringFormatComponent] -> StringFormat
- Hledger: TotalField :: ReportItemField
- Hledger: TotalPrice :: Amount -> Price
- Hledger: Transaction :: Integer -> GenericSourcePos -> Day -> Maybe Day -> Status -> Text -> Text -> Text -> [Tag] -> [Posting] -> Text -> Transaction
- Hledger: UnitPrice :: Amount -> Price
- Hledger: Unmarked :: Status
- Hledger: VirtualPosting :: PostingType
- Hledger: Vivid :: ColorIntensity
- Hledger: WeekPeriod :: Day -> Period
- Hledger: WeekdayOfMonth :: Int -> Int -> Interval
- Hledger: Weeks :: Int -> Interval
- Hledger: White :: Color
- Hledger: YearPeriod :: Year -> Period
- Hledger: Years :: Int -> Interval
- Hledger: Yellow :: Color
- Hledger: [aboring] :: Account -> Bool
- Hledger: [accountlistmode_] :: ReportOpts -> AccountListMode
- Hledger: [acode] :: Account -> Maybe AccountCode
- Hledger: [acommodity] :: Amount -> CommoditySymbol
- Hledger: [aebalance] :: Account -> MixedAmount
- Hledger: [aibalance] :: Account -> MixedAmount
- Hledger: [aliases_] :: InputOpts -> [String]
- Hledger: [amultiplier] :: Amount -> Bool
- Hledger: [aname] :: Account -> AccountName
- Hledger: [anon_] :: InputOpts -> Bool
- Hledger: [anumpostings] :: Account -> Int
- Hledger: [aparent] :: Account -> Maybe Account
- Hledger: [aprice] :: Amount -> Price
- Hledger: [aquantity] :: Amount -> Quantity
- Hledger: [ascommodityside] :: AmountStyle -> Side
- Hledger: [ascommodityspaced] :: AmountStyle -> Bool
- Hledger: [asdecimalpoint] :: AmountStyle -> Maybe Char
- Hledger: [asdigitgroups] :: AmountStyle -> Maybe DigitGroupStyle
- Hledger: [asprecision] :: AmountStyle -> !Int
- Hledger: [astyle] :: Amount -> AmountStyle
- Hledger: [asubs] :: Account -> [Account]
- Hledger: [auto_] :: InputOpts -> Bool
- Hledger: [average_] :: ReportOpts -> Bool
- Hledger: [balancetype_] :: ReportOpts -> BalanceType
- Hledger: [cformat] :: Commodity -> Maybe AmountStyle
- Hledger: [color_] :: ReportOpts -> Bool
- Hledger: [cost_] :: ReportOpts -> Bool
- Hledger: [csymbol] :: Commodity -> CommoditySymbol
- Hledger: [date2_] :: ReportOpts -> Bool
- Hledger: [depth_] :: ReportOpts -> Maybe Int
- Hledger: [display_] :: ReportOpts -> Maybe DisplayExp
- Hledger: [drop_] :: ReportOpts -> Int
- Hledger: [empty_] :: ReportOpts -> Bool
- Hledger: [forecast_] :: ReportOpts -> Bool
- Hledger: [format_] :: ReportOpts -> Maybe FormatStr
- Hledger: [ignore_assertions_] :: InputOpts -> Bool
- Hledger: [interval_] :: ReportOpts -> Interval
- Hledger: [invert_] :: ReportOpts -> Bool
- Hledger: [jaccounts] :: Journal -> [(AccountName, Maybe AccountCode)]
- Hledger: [jcommodities] :: Journal -> Map CommoditySymbol Commodity
- Hledger: [jfiles] :: Journal -> [(FilePath, Text)]
- Hledger: [jfinalcommentlines] :: Journal -> Text
- Hledger: [jinferredcommodities] :: Journal -> Map CommoditySymbol AmountStyle
- Hledger: [jlastreadtime] :: Journal -> ClockTime
- Hledger: [jmarketprices] :: Journal -> [MarketPrice]
- Hledger: [jmodifiertxns] :: Journal -> [ModifierTransaction]
- Hledger: [jparsealiases] :: Journal -> [AccountAlias]
- Hledger: [jparsedefaultcommodity] :: Journal -> Maybe (CommoditySymbol, AmountStyle)
- Hledger: [jparsedefaultyear] :: Journal -> Maybe Year
- Hledger: [jparseparentaccounts] :: Journal -> [AccountName]
- Hledger: [jparsetimeclockentries] :: Journal -> [TimeclockEntry]
- Hledger: [jperiodictxns] :: Journal -> [PeriodicTransaction]
- Hledger: [jtxns] :: Journal -> [Transaction]
- Hledger: [laccounts] :: Ledger -> [Account]
- Hledger: [ljournal] :: Ledger -> Journal
- Hledger: [mformat_] :: InputOpts -> Maybe StorageFormat
- Hledger: [mpamount] :: MarketPrice -> Amount
- Hledger: [mpcommodity] :: MarketPrice -> CommoditySymbol
- Hledger: [mpdate] :: MarketPrice -> Day
- Hledger: [mrules_file_] :: InputOpts -> Maybe FilePath
- Hledger: [mtpostings] :: ModifierTransaction -> [Posting]
- Hledger: [mtvalueexpr] :: ModifierTransaction -> Text
- Hledger: [new_] :: InputOpts -> Bool
- Hledger: [new_save_] :: InputOpts -> Bool
- Hledger: [no_elide_] :: ReportOpts -> Bool
- Hledger: [no_total_] :: ReportOpts -> Bool
- Hledger: [normalbalance_] :: ReportOpts -> Maybe NormalSign
- Hledger: [paccount] :: Posting -> AccountName
- Hledger: [pamount] :: Posting -> MixedAmount
- Hledger: [pbalanceassertion] :: Posting -> BalanceAssertion
- Hledger: [pcomment] :: Posting -> Text
- Hledger: [pdate2] :: Posting -> Maybe Day
- Hledger: [pdate] :: Posting -> Maybe Day
- Hledger: [period_] :: ReportOpts -> Period
- Hledger: [pivot_] :: InputOpts -> String
- Hledger: [porigin] :: Posting -> Maybe Posting
- Hledger: [pretty_tables_] :: ReportOpts -> Bool
- Hledger: [pstatus] :: Posting -> Status
- Hledger: [ptags] :: Posting -> [Tag]
- Hledger: [ptcode] :: PeriodicTransaction -> Text
- Hledger: [ptcomment] :: PeriodicTransaction -> Text
- Hledger: [ptdescription] :: PeriodicTransaction -> Text
- Hledger: [ptinterval] :: PeriodicTransaction -> Interval
- Hledger: [ptperiodexpr] :: PeriodicTransaction -> Text
- Hledger: [ptpostings] :: PeriodicTransaction -> [Posting]
- Hledger: [ptransaction] :: Posting -> Maybe Transaction
- Hledger: [ptspan] :: PeriodicTransaction -> DateSpan
- Hledger: [ptstatus] :: PeriodicTransaction -> Status
- Hledger: [pttags] :: PeriodicTransaction -> [Tag]
- Hledger: [ptype] :: Posting -> PostingType
- Hledger: [query_] :: ReportOpts -> String
- Hledger: [rExperimental] :: Reader -> Bool
- Hledger: [rExtensions] :: Reader -> [String]
- Hledger: [rFormat] :: Reader -> StorageFormat
- Hledger: [rParser] :: Reader -> InputOpts -> FilePath -> Text -> ExceptT String IO Journal
- Hledger: [real_] :: ReportOpts -> Bool
- Hledger: [related_] :: ReportOpts -> Bool
- Hledger: [row_total_] :: ReportOpts -> Bool
- Hledger: [sort_amount_] :: ReportOpts -> Bool
- Hledger: [statuses_] :: ReportOpts -> [Status]
- Hledger: [tcode] :: Transaction -> Text
- Hledger: [tcomment] :: Transaction -> Text
- Hledger: [tdate2] :: Transaction -> Maybe Day
- Hledger: [tdate] :: Transaction -> Day
- Hledger: [tdescription] :: Transaction -> Text
- Hledger: [tindex] :: Transaction -> Integer
- Hledger: [tlaccount] :: TimeclockEntry -> AccountName
- Hledger: [tlcode] :: TimeclockEntry -> TimeclockCode
- Hledger: [tldatetime] :: TimeclockEntry -> LocalTime
- Hledger: [tldescription] :: TimeclockEntry -> Text
- Hledger: [tlsourcepos] :: TimeclockEntry -> GenericSourcePos
- Hledger: [tpostings] :: Transaction -> [Posting]
- Hledger: [tpreceding_comment_lines] :: Transaction -> Text
- Hledger: [tsourcepos] :: Transaction -> GenericSourcePos
- Hledger: [tstatus] :: Transaction -> Status
- Hledger: [ttags] :: Transaction -> [Tag]
- Hledger: [value_] :: ReportOpts -> Bool
- Hledger: accountCodeAndNameForSort :: Account -> (AccountCode, AccountName)
- Hledger: accountLeafName :: AccountName -> Text
- Hledger: accountNameApplyAliases :: [AccountAlias] -> AccountName -> AccountName
- Hledger: accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> AccountName
- Hledger: accountNameComponents :: AccountName -> [Text]
- Hledger: accountNameDrop :: Int -> AccountName -> AccountName
- Hledger: accountNameFromComponents :: [Text] -> AccountName
- Hledger: accountNameLevel :: AccountName -> Int
- Hledger: accountNamePostingType :: AccountName -> PostingType
- Hledger: accountNameToAccountOnlyRegex :: AccountName -> Regexp
- Hledger: accountNameToAccountRegex :: AccountName -> Regexp
- Hledger: accountNameTreeFrom :: [AccountName] -> Tree AccountName
- Hledger: accountNameWithPostingType :: PostingType -> AccountName -> AccountName
- Hledger: accountNameWithoutPostingType :: AccountName -> AccountName
- Hledger: accountNamesFromPostings :: [Posting] -> [AccountName]
- Hledger: accountRegexToAccountName :: Regexp -> AccountName
- Hledger: accountSetCodeFrom :: Journal -> Account -> Account
- Hledger: accountSummarisedName :: AccountName -> Text
- Hledger: accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport
- Hledger: accountnamep :: TextParser m AccountName
- Hledger: accountsFromPostings :: [Posting] -> [Account]
- Hledger: accountsLevels :: Account -> [[Account]]
- Hledger: acctsep :: Text
- Hledger: acctsepchar :: Char
- Hledger: addAccountAlias :: MonadState Journal m => AccountAlias -> m ()
- Hledger: addMarketPrice :: MarketPrice -> Journal -> Journal
- Hledger: addModifierTransaction :: ModifierTransaction -> Journal -> Journal
- Hledger: addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal
- Hledger: addTransaction :: Transaction -> Journal -> Journal
- Hledger: amount :: Amount
- Hledger: amountValue :: Journal -> Day -> Amount -> Amount
- Hledger: amountWithCommodity :: CommoditySymbol -> Amount -> Amount
- Hledger: amountp :: JournalParser m Amount
- Hledger: amountp' :: String -> Amount
- Hledger: amounts :: MixedAmount -> [Amount]
- Hledger: amountstyle :: AmountStyle
- Hledger: anyAccounts :: (Account -> Bool) -> Account -> Bool
- Hledger: applyN :: Int -> (a -> a) -> a -> a
- Hledger: assertParse :: (Show t, Show e) => (Either (ParseError t e) a) -> Assertion
- Hledger: assertParseEqual :: (Show a, Eq a, Show t, Show e) => (Either (ParseError t e) a) -> a -> Assertion
- Hledger: assertParseFailure :: (Either (ParseError t e) a) -> Assertion
- Hledger: assignmentPostings :: Transaction -> [Posting]
- Hledger: at :: Amount -> Amount -> Amount
- Hledger: averageMixedAmounts :: [MixedAmount] -> MixedAmount
- Hledger: balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
- Hledger: balanceReportFromMultiBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
- Hledger: balanceTransaction :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Either String Transaction
- Hledger: balanceTransactionUpdate :: MonadError String m => (AccountName -> MixedAmount -> m ()) -> Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> m Transaction
- Hledger: balancedVirtualPostings :: Transaction -> [Posting]
- Hledger: bgColor :: ColorIntensity -> Color -> String -> String
- Hledger: boolopt :: String -> RawOpts -> Bool
- Hledger: bracketeddatetagsp :: Maybe Year -> TextParser m [(TagName, Day)]
- Hledger: branches :: () => Tree a -> Forest a
- Hledger: budgetJournal :: Bool -> ReportOpts -> DateSpan -> Journal -> Journal
- Hledger: budgetReport :: ReportOpts -> Bool -> Bool -> DateSpan -> Day -> Journal -> BudgetReport
- Hledger: budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount)
- Hledger: budgetReportAsText :: ReportOpts -> BudgetReport -> String
- Hledger: budgetReportSpan :: BudgetReport -> DateSpan
- Hledger: budgetRollUp :: [AccountName] -> Bool -> Journal -> Journal
- Hledger: canonicalStyleFrom :: [AmountStyle] -> AmountStyle
- Hledger: canonicaliseAmount :: Map CommoditySymbol AmountStyle -> Amount -> Amount
- Hledger: canonicaliseMixedAmount :: Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
- Hledger: charWidth :: Char -> Int
- Hledger: checkPeriodicTransactionStartDate :: Interval -> DateSpan -> Text -> Maybe String
- Hledger: checkReportOpts :: ReportOpts -> ReportOpts
- Hledger: choice' :: [TextParser m a] -> TextParser m a
- Hledger: choiceInState :: [StateT s (ParsecT CustomErr Text m) a] -> StateT s (ParsecT CustomErr Text m) a
- Hledger: chomp :: String -> String
- Hledger: clearAccountAliases :: MonadState Journal m => m ()
- Hledger: clipAccountName :: Int -> AccountName -> AccountName
- Hledger: clipAccounts :: Int -> Account -> Account
- Hledger: clipAccountsAndAggregate :: Int -> [Account] -> [Account]
- Hledger: clipOrEllipsifyAccountName :: Int -> AccountName -> AccountName
- Hledger: cliptopleft :: Int -> Int -> String -> String
- Hledger: codep :: TextParser m Text
- Hledger: color :: ColorIntensity -> Color -> String -> String
- Hledger: combineBudgetAndActual :: MultiBalanceReport -> MultiBalanceReport -> BudgetReport
- Hledger: comm :: String -> CommoditySymbol
- Hledger: commodity :: [Char]
- Hledger: commodityStylesFromAmounts :: [Amount] -> Map CommoditySymbol AmountStyle
- Hledger: commoditysymbolp :: TextParser m CommoditySymbol
- Hledger: commoditysymbols :: [(String, CommoditySymbol)]
- Hledger: concatAccountNames :: [AccountName] -> AccountName
- Hledger: concatBottomPadded :: [String] -> String
- Hledger: concatOneLine :: [String] -> String
- Hledger: concatTopPadded :: [String] -> String
- Hledger: conversionRate :: CommoditySymbol -> CommoditySymbol -> Double
- Hledger: costOfAmount :: Amount -> Amount
- Hledger: costOfMixedAmount :: MixedAmount -> MixedAmount
- Hledger: cshowAmount :: Amount -> String
- Hledger: cshowMixedAmountOneLineWithoutPrice :: MixedAmount -> String
- Hledger: cshowMixedAmountWithoutPrice :: MixedAmount -> String
- Hledger: data Account
- Hledger: data AccountAlias
- Hledger: data AccountListMode
- Hledger: data Amount
- Hledger: data AmountStyle
- Hledger: data BalanceType
- Hledger: data Color
- Hledger: data ColorIntensity
- Hledger: data Commodity
- Hledger: data CustomErr
- Hledger: data DateSpan
- Hledger: data DigitGroupStyle
- Hledger: data GenericSourcePos
- Hledger: data InputOpts
- Hledger: data Interval
- Hledger: data Journal
- Hledger: data Ledger
- Hledger: data MarketPrice
- Hledger: data ModifierTransaction
- Hledger: data NormalSign
- Hledger: data Period
- Hledger: data PeriodicReport a
- Hledger: data PeriodicTransaction
- Hledger: data Posting
- Hledger: data PostingType
- Hledger: data Price
- Hledger: data Query
- Hledger: data QueryOpt
- Hledger: data Reader
- Hledger: data ReportItemField
- Hledger: data ReportOpts
- Hledger: data Side
- Hledger: data Status
- Hledger: data StringFormat
- Hledger: data StringFormatComponent
- Hledger: data TimeclockCode
- Hledger: data TimeclockEntry
- Hledger: data Transaction
- Hledger: data WhichDate
- Hledger: dateSpanAsPeriod :: DateSpan -> Period
- Hledger: datep :: JournalParser m Day
- Hledger: datesepchar :: TextParser m Char
- Hledger: datesepchars :: [Char]
- Hledger: datetimep :: JournalParser m LocalTime
- Hledger: daysInSpan :: DateSpan -> Maybe Integer
- Hledger: dbg0 :: Show a => String -> a -> a
- Hledger: dbg0IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg1 :: Show a => String -> a -> a
- Hledger: dbg1IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg2 :: Show a => String -> a -> a
- Hledger: dbg2IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg3 :: Show a => String -> a -> a
- Hledger: dbg3IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg4 :: Show a => String -> a -> a
- Hledger: dbg4IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg5 :: Show a => String -> a -> a
- Hledger: dbg5IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg6 :: Show a => String -> a -> a
- Hledger: dbg6IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg7 :: Show a => String -> a -> a
- Hledger: dbg7IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg8 :: Show a => String -> a -> a
- Hledger: dbg8IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbg9 :: Show a => String -> a -> a
- Hledger: dbg9IO :: (MonadIO m, Show a) => String -> a -> m ()
- Hledger: dbgExit :: Show a => String -> a -> a
- Hledger: dbglog :: Show a => String -> a -> a
- Hledger: dbgppshow :: Show a => Int -> String -> a -> a
- Hledger: debugLevel :: Int
- Hledger: defaultJournal :: IO Journal
- Hledger: defaultJournalPath :: IO String
- Hledger: defaultStringFormatStyle :: [StringFormatComponent] -> StringFormat
- Hledger: definputopts :: InputOpts
- Hledger: defreportopts :: ReportOpts
- Hledger: descriptionp :: TextParser m Text
- Hledger: divideAmount :: Amount -> Quantity -> Amount
- Hledger: divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount
- Hledger: elapsedSeconds :: Fractional a => UTCTime -> UTCTime -> a
- Hledger: elideAccountName :: Int -> AccountName -> AccountName
- Hledger: elideLeft :: Int -> String -> String
- Hledger: elideRight :: Int -> String -> String
- Hledger: emptyTree :: () => FastTree a
- Hledger: emptyorcommentlinep :: TextParser m ()
- Hledger: ensureJournalFileExists :: FilePath -> IO ()
- Hledger: entriesReport :: ReportOpts -> Query -> Journal -> EntriesReport
- Hledger: entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction
- Hledger: eolof :: TextParser m ()
- Hledger: error' :: String -> a
- Hledger: escapeDoubleQuotes :: Text -> Text
- Hledger: escapeName :: AccountName -> Regexp
- Hledger: escapeQuotes :: String -> String
- Hledger: eur :: DecimalRaw Integer -> Amount
- Hledger: expandAccountName :: AccountName -> [AccountName]
- Hledger: expandAccountNames :: [AccountName] -> [AccountName]
- Hledger: expandPath :: FilePath -> FilePath -> IO FilePath
- Hledger: failIfInvalidDay :: (Monad m) => String -> m ()
- Hledger: failIfInvalidMonth :: (Monad m) => String -> m ()
- Hledger: failIfInvalidYear :: (Monad m) => String -> m ()
- Hledger: fifth5 :: () => (a, b, c, d, e) -> e
- Hledger: fifth6 :: () => (a, b, c, d, e, f) -> e
- Hledger: filterAccounts :: (Account -> Bool) -> Account -> [Account]
- Hledger: filterJournalAmounts :: Query -> Journal -> Journal
- Hledger: filterJournalPostings :: Query -> Journal -> Journal
- Hledger: filterJournalTransactions :: Query -> Journal -> Journal
- Hledger: filterMixedAmount :: (Amount -> Bool) -> MixedAmount -> MixedAmount
- Hledger: filterMixedAmountByCommodity :: CommoditySymbol -> MixedAmount -> MixedAmount
- Hledger: filterPostingAmount :: Query -> Posting -> Posting
- Hledger: filterQuery :: (Query -> Bool) -> Query -> Query
- Hledger: filterTests :: (Test -> Bool) -> Test -> Test
- Hledger: filterTransactionAmounts :: Query -> Transaction -> Transaction
- Hledger: filterTransactionPostings :: Query -> Transaction -> Transaction
- Hledger: first3 :: () => (a, b, c) -> a
- Hledger: first4 :: () => (a, b, c, d) -> a
- Hledger: first5 :: () => (a, b, c, d, e) -> a
- Hledger: first6 :: () => (a, b, c, d, e, f) -> a
- Hledger: firstJust :: Eq a => [Maybe a] -> Maybe a
- Hledger: firstMonthOfQuarter :: Num a => a -> a
- Hledger: fitString :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String
- Hledger: fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String
- Hledger: fitText :: Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text
- Hledger: fitto :: Int -> Int -> String -> String
- Hledger: fixSmartDate :: Day -> SmartDate -> Day
- Hledger: fixSmartDateStr :: Day -> Text -> String
- Hledger: fixSmartDateStrEither :: Day -> Text -> Either (ParseError Char CustomErr) String
- Hledger: fixSmartDateStrEither' :: Day -> Text -> Either (ParseError Char CustomErr) Day
- Hledger: fixedlotpricep :: JournalParser m (Maybe Amount)
- Hledger: flatShowsExclusiveBalance :: Bool
- Hledger: flat_ :: ReportOpts -> Bool
- Hledger: flattenAccounts :: Account -> [Account]
- Hledger: flattenTests :: Test -> [Test]
- Hledger: followingcommentp :: TextParser m Text
- Hledger: formatString :: Bool -> Maybe Int -> Maybe Int -> String -> String
- Hledger: fourth4 :: () => (a, b, c, d) -> d
- Hledger: fourth5 :: () => (a, b, c, d, e) -> d
- Hledger: fourth6 :: () => (a, b, c, d, e, f) -> d
- Hledger: fromRawNumber :: RawNumber -> Maybe Int -> Either String (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
- Hledger: fromSystemString :: SystemString -> String
- Hledger: fromparse :: (Show t, Show e) => Either (ParseError t e) a -> a
- Hledger: gbp :: DecimalRaw Integer -> Amount
- Hledger: generateAutomaticPostings :: Journal -> Journal
- Hledger: genericSourcePos :: SourcePos -> GenericSourcePos
- Hledger: getAccountAliases :: MonadState Journal m => m [AccountAlias]
- Hledger: getAmountStyle :: CommoditySymbol -> JournalParser m (Maybe AmountStyle)
- Hledger: getCurrentDay :: IO Day
- Hledger: getCurrentLocalTime :: IO LocalTime
- Hledger: getCurrentMonth :: IO Int
- Hledger: getCurrentYear :: IO Integer
- Hledger: getCurrentZonedTime :: IO ZonedTime
- Hledger: getDefaultAmountStyle :: JournalParser m (Maybe AmountStyle)
- Hledger: getDefaultCommodityAndStyle :: JournalParser m (Maybe (CommoditySymbol, AmountStyle))
- Hledger: getParentAccount :: JournalParser m AccountName
- Hledger: getYear :: JournalParser m (Maybe Year)
- Hledger: hasAmount :: Posting -> Bool
- Hledger: hasRealPostings :: Transaction -> Bool
- Hledger: hrs :: Quantity -> Amount
- Hledger: inAccount :: [QueryOpt] -> Maybe (AccountName, Bool)
- Hledger: inAccountQuery :: [QueryOpt] -> Maybe Query
- Hledger: inRawOpts :: String -> RawOpts -> Bool
- Hledger: intervalFromRawOpts :: RawOpts -> Interval
- Hledger: intopt :: String -> RawOpts -> Int
- Hledger: is :: (Eq a, Show a) => a -> a -> Assertion
- Hledger: isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
- Hledger: isAccountRegex :: String -> Bool
- Hledger: isAssignment :: Posting -> Bool
- Hledger: isBalancedVirtual :: Posting -> Bool
- Hledger: isDateSepChar :: Char -> Bool
- Hledger: isEmptyPosting :: Posting -> Bool
- Hledger: isLastDayOfMonth :: (Eq a1, Eq a2, Num a1, Num a2) => Integer -> a1 -> a2 -> Bool
- Hledger: isLeft :: Either a b -> Bool
- Hledger: isNegativeAmount :: Amount -> Bool
- Hledger: isNegativeMixedAmount :: MixedAmount -> Maybe Bool
- Hledger: isNonNewlineSpace :: Char -> Bool
- Hledger: isNonsimpleCommodityChar :: Char -> Bool
- Hledger: isPostingInDateSpan :: DateSpan -> Posting -> Bool
- Hledger: isPostingInDateSpan' :: WhichDate -> DateSpan -> Posting -> Bool
- Hledger: isReal :: Posting -> Bool
- Hledger: isReallyZeroAmount :: Amount -> Bool
- Hledger: isReallyZeroMixedAmount :: MixedAmount -> Bool
- Hledger: isReallyZeroMixedAmountCost :: MixedAmount -> Bool
- Hledger: isRight :: Either a b -> Bool
- Hledger: isStandardPeriod :: Period -> Bool
- Hledger: isSubAccountNameOf :: AccountName -> AccountName -> Bool
- Hledger: isTransactionBalanced :: Maybe (Map CommoditySymbol AmountStyle) -> Transaction -> Bool
- Hledger: isVirtual :: Posting -> Bool
- Hledger: isZeroAmount :: Amount -> Bool
- Hledger: isZeroMixedAmount :: MixedAmount -> Bool
- Hledger: jdatespan :: Journal -> DateSpan
- Hledger: joinAccountNames :: AccountName -> AccountName -> AccountName
- Hledger: journalAccountNames :: Journal -> [AccountName]
- Hledger: journalAccountNamesDeclared :: Journal -> [AccountName]
- Hledger: journalAccountNamesDeclaredOrImplied :: Journal -> [AccountName]
- Hledger: journalAccountNamesDeclaredOrUsed :: Journal -> [AccountName]
- Hledger: journalAccountNamesImplied :: Journal -> [AccountName]
- Hledger: journalAccountNamesUsed :: Journal -> [AccountName]
- Hledger: journalAddFile :: (FilePath, Text) -> Journal -> Journal
- Hledger: journalAmounts :: Journal -> [Amount]
- Hledger: journalApplyCommodityStyles :: Journal -> Journal
- Hledger: journalAssetAccountQuery :: Journal -> Query
- Hledger: journalBalanceSheetAccountQuery :: Journal -> Query
- Hledger: journalBalanceTransactions :: Bool -> Journal -> Either String Journal
- Hledger: journalCashAccountQuery :: Journal -> Query
- Hledger: journalCheckBalanceAssertions :: Journal -> Either String Journal
- Hledger: journalCommodityStyles :: Journal -> Map CommoditySymbol AmountStyle
- Hledger: journalConvertAmountsToCost :: Journal -> Journal
- Hledger: journalDateSpan :: Bool -> Journal -> DateSpan
- Hledger: journalDescriptions :: Journal -> [Text]
- Hledger: journalEquityAccountQuery :: Journal -> Query
- Hledger: journalExpenseAccountQuery :: Journal -> Query
- Hledger: journalFilePath :: Journal -> FilePath
- Hledger: journalFilePaths :: Journal -> [FilePath]
- Hledger: journalFinalise :: ClockTime -> FilePath -> Text -> Bool -> ParsedJournal -> Either String Journal
- Hledger: journalIncomeAccountQuery :: Journal -> Query
- Hledger: journalLiabilityAccountQuery :: Journal -> Query
- Hledger: journalNextTransaction :: Journal -> Transaction -> Maybe Transaction
- Hledger: journalNumberAndTieTransactions :: Journal -> Journal
- Hledger: journalPivot :: Text -> Journal -> Journal
- Hledger: journalPostings :: Journal -> [Posting]
- Hledger: journalPrevTransaction :: Journal -> Transaction -> Maybe Transaction
- Hledger: journalProfitAndLossAccountQuery :: Journal -> Query
- Hledger: journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal
- Hledger: journalSourcePos :: SourcePos -> SourcePos -> GenericSourcePos
- Hledger: journalTransactionAt :: Journal -> Integer -> Maybe Transaction
- Hledger: journalTransactionsReport :: ReportOpts -> Journal -> Query -> TransactionsReport
- Hledger: journalUntieTransactions :: Transaction -> Transaction
- Hledger: leaves :: Tree a -> [a]
- Hledger: ledgerAccount :: Ledger -> AccountName -> Maybe Account
- Hledger: ledgerAccountNames :: Ledger -> [AccountName]
- Hledger: ledgerAccountsMatching :: [String] -> Ledger -> [Account]
- Hledger: ledgerCommodities :: Ledger -> [CommoditySymbol]
- Hledger: ledgerDateSpan :: Ledger -> DateSpan
- Hledger: ledgerFromJournal :: Query -> Journal -> Ledger
- Hledger: ledgerLeafAccounts :: Ledger -> [Account]
- Hledger: ledgerPostings :: Ledger -> [Posting]
- Hledger: ledgerRootAccount :: Ledger -> Account
- Hledger: ledgerTopAccounts :: Ledger -> [Account]
- Hledger: listofstringopt :: String -> RawOpts -> [String]
- Hledger: log0 :: Show a => String -> a -> a
- Hledger: logPrettyAt :: Show a => Int -> String -> a -> a
- Hledger: lookupAccount :: AccountName -> [Account] -> Maybe Account
- Hledger: lowercase :: String -> String
- Hledger: lstrip :: String -> String
- Hledger: ltraceamount :: String -> MixedAmount -> MixedAmount
- Hledger: mamountp' :: String -> MixedAmount
- Hledger: mapAccounts :: (Account -> Account) -> Account -> Account
- Hledger: mapM' :: Monad f => (a -> f b) -> [a] -> f [b]
- Hledger: matchesAccount :: Query -> AccountName -> Bool
- Hledger: matchesAmount :: Query -> Amount -> Bool
- Hledger: matchesMixedAmount :: Query -> MixedAmount -> Bool
- Hledger: matchesPosting :: Query -> Posting -> Bool
- Hledger: matchesTransaction :: Query -> Transaction -> Bool
- Hledger: matchpats :: [String] -> String -> Bool
- Hledger: maximum' :: Integral a => [a] -> a
- Hledger: maximumStrict :: Ord a => [a] -> a
- Hledger: maxprecision :: Int
- Hledger: maxprecisionwithpoint :: Int
- Hledger: maybeAccountNameDrop :: ReportOpts -> AccountName -> AccountName
- Hledger: maybePeriod :: Day -> Text -> Maybe (Interval, DateSpan)
- Hledger: maybeintopt :: String -> RawOpts -> Maybe Int
- Hledger: maybestringopt :: String -> RawOpts -> Maybe String
- Hledger: mbrNegate :: MultiBalanceReport -> MultiBalanceReport
- Hledger: mbrNormaliseSign :: NormalSign -> MultiBalanceReport -> MultiBalanceReport
- Hledger: mergeTrees :: (Ord a) => FastTree a -> FastTree a -> FastTree a
- Hledger: minimumStrict :: Ord a => [a] -> a
- Hledger: missingamt :: Amount
- Hledger: missingmixedamt :: MixedAmount
- Hledger: mixed :: [Amount] -> MixedAmount
- Hledger: mixedAmountValue :: Journal -> Day -> MixedAmount -> MixedAmount
- Hledger: mkdatespan :: String -> String -> DateSpan
- Hledger: mkpostingsReportItem :: Bool -> Bool -> WhichDate -> Maybe Day -> Posting -> MixedAmount -> PostingsReportItem
- Hledger: modifiedaccountnamep :: JournalParser m AccountName
- Hledger: mondayBefore :: Day -> Day
- Hledger: mtvaluequery :: ModifierTransaction -> (Day -> Query)
- Hledger: multiBalanceReport :: ReportOpts -> Query -> Journal -> MultiBalanceReport
- Hledger: multiBalanceReportSpan :: MultiBalanceReport -> DateSpan
- Hledger: multilinecommentp :: TextParser m ()
- Hledger: nameTreeToAccount :: AccountName -> FastTree AccountName -> Account
- Hledger: newtype FastTree a
- Hledger: newtype MixedAmount
- Hledger: newtype MultiBalanceReport
- Hledger: nonsimplecommoditychars :: [Char]
- Hledger: nonspace :: TextParser m Char
- Hledger: normaliseMixedAmount :: MixedAmount -> MixedAmount
- Hledger: normaliseMixedAmountSquashPricesForDisplay :: MixedAmount -> MixedAmount
- Hledger: nullaccountnametree :: IsString a => Tree a
- Hledger: nullacct :: Account
- Hledger: nullamt :: Amount
- Hledger: nulldate :: Day
- Hledger: nulldatespan :: DateSpan
- Hledger: nulljournal :: Journal
- Hledger: nullledger :: Ledger
- Hledger: nullmixedamt :: MixedAmount
- Hledger: nullperiodictransaction :: PeriodicTransaction
- Hledger: nullposting :: Posting
- Hledger: nullsourcepos :: GenericSourcePos
- Hledger: nulltransaction :: Transaction
- Hledger: num :: Quantity -> Amount
- Hledger: numberp :: Maybe AmountStyle -> TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
- Hledger: originalPosting :: Posting -> Posting
- Hledger: overJournalAmounts :: (Amount -> Amount) -> Journal -> Journal
- Hledger: padLeftWide :: Int -> String -> String
- Hledger: padRightWide :: Int -> String -> String
- Hledger: padbottom :: Int -> String -> String
- Hledger: padleft :: Int -> String -> String
- Hledger: padright :: Int -> String -> String
- Hledger: padtop :: Int -> String -> String
- Hledger: parentAccountName :: AccountName -> AccountName
- Hledger: parentAccountNames :: AccountName -> [AccountName]
- Hledger: parentAccounts :: Account -> [Account]
- Hledger: parseAndFinaliseJournal :: JournalParser IO ParsedJournal -> InputOpts -> FilePath -> Text -> ExceptT String IO Journal
- Hledger: parseAndFinaliseJournal' :: JournalParser Identity ParsedJournal -> InputOpts -> FilePath -> Text -> ExceptT String IO Journal
- Hledger: parsePeriodExpr :: Day -> Text -> Either (ParseError Char CustomErr) (Interval, DateSpan)
- Hledger: parsePeriodExpr' :: Day -> Text -> (Interval, DateSpan)
- Hledger: parseQuery :: Day -> Text -> (Query, [QueryOpt])
- Hledger: parseStringFormat :: String -> Either String StringFormat
- Hledger: parseWithState :: Monad m => st -> StateT st (ParsecT CustomErr Text m) a -> Text -> m (Either (ParseError Char CustomErr) a)
- Hledger: parseWithState' :: (Stream s) => st -> StateT st (ParsecT e s Identity) a -> s -> (Either (ParseError (Token s) e) a)
- Hledger: parsedate :: String -> Day
- Hledger: parsedateM :: String -> Maybe Day
- Hledger: parseerror :: (Show t, Show e) => ParseError t e -> a
- Hledger: parsewith :: Parsec e Text a -> Text -> Either (ParseError Char e) a
- Hledger: parsewithString :: Parsec e String a -> String -> Either (ParseError Char e) a
- Hledger: partialbalanceassertionp :: JournalParser m BalanceAssertion
- Hledger: payeeAndNoteFromDescription :: Text -> (Text, Text)
- Hledger: pdbg :: Int -> String -> TextParser m ()
- Hledger: periodAsDateSpan :: Period -> DateSpan
- Hledger: periodContainsDate :: Period -> Day -> Bool
- Hledger: periodEnd :: Period -> Maybe Day
- Hledger: periodGrow :: Period -> Period
- Hledger: periodMoveTo :: Day -> Period -> Period
- Hledger: periodNext :: Period -> Period
- Hledger: periodNextIn :: DateSpan -> Period -> Period
- Hledger: periodPrevious :: Period -> Period
- Hledger: periodPreviousIn :: DateSpan -> Period -> Period
- Hledger: periodShrink :: Day -> Period -> Period
- Hledger: periodStart :: Period -> Maybe Day
- Hledger: periodTransactionInterval :: PeriodicTransaction -> Maybe Interval
- Hledger: periodexprp :: Day -> TextParser m (Interval, DateSpan)
- Hledger: popParentAccount :: JournalParser m ()
- Hledger: post :: AccountName -> Amount -> Posting
- Hledger: posting :: Posting
- Hledger: postingAllTags :: Posting -> [Tag]
- Hledger: postingDate :: Posting -> Day
- Hledger: postingDate2 :: Posting -> Day
- Hledger: postingDateFn :: ReportOpts -> (Posting -> Day)
- Hledger: postingStatus :: Posting -> Status
- Hledger: postingcommentp :: Maybe Year -> TextParser m (Text, [Tag], Maybe Day, Maybe Day)
- Hledger: postingp :: Maybe Year -> JournalParser m Posting
- Hledger: postingsDateSpan :: [Posting] -> DateSpan
- Hledger: postingsDateSpan' :: WhichDate -> [Posting] -> DateSpan
- Hledger: postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport
- Hledger: ppShow :: Show a => a -> String
- Hledger: pprint :: Show a => a -> IO ()
- Hledger: prevday :: Day -> Day
- Hledger: priceamountp :: JournalParser m Price
- Hledger: printAccounts :: Account -> IO ()
- Hledger: printParseError :: (Show a) => a -> IO ()
- Hledger: pruneAccounts :: (Account -> Bool) -> Account -> Maybe Account
- Hledger: ptrace :: String -> TextParser m ()
- Hledger: pushAccount :: AccountName -> JournalParser m ()
- Hledger: pushParentAccount :: AccountName -> JournalParser m ()
- Hledger: putTraceMsg :: String -> IO ()
- Hledger: quarterContainingMonth :: Integral a => a -> a
- Hledger: queryDateSpan :: Bool -> Query -> DateSpan
- Hledger: queryDateSpan' :: Query -> DateSpan
- Hledger: queryDepth :: Query -> Int
- Hledger: queryEndDate :: Bool -> Query -> Maybe Day
- Hledger: queryFromOpts :: Day -> ReportOpts -> Query
- Hledger: queryFromOptsOnly :: Day -> ReportOpts -> Query
- Hledger: queryIsAcct :: Query -> Bool
- Hledger: queryIsDate :: Query -> Bool
- Hledger: queryIsDate2 :: Query -> Bool
- Hledger: queryIsDateOrDate2 :: Query -> Bool
- Hledger: queryIsDepth :: Query -> Bool
- Hledger: queryIsEmpty :: Query -> Bool
- Hledger: queryIsNull :: Query -> Bool
- Hledger: queryIsReal :: Query -> Bool
- Hledger: queryIsStartDateOnly :: Bool -> Query -> Bool
- Hledger: queryIsStatus :: Query -> Bool
- Hledger: queryIsSym :: Query -> Bool
- Hledger: queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt]
- Hledger: queryStartDate :: Bool -> Query -> Maybe Day
- Hledger: quoteCommoditySymbolIfNeeded :: Text -> Text
- Hledger: quoteIfNeeded :: String -> String
- Hledger: quoteIfSpaced :: Text -> Text
- Hledger: rawOptsToInputOpts :: RawOpts -> InputOpts
- Hledger: rawOptsToReportOpts :: RawOpts -> IO ReportOpts
- Hledger: rawnumberp :: TextParser m (Either AmbiguousNumber RawNumber)
- Hledger: readFileOrStdinPortably :: String -> IO Text
- Hledger: readFilePortably :: FilePath -> IO Text
- Hledger: readHandlePortably :: Handle -> IO Text
- Hledger: readJournal :: InputOpts -> Maybe FilePath -> Text -> IO (Either String Journal)
- Hledger: readJournal' :: Text -> IO Journal
- Hledger: readJournalFile :: InputOpts -> PrefixedFilePath -> IO (Either String Journal)
- Hledger: readJournalFiles :: InputOpts -> [FilePath] -> IO (Either String Journal)
- Hledger: realPostings :: Transaction -> [Posting]
- Hledger: regexMatches :: Regexp -> String -> Bool
- Hledger: regexMatchesCI :: Regexp -> String -> Bool
- Hledger: regexReplace :: Regexp -> Replacement -> String -> String
- Hledger: regexReplaceBy :: Regexp -> (String -> String) -> String -> String
- Hledger: regexReplaceByCI :: Regexp -> (String -> String) -> String -> String
- Hledger: regexReplaceCI :: Regexp -> Replacement -> String -> String
- Hledger: regexReplaceCIMemo :: Regexp -> Replacement -> String -> String
- Hledger: regexReplaceMemo :: Regexp -> Replacement -> String -> String
- Hledger: relatedPostings :: Posting -> [Posting]
- Hledger: removePrices :: Posting -> Posting
- Hledger: reportEndDate :: Journal -> ReportOpts -> IO (Maybe Day)
- Hledger: reportOptsToggleStatus :: Status -> ReportOpts -> ReportOpts
- Hledger: reportSpan :: Journal -> ReportOpts -> IO DateSpan
- Hledger: reportStartDate :: Journal -> ReportOpts -> IO (Maybe Day)
- Hledger: requireJournalFileExists :: FilePath -> IO ()
- Hledger: restofline :: TextParser m String
- Hledger: rjp :: Monad m => JournalParser m a -> Text -> m (Either (ParseError Char CustomErr) a)
- Hledger: root :: () => Tree a -> a
- Hledger: rstrip :: String -> String
- Hledger: rtp :: TextParser Identity a -> Text -> Either (ParseError Char CustomErr) a
- Hledger: runJournalParser :: Monad m => JournalParser m a -> Text -> m (Either (ParseError Char CustomErr) a)
- Hledger: runModifierTransaction :: Query -> ModifierTransaction -> (Transaction -> Transaction)
- Hledger: runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]
- Hledger: runTextParser :: TextParser Identity a -> Text -> Either (ParseError Char CustomErr) a
- Hledger: samplejournal :: Journal
- Hledger: second3 :: () => (a, b, c) -> b
- Hledger: second4 :: () => (a, b, c, d) -> b
- Hledger: second5 :: () => (a, b, c, d, e) -> b
- Hledger: second6 :: () => (a, b, c, d, e, f) -> b
- Hledger: secondarydatep :: Day -> TextParser m Day
- Hledger: sequence' :: Monad f => [f a] -> f [a]
- Hledger: setAmountPrecision :: Int -> Amount -> Amount
- Hledger: setDefaultCommodityAndStyle :: (CommoditySymbol, AmountStyle) -> JournalParser m ()
- Hledger: setMixedAmountPrecision :: Int -> MixedAmount -> MixedAmount
- Hledger: setYear :: Year -> JournalParser m ()
- Hledger: setboolopt :: String -> RawOpts -> RawOpts
- Hledger: setopt :: String -> String -> RawOpts -> RawOpts
- Hledger: showAccountDebug :: PrintfType t => Account -> t
- Hledger: showAccountName :: Maybe Int -> PostingType -> AccountName -> String
- Hledger: showAccounts :: Account -> String
- Hledger: showAccountsBoringFlag :: Account -> String
- Hledger: showAmount :: Amount -> String
- Hledger: showAmountDebug :: Amount -> String
- Hledger: showAmountWithZeroCommodity :: Amount -> String
- Hledger: showAmountWithoutPrice :: Amount -> String
- Hledger: showComment :: Text -> String
- Hledger: showDate :: Day -> String
- Hledger: showDateParseError :: (Show t, Show e) => ParseError t e -> String
- Hledger: showDateSpan :: DateSpan -> String
- Hledger: showDateSpanMonthAbbrev :: DateSpan -> String
- Hledger: showGenericSourcePos :: GenericSourcePos -> String
- Hledger: showMarketPrice :: MarketPrice -> String
- Hledger: showMixedAmount :: MixedAmount -> String
- Hledger: showMixedAmountDebug :: MixedAmount -> String
- Hledger: showMixedAmountOneLine :: MixedAmount -> String
- Hledger: showMixedAmountOneLineWithoutPrice :: MixedAmount -> String
- Hledger: showMixedAmountWithPrecision :: Int -> MixedAmount -> String
- Hledger: showMixedAmountWithZeroCommodity :: MixedAmount -> String
- Hledger: showMixedAmountWithoutPrice :: MixedAmount -> String
- Hledger: showPeriod :: Period -> String
- Hledger: showPeriodMonthAbbrev :: Period -> String
- Hledger: showPosting :: Posting -> String
- Hledger: showPostingLine :: Posting -> String
- Hledger: showPostingLines :: Posting -> [String]
- Hledger: showTransaction :: Transaction -> String
- Hledger: showTransactionUnelided :: Transaction -> String
- Hledger: showTransactionUnelidedOneLineAmounts :: Transaction -> String
- Hledger: showforest :: Show a => Forest a -> String
- Hledger: showtree :: Show a => Tree a -> String
- Hledger: simplifyPeriod :: Period -> Period
- Hledger: simplifyQuery :: Query -> Query
- Hledger: simplifyStatuses :: Ord a => [a] -> [a]
- Hledger: singleQuoteIfNeeded :: String -> String
- Hledger: singlespacedtextp :: TextParser m Text
- Hledger: singlespacep :: () => ParsecT CustomErr Text m ()
- Hledger: sixth6 :: () => (a, b, c, d, e, f) -> f
- Hledger: smartdate :: TextParser m SmartDate
- Hledger: sortAccountTreeByAccountCodeAndName :: Account -> Account
- Hledger: sortAccountTreeByAmount :: NormalSign -> Account -> Account
- Hledger: sourceFilePath :: GenericSourcePos -> FilePath
- Hledger: sourceFirstLine :: GenericSourcePos -> Int
- Hledger: spaceandamountormissingp :: JournalParser m MixedAmount
- Hledger: spacenonewline :: (Stream s, Char ~ Token s) => ParsecT CustomErr s m Char
- Hledger: spanContainsDate :: DateSpan -> Day -> Bool
- Hledger: spanDefaultsFrom :: DateSpan -> DateSpan -> DateSpan
- Hledger: spanEnd :: DateSpan -> Maybe Day
- Hledger: spanIntersect :: DateSpan -> DateSpan -> DateSpan
- Hledger: spanIntervalIntersect :: Interval -> DateSpan -> DateSpan -> DateSpan
- Hledger: spanStart :: DateSpan -> Maybe Day
- Hledger: spanUnion :: DateSpan -> DateSpan -> DateSpan
- Hledger: spansIntersect :: [DateSpan] -> DateSpan
- Hledger: spansSpan :: [DateSpan] -> DateSpan
- Hledger: spansUnion :: [DateSpan] -> DateSpan
- Hledger: specifiedEndDate :: ReportOpts -> IO (Maybe Day)
- Hledger: specifiedStartDate :: ReportOpts -> IO (Maybe Day)
- Hledger: specifiedStartEndDates :: ReportOpts -> IO (Maybe Day, Maybe Day)
- Hledger: splitAtElement :: Eq a => a -> [a] -> [[a]]
- Hledger: splitReaderPrefix :: PrefixedFilePath -> (Maybe String, FilePath)
- Hledger: splitSpan :: Interval -> DateSpan -> [DateSpan]
- Hledger: startOfFirstWeekInMonth :: Integer -> Int -> Day
- Hledger: statusp :: TextParser m Status
- Hledger: strWidth :: String -> Int
- Hledger: stringopt :: String -> RawOpts -> String
- Hledger: strip :: String -> String
- Hledger: stripAnsi :: String -> String
- Hledger: stripbrackets :: String -> String
- Hledger: stripquotes :: Text -> Text
- Hledger: styleAmount :: Map CommoditySymbol AmountStyle -> Amount -> Amount
- Hledger: styleMixedAmount :: Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
- Hledger: subAccountNamesFrom :: [AccountName] -> AccountName -> [AccountName]
- Hledger: subs :: () => Tree a -> Forest a
- Hledger: subtreeat :: Eq a => a -> Tree a -> Maybe (Tree a)
- Hledger: subtreeinforest :: Eq a => a -> [Tree a] -> Maybe (Tree a)
- Hledger: sumAccounts :: Account -> Account
- Hledger: sumPostings :: [Posting] -> MixedAmount
- Hledger: sumStrict :: Num a => [a] -> a
- Hledger: surroundedBy :: Applicative m => m openclose -> m a -> m a
- Hledger: tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String
- Hledger: takeWidth :: Int -> String -> String
- Hledger: testName :: Test -> String
- Hledger: tests_Hledger_Data :: Test
- Hledger: tests_Hledger_Data_Account :: Test
- Hledger: tests_Hledger_Data_AccountName :: Test
- Hledger: tests_Hledger_Data_Amount :: Test
- Hledger: tests_Hledger_Data_Commodity :: Test
- Hledger: tests_Hledger_Data_Journal :: Test
- Hledger: tests_Hledger_Data_Ledger :: Test
- Hledger: tests_Hledger_Data_MarketPrice :: Test
- Hledger: tests_Hledger_Data_Posting :: Test
- Hledger: tests_Hledger_Data_StringFormat :: Test
- Hledger: tests_Hledger_Data_Timeclock :: Test
- Hledger: tests_Hledger_Data_Transaction :: Test
- Hledger: tests_Hledger_Query :: Test
- Hledger: tests_Hledger_Read :: Test
- Hledger: tests_Hledger_Reports :: Test
- Hledger: tests_Hledger_Reports_BalanceReport :: Test
- Hledger: tests_Hledger_Reports_BudgetReport :: Test
- Hledger: tests_Hledger_Reports_EntriesReport :: Test
- Hledger: tests_Hledger_Reports_MultiBalanceReport :: Test
- Hledger: tests_Hledger_Reports_PostingsReport :: Test
- Hledger: tests_Hledger_Reports_ReportOptions :: Test
- Hledger: tests_Hledger_Utils :: Test
- Hledger: tests_Hledger_Utils_Text :: Test
- Hledger: tests_ledgerFromJournal :: [Test]
- Hledger: textConcatTopPadded :: [Text] -> Text
- Hledger: textElideRight :: Int -> Text -> Text
- Hledger: textPadLeftWide :: Int -> Text -> Text
- Hledger: textPadRightWide :: Int -> Text -> Text
- Hledger: textTakeWidth :: Int -> Text -> Text
- Hledger: textUnbracket :: Text -> Text
- Hledger: textWidth :: Text -> Int
- Hledger: textlstrip :: Text -> Text
- Hledger: textrstrip :: Text -> Text
- Hledger: textstrip :: Text -> Text
- Hledger: third3 :: () => (a, b, c) -> c
- Hledger: third4 :: () => (a, b, c, d) -> c
- Hledger: third5 :: () => (a, b, c, d, e) -> c
- Hledger: third6 :: () => (a, b, c, d, e, f) -> c
- Hledger: tieAccountParents :: Account -> Account
- Hledger: timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]
- Hledger: toSystemString :: String -> SystemString
- Hledger: topAccountNames :: [AccountName] -> [AccountName]
- Hledger: trace :: () => String -> a -> a
- Hledger: traceEvent :: () => String -> a -> a
- Hledger: traceEventIO :: String -> IO ()
- Hledger: traceIO :: String -> IO ()
- Hledger: traceId :: String -> String
- Hledger: traceM :: Applicative f => String -> f ()
- Hledger: traceMarker :: () => String -> a -> a
- Hledger: traceMarkerIO :: String -> IO ()
- Hledger: tracePrettyAt :: Show a => Int -> String -> a -> a
- Hledger: tracePrettyAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()
- Hledger: traceShow :: Show a => a -> b -> b
- Hledger: traceShowId :: Show a => a -> a
- Hledger: traceShowM :: (Show a, Applicative f) => a -> f ()
- Hledger: traceStack :: () => String -> a -> a
- Hledger: traceWith :: (a -> String) -> a -> a
- Hledger: transactionAllTags :: Transaction -> [Tag]
- Hledger: transactionDate2 :: Transaction -> Day
- Hledger: transactionDateFn :: ReportOpts -> (Transaction -> Day)
- Hledger: transactionNote :: Transaction -> Text
- Hledger: transactionPayee :: Transaction -> Text
- Hledger: transactionPostingBalances :: Transaction -> (MixedAmount, MixedAmount, MixedAmount)
- Hledger: transactionRegisterDate :: Query -> Query -> Transaction -> Day
- Hledger: transactioncommentp :: TextParser m (Text, [Tag])
- Hledger: transactionsPostings :: [Transaction] -> [Posting]
- Hledger: transactionsReportByCommodity :: TransactionsReport -> [(CommoditySymbol, TransactionsReport)]
- Hledger: traverseJournalAmounts :: Applicative f => (Amount -> f Amount) -> Journal -> f Journal
- Hledger: treeFromPath :: [a] -> FastTree a
- Hledger: treeFromPaths :: (Ord a) => [[a]] -> FastTree a
- Hledger: tree_ :: ReportOpts -> Bool
- Hledger: treeany :: (a -> Bool) -> Tree a -> Bool
- Hledger: treefilter :: (a -> Bool) -> Tree a -> Tree a
- Hledger: treemap :: (a -> b) -> Tree a -> Tree b
- Hledger: treeprune :: Int -> Tree a -> Tree a
- Hledger: triAmount :: () => (a, b, c, d, e, f) -> e
- Hledger: triBalance :: () => (a, b, c, d, e, f) -> f
- Hledger: triCommodityAmount :: () => CommoditySymbol -> (a, b, c, d, MixedAmount, f) -> MixedAmount
- Hledger: triCommodityBalance :: () => CommoditySymbol -> (a, b, c, d, e, MixedAmount) -> MixedAmount
- Hledger: triDate :: () => (a, Transaction, c, d, e, f) -> Day
- Hledger: triOrigTransaction :: () => (a, b, c, d, e, f) -> a
- Hledger: txnTieKnot :: Transaction -> Transaction
- Hledger: txnUntieKnot :: Transaction -> Transaction
- Hledger: type AccountCode = Int
- Hledger: type AccountName = Text
- Hledger: type AccountTransactionsReport = (String, [AccountTransactionsReportItem])
- Hledger: type AccountTransactionsReportItem = (Transaction, Transaction, Bool, String, MixedAmount, MixedAmount)
- Hledger: type Average = MixedAmount The average of 'Change's or 'Balance's in a report or report row.
- Hledger: type Balance = MixedAmount An ending balance as of some date.
- Hledger: type BalanceAssertion = Maybe (Amount, GenericSourcePos)
- Hledger: type BalanceReport = ([BalanceReportItem], MixedAmount)
- Hledger: type BalanceReportItem = (AccountName, AccountName, Int, MixedAmount)
- Hledger: type BudgetAverage = Average
- Hledger: type BudgetGoal = Change
- Hledger: type BudgetReport = PeriodicReport (Maybe Change, Maybe BudgetGoal)
- Hledger: type BudgetTotal = Total
- Hledger: type Change = MixedAmount A change in balance during a certain period.
- Hledger: type CommoditySymbol = Text
- Hledger: type DateTag = (TagName, Day)
- Hledger: type EntriesReport = [EntriesReportItem]
- Hledger: type EntriesReportItem = Transaction
- Hledger: type FormatStr = String
- Hledger: type JournalParser m a = StateT Journal (ParsecT CustomErr Text m) a
- Hledger: type Month = Int
- Hledger: type MonthDay = Int
- Hledger: type MonthWeek = Int
- Hledger: type MultiBalanceReportRow = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
- Hledger: type ParsedJournal = Journal
- Hledger: type Percentage = Decimal
- Hledger: type PeriodicReportRow a = (AccountName, AccountName, Int, [a], a, a)
- Hledger: type PostingsReport = (String, [PostingsReportItem])
- Hledger: type PostingsReportItem = (Maybe Day, Maybe Day, Maybe String, Posting, MixedAmount)
- Hledger: type PrefixedFilePath = FilePath
- Hledger: type Quantity = Decimal
- Hledger: type Quarter = Int
- Hledger: type RawOpts = [(String, String)]
- Hledger: type Regexp = String
- Hledger: type Replacement = String
- Hledger: type SimpleStringParser a = Parsec CustomErr String a
- Hledger: type SimpleTextParser = Parsec CustomErr Text
- Hledger: type SmartDate = (String, String, String)
- Hledger: type StorageFormat = String
- Hledger: type SystemString = String
- Hledger: type Tag = (TagName, TagValue) A tag name and (possibly empty) value.
- Hledger: type TagName = Text
- Hledger: type TagValue = Text
- Hledger: type TextParser m a = ParsecT CustomErr Text m a
- Hledger: type Total = MixedAmount The sum of 'Change's in a report or a report row. Does not make sense for 'Balance's.
- Hledger: type TransactionsReport = (String, [TransactionsReportItem])
- Hledger: type TransactionsReportItem = (Transaction, Transaction, Bool, String, MixedAmount, MixedAmount)
- Hledger: type WeekDay = Int
- Hledger: type Year = Integer
- Hledger: type YearDay = Int
- Hledger: type YearWeek = Int
- Hledger: unbracket :: String -> String
- Hledger: unbudgetedAccountName :: Text
- Hledger: underline :: String -> String
- Hledger: unwords' :: [String] -> String
- Hledger: uppercase :: String -> String
- Hledger: usageError :: String -> a
- Hledger: usd :: DecimalRaw Integer -> Amount
- Hledger: userError' :: String -> IOError
- Hledger: vConcatLeftAligned :: [String] -> String
- Hledger: vConcatRightAligned :: [String] -> String
- Hledger: virtualPostings :: Transaction -> [Posting]
- Hledger: whichDateFromOpts :: ReportOpts -> WhichDate
- Hledger: withPrecision :: Amount -> Int -> Amount
- Hledger: words' :: String -> [String]
- Hledger: words'' :: [Text] -> Text -> [Text]
- Hledger: yearMonthContainingWeekStarting :: Day -> (Integer, Int)
- Hledger.Data: tests_Hledger_Data :: Test
- Hledger.Data.Account: accountCodeAndNameForSort :: Account -> (AccountCode, AccountName)
- Hledger.Data.Account: accountSetCodeFrom :: Journal -> Account -> Account
- Hledger.Data.Account: nameTreeToAccount :: AccountName -> FastTree AccountName -> Account
- Hledger.Data.Account: sortAccountTreeByAccountCodeAndName :: Account -> Account
- Hledger.Data.Account: tests_Hledger_Data_Account :: Test
- Hledger.Data.AccountName: isAccountRegex :: String -> Bool
- Hledger.Data.AccountName: nullaccountnametree :: IsString a => Tree a
- Hledger.Data.AccountName: tests_Hledger_Data_AccountName :: Test
- Hledger.Data.Amount: instance GHC.Show.Show Hledger.Data.Types.Amount
- Hledger.Data.Amount: instance GHC.Show.Show Hledger.Data.Types.MixedAmount
- Hledger.Data.Amount: tests_Hledger_Data_Amount :: Test
- Hledger.Data.AutoTransaction: checkPeriodicTransactionStartDate :: Interval -> DateSpan -> Text -> Maybe String
- Hledger.Data.AutoTransaction: jdatespan :: Journal -> DateSpan
- Hledger.Data.AutoTransaction: mtvaluequery :: ModifierTransaction -> (Day -> Query)
- Hledger.Data.AutoTransaction: periodTransactionInterval :: PeriodicTransaction -> Maybe Interval
- Hledger.Data.AutoTransaction: runModifierTransaction :: Query -> ModifierTransaction -> (Transaction -> Transaction)
- Hledger.Data.AutoTransaction: runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]
- Hledger.Data.Commodity: tests_Hledger_Data_Commodity :: Test
- Hledger.Data.Journal: addModifierTransaction :: ModifierTransaction -> Journal -> Journal
- Hledger.Data.Journal: tests_Hledger_Data_Journal :: Test
- Hledger.Data.Ledger: tests_Hledger_Data_Ledger :: Test
- Hledger.Data.Ledger: tests_ledgerFromJournal :: [Test]
- Hledger.Data.MarketPrice: tests_Hledger_Data_MarketPrice :: Test
- Hledger.Data.Posting: instance GHC.Show.Show Hledger.Data.Types.Posting
- Hledger.Data.Posting: tests_Hledger_Data_Posting :: Test
- Hledger.Data.StringFormat: tests_Hledger_Data_StringFormat :: Test
- Hledger.Data.Timeclock: entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction
- Hledger.Data.Timeclock: tests_Hledger_Data_Timeclock :: Test
- Hledger.Data.Transaction: instance GHC.Show.Show Hledger.Data.Types.ModifierTransaction
- Hledger.Data.Transaction: instance GHC.Show.Show Hledger.Data.Types.PeriodicTransaction
- Hledger.Data.Transaction: instance GHC.Show.Show Hledger.Data.Types.Transaction
- Hledger.Data.Transaction: tests_Hledger_Data_Transaction :: Test
- Hledger.Data.Types: ModifierTransaction :: Text -> [Posting] -> ModifierTransaction
- Hledger.Data.Types: [acode] :: Account -> Maybe AccountCode
- Hledger.Data.Types: [jaccounts] :: Journal -> [(AccountName, Maybe AccountCode)]
- Hledger.Data.Types: [jmodifiertxns] :: Journal -> [ModifierTransaction]
- Hledger.Data.Types: [mtpostings] :: ModifierTransaction -> [Posting]
- Hledger.Data.Types: [mtvalueexpr] :: ModifierTransaction -> Text
- Hledger.Data.Types: data ModifierTransaction
- Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.ModifierTransaction
- Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.ModifierTransaction
- Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.ModifierTransaction
- Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.ModifierTransaction
- Hledger.Query: tests_Hledger_Query :: Test
- Hledger.Read: samplejournal :: IO Journal
- Hledger.Read: tests_Hledger_Read :: Test
- Hledger.Read.Common: generateAutomaticPostings :: Journal -> Journal
- Hledger.Read.Common: parseAndFinaliseJournal' :: JournalParser Identity ParsedJournal -> InputOpts -> FilePath -> Text -> ExceptT String IO Journal
- Hledger.Read.Common: pushAccount :: AccountName -> JournalParser m ()
- Hledger.Read.CsvReader: tests_Hledger_Read_CsvReader :: Test
- Hledger.Read.JournalReader: tests_Hledger_Read_JournalReader :: Test
- Hledger.Read.TimeclockReader: tests_Hledger_Read_TimeclockReader :: Test
- Hledger.Read.TimedotReader: tests_Hledger_Read_TimedotReader :: Test
- Hledger.Reports: tests_Hledger_Reports :: Test
- Hledger.Reports.BalanceReport: tests_Hledger_Reports_BalanceReport :: Test
- Hledger.Reports.BudgetReport: tests_Hledger_Reports_BudgetReport :: Test
- Hledger.Reports.EntriesReport: tests_Hledger_Reports_EntriesReport :: Test
- Hledger.Reports.MultiBalanceReports: tests_Hledger_Reports_MultiBalanceReport :: Test
- Hledger.Reports.PostingsReport: tests_Hledger_Reports_PostingsReport :: Test
- Hledger.Reports.ReportOptions: tests_Hledger_Reports_ReportOptions :: Test
- Hledger.Utils: tests_Hledger_Utils :: Test
- Hledger.Utils.Debug: dbglog :: Show a => String -> a -> a
- Hledger.Utils.Debug: dbgppshow :: Show a => Int -> String -> a -> a
- Hledger.Utils.Debug: log0 :: Show a => String -> a -> a
- Hledger.Utils.Debug: logPrettyAt :: Show a => Int -> String -> a -> a
- Hledger.Utils.Debug: pdbg :: Int -> String -> TextParser m ()
- Hledger.Utils.Debug: ppShow :: Show a => a -> String
- Hledger.Utils.Debug: tracePrettyAt :: Show a => Int -> String -> a -> a
- Hledger.Utils.Debug: tracePrettyAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()
- Hledger.Utils.Test: assertParse :: (Show t, Show e) => (Either (ParseError t e) 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 t e) a) -> Assertion
- Hledger.Utils.Test: filterTests :: (Test -> Bool) -> Test -> Test
- Hledger.Utils.Test: flattenTests :: Test -> [Test]
- Hledger.Utils.Test: printParseError :: (Show a) => a -> IO ()
- Hledger.Utils.Test: testName :: Test -> String
- Hledger.Utils.Text: tests_Hledger_Utils_Text :: Test
+ Hledger.Data: tests_Data :: Test ()
+ Hledger.Data.Account: accountDeclarationOrderAndName :: Account -> (Int, AccountName)
+ Hledger.Data.Account: accountSetDeclarationOrder :: Journal -> Account -> Account
+ Hledger.Data.Account: accountTree :: AccountName -> [AccountName] -> Account
+ Hledger.Data.Account: sortAccountNamesByDeclaration :: Journal -> Bool -> [AccountName] -> [AccountName]
+ Hledger.Data.Account: sortAccountTreeByDeclaration :: Account -> Account
+ Hledger.Data.AccountName: tests_AccountName :: Test ()
+ Hledger.Data.Amount: multiplyAmount :: Amount -> Quantity -> Amount
+ Hledger.Data.Amount: multiplyMixedAmount :: MixedAmount -> Quantity -> MixedAmount
+ Hledger.Data.Amount: setAmountDecimalPoint :: Maybe Char -> Amount -> Amount
+ Hledger.Data.Amount: setAmountInternalPrecision :: Int -> Amount -> Amount
+ Hledger.Data.Amount: tests_Amount :: Test ()
+ Hledger.Data.Amount: withDecimalPoint :: Amount -> Maybe Char -> Amount
+ Hledger.Data.Amount: withInternalPrecision :: Amount -> Int -> Amount
+ Hledger.Data.Journal: addTransactionModifier :: TransactionModifier -> Journal -> Journal
+ Hledger.Data.Journal: tests_Journal :: Test ()
+ Hledger.Data.Ledger: tests_Ledger :: Test ()
+ Hledger.Data.PeriodicTransaction: checkPeriodicTransactionStartDate :: Interval -> DateSpan -> Text -> Maybe String
+ Hledger.Data.PeriodicTransaction: instance GHC.Show.Show Hledger.Data.Types.PeriodicTransaction
+ Hledger.Data.PeriodicTransaction: runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]
+ Hledger.Data.Posting: tests_Posting :: Test ()
+ Hledger.Data.RawOptions: maybecharopt :: String -> RawOpts -> Maybe Char
+ Hledger.Data.StringFormat: tests_StringFormat :: Test ()
+ Hledger.Data.Timeclock: tests_Timeclock :: Test ()
+ Hledger.Data.Transaction: tests_Transaction :: Test ()
+ Hledger.Data.TransactionModifier: transactionModifierToFunction :: TransactionModifier -> (Transaction -> Transaction)
+ Hledger.Data.Types: TransactionModifier :: Text -> [Posting] -> TransactionModifier
+ Hledger.Data.Types: [adeclarationorder] :: Account -> Maybe Int
+ Hledger.Data.Types: [jdeclaredaccounts] :: Journal -> [AccountName]
+ Hledger.Data.Types: [jtxnmodifiers] :: Journal -> [TransactionModifier]
+ Hledger.Data.Types: [tmpostings] :: TransactionModifier -> [Posting]
+ Hledger.Data.Types: [tmquerytxt] :: TransactionModifier -> Text
+ Hledger.Data.Types: data TransactionModifier
+ Hledger.Data.Types: instance Control.DeepSeq.NFData Hledger.Data.Types.TransactionModifier
+ Hledger.Data.Types: instance Data.Data.Data Hledger.Data.Types.TransactionModifier
+ Hledger.Data.Types: instance GHC.Classes.Eq Hledger.Data.Types.TransactionModifier
+ Hledger.Data.Types: instance GHC.Generics.Generic Hledger.Data.Types.TransactionModifier
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.Amount
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.MixedAmount
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.Posting
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.Price
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.Transaction
+ Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.TransactionModifier
+ Hledger.Data.Types: nulltransactionmodifier :: TransactionModifier
+ Hledger.Query: matchesCommodity :: Query -> CommoditySymbol -> Bool
+ Hledger.Query: matchesMarketPrice :: Query -> MarketPrice -> Bool
+ Hledger.Query: queryIsAmt :: Query -> Bool
+ Hledger.Query: tests_Query :: Test ()
+ Hledger.Read: tests_Read :: Test ()
+ Hledger.Read.Common: [separator_] :: InputOpts -> Char
+ Hledger.Read.Common: applyTransactionModifiers :: Journal -> Journal
+ Hledger.Read.Common: pushDeclaredAccount :: AccountName -> JournalParser m ()
+ Hledger.Read.Common: tests_Common :: Test ()
+ Hledger.Read.CsvReader: instance GHC.Show.Show Hledger.Read.CsvReader.CSVError
+ Hledger.Read.CsvReader: printCSV :: CSV -> String
+ Hledger.Read.CsvReader: tests_CsvReader :: Test ()
+ Hledger.Read.CsvReader: type CSV = [Record]
+ Hledger.Read.CsvReader: type Field = String
+ Hledger.Read.CsvReader: type Record = [Field]
+ Hledger.Read.JournalReader: tests_JournalReader :: Test ()
+ Hledger.Reports: tests_Reports :: Test ()
+ Hledger.Reports.BalanceReport: sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b]
+ Hledger.Reports.BalanceReport: tests_BalanceReport :: Test ()
+ Hledger.Reports.BudgetReport: sortBudgetReport :: ReportOpts -> Journal -> BudgetReport -> BudgetReport
+ Hledger.Reports.BudgetReport: tests_BudgetReport :: Test ()
+ Hledger.Reports.BudgetReport: type BudgetCell = (Maybe Change, Maybe BudgetGoal)
+ Hledger.Reports.BudgetReport: type BudgetReportRow = PeriodicReportRow BudgetCell
+ Hledger.Reports.EntriesReport: tests_EntriesReport :: Test ()
+ Hledger.Reports.MultiBalanceReports: tests_MultiBalanceReports :: Test ()
+ Hledger.Reports.PostingsReport: tests_PostingsReport :: Test ()
+ Hledger.Reports.ReportOptions: tests_ReportOptions :: Test ()
+ Hledger.Reports.TransactionsReports: tests_TransactionsReports :: Test ()
+ Hledger.Utils: tests_Utils :: Test ()
+ Hledger.Utils.Debug: dbgparse :: Int -> String -> TextParser m ()
+ Hledger.Utils.Debug: plog :: Show a => String -> a -> a
+ Hledger.Utils.Debug: plogAt :: Show a => Int -> String -> a -> a
+ Hledger.Utils.Debug: pshow :: Show a => a -> String
+ Hledger.Utils.Debug: ptraceAt :: Show a => Int -> String -> a -> a
+ Hledger.Utils.Debug: ptraceAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()
+ Hledger.Utils.Debug: traceParse :: String -> TextParser m ()
+ Hledger.Utils.Test: _it :: Text -> Test a -> Test a
+ Hledger.Utils.Test: _test :: Text -> Test a -> Test a
+ Hledger.Utils.Test: _tests :: Text -> [Test ()] -> Test ()
+ Hledger.Utils.Test: bool :: Test Bool
+ Hledger.Utils.Test: crash :: HasCallStack => Text -> Test a
+ Hledger.Utils.Test: data Test a
+ Hledger.Utils.Test: double :: Test Double
+ Hledger.Utils.Test: double' :: Double -> Double -> Test Double
+ Hledger.Utils.Test: expect :: HasCallStack -> Bool -> Test ()
+ Hledger.Utils.Test: expectEq :: (Eq a, Show a, HasCallStack) => a -> a -> Test ()
+ Hledger.Utils.Test: expectEqPP :: (Eq a, Show a, HasCallStack) => a -> a -> Test ()
+ Hledger.Utils.Test: expectJust :: HasCallStack => Maybe a -> Test ()
+ Hledger.Utils.Test: expectLeft :: (Show a, HasCallStack) => Either e a -> Test ()
+ Hledger.Utils.Test: expectLeftNoShow :: HasCallStack => Either e a -> Test ()
+ Hledger.Utils.Test: expectParse :: (Monoid st, Eq a, Show a, HasCallStack) => StateT st (ParsecT CustomErr Text IO) a -> Text -> Test ()
+ Hledger.Utils.Test: expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) => StateT st (ParsecT CustomErr Text IO) a -> Text -> a -> Test ()
+ Hledger.Utils.Test: expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) => StateT st (ParsecT CustomErr Text IO) a -> Text -> (a -> b) -> b -> Test ()
+ Hledger.Utils.Test: expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) => StateT st (ParsecT CustomErr Text IO) a -> Text -> String -> Test ()
+ Hledger.Utils.Test: expectRight :: (Show e, HasCallStack) => Either e a -> Test ()
+ Hledger.Utils.Test: expectRightNoShow :: HasCallStack => Either e a -> Test ()
+ Hledger.Utils.Test: fork :: () => Test a -> Test ()
+ Hledger.Utils.Test: fork' :: () => Test a -> Test Test a
+ Hledger.Utils.Test: int :: Test Int
+ Hledger.Utils.Test: int' :: Int -> Int -> Test Int
+ Hledger.Utils.Test: io :: () => IO a -> Test a
+ Hledger.Utils.Test: it :: Text -> Test a -> Test a
+ Hledger.Utils.Test: listOf :: () => Int -> Test a -> Test [a]
+ Hledger.Utils.Test: listsOf :: () => [Int] -> Test a -> Test [[a]]
+ Hledger.Utils.Test: mapOf :: Ord k => Int -> Test k -> Test v -> Test Map k v
+ Hledger.Utils.Test: mapsOf :: Ord k => [Int] -> Test k -> Test v -> Test [Map k v]
+ Hledger.Utils.Test: note :: Text -> Test ()
+ Hledger.Utils.Test: note' :: Show s => s -> Test ()
+ Hledger.Utils.Test: ok :: Test ()
+ Hledger.Utils.Test: pair :: () => Test a -> Test b -> Test (a, b)
+ Hledger.Utils.Test: pick :: () => [a] -> Test a
+ Hledger.Utils.Test: random :: Random a => Test a
+ Hledger.Utils.Test: random' :: Random a => a -> a -> Test a
+ Hledger.Utils.Test: rerun :: () => Int -> Test a -> IO ()
+ Hledger.Utils.Test: rerunOnly :: () => Int -> Text -> Test a -> IO ()
+ Hledger.Utils.Test: run :: () => Test a -> IO ()
+ Hledger.Utils.Test: runEasytests :: [String] -> Test () -> IO Bool
+ Hledger.Utils.Test: runOnly :: () => Text -> Test a -> IO ()
+ Hledger.Utils.Test: scope :: () => Text -> Test a -> Test a
+ Hledger.Utils.Test: skip :: Test ()
+ Hledger.Utils.Test: test :: Text -> Test a -> Test a
+ Hledger.Utils.Test: tests :: Text -> [Test ()] -> Test ()
+ Hledger.Utils.Test: type HasCallStack = ?callStack :: CallStack
+ Hledger.Utils.Test: using :: () => IO r -> r -> IO () -> r -> Test a -> Test a
+ Hledger.Utils.Test: word :: Test Word
+ Hledger.Utils.Test: word' :: Word -> Word -> Test Word
+ Hledger.Utils.Test: word8 :: Test Word8
+ Hledger.Utils.Test: word8' :: Word8 -> Word8 -> Test Word8
+ Hledger.Utils.Text: tests_Text :: Test ()
- Hledger: tests_Hledger :: Test
+ Hledger: tests_Hledger :: Test ()
- Hledger.Data.Types: Account :: AccountName -> Maybe AccountCode -> MixedAmount -> [Account] -> Int -> MixedAmount -> Maybe Account -> Bool -> Account
+ Hledger.Data.Types: Account :: AccountName -> Maybe Int -> MixedAmount -> [Account] -> Int -> MixedAmount -> Maybe Account -> Bool -> Account
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(AccountName, Maybe AccountCode)] -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [MarketPrice] -> [ModifierTransaction] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [AccountName] -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> Bool -> String -> Bool -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> Char -> [String] -> Bool -> Bool -> Bool -> Bool -> String -> Bool -> InputOpts
- Hledger.Reports.BudgetReport: type BudgetReport = PeriodicReport (Maybe Change, Maybe BudgetGoal)
+ Hledger.Reports.BudgetReport: type BudgetReport = PeriodicReport BudgetCell
- Hledger.Utils.Debug: ptrace :: String -> TextParser m ()
+ Hledger.Utils.Debug: ptrace :: Show a => a -> a
- Hledger.Utils.Test: is :: (Eq a, Show a) => a -> a -> Assertion
+ Hledger.Utils.Test: is :: (Eq a, Show a, HasCallStack) => a -> a -> Test ()

Files

CHANGES view
@@ -2,6 +2,79 @@ Most user-visible changes are noted in the hledger changelog, instead.  +# 1.11 (2018/9/30)++* compilation now works when locale is unset (#849)++* all unit tests have been converted from HUnit+test-framework to easytest++* doctests now run quicker by default, by skipping reloading between tests. +  This can be disabled by passing --slow to the doctests test suite+  executable.++* doctests test suite executable now supports --verbose, which shows+  progress output as tests are run if doctest 0.16.0+ is installed+  (and hopefully is harmless otherwise).++* doctests now support file pattern arguments, provide more informative output.+  Limiting to just the file(s) you're interested can make doctest start+  much quicker. With one big caveat: you can limit the starting files,+  but it always imports and tests all other local files those import.++* a bunch of custom Show instances have been replaced with defaults,+  for easier troubleshooting.  These were sometimes obscuring+  important details, eg in test failure output. Our new policy is:+  stick with default derived Show instances as far as possible, but+  when necessary adjust them to valid haskell syntax so pretty-show+  can pretty-print them (eg when they contain Day values, cf+  https://github.com/haskell/time/issues/101).  By convention, when+  fields are shown in less than full detail, and/or in double-quoted+  pseudo syntax, we show a double period (..) in the output.++* Amount has a new Show instance.  Amount's show instance hid+  important details by default, and showing more details required+  increasing the debug level, which was inconvenient.  Now it has a+  single show instance which shows more information, is fairly+  compact, and is pretty-printable.++  ghci> usd 1+  OLD:+  Amount {acommodity="$", aquantity=1.00, ..}+  NEW:+  Amount {acommodity = "$", aquantity = 1.00, aprice = NoPrice, astyle = AmountStyle "L False 2 Just '.' Nothing..", amultiplier = False}++  MixedAmount's show instance is unchanged, but showMixedAmountDebug+  is affected by this change:++  ghci> putStrLn $ showMixedAmountDebug $ Mixed [usd 1]+  OLD:+  Mixed [Amount {acommodity="$", aquantity=1.00, aprice=, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}}]+  NEW:+  Mixed [Amount {acommodity="$", aquantity=1.00, aprice=, astyle=AmountStyle "L False 2 Just '.' Nothing.."}]++* Same-line & next-line comments of transactions, postings, etc.+  are now parsed a bit more precisely (followingcommentp). +  Previously, parsing no comment gave the same result as an empty+  comment (a single newline); now it gives an empty string.  +  Also, and perhaps as a consequence of the above, when there's no+  same-line comment but there is a next-line comment, we'll insert an+  empty first line, since otherwise next-line comments would get moved+  up to the same line when rendered.++* Hledger.Utils.Test exports HasCallStack++* queryDateSpan, queryDateSpan' now intersect date AND'ed date spans+  instead of unioning them, and docs are clearer.++* pushAccount -> pushDeclaredAccount++* jaccounts -> jdeclaredaccounts++* AutoTransaction.hs -> PeriodicTransaction.hs & TransactionModifier.hs++* Hledger.Utils.Debug helpers have been renamed/cleaned up++ # 1.10 (2018/6/30)  * build cleanly with all supported GHC versions again (7.10 to 8.4)
Hledger.hs view
@@ -1,21 +1,21 @@+{-# LANGUAGE OverloadedStrings #-}+ module Hledger (   module X  ,tests_Hledger ) where-import           Test.HUnit  import           Hledger.Data    as X-import           Hledger.Query   as X-import           Hledger.Read    as X hiding (samplejournal)+import           Hledger.Read    as X import           Hledger.Reports as X+import           Hledger.Query   as X import           Hledger.Utils   as X -tests_Hledger = TestList-    [-     tests_Hledger_Data-    ,tests_Hledger_Query-    ,tests_Hledger_Read-    ,tests_Hledger_Reports-    ,tests_Hledger_Utils-    ]+tests_Hledger = tests "Hledger" [+   tests_Data+  ,tests_Query+  ,tests_Read+  ,tests_Reports+  ,tests_Utils+  ]
Hledger/Data.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {-|  The Hledger.Data library allows parsing and querying of C++ ledger-style@@ -17,17 +18,17 @@                module Hledger.Data.Ledger,                module Hledger.Data.MarketPrice,                module Hledger.Data.Period,+               module Hledger.Data.PeriodicTransaction,                module Hledger.Data.Posting,                module Hledger.Data.RawOptions,                module Hledger.Data.StringFormat,                module Hledger.Data.Timeclock,                module Hledger.Data.Transaction,-               module Hledger.Data.AutoTransaction,+               module Hledger.Data.TransactionModifier,                module Hledger.Data.Types,-               tests_Hledger_Data+               tests_Data               ) where-import Test.HUnit  import Hledger.Data.Account import Hledger.Data.AccountName@@ -38,28 +39,23 @@ import Hledger.Data.Ledger import Hledger.Data.MarketPrice import Hledger.Data.Period+import Hledger.Data.PeriodicTransaction import Hledger.Data.Posting import Hledger.Data.RawOptions import Hledger.Data.StringFormat import Hledger.Data.Timeclock import Hledger.Data.Transaction-import Hledger.Data.AutoTransaction+import Hledger.Data.TransactionModifier import Hledger.Data.Types+import Hledger.Utils.Test -tests_Hledger_Data :: Test-tests_Hledger_Data = TestList-    [-     tests_Hledger_Data_Account-    ,tests_Hledger_Data_AccountName-    ,tests_Hledger_Data_Amount-    ,tests_Hledger_Data_Commodity-    ,tests_Hledger_Data_Journal-    ,tests_Hledger_Data_MarketPrice-    ,tests_Hledger_Data_Ledger-    ,tests_Hledger_Data_Posting-    -- ,tests_Hledger_Data_RawOptions-    -- ,tests_Hledger_Data_StringFormat-    ,tests_Hledger_Data_Timeclock-    ,tests_Hledger_Data_Transaction-    -- ,tests_Hledger_Data_Types-    ]+tests_Data = tests "Data" [+   tests_AccountName+  ,tests_Amount+  ,tests_Journal+  ,tests_Ledger+  ,tests_Posting+  ,tests_StringFormat+  ,tests_Timeclock+  ,tests_Transaction+  ]
Hledger/Data/Account.hs view
@@ -16,7 +16,6 @@ import qualified Data.Map as M import Data.Text (pack,unpack) import Safe (headMay, lookupJustDef)-import Test.HUnit import Text.Printf  import Hledger.Data.AccountName@@ -47,7 +46,7 @@  nullacct = Account   { aname = ""-  , acode = Nothing+  , adeclarationorder = Nothing   , aparent = Nothing   , asubs = []   , anumpostings = 0@@ -68,9 +67,8 @@     grouped = groupSort [(paccount p,pamount p) | p <- ps]     counted = [(aname, length amts) | (aname, amts) <- grouped]     summed =  [(aname, sumStrict amts) | (aname, amts) <- grouped]  -- always non-empty-    nametree = treeFromPaths $ map (expandAccountName . fst) summed-    acctswithnames = nameTreeToAccount "root" nametree-    acctswithnumps = mapAccounts setnumps    acctswithnames where setnumps    a = a{anumpostings=fromMaybe 0 $ lookup (aname a) counted}+    acctstree      = accountTree "root" $ map fst summed+    acctswithnumps = mapAccounts setnumps    acctstree      where setnumps    a = a{anumpostings=fromMaybe 0 $ lookup (aname a) counted}     acctswithebals = mapAccounts setebalance acctswithnumps where setebalance a = a{aebalance=lookupJustDef nullmixedamt (aname a) summed}     acctswithibals = sumAccounts acctswithebals     acctswithparents = tieAccountParents acctswithibals@@ -78,10 +76,14 @@   in     acctsflattened --- | Convert an AccountName tree to an Account tree-nameTreeToAccount :: AccountName -> FastTree AccountName -> Account-nameTreeToAccount rootname (T m) =-    nullacct{ aname=rootname, asubs=map (uncurry nameTreeToAccount) $ M.assocs m }+-- | Convert a list of account names to a tree of Account objects, +-- with just the account names filled in. +-- A single root account with the given name is added.+accountTree :: AccountName -> [AccountName] -> Account+accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m }+  where+    T m = treeFromPaths $ map expandAccountName as :: FastTree AccountName+    accountTree' a (T m) = nullacct{aname=a, asubs=map (uncurry accountTree') $ M.assocs m}  -- | Tie the knot so all subaccounts' parents are set correctly. tieAccountParents :: Account -> Account@@ -91,10 +93,6 @@       where         a' = a{aparent=parent, asubs=map (tie (Just a')) asubs} --- | Look up an account's numeric code, if any, from the Journal and set it.-accountSetCodeFrom :: Journal -> Account -> Account-accountSetCodeFrom j a = a{acode=fromMaybe Nothing $ lookup (aname a) (jaccounts j)}- -- | Get this account's parent accounts, from the nearest up to the root. parentAccounts :: Account -> [Account] parentAccounts Account{aparent=Nothing} = []@@ -190,7 +188,7 @@     | p a       = a : concatMap (filterAccounts p) (asubs a)     | otherwise = concatMap (filterAccounts p) (asubs a) --- | Sort each level of an account tree by inclusive amount,+-- | Sort each group of siblings in an account tree by inclusive amount, -- so that the accounts with largest normal balances are listed first.   -- The provided normal balance sign determines whether normal balances -- are negative or positive, affecting the sort order. Ie,@@ -200,24 +198,54 @@ sortAccountTreeByAmount normalsign a   | null $ asubs a = a   | otherwise      = a{asubs=-                        sortBy (maybeflip $ comparing aibalance) $+                        sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . aibalance)) $                         map (sortAccountTreeByAmount normalsign) $ asubs a}   where     maybeflip | normalsign==NormallyNegative = id               | otherwise                  = flip --- | Sort each level of an account tree first by the account code--- if any, with the empty account code sorting last, and then by--- the account name. -sortAccountTreeByAccountCodeAndName :: Account -> Account-sortAccountTreeByAccountCodeAndName a+-- | Look up an account's declaration order, if any, from the Journal and set it.+-- This is the relative position of its account directive +-- among the other account directives.+accountSetDeclarationOrder :: Journal -> Account -> Account+accountSetDeclarationOrder j a@Account{..} = +  a{adeclarationorder = findIndex (==aname) (jdeclaredaccounts j)}++-- | Sort account names by the order in which they were declared in+-- the journal, at each level of the account tree (ie within each+-- group of siblings). Undeclared accounts are sorted last and+-- alphabetically. +-- This is hledger's default sort for reports organised by account.+-- The account list is converted to a tree temporarily, adding any+-- missing parents; these can be kept (suitable for a tree-mode report) +-- or removed (suitable for a flat-mode report).+--+sortAccountNamesByDeclaration :: Journal -> Bool -> [AccountName] -> [AccountName]+sortAccountNamesByDeclaration j keepparents as =+  (if keepparents then id else filter (`elem` as)) $  -- maybe discard missing parents that were added+  map aname $                                         -- keep just the names+  drop 1 $                                            -- drop the root node that was added+  flattenAccounts $                                   -- convert to an account list+  sortAccountTreeByDeclaration $                      -- sort by declaration order (and name)+  mapAccounts (accountSetDeclarationOrder j) $        -- add declaration order info  +  accountTree "root"                                  -- convert to an account tree+  as++-- | Sort each group of siblings in an account tree by declaration order, then account name.+-- So each group will contain first the declared accounts, +-- in the same order as their account directives were parsed, +-- and then the undeclared accounts, sorted by account name. +sortAccountTreeByDeclaration :: Account -> Account+sortAccountTreeByDeclaration a   | null $ asubs a = a   | otherwise      = a{asubs=-      sortBy (comparing accountCodeAndNameForSort) $ map sortAccountTreeByAccountCodeAndName $ asubs a}+      sortBy (comparing accountDeclarationOrderAndName) $ +      map sortAccountTreeByDeclaration $ asubs a+      } -accountCodeAndNameForSort a = (acode', aname a)+accountDeclarationOrderAndName a = (adeclarationorder', aname a)   where-    acode' = fromMaybe maxBound (acode a)+    adeclarationorder' = fromMaybe maxBound (adeclarationorder a)  -- | Search an account list by name. lookupAccount :: AccountName -> [Account] -> Maybe Account@@ -237,8 +265,3 @@                      (showMixedAmount $ aebalance a)                      (showMixedAmount $ aibalance a)                      (if aboring a then "b" else " " :: String)---tests_Hledger_Data_Account = TestList [- ]-
Hledger/Data/AccountName.hs view
@@ -9,8 +9,37 @@  -} -module Hledger.Data.AccountName+module Hledger.Data.AccountName (+   accountLeafName+  ,accountNameComponents+  ,accountNameDrop+  ,accountNameFromComponents+  ,accountNameLevel+  ,accountNameToAccountOnlyRegex+  ,accountNameToAccountRegex+  ,accountNameTreeFrom+  ,accountRegexToAccountName+  ,accountSummarisedName+  ,acctsep+  ,acctsepchar+  ,clipAccountName+  ,clipOrEllipsifyAccountName+  ,elideAccountName+  ,escapeName+  ,expandAccountName+  ,expandAccountNames+  ,isAccountNamePrefixOf+--  ,isAccountRegex +  ,isSubAccountNameOf+  ,parentAccountName+  ,parentAccountNames+  ,subAccountNamesFrom+  ,topAccountNames+  ,unbudgetedAccountName+  ,tests_AccountName+) where+ import Data.List #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid@@ -18,12 +47,13 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Tree-import Test.HUnit import Text.Printf  import Hledger.Data.Types-import Hledger.Utils+import Hledger.Utils  +-- $setup+-- >>> :set -XOverloadedStrings  acctsepchar :: Char acctsepchar = ':'@@ -122,7 +152,7 @@           accounttreesfrom as = [Node a (accounttreesfrom $ subs a) | a <- as]           subs = subAccountNamesFrom (expandAccountNames accts) -nullaccountnametree = Node "root" []+--nullaccountnametree = Node "root" []  -- | Elide an account name to fit in the specified width. -- From the ledger 2.6 news:@@ -192,33 +222,32 @@ accountRegexToAccountName :: Regexp -> AccountName accountRegexToAccountName = T.pack . regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1" -- XXX pack --- | Does this string look like an exact account-matching regular expression ?-isAccountRegex  :: String -> Bool-isAccountRegex s = take 1 s == "^" && take 5 (reverse s) == ")$|:("--tests_Hledger_Data_AccountName = TestList- [-  "accountNameTreeFrom" ~: do-    accountNameTreeFrom ["a"]       `is` Node "root" [Node "a" []]-    accountNameTreeFrom ["a","b"]   `is` Node "root" [Node "a" [], Node "b" []]-    accountNameTreeFrom ["a","a:b"] `is` Node "root" [Node "a" [Node "a:b" []]]-    accountNameTreeFrom ["a:b:c"]   `is` Node "root" [Node "a" [Node "a:b" [Node "a:b:c" []]]]+-- -- | Does this string look like an exact account-matching regular expression ?+--isAccountRegex  :: String -> Bool+--isAccountRegex s = take 1 s == "^" && take 5 (reverse s) == ")$|:(" -  ,"expandAccountNames" ~:+tests_AccountName = tests "AccountName" [+  tests "accountNameTreeFrom" [+     accountNameTreeFrom ["a"]       `is` Node "root" [Node "a" []]+    ,accountNameTreeFrom ["a","b"]   `is` Node "root" [Node "a" [], Node "b" []]+    ,accountNameTreeFrom ["a","a:b"] `is` Node "root" [Node "a" [Node "a:b" []]]+    ,accountNameTreeFrom ["a:b:c"]   `is` Node "root" [Node "a" [Node "a:b" [Node "a:b:c" []]]]+  ]+  ,tests "expandAccountNames" [     expandAccountNames ["assets:cash","assets:checking","expenses:vacation"] `is`      ["assets","assets:cash","assets:checking","expenses","expenses:vacation"]--  ,"isAccountNamePrefixOf" ~: do-    "assets" `isAccountNamePrefixOf` "assets" `is` False-    "assets" `isAccountNamePrefixOf` "assets:bank" `is` True-    "assets" `isAccountNamePrefixOf` "assets:bank:checking" `is` True-    "my assets" `isAccountNamePrefixOf` "assets:bank" `is` False--  ,"isSubAccountNameOf" ~: do-    "assets" `isSubAccountNameOf` "assets" `is` False-    "assets:bank" `isSubAccountNameOf` "assets" `is` True-    "assets:bank:checking" `isSubAccountNameOf` "assets" `is` False-    "assets:bank" `isSubAccountNameOf` "my assets" `is` False-+  ]+  ,tests "isAccountNamePrefixOf" [+     "assets" `isAccountNamePrefixOf` "assets" `is` False+    ,"assets" `isAccountNamePrefixOf` "assets:bank" `is` True+    ,"assets" `isAccountNamePrefixOf` "assets:bank:checking" `is` True+    ,"my assets" `isAccountNamePrefixOf` "assets:bank" `is` False+  ]+  ,tests "isSubAccountNameOf" [+     "assets" `isSubAccountNameOf` "assets" `is` False+    ,"assets:bank" `isSubAccountNameOf` "assets" `is` True+    ,"assets:bank:checking" `isSubAccountNameOf` "assets" `is` False+    ,"assets:bank" `isSubAccountNameOf` "my assets" `is` False+  ]  ] 
Hledger/Data/Amount.hs view
@@ -40,7 +40,7 @@  -} -{-# LANGUAGE CPP, StandaloneDeriving, RecordWildCards, OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving, RecordWildCards, OverloadedStrings #-}  module Hledger.Data.Amount (   -- * Amount@@ -58,6 +58,7 @@   -- ** arithmetic   costOfAmount,   divideAmount,+  multiplyAmount,   amountValue,   -- ** rendering   amountstyle,@@ -71,6 +72,10 @@   maxprecisionwithpoint,   setAmountPrecision,   withPrecision,+  setAmountInternalPrecision,+  withInternalPrecision,+  setAmountDecimalPoint,+  withDecimalPoint,   canonicaliseAmount,   -- * MixedAmount   nullmixedamt,@@ -84,6 +89,7 @@   -- ** arithmetic   costOfMixedAmount,   divideMixedAmount,+  multiplyMixedAmount,   averageMixedAmounts,   isNegativeAmount,   isNegativeMixedAmount,@@ -108,7 +114,7 @@   canonicaliseMixedAmount,   -- * misc.   ltraceamount,-  tests_Hledger_Data_Amount+  tests_Amount ) where  import Data.Char (isDigit)@@ -122,13 +128,12 @@ -- import Data.Text (Text) import qualified Data.Text as T import Safe (maximumDef)-import Test.HUnit import Text.Printf import qualified Data.Map as M  import Hledger.Data.Types import Hledger.Data.Commodity-import Hledger.Utils+import Hledger.Utils    deriving instance Show MarketPrice@@ -144,15 +149,6 @@ ------------------------------------------------------------------------------- -- Amount -instance Show Amount where-  show _a@Amount{..}-    --  debugLevel < 2 = showAmountWithoutPrice a-    --  debugLevel < 3 = showAmount a-    | debugLevel < 6 =-       printf "Amount {acommodity=%s, aquantity=%s, ..}" (show acommodity) (show aquantity)-    | otherwise      = --showAmountDebug a-       printf "Amount {acommodity=%s, aquantity=%s, aprice=%s, astyle=%s}" (show acommodity) (show aquantity) (showPriceDebug aprice) (show astyle)- instance Num Amount where     abs a@Amount{aquantity=q}    = a{aquantity=abs q}     signum a@Amount{aquantity=q} = a{aquantity=signum q}@@ -217,6 +213,10 @@ divideAmount :: Amount -> Quantity -> Amount divideAmount a@Amount{aquantity=q} d = a{aquantity=q/d} +-- | Multiply an amount's quantity by a constant.+multiplyAmount :: Amount -> Quantity -> Amount+multiplyAmount a@Amount{aquantity=q} d = a{aquantity=q*d}+ -- | Is this amount negative ? The price is ignored. isNegativeAmount :: Amount -> Bool isNegativeAmount Amount{aquantity=q} = q < 0@@ -255,6 +255,31 @@ showAmountWithoutPrice :: Amount -> String showAmountWithoutPrice a = showAmount a{aprice=NoPrice} +-- | Set an amount's internal precision, ie rounds the Decimal representing +-- the amount's quantity to some number of decimal places.+-- Rounding is done with Data.Decimal's default roundTo function:+-- "If the value ends in 5 then it is rounded to the nearest even value (Banker's Rounding)".+-- Does not change the amount's display precision.+-- Intended only for internal use, eg when comparing amounts in tests. +setAmountInternalPrecision :: Int -> Amount -> Amount+setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{ +   astyle=s{asprecision=p} +  ,aquantity=roundTo (fromIntegral p) q+  }++-- | Set an amount's internal precision, flipped.+-- Intended only for internal use, eg when comparing amounts in tests. +withInternalPrecision :: Amount -> Int -> Amount+withInternalPrecision = flip setAmountInternalPrecision++-- | Set (or clear) an amount's display decimal point.+setAmountDecimalPoint :: Maybe Char -> Amount -> Amount+setAmountDecimalPoint mc a@Amount{ astyle=s } = a{ astyle=s{asdecimalpoint=mc} }++-- | Set (or clear) an amount's display decimal point, flipped.+withDecimalPoint :: Amount -> Maybe Char -> Amount+withDecimalPoint = flip setAmountDecimalPoint+ -- | Colour version. cshowAmountWithoutPrice :: Amount -> String cshowAmountWithoutPrice a = cshowAmount a{aprice=NoPrice}@@ -375,9 +400,7 @@ amountValue :: Journal -> Day -> Amount -> Amount amountValue j d a =   case commodityValue j d (acommodity a) of-    Just v  -> v{aquantity=aquantity v * aquantity a-                ,aprice=aprice a-                }+    Just v  -> v{aquantity=aquantity v * aquantity a}     Nothing -> a  -- This is here not in Commodity.hs to use the Amount Show instance above for debugging. @@ -403,12 +426,6 @@ ------------------------------------------------------------------------------- -- MixedAmount -instance Show MixedAmount where-  show-    | debugLevel < 3 = intercalate "\\n" . lines . showMixedAmountWithoutPrice-    --  debugLevel < 6 = intercalate "\\n" . lines . showMixedAmount-    | otherwise      = showMixedAmountDebug- instance Num MixedAmount where     fromInteger i = Mixed [fromInteger i]     negate (Mixed as) = Mixed $ map negate as@@ -467,36 +484,12 @@     combinableprices Amount{aprice=UnitPrice p1} Amount{aprice=UnitPrice p2} = p1 == p2     combinableprices _ _ = False -tests_normaliseMixedAmount = [-  "normaliseMixedAmount" ~: do-   -- assertEqual "missing amount is discarded" (Mixed [nullamt]) (normaliseMixedAmount $ Mixed [usd 0, missingamt])-   assertEqual "any missing amount means a missing mixed amount" missingmixedamt (normaliseMixedAmount $ Mixed [usd 0, missingamt])-   assertEqual "unpriced same-commodity amounts are combined" (Mixed [usd 2]) (normaliseMixedAmount $ Mixed [usd 0, usd 2])-   -- amounts with same unit price are combined-   normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) `is` Mixed [usd 2 `at` eur 1]-   -- amounts with different unit prices are not combined-   normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) `is` Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]-   -- amounts with total prices are not combined-   normaliseMixedAmount (Mixed  [usd 1 @@ eur 1, usd 1 @@ eur 1]) `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]- ]- -- | Like normaliseMixedAmount, but combine each commodity's amounts -- into just one by throwing away all prices except the first. This is -- only used as a rendering helper, and could show a misleading price. normaliseMixedAmountSquashPricesForDisplay :: MixedAmount -> MixedAmount normaliseMixedAmountSquashPricesForDisplay = normaliseHelper True -tests_normaliseMixedAmountSquashPricesForDisplay = [-  "normaliseMixedAmountSquashPricesForDisplay" ~: do-    normaliseMixedAmountSquashPricesForDisplay (Mixed []) `is` Mixed [nullamt]-    assertBool "" $ isZeroMixedAmount $ normaliseMixedAmountSquashPricesForDisplay-      (Mixed [usd 10-             ,usd 10 @@ eur 7-             ,usd (-10)-             ,usd (-10) @@ eur 7-             ])- ]- -- | Sum same-commodity amounts in a lossy way, applying the first -- price to the result and discarding any other prices. Only used as a -- rendering helper.@@ -539,6 +532,10 @@ divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount divideMixedAmount (Mixed as) d = Mixed $ map (`divideAmount` d) as +-- | Multiply a mixed amount's quantities by a constant.+multiplyMixedAmount :: MixedAmount -> Quantity -> MixedAmount+multiplyMixedAmount (Mixed as) d = Mixed $ map (`multiplyAmount` d) as+ -- | Calculate the average of some mixed amounts. averageMixedAmounts :: [MixedAmount] -> MixedAmount averageMixedAmounts [] = 0@@ -670,80 +667,105 @@   ---------------------------------------------------------------------------------- misc--tests_Hledger_Data_Amount = TestList $-     tests_normaliseMixedAmount-  ++ tests_normaliseMixedAmountSquashPricesForDisplay-  ++ [--  -- Amount--   "costOfAmount" ~: do-    costOfAmount (eur 1) `is` eur 1-    costOfAmount (eur 2){aprice=UnitPrice $ usd 2} `is` usd 4-    costOfAmount (eur 1){aprice=TotalPrice $ usd 2} `is` usd 2-    costOfAmount (eur (-1)){aprice=TotalPrice $ usd 2} `is` usd (-2)--  ,"isZeroAmount" ~: do-    assertBool "" $ isZeroAmount amount-    assertBool "" $ isZeroAmount $ usd 0--  ,"negating amounts" ~: do-    let a = usd 1-    negate a `is` a{aquantity= -1}-    let b = (usd 1){aprice=UnitPrice $ eur 2}-    negate b `is` b{aquantity= -1}--  ,"adding amounts without prices" ~: do-    let a1 = usd 1.23-    let a2 = usd (-1.23)-    let a3 = usd (-1.23)-    (a1 + a2) `is` usd 0-    (a1 + a3) `is` usd 0-    (a2 + a3) `is` usd (-2.46)-    (a3 + a3) `is` usd (-2.46)-    sum [a1,a2,a3,-a3] `is` usd 0-    -- highest precision is preserved-    let ap1 = usd 1 `withPrecision` 1-        ap3 = usd 1 `withPrecision` 3-    asprecision (astyle $ sum [ap1,ap3]) `is` 3-    asprecision (astyle $ sum [ap3,ap1]) `is` 3-    -- adding different commodities assumes conversion rate 1-    assertBool "" $ isZeroAmount (a1 - eur 1.23)--  ,"showAmount" ~: do-    showAmount (usd 0 + gbp 0) `is` "0"+-- tests -  -- MixedAmount+tests_Amount = tests "Amount" [+   tests "Amount" [ -  ,"adding mixed amounts to zero, the commodity and amount style are preserved" ~: do-    sum (map (Mixed . (:[]))-             [usd 1.25-             ,usd (-1) `withPrecision` 3-             ,usd (-0.25)-             ])-      `is` Mixed [usd 0 `withPrecision` 3]+     tests "costOfAmount" [+       costOfAmount (eur 1) `is` eur 1+      ,costOfAmount (eur 2){aprice=UnitPrice $ usd 2} `is` usd 4+      ,costOfAmount (eur 1){aprice=TotalPrice $ usd 2} `is` usd 2+      ,costOfAmount (eur (-1)){aprice=TotalPrice $ usd 2} `is` usd (-2)+    ]+  +    ,tests "isZeroAmount" [+       expect $ isZeroAmount amount+      ,expect $ isZeroAmount $ usd 0+    ]+  +    ,tests "negating amounts" [+       negate (usd 1) `is` (usd 1){aquantity= -1}+      ,let b = (usd 1){aprice=UnitPrice $ eur 2} in negate b `is` b{aquantity= -1}+    ]+  +    ,tests "adding amounts without prices" [+       (usd 1.23 + usd (-1.23)) `is` usd 0+      ,(usd 1.23 + usd (-1.23)) `is` usd 0+      ,(usd (-1.23) + usd (-1.23)) `is` usd (-2.46)+      ,sum [usd 1.23,usd (-1.23),usd (-1.23),-(usd (-1.23))] `is` usd 0+      -- highest precision is preserved+      ,asprecision (astyle $ sum [usd 1 `withPrecision` 1, usd 1 `withPrecision` 3]) `is` 3+      ,asprecision (astyle $ sum [usd 1 `withPrecision` 3, usd 1 `withPrecision` 1]) `is` 3+      -- adding different commodities assumes conversion rate 1+      ,expect $ isZeroAmount (usd 1.23 - eur 1.23)+    ]+  +    ,tests "showAmount" [+      showAmount (usd 0 + gbp 0) `is` "0"+    ] -  ,"adding mixed amounts with total prices" ~: do-    sum (map (Mixed . (:[]))-     [usd 1 @@ eur 1-     ,usd (-2) @@ eur 1-     ])-      `is` Mixed [usd 1 @@ eur 1-                 ,usd (-2) @@ eur 1-                 ]+  ] -  ,"showMixedAmount" ~: do-    showMixedAmount (Mixed [usd 1]) `is` "$1.00"-    showMixedAmount (Mixed [usd 1 `at` eur 2]) `is` "$1.00 @ €2.00"-    showMixedAmount (Mixed [usd 0]) `is` "0"-    showMixedAmount (Mixed []) `is` "0"-    showMixedAmount missingmixedamt `is` ""+  ,tests "MixedAmount" [ -  ,"showMixedAmountWithoutPrice" ~: do-    let a = usd 1 `at` eur 2-    showMixedAmountWithoutPrice (Mixed [a]) `is` "$1.00"-    showMixedAmountWithoutPrice (Mixed [a, -a]) `is` "0"+     tests "adding mixed amounts to zero, the commodity and amount style are preserved" [+      sum (map (Mixed . (:[]))+               [usd 1.25+               ,usd (-1) `withPrecision` 3+               ,usd (-0.25)+               ])+        `is` Mixed [usd 0 `withPrecision` 3]+    ]+  +    ,tests "adding mixed amounts with total prices" [+      sum (map (Mixed . (:[]))+       [usd 1 @@ eur 1+       ,usd (-2) @@ eur 1+       ])+        `is` Mixed [usd 1 @@ eur 1+                   ,usd (-2) @@ eur 1+                   ]+    ]+  +    ,tests "showMixedAmount" [+       showMixedAmount (Mixed [usd 1]) `is` "$1.00"+      ,showMixedAmount (Mixed [usd 1 `at` eur 2]) `is` "$1.00 @ €2.00"+      ,showMixedAmount (Mixed [usd 0]) `is` "0"+      ,showMixedAmount (Mixed []) `is` "0"+      ,showMixedAmount missingmixedamt `is` ""+    ]+  +    ,tests "showMixedAmountWithoutPrice" $+      let a = usd 1 `at` eur 2 in +    [+        showMixedAmountWithoutPrice (Mixed [a]) `is` "$1.00"+       ,showMixedAmountWithoutPrice (Mixed [a, -a]) `is` "0"+    ]+  +    ,tests "normaliseMixedAmount" [+       test "a missing amount overrides any other amounts" $ +        normaliseMixedAmount (Mixed [usd 1, missingamt]) `is` missingmixedamt+      ,test "unpriced same-commodity amounts are combined" $ +        normaliseMixedAmount (Mixed [usd 0, usd 2]) `is` Mixed [usd 2]+      ,test "amounts with same unit price are combined" $ +        normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) `is` Mixed [usd 2 `at` eur 1]+      ,test "amounts with different unit prices are not combined" $ +        normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) `is` Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]+      ,test "amounts with total prices are not combined" $+        normaliseMixedAmount (Mixed  [usd 1 @@ eur 1, usd 1 @@ eur 1]) `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]+    ]+  +    ,tests "normaliseMixedAmountSquashPricesForDisplay" [+       normaliseMixedAmountSquashPricesForDisplay (Mixed []) `is` Mixed [nullamt]+      ,expect $ isZeroMixedAmount $ normaliseMixedAmountSquashPricesForDisplay+        (Mixed [usd 10+               ,usd 10 @@ eur 7+               ,usd (-10)+               ,usd (-10) @@ eur 7+               ])+    ]    ]++ ]
− Hledger/Data/AutoTransaction.hs
@@ -1,333 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE CPP #-}-{-|--This module provides utilities for applying automated transactions like-'ModifierTransaction' and 'PeriodicTransaction'.---}-module Hledger.Data.AutoTransaction-    (-    -- * Transaction processors-      runModifierTransaction-    , runPeriodicTransaction--    -- * Accessors-    , mtvaluequery-    , jdatespan-    , periodTransactionInterval--    -- * Misc-    , checkPeriodicTransactionStartDate-    )-where--import Data.Maybe-#if !(MIN_VERSION_base(4,11,0))-import Data.Monoid ((<>))-#endif-import Data.Time.Calendar-import qualified Data.Text as T-import Hledger.Data.Types-import Hledger.Data.Dates-import Hledger.Data.Amount-import Hledger.Data.Posting (post)-import Hledger.Data.Transaction-import Hledger.Utils.UTF8IOCompat (error')-import Hledger.Query--- import Hledger.Utils.Debug---- $setup--- >>> :set -XOverloadedStrings--- >>> import Hledger.Data.Posting--- >>> import Hledger.Data.Journal---- | Builds a 'Transaction' transformer based on 'ModifierTransaction'.------ 'Query' parameter allows injection of additional restriction on posting--- match. Don't forget to call 'txnTieKnot'.------ >>> runModifierTransaction Any (ModifierTransaction "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}--- 0000/01/01---     ping           $1.00---     pong           $2.00--- <BLANKLINE>--- <BLANKLINE>--- >>> runModifierTransaction Any (ModifierTransaction "miss" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}--- 0000/01/01---     ping           $1.00--- <BLANKLINE>--- <BLANKLINE>--- >>> runModifierTransaction None (ModifierTransaction "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}--- 0000/01/01---     ping           $1.00--- <BLANKLINE>--- <BLANKLINE>--- >>> runModifierTransaction Any (ModifierTransaction "ping" ["pong" `post` amount{amultiplier=True, aquantity=3}]) nulltransaction{tpostings=["ping" `post` usd 2]}--- 0000/01/01---     ping           $2.00---     pong           $6.00--- <BLANKLINE>--- <BLANKLINE>-runModifierTransaction :: Query -> ModifierTransaction -> (Transaction -> Transaction)-runModifierTransaction q mt = modifier where-    q' = simplifyQuery $ And [q, mtvaluequery mt (error "query cannot depend on current time")]-    mods = map runModifierPosting $ mtpostings mt-    generatePostings ps = [p' | p <- ps-                              , p' <- if q' `matchesPosting` p then p:[ m p | m <- mods] else [p]]-    modifier t@(tpostings -> ps) = t { tpostings = generatePostings ps }---- | Extract 'Query' equivalent of 'mtvalueexpr' from 'ModifierTransaction'------ >>> mtvaluequery (ModifierTransaction "" []) undefined--- Any--- >>> mtvaluequery (ModifierTransaction "ping" []) undefined--- Acct "ping"--- >>> mtvaluequery (ModifierTransaction "date:2016" []) undefined--- Date (DateSpan 2016)--- >>> mtvaluequery (ModifierTransaction "date:today" []) (read "2017-01-01")--- Date (DateSpan 2017/01/01)-mtvaluequery :: ModifierTransaction -> (Day -> Query)-mtvaluequery mt = fst . flip parseQuery (mtvalueexpr mt)---- | 'DateSpan' of all dates mentioned in 'Journal'------ >>> jdatespan nulljournal--- DateSpan ---- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01"}] }--- DateSpan 2016/01/01--- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01", tpostings=[nullposting{pdate=Just $ read "2016-02-01"}]}] }--- DateSpan 2016/01/01-2016/02/01-jdatespan :: Journal -> DateSpan-jdatespan j-        | null dates = nulldatespan-        | otherwise = DateSpan (Just $ minimum dates) (Just $ 1 `addDays` maximum dates)-    where-        dates = concatMap tdates $ jtxns j---- | 'DateSpan' of all dates mentioned in 'Transaction'------ >>> tdates nulltransaction--- [0000-01-01]-tdates :: Transaction -> [Day]-tdates t = tdate t : concatMap pdates (tpostings t) ++ maybeToList (tdate2 t) where-    pdates p = catMaybes [pdate p, pdate2 p]--postingScale :: Posting -> Maybe Quantity-postingScale p =-    case amounts $ pamount p of-        [a] | amultiplier a -> Just $ aquantity a-        _ -> Nothing--runModifierPosting :: Posting -> (Posting -> Posting)-runModifierPosting p' = modifier where-    modifier p = renderPostingCommentDates $ p'-        { pdate = pdate p-        , pdate2 = pdate2 p-        , pamount = amount' p-        }-    amount' = case postingScale p' of-        Nothing -> const $ pamount p'-        Just n -> \p -> withAmountType (head $ amounts $ pamount p') $ pamount p `divideMixedAmount` (1/n)-    withAmountType amount (Mixed as) = case acommodity amount of-        "" -> Mixed as-        c -> Mixed [a{acommodity = c, astyle = astyle amount, aprice = aprice amount} | a <- as]--renderPostingCommentDates :: Posting -> Posting-renderPostingCommentDates p = p { pcomment = comment' }-    where-        datesComment = T.concat $ catMaybes [T.pack . showDate <$> pdate p, ("=" <>) . T.pack . showDate <$> pdate2 p]-        comment'-            | T.null datesComment = pcomment p-            | otherwise = T.intercalate "\n" $ filter (not . T.null) [T.strip $ pcomment p, "[" <> datesComment <> "]"]---- doctest helper, too much hassle to define in the comment--- XXX duplicates some logic in periodictransactionp-_ptgen str = do-  let -    t = T.pack str-    (i,s) = parsePeriodExpr' nulldate t-  case checkPeriodicTransactionStartDate i s t of-    Just e  -> error' e-    Nothing ->-      mapM_ (putStr . show) $-        runPeriodicTransaction-          nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } -          nulldatespan---- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan'------ Note that new transactions require 'txnTieKnot' post-processing.------ >>> _ptgen "monthly from 2017/1 to 2017/4"--- 2017/01/01---     ; recur: monthly from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>--- 2017/02/01---     ; recur: monthly from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>--- 2017/03/01---     ; recur: monthly from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>------ >>> _ptgen "monthly from 2017/1 to 2017/5"--- 2017/01/01---     ; recur: monthly from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/02/01---     ; recur: monthly from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/03/01---     ; recur: monthly from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/04/01---     ; recur: monthly from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>------ >>> _ptgen "every 2nd day of month from 2017/02 to 2017/04"--- 2017/01/02---     ; recur: every 2nd day of month from 2017/02 to 2017/04---     a           $1.00--- <BLANKLINE>--- 2017/02/02---     ; recur: every 2nd day of month from 2017/02 to 2017/04---     a           $1.00--- <BLANKLINE>--- 2017/03/02---     ; recur: every 2nd day of month from 2017/02 to 2017/04---     a           $1.00--- <BLANKLINE>------ >>> _ptgen "every 30th day of month from 2017/1 to 2017/5"--- 2016/12/30---     ; recur: every 30th day of month from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/01/30---     ; recur: every 30th day of month from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/02/28---     ; recur: every 30th day of month from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/03/30---     ; recur: every 30th day of month from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>--- 2017/04/30---     ; recur: every 30th day of month from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE>------ >>> _ptgen "every 2nd Thursday of month from 2017/1 to 2017/4"--- 2016/12/08---     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>--- 2017/01/12---     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>--- 2017/02/09---     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>--- 2017/03/09---     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE>------ >>> _ptgen "every nov 29th from 2017 to 2019"--- 2016/11/29---     ; recur: every nov 29th from 2017 to 2019---     a           $1.00--- <BLANKLINE>--- 2017/11/29---     ; recur: every nov 29th from 2017 to 2019---     a           $1.00--- <BLANKLINE>--- 2018/11/29---     ; recur: every nov 29th from 2017 to 2019---     a           $1.00--- <BLANKLINE>------ >>> _ptgen "2017/1"--- 2017/01/01---     ; recur: 2017/1---     a           $1.00--- <BLANKLINE>------ >>> _ptgen ""--- *** Exception: failed to parse...--- ...------ >>> _ptgen "weekly from 2017"--- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the week------ >>> _ptgen "monthly from 2017/5/4"--- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month        ------ >>> _ptgen "every quarter from 2017/1/2"--- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter        ------ >>> _ptgen "yearly from 2017/1/14"--- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year        ------ >>> let reportperiod="daily from 2018/01/03" in let (i,s) = parsePeriodExpr' nulldate reportperiod in runPeriodicTransaction (nullperiodictransaction{ptperiodexpr=reportperiod, ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1]}) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03"))--- []----runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]-runPeriodicTransaction PeriodicTransaction{..} requestedspan =-    [ t{tdate=d} | (DateSpan (Just d) _) <- ptinterval `splitSpan` spantofill ]-  where-    spantofill = spanIntervalIntersect ptinterval ptspan requestedspan-    t = nulltransaction{-           tstatus      = ptstatus-          ,tcode        = ptcode-          ,tdescription = ptdescription -          ,tcomment     = (if T.null ptcomment then "\n" else ptcomment) <> "recur: " <> ptperiodexpr-          ,ttags        = ("recur", ptperiodexpr) : pttags -          ,tpostings    = ptpostings-          }---- | Check that this date span begins at a boundary of this interval, --- or return an explanatory error message including the provided period expression--- (from which the span and interval are derived).-checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String -checkPeriodicTransactionStartDate i s periodexpr = -  case (i, spanStart s) of-    (Weeks _,    Just d) -> checkStart d "week"-    (Months _,   Just d) -> checkStart d "month"-    (Quarters _, Just d) -> checkStart d "quarter"-    (Years _,    Just d) -> checkStart d "year"-    _                    -> Nothing -    where-      checkStart d x =-        let firstDate = fixSmartDate d ("","this",x) -        in   -         if d == firstDate -         then Nothing-         else Just $-          "Unable to generate transactions according to "++show (T.unpack periodexpr)-          ++" because "++show d++" is not a first day of the "++x---- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?-periodTransactionInterval :: PeriodicTransaction -> Maybe Interval-periodTransactionInterval pt =-  let-    expr = ptperiodexpr pt-    err  = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)-  in-    case parsePeriodExpr err expr of-      Left _      -> Nothing-      Right (i,_) -> Just i
Hledger/Data/Commodity.hs view
@@ -19,7 +19,6 @@ import Data.Monoid #endif import qualified Data.Text as T-import Test.HUnit -- import qualified Data.Map as M  import Hledger.Data.Types@@ -79,7 +78,4 @@ --     commoditymap = Map.fromList [(s, commoditieswithsymbol s) | s <- symbols] --     commoditieswithsymbol s = filter ((s==) . symbol) cs --     symbols = nub $ map symbol cs--tests_Hledger_Data_Commodity = TestList [- ] 
Hledger/Data/Dates.hs view
@@ -205,7 +205,7 @@ splitSpan (DayOfMonth n) s = splitspan (nthdayofmonthcontaining n) (nthdayofmonth n . nextmonth) s splitSpan (WeekdayOfMonth n wd) s = splitspan (nthweekdayofmonthcontaining n wd) (advancetonthweekday n wd . nextmonth) s splitSpan (DayOfWeek n)  s = splitspan (nthdayofweekcontaining n)  (applyN (n-1) nextday . nextweek)  s-splitSpan (DayOfYear m n) s= splitspan (nthdayofyearcontaining m n) (applyN (n-1) nextday . applyN (m-1) nextmonth . nextyear) s+splitSpan (DayOfYear m n) s = splitspan (nthdayofyearcontaining m n) (applyN (n-1) nextday . applyN (m-1) nextmonth . nextyear) s -- splitSpan (WeekOfYear n)    s = splitspan startofweek    (applyN n nextweek)    s -- splitSpan (MonthOfYear n)   s = splitspan startofmonth   (applyN n nextmonth)   s -- splitSpan (QuarterOfYear n) s = splitspan startofquarter (applyN n nextquarter) s@@ -526,8 +526,10 @@ nextyear = startofyear . addGregorianYearsClip 1 startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day --- | For given date d find year-long interval that starts on given MM/DD of year--- and covers it. +-- | For given date d find year-long interval that starts on given+-- MM/DD of year and covers it.+-- The given MM and DD should be basically valid (1-12 & 1-31),+-- or an error is raised. -- -- Examples: lets take 2017-11-22. Year-long intervals covering it that -- starts before Nov 22 will start in 2017. However@@ -545,14 +547,19 @@ -- 2016-12-31           -- >>> nthdayofyearcontaining 1 1 wed22nd -- 2017-01-01          -nthdayofyearcontaining m n d | mmddOfSameYear <= d = mmddOfSameYear-                             | otherwise = mmddOfPrevYear-    where mmddOfSameYear = addDays (fromIntegral n-1) $ applyN (m-1) nextmonth s-          mmddOfPrevYear = addDays (fromIntegral n-1) $ applyN (m-1) nextmonth $ prevyear s-          s = startofyear d+nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day+nthdayofyearcontaining m md date+  | not (validMonth $ show m)  = error' $ "nthdayofyearcontaining: invalid month "++show m+  | not (validDay   $ show md) = error' $ "nthdayofyearcontaining: invalid day "  ++show md+  | mmddOfSameYear <= date = mmddOfSameYear+  | otherwise = mmddOfPrevYear+  where mmddOfSameYear = addDays (fromIntegral md-1) $ applyN (m-1) nextmonth s+        mmddOfPrevYear = addDays (fromIntegral md-1) $ applyN (m-1) nextmonth $ prevyear s+        s = startofyear date  -- | For given date d find month-long interval that starts on nth day of month -- and covers it. +-- The given day of month should be basically valid (1-31), or an error is raised. -- -- Examples: lets take 2017-11-22. Month-long intervals covering it that -- start on 1st-22nd of month will start in Nov. However@@ -568,11 +575,14 @@ -- 2017-10-23           -- >>> nthdayofmonthcontaining 30 wed22nd -- 2017-10-30          -nthdayofmonthcontaining n d | nthOfSameMonth <= d = nthOfSameMonth-                            | otherwise = nthOfPrevMonth-    where nthOfSameMonth = nthdayofmonth n s-          nthOfPrevMonth = nthdayofmonth n $ prevmonth s-          s = startofmonth d+nthdayofmonthcontaining :: MonthDay -> Day -> Day+nthdayofmonthcontaining md date+  | not (validDay $ show md) = error' $ "nthdayofmonthcontaining: invalid day "  ++show md+  | nthOfSameMonth <= date = nthOfSameMonth+  | otherwise = nthOfPrevMonth+  where nthOfSameMonth = nthdayofmonth md s+        nthOfPrevMonth = nthdayofmonth md $ prevmonth s+        s = startofmonth date  -- | For given date d find week-long interval that starts on nth day of week -- and covers it. @@ -591,6 +601,7 @@ -- 2017-11-16           -- >>> nthdayofweekcontaining 5 wed22nd -- 2017-11-17          +nthdayofweekcontaining :: WeekDay -> Day -> Day nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek                            | otherwise = nthOfPrevWeek     where nthOfSameWeek = addDays (fromIntegral n-1) s@@ -614,16 +625,20 @@ -- 2017-10-26 -- >>> nthweekdayofmonthcontaining 4 5 wed22nd -- 2017-10-27+nthweekdayofmonthcontaining :: Int -> WeekDay -> Day -> Day nthweekdayofmonthcontaining n wd d | nthWeekdaySameMonth <= d  = nthWeekdaySameMonth                                    | otherwise = nthWeekdayPrevMonth     where nthWeekdaySameMonth = advancetonthweekday n wd $ startofmonth d           nthWeekdayPrevMonth = advancetonthweekday n wd $ prevmonth d-          + -- | Advance to nth weekday wd after given start day s-advancetonthweekday n wd s = addWeeks (n-1) . firstMatch (>=s) . iterate (addWeeks 1) $ firstweekday s-  where          +advancetonthweekday :: Int -> WeekDay -> Day -> Day+advancetonthweekday n wd s = +  maybe err (addWeeks (n-1)) $ firstMatch (>=s) $ iterate (addWeeks 1) $ firstweekday s+  where+    err = error' "advancetonthweekday: should not happen"     addWeeks k = addDays (7 * fromIntegral k)-    firstMatch p = head . dropWhile (not . p)+    firstMatch p = headMay . dropWhile (not . p)      firstweekday = addDays (fromIntegral wd-1) . startofweek  ----------------------------------------------------------------------@@ -822,15 +837,19 @@   failIfInvalidDay d   return ("",m,d) +-- These are compared case insensitively, and should all be kept lower case.  months         = ["january","february","march","april","may","june",                   "july","august","september","october","november","december"] monthabbrevs   = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"] weekdays       = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"] weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"] -monthIndex t = maybe 0 (+1) $ t `elemIndex` months-monIndex t   = maybe 0 (+1) $ t `elemIndex` monthabbrevs+-- | Convert a case insensitive english month name to a month number.+monthIndex name = maybe 0 (+1) $ T.toLower name `elemIndex` months +-- | Convert a case insensitive english three-letter month abbreviation to a month number.+monIndex   name = maybe 0 (+1) $ T.toLower name `elemIndex` monthabbrevs+ month :: TextParser m SmartDate month = do   m <- choice $ map (try . string') months@@ -845,9 +864,11 @@  weekday :: TextParser m Int weekday = do-  wday <- choice . map string' $ weekdays ++ weekdayabbrevs-  let i = head . catMaybes $ [wday `elemIndex` weekdays, wday `elemIndex` weekdayabbrevs]-  return (i+1)+  wday <- T.toLower <$> (choice . map string' $ weekdays ++ weekdayabbrevs)+  case catMaybes $ [wday `elemIndex` weekdays, wday `elemIndex` weekdayabbrevs] of+    (i:_) -> return (i+1)+    []    -> fail  $ "weekday: should not happen: attempted to find " <> +                      show wday <> " in " <> show (weekdays ++ weekdayabbrevs)   today,yesterday,tomorrow :: TextParser m SmartDate today     = string' "today"     >> return ("","","today")
Hledger/Data/Journal.hs view
@@ -13,7 +13,7 @@ module Hledger.Data.Journal (   -- * Parsing helpers   addMarketPrice,-  addModifierTransaction,+  addTransactionModifier,   addPeriodicTransaction,   addTransaction,   journalBalanceTransactions,@@ -68,7 +68,7 @@   journalUntieTransactions,   -- * Tests   samplejournal,-  tests_Hledger_Data_Journal,+  tests_Journal, ) where import Control.Applicative (Const(..))@@ -82,7 +82,6 @@ import qualified Data.HashTable.ST.Cuckoo as HT import Data.List import Data.List.Extra (groupSort)--- import Data.Map (findWithDefault) import Data.Maybe #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid@@ -95,15 +94,13 @@ import Data.Time.Calendar import Data.Tree import System.Time (ClockTime(TOD))-import Test.HUnit import Text.Printf import qualified Data.Map as M -import Hledger.Utils+import Hledger.Utils  import Hledger.Data.Types import Hledger.Data.AccountName import Hledger.Data.Amount--- import Hledger.Data.Commodity import Hledger.Data.Dates import Hledger.Data.Transaction import Hledger.Data.Posting@@ -138,7 +135,7 @@ -- showJournalDebug j = unlines [ --                       show j --                      ,show (jtxns j)---                      ,show (jmodifiertxns j)+--                      ,show (jtxnmodifiers j) --                      ,show (jperiodictxns j) --                      ,show $ jparsetimeclockentries j --                      ,show $ jmarketprices j@@ -166,11 +163,11 @@     ,jparsealiases              = jparsealiases              j2     -- ,jparsetransactioncount     = jparsetransactioncount     j1 +  jparsetransactioncount     j2     ,jparsetimeclockentries = jparsetimeclockentries j1 <> jparsetimeclockentries j2-    ,jaccounts                  = jaccounts                  j1 <> jaccounts                  j2+    ,jdeclaredaccounts                  = jdeclaredaccounts                  j1 <> jdeclaredaccounts                  j2     ,jcommodities               = jcommodities               j1 <> jcommodities               j2     ,jinferredcommodities       = jinferredcommodities       j1 <> jinferredcommodities       j2     ,jmarketprices              = jmarketprices              j1 <> jmarketprices              j2-    ,jmodifiertxns              = jmodifiertxns              j1 <> jmodifiertxns              j2+    ,jtxnmodifiers              = jtxnmodifiers              j1 <> jtxnmodifiers              j2     ,jperiodictxns              = jperiodictxns              j1 <> jperiodictxns              j2     ,jtxns                      = jtxns                      j1 <> jtxns                      j2     ,jfinalcommentlines         = jfinalcommentlines         j2@@ -193,11 +190,11 @@   ,jparsealiases              = []   -- ,jparsetransactioncount     = 0   ,jparsetimeclockentries = []-  ,jaccounts                  = []+  ,jdeclaredaccounts                  = []   ,jcommodities               = M.fromList []   ,jinferredcommodities       = M.fromList []   ,jmarketprices              = []-  ,jmodifiertxns              = []+  ,jtxnmodifiers              = []   ,jperiodictxns              = []   ,jtxns                      = []   ,jfinalcommentlines         = ""@@ -217,8 +214,8 @@ addTransaction :: Transaction -> Journal -> Journal addTransaction t j = j { jtxns = t : jtxns j } -addModifierTransaction :: ModifierTransaction -> Journal -> Journal-addModifierTransaction mt j = j { jmodifiertxns = mt : jmodifiertxns j }+addTransactionModifier :: TransactionModifier -> Journal -> Journal+addTransactionModifier mt j = j { jtxnmodifiers = mt : jtxnmodifiers j }  addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal addPeriodicTransaction pt j = j { jperiodictxns = pt : jperiodictxns j }@@ -259,7 +256,7 @@  -- | Sorted unique account names declared by account directives in this journal. journalAccountNamesDeclared :: Journal -> [AccountName]-journalAccountNamesDeclared = nub . sort . map fst . jaccounts+journalAccountNamesDeclared = nub . sort . jdeclaredaccounts  -- | Sorted unique account names declared by account directives or posted to -- by transactions in this journal.@@ -284,8 +281,8 @@ -- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Profit_.26_Loss_accounts>. journalProfitAndLossAccountQuery  :: Journal -> Query journalProfitAndLossAccountQuery j = Or [journalIncomeAccountQuery j-                                               ,journalExpenseAccountQuery j-                                               ]+                                        ,journalExpenseAccountQuery j+                                        ]  -- | A query for Income (Revenue) accounts in this journal. -- This is currently hard-coded to the case-insensitive regex @^(income|revenue)s?(:|$)@.@@ -301,9 +298,9 @@ -- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Balance_Sheet_Accounts>. journalBalanceSheetAccountQuery  :: Journal -> Query journalBalanceSheetAccountQuery j = Or [journalAssetAccountQuery j-                                              ,journalLiabilityAccountQuery j-                                              ,journalEquityAccountQuery j-                                              ]+                                       ,journalLiabilityAccountQuery j+                                       ,journalEquityAccountQuery j+                                       ]  -- | A query for Asset accounts in this journal. -- This is currently hard-coded to the case-insensitive regex @^assets?(:|$)@.@@ -496,8 +493,9 @@    journalApplyCommodityStyles $    j {jfiles        = (path,txt) : reverse fs      ,jlastreadtime = t-     ,jtxns         = reverse $ jtxns j -- NOTE: see addTransaction-     ,jmodifiertxns = reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction+     ,jdeclaredaccounts = reverse $ jdeclaredaccounts j+     ,jtxns         = reverse $ jtxns j         -- NOTE: see addTransaction+     ,jtxnmodifiers = reverse $ jtxnmodifiers j -- NOTE: see addTransactionModifier      ,jperiodictxns = reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction      ,jmarketprices = reverse $ jmarketprices j -- NOTE: see addMarketPrice      })@@ -765,8 +763,7 @@     commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts]  -- | Given an ordered list of amount styles, choose a canonical style.--- That is: the style of the first, and the--- maximum precision of all.+-- That is: the style of the first, and the maximum precision of all. canonicalStyleFrom :: [AmountStyle] -> AmountStyle canonicalStyleFrom [] = amountstyle canonicalStyleFrom ss@(first:_) =@@ -881,21 +878,6 @@       pdates   = concatMap (catMaybes . map (if secondary then (Just . postingDate2) else pdate) . tpostings) ts       ts       = jtxns j --- #ifdef TESTS-test_journalDateSpan = do- "journalDateSpan" ~: do-  assertEqual "" (DateSpan (Just $ fromGregorian 2014 1 10) (Just $ fromGregorian 2014 10 11))-                 (journalDateSpan True j)-  where-    j = nulljournal{jtxns = [nulltransaction{tdate = parsedate "2014/02/01"-                                            ,tpostings = [posting{pdate=Just (parsedate "2014/01/10")}]-                                            }-                            ,nulltransaction{tdate = parsedate "2014/09/01"-                                            ,tpostings = [posting{pdate2=Just (parsedate "2014/10/10")}]-                                            }-                            ]}--- #endif- -- | Apply the pivot transformation to all postings in a journal, -- replacing their account name by their value for the given field or tag. journalPivot :: Text -> Journal -> Journal@@ -1079,12 +1061,32 @@           ]          } -tests_Hledger_Data_Journal = TestList $- [-  test_journalDateSpan-  -- "query standard account types" ~:-  --  do-  --   let j = journal1-  --   journalBalanceSheetAccountNames j `is` ["assets","assets:a","equity","equity:q","equity:q:qq","liabilities","liabilities:l"]-  --   journalProfitAndLossAccountNames j `is` ["expenses","expenses:e","income","income:i"]- ]+tests_Journal = tests "Journal" [++   test "journalDateSpan" $+    journalDateSpan True nulljournal{+      jtxns = [nulltransaction{tdate = parsedate "2014/02/01"+                              ,tpostings = [posting{pdate=Just (parsedate "2014/01/10")}]+                              }+              ,nulltransaction{tdate = parsedate "2014/09/01"+                              ,tpostings = [posting{pdate2=Just (parsedate "2014/10/10")}]+                              }+              ]+      }+    `is` (DateSpan (Just $ fromGregorian 2014 1 10) (Just $ fromGregorian 2014 10 11))++  ,tests "standard account type queries" $+    let+      j = samplejournal+      journalAccountNamesMatching :: Query -> Journal -> [AccountName]+      journalAccountNamesMatching q = filter (q `matchesAccount`) . journalAccountNames+      namesfrom qfunc = journalAccountNamesMatching (qfunc j) j+    in [+       test "assets"      $ expectEq (namesfrom journalAssetAccountQuery)     ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]+      ,test "liabilities" $ expectEq (namesfrom journalLiabilityAccountQuery) ["liabilities","liabilities:debts"]+      ,test "equity"      $ expectEq (namesfrom journalEquityAccountQuery)    []+      ,test "income"      $ expectEq (namesfrom journalIncomeAccountQuery)    ["income","income:gifts","income:salary"]+      ,test "expenses"    $ expectEq (namesfrom journalExpenseAccountQuery)   ["expenses","expenses:food","expenses:supplies"]+    ]++  ]
Hledger/Data/Ledger.hs view
@@ -7,15 +7,31 @@  -} -module Hledger.Data.Ledger+{-# LANGUAGE OverloadedStrings #-}++module Hledger.Data.Ledger (+   nullledger+  ,ledgerFromJournal+  ,ledgerAccountNames+  ,ledgerAccount+  ,ledgerRootAccount+  ,ledgerTopAccounts+  ,ledgerLeafAccounts+  ,ledgerAccountsMatching+  ,ledgerPostings+  ,ledgerDateSpan+  ,ledgerCommodities+  ,tests_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 +import Hledger.Utils.Test  import Hledger.Data.Types import Hledger.Data.Account import Hledger.Data.Journal@@ -26,7 +42,7 @@ instance Show Ledger where     show l = printf "Ledger with %d transactions, %d accounts\n" --"%s"              (length (jtxns $ ljournal l) +-              length (jmodifiertxns $ ljournal l) ++              length (jtxnmodifiers $ ljournal l) +               length (jperiodictxns $ ljournal l))              (length $ ledgerAccountNames l)              -- (showtree $ ledgerAccountNameTree l)@@ -47,7 +63,7 @@     (q',depthq)  = (filterQuery (not . queryIsDepth) q, filterQuery queryIsDepth q)     j'  = filterJournalAmounts (filterQuery queryIsSym q) $ -- remove amount parts which the query's sym: terms would exclude           filterJournalPostings q' j-    as  = map (accountSetCodeFrom j) $ accountsFromPostings $ journalPostings j'+    as  = accountsFromPostings $ journalPostings j'     j'' = filterJournalPostings depthq j'  -- | List a ledger's account names.@@ -89,13 +105,14 @@ ledgerCommodities :: Ledger -> [CommoditySymbol] ledgerCommodities = M.keys . jinferredcommodities . ljournal +-- tests -tests_ledgerFromJournal = [- "ledgerFromJournal" ~: do-  assertEqual "" (0) (length $ ledgerPostings $ ledgerFromJournal Any nulljournal)-  assertEqual "" (13) (length $ ledgerPostings $ ledgerFromJournal Any samplejournal)-  assertEqual "" (7) (length $ ledgerPostings $ ledgerFromJournal (Depth 2) samplejournal)- ]+tests_Ledger = tests "Ledger" [ -tests_Hledger_Data_Ledger = TestList $-    tests_ledgerFromJournal+  tests "ledgerFromJournal" [+     (length $ ledgerPostings $ ledgerFromJournal Any nulljournal) `is` 0+    ,(length $ ledgerPostings $ ledgerFromJournal Any samplejournal) `is` 13+    ,(length $ ledgerPostings $ ledgerFromJournal (Depth 2) samplejournal) `is` 7+  ]++ ]
Hledger/Data/MarketPrice.hs view
@@ -13,7 +13,6 @@ module Hledger.Data.MarketPrice where import qualified Data.Text as T-import Test.HUnit  import Hledger.Data.Amount import Hledger.Data.Dates@@ -28,5 +27,3 @@     , T.unpack (mpcommodity mp)     , (showAmount . setAmountPrecision maxprecision) (mpamount mp)     ]--tests_Hledger_Data_MarketPrice = TestList []
+ Hledger/Data/PeriodicTransaction.hs view
@@ -0,0 +1,243 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE StandaloneDeriving #-}+{-|++A 'PeriodicTransaction' is a rule describing recurring transactions.++-}+module Hledger.Data.PeriodicTransaction (+    runPeriodicTransaction+  , checkPeriodicTransactionStartDate+)+where++#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Data.Text as T+import Text.Printf++import Hledger.Data.Types+import Hledger.Data.Dates+import Hledger.Data.Amount+import Hledger.Data.Posting (post)+import Hledger.Data.Transaction+import Hledger.Utils.UTF8IOCompat (error')+-- import Hledger.Utils.Debug++-- $setup+-- >>> :set -XOverloadedStrings+-- >>> import Hledger.Data.Posting+-- >>> import Hledger.Data.Journal++-- doctest helper, too much hassle to define in the comment+-- XXX duplicates some logic in periodictransactionp+_ptgen str = do+  let +    t = T.pack str+    (i,s) = parsePeriodExpr' nulldate t+  case checkPeriodicTransactionStartDate i s t of+    Just e  -> error' e+    Nothing ->+      mapM_ (putStr . showTransaction) $+        runPeriodicTransaction+          nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } +          nulldatespan+++--deriving instance Show PeriodicTransaction+-- for better pretty-printing:+instance Show PeriodicTransaction where+  show PeriodicTransaction{..} =+    printf "PeriodicTransactionPP {%s, %s, %s, %s, %s, %s, %s, %s, %s}"+      ("ptperiodexpr=" ++ show ptperiodexpr)+      ("ptinterval=" ++ show ptinterval)+      ("ptspan=" ++ show (show ptspan))+      ("ptstatus=" ++ show (show ptstatus))+      ("ptcode=" ++ show ptcode)+      ("ptdescription=" ++ show ptdescription)+      ("ptcomment=" ++ show ptcomment)+      ("pttags=" ++ show pttags)+      ("ptpostings=" ++ show ptpostings)++-- A basic human-readable rendering.+--showPeriodicTransaction t = "~ " ++ T.unpack (ptperiodexpr t) ++ "\n" ++ unlines (map show (ptpostings t))++--nullperiodictransaction is defined in Types.hs++-- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan'+--+-- Note that new transactions require 'txnTieKnot' post-processing.+--+-- >>> _ptgen "monthly from 2017/1 to 2017/4"+-- 2017/01/01+--     ; recur: monthly from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+-- 2017/02/01+--     ; recur: monthly from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+-- 2017/03/01+--     ; recur: monthly from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen "monthly from 2017/1 to 2017/5"+-- 2017/01/01+--     ; recur: monthly from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/02/01+--     ; recur: monthly from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/03/01+--     ; recur: monthly from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/04/01+--     ; recur: monthly from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen "every 2nd day of month from 2017/02 to 2017/04"+-- 2017/01/02+--     ; recur: every 2nd day of month from 2017/02 to 2017/04+--     a           $1.00+-- <BLANKLINE>+-- 2017/02/02+--     ; recur: every 2nd day of month from 2017/02 to 2017/04+--     a           $1.00+-- <BLANKLINE>+-- 2017/03/02+--     ; recur: every 2nd day of month from 2017/02 to 2017/04+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen "every 30th day of month from 2017/1 to 2017/5"+-- 2016/12/30+--     ; recur: every 30th day of month from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/01/30+--     ; recur: every 30th day of month from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/02/28+--     ; recur: every 30th day of month from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/03/30+--     ; recur: every 30th day of month from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+-- 2017/04/30+--     ; recur: every 30th day of month from 2017/1 to 2017/5+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen "every 2nd Thursday of month from 2017/1 to 2017/4"+-- 2016/12/08+--     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+-- 2017/01/12+--     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+-- 2017/02/09+--     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+-- 2017/03/09+--     ; recur: every 2nd Thursday of month from 2017/1 to 2017/4+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen "every nov 29th from 2017 to 2019"+-- 2016/11/29+--     ; recur: every nov 29th from 2017 to 2019+--     a           $1.00+-- <BLANKLINE>+-- 2017/11/29+--     ; recur: every nov 29th from 2017 to 2019+--     a           $1.00+-- <BLANKLINE>+-- 2018/11/29+--     ; recur: every nov 29th from 2017 to 2019+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen "2017/1"+-- 2017/01/01+--     ; recur: 2017/1+--     a           $1.00+-- <BLANKLINE>+--+-- >>> _ptgen ""+-- *** Exception: failed to parse...+-- ...+--+-- >>> _ptgen "weekly from 2017"+-- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the week+--+-- >>> _ptgen "monthly from 2017/5/4"+-- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month        +--+-- >>> _ptgen "every quarter from 2017/1/2"+-- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter        +--+-- >>> _ptgen "yearly from 2017/1/14"+-- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year        +--+-- >>> let reportperiod="daily from 2018/01/03" in let (i,s) = parsePeriodExpr' nulldate reportperiod in runPeriodicTransaction (nullperiodictransaction{ptperiodexpr=reportperiod, ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1]}) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03"))+-- []+--+runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]+runPeriodicTransaction PeriodicTransaction{..} requestedspan =+    [ t{tdate=d} | (DateSpan (Just d) _) <- ptinterval `splitSpan` spantofill ]+  where+    spantofill = spanIntervalIntersect ptinterval ptspan requestedspan+    t = nulltransaction{+           tstatus      = ptstatus+          ,tcode        = ptcode+          ,tdescription = ptdescription +          ,tcomment     = (if T.null ptcomment then "\n" else ptcomment) <> "recur: " <> ptperiodexpr+          ,ttags        = ("recur", ptperiodexpr) : pttags +          ,tpostings    = ptpostings+          }++-- | Check that this date span begins at a boundary of this interval, +-- or return an explanatory error message including the provided period expression+-- (from which the span and interval are derived).+checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String +checkPeriodicTransactionStartDate i s periodexpr = +  case (i, spanStart s) of+    (Weeks _,    Just d) -> checkStart d "week"+    (Months _,   Just d) -> checkStart d "month"+    (Quarters _, Just d) -> checkStart d "quarter"+    (Years _,    Just d) -> checkStart d "year"+    _                    -> Nothing +    where+      checkStart d x =+        let firstDate = fixSmartDate d ("","this",x) +        in   +         if d == firstDate +         then Nothing+         else Just $+          "Unable to generate transactions according to "++show (T.unpack periodexpr)+          ++" because "++show d++" is not a first day of the "++x++---- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?+--periodTransactionInterval :: PeriodicTransaction -> Maybe Interval+--periodTransactionInterval pt =+--  let+--    expr = ptperiodexpr pt+--    err  = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)+--  in+--    case parsePeriodExpr err expr of+--      Left _      -> Nothing+--      Right (i,_) -> Just i
Hledger/Data/Posting.hs view
@@ -54,7 +54,7 @@   showPosting,   -- * misc.   showComment,-  tests_Hledger_Data_Posting+  tests_Posting ) where import Data.List@@ -68,16 +68,14 @@ import qualified Data.Text as T import Data.Time.Calendar import Safe-import Test.HUnit -import Hledger.Utils+import Hledger.Utils  import Hledger.Data.Types import Hledger.Data.Amount import Hledger.Data.AccountName import Hledger.Data.Dates (nulldate, spanContainsDate)  -instance Show Posting where show = showPosting  nullposting, posting :: Posting nullposting = Posting@@ -293,28 +291,35 @@ aliasReplace (RegexAlias re repl) a = T.pack $ regexReplaceCIMemo re repl $ T.unpack a -- XXX  -tests_Hledger_Data_Posting = TestList [+-- tests -  "accountNamePostingType" ~: do+tests_Posting = tests "Posting" [++  tests "accountNamePostingType" [     accountNamePostingType "a" `is` RegularPosting-    accountNamePostingType "(a)" `is` VirtualPosting-    accountNamePostingType "[a]" `is` BalancedVirtualPosting+    ,accountNamePostingType "(a)" `is` VirtualPosting+    ,accountNamePostingType "[a]" `is` BalancedVirtualPosting+  ] - ,"accountNameWithoutPostingType" ~: do+ ,tests "accountNameWithoutPostingType" [     accountNameWithoutPostingType "(a)" `is` "a"+  ] - ,"accountNameWithPostingType" ~: do+ ,tests "accountNameWithPostingType" [     accountNameWithPostingType VirtualPosting "[a]" `is` "(a)"+  ] - ,"joinAccountNames" ~: do+ ,tests "joinAccountNames" [     "a" `joinAccountNames` "b:c" `is` "a:b:c"-    "a" `joinAccountNames` "(b:c)" `is` "(a:b:c)"-    "[a]" `joinAccountNames` "(b:c)" `is` "[a:b:c]"-    "" `joinAccountNames` "a" `is` "a"+    ,"a" `joinAccountNames` "(b:c)" `is` "(a:b:c)"+    ,"[a]" `joinAccountNames` "(b:c)" `is` "[a:b:c]"+    ,"" `joinAccountNames` "a" `is` "a"+  ] - ,"concatAccountNames" ~: do+ ,tests "concatAccountNames" [     concatAccountNames [] `is` ""-    concatAccountNames ["a","(b)","[c:d]"] `is` "(a:b:c:d)"+    ,concatAccountNames ["a","(b)","[c:d]"] `is` "(a:b:c:d)"+  ]   ] 
Hledger/Data/RawOptions.hs view
@@ -17,7 +17,8 @@   maybestringopt,   listofstringopt,   intopt,-  maybeintopt+  maybeintopt,+  maybecharopt ) where @@ -49,6 +50,9 @@  stringopt :: String -> RawOpts -> String stringopt name = fromMaybe "" . maybestringopt name++maybecharopt :: String -> RawOpts -> Maybe Char+maybecharopt name rawopts = lookup name rawopts >>= headMay  listofstringopt :: String -> RawOpts -> [String] listofstringopt name rawopts = [v | (k,v) <- rawopts, k==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, TypeFamilies, PackageImports #-}+{-# LANGUAGE FlexibleContexts, OverloadedStrings, TypeFamilies, PackageImports #-}  module Hledger.Data.StringFormat (           parseStringFormat@@ -10,7 +10,7 @@         , StringFormat(..)         , StringFormatComponent(..)         , ReportItemField(..)-        , tests_Hledger_Data_StringFormat+        , tests_StringFormat         ) where  import Prelude ()@@ -18,12 +18,13 @@ import Numeric import Data.Char (isPrint) import Data.Maybe-import Test.HUnit+import qualified Data.Text as T import Text.Megaparsec import Text.Megaparsec.Char  import Hledger.Utils.Parse import Hledger.Utils.String (formatString)+import Hledger.Utils.Test  -- | A format specification/template to use when rendering a report line item as text. --@@ -89,7 +90,7 @@  stringformatp :: SimpleStringParser StringFormat stringformatp = do-  alignspec <- optional (try $ char '%' >> oneOf "^_,")+  alignspec <- optional (try $ char '%' >> oneOf ("^_,"::String))   let constructor =         case alignspec of           Just '^' -> TopAligned@@ -136,47 +137,45 @@  ---------------------------------------------------------------------- -testFormat :: StringFormatComponent -> String -> String -> Assertion-testFormat fs value expected = assertEqual name expected actual-    where-        (name, actual) = case fs of-            FormatLiteral l -> ("literal", formatString False Nothing Nothing l)-            FormatField leftJustify min max _ -> ("field", formatString leftJustify min max value)--testParser :: String -> StringFormat -> Assertion-testParser s expected = case (parseStringFormat s) of-    Left  error -> assertFailure $ show error-    Right actual -> assertEqual ("Input: " ++ s) expected actual+formatStringTester fs value expected = actual `is` expected +  where+    actual = case fs of+      FormatLiteral l                   -> formatString False Nothing Nothing l+      FormatField leftJustify min max _ -> formatString leftJustify min max value -tests_Hledger_Data_StringFormat = test [ formattingTests ++ parserTests ]+tests_StringFormat = tests "StringFormat" [ -formattingTests = [-      testFormat (FormatLiteral " ")                                ""            " "-    , testFormat (FormatField False Nothing Nothing DescriptionField)    "description" "description"-    , testFormat (FormatField False (Just 20) Nothing DescriptionField)  "description" "         description"-    , testFormat (FormatField False Nothing (Just 20) DescriptionField)  "description" "description"-    , testFormat (FormatField True Nothing (Just 20) DescriptionField)   "description" "description"-    , testFormat (FormatField True (Just 20) Nothing DescriptionField)   "description" "description         "-    , testFormat (FormatField True (Just 20) (Just 20) DescriptionField) "description" "description         "-    , testFormat (FormatField True Nothing (Just 3) DescriptionField)    "description" "des"+   tests "formatStringHelper" [+      formatStringTester (FormatLiteral " ")                                     ""            " "+    , formatStringTester (FormatField False Nothing Nothing DescriptionField)    "description" "description"+    , formatStringTester (FormatField False (Just 20) Nothing DescriptionField)  "description" "         description"+    , formatStringTester (FormatField False Nothing (Just 20) DescriptionField)  "description" "description"+    , formatStringTester (FormatField True Nothing (Just 20) DescriptionField)   "description" "description"+    , formatStringTester (FormatField True (Just 20) Nothing DescriptionField)   "description" "description         "+    , formatStringTester (FormatField True (Just 20) (Just 20) DescriptionField) "description" "description         "+    , formatStringTester (FormatField True Nothing (Just 3) DescriptionField)    "description" "des"     ] -parserTests = [-      testParser ""                             (defaultStringFormatStyle [])-    , testParser "D"                            (defaultStringFormatStyle [FormatLiteral "D"])-    , testParser "%(date)"                      (defaultStringFormatStyle [FormatField False Nothing Nothing DescriptionField])-    , testParser "%(total)"                     (defaultStringFormatStyle [FormatField False Nothing Nothing TotalField])-    , testParser "^%(total)"                    (TopAligned [FormatField False Nothing Nothing TotalField])-    , testParser "_%(total)"                    (BottomAligned [FormatField False Nothing Nothing TotalField])-    , testParser ",%(total)"                    (OneLine [FormatField False Nothing Nothing TotalField])-    , testParser "Hello %(date)!"               (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])-    , testParser "%-(date)"                     (defaultStringFormatStyle [FormatField True Nothing Nothing DescriptionField])-    , testParser "%20(date)"                    (defaultStringFormatStyle [FormatField False (Just 20) Nothing DescriptionField])-    , testParser "%.10(date)"                   (defaultStringFormatStyle [FormatField False Nothing (Just 10) DescriptionField])-    , testParser "%20.10(date)"                 (defaultStringFormatStyle [FormatField False (Just 20) (Just 10) DescriptionField])-    , testParser "%20(account) %.10(total)\n"   (defaultStringFormatStyle [FormatField False (Just 20) Nothing AccountField-                                                , FormatLiteral " "-                                                , FormatField False Nothing (Just 10) TotalField-                                                , FormatLiteral "\n"-                                                ])-  ]+  ,tests "parseStringFormat" $+    let s `gives` expected = test (T.pack s) $ parseStringFormat s `is` Right expected+    in [+      ""                           `gives` (defaultStringFormatStyle [])+    , "D"                          `gives` (defaultStringFormatStyle [FormatLiteral "D"])+    , "%(date)"                    `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing DescriptionField])+    , "%(total)"                   `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing TotalField])+    -- TODO+    -- , "^%(total)"                  `gives` (TopAligned [FormatField False Nothing Nothing TotalField])+    -- , "_%(total)"                  `gives` (BottomAligned [FormatField False Nothing Nothing TotalField])+    -- , ",%(total)"                  `gives` (OneLine [FormatField False Nothing Nothing TotalField])+    , "Hello %(date)!"             `gives` (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])+    , "%-(date)"                   `gives` (defaultStringFormatStyle [FormatField True Nothing Nothing DescriptionField])+    , "%20(date)"                  `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing DescriptionField])+    , "%.10(date)"                 `gives` (defaultStringFormatStyle [FormatField False Nothing (Just 10) DescriptionField])+    , "%20.10(date)"               `gives` (defaultStringFormatStyle [FormatField False (Just 20) (Just 10) DescriptionField])+    , "%20(account) %.10(total)"   `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing AccountField+                                                                     ,FormatLiteral " "+                                                                     ,FormatField False Nothing (Just 10) TotalField+                                                                     ])+    , test "newline not parsed" $ expectLeft $ parseStringFormat "\n"+    ]+ ]
Hledger/Data/Timeclock.hs view
@@ -8,8 +8,12 @@  {-# LANGUAGE CPP, OverloadedStrings #-} -module Hledger.Data.Timeclock+module Hledger.Data.Timeclock (+   timeclockEntriesToTransactions+  ,tests_Timeclock+) where+ import Data.Maybe -- import Data.Text (Text) import qualified Data.Text as T@@ -20,10 +24,9 @@ #if !(MIN_VERSION_time(1,5,0)) import System.Locale (defaultTimeLocale) #endif-import Test.HUnit import Text.Printf -import Hledger.Utils+import Hledger.Utils  import Hledger.Data.Types import Hledger.Data.Dates import Hledger.Data.Amount@@ -108,38 +111,35 @@       ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]  -tests_Hledger_Data_Timeclock = TestList [+-- tests -   "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 .+tests_Timeclock = tests "Timeclock" [+  do+   today <- io getCurrentDay+   now' <- io getCurrentTime+   tz <- io 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"+                  parseTimeM True defaultTimeLocale "%H:%M:%S" #else-                    parseTime defaultTimeLocale "%H:%M:%S"+                  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]-+       showtime = formatTime defaultTimeLocale "%H:%M"+       txndescs = map (T.unpack . tdescription) . timeclockEntriesToTransactions now +       future = utcToLocalTime tz $ addUTCTime 100 now'+       futurestr = showtime future+   tests "timeclockEntriesToTransactions" [ +     test "started yesterday, split session at midnight" $+      txndescs [clockin (mktime yesterday "23:00:00") "" ""] `is` ["23:00-23:59","00:00-"++nowstr]+     ,test "split multi-day sessions at each midnight" $+      txndescs [clockin (mktime (addDays (-2) today) "23:00:00") "" ""] `is `["23:00-23:59","00:00-23:59","00:00-"++nowstr]+     ,test "auto-clock-out if needed" $+      txndescs [clockin (mktime today "00:00:00") "" ""] `is` ["00:00-"++nowstr]+     ,test "use the clockin time for auto-clockout if it's in the future" $+      txndescs [clockin future "" ""] `is` [printf "%s-%s" futurestr futurestr]+     ]  ]
Hledger/Data/Transaction.hs view
@@ -42,8 +42,8 @@   sourceFilePath,   sourceFirstLine,   showGenericSourcePos,-  -- * misc.-  tests_Hledger_Data_Transaction+  -- * tests+  tests_Transaction ) where import Data.List@@ -53,24 +53,15 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar-import Test.HUnit import Text.Printf import qualified Data.Map as Map -import Hledger.Utils+import Hledger.Utils  import Hledger.Data.Types import Hledger.Data.Dates import Hledger.Data.Posting import Hledger.Data.Amount -instance Show Transaction where show = showTransactionUnelided--instance Show ModifierTransaction where-    show t = "= " ++ T.unpack (mtvalueexpr t) ++ "\n" ++ unlines (map show (mtpostings t))--instance Show PeriodicTransaction where-    show t = "~ " ++ T.unpack (ptperiodexpr t) ++ "\n" ++ unlines (map show (ptpostings t))- sourceFilePath :: GenericSourcePos -> FilePath sourceFilePath = \case     GenericSourcePos fp _ _ -> fp@@ -87,7 +78,7 @@     JournalSourcePos fp (line, line') -> show fp ++ " (lines " ++ show line ++ "-" ++ show line' ++ ")"  nullsourcepos :: GenericSourcePos-nullsourcepos = GenericSourcePos "" 1 1+nullsourcepos = JournalSourcePos "" (1,1)  nulltransaction :: Transaction nulltransaction = Transaction {@@ -126,40 +117,6 @@ showTransactionUnelided :: Transaction -> String showTransactionUnelided = showTransactionHelper False False -tests_showTransactionUnelided = [-   "showTransactionUnelided" ~: do-    let t `gives` s = assertEqual "" s (showTransactionUnelided t)-    nulltransaction `gives` "0000/01/01\n\n"-    nulltransaction{-      tdate=parsedate "2012/05/14",-      tdate2=Just $ parsedate "2012/05/15",-      tstatus=Unmarked,-      tcode="code",-      tdescription="desc",-      tcomment="tcomment1\ntcomment2\n",-      ttags=[("ttag1","val1")],-      tpostings=[-        nullposting{-          pstatus=Cleared,-          paccount="a",-          pamount=Mixed [usd 1, hrs 2],-          pcomment="\npcomment2\n",-          ptype=RegularPosting,-          ptags=[("ptag1","val1"),("ptag2","val2")]-          }-       ]-      }-      `gives` unlines [-      "2012/05/14=2012/05/15 (code) desc    ; tcomment1",-      "    ; tcomment2",-      "    * a         $1.00",-      "    ; pcomment2",-      "    * a         2.00h",-      "    ; pcomment2",-      ""-      ]- ]- showTransactionUnelidedOneLineAmounts :: Transaction -> String showTransactionUnelidedOneLineAmounts = showTransactionHelper False True @@ -252,60 +209,6 @@     ps | Just t <- ptransaction p = tpostings t        | otherwise = [p] -tests_postingAsLines = [-   "postingAsLines" ~: do-    let p `gives` ls = assertEqual (show p) ls (postingAsLines False False [p] p)-    posting `gives` [""]-    posting{-      pstatus=Cleared,-      paccount="a",-      pamount=Mixed [usd 1, hrs 2],-      pcomment="pcomment1\npcomment2\n  tag3: val3  \n",-      ptype=RegularPosting,-      ptags=[("ptag1","val1"),("ptag2","val2")]-      }-     `gives` [-      "    * a         $1.00    ; pcomment1",-      "    ; pcomment2",-      "    ;   tag3: val3  ",-      "    * a         2.00h    ; pcomment1",-      "    ; pcomment2",-      "    ;   tag3: val3  "-      ]- ]--tests_inference = [-    "inferBalancingAmount" ~: do-    let p `gives` p' = assertEqual (show p) (Right p') $ inferTransaction p-        inferTransaction :: Transaction -> Either String Transaction-        inferTransaction = runIdentity . runExceptT . inferBalancingAmount (\_ _ -> return ()) Map.empty-    nulltransaction `gives` nulltransaction-    nulltransaction{-      tpostings=[-        "a" `post` usd (-5),-        "b" `post` missingamt-      ]}-      `gives`-      nulltransaction{-        tpostings=[-          "a" `post` usd (-5),-          "b" `post` usd 5-        ]}-    nulltransaction{-      tpostings=[-        "a" `post` usd (-5),-        "b" `post` (eur 3 @@ usd 4),-        "c" `post` missingamt-      ]}-      `gives`-      nulltransaction{-        tpostings=[-          "a" `post` usd (-5),-          "b" `post` (eur 3 @@ usd 4),-          "c" `post` usd 1-        ]}- ]- indent :: String -> String indent = ("    "++) @@ -537,178 +440,273 @@ postingSetTransaction :: Transaction -> Posting -> Posting postingSetTransaction t p = p{ptransaction=Just t} -tests_Hledger_Data_Transaction = TestList $ concat [-  tests_postingAsLines,-  tests_showTransactionUnelided,-  tests_inference,-  [-  "showTransaction" ~: do-     assertEqual "show a balanced transaction, eliding last amount"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries          $47.18"-        ,"    assets:checking"-        ,""-        ])-       (let t = Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []+-- tests++tests_Transaction = tests "Transaction" [++  tests "showTransactionUnelided" [+    showTransactionUnelided nulltransaction `is` "0000/01/01\n\n"+    ,showTransactionUnelided nulltransaction{+      tdate=parsedate "2012/05/14",+      tdate2=Just $ parsedate "2012/05/15",+      tstatus=Unmarked,+      tcode="code",+      tdescription="desc",+      tcomment="tcomment1\ntcomment2\n",+      ttags=[("ttag1","val1")],+      tpostings=[+        nullposting{+          pstatus=Cleared,+          paccount="a",+          pamount=Mixed [usd 1, hrs 2],+          pcomment="\npcomment2\n",+          ptype=RegularPosting,+          ptags=[("ptag1","val1"),("ptag2","val2")]+          }+       ]+      }+      `is` unlines [+      "2012/05/14=2012/05/15 (code) desc    ; tcomment1",+      "    ; tcomment2",+      "    * a         $1.00",+      "    ; pcomment2",+      "    * a         2.00h",+      "    ; pcomment2",+      ""+      ]+    ]++  ,tests "postingAsLines" [+    postingAsLines False False [posting] posting `is` [""]+    ,let p = posting{+      pstatus=Cleared,+      paccount="a",+      pamount=Mixed [usd 1, hrs 2],+      pcomment="pcomment1\npcomment2\n  tag3: val3  \n",+      ptype=RegularPosting,+      ptags=[("ptag1","val1"),("ptag2","val2")]+      }+     in postingAsLines False False [p] p `is`+      [+      "    * a         $1.00    ; pcomment1",+      "    ; pcomment2",+      "    ;   tag3: val3  ",+      "    * a         2.00h    ; pcomment1",+      "    ; pcomment2",+      "    ;   tag3: val3  "+      ]+    ]++  ,do+    let inferTransaction :: Transaction -> Either String Transaction+        inferTransaction = runIdentity . runExceptT . inferBalancingAmount (\_ _ -> return ()) Map.empty+    tests "inferBalancingAmount" [ +       inferTransaction nulltransaction `is` Right nulltransaction+      ,inferTransaction nulltransaction{+        tpostings=[+          "a" `post` usd (-5),+          "b" `post` missingamt+        ]}+      `is` Right+        nulltransaction{+          tpostings=[+            "a" `post` usd (-5),+            "b" `post` usd 5+          ]}+      ,inferTransaction nulltransaction{+        tpostings=[+          "a" `post` usd (-5),+          "b" `post` (eur 3 @@ usd 4),+          "c" `post` missingamt+        ]}+      `is` Right+        nulltransaction{+          tpostings=[+            "a" `post` usd (-5),+            "b" `post` (eur 3 @@ usd 4),+            "c" `post` usd 1+          ]}+      ]++  ,tests "showTransaction" [+     test "show a balanced transaction, eliding last amount" $+       let t = Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []                 [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18], ptransaction=Just t}                 ,posting{paccount="assets:checking", pamount=Mixed [usd (-47.18)], ptransaction=Just t}                 ] ""-        in showTransaction t)+       in +        showTransaction t+         `is`+         unlines+          ["2007/01/28 coopportunity"+          ,"    expenses:food:groceries          $47.18"+          ,"    assets:checking"+          ,""+          ]+    ] -  ,"showTransaction" ~: do-     assertEqual "show a balanced transaction, no eliding"-       (unlines-        ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries          $47.18"-        ,"    assets:checking                 $-47.18"-        ,""-        ])+  ,tests "showTransaction" [+     test "show a balanced transaction, no eliding" $        (let t = Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []                 [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18], ptransaction=Just t}                 ,posting{paccount="assets:checking", pamount=Mixed [usd (-47.18)], ptransaction=Just t}                 ] ""         in showTransactionUnelided t)--     -- document some cases that arise in debug/testing:-  ,"showTransaction" ~: do-     assertEqual "show an unbalanced transaction, should not elide"+       `is`        (unlines         ["2007/01/28 coopportunity"         ,"    expenses:food:groceries          $47.18"-        ,"    assets:checking                 $-47.19"+        ,"    assets:checking                 $-47.18"         ,""         ])++     -- document some cases that arise in debug/testing:+    ,test "show an unbalanced transaction, should not elide" $        (showTransaction         (txnTieKnot $ Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []          [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18]}          ,posting{paccount="assets:checking", pamount=Mixed [usd (-47.19)]}          ] ""))--  ,"showTransaction" ~: do-     assertEqual "show an unbalanced transaction with one posting, should not elide"+       `is`        (unlines         ["2007/01/28 coopportunity"         ,"    expenses:food:groceries          $47.18"+        ,"    assets:checking                 $-47.19"         ,""         ])++    ,test "show an unbalanced transaction with one posting, should not elide" $        (showTransaction         (txnTieKnot $ Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []          [posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18]}          ] ""))--  ,"showTransaction" ~: do-     assertEqual "show a transaction with one posting and a missing amount"+       `is`        (unlines         ["2007/01/28 coopportunity"-        ,"    expenses:food:groceries"+        ,"    expenses:food:groceries          $47.18"         ,""         ])++    ,test "show a transaction with one posting and a missing amount" $        (showTransaction         (txnTieKnot $ Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []          [posting{paccount="expenses:food:groceries", pamount=missingmixedamt}          ] ""))--  ,"showTransaction" ~: do-     assertEqual "show a transaction with a priced commodityless amount"+       `is`        (unlines-        ["2010/01/01 x"-        ,"    a          1 @ $2"-        ,"    b"+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries"         ,""         ])++    ,test "show a transaction with a priced commodityless amount" $        (showTransaction         (txnTieKnot $ Transaction 0 nullsourcepos (parsedate "2010/01/01") Nothing Unmarked "" "x" "" []          [posting{paccount="a", pamount=Mixed [num 1 `at` (usd 2 `withPrecision` 0)]}          ,posting{paccount="b", pamount= missingmixedamt}          ] ""))+       `is`+       (unlines+        ["2010/01/01 x"+        ,"    a          1 @ $2"+        ,"    b"+        ,""+        ])+    ] -  ,"balanceTransaction" ~: do-     assertBool "detect unbalanced entry, sign error"-                    (isLeft $ balanceTransaction Nothing+  ,tests "balanceTransaction" [+     test "detect unbalanced entry, sign error" $+                    (expectLeft $ balanceTransaction Nothing                            (Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "test" "" []                             [posting{paccount="a", pamount=Mixed [usd 1]}                             ,posting{paccount="b", pamount=Mixed [usd 1]}                             ] "")) -     assertBool "detect unbalanced entry, multiple missing amounts"-                    (isLeft $ balanceTransaction Nothing+    ,test "detect unbalanced entry, multiple missing amounts" $+                    (expectLeft $ balanceTransaction Nothing                            (Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "test" "" []                             [posting{paccount="a", pamount=missingmixedamt}                             ,posting{paccount="b", pamount=missingmixedamt}                             ] "")) -     let e = balanceTransaction Nothing (Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "" "" []-                           [posting{paccount="a", pamount=Mixed [usd 1]}-                           ,posting{paccount="b", pamount=missingmixedamt}-                           ] "")-     assertBool "balanceTransaction allows one missing amount" (isRight e)-     assertEqual "balancing amount is inferred"-                     (Mixed [usd (-1)])-                     (case e of-                        Right e' -> (pamount $ last $ tpostings e')-                        Left _ -> error' "should not happen")+    ,test "one missing amount is inferred" $+         (pamount . last . tpostings <$> balanceTransaction+           Nothing+           (Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "" "" []+             [posting{paccount="a", pamount=Mixed [usd 1]}+             ,posting{paccount="b", pamount=missingmixedamt}+             ] ""))+         `is` Right (Mixed [usd (-1)]) -     let e = balanceTransaction Nothing (Transaction 0 nullsourcepos (parsedate "2011/01/01") Nothing Unmarked "" "" "" []-                           [posting{paccount="a", pamount=Mixed [usd 1.35]}-                           ,posting{paccount="b", pamount=Mixed [eur (-1)]}-                           ] "")-     assertBool "balanceTransaction can infer conversion price" (isRight e)-     assertEqual "balancing conversion price is inferred"-                     (Mixed [usd 1.35 @@ (eur 1 `withPrecision` maxprecision)])-                     (case e of-                        Right e' -> (pamount $ head $ tpostings e')-                        Left _ -> error' "should not happen")+    ,test "conversion price is inferred" $+         (pamount . head . tpostings <$> balanceTransaction+           Nothing+           (Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "" "" []+             [posting{paccount="a", pamount=Mixed [usd 1.35]}+             ,posting{paccount="b", pamount=Mixed [eur (-1)]}+             ] ""))+         `is` Right (Mixed [usd 1.35 @@ (eur 1 `withPrecision` maxprecision)]) -     assertBool "balanceTransaction balances based on cost if there are unit prices" (isRight $+    ,test "balanceTransaction balances based on cost if there are unit prices" $+       expectRight $        balanceTransaction Nothing (Transaction 0 nullsourcepos (parsedate "2011/01/01") Nothing Unmarked "" "" "" []                            [posting{paccount="a", pamount=Mixed [usd 1 `at` eur 2]}                            ,posting{paccount="a", pamount=Mixed [usd (-2) `at` eur 1]}-                           ] ""))+                           ] "") -     assertBool "balanceTransaction balances based on cost if there are total prices" (isRight $+    ,test "balanceTransaction balances based on cost if there are total prices" $+       expectRight $        balanceTransaction Nothing (Transaction 0 nullsourcepos (parsedate "2011/01/01") Nothing Unmarked "" "" "" []                            [posting{paccount="a", pamount=Mixed [usd 1    @@ eur 1]}                            ,posting{paccount="a", pamount=Mixed [usd (-2) @@ eur 1]}-                           ] ""))+                           ] "")+  ] -  ,"isTransactionBalanced" ~: do-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}-             ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}+  ,tests "isTransactionBalanced" [+     test "detect balanced" $ expect $+       isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 1.00]}+             ,posting{paccount="c", pamount=Mixed [usd (-1.00)]}              ] ""-     assertBool "detect balanced" (isTransactionBalanced Nothing t)-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}-             ,posting{paccount="c", pamount=Mixed [usd (-1.01)], ptransaction=Just t}+     +    ,test "detect unbalanced" $ expect $+       not $ isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 1.00]}+             ,posting{paccount="c", pamount=Mixed [usd (-1.01)]}              ] ""-     assertBool "detect unbalanced" (not $ isTransactionBalanced Nothing t)-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}+     +    ,test "detect unbalanced, one posting" $ expect $+       not $ isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 1.00]}              ] ""-     assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced Nothing t)-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 0], ptransaction=Just t}+     +    ,test "one zero posting is considered balanced for now" $ expect $+       isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 0]}              ] ""-     assertBool "one zero posting is considered balanced for now" (isTransactionBalanced Nothing t)-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}-             ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}-             ,posting{paccount="d", pamount=Mixed [usd 100], ptype=VirtualPosting, ptransaction=Just t}+     +    ,test "virtual postings don't need to balance" $ expect $+       isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 1.00]}+             ,posting{paccount="c", pamount=Mixed [usd (-1.00)]}+             ,posting{paccount="d", pamount=Mixed [usd 100], ptype=VirtualPosting}              ] ""-     assertBool "virtual postings don't need to balance" (isTransactionBalanced Nothing t)-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}-             ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}-             ,posting{paccount="d", pamount=Mixed [usd 100], ptype=BalancedVirtualPosting, ptransaction=Just t}+     +    ,test "balanced virtual postings need to balance among themselves" $ expect $+       not $ isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 1.00]}+             ,posting{paccount="c", pamount=Mixed [usd (-1.00)]}+             ,posting{paccount="d", pamount=Mixed [usd 100], ptype=BalancedVirtualPosting}              ] ""-     assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced Nothing t)-     let t = Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []-             [posting{paccount="b", pamount=Mixed [usd 1.00], ptransaction=Just t}-             ,posting{paccount="c", pamount=Mixed [usd (-1.00)], ptransaction=Just t}-             ,posting{paccount="d", pamount=Mixed [usd 100], ptype=BalancedVirtualPosting, ptransaction=Just t}-             ,posting{paccount="3", pamount=Mixed [usd (-100)], ptype=BalancedVirtualPosting, ptransaction=Just t}+     +    ,test "balanced virtual postings need to balance among themselves (2)" $ expect $+       isTransactionBalanced Nothing $ Transaction 0 nullsourcepos (parsedate "2009/01/01") Nothing Unmarked "" "a" "" []+             [posting{paccount="b", pamount=Mixed [usd 1.00]}+             ,posting{paccount="c", pamount=Mixed [usd (-1.00)]}+             ,posting{paccount="d", pamount=Mixed [usd 100], ptype=BalancedVirtualPosting}+             ,posting{paccount="3", pamount=Mixed [usd (-100)], ptype=BalancedVirtualPosting}              ] ""-     assertBool "balanced virtual postings need to balance among themselves (2)" (isTransactionBalanced Nothing t)+     +  ] -  ]]+ ]
+ Hledger/Data/TransactionModifier.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE CPP #-}+{-|++A 'TransactionModifier' is a rule that modifies certain 'Transaction's,+typically adding automated postings to them. ++-}+module Hledger.Data.TransactionModifier (+    transactionModifierToFunction+)+where++import Data.Maybe+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Data.Text as T+import Data.Time.Calendar+import Hledger.Data.Types+import Hledger.Data.Dates+import Hledger.Data.Amount+import Hledger.Data.Transaction+import Hledger.Query+import Hledger.Utils.UTF8IOCompat (error')+-- import Hledger.Utils.Debug++-- $setup+-- >>> :set -XOverloadedStrings+-- >>> import Hledger.Data.Posting+-- >>> import Hledger.Data.Transaction+-- >>> import Hledger.Data.Journal++-- | Converts a 'TransactionModifier' to a 'Transaction'-transforming function,+-- which applies the modification(s) specified by the TransactionModifier.+-- Currently this means adding automated postings when certain other postings are present.+-- The postings of the transformed transaction will reference it in the usual +-- way (ie, 'txnTieKnot' is called).+--+-- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}+-- 0000/01/01+--     ping           $1.00+--     pong           $2.00+-- <BLANKLINE>+-- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "miss" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}+-- 0000/01/01+--     ping           $1.00+-- <BLANKLINE>+-- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "ping" ["pong" `post` amount{amultiplier=True, aquantity=3}]) nulltransaction{tpostings=["ping" `post` usd 2]}+-- 0000/01/01+--     ping           $2.00+--     pong           $6.00+-- <BLANKLINE>+--+transactionModifierToFunction :: TransactionModifier -> (Transaction -> Transaction)+transactionModifierToFunction mt = +  \t@(tpostings -> ps) -> txnTieKnot t{ tpostings=generatePostings ps } -- TODO add modifier txn comment/tags ?+  where+    q = simplifyQuery $ tmParseQuery mt (error' "a transaction modifier's query cannot depend on current date")+    mods = map tmPostingToFunction $ tmpostings mt+    generatePostings ps = [p' | p <- ps+                              , p' <- if q `matchesPosting` p then p:[ m p | m <- mods] else [p]]+    +-- | Parse the 'Query' from a 'TransactionModifier's 'tmquerytxt', +-- and return it as a function requiring the current date. +--+-- >>> tmParseQuery (TransactionModifier "" []) undefined+-- Any+-- >>> tmParseQuery (TransactionModifier "ping" []) undefined+-- Acct "ping"+-- >>> tmParseQuery (TransactionModifier "date:2016" []) undefined+-- Date (DateSpan 2016)+-- >>> tmParseQuery (TransactionModifier "date:today" []) (read "2017-01-01")+-- Date (DateSpan 2017/01/01)+tmParseQuery :: TransactionModifier -> (Day -> Query)+tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt)++---- | 'DateSpan' of all dates mentioned in 'Journal'+----+---- >>> jdatespan nulljournal+---- DateSpan -+---- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01"}] }+---- DateSpan 2016/01/01+---- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01", tpostings=[nullposting{pdate=Just $ read "2016-02-01"}]}] }+---- DateSpan 2016/01/01-2016/02/01+--jdatespan :: Journal -> DateSpan+--jdatespan j+--        | null dates = nulldatespan+--        | otherwise = DateSpan (Just $ minimum dates) (Just $ 1 `addDays` maximum dates)+--    where+--        dates = concatMap tdates $ jtxns j++---- | 'DateSpan' of all dates mentioned in 'Transaction'+----+---- >>> tdates nulltransaction+---- [0000-01-01]+--tdates :: Transaction -> [Day]+--tdates t = tdate t : concatMap pdates (tpostings t) ++ maybeToList (tdate2 t) where+--    pdates p = catMaybes [pdate p, pdate2 p]++postingScale :: Posting -> Maybe Quantity+postingScale p =+    case amounts $ pamount p of+        [a] | amultiplier a -> Just $ aquantity a+        _ -> Nothing++-- | Converts a 'TransactionModifier''s posting to a 'Posting'-generating function,+-- which will be used to make a new posting based on the old one (an "automated posting").+tmPostingToFunction :: Posting -> (Posting -> Posting)+tmPostingToFunction p' = +  \p -> renderPostingCommentDates $ p'+      { pdate = pdate p+      , pdate2 = pdate2 p+      , pamount = amount' p+      }+  where+    amount' = case postingScale p' of+        Nothing -> const $ pamount p'+        Just n -> \p -> withAmountType (head $ amounts $ pamount p') $ pamount p `divideMixedAmount` (1/n)+    withAmountType amount (Mixed as) = case acommodity amount of+        "" -> Mixed as+        c -> Mixed [a{acommodity = c, astyle = astyle amount, aprice = aprice amount} | a <- as]++renderPostingCommentDates :: Posting -> Posting+renderPostingCommentDates p = p { pcomment = comment' }+    where+        datesComment = T.concat $ catMaybes [T.pack . showDate <$> pdate p, ("=" <>) . T.pack . showDate <$> pdate2 p]+        comment'+            | T.null datesComment = pcomment p+            | otherwise = T.intercalate "\n" $ filter (not . T.null) [T.strip $ pcomment p, "[" <> datesComment <> "]"]
Hledger/Data/Types.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE CPP, DeriveDataTypeable, StandaloneDeriving, DeriveGeneric, TypeSynonymInstances, FlexibleInstances, OverloadedStrings #-}+{-# LANGUAGE DeriveDataTypeable, StandaloneDeriving, DeriveGeneric, TypeSynonymInstances, FlexibleInstances, OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-|  Most data types are defined here to avoid import cycles.@@ -25,13 +26,19 @@ import Data.Data import Data.Decimal import Data.Default+import Data.List (intercalate) import Text.Blaze (ToMarkup(..))+--XXX https://hackage.haskell.org/package/containers/docs/Data-Map.html +--Note: You should use Data.Map.Strict instead of this module if:+--You will eventually need all the values stored.+--The stored values don't represent large virtual data structures to be lazily computed. 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(..))+import Text.Printf  import Hledger.Utils.Regex @@ -126,7 +133,8 @@  -- | 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.-data Price = NoPrice | UnitPrice Amount | TotalPrice Amount deriving (Eq,Ord,Typeable,Data,Generic)+data Price = NoPrice | UnitPrice Amount | TotalPrice Amount +  deriving (Eq,Ord,Typeable,Data,Generic,Show)  instance NFData Price @@ -137,10 +145,19 @@       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)+} deriving (Eq,Ord,Read,Typeable,Data,Generic)  instance NFData AmountStyle +instance Show AmountStyle where+  show AmountStyle{..} =+    printf "AmountStylePP \"%s %s %s %s %s..\""+    (show ascommodityside)+    (show ascommodityspaced)+    (show asprecision)+    (show asdecimalpoint)+    (show asdigitgroups)+ -- | A style for displaying digit groups in the integer part of a -- floating point number. It consists of the character used to -- separate groups (comma or period, whichever is not used as decimal@@ -166,12 +183,12 @@       aquantity   :: Quantity,       aprice      :: Price,           -- ^ the (fixed) price for this amount, if any       astyle      :: AmountStyle,-      amultiplier :: Bool             -- ^ amount is a multipier for AutoTransactions-    } deriving (Eq,Ord,Typeable,Data,Generic)+      amultiplier :: Bool             -- ^ amount is a multipier used in TransactionModifier postings+    } deriving (Eq,Ord,Typeable,Data,Generic,Show)  instance NFData Amount -newtype MixedAmount = Mixed [Amount] deriving (Eq,Ord,Typeable,Data,Generic)+newtype MixedAmount = Mixed [Amount] deriving (Eq,Ord,Typeable,Data,Generic,Show)  instance NFData MixedAmount @@ -217,10 +234,27 @@ instance NFData Posting  -- The equality test for postings ignores the parent transaction's--- identity, to avoid infinite loops.+-- identity, to avoid recuring ad infinitum.+-- XXX could check that it's Just or Nothing. instance Eq Posting where     (==) (Posting a1 b1 c1 d1 e1 f1 g1 h1 i1 _ _) (Posting a2 b2 c2 d2 e2 f2 g2 h2 i2 _ _) =  a1==a2 && b1==b2 && c1==c2 && d1==d2 && e1==e2 && f1==f2 && g1==g2 && h1==h2 && i1==i2 +-- | Posting's show instance elides the parent transaction so as not to recurse forever.+instance Show Posting where+  show Posting{..} = "PostingPP {" ++ intercalate ", " [+     ("pdate="             ++ show (show pdate))+    ,("pdate2="            ++ show (show pdate2))+    ,("pstatus="           ++ show (show pstatus))+    ,("paccount="          ++ show paccount)+    ,("pamount="           ++ show pamount)+    ,("pcomment="          ++ show pcomment)+    ,("ptype="             ++ show ptype)+    ,("ptags="             ++ show ptags)+    ,("pbalanceassertion=" ++ show pbalanceassertion)+    ,("ptransaction="      ++ show (const "<txn>" <$> ptransaction))+    ,("porigin="           ++ show porigin)+    ] ++ "}"+ -- TODO: needs renaming, or removal if no longer needed. See also TextPosition in Hledger.UI.Editor -- | The position of parse errors (eg), like parsec's SourcePos but generic. data GenericSourcePos = GenericSourcePos FilePath Int Int    -- ^ file path, 1-based line number and 1-based column number.@@ -245,18 +279,23 @@       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)+    } deriving (Eq,Typeable,Data,Generic,Show)  instance NFData Transaction -data ModifierTransaction = ModifierTransaction {-      mtvalueexpr :: Text,-      mtpostings  :: [Posting]-    } deriving (Eq,Typeable,Data,Generic)+data TransactionModifier = TransactionModifier {+      tmquerytxt :: Text,+      tmpostings :: [Posting]+    } deriving (Eq,Typeable,Data,Generic,Show) -instance NFData ModifierTransaction+instance NFData TransactionModifier --- ^ A periodic transaction rule, describing a transaction that recurs.+nulltransactionmodifier = TransactionModifier{+  tmquerytxt = ""+ ,tmpostings = []+}++-- | A periodic transaction rule, describing a transaction that recurs. data PeriodicTransaction = PeriodicTransaction {       ptperiodexpr   :: Text,     -- ^ the period expression as written       ptinterval     :: Interval, -- ^ the interval at which this transaction recurs @@ -268,7 +307,7 @@       ptcomment      :: Text,       pttags         :: [Tag],       ptpostings     :: [Posting]-    } deriving (Eq,Typeable,Data,Generic)+    } deriving (Eq,Typeable,Data,Generic) -- , Show in PeriodicTransaction.hs  nullperiodictransaction = PeriodicTransaction{       ptperiodexpr   = ""@@ -302,7 +341,7 @@       mpdate      :: Day,       mpcommodity :: CommoditySymbol,       mpamount    :: Amount-    } deriving (Eq,Ord,Typeable,Data,Generic) -- & Show (in Amount.hs)+    } deriving (Eq,Ord,Typeable,Data,Generic) -- , Show in Amount.hs  instance NFData MarketPrice @@ -323,13 +362,13 @@   ,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+  ,jparsetimeclockentries :: [TimeclockEntry]                       -- ^ timeclock sessions which have not been clocked out   -- principal data-  ,jaccounts              :: [(AccountName, Maybe AccountCode)]     -- ^ accounts that have been declared by account directives+  ,jdeclaredaccounts      :: [AccountName]                          -- ^ Accounts declared by account directives, in parse order (after journal finalisation)    ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives-  ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts  XXX misnamed+  ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts  TODO misnamed - jusedstyles   ,jmarketprices          :: [MarketPrice]-  ,jmodifiertxns          :: [ModifierTransaction]+  ,jtxnmodifiers          :: [TransactionModifier]   ,jperiodictxns          :: [PeriodicTransaction]   ,jtxns                  :: [Transaction]   ,jfinalcommentlines     :: Text                                   -- ^ any final trailing comments in the (main) journal file@@ -357,7 +396,7 @@ -- which let you walk up or down the account tree. data Account = Account {   aname                     :: AccountName,   -- ^ this account's full name-  acode                     :: Maybe AccountCode,   -- ^ this account's numeric code, if any (not always set) +  adeclarationorder         :: Maybe Int  ,   -- ^ the relative position of this account's account directive, if any. Normally a natural number.    aebalance                 :: MixedAmount,   -- ^ this account's balance, excluding subaccounts   asubs                     :: [Account],     -- ^ sub-accounts   anumpostings              :: Int,           -- ^ number of postings to this account
Hledger/Query.hs view
@@ -19,6 +19,7 @@   -- * accessors   queryIsNull,   queryIsAcct,+  queryIsAmt,   queryIsDepth,   queryIsDate,   queryIsDate2,@@ -41,11 +42,14 @@   matchesAccount,   matchesMixedAmount,   matchesAmount,+  matchesCommodity,+  matchesMarketPrice,   words'',   -- * tests-  tests_Hledger_Query+  tests_Query ) where+ import Data.Data import Data.Either import Data.List@@ -53,18 +57,16 @@ #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>)) #endif--- import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar import Safe (readDef, headDef)-import Test.HUnit import Text.Megaparsec import Text.Megaparsec.Char  import Hledger.Utils hiding (words') import Hledger.Data.Types import Hledger.Data.AccountName-import Hledger.Data.Amount (amount, nullamt, usd)+import Hledger.Data.Amount (nullamt, usd) import Hledger.Data.Dates import Hledger.Data.Posting import Hledger.Data.Transaction@@ -115,6 +117,11 @@   show (Depth n)     = "Depth "  ++ show n   show (Tag s ms)    = "Tag "    ++ show s ++ " (" ++ show ms ++ ")" +-- | A more expressive Ord, used for amt: queries. The Abs* variants+-- compare with the absolute value of a number, ignoring sign.+data OrdPlus = Lt | LtEq | Gt | GtEq | Eq | AbsLt | AbsLtEq | AbsGt | AbsGtEq | AbsEq+ deriving (Show,Eq,Data,Typeable)+ -- | A query option changes a query's/report's behaviour and output in some way. data QueryOpt = QueryOptInAcctOnly AccountName  -- ^ show an account register focussed on this account               | QueryOptInAcct AccountName      -- ^ as above but include sub-accounts in the account register@@ -170,17 +177,6 @@     (statuspats, otherpats) = partition queryIsStatus pats''     q = simplifyQuery $ And $ [Or acctpats, Or descpats, Or statuspats] ++ otherpats -tests_parseQuery = [-  "parseQuery" ~: do-    let d = nulldate -- parsedate "2011/1/1"-    parseQuery d "acct:'expenses:autres d\233penses' desc:b" `is` (And [Acct "expenses:autres d\233penses", Desc "b"], [])-    parseQuery d "inacct:a desc:\"b b\"" `is` (Desc "b b", [QueryOptInAcct "a"])-    parseQuery d "inacct:a inacct:b" `is` (Any, [QueryOptInAcct "a", QueryOptInAcct "b"])-    parseQuery d "desc:'x x'" `is` (Desc "x x", [])-    parseQuery d "'a a' 'b" `is` (Or [Acct "a a",Acct "'b"], [])-    parseQuery d "\"" `is` (Acct "\"", [])- ]- -- XXX -- | 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@@ -207,19 +203,6 @@       pattern :: SimpleTextParser T.Text       pattern = fmap T.pack $ many (noneOf (" \n\r" :: [Char])) -tests_words'' = [-   "words''" ~: do-    assertEqual "1" ["a","b"]        (words'' [] "a b")-    assertEqual "2" ["a b"]          (words'' [] "'a b'")-    assertEqual "3" ["not:a","b"]    (words'' [] "not:a b")-    assertEqual "4" ["not:a b"]    (words'' [] "not:'a b'")-    assertEqual "5" ["not:a b"]    (words'' [] "'not:a b'")-    assertEqual "6" ["not:desc:a b"]    (words'' ["desc:"] "not:desc:'a b'")-    let s `gives` r = assertEqual "" r (words'' prefixes s)-    "\"acct:expenses:autres d\233penses\"" `gives` ["acct:expenses:autres d\233penses"]-    "\"" `gives` ["\""]- ]- -- XXX -- keep synced with patterns below, excluding "not" prefixes :: [T.Text]@@ -291,36 +274,7 @@ parseQueryTerm _ "" = Left $ Any parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s -tests_parseQueryTerm = [-  "parseQueryTerm" ~: do-    let s `gives` r = parseQueryTerm nulldate s `is` r-    "a" `gives` (Left $ Acct "a")-    "acct:expenses:autres d\233penses" `gives` (Left $ Acct "expenses:autres d\233penses")-    "not:desc:a b" `gives` (Left $ Not $ Desc "a b")-    "status:1" `gives` (Left $ StatusQ Cleared)-    "status:*" `gives` (Left $ StatusQ Cleared)-    "status:!" `gives` (Left $ StatusQ Pending)-    "status:0" `gives` (Left $ StatusQ Unmarked)-    "status:" `gives` (Left $ StatusQ Unmarked)-    "payee:x" `gives` (Left $ Tag "payee" (Just "x"))-    "note:x" `gives` (Left $ Tag "note" (Just "x"))-    "real:1" `gives` (Left $ Real True)-    "date:2008" `gives` (Left $ Date $ DateSpan (Just $ parsedate "2008/01/01") (Just $ parsedate "2009/01/01"))-    "date:from 2012/5/17" `gives` (Left $ Date $ DateSpan (Just $ parsedate "2012/05/17") Nothing)-    "date:20180101-201804" `gives` (Left $ Date $ DateSpan (Just $ parsedate "2018/01/01") (Just $ parsedate "2018/04/01"))-    "inacct:a" `gives` (Right $ QueryOptInAcct "a")-    "tag:a" `gives` (Left $ Tag "a" Nothing)-    "tag:a=some value" `gives` (Left $ Tag "a" (Just "some value"))-    -- "amt:<0" `gives` (Left $ Amt LT 0)-    -- "amt:=.23" `gives` (Left $ Amt EQ 0.23)-    -- "amt:>10000.10" `gives` (Left $ Amt GT 10000.1)- ]---data OrdPlus = Lt | LtEq | Gt | GtEq | Eq | AbsLt | AbsLtEq | AbsGt | AbsGtEq | AbsEq- deriving (Show,Eq,Data,Typeable)---- can fail+-- | Parse what comes after amt: . parseAmountQueryTerm :: T.Text -> (OrdPlus, Quantity) parseAmountQueryTerm s' =   case s' of@@ -356,18 +310,6 @@   where     err = error' $ "could not parse as '=', '<', or '>' (optional) followed by a (optionally signed) numeric quantity: " ++ T.unpack s' -tests_parseAmountQueryTerm = [-  "parseAmountQueryTerm" ~: do-    let s `gives` r = parseAmountQueryTerm s `is` r-    "<0" `gives` (Lt,0) -- special case for convenience, since AbsLt 0 would be always false-    ">0" `gives` (Gt,0) -- special case for convenience and consistency with above-    ">10000.10" `gives` (AbsGt,10000.1)-    "=0.23" `gives` (AbsEq,0.23)-    "0.23" `gives` (AbsEq,0.23)-    "<=+0.23" `gives` (LtEq,0.23)-    "-0.23" `gives` (Eq,(-0.23))-  ]- parseTag :: T.Text -> (Regexp, Maybe Regexp) parseTag s | "=" `T.isInfixOf` s = (T.unpack n, Just $ tail $ T.unpack v)            | otherwise    = (T.unpack s, Nothing)@@ -410,20 +352,6 @@     simplify (Date2 (DateSpan Nothing Nothing)) = Any     simplify q = q -tests_simplifyQuery = [- "simplifyQuery" ~: do-  let q `gives` r = assertEqual "" r (simplifyQuery q)-  Or [Acct "a"] `gives` Acct "a"-  Or [Any,None] `gives` Any-  And [Any,None] `gives` None-  And [Any,Any] `gives` Any-  And [Acct "b",Any] `gives` Acct "b"-  And [Any,And [Date (DateSpan Nothing Nothing)]] `gives` Any-  And [Date (DateSpan Nothing (Just $ parsedate "2013-01-01")), Date (DateSpan (Just $ parsedate "2012-01-01") Nothing)]-      `gives` Date (DateSpan (Just $ parsedate "2012-01-01") (Just $ parsedate "2013-01-01"))-  And [Or [],Or [Desc "b b"]] `gives` Desc "b b"- ]- same [] = True same (a:as) = all (a==) as @@ -438,15 +366,6 @@ -- filterQuery' p (Not q) = Not $ filterQuery p q filterQuery' p q = if p q then q else Any -tests_filterQuery = [- "filterQuery" ~: do-  let (q,p) `gives` r = assertEqual "" r (filterQuery p q)-  (Any, queryIsDepth) `gives` Any-  (Depth 1, queryIsDepth) `gives` Depth 1-  (And [And [StatusQ Cleared,Depth 1]], not . queryIsDepth) `gives` StatusQ Cleared-  -- (And [Date nulldatespan, Not (Or [Any, Depth 1])], queryIsDepth) `gives` And [Not (Or [Depth 1])]- ]- -- * accessors  -- | Does this query match everything ?@@ -481,6 +400,10 @@ queryIsAcct (Acct _) = True queryIsAcct _ = False +queryIsAmt :: Query -> Bool+queryIsAmt (Amt _ _) = True+queryIsAmt _         = False+ queryIsSym :: Query -> Bool queryIsSym (Sym _) = True queryIsSym _ = False@@ -530,33 +453,27 @@ queryTermDateSpan (Date span) = Just span queryTermDateSpan _ = Nothing --- | What date span (or secondary date span) does this query specify ?--- For OR expressions, use the widest possible span. NOT is ignored.+-- | What date span (or with a true argument, what secondary date span) does this query specify ?+-- OR clauses specifying multiple spans return their union (the span enclosing all of them).+-- AND clauses specifying multiple spans return their intersection.+-- NOT clauses are ignored. queryDateSpan :: Bool -> Query -> DateSpan-queryDateSpan secondary q = spansUnion $ queryDateSpans secondary q---- | Extract all date (or secondary date) spans specified in this query.--- NOT is ignored.-queryDateSpans :: Bool -> Query -> [DateSpan]-queryDateSpans secondary (Or qs) = concatMap (queryDateSpans secondary) qs-queryDateSpans secondary (And qs) = concatMap (queryDateSpans secondary) qs-queryDateSpans False (Date span) = [span]-queryDateSpans True (Date2 span) = [span]-queryDateSpans _ _ = []+queryDateSpan secondary (Or qs)  = spansUnion     $ map (queryDateSpan secondary) qs+queryDateSpan secondary (And qs) = spansIntersect $ map (queryDateSpan secondary) qs+queryDateSpan False (Date span)  = span+queryDateSpan True (Date2 span)  = span+queryDateSpan _ _                = nulldatespan --- | What date span (or secondary date span) does this query specify ?--- For OR expressions, use the widest possible span. NOT is ignored.+-- | What date span does this query specify, treating primary and secondary dates as equivalent ?+-- OR clauses specifying multiple spans return their union (the span enclosing all of them).+-- AND clauses specifying multiple spans return their intersection.+-- NOT clauses are ignored. queryDateSpan' :: Query -> DateSpan-queryDateSpan' q = spansUnion $ queryDateSpans' q---- | Extract all date (or secondary date) spans specified in this query.--- NOT is ignored.-queryDateSpans' :: Query -> [DateSpan]-queryDateSpans' (Or qs) = concatMap queryDateSpans' qs-queryDateSpans' (And qs) = concatMap queryDateSpans' qs-queryDateSpans' (Date span) = [span]-queryDateSpans' (Date2 span) = [span]-queryDateSpans' _ = []+queryDateSpan' (Or qs)      = spansUnion     $ map queryDateSpan' qs+queryDateSpan' (And qs)     = spansIntersect $ map queryDateSpan' qs+queryDateSpan' (Date span)  = span+queryDateSpan' (Date2 span) = span+queryDateSpan' _            = nulldatespan  -- | What is the earliest of these dates, where Nothing is latest ? earliestMaybeDate :: [Maybe Day] -> Maybe Day@@ -623,24 +540,14 @@ matchesAccount (Tag _ _) _ = False matchesAccount _ _ = True -tests_matchesAccount = [-   "matchesAccount" ~: do-    assertBool "positive acct match" $ matchesAccount (Acct "b:c") "a:bb:c:d"-    -- assertBool "acct should match at beginning" $ not $ matchesAccount (Acct True "a:b") "c:a:b"-    let q `matches` a = assertBool "" $ q `matchesAccount` a-    Depth 2 `matches` "a:b"-    assertBool "" $ Depth 2 `matchesAccount` "a"-    assertBool "" $ Depth 2 `matchesAccount` "a:b"-    assertBool "" $ not $ Depth 2 `matchesAccount` "a:b:c"-    assertBool "" $ Date nulldatespan `matchesAccount` "a"-    assertBool "" $ Date2 nulldatespan `matchesAccount` "a"-    assertBool "" $ not $ (Tag "a" Nothing) `matchesAccount` "a"- ]- matchesMixedAmount :: Query -> MixedAmount -> Bool matchesMixedAmount q (Mixed []) = q `matchesAmount` nullamt matchesMixedAmount q (Mixed as) = any (q `matchesAmount`) as +matchesCommodity :: Query -> CommoditySymbol -> Bool+matchesCommodity (Sym r) s = regexMatchesCI ("^" ++ r ++ "$") (T.unpack s)+matchesCommodity _ _ = True+ -- | Does the match expression match this (simple) amount ? matchesAmount :: Query -> Amount -> Bool matchesAmount (Not q) a = not $ q `matchesAmount` a@@ -650,7 +557,7 @@ matchesAmount (And qs) a = all (`matchesAmount` a) qs -- matchesAmount (Amt ord n) a = compareAmount ord n a-matchesAmount (Sym r) a = regexMatchesCI ("^" ++ r ++ "$") $ T.unpack $ acommodity a+matchesAmount (Sym r) a = matchesCommodity (Sym r) (acommodity a) -- matchesAmount _ _ = True @@ -694,44 +601,12 @@ -- 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 (T.unpack . acommodity) as+matchesPosting (Sym r) Posting{pamount=Mixed as} = any (matchesCommodity (Sym r)) $ map acommodity as matchesPosting (Tag n v) p = case (n, v) of   ("payee", Just v) -> maybe False (regexMatchesCI v . T.unpack . transactionPayee) $ ptransaction p   ("note", Just v) -> maybe False (regexMatchesCI v . T.unpack . transactionNote) $ ptransaction p   (n, v) -> matchesTags n v $ postingAllTags p -tests_matchesPosting = [-   "matchesPosting" ~: do-    -- matching posting status..-    assertBool "positive match on cleared posting status"  $-                   (StatusQ Cleared)  `matchesPosting` nullposting{pstatus=Cleared}-    assertBool "negative match on cleared posting status"  $-               not $ (Not $ StatusQ Cleared)  `matchesPosting` nullposting{pstatus=Cleared}-    assertBool "positive match on unmarked posting status" $-                   (StatusQ Unmarked) `matchesPosting` nullposting{pstatus=Unmarked}-    assertBool "negative match on unmarked posting status" $-               not $ (Not $ StatusQ Unmarked) `matchesPosting` nullposting{pstatus=Unmarked}-    assertBool "positive match on true posting status acquired from transaction" $-                   (StatusQ Cleared) `matchesPosting` nullposting{pstatus=Unmarked,ptransaction=Just nulltransaction{tstatus=Cleared}}-    assertBool "real:1 on real posting" $ (Real True) `matchesPosting` nullposting{ptype=RegularPosting}-    assertBool "real:1 on virtual posting fails" $ not $ (Real True) `matchesPosting` nullposting{ptype=VirtualPosting}-    assertBool "real:1 on balanced virtual posting fails" $ not $ (Real True) `matchesPosting` nullposting{ptype=BalancedVirtualPosting}-    assertBool "a" $ (Acct "'b") `matchesPosting` nullposting{paccount="'b"}-    assertBool "b" $ not $ (Tag "a" (Just "r$")) `matchesPosting` nullposting-    assertBool "c" $ (Tag "foo" Nothing) `matchesPosting` nullposting{ptags=[("foo","")]}-    assertBool "d" $ (Tag "foo" Nothing) `matchesPosting` nullposting{ptags=[("foo","baz")]}-    assertBool "e" $ (Tag "foo" (Just "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}-    assertBool "f" $ not $ (Tag "foo" (Just "a$")) `matchesPosting` nullposting{ptags=[("foo","bar")]}-    assertBool "g" $ not $ (Tag " foo " (Just "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}-    assertBool "h" $ not $ (Tag "foo foo" (Just " ar ba ")) `matchesPosting` nullposting{ptags=[("foo foo","bar bar")]}-    -- a tag match on a posting also sees inherited tags-    assertBool "i" $ (Tag "txntag" Nothing) `matchesPosting` nullposting{ptransaction=Just nulltransaction{ttags=[("txntag","")]}}-    assertBool "j" $ not $ (Sym "$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- becomes "^$$", ie testing for null symbol-    assertBool "k" $ (Sym "\\$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- have to quote $ for regexpr-    assertBool "l" $ (Sym "shekels") `matchesPosting` nullposting{pamount=Mixed [amount{acommodity="shekels"}]}-    assertBool "m" $ not $ (Sym "shek") `matchesPosting` nullposting{pamount=Mixed [amount{acommodity="shekels"}]}- ]- -- | Does the match expression match this transaction ? matchesTransaction :: Query -> Transaction -> Bool matchesTransaction (Not q) t = not $ q `matchesTransaction` t@@ -755,20 +630,6 @@   ("note", Just v) -> regexMatchesCI v . T.unpack . transactionNote $ t   (n, v) -> matchesTags n v $ transactionAllTags t -tests_matchesTransaction = [-  "matchesTransaction" ~: do-   let q `matches` t = assertBool "" $ q `matchesTransaction` t-   Any `matches` nulltransaction-   assertBool "" $ not $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x"}-   assertBool "" $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x x"}-   -- see posting for more tag tests-   assertBool "" $ (Tag "foo" (Just "a")) `matchesTransaction` nulltransaction{ttags=[("foo","bar")]}-   assertBool "" $ (Tag "payee" (Just "payee")) `matchesTransaction` nulltransaction{tdescription="payee|note"}-   assertBool "" $ (Tag "note" (Just "note")) `matchesTransaction` nulltransaction{tdescription="payee|note"}-   -- a tag match on a transaction also matches posting tags-   assertBool "" $ (Tag "postingtag" Nothing) `matchesTransaction` nulltransaction{tpostings=[nullposting{ptags=[("postingtag","")]}]}- ]- -- | Filter a list of tags by matching against their names and -- optionally also their values. matchesTags :: Regexp -> Maybe Regexp -> [Tag] -> Bool@@ -777,17 +638,145 @@     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) +-- | Does the query match this market price ?+matchesMarketPrice :: Query -> MarketPrice -> Bool+matchesMarketPrice (None) _      = False+matchesMarketPrice (Not q) p     = not $ matchesMarketPrice q p+matchesMarketPrice (Or qs) p     = any (`matchesMarketPrice` p) qs+matchesMarketPrice (And qs) p    = all (`matchesMarketPrice` p) qs+matchesMarketPrice q@(Amt _ _) p = matchesAmount q (mpamount p)+matchesMarketPrice q@(Sym _) p   = matchesCommodity q (mpcommodity p)+matchesMarketPrice (Date span) p = spanContainsDate span (mpdate p)+matchesMarketPrice _ _           = True++ -- tests -tests_Hledger_Query :: Test-tests_Hledger_Query = TestList $-    tests_simplifyQuery- ++ tests_words''- ++ tests_filterQuery- ++ tests_parseQueryTerm- ++ tests_parseAmountQueryTerm- ++ tests_parseQuery- ++ tests_matchesAccount- ++ tests_matchesPosting- ++ tests_matchesTransaction+tests_Query = tests "Query" [+   tests "simplifyQuery" [+    +     (simplifyQuery $ Or [Acct "a"])      `is` (Acct "a")+    ,(simplifyQuery $ Or [Any,None])      `is` (Any)+    ,(simplifyQuery $ And [Any,None])     `is` (None)+    ,(simplifyQuery $ And [Any,Any])      `is` (Any)+    ,(simplifyQuery $ And [Acct "b",Any]) `is` (Acct "b")+    ,(simplifyQuery $ And [Any,And [Date (DateSpan Nothing Nothing)]]) `is` (Any)+    ,(simplifyQuery $ And [Date (DateSpan Nothing (Just $ parsedate "2013-01-01")), Date (DateSpan (Just $ parsedate "2012-01-01") Nothing)])+      `is` (Date (DateSpan (Just $ parsedate "2012-01-01") (Just $ parsedate "2013-01-01")))+    ,(simplifyQuery $ And [Or [],Or [Desc "b b"]]) `is` (Desc "b b")+   ]+  +  ,tests "parseQuery" [+     (parseQuery nulldate "acct:'expenses:autres d\233penses' desc:b") `is` (And [Acct "expenses:autres d\233penses", Desc "b"], [])+    ,parseQuery nulldate "inacct:a desc:\"b b\""                     `is` (Desc "b b", [QueryOptInAcct "a"])+    ,parseQuery nulldate "inacct:a inacct:b"                         `is` (Any, [QueryOptInAcct "a", QueryOptInAcct "b"])+    ,parseQuery nulldate "desc:'x x'"                                `is` (Desc "x x", [])+    ,parseQuery nulldate "'a a' 'b"                                  `is` (Or [Acct "a a",Acct "'b"], [])+    ,parseQuery nulldate "\""                                        `is` (Acct "\"", [])+   ]+  +  ,tests "words''" [+      (words'' [] "a b")                   `is` ["a","b"]        +    , (words'' [] "'a b'")                 `is` ["a b"]          +    , (words'' [] "not:a b")               `is` ["not:a","b"]    +    , (words'' [] "not:'a b'")             `is` ["not:a b"]      +    , (words'' [] "'not:a b'")             `is` ["not:a b"]      +    , (words'' ["desc:"] "not:desc:'a b'") `is` ["not:desc:a b"] +    , (words'' prefixes "\"acct:expenses:autres d\233penses\"") `is` ["acct:expenses:autres d\233penses"]+    , (words'' prefixes "\"")              `is` ["\""]+    ]+  +  ,tests "filterQuery" [+     filterQuery queryIsDepth Any       `is` Any+    ,filterQuery queryIsDepth (Depth 1) `is` Depth 1+    ,filterQuery (not.queryIsDepth) (And [And [StatusQ Cleared,Depth 1]]) `is` StatusQ Cleared+    ,filterQuery queryIsDepth (And [Date nulldatespan, Not (Or [Any, Depth 1])]) `is` Any   -- XXX unclear+   ] +  ,tests "parseQueryTerm" [+     parseQueryTerm nulldate "a"                                `is` (Left $ Acct "a")+    ,parseQueryTerm nulldate "acct:expenses:autres d\233penses" `is` (Left $ Acct "expenses:autres d\233penses")+    ,parseQueryTerm nulldate "not:desc:a b"                     `is` (Left $ Not $ Desc "a b")+    ,parseQueryTerm nulldate "status:1"                         `is` (Left $ StatusQ Cleared)+    ,parseQueryTerm nulldate "status:*"                         `is` (Left $ StatusQ Cleared)+    ,parseQueryTerm nulldate "status:!"                         `is` (Left $ StatusQ Pending)+    ,parseQueryTerm nulldate "status:0"                         `is` (Left $ StatusQ Unmarked)+    ,parseQueryTerm nulldate "status:"                          `is` (Left $ StatusQ Unmarked)+    ,parseQueryTerm nulldate "payee:x"                          `is` (Left $ Tag "payee" (Just "x"))+    ,parseQueryTerm nulldate "note:x"                           `is` (Left $ Tag "note" (Just "x"))+    ,parseQueryTerm nulldate "real:1"                           `is` (Left $ Real True)+    ,parseQueryTerm nulldate "date:2008"                        `is` (Left $ Date $ DateSpan (Just $ parsedate "2008/01/01") (Just $ parsedate "2009/01/01"))+    ,parseQueryTerm nulldate "date:from 2012/5/17"              `is` (Left $ Date $ DateSpan (Just $ parsedate "2012/05/17") Nothing)+    ,parseQueryTerm nulldate "date:20180101-201804"             `is` (Left $ Date $ DateSpan (Just $ parsedate "2018/01/01") (Just $ parsedate "2018/04/01"))+    ,parseQueryTerm nulldate "inacct:a"                         `is` (Right $ QueryOptInAcct "a")+    ,parseQueryTerm nulldate "tag:a"                            `is` (Left $ Tag "a" Nothing)+    ,parseQueryTerm nulldate "tag:a=some value"                 `is` (Left $ Tag "a" (Just "some value"))+    ,parseQueryTerm nulldate "amt:<0"                           `is` (Left $ Amt Lt 0)+    ,parseQueryTerm nulldate "amt:>10000.10"                    `is` (Left $ Amt AbsGt 10000.1)+   ]+  +  ,tests "parseAmountQueryTerm" [+     parseAmountQueryTerm "<0"        `is` (Lt,0) -- special case for convenience, since AbsLt 0 would be always false+    ,parseAmountQueryTerm ">0"        `is` (Gt,0) -- special case for convenience and consistency with above+    ,parseAmountQueryTerm ">10000.10" `is` (AbsGt,10000.1)+    ,parseAmountQueryTerm "=0.23"     `is` (AbsEq,0.23)+    ,parseAmountQueryTerm "0.23"      `is` (AbsEq,0.23)+    ,parseAmountQueryTerm "<=+0.23"   `is` (LtEq,0.23)+    ,parseAmountQueryTerm "-0.23"     `is` (Eq,(-0.23))+    ,_test "number beginning with decimal mark" $ parseAmountQueryTerm "=.23" `is` (AbsEq,0.23)  -- XXX+    ]+  +  ,tests "matchesAccount" [+     expect $ (Acct "b:c") `matchesAccount` "a:bb:c:d"+    ,expect $ not $ (Acct "^a:b") `matchesAccount` "c:a:b"+    ,expect $ Depth 2 `matchesAccount` "a"+    ,expect $ Depth 2 `matchesAccount` "a:b"+    ,expect $ not $ Depth 2 `matchesAccount` "a:b:c"+    ,expect $ Date nulldatespan `matchesAccount` "a"+    ,expect $ Date2 nulldatespan `matchesAccount` "a"+    ,expect $ not $ (Tag "a" Nothing) `matchesAccount` "a"+  ]+  +  ,tests "matchesPosting" [+     test "positive match on cleared posting status"  $+      expect $ (StatusQ Cleared)  `matchesPosting` nullposting{pstatus=Cleared}+    ,test "negative match on cleared posting status"  $+      expect $ not $ (Not $ StatusQ Cleared)  `matchesPosting` nullposting{pstatus=Cleared}+    ,test "positive match on unmarked posting status" $+      expect $ (StatusQ Unmarked) `matchesPosting` nullposting{pstatus=Unmarked}+    ,test "negative match on unmarked posting status" $+      expect $ not $ (Not $ StatusQ Unmarked) `matchesPosting` nullposting{pstatus=Unmarked}+    ,test "positive match on true posting status acquired from transaction" $+      expect $ (StatusQ Cleared) `matchesPosting` nullposting{pstatus=Unmarked,ptransaction=Just nulltransaction{tstatus=Cleared}}+    ,test "real:1 on real posting" $ expect $ (Real True) `matchesPosting` nullposting{ptype=RegularPosting}+    ,test "real:1 on virtual posting fails" $ expect $ not $ (Real True) `matchesPosting` nullposting{ptype=VirtualPosting}+    ,test "real:1 on balanced virtual posting fails" $ expect $ not $ (Real True) `matchesPosting` nullposting{ptype=BalancedVirtualPosting}+    ,test "a" $ expect $ (Acct "'b") `matchesPosting` nullposting{paccount="'b"}+    ,test "b" $ expect $ not $ (Tag "a" (Just "r$")) `matchesPosting` nullposting+    ,test "c" $ expect $ (Tag "foo" Nothing) `matchesPosting` nullposting{ptags=[("foo","")]}+    ,test "d" $ expect $ (Tag "foo" Nothing) `matchesPosting` nullposting{ptags=[("foo","baz")]}+    ,test "e" $ expect $ (Tag "foo" (Just "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}+    ,test "f" $ expect $ not $ (Tag "foo" (Just "a$")) `matchesPosting` nullposting{ptags=[("foo","bar")]}+    ,test "g" $ expect $ not $ (Tag " foo " (Just "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}+    ,test "h" $ expect $ not $ (Tag "foo foo" (Just " ar ba ")) `matchesPosting` nullposting{ptags=[("foo foo","bar bar")]}+     -- a tag match on a posting also sees inherited tags+    ,test "i" $ expect $ (Tag "txntag" Nothing) `matchesPosting` nullposting{ptransaction=Just nulltransaction{ttags=[("txntag","")]}}+    ,test "j" $ expect $ not $ (Sym "$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- becomes "^$$", ie testing for null symbol+    ,test "k" $ expect $ (Sym "\\$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- have to quote $ for regexpr+    ,test "l" $ expect $ (Sym "shekels") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}+    ,test "m" $ expect $ not $ (Sym "shek") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}+  ]+  +  ,tests "matchesTransaction" [+     expect $ Any `matchesTransaction` nulltransaction+    ,expect $ not $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x"}+    ,expect $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x x"}+     -- see posting for more tag tests+    ,expect $ (Tag "foo" (Just "a")) `matchesTransaction` nulltransaction{ttags=[("foo","bar")]}+    ,expect $ (Tag "payee" (Just "payee")) `matchesTransaction` nulltransaction{tdescription="payee|note"}+    ,expect $ (Tag "note" (Just "note")) `matchesTransaction` nulltransaction{tdescription="payee|note"}+     -- a tag match on a transaction also matches posting tags+    ,expect $ (Tag "postingtag" Nothing) `matchesTransaction` nulltransaction{tpostings=[nullposting{ptags=[("postingtag","")]}]}+  ]++ ]
Hledger/Read.hs view
@@ -29,8 +29,7 @@   module Hledger.Read.Common,    -- * Tests-  samplejournal,-  tests_Hledger_Read,+  tests_Read,  ) where @@ -50,17 +49,16 @@ import System.Exit (exitFailure) import System.FilePath import System.IO-import Test.HUnit import Text.Printf  import Hledger.Data.Dates (getCurrentDay, parsedate, showDate) import Hledger.Data.Types import Hledger.Read.Common-import qualified Hledger.Read.JournalReader   as JournalReader+import Hledger.Read.JournalReader   as JournalReader -- import qualified Hledger.Read.LedgerReader    as LedgerReader import qualified Hledger.Read.TimedotReader   as TimedotReader import qualified Hledger.Read.TimeclockReader as TimeclockReader-import qualified Hledger.Read.CsvReader       as CsvReader+import Hledger.Read.CsvReader as CsvReader import Hledger.Utils import Prelude hiding (getContents, writeFile) @@ -149,12 +147,6 @@ readJournal' :: Text -> IO Journal readJournal' t = readJournal def Nothing t >>= either error' return -tests_readJournal' = [-  "readJournal' parses sample journal" ~: do-     _ <- samplejournal-     assertBool "" True- ]- -- | @findReader mformat mpath@ -- -- Find the reader named by @mformat@, if provided.@@ -315,47 +307,37 @@  -- tests -samplejournal = readJournal' $ T.unlines- ["2008/01/01 income"- ,"    assets:bank:checking  $1"- ,"    income:salary"- ,""- ,"comment"- ,"multi line comment here"- ,"for testing purposes"- ,"end comment"- ,""- ,"2008/06/01 gift"- ,"    assets:bank:checking  $1"- ,"    income:gifts"- ,""- ,"2008/06/02 save"- ,"    assets:bank:saving  $1"- ,"    assets:bank:checking"- ,""- ,"2008/06/03 * eat & shop"- ,"    expenses:food      $1"- ,"    expenses:supplies  $1"- ,"    assets:cash"- ,""- ,"2008/12/31 * pay off"- ,"    liabilities:debts  $1"- ,"    assets:bank:checking"- ]--tests_Hledger_Read = TestList $-  tests_readJournal'-  ++ [-   JournalReader.tests_Hledger_Read_JournalReader,---    LedgerReader.tests_Hledger_Read_LedgerReader,-   TimeclockReader.tests_Hledger_Read_TimeclockReader,-   TimedotReader.tests_Hledger_Read_TimedotReader,-   CsvReader.tests_Hledger_Read_CsvReader,+tests_Read = tests "Read" [+   tests_Common+  ,tests_CsvReader+  ,tests_JournalReader+  ] -   "journal" ~: do-    r <- runExceptT $ parseWithState mempty JournalReader.journalp ""-    assertBool "journalp should parse an empty file" (isRight $ r)-    jE <- readJournal def 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+--samplejournal = readJournal' $ T.unlines+-- ["2008/01/01 income"+-- ,"    assets:bank:checking  $1"+-- ,"    income:salary"+-- ,""+-- ,"comment"+-- ,"multi line comment here"+-- ,"for testing purposes"+-- ,"end comment"+-- ,""+-- ,"2008/06/01 gift"+-- ,"    assets:bank:checking  $1"+-- ,"    income:gifts"+-- ,""+-- ,"2008/06/02 save"+-- ,"    assets:bank:saving  $1"+-- ,"    assets:bank:checking"+-- ,""+-- ,"2008/06/03 * eat & shop"+-- ,"    expenses:food      $1"+-- ,"    expenses:supplies  $1"+-- ,"    assets:cash"+-- ,""+-- ,"2008/12/31 * pay off"+-- ,"    liabilities:debts  $1"+-- ,"    assets:bank:checking"+-- ] -  ]
Hledger/Read/Common.hs view
@@ -31,16 +31,15 @@   rjp,   genericSourcePos,   journalSourcePos,-  generateAutomaticPostings,+  applyTransactionModifiers,   parseAndFinaliseJournal,-  parseAndFinaliseJournal',  -- TODO unused ? check addons   setYear,   getYear,   setDefaultCommodityAndStyle,   getDefaultCommodityAndStyle,   getDefaultAmountStyle,   getAmountStyle,-  pushAccount,+  pushDeclaredAccount,   pushParentAccount,   popParentAccount,   getParentAccount,@@ -90,7 +89,10 @@    -- ** misc   singlespacedtextp,-  singlespacep+  singlespacep,++  -- * tests+  tests_Common, ) where --- * imports@@ -122,8 +124,10 @@  import Hledger.Data import Hledger.Utils-import qualified Hledger.Query as Q (Query(Any)) +-- $setup+-- >>> :set -XOverloadedStrings+ -- | A hledger journal reader is a triple of storage format name, a -- detector of that format, and a parser from that format to Journal. data Reader = Reader {@@ -154,6 +158,7 @@      mformat_           :: Maybe StorageFormat  -- ^ a file/storage format to try, unless overridden                                                 --   by a filename prefix. Nothing means try all.     ,mrules_file_       :: Maybe FilePath       -- ^ a conversion rules file to use (when reading CSV)+    ,separator_         :: Char                 -- ^ the separator to use (when reading CSV)     ,aliases_           :: [String]             -- ^ account name aliases to apply     ,anon_              :: Bool                 -- ^ do light anonymisation/obfuscation of the data      ,ignore_assertions_ :: Bool                 -- ^ don't check balance assertions@@ -166,13 +171,14 @@ instance Default InputOpts where def = definputopts  definputopts :: InputOpts-definputopts = InputOpts def def def def def def True def def+definputopts = InputOpts def def ',' def def def def True def def  rawOptsToInputOpts :: RawOpts -> InputOpts rawOptsToInputOpts rawopts = InputOpts{    -- files_             = map (T.unpack . stripquotes . T.pack) $ listofstringopt "file" rawopts    mformat_           = Nothing   ,mrules_file_       = maybestringopt "rules-file" rawopts+  ,separator_         = fromMaybe ',' (maybecharopt "separator" rawopts)   ,aliases_           = map (T.unpack . stripquotes . T.pack) $ listofstringopt "alias" rawopts   ,anon_              = boolopt "anon" rawopts   ,ignore_assertions_ = boolopt "ignore-assertions" rawopts@@ -184,12 +190,12 @@  --- * parsing utilities --- | Run a string parser with no state in the identity monad.+-- | Run a text parser in the identity monad. See also: parseWithState. runTextParser, rtp :: TextParser Identity a -> Text -> Either (ParseError Char CustomErr) a runTextParser p t =  runParser p "" t rtp = runTextParser --- | Run a journal parser with a null journal-parsing state.+-- | Run a journal parser in some monad. See also: parseWithState. runJournalParser, rjp :: Monad m => JournalParser m a -> Text -> m (Either (ParseError Char CustomErr) a) runJournalParser p t = runParserT (evalStateT p mempty) "" t rjp = runJournalParser@@ -205,13 +211,13 @@             | otherwise = unPos $ sourceLine p' -- might be at end of file withat last new-line  --- | Generate Automatic postings and add them to the current journal.-generateAutomaticPostings :: Journal -> Journal-generateAutomaticPostings j = j { jtxns = map modifier $ jtxns j }+-- | Apply any transaction modifier rules in the journal +-- (adding automated postings to transactions, eg).+applyTransactionModifiers :: Journal -> Journal+applyTransactionModifiers j = j { jtxns = map applyallmodifiers $ jtxns j }   where-    modifier = foldr (flip (.) . runModifierTransaction') id mtxns-    runModifierTransaction' = fmap txnTieKnot . runModifierTransaction Q.Any-    mtxns = jmodifiertxns j+    applyallmodifiers = +      foldr (flip (.) . transactionModifierToFunction) id (jtxnmodifiers j)  -- | Given a megaparsec ParsedJournal parser, input options, file -- path and file content: parse and post-process a Journal, or give an error.@@ -223,26 +229,12 @@   ep <- liftIO $ runParserT (evalStateT parser nulljournal {jparsedefaultyear=Just y}) f txt   case ep of     Right pj -> -      let pj' = if auto_ iopts then generateAutomaticPostings pj else pj in+      let pj' = if auto_ iopts then applyTransactionModifiers pj else pj in       case journalFinalise t f txt (not $ ignore_assertions_ iopts) pj' of                         Right j -> return j                         Left e  -> throwError e     Left e   -> throwError $ customParseErrorPretty txt e -parseAndFinaliseJournal' :: JournalParser Identity ParsedJournal -> InputOpts -                            -> FilePath -> Text -> ExceptT String IO Journal-parseAndFinaliseJournal' parser iopts 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 -> -      let pj' = if auto_ iopts then generateAutomaticPostings pj else pj in      -      case journalFinalise t f txt (not $ ignore_assertions_ iopts) pj' of-                        Right j -> return j-                        Left e  -> throwError e-    Left e   -> throwError $ parseErrorPretty e- setYear :: Year -> JournalParser m () setYear y = modify' (\j -> j{jparsedefaultyear=Just y}) @@ -273,8 +265,8 @@     let effectiveStyle = listToMaybe $ catMaybes [specificStyle, defaultStyle]     return effectiveStyle -pushAccount :: AccountName -> JournalParser m ()-pushAccount acct = modify' (\j -> j{jaccounts = (acct, Nothing) : jaccounts j})+pushDeclaredAccount :: AccountName -> JournalParser m ()+pushDeclaredAccount acct = modify' (\j -> j{jdeclaredaccounts = acct : jdeclaredaccounts j})  pushParentAccount :: AccountName -> JournalParser m () pushParentAccount acct = modify' (\j -> j{jparseparentaccounts = acct : jparseparentaccounts j})@@ -474,10 +466,14 @@     a  -- | Parse an account name, plus one following space if present. --- Account names start with a non-space, may have single spaces inside them, +-- Account names have one or more parts separated by the account separator character, -- 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 character, but we don't check that here.) +-- Each part is at least one character long, may have single spaces inside it,+-- and starts with a non-whitespace.+-- Note, this means "{account}", "%^!" and ";comment" are all accepted+-- (parent parsers usually prevent/consume the last).+-- It should have required parts to start with an alphanumeric;+-- for now it remains as-is for backwards compatibility. accountnamep :: TextParser m AccountName accountnamep = singlespacedtextp @@ -505,20 +501,6 @@     lift $ skipSome spacenonewline     Mixed . (:[]) <$> amountp -#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.@@ -540,52 +522,42 @@   leftsymbolamountp mult sign = label "amount" $ do     c <- lift commoditysymbolp     suggestedStyle <- getAmountStyle c-     commodityspaced <- lift $ skipMany' spacenonewline-     sign2 <- lift $ signp     posBeforeNum <- getPosition     ambiguousRawNum <- lift rawnumberp     mExponent <- lift $ optional $ try exponentp     posAfterNum <- getPosition     let numRegion = (posBeforeNum, posAfterNum)--    (q,prec,mdec,mgrps) <- lift $-      interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent+    (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent     let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}     return $ Amount c (sign (sign2 q)) NoPrice s mult -  rightornosymbolamountp-    :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount+  rightornosymbolamountp :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount   rightornosymbolamountp mult sign = label "amount" $ do     posBeforeNum <- getPosition     ambiguousRawNum <- lift rawnumberp     mExponent <- lift $ optional $ try exponentp     posAfterNum <- getPosition     let numRegion = (posBeforeNum, posAfterNum)--    mSpaceAndCommodity <- lift $ optional $ try $-      (,) <$> skipMany' spacenonewline <*> commoditysymbolp-+    mSpaceAndCommodity <- lift $ optional $ try $ (,) <$> skipMany' spacenonewline <*> commoditysymbolp     case mSpaceAndCommodity of+      -- right symbol amount       Just (commodityspaced, c) -> do         suggestedStyle <- getAmountStyle c-        (q,prec,mdec,mgrps) <- lift $-          interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent-+        (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent         let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}         return $ Amount c (sign q) NoPrice s mult-+      -- no symbol amount       Nothing -> do         suggestedStyle <- getDefaultAmountStyle-        (q,prec,mdec,mgrps) <- lift $-          interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent--        -- apply the most recently seen default commodity and style to this commodityless amount+        (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent+        -- if a default commodity has been set, apply it and its style to this amount+        -- (unless it's a multiplier in an automated posting)         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})+        let (c,s) = case (mult, defcs) of+              (False, Just (defc,defs)) -> (defc, defs{asprecision=max (asprecision defs) prec})+              _ -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})         return $ Amount c (sign q) NoPrice s mult    -- For reducing code duplication. Doesn't parse anything. Has the type@@ -602,21 +574,6 @@           Left errMsg -> uncurry parseErrorAtRegion posRegion errMsg           Right res -> pure res --#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 =@@ -719,7 +676,7 @@ numberp suggestedStyle = label "number" $ do     -- a number is an optional sign followed by a sequence of digits possibly     -- interspersed with periods, commas, or both-    -- ptrace "numberp"+    -- dbgparse 0 "numberp"     sign <- signp     rawNum <- either (disambiguateNumber suggestedStyle) id <$> rawnumberp     mExp <- optional $ try $ exponentp@@ -917,7 +874,6 @@     makeGroup = uncurry DigitGrp . foldl' step (0, 0) . T.unpack     step (!l, !a) c = (l+1, a*10 + fromIntegral (digitToInt c)) - data RawNumber   = NoSeparators   DigitGrp (Maybe (Char, DigitGrp))        -- 100 or 100. or .100 or 100.50   | WithSeparators Char [DigitGrp] (Maybe (Char, DigitGrp)) -- 1,000,000 or 1,000.50@@ -926,28 +882,6 @@ data AmbiguousNumber = AmbiguousNumber DigitGrp Char DigitGrp  -- 1,000   deriving (Show, Eq) --- 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 :: TextParser m ()@@ -989,26 +923,46 @@ -- until the next newline. This parser should extract the "content" from -- comments. The resulting parser returns this content plus the raw text -- of the comment itself.-followingcommentp' :: (Monoid a) => TextParser m a -> TextParser m (Text, a)+--+-- See followingcommentp for tests.+--+followingcommentp' :: (Monoid a, Show a) => TextParser m a -> TextParser m (Text, a) followingcommentp' contentp = do   skipMany spacenonewline-  sameLine <- try headerp *> match' contentp <|> pure ("", mempty)+  -- there can be 0 or 1 sameLine+  sameLine <- try headerp *> ((:[]) <$> match' contentp) <|> pure []   _ <- eolof-  lowerLines <- many $+  -- there can be 0 or more nextLines+  nextLines <- many $     try (skipSome spacenonewline *> headerp) *> match' contentp <* eolof--  let (textLines, results) = unzip $ sameLine : lowerLines-      strippedCommentText = T.unlines $ map T.strip textLines-      result = mconcat results-  pure (strippedCommentText, result)+  let+    -- if there's just a next-line comment, insert an empty same-line comment+    -- so the next-line comment doesn't get rendered as a same-line comment.+    sameLine' | null sameLine && not (null nextLines) = [("",mempty)]+              | otherwise = sameLine +    (texts, contents) = unzip $ sameLine' ++ nextLines+    strippedCommentText = T.unlines $ map T.strip texts+    commentContent = mconcat contents+  pure (strippedCommentText, commentContent)    where     headerp = char ';' *> skipMany spacenonewline  {-# INLINABLE followingcommentp' #-} --- | Parse the text of a (possibly multiline) comment following a journal--- item.+-- | Parse the text of a (possibly multiline) comment following a journal item.+--+-- >>> rtp followingcommentp ""   -- no comment+-- Right ""+-- >>> rtp followingcommentp ";"    -- just a (empty) same-line comment. newline is added+-- Right "\n"+-- >>> rtp followingcommentp ";  \n"+-- Right "\n"+-- >>> rtp followingcommentp ";\n ;\n"  -- a same-line and a next-line comment+-- Right "\n\n"+-- >>> rtp followingcommentp "\n ;\n"  -- just a next-line comment. Insert an empty same-line comment so the next-line comment doesn't become a same-line comment.+-- Right "\n\n"+-- followingcommentp :: TextParser m Text followingcommentp =   fst <$> followingcommentp' (void $ takeWhileP Nothing (/= '\n'))@@ -1214,7 +1168,7 @@ bracketeddatetagsp   :: Maybe Year -> TextParser m [(TagName, Day)] bracketeddatetagsp mYear1 = do-  -- pdbg 0 "bracketeddatetagsp"+  -- dbgparse 0 "bracketeddatetagsp"   try $ do     s <- lookAhead        $ between (char '[') (char ']')@@ -1245,3 +1199,69 @@ match' p = do   (!txt, p) <- match p   pure (txt, p)++--- * tests++tests_Common = tests "Common" [++  tests "amountp" [+    test "basic"                  $ expectParseEq amountp "$47.18"     (usd 47.18)+   ,test "ends with decimal mark" $ expectParseEq amountp "$1."        (usd 1  `withPrecision` 0)+   ,test "unit price"             $ expectParseEq amountp "$10 @ €0.5" +      -- not precise enough:+      -- (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1)) -- `withStyle` asdecimalpoint=Just '.'+      amount{+         acommodity="$"+        ,aquantity=10 -- need to test internal precision with roundTo ? I think not +        ,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing}+        ,aprice=UnitPrice $+          amount{+             acommodity="€"+            ,aquantity=0.5+            ,astyle=amountstyle{asprecision=1, asdecimalpoint=Just '.'}+            } +        } +   ,test "total price"            $ expectParseEq amountp "$10 @@ €5"+      amount{+         acommodity="$"+        ,aquantity=10 +        ,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing}+        ,aprice=TotalPrice $+          amount{+             acommodity="€"+            ,aquantity=5+            ,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing}+            } +        } +    ]++  ,let p = lift (numberp Nothing) :: JournalParser IO (Quantity, Int, Maybe Char, Maybe DigitGroupStyle) in+   tests "numberp" [+     test "." $ expectParseEq p "0"          (0, 0, Nothing, Nothing)+    ,test "." $ expectParseEq p "1"          (1, 0, Nothing, Nothing)+    ,test "." $ expectParseEq p "1.1"        (1.1, 1, Just '.', Nothing)+    ,test "." $ expectParseEq p "1,000.1"    (1000.1, 1, Just '.', Just $ DigitGroups ',' [3])+    ,test "." $ expectParseEq p "1.00.000,1" (100000.1, 1, Just ',', Just $ DigitGroups '.' [3,2])+    ,test "." $ expectParseEq p "1,000,000"  (1000000, 0, Nothing, Just $ DigitGroups ',' [3,3])  -- could be simplified to [3]+    ,test "." $ expectParseEq p "1."         (1, 0, Just '.', Nothing)+    ,test "." $ expectParseEq p "1,"         (1, 0, Just ',', Nothing)+    ,test "." $ expectParseEq p ".1"         (0.1, 1, Just '.', Nothing)+    ,test "." $ expectParseEq p ",1"         (0.1, 1, Just ',', Nothing)+    ,test "." $ expectParseError p "" ""+    ,test "." $ expectParseError p "1,000.000,1" ""+    ,test "." $ expectParseError p "1.000,000.1" ""+    ,test "." $ expectParseError p "1,000.000.1" ""+    ,test "." $ expectParseError p "1,,1" ""+    ,test "." $ expectParseError p "1..1" ""+    ,test "." $ expectParseError p ".1," ""+    ,test "." $ expectParseError p ",1." ""+    ]+  +  ,tests "spaceandamountormissingp" [+     test "space and amount" $ expectParseEq spaceandamountormissingp " $47.18" (Mixed [usd 47.18])+    ,test "empty string" $ expectParseEq spaceandamountormissingp "" missingmixedamt+    ,_test "just space" $ expectParseEq spaceandamountormissingp " " missingmixedamt  -- XXX should it ?+    -- ,test "just amount" $ expectParseError spaceandamountormissingp "$47.18" ""  -- succeeds, consuming nothing+    ]++  ]
Hledger/Read/CsvReader.hs view
@@ -18,24 +18,25 @@   reader,   -- * Misc.   CsvRecord,+  CSV, Record, Field,   -- rules,   rulesFileFor,   parseRulesFile,   parseAndValidateCsvRules,   expandIncludes,   transactionFromCsvRecord,+  printCSV,   -- * Tests-  tests_Hledger_Read_CsvReader+  tests_CsvReader, ) where import Prelude ()-import "base-compat-batteries" Prelude.Compat hiding (getContents)+import "base-compat-batteries" Prelude.Compat 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.Char (toLower, isDigit, isSpace, ord) import "base-compat-batteries" Data.List.Compat import Data.List.NonEmpty (fromList) import Data.Maybe@@ -43,6 +44,7 @@ import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Encoding as T import qualified Data.Text.IO as T import Data.Time.Calendar (Day) #if MIN_VERSION_time(1,5,0)@@ -54,19 +56,29 @@ import Safe import System.Directory (doesFileExist) import System.FilePath-import Test.HUnit hiding (State)-import Text.CSV (parseCSV, CSV)+import qualified Data.Csv as Cassava+import qualified Data.Csv.Parser.Megaparsec as CassavaMP+import qualified Data.ByteString as B+import Data.ByteString.Lazy (fromStrict)+import Data.Foldable import Text.Megaparsec hiding (parse) import Text.Megaparsec.Char-import qualified Text.Parsec as Parsec import Text.Printf (printf)+import Data.Word  import Hledger.Data-import Hledger.Utils.UTF8IOCompat (getContents) import Hledger.Utils import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos) +type CSV = [Record] +type Record = [Field]++type Field = String++data CSVError = CSVError (ParseError Word8 CassavaMP.ConversionError)+    deriving Show+ reader :: Reader reader = Reader   {rFormat     = "csv"@@ -80,7 +92,8 @@ parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal parse iopts f t = do   let rulesfile = mrules_file_ iopts-  r <- liftIO $ readJournalFromCsv rulesfile f t+  let separator = separator_ iopts+  r <- liftIO $ readJournalFromCsv separator rulesfile f t   case r of Left e -> throwError e             Right j -> return $ journalNumberAndTieTransactions j -- XXX does not use parseAndFinaliseJournal like the other readers@@ -94,11 +107,11 @@ -- 2. parse the CSV data, or throw a parse error -- 3. convert the CSV records to transactions using the rules -- 4. if the rules file didn't exist, create it with the default rules and filename--- 5. return the transactions as a Journal +-- 5. return the transactions as a 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 =+readJournalFromCsv :: Char -> Maybe FilePath -> FilePath -> Text -> IO (Either String Journal)+readJournalFromCsv _ Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin"+readJournalFromCsv separator mrulesfile csvfile csvdata =  handle (\e -> return $ Left $ show (e :: IOException)) $ do   let throwerr = throw.userError @@ -111,7 +124,7 @@       dbg1IO "using conversion rules file" rulesfile       liftIO $ (readFilePortably rulesfile >>= expandIncludes (takeDirectory rulesfile))     else return $ defaultRulesText rulesfile-  rules <- liftIO (runExceptT $ parseAndValidateCsvRules rulesfile rulestext) >>= either throwerr return +  rules <- liftIO (runExceptT $ parseAndValidateCsvRules rulesfile rulestext) >>= either throwerr return   dbg2IO "rules" rules    -- apply skip directive@@ -126,17 +139,17 @@   records <- (either throwerr id .               dbg2 "validateCsv" . validateCsv skip .               dbg2 "parseCsv")-             `fmap` parseCsv parsecfilename (T.unpack csvdata)+             `fmap` parseCsv separator parsecfilename csvdata   dbg1IO "first 3 csv records" $ take 3 records    -- identify header lines   -- let (headerlines, datalines) = identifyHeaderLines records   --     mfieldnames = lastMay headerlines -  let +  let     -- convert CSV records to transactions     txns = snd $ mapAccumL-                   (\pos r -> +                   (\pos r ->                       let                         SourcePos name line col = pos                         line' = (mkPos . (+1) . unPos) line@@ -148,16 +161,16 @@      -- Ensure transactions are ordered chronologically.     -- First, reverse them to get same-date transactions ordered chronologically,-    -- if the CSV records seem to be most-recent-first, ie if there's an explicit +    -- if the CSV records seem to be most-recent-first, ie if there's an explicit     -- "newest-first" directive, or if there's more than one date and the first date     -- is more recent than the last.-    txns' = +    txns' =       (if newestfirst || mseemsnewestfirst == Just True then reverse else id) txns       where         newestfirst = dbg3 "newestfirst" $ isJust $ getDirective "newest-first" rules-        mseemsnewestfirst = dbg3 "mseemsnewestfirst" $  -          case nub $ map tdate txns of -            ds | length ds > 1 -> Just $ head ds > last ds +        mseemsnewestfirst = dbg3 "mseemsnewestfirst" $+          case nub $ map tdate txns of+            ds | length ds > 1 -> Just $ head ds > last ds             _                  -> Nothing     -- Second, sort by date.     txns'' = sortBy (comparing tdate) txns'@@ -168,14 +181,41 @@    return $ Right nulljournal{jtxns=txns''} -parseCsv :: FilePath -> String -> IO (Either Parsec.ParseError CSV)-parseCsv path csvdata =-  case path of-    "-" -> liftM (parseCSV "(stdin)") getContents-    _   -> return $ parseCSV path csvdata+parseCsv :: Char -> FilePath -> Text -> IO (Either CSVError CSV)+parseCsv separator filePath csvdata =+  case filePath of+    "-" -> liftM (parseCassava separator "(stdin)") T.getContents+    _   -> return $ parseCassava separator filePath csvdata +parseCassava :: Char -> FilePath -> Text -> Either CSVError CSV+parseCassava separator path content =+    case parseResult of+        Left  msg -> Left $ CSVError msg+        Right a   -> Right a+    where parseResult = fmap parseResultToCsv $ CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path lazyContent+          lazyContent = fromStrict $ T.encodeUtf8 content++decodeOptions :: Char -> Cassava.DecodeOptions+decodeOptions separator = Cassava.defaultDecodeOptions {+                      Cassava.decDelimiter = fromIntegral (ord separator)+                    }++parseResultToCsv :: (Foldable t, Functor t) => t (t B.ByteString) -> CSV+parseResultToCsv = toListList . unpackFields+    where+        toListList = toList . fmap toList+        unpackFields  = (fmap . fmap) (T.unpack . T.decodeUtf8)++printCSV :: CSV -> String+printCSV records = unlined (printRecord `map` records)+    where printRecord = concat . intersperse "," . map printField+          printField f = "\"" ++ concatMap escape f ++ "\""+          escape '"' = "\"\""+          escape x = [x]+          unlined = concat . intersperse "\n"+ -- | Return the cleaned up and validated CSV data (can be empty), or an error.-validateCsv :: Int -> Either Parsec.ParseError CSV -> Either String [CsvRecord]+validateCsv :: Int -> Either CSVError CSV -> Either String [CsvRecord] validateCsv _ (Left e) = Left $ show e validateCsv numhdrlines (Right rs) = validate $ drop numhdrlines $ filternulls rs   where@@ -365,11 +405,11 @@ instance ShowErrorComponent String where   showErrorComponent = id --- | An error-throwing action that parses this file's content --- as CSV conversion rules, interpolating any included files first, +-- | An error-throwing action that parses this file's content+-- as CSV conversion rules, interpolating any included files first, -- and runs some extra validation checks. parseRulesFile :: FilePath -> ExceptT String IO CsvRules-parseRulesFile f = +parseRulesFile f =   liftIO (readFilePortably f >>= expandIncludes (takeDirectory f)) >>= parseAndValidateCsvRules f  -- | Inline all files referenced by include directives in this hledger CSV rules text, recursively.@@ -383,9 +423,9 @@           where             f' = dir </> dropWhile isSpace (T.unpack f)             dir' = takeDirectory f'-        _ -> return line +        _ -> return line --- | An error-throwing action that parses this text as CSV conversion rules +-- | An error-throwing action that parses this text as CSV conversion rules -- and runs some extra validation checks. The file path is for error messages. parseAndValidateCsvRules :: FilePath -> T.Text -> ExceptT String IO CsvRules parseAndValidateCsvRules rulesfile s = do@@ -441,7 +481,7 @@           }  blankorcommentlinep :: CsvRulesParser ()-blankorcommentlinep = lift (pdbg 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep]+blankorcommentlinep = lift (dbgparse 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep]  blanklinep :: CsvRulesParser () blanklinep = lift (skipMany spacenonewline) >> newline >> return () <?> "blank line"@@ -454,7 +494,7 @@  directivep :: CsvRulesParser (DirectiveName, String) directivep = (do-  lift $ pdbg 3 "trying directive"+  lift $ dbgparse 3 "trying directive"   d <- fmap T.unpack $ choiceInState $ map (lift . string . T.pack) directives   v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp)        <|> (optional (char ':') >> lift (skipMany spacenonewline) >> lift eolof >> return "")@@ -477,7 +517,7 @@  fieldnamelistp :: CsvRulesParser [CsvFieldName] fieldnamelistp = (do-  lift $ pdbg 3 "trying fieldnamelist"+  lift $ dbgparse 3 "trying fieldnamelist"   string "fields"   optional $ char ':'   lift (skipSome spacenonewline)@@ -503,7 +543,7 @@  fieldassignmentp :: CsvRulesParser (JournalFieldName, FieldTemplate) fieldassignmentp = do-  lift $ pdbg 3 "trying fieldassignmentp"+  lift $ dbgparse 3 "trying fieldassignmentp"   f <- journalfieldnamep   assignmentseparatorp   v <- fieldvalp@@ -512,11 +552,11 @@  journalfieldnamep :: CsvRulesParser String journalfieldnamep = do-  lift (pdbg 2 "trying journalfieldnamep")+  lift (dbgparse 2 "trying journalfieldnamep")   T.unpack <$> choiceInState (map (lift . string . T.pack) journalfieldnames) --- Transaction fields and pseudo fields for CSV conversion. --- Names must precede any other name they contain, for the parser +-- Transaction fields and pseudo fields for CSV conversion.+-- Names must precede any other name they contain, for the parser -- (amount-in before amount; date2 before date). TODO: fix journalfieldnames = [    "account1"@@ -536,7 +576,7 @@  assignmentseparatorp :: CsvRulesParser () assignmentseparatorp = do-  lift $ pdbg 3 "trying assignmentseparatorp"+  lift $ dbgparse 3 "trying assignmentseparatorp"   choice [     -- try (lift (skipMany spacenonewline) >> oneOf ":="),     try (lift (skipMany spacenonewline) >> char ':'),@@ -547,12 +587,12 @@  fieldvalp :: CsvRulesParser String fieldvalp = do-  lift $ pdbg 2 "trying fieldvalp"+  lift $ dbgparse 2 "trying fieldvalp"   anyChar `manyTill` lift eolof  conditionalblockp :: CsvRulesParser ConditionalBlock conditionalblockp = do-  lift $ pdbg 3 "trying conditionalblockp"+  lift $ dbgparse 3 "trying conditionalblockp"   string "if" >> lift (skipMany spacenonewline) >> optional newline   ms <- some recordmatcherp   as <- many (lift (skipSome spacenonewline) >> fieldassignmentp)@@ -563,7 +603,7 @@  recordmatcherp :: CsvRulesParser [String] recordmatcherp = do-  lift $ pdbg 2 "trying recordmatcherp"+  lift $ dbgparse 2 "trying recordmatcherp"   -- pos <- currentPos   _  <- optional (matchoperatorp >> lift (skipMany spacenonewline) >> optional newline)   ps <- patternsp@@ -582,20 +622,20 @@  patternsp :: CsvRulesParser [String] patternsp = do-  lift $ pdbg 3 "trying patternsp"+  lift $ dbgparse 3 "trying patternsp"   ps <- many regexp   return ps  regexp :: CsvRulesParser String regexp = do-  lift $ pdbg 3 "trying regexp"+  lift $ dbgparse 3 "trying regexp"   notFollowedBy matchoperatorp   c <- lift nonspace   cs <- anyChar `manyTill` lift eolof   return $ strip $ c:cs  -- fieldmatcher = do---   pdbg 2 "trying fieldmatcher"+--   dbgparse 2 "trying fieldmatcher" --   f <- fromMaybe "all" `fmap` (optional $ do --          f' <- fieldname --          lift (skipMany spacenonewline)@@ -686,7 +726,7 @@     account1    = T.pack $ maybe "" render (mfieldtemplate "account1") `or` defaccount1     account2    = T.pack $ maybe "" render (mfieldtemplate "account2") `or` defaccount2     balance     = maybe Nothing (parsebalance.render) $ mfieldtemplate "balance"-    parsebalance str +    parsebalance str       | all isSpace str  = Nothing       | otherwise = Just $ (either (balanceerror str) id $ runParser (evalStateT (amountp <* eof) mempty) "" $ T.pack $ (currency++) $ simplifySign str, nullsourcepos)     balanceerror str err = error' $ unlines@@ -740,7 +780,7 @@ type CsvAmountString = String  -- | Canonicalise the sign in a CSV amount string.--- Such strings can have a minus sign, negating parentheses, +-- Such strings can have a minus sign, negating parentheses, -- or any two of these (which cancels out). -- -- >>> simplifySign "1"@@ -800,10 +840,10 @@ renderTemplate ::  CsvRules -> CsvRecord -> FieldTemplate -> String renderTemplate rules record t = regexReplaceBy "%[A-z0-9]+" replace t   where-    replace ('%':pat) = maybe pat (\i -> atDef "" record (i-1)) mi+    replace ('%':pat) = maybe pat (\i -> atDef "" record (i-1)) mindex       where-        mi | all isDigit pat = readMay pat-           | otherwise       = lookup pat $ rcsvfieldindexes rules+        mindex | all isDigit pat = readMay pat+               | otherwise       = lookup (map toLower pat) $ rcsvfieldindexes rules     replace pat       = pat  -- Parse the date string using the specified date-format, or if unspecified try these default formats:@@ -834,67 +874,24 @@ -------------------------------------------------------------------------------- -- tests -tests_Hledger_Read_CsvReader = TestList (test_parser)-                               -- ++ test_description_parsing)---- test_description_parsing = [---       "description-field 1" ~: assertParseDescription "description-field 1\n" [FormatField False Nothing Nothing (FieldNo 1)]---     , "description-field 1 " ~: assertParseDescription "description-field 1 \n" [FormatField False Nothing Nothing (FieldNo 1)]---     , "description-field %(1)" ~: assertParseDescription "description-field %(1)\n" [FormatField False Nothing Nothing (FieldNo 1)]---     , "description-field %(1)/$(2)" ~: assertParseDescription "description-field %(1)/%(2)\n" [---           FormatField False Nothing Nothing (FieldNo 1)---         , FormatLiteral "/"---         , FormatField False Nothing Nothing (FieldNo 2)---         ]---     ]---   where---     assertParseDescription string expected = do assertParseEqual (parseDescription string) (rules {descriptionField = expected})---     parseDescription :: String -> Either ParseError CsvRules---     parseDescription x = runParser descriptionfieldWrapper rules "(unknown)" x---     descriptionfieldWrapper :: GenParser Char CsvRules CsvRules---     descriptionfieldWrapper = do---       descriptionfield---       r <- getState---       return r--test_parser =  [--   "convert rules parsing: empty file" ~: do-     -- let assertMixedAmountParse parseresult mixedamount =-     --         (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)-    assertParseEqual (parseCsvRules "unknown" "") rules--  -- ,"convert rules parsing: accountrule" ~: do-  --    assertParseEqual (parseWithState rules accountrule "A\na\n") -- leading blank line required-  --                ([("A",Nothing)], "a")--  ,"convert rules parsing: trailing comments" ~: do-     assertParse (parseWithState' rules rulesp "skip\n# \n#\n")--  ,"convert rules parsing: trailing blank lines" ~: do-     assertParse (parseWithState' rules rulesp "skip\n\n  \n")+tests_CsvReader = tests "CsvReader" [+   tests "parseCsvRules" [+     test "empty file" $+      parseCsvRules "unknown" "" `is` Right rules+    ]+  ,tests "rulesp" [+     test "trailing comments" $+      parseWithState' rules rulesp "skip\n# \n#\n" `is` Right rules{rdirectives = [("skip","")]} -  ,"convert rules parsing: empty field value" ~: do-     assertParse (parseWithState' rules rulesp "account1 \nif foo\n  account2 foo\n")+    ,test "trailing blank lines" $+      parseWithState' rules rulesp "skip\n\n  \n" `is` (Right rules{rdirectives = [("skip","")]}) -  -- not supported-  -- ,"convert rules parsing: no final newline" ~: do-  --    assertParse (parseWithState rules csvrulesfile "A\na")-  --    assertParse (parseWithState rules csvrulesfile "A\na\n# \n#")-  --    assertParse (parseWithState rules csvrulesfile "A\na\n\n  ")+    ,test "no final newline" $+      parseWithState' rules rulesp "skip" `is` (Right rules{rdirectives=[("skip","")]}) -                 -- (rules{-                 --   -- dateField=Maybe FieldPosition,-                 --   -- statusField=Maybe FieldPosition,-                 --   -- codeField=Maybe FieldPosition,-                 --   -- descriptionField=Maybe FieldPosition,-                 --   -- amountField=Maybe FieldPosition,-                 --   -- currencyField=Maybe FieldPosition,-                 --   -- baseCurrency=Maybe String,-                 --   -- baseAccount=AccountName,-                 --   accountRules=[-                 --        ([("A",Nothing)], "a")-                 --       ]-                 --  })+    ,test "assignment with empty value" $+      parseWithState' rules rulesp "account1 \nif foo\n  account2 foo\n" `is`+        (Right rules{rassignments = [("account1","")], rconditionalblocks = [([["foo"]],[("account2","foo")])]}) +    ]   ]
Hledger/Read/JournalReader.hs view
@@ -51,21 +51,14 @@   marketpricedirectivep,   datetimep,   datep,-  -- codep,-  -- accountnamep,   modifiedaccountnamep,   postingp,-  -- amountp,-  -- amountp',-  -- mamountp',-  -- numberp,   statusp,   emptyorcommentlinep,   followingcommentp    -- * Tests-  ,tests_Hledger_Read_JournalReader-+  ,tests_JournalReader ) where --- * imports@@ -84,16 +77,12 @@ 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.Megaparsec.Char import Text.Megaparsec.Custom import Text.Printf import System.FilePath+import "Glob" System.FilePath.Glob hiding (match)  import Hledger.Data import Hledger.Read.Common@@ -153,7 +142,7 @@   choice [       directivep     , transactionp          >>= modify' . addTransaction-    , modifiertransactionp  >>= modify' . addModifierTransaction+    , transactionmodifierp  >>= modify' . addTransactionModifier     , periodictransactionp  >>= modify' . addPeriodicTransaction     , marketpricedirectivep >>= modify' . addMarketPrice     , void (lift emptyorcommentlinep)@@ -191,39 +180,62 @@   lift (skipSome spacenonewline)   filename <- T.unpack <$> takeWhileP Nothing (/= '\n') -- don't consume newline yet -  -- save parent state-  parentParserState <- getParserState-  parentj <- get--  let childj = newJournalWithParseStateFrom parentj   parentpos <- getPosition -  -- read child input-  let curdir = takeDirectory (sourceName parentpos)-  filepath <- lift $ expandPath curdir filename `orRethrowIOError` (show parentpos ++ " locating " ++ filename)-  childInput <- lift $ readFilePortably filepath `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)+  filepaths <- getFilePaths parentpos filename -  -- set child state-  setInput childInput-  pushPosition $ initialPos filepath-  put childj+  forM_ filepaths $ parseChild parentpos -  -- parse include file-  let parsers = [ journalp-                , timeclockfilep-                , timedotfilep-                ] -- can't include a csv file yet, that reader is special-  updatedChildj <- journalAddFile (filepath, childInput) <$>-                   region (withSource childInput) (choiceInState parsers)+  void newline -  -- restore parent state, prepending the child's parse info-  setParserState parentParserState-  put $ updatedChildj <> parentj-  -- discard child's parse info, prepend its (reversed) list data, combine other fields+  where+    getFilePaths parserpos filename = do+        curdir <- lift $ expandPath (takeDirectory $ sourceName parserpos) ""+                         `orRethrowIOError` (show parserpos ++ " locating " ++ filename)+        -- Compiling filename as a glob pattern works even if it is a literal+        fileglob <- case tryCompileWith compDefault{errorRecovery=False} filename of+            Right x -> pure x+            Left e -> parseErrorAt parserpos $ "Invalid glob pattern: " ++ e+        -- Get all matching files in the current working directory, sorting in+        -- lexicographic order to simulate the output of 'ls'.+        filepaths <- liftIO $ sort <$> globDir1 fileglob curdir+        if (not . null) filepaths+            then pure filepaths+            else parseErrorAt parserpos $ "No existing files match pattern: " ++ filename -  void newline+    parseChild parentpos filepath = do+        parentfilestack <- fmap sourceName . statePos <$> getParserState+        when (filepath `elem` parentfilestack)+            $ parseErrorAt parentpos ("Cyclic include: " ++ filepath) +        childInput <- lift $ readFilePortably filepath+                             `orRethrowIOError` (show parentpos ++ " reading " ++ filepath) +        -- save parent state+        parentParserState <- getParserState+        parentj <- get++        let childj = newJournalWithParseStateFrom parentj++        -- set child state+        setInput childInput+        pushPosition $ initialPos filepath+        put childj++        -- parse include file+        let parsers = [ journalp+                      , timeclockfilep+                      , timedotfilep+                      ] -- can't include a csv file yet, that reader is special+        updatedChildj <- journalAddFile (filepath, childInput) <$>+                        region (withSource childInput) (choiceInState parsers)++        -- restore parent state, prepending the child's parse info+        setParserState parentParserState+        put $ updatedChildj <> parentj+        -- discard child's parse info, prepend its (reversed) list data, combine other fields++ newJournalWithParseStateFrom :: Journal -> Journal newJournalWithParseStateFrom j = mempty{    jparsedefaultyear      = jparsedefaultyear j@@ -249,12 +261,10 @@   string "account"   lift (skipSome spacenonewline)   acct <- modifiedaccountnamep  -- account directives can be modified by alias/apply account-  macode' :: Maybe String <- (optional $ lift $ skipSome spacenonewline >> some digitChar)-  let macode :: Maybe AccountCode = read <$> macode'+  _ :: Maybe String <- (optional $ lift $ skipSome spacenonewline >> some digitChar)  -- compatibility: ignore account codes supported in 1.9/1.10   newline   skipMany indentedlinep-    -  modify' (\j -> j{jaccounts = (acct, macode) : jaccounts j})+  pushDeclaredAccount acct  indentedlinep :: JournalParser m String indentedlinep = lift (skipSome spacenonewline) >> (rstrip <$> lift restofline)@@ -288,7 +298,7 @@   else modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})  pleaseincludedecimalpoint :: String-pleaseincludedecimalpoint = "to avoid ambiguity, please include a decimal point in commodity directives"+pleaseincludedecimalpoint = "to avoid ambiguity, please include a decimal separator in commodity directives"  -- | Parse a multi-line commodity directive, containing 0 or more format subdirectives. --@@ -357,7 +367,7 @@  basicaliasp :: TextParser m AccountAlias basicaliasp = do-  -- pdbg 0 "basicaliasp"+  -- dbgparse 0 "basicaliasp"   old <- rstrip <$> (some $ noneOf ("=" :: [Char]))   char '='   skipMany spacenonewline@@ -366,7 +376,7 @@  regexaliasp :: TextParser m AccountAlias regexaliasp = do-  -- pdbg 0 "regexaliasp"+  -- dbgparse 0 "regexaliasp"   char '/'   re <- some $ noneOf ("/\n\r" :: [Char]) -- paranoid: don't try to read past line end   char '/'@@ -449,13 +459,14 @@  --- ** transactions -modifiertransactionp :: JournalParser m ModifierTransaction-modifiertransactionp = do+transactionmodifierp :: JournalParser m TransactionModifier+transactionmodifierp = do   char '=' <?> "modifier transaction"   lift (skipMany spacenonewline)-  valueexpr <- T.pack <$> lift restofline+  querytxt <- lift $ T.strip <$> descriptionp+  (_comment, _tags) <- lift transactioncommentp   -- TODO apply these to modified txns ?   postings <- postingsp Nothing-  return $ ModifierTransaction valueexpr postings+  return $ TransactionModifier querytxt postings  -- | Parse a periodic transaction periodictransactionp :: MonadIO m => JournalParser m PeriodicTransaction@@ -504,7 +515,7 @@ -- | Parse a (possibly unbalanced) transaction. transactionp :: JournalParser m Transaction transactionp = do-  -- ptrace "transactionp"+  -- dbgparse 0 "transactionp"   startpos <- getPosition   date <- datep <?> "transaction"   edate <- optional (lift $ secondarydatep date) <?> "secondary date"@@ -519,99 +530,6 @@   let sourcepos = journalSourcePos startpos endpos   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=Unmarked,-      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@@ -628,7 +546,7 @@  postingp :: Maybe Year -> JournalParser m Posting postingp mTransactionYear = do-  -- pdbg 0 "postingp"+  -- lift $ dbgparse 0 "postingp"   (status, account) <- try $ do     lift (skipSome spacenonewline)     status <- lift statusp@@ -654,155 +572,261 @@    , 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")]}+--- * tests -    " 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"}+tests_JournalReader = tests "JournalReader" [ -    " 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}+   let p = lift accountnamep :: JournalParser IO AccountName in+   tests "accountnamep" [+     test "basic" $ expectParse p "a:b:c"+    ,_test "empty inner component" $ expectParseError p "a::c" ""  -- TODO+    ,_test "empty leading component" $ expectParseError p ":b:c" "x"+    ,_test "empty trailing component" $ expectParseError p "a:b:" "x"+    ] -    " 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"}+  -- "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."+  ,test "datep" $ do+    test "YYYY/MM/DD" $ expectParseEq datep "2018/01/01" (fromGregorian 2018 1 1)+    test "YYYY-MM-DD" $ expectParse datep "2018-01-01"+    test "YYYY.MM.DD" $ expectParse datep "2018.01.01"+    test "yearless date with no default year" $ expectParseError datep "1/1" "current year is unknown"+    test "yearless date with default year" $ do +      ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep "1/1"+      either (fail.("parse error at "++).parseErrorPretty) (const ok) ep+    test "no leading zero" $ expectParse datep "2018/1/1" -    assertBool -- "postingp parses a quoted commodity with numbers"-      (isRight $ parseWithState mempty (postingp Nothing) "  a  1 \"DE123\"\n")+  ,test "datetimep" $ do+      let+        good = expectParse datetimep+        bad = (\t -> expectParseError datetimep t "")+      good "2011/1/1 00:00"+      good "2011/1/1 23:59:59"+      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"+      bad "2011/1/1 3:5:7"+      test "timezone is parsed but ignored" $ do+        let t = LocalTime (fromGregorian 2018 1 1) (TimeOfDay 0 0 (fromIntegral 0))+        expectParseEq datetimep "2018/1/1 00:00-0800" t+        expectParseEq datetimep "2018/1/1 00:00+1234" t -  -- ,"postingp parses balance assertions and fixed lot prices" ~: do-    assertBool (isRight $ parseWithState mempty (postingp Nothing) "  a  1 \"DE123\" =$1 { =2.2 EUR} \n")+  ,tests "periodictransactionp" [ -    -- 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+    test "more period text in comment after one space" $ expectParseEq periodictransactionp+      "~ monthly from 2018/6 ;In 2019 we will change this\n" +      nullperiodictransaction {+         ptperiodexpr  = "monthly from 2018/6"+        ,ptinterval    = Months 1+        ,ptspan        = DateSpan (Just $ fromGregorian 2018 6 1) Nothing+        ,ptstatus      = Unmarked+        ,ptcode        = ""+        ,ptdescription = ""+        ,ptcomment     = "In 2019 we will change this\n"+        ,pttags        = []+        ,ptpostings    = []+        } ---- * more tests+     -- TODO #807+    ,_test "more period text in description after two spaces" $ expectParseEq periodictransactionp +      "~ monthly from 2018/6   In 2019 we will change this\n" +      nullperiodictransaction {+         ptperiodexpr  = "monthly from 2018/6"+        ,ptinterval    = Months 1+        ,ptspan        = DateSpan (Just $ fromGregorian 2018 6 1) Nothing+        ,ptdescription = "In 2019 we will change this\n"+        } -tests_Hledger_Read_JournalReader = TestList $ concat [-    -- test_numberp-    [-    "showParsedMarketPrice" ~: do-      let mp = parseWithState mempty marketpricedirectivep "P 2017/01/30 BTC $922.83\n"-          mpString = (fmap . fmap) showMarketPrice mp-      mpString `is` (Just (Right "P 2017/01/30 BTC $922.83"))+    ,_test "more period text in description after one space" $ expectParseEq periodictransactionp+      "~ monthly from 2018/6 In 2019 we will change this\n" +      nullperiodictransaction {+         ptperiodexpr  = "monthly from 2018/6"+        ,ptinterval    = Months 1+        ,ptspan        = DateSpan (Just $ fromGregorian 2018 6 1) Nothing+        ,ptdescription = "In 2019 we will change this\n"+        }++    ,_test "Next year in description" $ expectParseEq periodictransactionp+      "~ monthly  Next year blah blah\n"+      nullperiodictransaction {+         ptperiodexpr  = "monthly"+        ,ptinterval    = Months 1+        ,ptspan        = DateSpan Nothing Nothing+        ,ptdescription = "Next year blah blah\n"+        }+     ]- ] -{- old hunit tests+  ,tests "postingp" [+     test "basic" $ expectParseEq (postingp Nothing) +      "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n"+      posting{+        paccount="expenses:food:dining", +        pamount=Mixed [usd 10], +        pcomment="a: a a\nb: b b\n", +        ptags=[("a","a a"), ("b","b b")]+        } -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")+    ,test "posting dates" $ expectParseEq (postingp Nothing) +      " a  1. ; date:2012/11/28, date2=2012/11/29,b:b\n"+      nullposting{+         paccount="a"+        ,pamount=Mixed [num 1]+        ,pcomment="date:2012/11/28, date2=2012/11/29,b:b\n"+        ,ptags=[("date", "2012/11/28"), ("date2=2012/11/29,b", "b")] -- TODO tag name parsed too greedily+        ,pdate=Just $ fromGregorian 2012 11 28+        ,pdate2=Nothing  -- Just $ fromGregorian 2012 11 29+        } -  ,"periodictransactionp" ~: do-     assertParse (parseWithState mempty periodictransactionp "~ (some period expr)\n some:postings  1\n")+    ,test "posting dates bracket syntax" $ expectParseEq (postingp Nothing) +      " a  1. ; [2012/11/28=2012/11/29]\n"+      nullposting{+         paccount="a"+        ,pamount=Mixed [num 1]+        ,pcomment="[2012/11/28=2012/11/29]\n"+        ,ptags=[]+        ,pdate= Just $ fromGregorian 2012 11 28 +        ,pdate2=Just $ fromGregorian 2012 11 29+        } -  ,"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")+    ,test "quoted commodity symbol with digits" $ expectParse (postingp Nothing) "  a  1 \"DE123\"\n" -  ,"comment" ~: do-     assertParse (parseWithState mempty comment "; some comment \n")-     assertParse (parseWithState mempty comment " \t; x\n")-     assertParse (parseWithState mempty comment "#x")+    ,test "balance assertion and fixed lot price" $ expectParse (postingp Nothing) "  a  1 \"DE123\" =$1 { =2.2 EUR} \n"+    ] -  ,"datep" ~: do-     assertParse (parseWithState mempty datep "2011/1/1")-     assertParseFailure (parseWithState mempty datep "1/1")-     assertParse (parseWithState mempty{jpsYear=Just 2011} datep "1/1")+  ,tests "transactionmodifierp" [ -  ,"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+    test "basic" $ expectParseEq transactionmodifierp +      "= (some value expr)\n some:postings  1.\n"+      nulltransactionmodifier {+        tmquerytxt = "(some value expr)"+       ,tmpostings = [nullposting{paccount="some:postings", pamount=Mixed[num 1]}]+      }+    ] -  ,"defaultyeardirectivep" ~: do-     assertParse (parseWithState mempty defaultyeardirectivep "Y 2010\n")-     assertParse (parseWithState mempty defaultyeardirectivep "Y 10001\n")+  ,tests "transactionp" [+  +     test "just a date" $ expectParseEq transactionp "2015/1/1\n" nulltransaction{tdate=fromGregorian 2015 1 1}+  +    ,test "more complex" $ expectParseEq transactionp +      (T.unlines [+        "2012/05/14=2012/05/15 (code) desc  ; tcomment1",+        "    ; tcomment2",+        "    ; ttag1: val1",+        "    * a         $1.00  ; pcomment1",+        "    ; pcomment2",+        "    ; ptag1: val1",+        "    ; ptag2: val2"+        ])+      nulltransaction{+        tsourcepos=JournalSourcePos "" (1,7),  -- XXX why 7 here ?+        tpreceding_comment_lines="",+        tdate=fromGregorian 2012 5 14,+        tdate2=Just $ fromGregorian 2012 5 15,+        tstatus=Unmarked,+        tcode="code",+        tdescription="desc",+        tcomment="tcomment1\ntcomment2\nttag1: val1\n",+        ttags=[("ttag1","val1")],+        tpostings=[+          nullposting{+            pdate=Nothing,+            pstatus=Cleared,+            paccount="a",+            pamount=Mixed [usd 1],+            pcomment="pcomment1\npcomment2\nptag1: val1\nptag2: val2\n",+            ptype=RegularPosting,+            ptags=[("ptag1","val1"),("ptag2","val2")],+            ptransaction=Nothing+            }+          ]+      }+  +    ,test "parses a well-formed transaction" $+      expect $ isRight $ rjp transactionp $ T.unlines+        ["2007/01/28 coopportunity"+        ,"    expenses:food:groceries                   $47.18"+        ,"    assets:checking                          $-47.18"+        ,""+        ]+  +    ,test "does not parse a following comment as part of the description" $+      expectParseEqOn transactionp "2009/1/1 a ;comment\n b 1\n" tdescription "a"+  +    ,test "transactionp parses a following whitespace line" $+      expect $ isRight $ rjp transactionp $ T.unlines+        ["2012/1/1"+        ,"  a  1"+        ,"  b"+        ," "+        ]+  +    ,test "comments everywhere, two postings parsed" $+      expectParseEqOn transactionp +        (T.unlines+          ["2009/1/1 x  ; transaction comment"+          ," a  1  ; posting 1 comment"+          ," ; posting 1 comment 2"+          ," b"+          ," ; posting 2 comment"+          ])+        (length . tpostings)+        2+  +    ] -  ,"marketpricedirectivep" ~:-    assertParseEqual (parseWithState mempty marketpricedirectivep "P 2004/05/01 XYZ $55.00\n") (MarketPrice (parsedate "2004/05/01") "XYZ" $ usd 55)+  -- directives -  ,"ignoredpricecommoditydirectivep" ~: do-     assertParse (parseWithState mempty ignoredpricecommoditydirectivep "N $\n")+  ,tests "directivep" [+    test "supports !" $ do +      expectParse directivep "!account a\n"+      expectParse directivep "!D 1.0\n"+    ] -  ,"defaultcommoditydirectivep" ~: do-     assertParse (parseWithState mempty defaultcommoditydirectivep "D $1,000.0\n")+  ,test "accountdirectivep" $ do+    test "account" $ expectParse accountdirectivep "account a:b\n"+    test "does not support !" $ expectParseError accountdirectivep "!account a:b\n" "" -  ,"commodityconversiondirectivep" ~: do-     assertParse (parseWithState mempty commodityconversiondirectivep "C 1h = $50.00\n")+  ,test "commodityconversiondirectivep" $ do+     expectParse commodityconversiondirectivep "C 1h = $50.00\n" -  ,"tagdirectivep" ~: do-     assertParse (parseWithState mempty tagdirectivep "tag foo \n")+  ,test "defaultcommoditydirectivep" $ do+     expectParse defaultcommoditydirectivep "D $1,000.0\n"+     expectParseError defaultcommoditydirectivep "D $1000\n" "please include a decimal separator" -  ,"endtagdirectivep" ~: do-     assertParse (parseWithState mempty endtagdirectivep "end tag \n")-     assertParse (parseWithState mempty endtagdirectivep "pop \n")+  ,test "defaultyeardirectivep" $ do+    test "1000" $ expectParse defaultyeardirectivep "Y 1000" -- XXX no \n like the others+    test "999" $ expectParseError defaultyeardirectivep "Y 999" "bad year number"+    test "12345" $ expectParse defaultyeardirectivep "Y 12345" -  ,"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:")+  ,test "ignoredpricecommoditydirectivep" $ do+     expectParse ignoredpricecommoditydirectivep "N $\n" -  ,"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)+  ,test "includedirectivep" $ do+    test "include" $ expectParseError includedirectivep "include nosuchfile\n" "No existing files match pattern: nosuchfile"+    test "glob" $ expectParseError includedirectivep "include nosuchfile*\n" "No existing files match pattern: nosuchfile*" -  ,"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))+  ,test "marketpricedirectivep" $ expectParseEq marketpricedirectivep+    "P 2017/01/30 BTC $922.83\n"+    MarketPrice{+      mpdate      = fromGregorian 2017 1 30,+      mpcommodity = "BTC",+      mpamount    = usd 922.83+      } - ]]--}+  ,test "tagdirectivep" $ do+     expectParse tagdirectivep "tag foo \n"++  ,test "endtagdirectivep" $ do+     expectParse endtagdirectivep "end tag \n"+     expectParse endtagdirectivep "pop \n"+++  ,tests "journalp" [+    test "empty file" $ expectParseEq journalp "" nulljournal+    ]++  ]
Hledger/Read/TimeclockReader.hs view
@@ -47,8 +47,6 @@   reader,   -- * Misc other exports   timeclockfilep,-  -- * Tests-  tests_Hledger_Read_TimeclockReader ) where import           Prelude ()@@ -59,7 +57,6 @@ import           Data.Maybe (fromMaybe) import           Data.Text (Text) import qualified Data.Text as T-import           Test.HUnit import           Text.Megaparsec hiding (parse) import           Text.Megaparsec.Char @@ -116,6 +113,4 @@   description <- T.pack . fromMaybe "" <$> lift (optional (skipSome spacenonewline >> restofline))   return $ TimeclockEntry sourcepos (read [code]) datetime account description -tests_Hledger_Read_TimeclockReader = TestList [- ] 
Hledger/Read/TimedotReader.hs view
@@ -30,8 +30,6 @@   reader,   -- * Misc other exports   timedotfilep,-  -- * Tests-  tests_Hledger_Read_TimedotReader ) where import Prelude ()@@ -43,19 +41,18 @@ import Data.List (foldl') import Data.Maybe import Data.Text (Text)-import Test.HUnit import Text.Megaparsec hiding (parse) import Text.Megaparsec.Char  import Hledger.Data import Hledger.Read.Common-import Hledger.Utils hiding (ptrace)+import Hledger.Utils hiding (traceParse)  -- easier to toggle this here sometimes--- import qualified Hledger.Utils (ptrace)--- ptrace = Hledger.Utils.ptrace-ptrace :: Monad m => a -> m a-ptrace = return+-- import qualified Hledger.Utils (parsertrace)+-- parsertrace = Hledger.Utils.parsertrace+traceParse :: Monad m => a -> m a+traceParse = return  reader :: Reader reader = Reader@@ -76,7 +73,7 @@     where       timedotfileitemp :: JournalParser m ()       timedotfileitemp = do-        ptrace "timedotfileitemp"+        traceParse "timedotfileitemp"         choice [           void $ lift emptyorcommentlinep          ,timedotdayp >>= \ts -> modify' (addTransactions ts)@@ -94,7 +91,7 @@ -- @ timedotdayp :: JournalParser m [Transaction] timedotdayp = do-  ptrace " timedotdayp"+  traceParse " timedotdayp"   d <- datep <* lift eolof   es <- catMaybes <$> many (const Nothing <$> try (lift emptyorcommentlinep) <|>                             Just <$> (notFollowedBy datep >> timedotentryp))@@ -106,7 +103,7 @@ -- @ timedotentryp :: JournalParser m Transaction timedotentryp = do-  ptrace "  timedotentryp"+  traceParse "  timedotentryp"   pos <- genericSourcePos <$> getPosition   lift (skipMany spacenonewline)   a <- modifiedaccountnamep@@ -173,7 +170,3 @@ timedotdotsp = do   dots <- filter (not.isSpace) <$> many (oneOf (". " :: [Char]))   return $ (/4) $ fromIntegral $ length dots--tests_Hledger_Read_TimedotReader = TestList [- ]-
Hledger/Reports.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveDataTypeable, FlexibleInstances #-} {-|  Generate several common kinds of report from a journal, as \"*Report\" -@@ -18,14 +18,11 @@   module Hledger.Reports.MultiBalanceReports,   module Hledger.Reports.BudgetReport, --   module Hledger.Reports.BalanceHistoryReport,-   -- * Tests-  tests_Hledger_Reports+  tests_Reports ) where -import Test.HUnit- import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes import Hledger.Reports.EntriesReport@@ -35,15 +32,14 @@ import Hledger.Reports.MultiBalanceReports import Hledger.Reports.BudgetReport -- import Hledger.Reports.BalanceHistoryReport+import Hledger.Utils.Test -tests_Hledger_Reports :: Test-tests_Hledger_Reports = TestList $- -- ++ tests_isInterestingIndented- [- tests_Hledger_Reports_ReportOptions,- tests_Hledger_Reports_EntriesReport,- tests_Hledger_Reports_PostingsReport,- tests_Hledger_Reports_BalanceReport,- tests_Hledger_Reports_MultiBalanceReport,- tests_Hledger_Reports_BudgetReport- ]+tests_Reports = tests "Reports" [+   tests_BalanceReport+  ,tests_BudgetReport+  ,tests_EntriesReport+  ,tests_MultiBalanceReports+  ,tests_PostingsReport+  ,tests_ReportOptions+  ,tests_TransactionsReports+  ]
Hledger/Reports/BalanceHistoryReport.hs view
@@ -8,14 +8,10 @@  module Hledger.Reports.BalanceHistoryReport (   accountBalanceHistory--  -- -- * Tests-  -- tests_Hledger_Reports_BalanceReport ) where  import Data.Time.Calendar--- import Test.HUnit  import Hledger.Data import Hledger.Query
Hledger/Reports/BalanceReport.hs view
@@ -18,9 +18,10 @@   BalanceReportItem,   balanceReport,   flatShowsExclusiveBalance,+  sortAccountItemsLike,     -- * Tests-  tests_Hledger_Reports_BalanceReport+  tests_BalanceReport ) where @@ -28,12 +29,11 @@ import Data.Ord import Data.Maybe import Data.Time.Calendar-import Test.HUnit  import Hledger.Data import Hledger.Read (mamountp') import Hledger.Query-import Hledger.Utils+import Hledger.Utils  import Hledger.Reports.ReportOptions  @@ -79,7 +79,7 @@ balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport balanceReport opts q j =    (if invert_ opts then brNegate else id) $ -  (items, total)+  (sorteditems, total)     where       -- dbg1 = const id -- exclude from debug output       dbg1 s = let p = "balanceReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in debug output@@ -90,7 +90,6 @@                          dbg1 "accts" $                          take 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts           | flat_ opts = dbg1 "accts" $-                         sortflat $                          filterzeros $                          filterempty $                          drop 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts@@ -99,27 +98,52 @@                          drop 1 $ flattenAccounts $                          markboring $                          prunezeros $-                         sorttree $+                         sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) $                          clipAccounts (queryDepth q) accts           where-            balance     = if flat_ opts then aebalance else aibalance+            balance   = if flat_ opts then aebalance else aibalance             filterzeros = if empty_ opts then id else filter (not . isZeroMixedAmount . balance)             filterempty = filter (\a -> anumpostings a > 0 || not (isZeroMixedAmount (balance a)))             prunezeros  = if empty_ opts then id else fromMaybe nullacct . pruneAccounts (isZeroMixedAmount . balance)             markboring  = if no_elide_ opts then id else markBoringParentAccounts-            sortflat | sort_amount_ opts = sortBy (maybeflip $ comparing balance)-                     | otherwise         = sortBy (comparing accountCodeAndNameForSort)-              where-                maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip-            sorttree | sort_amount_ opts = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts)-                     | otherwise         = sortAccountTreeByAccountCodeAndName+       items = dbg1 "items" $ map (balanceReportItem opts q) accts'++      -- now sort items like MultiBalanceReport, except +      -- sorting a tree by amount was more easily done above+      sorteditems +        | sort_amount_ opts && tree_ opts = items+        | sort_amount_ opts               = sortFlatBRByAmount items+        | otherwise                       = sortBRByAccountDeclaration items+      +        where    +          -- Sort the report rows, representing a flat account list, by row total. +          sortFlatBRByAmount :: [BalanceReportItem] -> [BalanceReportItem]+          sortFlatBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fourth4))+            where+              maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip+    +          -- Sort the report rows by account declaration order then account name. +          sortBRByAccountDeclaration :: [BalanceReportItem] -> [BalanceReportItem]+          sortBRByAccountDeclaration rows = sortedrows+            where +              anamesandrows = [(first4 r, r) | r <- rows]+              anames = map fst anamesandrows+              sortedanames = sortAccountNamesByDeclaration j (tree_ opts) anames+              sortedrows = sortAccountItemsLike sortedanames anamesandrows +       total | not (flat_ opts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0]             | otherwise        = dbg1 "total" $                                  if flatShowsExclusiveBalance                                  then sum $ map fourth4 items                                  else sum $ map aebalance $ clipAccountsAndAggregate 1 accts' +-- | A sorting helper: sort a list of things (eg report rows) keyed by account name+-- to match the provided ordering of those same account names.+sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b] +sortAccountItemsLike sortedas items =+  concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas+ -- | In an account tree with zero-balance leaves removed, mark the -- elidable parent accounts (those with one subaccount and no balance -- of their own).@@ -156,216 +180,6 @@   where     brItemNegate (a, a', d, amt) = (a, a', d, -amt) -tests_balanceReport =-  let-    (opts,journal) `gives` r = do-      let (eitems, etotal) = r-          (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal-          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-  in [--   "balanceReport with no args on null journal" ~: do-   (defreportopts, nulljournal) `gives` ([], Mixed [nullamt])--  ,"balanceReport with no args on sample journal" ~: do-   (defreportopts, samplejournal) `gives`-    ([-      ("assets","assets",0, mamountp' "$0.00")-     ,("assets:bank","bank",1, mamountp' "$2.00")-     ,("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","income",0, mamountp' "$-2.00")-     ,("income:gifts","gifts",1, mamountp' "$-1.00")-     ,("income:salary","salary",1, mamountp' "$-1.00")-     ],-     Mixed [usd0])--  ,"balanceReport with --depth=N" ~: do-   (defreportopts{depth_=Just 1}, samplejournal) `gives`-    ([-     ("expenses",    "expenses",    0, mamountp'  "$2.00")-     ,("income",      "income",      0, mamountp' "$-2.00")-     ],-     Mixed [usd0])--  ,"balanceReport with depth:N" ~: do-   (defreportopts{query_="depth:1"}, samplejournal) `gives`-    ([-     ("expenses",    "expenses",    0, mamountp'  "$2.00")-     ,("income",      "income",      0, mamountp' "$-2.00")-     ],-     Mixed [usd0])--  ,"balanceReport with a date or secondary date span" ~: do-   (defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives`-    ([],-     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")-     ],-     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")-     ],-     Mixed [usd0])--  ,"balanceReport with not:desc:" ~: do-   (defreportopts{query_="not:desc:income"}, 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:gifts","income:gifts",0, mamountp' "$-1.00")-     ],-     Mixed [usd0])--  ,"balanceReport with period on a populated period" ~: do-    (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}, samplejournal) `gives`-     (-      [-       ("assets:bank:checking","assets:bank:checking",0, mamountp' "$1.00")-      ,("income:salary","income:salary",0, mamountp' "$-1.00")-      ],-      Mixed [usd0])--   ,"balanceReport with period on an unpopulated period" ~: do-    (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}, samplejournal) `gives`-     ([],Mixed [nullamt])----{--    ,"accounts report with account pattern o" ~:-     defreportopts{patterns_=["o"]} `gives`-     ["                  $1  expenses:food"-     ,"                 $-2  income"-     ,"                 $-1    gifts"-     ,"                 $-1    salary"-     ,"--------------------"-     ,"                 $-1"-     ]--    ,"accounts report with account pattern o and --depth 1" ~:-     defreportopts{patterns_=["o"],depth_=Just 1} `gives`-     ["                  $1  expenses"-     ,"                 $-2  income"-     ,"--------------------"-     ,"                 $-1"-     ]--    ,"accounts report with account pattern a" ~:-     defreportopts{patterns_=["a"]} `gives`-     ["                 $-1  assets"-     ,"                  $1    bank:saving"-     ,"                 $-2    cash"-     ,"                 $-1  income:salary"-     ,"                  $1  liabilities:debts"-     ,"--------------------"-     ,"                 $-1"-     ]--    ,"accounts report with account pattern e" ~:-     defreportopts{patterns_=["e"]} `gives`-     ["                 $-1  assets"-     ,"                  $1    bank:saving"-     ,"                 $-2    cash"-     ,"                  $2  expenses"-     ,"                  $1    food"-     ,"                  $1    supplies"-     ,"                 $-2  income"-     ,"                 $-1    gifts"-     ,"                 $-1    salary"-     ,"                  $1  liabilities:debts"-     ,"--------------------"-     ,"                   0"-     ]--    ,"accounts report with unmatched parent of two matched subaccounts" ~:-     defreportopts{patterns_=["cash","saving"]} `gives`-     ["                 $-1  assets"-     ,"                  $1    bank:saving"-     ,"                 $-2    cash"-     ,"--------------------"-     ,"                 $-1"-     ]--    ,"accounts report with multi-part account name" ~:-     defreportopts{patterns_=["expenses:food"]} `gives`-     ["                  $1  expenses:food"-     ,"--------------------"-     ,"                  $1"-     ]--    ,"accounts report with negative account pattern" ~:-     defreportopts{patterns_=["not:assets"]} `gives`-     ["                  $2  expenses"-     ,"                  $1    food"-     ,"                  $1    supplies"-     ,"                 $-2  income"-     ,"                 $-1    gifts"-     ,"                 $-1    salary"-     ,"                  $1  liabilities:debts"-     ,"--------------------"-     ,"                  $1"-     ]--    ,"accounts report negative account pattern always matches full name" ~:-     defreportopts{patterns_=["not:e"]} `gives`-     ["--------------------"-     ,"                   0"-     ]--    ,"accounts report negative patterns affect totals" ~:-     defreportopts{patterns_=["expenses","not:food"]} `gives`-     ["                  $1  expenses:supplies"-     ,"--------------------"-     ,"                  $1"-     ]--    ,"accounts report with -E shows zero-balance accounts" ~:-     defreportopts{patterns_=["assets"],empty_=True} `gives`-     ["                 $-1  assets"-     ,"                  $1    bank"-     ,"                   0      checking"-     ,"                  $1      saving"-     ,"                 $-2    cash"-     ,"--------------------"-     ,"                 $-1"-     ]--    ,"accounts report with cost basis" ~: do-       j <- (readJournal def Nothing $ unlines-              [""-              ,"2008/1/1 test           "-              ,"  a:b          10h @ $50"-              ,"  c:d                   "-              ]) >>= either error' return-       let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment-       balanceReportAsText defreportopts (balanceReport defreportopts Any j') `is`-         ["                $500  a:b"-         ,"               $-500  c:d"-         ,"--------------------"-         ,"                   0"-         ]--}- ]- Right samplejournal2 =   journalBalanceTransactions False     nulljournal{@@ -389,14 +203,219 @@       ]     } --- tests_isInterestingIndented = [---   "isInterestingIndented" ~: do---    let (opts, journal, acctname) `gives` r = isInterestingIndented opts l acctname `is` r---           where l = ledgerFromJournal (queryFromOpts nulldate opts) journal+-- tests ---    (defreportopts, samplejournal, "expenses") `gives` True---  ]+tests_BalanceReport = tests "BalanceReport" [+  tests "balanceReport" $+    let+      (opts,journal) `gives` r = do+        let (eitems, etotal) = r+            (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal+            showw (acct,acct',indent,amt) = (acct, acct', indent, showMixedAmountDebug amt)+        (map showw eitems) `is` (map showw aitems)+        (showMixedAmountDebug etotal) `is` (showMixedAmountDebug atotal)+      usd0 = usd 0+    in [+  +     test "balanceReport with no args on null journal" $+     (defreportopts, nulljournal) `gives` ([], Mixed [nullamt])+  +    ,test "balanceReport with no args on sample journal" $+     (defreportopts, samplejournal) `gives`+      ([+        ("assets","assets",0, mamountp' "$0.00")+       ,("assets:bank","bank",1, mamountp' "$2.00")+       ,("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","income",0, mamountp' "$-2.00")+       ,("income:gifts","gifts",1, mamountp' "$-1.00")+       ,("income:salary","salary",1, mamountp' "$-1.00")+       ],+       Mixed [usd0])+  +    ,test "balanceReport with --depth=N" $+     (defreportopts{depth_=Just 1}, samplejournal) `gives`+      ([+       ("expenses",    "expenses",    0, mamountp'  "$2.00")+       ,("income",      "income",      0, mamountp' "$-2.00")+       ],+       Mixed [usd0])+  +    ,test "balanceReport with depth:N" $+     (defreportopts{query_="depth:1"}, samplejournal) `gives`+      ([+       ("expenses",    "expenses",    0, mamountp'  "$2.00")+       ,("income",      "income",      0, mamountp' "$-2.00")+       ],+       Mixed [usd0])+  +    ,tests "balanceReport with a date or secondary date span" [+     (defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives`+      ([],+       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")+       ],+       Mixed [usd0])+     ] -tests_Hledger_Reports_BalanceReport :: Test-tests_Hledger_Reports_BalanceReport = TestList-  tests_balanceReport+    ,test "balanceReport with desc:" $+     (defreportopts{query_="desc:income"}, samplejournal) `gives`+      ([+        ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")+       ,("income:salary","income:salary",0, mamountp' "$-1.00")+       ],+       Mixed [usd0])+  +    ,test "balanceReport with not:desc:" $+     (defreportopts{query_="not:desc:income"}, 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:gifts","income:gifts",0, mamountp' "$-1.00")+       ],+       Mixed [usd0])+  +    ,test "balanceReport with period on a populated period" $+      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}, samplejournal) `gives`+       (+        [+         ("assets:bank:checking","assets:bank:checking",0, mamountp' "$1.00")+        ,("income:salary","income:salary",0, mamountp' "$-1.00")+        ],+        Mixed [usd0])+  +     ,test "balanceReport with period on an unpopulated period" $+      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}, samplejournal) `gives`+       ([],Mixed [nullamt])+  +  +  +  {-+      ,test "accounts report with account pattern o" ~:+       defreportopts{patterns_=["o"]} `gives`+       ["                  $1  expenses:food"+       ,"                 $-2  income"+       ,"                 $-1    gifts"+       ,"                 $-1    salary"+       ,"--------------------"+       ,"                 $-1"+       ]+  +      ,test "accounts report with account pattern o and --depth 1" ~:+       defreportopts{patterns_=["o"],depth_=Just 1} `gives`+       ["                  $1  expenses"+       ,"                 $-2  income"+       ,"--------------------"+       ,"                 $-1"+       ]+  +      ,test "accounts report with account pattern a" ~:+       defreportopts{patterns_=["a"]} `gives`+       ["                 $-1  assets"+       ,"                  $1    bank:saving"+       ,"                 $-2    cash"+       ,"                 $-1  income:salary"+       ,"                  $1  liabilities:debts"+       ,"--------------------"+       ,"                 $-1"+       ]+  +      ,test "accounts report with account pattern e" ~:+       defreportopts{patterns_=["e"]} `gives`+       ["                 $-1  assets"+       ,"                  $1    bank:saving"+       ,"                 $-2    cash"+       ,"                  $2  expenses"+       ,"                  $1    food"+       ,"                  $1    supplies"+       ,"                 $-2  income"+       ,"                 $-1    gifts"+       ,"                 $-1    salary"+       ,"                  $1  liabilities:debts"+       ,"--------------------"+       ,"                   0"+       ]+  +      ,test "accounts report with unmatched parent of two matched subaccounts" ~:+       defreportopts{patterns_=["cash","saving"]} `gives`+       ["                 $-1  assets"+       ,"                  $1    bank:saving"+       ,"                 $-2    cash"+       ,"--------------------"+       ,"                 $-1"+       ]+  +      ,test "accounts report with multi-part account name" ~:+       defreportopts{patterns_=["expenses:food"]} `gives`+       ["                  $1  expenses:food"+       ,"--------------------"+       ,"                  $1"+       ]+  +      ,test "accounts report with negative account pattern" ~:+       defreportopts{patterns_=["not:assets"]} `gives`+       ["                  $2  expenses"+       ,"                  $1    food"+       ,"                  $1    supplies"+       ,"                 $-2  income"+       ,"                 $-1    gifts"+       ,"                 $-1    salary"+       ,"                  $1  liabilities:debts"+       ,"--------------------"+       ,"                  $1"+       ]+  +      ,test "accounts report negative account pattern always matches full name" ~:+       defreportopts{patterns_=["not:e"]} `gives`+       ["--------------------"+       ,"                   0"+       ]+  +      ,test "accounts report negative patterns affect totals" ~:+       defreportopts{patterns_=["expenses","not:food"]} `gives`+       ["                  $1  expenses:supplies"+       ,"--------------------"+       ,"                  $1"+       ]+  +      ,test "accounts report with -E shows zero-balance accounts" ~:+       defreportopts{patterns_=["assets"],empty_=True} `gives`+       ["                 $-1  assets"+       ,"                  $1    bank"+       ,"                   0      checking"+       ,"                  $1      saving"+       ,"                 $-2    cash"+       ,"--------------------"+       ,"                 $-1"+       ]+  +      ,test "accounts report with cost basis" $+         j <- (readJournal def Nothing $ unlines+                [""+                ,"2008/1/1 test           "+                ,"  a:b          10h @ $50"+                ,"  c:d                   "+                ]) >>= either error' return+         let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment+         balanceReportAsText defreportopts (balanceReport defreportopts Any j') `is`+           ["                $500  a:b"+           ,"               $-500  c:d"+           ,"--------------------"+           ,"                   0"+           ]+  -}+   ]++ ]+
Hledger/Reports/BudgetReport.hs view
@@ -17,7 +17,6 @@ import Data.Ord import Data.Time.Calendar import Safe-import Test.HUnit --import Data.List --import Data.Maybe import qualified Data.Map as Map@@ -26,8 +25,6 @@ --import qualified Data.Text.Lazy as TL --import System.Console.CmdArgs.Explicit as C --import Lucid as L---import Text.CSV---import Test.HUnit import Text.Printf (printf) import Text.Tabular as T --import Text.Tabular.AsciiWide@@ -38,12 +35,15 @@ --import Hledger.Read (mamountp') import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes+import Hledger.Reports.BalanceReport (sortAccountItemsLike) import Hledger.Reports.MultiBalanceReports  +-- for reference:+-- --type MultiBalanceReportRow    = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount) --type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)-+-- --type PeriodicReportRow a = --  ( AccountName  -- ^ A full account name. --  , [a]          -- ^ The data value for each subperiod.@@ -56,7 +56,9 @@ type BudgetAverage = Average  -- | A budget report tracks expected and actual changes per account and subperiod.-type BudgetReport = PeriodicReport (Maybe Change, Maybe BudgetGoal)+type BudgetCell = (Maybe Change, Maybe BudgetGoal)+type BudgetReport = PeriodicReport BudgetCell+type BudgetReportRow = PeriodicReportRow BudgetCell  -- | Calculate budget goals from all periodic transactions, -- actual balance changes from the regular transactions,@@ -82,9 +84,55 @@       -- it should be safe to replace it with the latter, so they combine well.        | interval_ ropts == NoInterval = MultiBalanceReport (actualspans, budgetgoalitems, budgetgoaltotals)       | otherwise = budgetgoalreport +    budgetreport = combineBudgetAndActual budgetgoalreport' actualreport+    sortedbudgetreport = sortBudgetReport ropts j budgetreport   in-    dbg1 "budgetreport" $ combineBudgetAndActual budgetgoalreport' actualreport+    dbg1 "sortedbudgetreport" sortedbudgetreport +-- | Sort a budget report's rows according to options.+sortBudgetReport :: ReportOpts -> Journal -> BudgetReport -> BudgetReport+sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, sortedrows, trow)+  where+    sortedrows +      | sort_amount_ ropts && tree_ ropts = sortTreeBURByActualAmount rows+      | sort_amount_ ropts                = sortFlatBURByActualAmount rows+      | otherwise                         = sortByAccountDeclaration rows++    -- Sort a tree-mode budget report's rows by total actual amount at each level.+    sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow] +    sortTreeBURByActualAmount rows = sortedrows+      where+        anamesandrows = [(first6 r, r) | r <- rows]+        anames = map fst anamesandrows+        atotals = [(a,tot) | (a,_,_,_,(tot,_),_) <- rows]+        accounttree = accountTree "root" anames+        accounttreewithbals = mapAccounts setibalance accounttree+          where+            setibalance a = a{aibalance=+              fromMaybe 0 $ -- when there's no actual amount, assume 0; will mess up with negative amounts ? TODO +              fromMaybe (error "sortTreeByAmount 1") $ -- should not happen, but it's ugly; TODO +              lookup (aname a) atotals+              }+        sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals+        sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree+        sortedrows = sortAccountItemsLike sortedanames anamesandrows ++    -- Sort a flat-mode budget report's rows by total actual amount.+    sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow] +    sortFlatBURByActualAmount = sortBy (maybeflip $ comparing (fst . fifth6))+      where+        maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip++    -- Sort the report rows by account declaration order then account name. +    -- <unbudgeted> remains at the top.+    sortByAccountDeclaration rows = sortedrows+      where+        (unbudgetedrow,rows') = partition ((=="<unbudgeted>").first6) rows+        anamesandrows = [(first6 r, r) | r <- rows']+        anames = map fst anamesandrows+        sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames+        sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows + -- | Use all periodic transactions in the journal to generate  -- budget transactions in the specified report period. -- Budget transactions are similar to forecast transactions except@@ -181,69 +229,11 @@         acctsdone = map first6 rows1      -- combine and re-sort rows-    -- TODO: respect hierarchy in tree mode+    -- TODO: use MBR code     -- TODO: respect --sort-amount     -- TODO: add --sort-budget to sort by budget goal amount     rows :: [PeriodicReportRow (Maybe Change, Maybe BudgetGoal)] =       sortBy (comparing first6) $ rows1 ++ rows2--- massive duplication from multiBalanceReport to handle tree mode sorting ?---      dbg1 "sorteditems" $---      sortitems items---      where---        sortitems---          | sort_amount_ opts && accountlistmode_ opts == ALTree       = sortTreeMultiBalanceReportRowsByAmount---          | sort_amount_ opts                                          = sortFlatMultiBalanceReportRowsByAmount---          | not (sort_amount_ opts) && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAccountCodeAndName---          | otherwise                                                  = sortFlatMultiBalanceReportRowsByAccountCodeAndName---          where---            -- Sort the report rows, representing a flat account list, by row total.---            sortFlatMultiBalanceReportRowsByAmount = sortBy (maybeflip $ comparing fifth6)---              where---                maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip------            -- Sort the report rows, representing a tree of accounts, by row total at each level.---            -- To do this we recreate an Account tree with the row totals as balances,---            -- so we can do a hierarchical sort, flatten again, and then reorder the---            -- report rows similarly. Yes this is pretty long winded.---            sortTreeMultiBalanceReportRowsByAmount rows = sortedrows---              where---                anamesandrows = [(first6 r, r) | r <- rows]---                anames = map fst anamesandrows---                atotals = [(a,tot) | (a,_,_,_,tot,_) <- rows]---                nametree = treeFromPaths $ map expandAccountName anames---                accounttree = nameTreeToAccount "root" nametree---                accounttreewithbals = mapAccounts setibalance accounttree---                  where---                    -- this error should not happen, but it's ugly TODO---                    setibalance a = a{aibalance=fromMaybe (error "sortTreeMultiBalanceReportRowsByAmount 1") $ lookup (aname a) atotals}---                sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals---                sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree---                -- dropped the root account, also ignore any parent accounts not in rows---                sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts------            -- Sort the report rows by account code if any, with the empty account code coming last, then account name.---            sortFlatMultiBalanceReportRowsByAccountCodeAndName = sortBy (comparing acodeandname)---              where---                acodeandname r = (acode', aname)---                  where---                    aname = first6 r---                    macode = fromMaybe Nothing $ lookup aname $ jaccounts j---                    acode' = fromMaybe maxBound macode------            -- Sort the report rows, representing a tree of accounts, by account code and then account name at each level.---            -- Convert a tree of account names, look up the account codes, sort and flatten the tree, reorder the rows.---            sortTreeMultiBalanceReportRowsByAccountCodeAndName rows = sortedrows---              where---                anamesandrows = [(first6 r, r) | r <- rows]---                anames = map fst anamesandrows---                nametree = treeFromPaths $ map expandAccountName anames---                accounttree = nameTreeToAccount "root" nametree---                accounttreewithcodes = mapAccounts (accountSetCodeFrom j) accounttree---                sortedaccounttree = sortAccountTreeByAccountCodeAndName accounttreewithcodes---                sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree---                -- dropped the root account, also ignore any parent accounts not in rows---                sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts---      -- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells     totalrow =@@ -356,6 +346,7 @@ maybeAccountNameDrop opts a | flat_ opts = accountNameDrop (drop_ opts) a                             | otherwise  = a -tests_Hledger_Reports_BudgetReport :: Test-tests_Hledger_Reports_BudgetReport = TestList [-  ]+-- tests++tests_BudgetReport = tests "BudgetReport" [+ ]
Hledger/Reports/EntriesReport.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveDataTypeable, FlexibleInstances #-} {-|  Journal entries report, used by the print command.@@ -10,17 +10,17 @@   EntriesReportItem,   entriesReport,   -- * Tests-  tests_Hledger_Reports_EntriesReport+  tests_EntriesReport ) where  import Data.List import Data.Ord-import Test.HUnit  import Hledger.Data import Hledger.Query import Hledger.Reports.ReportOptions+import Hledger.Utils    -- | A journal entries report is a list of whole transactions as@@ -37,15 +37,10 @@       date = transactionDateFn opts       ts = jtxns $ journalSelectingAmountFromOpts opts j -tests_entriesReport :: [Test]-tests_entriesReport = [-  "entriesReport" ~: do-    assertEqual "not acct" 1 (length $ entriesReport defreportopts (Not $ Acct "bank") samplejournal)-    let sp = mkdatespan "2008/06/01" "2008/07/01"-    assertEqual "date" 3 (length $ entriesReport defreportopts (Date sp) samplejournal)+tests_EntriesReport = tests "EntriesReport" [+  tests "entriesReport" [+     test "not acct" $ (length $ entriesReport defreportopts (Not $ Acct "bank") samplejournal) `is` 1+    ,test "date" $ (length $ entriesReport defreportopts (Date $ mkdatespan "2008/06/01" "2008/07/01") samplejournal) `is` 3+  ]  ]--tests_Hledger_Reports_EntriesReport :: Test-tests_Hledger_Reports_EntriesReport = TestList $- tests_entriesReport 
Hledger/Reports/MultiBalanceReports.hs view
@@ -16,7 +16,7 @@   tableAsText,    -- -- * Tests-  tests_Hledger_Reports_MultiBalanceReport+  tests_MultiBalanceReports ) where @@ -25,13 +25,12 @@ import Data.Ord import Data.Time.Calendar import Safe-import Test.HUnit import Text.Tabular as T import Text.Tabular.AsciiWide  import Hledger.Data import Hledger.Query-import Hledger.Utils+import Hledger.Utils  import Hledger.Read (mamountp') import Hledger.Reports.ReportOptions import Hledger.Reports.BalanceReport@@ -69,11 +68,11 @@ type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)  instance Show MultiBalanceReport where-    -- use ppShow to break long lists onto multiple lines-    -- we add some bogus extra shows here to help ppShow parse the output+    -- use pshow (pretty-show's ppShow) to break long lists onto multiple lines+    -- we add some bogus extra shows here to help it parse the output     -- and wrap tuples and lists properly     show (MultiBalanceReport (spans, items, totals)) =-        "MultiBalanceReport (ignore extra quotes):\n" ++ ppShow (show spans, map show items, totals)+        "MultiBalanceReport (ignore extra quotes):\n" ++ pshow (show spans, map show items, totals)  -- type alias just to remind us which AccountNames might be depth-clipped, below. type ClippedAccountName = AccountName@@ -176,64 +175,45 @@            , empty_ opts || depth == 0 || any (not . isZeroMixedAmount) displayedBals            ] +      -- TODO TBD: is it always ok to sort report rows after report has been generated ?+      -- Or does sorting sometimes need to be done as part of the report generation ?         sorteditems :: [MultiBalanceReportRow] =         dbg1 "sorteditems" $         sortitems items         where           sortitems-            | sort_amount_ opts && accountlistmode_ opts == ALTree       = sortTreeMultiBalanceReportRowsByAmount-            | sort_amount_ opts                                          = sortFlatMultiBalanceReportRowsByAmount-            | not (sort_amount_ opts) && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAccountCodeAndName-            | otherwise                                                  = sortFlatMultiBalanceReportRowsByAccountCodeAndName+            | sort_amount_ opts && accountlistmode_ opts == ALTree       = sortTreeMBRByAmount+            | sort_amount_ opts                                          = sortFlatMBRByAmount+            | otherwise                                                  = sortMBRByAccountDeclaration             where-              -- Sort the report rows, representing a flat account list, by row total. -              sortFlatMultiBalanceReportRowsByAmount = sortBy (maybeflip $ comparing fifth6)-                where-                  maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip-               -- Sort the report rows, representing a tree of accounts, by row total at each level.-              -- To do this we recreate an Account tree with the row totals as balances, -              -- so we can do a hierarchical sort, flatten again, and then reorder the  -              -- report rows similarly. Yes this is pretty long winded. -              sortTreeMultiBalanceReportRowsByAmount rows = sortedrows+              -- Similar to sortMBRByAccountDeclaration/sortAccountNamesByDeclaration.+              sortTreeMBRByAmount rows = sortedrows                 where                   anamesandrows = [(first6 r, r) | r <- rows]                   anames = map fst anamesandrows                   atotals = [(a,tot) | (a,_,_,_,tot,_) <- rows]-                  nametree = treeFromPaths $ map expandAccountName anames-                  accounttree = nameTreeToAccount "root" nametree+                  accounttree = accountTree "root" anames                   accounttreewithbals = mapAccounts setibalance accounttree                     where-                      -- this error should not happen, but it's ugly TODO -                      setibalance a = a{aibalance=fromMaybe (error "sortTreeMultiBalanceReportRowsByAmount 1") $ lookup (aname a) atotals}+                      -- should not happen, but it's dangerous; TODO +                      setibalance a = a{aibalance=fromMaybe (error "sortTreeMBRByAmount 1") $ lookup (aname a) atotals}                   sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals-                  sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree-                  -- dropped the root account, also ignore any parent accounts not in rows-                  sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts +                  sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree+                  sortedrows = sortAccountItemsLike sortedanames anamesandrows  -              -- Sort the report rows by account code if any, with the empty account code coming last, then account name. -              -- TODO keep children below their parent. Have to convert to tree ? -              sortFlatMultiBalanceReportRowsByAccountCodeAndName = sortBy (comparing acodeandname)+              -- Sort the report rows, representing a flat account list, by row total. +              sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fifth6))                 where-                  acodeandname r = (acode', aname)-                    where-                      aname = first6 r-                      macode = fromMaybe Nothing $ lookup aname $ jaccounts j-                      acode' = fromMaybe maxBound macode +                  maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip -              -- Sort the report rows, representing a tree of accounts, by account code and then account name at each level.-              -- Convert a tree of account names, look up the account codes, sort and flatten the tree, reorder the rows.-              sortTreeMultiBalanceReportRowsByAccountCodeAndName rows = sortedrows-                where+              -- Sort the report rows by account declaration order then account name. +              sortMBRByAccountDeclaration rows = sortedrows+                where                    anamesandrows = [(first6 r, r) | r <- rows]                   anames = map fst anamesandrows-                  nametree = treeFromPaths $ map expandAccountName anames-                  accounttree = nameTreeToAccount "root" nametree-                  accounttreewithcodes = mapAccounts (accountSetCodeFrom j) accounttree-                  sortedaccounttree = sortAccountTreeByAccountCodeAndName accounttreewithcodes-                  sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree-                  -- dropped the root account, also ignore any parent accounts not in rows-                  sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts +                  sortedanames = sortAccountNamesByDeclaration j (tree_ opts) anames+                  sortedrows = sortAccountItemsLike sortedanames anamesandrows         totals :: [MixedAmount] =           -- dbg1 "totals" $@@ -285,53 +265,6 @@     total = headDef nullmixedamt totals  -tests_multiBalanceReport =-  let-    (opts,journal) `gives` r = do-      let (eitems, etotal) = r-          (MultiBalanceReport (_, aitems, atotal)) = multiBalanceReport opts (queryFromOpts nulldate opts) journal-          showw (acct,acct',indent,lAmt,amt,amt') = (acct, acct', indent, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')-      assertEqual "items" (map showw eitems) (map showw aitems)-      assertEqual "total" (showMixedAmountDebug etotal) ((\(_, b, _) -> showMixedAmountDebug b) atotal) -- we only check the sum of the totals-    usd0 = usd 0-    amount0 = Amount {acommodity="$", aquantity=0, aprice=NoPrice, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}, amultiplier=False}-  in [-   "multiBalanceReport with no args on null journal" ~: do-   (defreportopts, nulljournal) `gives` ([], Mixed [nullamt])--   ,"multiBalanceReport with -H on a populated period" ~: do-    (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`-     (-      [-       ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])-      ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])-      ],-      Mixed [usd0])--   ,"multiBalanceReport tests the ability to have a valid history on an empty period" ~: do-    (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3), balancetype_=HistoricalBalance}, samplejournal) `gives`-     (-      [-       ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])-      ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])-      ],-      Mixed [usd0])--   ,"multiBalanceReport tests the ability to have a valid history on an empty period (More complex)" ~: do-    (defreportopts{period_= PeriodBetween (fromGregorian 2009 1 1) (fromGregorian 2009 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`-     (-      [-      ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])-      ,("assets:bank:saving","saving",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])-      ,("assets:cash","cash",2, [mamountp' "$-2.00"], mamountp' "$-2.00",Mixed [amount0 {aquantity=(-2)}])-      ,("expenses:food","food",2, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=(1)}])-      ,("expenses:supplies","supplies",2, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=(1)}])-      ,("income:gifts","gifts",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])-      ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])-      ],-      Mixed [usd0])-  ]- -- common rendering helper, XXX here for now  tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String@@ -348,6 +281,53 @@         acctswidth = maximum' $ map strWidth (headerContents l)         l'         = padRightWide acctswidth <$> l -tests_Hledger_Reports_MultiBalanceReport :: Test-tests_Hledger_Reports_MultiBalanceReport = TestList-  tests_multiBalanceReport+-- tests++tests_MultiBalanceReports = tests "MultiBalanceReports" [+  let+    (opts,journal) `gives` r = do+      let (eitems, etotal) = r+          (MultiBalanceReport (_, aitems, atotal)) = multiBalanceReport opts (queryFromOpts nulldate opts) journal+          showw (acct,acct',indent,lAmt,amt,amt') = (acct, acct', indent, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')+      (map showw aitems) `is` (map showw eitems)+      ((\(_, b, _) -> showMixedAmountDebug b) atotal) `is` (showMixedAmountDebug etotal) -- we only check the sum of the totals+    usd0 = usd 0+    amount0 = Amount {acommodity="$", aquantity=0, aprice=NoPrice, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}, amultiplier=False}+  in +   tests "multiBalanceReport" [+      test "null journal"  $+      (defreportopts, nulljournal) `gives` ([], Mixed [nullamt])+  +     ,test "with -H on a populated period"  $+      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`+       (+        [+         ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])+        ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])+        ],+        Mixed [usd0])+  +     ,test "a valid history on an empty period"  $+      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3), balancetype_=HistoricalBalance}, samplejournal) `gives`+       (+        [+         ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])+        ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])+        ],+        Mixed [usd0])+  +     ,test "a valid history on an empty period (more complex)"  $+      (defreportopts{period_= PeriodBetween (fromGregorian 2009 1 1) (fromGregorian 2009 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`+       (+        [+        ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])+        ,("assets:bank:saving","saving",3, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=1}])+        ,("assets:cash","cash",2, [mamountp' "$-2.00"], mamountp' "$-2.00",Mixed [amount0 {aquantity=(-2)}])+        ,("expenses:food","food",2, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=(1)}])+        ,("expenses:supplies","supplies",2, [mamountp' "$1.00"], mamountp' "$1.00",Mixed [amount0 {aquantity=(1)}])+        ,("income:gifts","gifts",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])+        ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])+        ],+        Mixed [usd0])+    ]+ ]
Hledger/Reports/PostingsReport.hs view
@@ -12,7 +12,7 @@   mkpostingsReportItem,    -- * Tests-  tests_Hledger_Reports_PostingsReport+  tests_PostingsReport ) where @@ -23,11 +23,10 @@ import qualified Data.Text as T import Data.Time.Calendar import Safe (headMay, lastMay)-import Test.HUnit  import Hledger.Data import Hledger.Query-import Hledger.Utils+import Hledger.Utils  import Hledger.Reports.ReportOptions  @@ -175,11 +174,6 @@       summarisespan s = summarisePostingsInDateSpan s wd depth showempty (postingsinspan s)       postingsinspan s = filter (isPostingInDateSpan' wd s) ps -tests_summarisePostingsByInterval = [-  "summarisePostingsByInterval" ~: do-    summarisePostingsByInterval (Quarters 1) PrimaryDate 99999 False (DateSpan Nothing Nothing) [] ~?= []- ]- -- | A summary posting summarises the activity in one account within a report -- interval. It is currently kludgily represented by a regular Posting with no -- description, the interval's start date stored as the posting date, and the@@ -221,210 +215,214 @@           bal = if isclipped a then aibalance else aebalance           isclipped a = accountNameLevel a >= depth --- tests_summarisePostingsInDateSpan = [-  --  "summarisePostingsInDateSpan" ~: do-  --   let gives (b,e,depth,showempty,ps) =-  --           (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)-  --   let ps =-  --           [-  --            nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]}-  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 2]}-  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food",          lpamount=Mixed [usd 4]}-  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 8]}-  --           ]-  --   ("2008/01/01","2009/01/01",0,9999,False,[]) `gives`-  --    []-  --   ("2008/01/01","2009/01/01",0,9999,True,[]) `gives`-  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"}-  --    ]-  --   ("2008/01/01","2009/01/01",0,9999,False,ts) `gives`-  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",          lpamount=Mixed [usd 4]}-  --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 10]}-  --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]}-  --    ]-  --   ("2008/01/01","2009/01/01",0,2,False,ts) `gives`-  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [usd 15]}-  --    ]-  --   ("2008/01/01","2009/01/01",0,1,False,ts) `gives`-  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [usd 15]}-  --    ]-  --   ("2008/01/01","2009/01/01",0,0,False,ts) `gives`-  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [usd 15]}-  --    ]--tests_postingsReport = [-  "postingsReport" ~: do--   -- with the query specified explicitly-   let (query, journal) `gives` n = (length $ snd $ postingsReport defreportopts query journal) `is` n-   (Any, nulljournal) `gives` 0-   (Any, samplejournal) `gives` 13-   -- register --depth just clips account names-   (Depth 2, samplejournal) `gives` 13-   (And [Depth 1, StatusQ Cleared, Acct "expenses"], samplejournal) `gives` 2-   (And [And [Depth 1, StatusQ Cleared], Acct "expenses"], samplejournal) `gives` 2--   -- with query and/or command-line options-   assertEqual "" 13 (length $ snd $ postingsReport defreportopts Any samplejournal)-   assertEqual "" 11 (length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal)-   assertEqual "" 20 (length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal)-   assertEqual ""  5 (length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal)--   -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0-   -- [(Just (parsedate "2008-01-01","income"),assets:bank:checking             $1,$1)-   -- ,(Nothing,income:salary                   $-1,0)-   -- ,(Just (2008-06-01,"gift"),assets:bank:checking             $1,$1)-   -- ,(Nothing,income:gifts                    $-1,0)-   -- ,(Just (2008-06-02,"save"),assets:bank:saving               $1,$1)-   -- ,(Nothing,assets:bank:checking            $-1,0)-   -- ,(Just (2008-06-03,"eat & shop"),expenses:food                    $1,$1)-   -- ,(Nothing,expenses:supplies                $1,$2)-   -- ,(Nothing,assets:cash                     $-2,0)-   -- ,(Just (2008-12-31,"pay off"),liabilities:debts                $1,$1)-   -- ,(Nothing,assets:bank:checking            $-1,0)-   -- ]--{--    let opts = defreportopts-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/01/01 income               assets:bank:checking             $1           $1"-     ,"                                income:salary                   $-1            0"-     ,"2008/06/01 gift                 assets:bank:checking             $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"2008/06/02 save                 assets:bank:saving               $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ,"2008/06/03 eat & shop           expenses:food                    $1           $1"-     ,"                                expenses:supplies                $1           $2"-     ,"                                assets:cash                     $-2            0"-     ,"2008/12/31 pay off              liabilities:debts                $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ]--  ,"postings report with cleared option" ~:-   do-    let opts = defreportopts{cleared_=True}-    j <- readJournal' sample_journal_str-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/06/03 eat & shop           expenses:food                    $1           $1"-     ,"                                expenses:supplies                $1           $2"-     ,"                                assets:cash                     $-2            0"-     ,"2008/12/31 pay off              liabilities:debts                $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ]--  ,"postings report with uncleared option" ~:-   do-    let opts = defreportopts{uncleared_=True}-    j <- readJournal' sample_journal_str-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/01/01 income               assets:bank:checking             $1           $1"-     ,"                                income:salary                   $-1            0"-     ,"2008/06/01 gift                 assets:bank:checking             $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"2008/06/02 save                 assets:bank:saving               $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ]--  ,"postings report sorts by date" ~:-   do-    j <- readJournal' $ unlines-        ["2008/02/02 a"-        ,"  b  1"-        ,"  c"-        ,""-        ,"2008/01/01 d"-        ,"  e  1"-        ,"  f"-        ]-    let opts = defreportopts-    registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/02/02"]--  ,"postings report with account pattern" ~:-   do-    j <- samplejournal-    let opts = defreportopts{patterns_=["cash"]}-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"-     ]--  ,"postings report with account pattern, case insensitive" ~:-   do-    j <- samplejournal-    let opts = defreportopts{patterns_=["cAsH"]}-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"-     ]--  ,"postings report with display expression" ~:-   do-    j <- samplejournal-    let gives displayexpr =-            (registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is`)-                where opts = defreportopts{display_=Just displayexpr}-    "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"]-    "d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"]-    "d=[2008/6/2]"  `gives` ["2008/06/02"]-    "d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]-    "d>[2008/6/2]"  `gives` ["2008/06/03","2008/12/31"]+-- tests -  ,"postings report with period expression" ~:-   do-    j <- samplejournal-    let periodexpr `gives` dates = do-          j' <- samplejournal-          registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j') `is` dates-              where opts = defreportopts{period_=maybePeriod date1 periodexpr}-    ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]-    "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]-    "2007" `gives` []-    "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]-    "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]-    "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]-    let opts = defreportopts{period_=maybePeriod date1 "yearly"}-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1"-     ,"                                assets:cash                     $-2          $-1"-     ,"                                expenses:food                    $1            0"-     ,"                                expenses:supplies                $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"                                income:salary                   $-1          $-1"-     ,"                                liabilities:debts                $1            0"-     ]-    let opts = defreportopts{period_=maybePeriod date1 "quarterly"}-    registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/10/01"]-    let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True}-    registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]+tests_PostingsReport = tests "PostingsReport" [ -  ]+   tests "postingsReport" $+    let (query, journal) `gives` n = (length $ snd $ postingsReport defreportopts query journal) `is` n+    in [+     -- with the query specified explicitly+      (Any, nulljournal) `gives` 0+     ,(Any, samplejournal) `gives` 13+     -- register --depth just clips account names+     ,(Depth 2, samplejournal) `gives` 13+     ,(And [Depth 1, StatusQ Cleared, Acct "expenses"], samplejournal) `gives` 2+     ,(And [And [Depth 1, StatusQ Cleared], Acct "expenses"], samplejournal) `gives` 2+  +     -- with query and/or command-line options+     ,(length $ snd $ postingsReport defreportopts Any samplejournal) `is` 13+     ,(length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal) `is` 11+     ,(length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal) `is` 20+     ,(length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal) `is` 5+  +     -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0+     -- [(Just (parsedate "2008-01-01","income"),assets:bank:checking             $1,$1)+     -- ,(Nothing,income:salary                   $-1,0)+     -- ,(Just (2008-06-01,"gift"),assets:bank:checking             $1,$1)+     -- ,(Nothing,income:gifts                    $-1,0)+     -- ,(Just (2008-06-02,"save"),assets:bank:saving               $1,$1)+     -- ,(Nothing,assets:bank:checking            $-1,0)+     -- ,(Just (2008-06-03,"eat & shop"),expenses:food                    $1,$1)+     -- ,(Nothing,expenses:supplies                $1,$2)+     -- ,(Nothing,assets:cash                     $-2,0)+     -- ,(Just (2008-12-31,"pay off"),liabilities:debts                $1,$1)+     -- ,(Nothing,assets:bank:checking            $-1,0)     -  , "postings report with depth arg" ~:-   do-    j <- samplejournal-    let opts = defreportopts{depth_=Just 2}-    (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines-     ["2008/01/01 income               assets:bank                      $1           $1"-     ,"                                income:salary                   $-1            0"-     ,"2008/06/01 gift                 assets:bank                      $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"2008/06/02 save                 assets:bank                      $1           $1"-     ,"                                assets:bank                     $-1            0"-     ,"2008/06/03 eat & shop           expenses:food                    $1           $1"-     ,"                                expenses:supplies                $1           $2"-     ,"                                assets:cash                     $-2            0"-     ,"2008/12/31 pay off              liabilities:debts                $1           $1"-     ,"                                assets:bank                     $-1            0"-     ]+    {-+        let opts = defreportopts+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/01/01 income               assets:bank:checking             $1           $1"+         ,"                                income:salary                   $-1            0"+         ,"2008/06/01 gift                 assets:bank:checking             $1           $1"+         ,"                                income:gifts                    $-1            0"+         ,"2008/06/02 save                 assets:bank:saving               $1           $1"+         ,"                                assets:bank:checking            $-1            0"+         ,"2008/06/03 eat & shop           expenses:food                    $1           $1"+         ,"                                expenses:supplies                $1           $2"+         ,"                                assets:cash                     $-2            0"+         ,"2008/12/31 pay off              liabilities:debts                $1           $1"+         ,"                                assets:bank:checking            $-1            0"+         ]+    +      ,"postings report with cleared option" ~:+       do+        let opts = defreportopts{cleared_=True}+        j <- readJournal' sample_journal_str+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/06/03 eat & shop           expenses:food                    $1           $1"+         ,"                                expenses:supplies                $1           $2"+         ,"                                assets:cash                     $-2            0"+         ,"2008/12/31 pay off              liabilities:debts                $1           $1"+         ,"                                assets:bank:checking            $-1            0"+         ]+    +      ,"postings report with uncleared option" ~:+       do+        let opts = defreportopts{uncleared_=True}+        j <- readJournal' sample_journal_str+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/01/01 income               assets:bank:checking             $1           $1"+         ,"                                income:salary                   $-1            0"+         ,"2008/06/01 gift                 assets:bank:checking             $1           $1"+         ,"                                income:gifts                    $-1            0"+         ,"2008/06/02 save                 assets:bank:saving               $1           $1"+         ,"                                assets:bank:checking            $-1            0"+         ]+    +      ,"postings report sorts by date" ~:+       do+        j <- readJournal' $ unlines+            ["2008/02/02 a"+            ,"  b  1"+            ,"  c"+            ,""+            ,"2008/01/01 d"+            ,"  e  1"+            ,"  f"+            ]+        let opts = defreportopts+        registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/02/02"]+    +      ,"postings report with account pattern" ~:+       do+        j <- samplejournal+        let opts = defreportopts{patterns_=["cash"]}+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"+         ]+    +      ,"postings report with account pattern, case insensitive" ~:+       do+        j <- samplejournal+        let opts = defreportopts{patterns_=["cAsH"]}+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"+         ]+    +      ,"postings report with display expression" ~:+       do+        j <- samplejournal+        let gives displayexpr =+                (registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is`)+                    where opts = defreportopts{display_=Just displayexpr}+        "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"]+        "d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"]+        "d=[2008/6/2]"  `gives` ["2008/06/02"]+        "d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]+        "d>[2008/6/2]"  `gives` ["2008/06/03","2008/12/31"]+    +      ,"postings report with period expression" ~:+       do+        j <- samplejournal+        let periodexpr `gives` dates = do+              j' <- samplejournal+              registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j') `is` dates+                  where opts = defreportopts{period_=maybePeriod date1 periodexpr}+        ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]+        "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]+        "2007" `gives` []+        "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]+        "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]+        "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]+        let opts = defreportopts{period_=maybePeriod date1 "yearly"}+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1"+         ,"                                assets:cash                     $-2          $-1"+         ,"                                expenses:food                    $1            0"+         ,"                                expenses:supplies                $1           $1"+         ,"                                income:gifts                    $-1            0"+         ,"                                income:salary                   $-1          $-1"+         ,"                                liabilities:debts                $1            0"+         ]+        let opts = defreportopts{period_=maybePeriod date1 "quarterly"}+        registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/10/01"]+        let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True}+        registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]+    +      ]+    +      , "postings report with depth arg" ~:+       do+        j <- samplejournal+        let opts = defreportopts{depth_=Just 2}+        (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines+         ["2008/01/01 income               assets:bank                      $1           $1"+         ,"                                income:salary                   $-1            0"+         ,"2008/06/01 gift                 assets:bank                      $1           $1"+         ,"                                income:gifts                    $-1            0"+         ,"2008/06/02 save                 assets:bank                      $1           $1"+         ,"                                assets:bank                     $-1            0"+         ,"2008/06/03 eat & shop           expenses:food                    $1           $1"+         ,"                                expenses:supplies                $1           $2"+         ,"                                assets:cash                     $-2            0"+         ,"2008/12/31 pay off              liabilities:debts                $1           $1"+         ,"                                assets:bank                     $-1            0"+         ]+    +    -}+    ] --}+  ,tests "summarisePostingsByInterval" [+    tests "summarisePostingsByInterval" [+      summarisePostingsByInterval (Quarters 1) PrimaryDate 99999 False (DateSpan Nothing Nothing) [] `is` []+      ]+   ]+  +  -- ,tests_summarisePostingsInDateSpan = [+    --  "summarisePostingsInDateSpan" ~: do+    --   let gives (b,e,depth,showempty,ps) =+    --           (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)+    --   let ps =+    --           [+    --            nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]}+    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 2]}+    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food",          lpamount=Mixed [usd 4]}+    --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 8]}+    --           ]+    --   ("2008/01/01","2009/01/01",0,9999,False,[]) `gives`+    --    []+    --   ("2008/01/01","2009/01/01",0,9999,True,[]) `gives`+    --    [+    --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"}+    --    ]+    --   ("2008/01/01","2009/01/01",0,9999,False,ts) `gives`+    --    [+    --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",          lpamount=Mixed [usd 4]}+    --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining",   lpamount=Mixed [usd 10]}+    --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]}+    --    ]+    --   ("2008/01/01","2009/01/01",0,2,False,ts) `gives`+    --    [+    --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [usd 15]}+    --    ]+    --   ("2008/01/01","2009/01/01",0,1,False,ts) `gives`+    --    [+    --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [usd 15]}+    --    ]+    --   ("2008/01/01","2009/01/01",0,0,False,ts) `gives`+    --    [+    --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [usd 15]}+    --    ]+    ]--tests_Hledger_Reports_PostingsReport :: Test-tests_Hledger_Reports_PostingsReport = TestList $-    tests_summarisePostingsByInterval- ++ tests_postingsReport-
Hledger/Reports/ReportOptions.hs view
@@ -1,10 +1,11 @@-{-# LANGUAGE CPP, RecordWildCards, DeriveDataTypeable #-} {-|  Options common to most hledger reports.  -} +{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveDataTypeable #-}+ module Hledger.Reports.ReportOptions (   ReportOpts(..),   BalanceType(..),@@ -32,7 +33,7 @@   specifiedStartDate,   specifiedEndDate, -  tests_Hledger_Reports_ReportOptions+  tests_ReportOptions ) where @@ -47,7 +48,6 @@ import Safe import System.Console.ANSI (hSupportsANSI) import System.IO (stdout)-import Test.HUnit import Text.Megaparsec.Error  import Hledger.Data@@ -372,22 +372,6 @@               ++ [Or $ map StatusQ statuses_]               ++ (maybe [] ((:[]) . Depth) depth_) -tests_queryFromOpts :: [Test]-tests_queryFromOpts = [- "queryFromOpts" ~: do-  assertEqual "" Any (queryFromOpts nulldate defreportopts)-  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{period_=PeriodFrom (parsedate "2012/01/01")-                                                      ,query_="date:'to 2013'"-                                                      })-  assertEqual "" (Date2 $ mkdatespan "2012/01/01" "2013/01/01")-                 (queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"})-  assertEqual "" (Or [Acct "a a", Acct "'b"])-                 (queryFromOpts nulldate defreportopts{query_="'a a' 'b"})- ]- -- | Convert report options and arguments to query options. queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt] queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts@@ -395,16 +379,6 @@     flagsqopts = []     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{period_=PeriodFrom (parsedate "2012/01/01")-                                                             ,query_="date:'to 2013'"-                                                             })- ]- -- | The effective report span is the start and end dates specified by -- options or queries, or otherwise the earliest and latest transaction or  -- posting dates in the journal. If no dates are specified by options/queries@@ -444,8 +418,26 @@ specifiedEndDate :: ReportOpts -> IO (Maybe Day) specifiedEndDate ropts = snd <$> specifiedStartEndDates ropts +-- tests -tests_Hledger_Reports_ReportOptions :: Test-tests_Hledger_Reports_ReportOptions = TestList $-    tests_queryFromOpts- ++ tests_queryOptsFromOpts+tests_ReportOptions = tests "ReportOptions" [+   tests "queryFromOpts" [+      (queryFromOpts nulldate defreportopts) `is` Any+     ,(queryFromOpts nulldate defreportopts{query_="a"}) `is` (Acct "a")+     ,(queryFromOpts nulldate defreportopts{query_="desc:'a a'"}) `is` (Desc "a a")+     ,(queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01"),query_="date:'to 2013'" }) +      `is` (Date $ mkdatespan "2012/01/01" "2013/01/01")+     ,(queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"}) `is` (Date2 $ mkdatespan "2012/01/01" "2013/01/01")+     ,(queryFromOpts nulldate defreportopts{query_="'a a' 'b"}) `is` (Or [Acct "a a", Acct "'b"])+     ]++  ,tests "queryOptsFromOpts" [+      (queryOptsFromOpts nulldate defreportopts) `is` []+     ,(queryOptsFromOpts nulldate defreportopts{query_="a"}) `is` []+     ,(queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01")+                                                                   ,query_="date:'to 2013'"+                                                                   })+      `is` []+    ]+ ]+
Hledger/Reports/TransactionsReports.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveDataTypeable, FlexibleInstances #-} {-|  Here are several variants of a transactions report.@@ -22,10 +22,8 @@   journalTransactionsReport,   accountTransactionsReport,   transactionsReportByCommodity,-  transactionRegisterDate--  -- -- * Tests-  -- tests_Hledger_Reports_TransactionsReports+  transactionRegisterDate,+  tests_TransactionsReports ) where @@ -34,12 +32,11 @@ -- 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+import Hledger.Utils   -- | A transactions report includes a list of transactions@@ -48,7 +45,7 @@ -- indicating multiple other accounts and a display string describing -- them) with or without a notion of current account(s). -- Two kinds of report use this data structure, see journalTransactionsReport--- and accountTransactionsReport below for detais.+-- and accountTransactionsReport below for details. type TransactionsReport = (String                   -- label for the balance column, eg "balance" or "total"                           ,[TransactionsReportItem] -- line items, one per transaction                           )@@ -279,3 +276,7 @@  ------------------------------------------------------------------------------- +-- tests++tests_TransactionsReports = tests "TransactionsReports" [+ ]
Hledger/Utils.hs view
@@ -4,6 +4,7 @@ in the module hierarchy. This is the bottom of hledger's module graph.  -}+{-# LANGUAGE OverloadedStrings #-}  module Hledger.Utils (---- provide these frequently used modules - or not, for clearer api:                           -- module Control.Monad,@@ -14,7 +15,6 @@                           -- module Data.Time.LocalTime,                           -- module Data.Tree,                           -- module Text.RegexPR,-                          -- module Test.HUnit,                           -- module Text.Printf,                           ---- all of this one:                           module Hledger.Utils,@@ -33,7 +33,6 @@                           -- the rest need to be done in each module I think                           ) where-import Test.HUnit  import Control.Monad (liftM, when) -- import Data.Char@@ -129,9 +128,12 @@ isRight :: Either a b -> Bool isRight = not . isLeft --- | Apply a function the specified number of times. Possibly uses O(n) stack ?+-- | Apply a function the specified number of times,+-- which should be > 0 (otherwise does nothing).+-- Possibly uses O(n) stack ? applyN :: Int -> (a -> a) -> a -> a-applyN n f = (!! n) . iterate f+applyN n f | n < 1     = id+           | otherwise = (!! n) . iterate f -- from protolude, compare -- applyN :: Int -> (a -> a) -> a -> a -- applyN n f = X.foldr (.) identity (X.replicate n f)@@ -215,7 +217,6 @@ mapM' :: Monad f => (a -> f b) -> [a] -> f [b] mapM' f = sequence' . map f -tests_Hledger_Utils :: Test-tests_Hledger_Utils = TestList [-    tests_Hledger_Utils_Text-    ]+tests_Utils = tests "Utils" [+  tests_Text+  ]
Hledger/Utils/Debug.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP, FlexibleContexts, TypeFamilies #-}+{-# LANGUAGE FlexibleContexts, TypeFamilies #-} -- | Debugging helpers  -- more:@@ -8,9 +8,39 @@ -- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html  module Hledger.Utils.Debug (-  module Hledger.Utils.Debug+   pprint+  ,pshow+  ,ptrace+  ,traceWith+  ,debugLevel+  ,ptraceAt+  ,dbg0+  ,dbgExit+  ,dbg1+  ,dbg2+  ,dbg3+  ,dbg4+  ,dbg5+  ,dbg6+  ,dbg7+  ,dbg8+  ,dbg9+  ,ptraceAtIO+  ,dbg0IO+  ,dbg1IO+  ,dbg2IO+  ,dbg3IO+  ,dbg4IO+  ,dbg5IO+  ,dbg6IO+  ,dbg7IO+  ,dbg8IO+  ,dbg9IO+  ,plog+  ,plogAt+  ,traceParse+  ,dbgparse   ,module Debug.Trace-  ,ppShow ) where @@ -26,28 +56,24 @@ import           System.IO.Unsafe (unsafePerformIO) import           Text.Megaparsec import           Text.Printf-import           Text.Show.Pretty (ppShow)+import           Text.Show.Pretty (ppShow, pPrint) +-- | Pretty print. Easier alias for pretty-show's pPrint. pprint :: Show a => a -> IO ()-pprint = putStrLn . ppShow+pprint = pPrint +-- | Pretty show. Easier alias for pretty-show's ppShow.+pshow :: Show a => a -> String+pshow = ppShow++-- | Pretty trace. Easier alias for traceShowId + ppShow.+ptrace :: Show a => a -> a+ptrace = traceWith pshow+ -- | Trace (print to stderr) a showable value using a custom show function. traceWith :: (a -> String) -> a -> a 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 :: String -> TextParser m ()-ptrace msg = do-  pos <- getPosition-  next <- (T.take peeklength) `fmap` getInput-  let (l,c) = (sourceLine pos, sourceColumn pos)-      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-    peeklength = 30- -- | Global debug level, which controls the verbosity of debug output -- on the console. The default is 0 meaning no debug output. The -- @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to@@ -71,105 +97,109 @@     where       args = unsafePerformIO getArgs --- | Convenience aliases for tracePrettyAt.+-- | Pretty-print a label and a showable value to the console+-- if the global debug level is at or above the specified level.+-- At level 0, always prints. Otherwise, uses unsafePerformIO.+ptraceAt :: Show a => Int -> String -> a -> a+ptraceAt level+    | level > 0 && debugLevel < level = flip const+    | otherwise = \s a -> let p = ppShow a+                              ls = lines p+                              nlorspace | length ls > 1 = "\n"+                                        | otherwise     = " " ++ take (10 - length s) (repeat ' ')+                              ls' | length ls > 1 = map (" "++) ls+                                  | otherwise     = ls+                          in trace (s++":"++nlorspace++intercalate "\n" ls') a --- Always pretty-print a message and the showable value to the console, then return it.--- ("dbg" without the 0 clashes with megaparsec 5.1).+-- | Pretty-print a message and the showable value to the console, then return it. dbg0 :: Show a => String -> a -> a-dbg0 = tracePrettyAt 0+dbg0 = ptraceAt 0+-- "dbg" would clash with megaparsec --- | Pretty-print a message and the showable value to the console when the debug level is >= 1, then return it. Uses unsafePerformIO.+-- | Like dbg0, but also exit the program. Uses unsafePerformIO.+dbgExit :: Show a => String -> a -> a+dbgExit msg = const (unsafePerformIO exitFailure) . dbg0 msg++-- | Pretty-print a message and the showable value to the console when the global debug level is >= 1, then return it.+-- Uses unsafePerformIO. dbg1 :: Show a => String -> a -> a-dbg1 = tracePrettyAt 1+dbg1 = ptraceAt 1  dbg2 :: Show a => String -> a -> a-dbg2 = tracePrettyAt 2+dbg2 = ptraceAt 2  dbg3 :: Show a => String -> a -> a-dbg3 = tracePrettyAt 3+dbg3 = ptraceAt 3  dbg4 :: Show a => String -> a -> a-dbg4 = tracePrettyAt 4+dbg4 = ptraceAt 4  dbg5 :: Show a => String -> a -> a-dbg5 = tracePrettyAt 5+dbg5 = ptraceAt 5  dbg6 :: Show a => String -> a -> a-dbg6 = tracePrettyAt 6+dbg6 = ptraceAt 6  dbg7 :: Show a => String -> a -> a-dbg7 = tracePrettyAt 7+dbg7 = ptraceAt 7  dbg8 :: Show a => String -> a -> a-dbg8 = tracePrettyAt 8+dbg8 = ptraceAt 8  dbg9 :: Show a => String -> a -> a-dbg9 = tracePrettyAt 9+dbg9 = ptraceAt 9 --- | Convenience aliases for tracePrettyAtIO.--- Like dbg, but convenient to insert in an IO monad.--- XXX These have a bug; they should use traceIO, not trace,--- otherwise GHC can occasionally over-optimise+-- | Like ptraceAt, but convenient to insert in an IO monad (plus+-- convenience aliases).+-- XXX These have a bug; they should use+-- traceIO, not trace, otherwise GHC can occasionally over-optimise -- (cf lpaste a few days ago where it killed/blocked a child thread).+ptraceAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()+ptraceAtIO lvl lbl x = liftIO $ ptraceAt lvl lbl x `seq` return ()++-- XXX Could not deduce (a ~ ())+-- ptraceAtM :: (Monad m, Show a) => Int -> String -> a -> m a+-- ptraceAtM lvl lbl x = ptraceAt lvl lbl x `seq` return x+ dbg0IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg0IO = tracePrettyAtIO 0+dbg0IO = ptraceAtIO 0  dbg1IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg1IO = tracePrettyAtIO 1+dbg1IO = ptraceAtIO 1  dbg2IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg2IO = tracePrettyAtIO 2+dbg2IO = ptraceAtIO 2  dbg3IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg3IO = tracePrettyAtIO 3+dbg3IO = ptraceAtIO 3  dbg4IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg4IO = tracePrettyAtIO 4+dbg4IO = ptraceAtIO 4  dbg5IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg5IO = tracePrettyAtIO 5+dbg5IO = ptraceAtIO 5  dbg6IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg6IO = tracePrettyAtIO 6+dbg6IO = ptraceAtIO 6  dbg7IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg7IO = tracePrettyAtIO 7+dbg7IO = ptraceAtIO 7  dbg8IO :: (MonadIO m, Show a) => String -> a -> m ()-dbg8IO = tracePrettyAtIO 8+dbg8IO = ptraceAtIO 8  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.--- At level 0, always prints. Otherwise, uses unsafePerformIO.-tracePrettyAt :: Show a => Int -> String -> a -> a-tracePrettyAt lvl = dbgppshow lvl---- 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 ()---   at hledger/Hledger/Cli/Main.hs:200:13-42---   ‘a’ is a rigid type variable bound by---       the type signature for dbgM :: Show a => String -> a -> IO ()---       at hledger/Hledger/Cli/Main.hs:200:13--- Expected type: String -> a -> IO ()---   Actual type: String -> a -> IO a--tracePrettyAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()-tracePrettyAtIO lvl lbl x = liftIO $ tracePrettyAt lvl lbl x `seq` return ()+dbg9IO = ptraceAtIO 9 -log0 :: Show a => String -> a -> a-log0 = logPrettyAt 0+-- | Log a message and a pretty-printed showable value to ./debug.log, then return it.+plog :: Show a => String -> a -> a+plog = plogAt 0  -- | Log a message and a pretty-printed showable value to ./debug.log, --- if the debug level is at or above the specified level.+-- if the global debug level is at or above the specified level. -- At level 0, always logs. Otherwise, uses unsafePerformIO.-logPrettyAt :: Show a => Int -> String -> a -> a-logPrettyAt lvl+plogAt :: Show a => Int -> String -> a -> a+plogAt lvl     | lvl > 0 && debugLevel < lvl = flip const     | otherwise = \s a ->          let p = ppShow a@@ -181,66 +211,37 @@             output = s++":"++nlorspace++intercalate "\n" ls'         in unsafePerformIO $ appendFile "debug.log" output >> return a --- | print this string to the console before evaluating the expression,--- 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---- | Print a showable value to the console, with a message, if the--- debug level is at or above the specified level (uses--- unsafePerformIO).--- Values are displayed with ppShow, each field/constructor on its own line.-dbgppshow :: Show a => Int -> String -> a -> a-dbgppshow level-    | level > 0 && debugLevel < level = flip const-    | otherwise = \s a -> let p = ppShow a-                              ls = lines p-                              nlorspace | length ls > 1 = "\n"-                                        | otherwise     = " " ++ take (10 - length s) (repeat ' ')-                              ls' | length ls > 1 = map (" "++) ls-                                  | otherwise     = ls-                          in trace (s++":"++nlorspace++intercalate "\n" ls') a+-- XXX redundant ? More/less robust than log0 ?+-- -- | 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 --- -- | Print a showable value to the console, with a message, if the--- -- debug level is at or above the specified level (uses--- -- unsafePerformIO).--- -- Values are displayed with pprint. Field names are not shown, but the--- -- output is compact with smart line wrapping, long data elided,--- -- and slow calculations timed out.--- dbgpprint :: Data a => Int -> String -> a -> a--- dbgpprint level msg a---     | debugLevel >= level = unsafePerformIO $ do---                               pprint a >>= putStrLn . ((msg++": \n") ++) . show---                               return a---     | otherwise           = a+-- | Print the provided label (if non-null) and current parser state+-- (position and next input) to the console. (See also megaparsec's dbg.)+traceParse :: String -> TextParser m ()+traceParse msg = do+  pos <- getPosition+  next <- (T.take peeklength) `fmap` getInput+  let (l,c) = (sourceLine pos, sourceColumn pos)+      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+    peeklength = 30 --- | Like dbg, then exit the program. Uses unsafePerformIO.-dbgExit :: Show a => String -> a -> a-dbgExit msg = const (unsafePerformIO exitFailure) . dbg0 msg+-- | Print the provided label (if non-null) and current parser state+-- (position and next input) to the console if the global debug level+-- is at or above the specified level. Uses unsafePerformIO.+-- (See also megaparsec's dbg.)+traceParseAt :: Int -> String -> TextParser m ()+traceParseAt level msg = when (level <= debugLevel) $ traceParse msg --- | Print a message and parsec debug info (parse position and next--- input) to the console when the debug level is at or above--- this level. Uses unsafePerformIO.--- pdbgAt :: GenParser m => Float -> String -> m ()-pdbg :: Int -> String -> TextParser m ()-pdbg level msg = when (level <= debugLevel) $ ptrace msg+-- | Convenience alias for traceParseAt+dbgparse :: Int -> String -> TextParser m ()+dbgparse level msg = traceParseAt level 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,4 +1,4 @@-{-# LANGUAGE CPP, TypeFamilies #-}+{-# LANGUAGE TypeFamilies #-}  module Hledger.Utils.Parse (   SimpleStringParser,@@ -71,6 +71,8 @@ parsewithString :: Parsec e String a -> String -> Either (ParseError Char e) a parsewithString p = runParser p "" +-- | Run a stateful parser with some initial state on a text.+-- See also: runTextParser, runJournalParser. parseWithState :: Monad m => st -> StateT st (ParsecT CustomErr Text m) a -> Text -> m (Either (ParseError Char CustomErr) a) parseWithState ctx p s = runParserT (evalStateT p ctx) "" s 
Hledger/Utils/Test.hs view
@@ -1,42 +1,138 @@-module Hledger.Utils.Test where+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} -import Test.HUnit+module Hledger.Utils.Test (+   HasCallStack+  ,module EasyTest+  ,runEasytests+  ,tests+  ,_tests+  ,test+  ,_test+  ,it+  ,_it+  ,is+  ,expectEqPP+  ,expectParse+  ,expectParseError+  ,expectParseEq+  ,expectParseEqOn+) +where++import Control.Exception+import Control.Monad.State.Strict (StateT, evalStateT)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import Data.CallStack+import Data.List+import qualified Data.Text as T+import Safe +import System.Exit import Text.Megaparsec+import Text.Megaparsec.Custom --- | Get a Test's label, or the empty string.-testName :: Test -> String-testName (TestLabel n _) = n-testName _ = ""+import EasyTest hiding (char, char', tests)  -- reexported+import qualified EasyTest as E               -- used here --- | Flatten a Test containing TestLists into a list of single tests.-flattenTests :: Test -> [Test]-flattenTests (TestLabel _ t@(TestList _)) = flattenTests t-flattenTests (TestList ts) = concatMap flattenTests ts-flattenTests t = [t]+import Hledger.Utils.Debug (pshow)+import Hledger.Utils.UTF8IOCompat (error') --- | Filter TestLists in a Test, recursively, preserving the structure.-filterTests :: (Test -> Bool) -> Test -> Test-filterTests p (TestLabel l ts) = TestLabel l (filterTests p ts)-filterTests p (TestList ts) = TestList $ filter (any p . flattenTests) $ map (filterTests p) ts-filterTests _ t = t+-- * easytest helpers --- | Simple way to assert something is some expected value, with no label.-is :: (Eq a, Show a) => a -> a -> Assertion-a `is` e = assertEqual "" e a+-- | Name the given test(s). A readability synonym for easytest's "scope".+test :: T.Text -> E.Test a -> E.Test a +test = E.scope --- | Assert a parse result is successful, printing the parse error on failure.-assertParse :: (Show t, Show e) => (Either (ParseError t e) a) -> Assertion-assertParse parse = either (assertFailure.show) (const (return ())) parse+-- | Skip the given test(s), with the same type signature as "test".+-- If called in a monadic sequence of tests, also skips following tests.+_test :: T.Text -> E.Test a -> E.Test a +_test _name = (E.skip >>)  +-- | Name the given test(s). A synonym for "test".+it :: T.Text -> E.Test a -> E.Test a +it = test --- | Assert a parse result is successful, printing the parse error on failure.-assertParseFailure :: (Either (ParseError t e) a) -> Assertion-assertParseFailure parse = either (const $ return ()) (const $ assertFailure "parse should not have succeeded") parse+-- | Skip the given test(s), and any following tests in a monadic sequence. +-- A synonym for "_test".+_it :: T.Text -> E.Test a -> E.Test a +_it = _test --- | Assert a parse result is some expected value, printing the parse error on failure.-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+-- | Name and group a list of tests. Combines easytest's "scope" and "tests".+tests :: T.Text -> [E.Test ()] -> E.Test () +tests name = E.scope name . E.tests -printParseError :: (Show a) => a -> IO ()-printParseError e = do putStr "parse error at "; print e+-- | Skip the given list of tests, and any following tests in a monadic sequence,+-- with the same type signature as "group".+_tests :: T.Text -> [E.Test ()] -> E.Test () +_tests _name = (E.skip >>) . E.tests++-- | Run some easytest tests, catching easytest's ExitCode exception,+-- returning True if there was a problem.+-- With arguments, runs only the scope (or single test) named by the first argument+-- (exact, case sensitive). +-- If there is a second argument, it should be an integer and will be used+-- as the seed for randomness. +runEasytests :: [String] -> E.Test () -> IO Bool+runEasytests args tests = (do+  case args of+    []    -> E.run tests+    [a]   -> E.runOnly (T.pack a) tests+    a:b:_ -> do+      case readMay b :: Maybe Int of+        Nothing   -> error' "the second argument should be an integer (a seed for easytest)"+        Just seed -> E.rerunOnly seed (T.pack a) tests+  return False+  )+  `catch` (\(_::ExitCode) -> return True)++-- | Like easytest's expectEq (asserts the second (actual) value equals the first (expected) value)+-- but pretty-prints the values in the failure output. +expectEqPP :: (Eq a, Show a, HasCallStack) => a -> a -> E.Test ()+expectEqPP expected actual = if expected == actual then E.ok else E.crash $+  "\nexpected:\n" <> T.pack (pshow expected) <> "\nbut got:\n" <> T.pack (pshow actual) <> "\n"++-- | Shorter and flipped version of expectEqPP. The expected value goes last.+is :: (Eq a, Show a, HasCallStack) => a -> a -> Test ()+is = flip expectEqPP++-- | Test that this stateful parser runnable in IO successfully parses +-- all of the given input text, showing the parse error if it fails. +-- Suitable for hledger's JournalParser parsers.+expectParse :: (Monoid st, Eq a, Show a, HasCallStack) => +  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> E.Test ()+expectParse parser input = do+  ep <- E.io (runParserT (evalStateT (parser <* eof) mempty) "" input)+  either (fail.(++"\n").("\nparse error at "++).parseErrorPretty) (const ok) ep++-- | Test that this stateful parser runnable in IO fails to parse +-- the given input text, with a parse error containing the given string. +expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) => +  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> String -> E.Test ()+expectParseError parser input errstr = do+  ep <- E.io (runParserT (evalStateT parser mempty) "" input)+  case ep of+    Right v -> fail $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"+    Left e  -> do+      let e' = parseErrorPretty e+      if errstr `isInfixOf` e'+      then ok+      else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n"++-- | Like expectParse, but also test the parse result is an expected value,+-- pretty-printing both if it fails. +expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) => +  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> E.Test ()+expectParseEq parser input expected = expectParseEqOn parser input id expected++-- | Like expectParseEq, but transform the parse result with the given function +-- before comparing it.+expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) => +  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> E.Test ()+expectParseEqOn parser input f expected = do+  ep <- E.io $ runParserT (evalStateT (parser <* eof) mempty) "" input+  either (fail . (++"\n") . ("\nparse error at "++) . parseErrorPretty) (expectEqPP expected . f) ep 
Hledger/Utils/Text.hs view
@@ -54,10 +54,10 @@  -- fitStringMulti,   textPadLeftWide,   textPadRightWide,-  tests_Hledger_Utils_Text+  -- -- * tests+  tests_Text   ) where-import Test.HUnit  -- import Data.Char import Data.List@@ -72,6 +72,7 @@ -- import Hledger.Utils.Parse -- import Hledger.Utils.Regex import Hledger.Utils.String (charWidth)+import Hledger.Utils.Test  -- lowercase, uppercase :: String -> String -- lowercase = map toLower@@ -419,12 +420,14 @@ --         | otherwise                        -> 1  -tests_Hledger_Utils_Text = TestList [-      quoteIfSpaced "a'a" ~?= "a'a"-    , quoteIfSpaced "a\"a" ~?= "a\"a"-    , quoteIfSpaced "a a" ~?= "\"a a\""-    , quoteIfSpaced "mimi's cafe" ~?= "\"mimi's cafe\""-    , quoteIfSpaced "\"alex\" cafe" ~?= "\"\\\"alex\\\" cafe\""-    , quoteIfSpaced "le'shan's cafe" ~?= "\"le'shan's cafe\""-    , quoteIfSpaced "\"be'any's\" cafe" ~?= "\"\\\"be'any's\\\" cafe\""-    ]+tests_Text = tests "Text" [+   tests "quoteIfSpaced" [+     quoteIfSpaced "a'a" `is` "a'a"+    ,quoteIfSpaced "a\"a" `is` "a\"a"              +    ,quoteIfSpaced "a a" `is` "\"a a\""               +    ,quoteIfSpaced "mimi's cafe" `is` "\"mimi's cafe\""       +    ,quoteIfSpaced "\"alex\" cafe" `is` "\"\\\"alex\\\" cafe\""     +    ,quoteIfSpaced "le'shan's cafe" `is` "\"le'shan's cafe\""    +    ,quoteIfSpaced "\"be'any's\" cafe" `is` "\"\\\"be'any's\\\" cafe\"" +    ] +  ]
hledger-lib.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 757469e81007e12da21192dc913e3abfe79ca36c62479f7d08abc8726656b4c1+-- hash: f3cc307bb564ecec4c16143a1d254c4cbbbee1483eb7860c711e3c4c5ed46431  name:           hledger-lib-version:        1.10+version:        1.11 synopsis:       Core data types, parsers and functionality for the hledger accounting tools description:    This is a reusable library containing hledger's core functionality.                 .@@ -23,7 +23,7 @@ maintainer:     Simon Michael <simon@joyful.com> license:        GPL-3 license-file:   LICENSE-tested-with:    GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.1+tested-with:    GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.3 build-type:     Simple cabal-version:  >= 1.10 extra-source-files:@@ -59,12 +59,13 @@       Hledger.Data.Ledger       Hledger.Data.MarketPrice       Hledger.Data.Period+      Hledger.Data.PeriodicTransaction       Hledger.Data.StringFormat       Hledger.Data.Posting       Hledger.Data.RawOptions       Hledger.Data.Timeclock       Hledger.Data.Transaction-      Hledger.Data.AutoTransaction+      Hledger.Data.TransactionModifier       Hledger.Data.Types       Hledger.Query       Hledger.Read@@ -102,23 +103,26 @@   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:       Decimal-    , HUnit+    , Glob >=0.9     , ansi-terminal >=0.6.2.3     , array     , base >=4.8 && <4.12     , base-compat-batteries >=0.10.1 && <0.11     , blaze-markup >=0.5.1     , bytestring+    , call-stack+    , cassava+    , cassava-megaparsec     , cmdargs >=0.10     , containers-    , csv     , data-default >=0.5     , deepseq     , directory+    , easytest     , extra     , filepath     , hashtables >=1.2.3.1-    , megaparsec >=6.4.1+    , megaparsec >=6.4.1 && <7     , mtl     , mtl-compat     , old-time@@ -148,18 +152,19 @@       Hledger.Data.Account       Hledger.Data.AccountName       Hledger.Data.Amount-      Hledger.Data.AutoTransaction       Hledger.Data.Commodity       Hledger.Data.Dates       Hledger.Data.Journal       Hledger.Data.Ledger       Hledger.Data.MarketPrice       Hledger.Data.Period+      Hledger.Data.PeriodicTransaction       Hledger.Data.Posting       Hledger.Data.RawOptions       Hledger.Data.StringFormat       Hledger.Data.Timeclock       Hledger.Data.Transaction+      Hledger.Data.TransactionModifier       Hledger.Data.Types       Hledger.Query       Hledger.Read@@ -193,29 +198,31 @@       Paths_hledger_lib   hs-source-dirs:       ./.-      tests+      test   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:       Decimal     , Glob >=0.7-    , HUnit     , ansi-terminal >=0.6.2.3     , array     , base >=4.8 && <4.12     , base-compat-batteries >=0.10.1 && <0.11     , blaze-markup >=0.5.1     , bytestring+    , call-stack+    , cassava+    , cassava-megaparsec     , cmdargs >=0.10     , containers-    , csv     , data-default >=0.5     , deepseq     , directory-    , doctest >=0.8+    , doctest >=0.16+    , easytest     , extra     , filepath     , hashtables >=1.2.3.1-    , megaparsec >=6.4.1+    , megaparsec >=6.4.1 && <7     , mtl     , mtl-compat     , old-time@@ -245,18 +252,19 @@       Hledger.Data.Account       Hledger.Data.AccountName       Hledger.Data.Amount-      Hledger.Data.AutoTransaction       Hledger.Data.Commodity       Hledger.Data.Dates       Hledger.Data.Journal       Hledger.Data.Ledger       Hledger.Data.MarketPrice       Hledger.Data.Period+      Hledger.Data.PeriodicTransaction       Hledger.Data.Posting       Hledger.Data.RawOptions       Hledger.Data.StringFormat       Hledger.Data.Timeclock       Hledger.Data.Transaction+      Hledger.Data.TransactionModifier       Hledger.Data.Types       Hledger.Query       Hledger.Read@@ -290,20 +298,22 @@       Paths_hledger_lib   hs-source-dirs:       ./.-      tests+      test   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:       Decimal-    , HUnit+    , Glob >=0.9     , ansi-terminal >=0.6.2.3     , array     , base >=4.8 && <4.12     , base-compat-batteries >=0.10.1 && <0.11     , blaze-markup >=0.5.1     , bytestring+    , call-stack+    , cassava+    , cassava-megaparsec     , cmdargs >=0.10     , containers-    , csv     , data-default >=0.5     , deepseq     , directory@@ -312,103 +322,7 @@     , filepath     , hashtables >=1.2.3.1     , hledger-lib-    , megaparsec >=6.4.1-    , mtl-    , mtl-compat-    , old-time-    , parsec >=3-    , parser-combinators >=0.4.0-    , pretty-show >=1.6.4-    , regex-tdfa-    , safe >=0.2-    , split >=0.1-    , tabular >=0.2-    , text >=1.2-    , time >=1.5-    , transformers >=0.2-    , uglymemo-    , utf8-string >=0.3.5-  if (!impl(ghc >= 8.0))-    build-depends:-        semigroups ==0.18.*-  default-language: Haskell2010--test-suite hunittests-  type: exitcode-stdio-1.0-  main-is: hunittests.hs-  other-modules:-      Hledger-      Hledger.Data-      Hledger.Data.Account-      Hledger.Data.AccountName-      Hledger.Data.Amount-      Hledger.Data.AutoTransaction-      Hledger.Data.Commodity-      Hledger.Data.Dates-      Hledger.Data.Journal-      Hledger.Data.Ledger-      Hledger.Data.MarketPrice-      Hledger.Data.Period-      Hledger.Data.Posting-      Hledger.Data.RawOptions-      Hledger.Data.StringFormat-      Hledger.Data.Timeclock-      Hledger.Data.Transaction-      Hledger.Data.Types-      Hledger.Query-      Hledger.Read-      Hledger.Read.Common-      Hledger.Read.CsvReader-      Hledger.Read.JournalReader-      Hledger.Read.TimeclockReader-      Hledger.Read.TimedotReader-      Hledger.Reports-      Hledger.Reports.BalanceHistoryReport-      Hledger.Reports.BalanceReport-      Hledger.Reports.BudgetReport-      Hledger.Reports.EntriesReport-      Hledger.Reports.MultiBalanceReports-      Hledger.Reports.PostingsReport-      Hledger.Reports.ReportOptions-      Hledger.Reports.ReportTypes-      Hledger.Reports.TransactionsReports-      Hledger.Utils-      Hledger.Utils.Color-      Hledger.Utils.Debug-      Hledger.Utils.Parse-      Hledger.Utils.Regex-      Hledger.Utils.String-      Hledger.Utils.Test-      Hledger.Utils.Text-      Hledger.Utils.Tree-      Hledger.Utils.UTF8IOCompat-      Text.Megaparsec.Custom-      Text.Tabular.AsciiWide-      Paths_hledger_lib-  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:-      Decimal-    , HUnit-    , ansi-terminal >=0.6.2.3-    , array-    , base >=4.8 && <4.12-    , base-compat-batteries >=0.10.1 && <0.11-    , blaze-markup >=0.5.1-    , bytestring-    , cmdargs >=0.10-    , containers-    , csv-    , data-default >=0.5-    , deepseq-    , directory-    , extra-    , filepath-    , hashtables >=1.2.3.1-    , hledger-lib-    , megaparsec >=6.4.1+    , megaparsec >=6.4.1 && <7     , mtl     , mtl-compat     , old-time@@ -419,8 +333,6 @@     , safe >=0.2     , split >=0.1     , tabular >=0.2-    , test-framework-    , test-framework-hunit     , text >=1.2     , time >=1.5     , transformers >=0.2
hledger_csv.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_csv" "5" "June 2018" "hledger 1.9.99" "hledger User Manuals"+.TH "hledger_csv" "5" "September 2018" "hledger 1.10.99" "hledger User Manuals"   
hledger_csv.info view
@@ -3,8 +3,8 @@  File: hledger_csv.info,  Node: Top,  Next: CSV RULES,  Up: (dir) -hledger_csv(5) hledger 1.9.99-*****************************+hledger_csv(5) hledger 1.10.99+******************************  hledger can read CSV (comma-separated value) files as if they were journal files, automatically converting each CSV record into a@@ -317,33 +317,33 @@  Tag Table: Node: Top72-Node: CSV RULES2167-Ref: #csv-rules2275-Node: skip2537-Ref: #skip2631-Node: date-format2803-Ref: #date-format2930-Node: field list3436-Ref: #field-list3573-Node: field assignment4278-Ref: #field-assignment4433-Node: conditional block4937-Ref: #conditional-block5091-Node: include5987-Ref: #include6117-Node: newest-first6348-Ref: #newest-first6462-Node: CSV TIPS6873-Ref: #csv-tips6967-Node: CSV ordering7085-Ref: #csv-ordering7203-Node: CSV accounts7384-Ref: #csv-accounts7522-Node: CSV amounts7776-Ref: #csv-amounts7922-Node: CSV balance assertions8697-Ref: #csv-balance-assertions8879-Node: Reading multiple CSV files9084-Ref: #reading-multiple-csv-files9254+Node: CSV RULES2169+Ref: #csv-rules2277+Node: skip2539+Ref: #skip2633+Node: date-format2805+Ref: #date-format2932+Node: field list3438+Ref: #field-list3575+Node: field assignment4280+Ref: #field-assignment4435+Node: conditional block4939+Ref: #conditional-block5093+Node: include5989+Ref: #include6119+Node: newest-first6350+Ref: #newest-first6464+Node: CSV TIPS6875+Ref: #csv-tips6969+Node: CSV ordering7087+Ref: #csv-ordering7205+Node: CSV accounts7386+Ref: #csv-accounts7524+Node: CSV amounts7778+Ref: #csv-amounts7924+Node: CSV balance assertions8699+Ref: #csv-balance-assertions8881+Node: Reading multiple CSV files9086+Ref: #reading-multiple-csv-files9256  End Tag Table
hledger_csv.txt view
@@ -249,4 +249,4 @@   -hledger 1.9.99                     June 2018                    hledger_csv(5)+hledger 1.10.99                 September 2018                  hledger_csv(5)
hledger_journal.5 view
@@ -1,6 +1,6 @@ .\"t -.TH "hledger_journal" "5" "June 2018" "hledger 1.9.99" "hledger User Manuals"+.TH "hledger_journal" "5" "September 2018" "hledger 1.10.99" "hledger User Manuals"   @@ -1011,7 +1011,8 @@ .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.+The include file path may contain common glob patterns (e.g.+\f[C]*\f[]). .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.@@ -1158,54 +1159,84 @@ .PD In future it will also help detect misspelled accounts. .PP-Account names can be followed by a numeric account code:+An account directive can also have indented subdirectives following it,+which are currently ignored.+Here is the full syntax: .IP .nf \f[C]-account\ assets\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 1000-account\ assets:bank:checking\ \ \ \ 1110-account\ liabilities\ \ \ \ \ \ \ \ \ \ \ \ \ 2000-account\ revenues\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4000-account\ expenses\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 6000+;\ account\ ACCTNAME+;\ \ \ [OPTIONALSUBDIRECTIVES]++account\ assets:bank:checking+\ \ a\ comment+\ \ some\-tag:12345 \f[] .fi+.SS Account display order .PP-This affects how accounts are sorted in account and balance reports:-accounts with codes are listed before accounts without codes, and in-increasing code order (instead of listing all accounts alphabetically).-Warning, this feature is incomplete; account codes do not yet affect-sort order in-.IP \[bu] 2-the \f[C]accounts\f[] command-.IP \[bu] 2-the \f[C]balance\f[] command's single\-column mode-.IP \[bu] 2-flat mode balance reports (to work around this, declare account codes on-the subaccounts as well).-.IP \[bu] 2-hledger\-web's sidebar+Account directives have another purpose: they set the order in which+accounts are displayed, in hledger reports, hledger\-ui accounts screen,+hledger\-web sidebar etc.+For example, say you have these top\-level accounts:+.IP+.nf+\f[C]+$\ accounts\ \-1+assets+equity+expenses+liabilities+misc+other+revenues+\f[]+.fi .PP-Account codes should be all numeric digits, unique, and separated from-the account name by at least two spaces (since account names may contain-single spaces).-By convention, often the first digit indicates the type of account, as-in this numbering scheme and the example above.-In future, we might use this to recognize account types.+By default, they are displayed in alphabetical order.+But if you add the following account directives to the journal:+.IP+.nf+\f[C]+account\ assets+account\ liabilities+account\ equity+account\ revenues+account\ expenses+\f[]+.fi .PP-An account directive can also have indented subdirectives following it,-which are currently ignored.-Here is the full syntax:+the display order changes to: .IP .nf \f[C]-;\ account\ ACCTNAME\ \ [OPTIONALCODE]-;\ \ \ [OPTIONALSUBDIRECTIVES]--account\ assets:bank:checking\ \ \ 1110-\ \ a\ comment-\ \ some\-tag:12345+$\ accounts\ \-1+assets+liabilities+equity+revenues+expenses+misc+other \f[] .fi+.PP+Ie, declared accounts first, in the order they were declared, followed+by any undeclared accounts in alphabetic order.+.PP+Note that sorting is done at each level of the account tree (within each+group of sibling accounts under the same parent).+This directive:+.IP+.nf+\f[C]+account\ other:zoo+\f[]+.fi+.PP+would influence the position of \f[C]zoo\f[] among \f[C]other\f[]'s+subaccounts, but not the position of \f[C]other\f[] among the top\-level+accounts. .SS Rewriting accounts .PP You can define account alias rules which rewrite your account names, or@@ -1373,7 +1404,7 @@ .PP A periodic transaction rule looks like a normal journal entry, with the date replaced by a tilde (\f[C]~\f[]) followed by a period expression-(mnemonic: \f[C]~\f[] looks like a repeating sine wave):+(mnemonic: \f[C]~\f[] looks like a recurring sine wave.): .IP .nf \f[C]@@ -1410,11 +1441,26 @@ They will look like normal transactions, but with an extra tag named \f[C]recur\f[], whose value is the generating period expression. .PP-Forecast transactions begin on or after the day after the latest normal-(non\-periodic) transaction in the journal, or today if there are none.+Forecast transactions start on the first occurrence, and end on the last+occurrence, of their interval within the forecast period.+The forecast period:+.IP \[bu] 2+begins on the later of+.RS 2+.IP \[bu] 2+the report start date if specified with \-b/\-p/date:+.IP \[bu] 2+the day after the latest normal (non\-periodic) transaction in the+journal, or today if there are no normal transactions.+.RE+.IP \[bu] 2+ends on the report end date if specified with \-e/\-p/date:, or 180 days+from today. .PP-They end on or before the report end date if specified, or 180 days from-today if unspecified.+where \[lq]today\[rq] means the current date at report time.+The \[lq]later of\[rq] rule ensures that forecast transactions do not+overlap normal transactions in time; they will begin only after normal+transactions end. .PP Forecasting can be useful for estimating balances into the future, and experimenting with different scenarios.@@ -1440,15 +1486,18 @@ .PP For more details, see: balance: Budget report and Cookbook: Budgeting and Forecasting.-.SS Automated postings .PP-Automated posting rules describe extra postings that should be added to-certain transactions at report time, when the \f[C]\-\-auto\f[] flag is-used.+.SS Transaction Modifiers .PP-An automated posting rule looks like a normal journal entry, except the-first line is an equal sign (\f[C]=\f[]) followed by a query (mnemonic:-\f[C]=\f[] looks like posting lines):+Transaction modifier rules describe changes that should be applied+automatically to certain transactions.+Currently, this means adding extra postings (also known as+\[lq]automated postings\[rq]).+Transaction modifiers are enabled by the \f[C]\-\-auto\f[] flag.+.PP+A transaction modifier rule looks a bit like a normal journal entry,+except the first line is an equal sign (\f[C]=\f[]) followed by a query+(mnemonic: \f[C]=\f[] suggests matching something.): .IP .nf \f[C]
hledger_journal.info view
@@ -4,8 +4,8 @@  File: hledger_journal.info,  Node: Top,  Next: FILE FORMAT,  Up: (dir) -hledger_journal(5) hledger 1.9.99-*********************************+hledger_journal(5) hledger 1.10.99+**********************************  hledger's usual data source is a plain text file containing journal entries in hledger journal format.  This file represents a standard@@ -82,7 +82,7 @@ * Tags:: * Directives:: * Periodic transactions::-* Automated postings::+* Transaction Modifiers::   File: hledger_journal.info,  Node: Transactions,  Next: Postings,  Up: FILE FORMAT@@ -853,6 +853,7 @@ * Default commodity:: * Market prices:: * Declaring accounts::+* Account display order:: * Rewriting accounts:: * Default parent account:: @@ -878,7 +879,8 @@ 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.+current file.  The include file path may contain common glob patterns+(e.g.  '*').     The 'include' directive can only be used in journal files.  It can include journal, timeclock or timedot files, but not CSV files.@@ -1001,7 +1003,7 @@ another commodity using these prices.  -File: hledger_journal.info,  Node: Declaring accounts,  Next: Rewriting accounts,  Prev: Market prices,  Up: Directives+File: hledger_journal.info,  Node: Declaring accounts,  Next: Account display order,  Prev: Market prices,  Up: Directives  1.14.7 Declaring accounts -------------------------@@ -1015,46 +1017,71 @@ hledger add, hledger-iadd, hledger-web, and ledger-mode. In future it will also help detect misspelled accounts. -   Account names can be followed by a numeric account code:--account assets                  1000-account assets:bank:checking    1110-account liabilities             2000-account revenues                4000-account expenses                6000--   This affects how accounts are sorted in account and balance reports:-accounts with codes are listed before accounts without codes, and in-increasing code order (instead of listing all accounts alphabetically).-Warning, this feature is incomplete; account codes do not yet affect-sort order in--   * the 'accounts' command-   * the 'balance' command's single-column mode-   * flat mode balance reports (to work around this, declare account-     codes on the subaccounts as well).-   * hledger-web's sidebar--   Account codes should be all numeric digits, unique, and separated-from the account name by at least two spaces (since account names may-contain single spaces).  By convention, often the first digit indicates-the type of account, as in this numbering scheme and the example above.-In future, we might use this to recognize account types.-    An account directive can also have indented subdirectives following it, which are currently ignored.  Here is the full syntax: -; account ACCTNAME  [OPTIONALCODE]+; account ACCTNAME ;   [OPTIONALSUBDIRECTIVES] -account assets:bank:checking   1110+account assets:bank:checking   a comment   some-tag:12345  -File: hledger_journal.info,  Node: Rewriting accounts,  Next: Default parent account,  Prev: Declaring accounts,  Up: Directives+File: hledger_journal.info,  Node: Account display order,  Next: Rewriting accounts,  Prev: Declaring accounts,  Up: Directives -1.14.8 Rewriting accounts+1.14.8 Account display order+----------------------------++Account directives have another purpose: they set the order in which+accounts are displayed, in hledger reports, hledger-ui accounts screen,+hledger-web sidebar etc.  For example, say you have these top-level+accounts:++$ accounts -1+assets+equity+expenses+liabilities+misc+other+revenues++   By default, they are displayed in alphabetical order.  But if you add+the following account directives to the journal:++account assets+account liabilities+account equity+account revenues+account expenses++   the display order changes to:++$ accounts -1+assets+liabilities+equity+revenues+expenses+misc+other++   Ie, declared accounts first, in the order they were declared,+followed by any undeclared accounts in alphabetic order.++   Note that sorting is done at each level of the account tree (within+each group of sibling accounts under the same parent).  This directive:++account other:zoo++   would influence the position of 'zoo' among 'other''s subaccounts,+but not the position of 'other' among the top-level accounts.+++File: hledger_journal.info,  Node: Rewriting accounts,  Next: Default parent account,  Prev: Account display order,  Up: Directives++1.14.9 Rewriting accounts -------------------------  You can define account alias rules which rewrite your account names, or@@ -1082,7 +1109,7 @@  File: hledger_journal.info,  Node: Basic aliases,  Next: Regex aliases,  Up: Rewriting accounts -1.14.8.1 Basic aliases+1.14.9.1 Basic aliases ......................  To set an account alias, use the 'alias' directive in your journal file.@@ -1105,7 +1132,7 @@  File: hledger_journal.info,  Node: Regex aliases,  Next: Multiple aliases,  Prev: Basic aliases,  Up: Rewriting accounts -1.14.8.2 Regex aliases+1.14.9.2 Regex aliases ......................  There is also a more powerful variant that uses a regular expression,@@ -1130,7 +1157,7 @@  File: hledger_journal.info,  Node: Multiple aliases,  Next: end aliases,  Prev: Regex aliases,  Up: Rewriting accounts -1.14.8.3 Multiple aliases+1.14.9.3 Multiple aliases .........................  You can define as many aliases as you like using directives or@@ -1146,7 +1173,7 @@  File: hledger_journal.info,  Node: end aliases,  Prev: Multiple aliases,  Up: Rewriting accounts -1.14.8.4 'end aliases'+1.14.9.4 'end aliases' ......................  You can clear (forget) all currently defined aliases with the 'end@@ -1157,8 +1184,8 @@  File: hledger_journal.info,  Node: Default parent account,  Prev: Rewriting accounts,  Up: Directives -1.14.9 Default parent account------------------------------+1.14.10 Default parent account+------------------------------  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@@ -1196,7 +1223,7 @@ parent account.  -File: hledger_journal.info,  Node: Periodic transactions,  Next: Automated postings,  Prev: Directives,  Up: FILE FORMAT+File: hledger_journal.info,  Node: Periodic transactions,  Next: Transaction Modifiers,  Prev: Directives,  Up: FILE FORMAT  1.15 Periodic transactions ==========================@@ -1208,7 +1235,7 @@     A periodic transaction rule looks like a normal journal entry, with the date replaced by a tilde ('~') followed by a period expression-(mnemonic: '~' looks like a repeating sine wave):+(mnemonic: '~' looks like a recurring sine wave.):  ~ monthly     expenses:rent          $2000@@ -1245,13 +1272,23 @@ normal transactions, but with an extra tag named 'recur', whose value is the generating period expression. -   Forecast transactions begin on or after the day after the latest-normal (non-periodic) transaction in the journal, or today if there are-none.+   Forecast transactions start on the first occurrence, and end on the+last occurrence, of their interval within the forecast period.  The+forecast period: -   They end on or before the report end date if specified, or 180 days-from today if unspecified.+   * begins on the later of+        * the report start date if specified with -b/-p/date:+        * the day after the latest normal (non-periodic) transaction in+          the journal, or today if there are no normal transactions. +   * ends on the report end date if specified with -e/-p/date:, or 180+     days from today.++   where "today" means the current date at report time.  The "later of"+rule ensures that forecast transactions do not overlap normal+transactions in time; they will begin only after normal transactions+end.+    Forecasting can be useful for estimating balances into the future, and experimenting with different scenarios.  Note the start date logic means that forecasted transactions are automatically replaced by normal@@ -1283,17 +1320,19 @@ and Forecasting.  -File: hledger_journal.info,  Node: Automated postings,  Prev: Periodic transactions,  Up: FILE FORMAT+File: hledger_journal.info,  Node: Transaction Modifiers,  Prev: Periodic transactions,  Up: FILE FORMAT -1.16 Automated postings-=======================+1.16 Transaction Modifiers+========================== -Automated posting rules describe extra postings that should be added to-certain transactions at report time, when the '--auto' flag is used.+Transaction modifier rules describe changes that should be applied+automatically to certain transactions.  Currently, this means adding+extra postings (also known as "automated postings").  Transaction+modifiers are enabled by the '--auto' flag. -   An automated posting rule looks like a normal journal entry, except-the first line is an equal sign ('=') followed by a query (mnemonic: '='-looks like posting lines):+   A transaction modifier rule looks a bit like a normal journal entry,+except the first line is an equal sign ('=') followed by a query+(mnemonic: '=' suggests matching something.):  = expenses:gifts     budget:gifts  *-1@@ -1354,91 +1393,93 @@  Tag Table: Node: Top76-Node: FILE FORMAT2376-Ref: #file-format2500-Node: Transactions2784-Ref: #transactions2905-Node: Postings3589-Ref: #postings3716-Node: Dates4711-Ref: #dates4826-Node: Simple dates4891-Ref: #simple-dates5017-Node: Secondary dates5383-Ref: #secondary-dates5537-Node: Posting dates7100-Ref: #posting-dates7229-Node: Status8603-Ref: #status8723-Node: Description10431-Ref: #description10569-Node: Payee and note10888-Ref: #payee-and-note11002-Node: Account names11244-Ref: #account-names11387-Node: Amounts11874-Ref: #amounts12010-Node: Virtual Postings15027-Ref: #virtual-postings15186-Node: Balance Assertions16406-Ref: #balance-assertions16581-Node: Assertions and ordering17477-Ref: #assertions-and-ordering17663-Node: Assertions and included files18363-Ref: #assertions-and-included-files18604-Node: Assertions and multiple -f options18937-Ref: #assertions-and-multiple--f-options19191-Node: Assertions and commodities19323-Ref: #assertions-and-commodities19558-Node: Assertions and subaccounts20254-Ref: #assertions-and-subaccounts20486-Node: Assertions and virtual postings21007-Ref: #assertions-and-virtual-postings21214-Node: Balance Assignments21356-Ref: #balance-assignments21537-Node: Transaction prices22657-Ref: #transaction-prices22826-Node: Comments25094-Ref: #comments25228-Node: Tags26398-Ref: #tags26516-Node: Directives27918-Ref: #directives28061-Node: Comment blocks33917-Ref: #comment-blocks34062-Node: Including other files34238-Ref: #including-other-files34418-Node: Default year34807-Ref: #default-year34976-Node: Declaring commodities35399-Ref: #declaring-commodities35582-Node: Default commodity36809-Ref: #default-commodity36985-Node: Market prices37621-Ref: #market-prices37786-Node: Declaring accounts38627-Ref: #declaring-accounts38803-Node: Rewriting accounts40474-Ref: #rewriting-accounts40659-Node: Basic aliases41393-Ref: #basic-aliases41539-Node: Regex aliases42243-Ref: #regex-aliases42414-Node: Multiple aliases43132-Ref: #multiple-aliases43307-Node: end aliases43805-Ref: #end-aliases43952-Node: Default parent account44053-Ref: #default-parent-account44219-Node: Periodic transactions45103-Ref: #periodic-transactions45282-Node: Forecasting with periodic transactions46492-Ref: #forecasting-with-periodic-transactions46735-Node: Budgeting with periodic transactions47976-Ref: #budgeting-with-periodic-transactions48215-Node: Automated postings48674-Ref: #automated-postings48828-Node: EDITOR SUPPORT49967-Ref: #editor-support50085+Node: FILE FORMAT2378+Ref: #file-format2502+Node: Transactions2789+Ref: #transactions2910+Node: Postings3594+Ref: #postings3721+Node: Dates4716+Ref: #dates4831+Node: Simple dates4896+Ref: #simple-dates5022+Node: Secondary dates5388+Ref: #secondary-dates5542+Node: Posting dates7105+Ref: #posting-dates7234+Node: Status8608+Ref: #status8728+Node: Description10436+Ref: #description10574+Node: Payee and note10893+Ref: #payee-and-note11007+Node: Account names11249+Ref: #account-names11392+Node: Amounts11879+Ref: #amounts12015+Node: Virtual Postings15032+Ref: #virtual-postings15191+Node: Balance Assertions16411+Ref: #balance-assertions16586+Node: Assertions and ordering17482+Ref: #assertions-and-ordering17668+Node: Assertions and included files18368+Ref: #assertions-and-included-files18609+Node: Assertions and multiple -f options18942+Ref: #assertions-and-multiple--f-options19196+Node: Assertions and commodities19328+Ref: #assertions-and-commodities19563+Node: Assertions and subaccounts20259+Ref: #assertions-and-subaccounts20491+Node: Assertions and virtual postings21012+Ref: #assertions-and-virtual-postings21219+Node: Balance Assignments21361+Ref: #balance-assignments21542+Node: Transaction prices22662+Ref: #transaction-prices22831+Node: Comments25099+Ref: #comments25233+Node: Tags26403+Ref: #tags26521+Node: Directives27923+Ref: #directives28066+Node: Comment blocks33948+Ref: #comment-blocks34093+Node: Including other files34269+Ref: #including-other-files34449+Node: Default year34857+Ref: #default-year35026+Node: Declaring commodities35449+Ref: #declaring-commodities35632+Node: Default commodity36859+Ref: #default-commodity37035+Node: Market prices37671+Ref: #market-prices37836+Node: Declaring accounts38677+Ref: #declaring-accounts38856+Node: Account display order39406+Ref: #account-display-order39596+Node: Rewriting accounts40617+Ref: #rewriting-accounts40805+Node: Basic aliases41539+Ref: #basic-aliases41685+Node: Regex aliases42389+Ref: #regex-aliases42560+Node: Multiple aliases43278+Ref: #multiple-aliases43453+Node: end aliases43951+Ref: #end-aliases44098+Node: Default parent account44199+Ref: #default-parent-account44367+Node: Periodic transactions45251+Ref: #periodic-transactions45433+Node: Forecasting with periodic transactions46644+Ref: #forecasting-with-periodic-transactions46887+Node: Budgeting with periodic transactions48574+Ref: #budgeting-with-periodic-transactions48813+Node: Transaction Modifiers49272+Ref: #transaction-modifiers49435+Node: EDITOR SUPPORT50691+Ref: #editor-support50809  End Tag Table
hledger_journal.txt view
@@ -705,14 +705,15 @@               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.+       file.  The include file path may contain  common  glob  patterns  (e.g.+       *). -       The include directive can only  be  used  in  journal  files.   It  can+       The  include  directive  can  only  be  used  in journal files.  It can        include journal, timeclock or timedot files, but not CSV files.     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.+       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@@ -732,8 +733,8 @@                 assets     Declaring commodities-       The  commodity  directive declares commodities which may be used in the-       journal (though currently we do not enforce this).  It may  be  written+       The commodity directive declares commodities which may be used  in  the+       journal  (though  currently we do not enforce this).  It may be written        on a single line, like this:                ; commodity EXAMPLEAMOUNT@@ -743,8 +744,8 @@               ; 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+       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@@ -756,19 +757,19 @@               commodity INR                 format INR 9,99,99,999.00 -       Commodity  directives  have  a second purpose: they define the standard+       Commodity directives have a second purpose: they  define  the  standard        display format for amounts in the commodity.  Normally the display for--       mat  is  inferred  from journal entries, but this can be unpredictable;-       declaring it with a commodity  directive  overrides  this  and  removes-       ambiguity.   Towards  this  end,  amounts  in commodity directives must-       always be written with a decimal point (a period or comma, followed  by+       mat is inferred from journal entries, but this  can  be  unpredictable;+       declaring  it  with  a  commodity  directive overrides this and removes+       ambiguity.  Towards this end,  amounts  in  commodity  directives  must+       always  be written with a decimal point (a period or comma, followed by        0 or more decimal digits).     Default commodity-       The  D  directive  sets a default commodity (and display format), to be+       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+       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@@ -783,9 +784,9 @@        a decimal point.     Market prices-       The P directive declares a market price,  which  is  an  exchange  rate+       The  P  directive  declares  a  market price, which is an exchange rate        between two commodities on a certain date.  (In Ledger, they are called-       "historical prices".) These are often obtained from a  stock  exchange,+       "historical  prices".)  These are often obtained from a stock exchange,        cryptocurrency exchange, or the foreign exchange market.         Here is the format:@@ -796,67 +797,84 @@         o COMMODITYA is the symbol of the commodity being priced -       o COMMODITYBAMOUNT  is an amount (symbol and quantity) in a second com-+       o COMMODITYBAMOUNT is an amount (symbol and quantity) in a second  com-          modity, giving the price in commodity B of one unit of commodity A. -       These two market price directives say that one euro was worth  1.35  US+       These  two  market price directives say that one euro was worth 1.35 US        dollars during 2009, and $1.40 from 2010 onward:                P 2009/1/1  $1.35               P 2010/1/1  $1.40 -       The  -V/--value flag can be used to convert reported amounts to another+       The -V/--value flag can be used to convert reported amounts to  another        commodity using these prices.     Declaring accounts-       The account directive predeclares account names.  The simplest form  is+       The  account directive predeclares account names.  The simplest form is        account ACCTNAME, eg:                account assets:bank:checking -       Currently  this  mainly  helps  with  account name autocompletion in eg+       Currently this mainly helps with  account  name  autocompletion  in  eg        hledger add, hledger-iadd, hledger-web, and ledger-mode.        In future it will also help detect misspelled accounts. -       Account names can be followed by a numeric account code:+       An account directive can also have indented subdirectives following it,+       which are currently ignored.  Here is the full syntax: -              account assets                  1000-              account assets:bank:checking    1110-              account liabilities             2000-              account revenues                4000-              account expenses                6000+              ; account ACCTNAME+              ;   [OPTIONALSUBDIRECTIVES] -       This affects how accounts are sorted in account  and  balance  reports:-       accounts  with  codes  are listed before accounts without codes, and in-       increasing code order (instead of listing all accounts alphabetically).-       Warning,  this  feature  is incomplete; account codes do not yet affect-       sort order in+              account assets:bank:checking+                a comment+                some-tag:12345 -       o the accounts command+   Account display order+       Account directives have another purpose: they set the  order  in  which+       accounts are displayed, in hledger reports, hledger-ui accounts screen,+       hledger-web sidebar etc.  For example, say  you  have  these  top-level+       accounts: -       o the balance command's single-column mode+              $ accounts -1+              assets+              equity+              expenses+              liabilities+              misc+              other+              revenues -       o flat mode balance reports (to work around this, declare account codes-         on the subaccounts as well).+       By  default,  they are displayed in alphabetical order.  But if you add+       the following account directives to the journal: -       o hledger-web's sidebar+              account assets+              account liabilities+              account equity+              account revenues+              account expenses -       Account  codes should be all numeric digits, unique, and separated from-       the account name by at least two spaces (since account names  may  con--       tain  single  spaces).   By convention, often the first digit indicates-       the type of account, as in this numbering scheme and the example above.-       In future, we might use this to recognize account types.+       the display order changes to: -       An account directive can also have indented subdirectives following it,-       which are currently ignored.  Here is the full syntax:+              $ accounts -1+              assets+              liabilities+              equity+              revenues+              expenses+              misc+              other -              ; account ACCTNAME  [OPTIONALCODE]-              ;   [OPTIONALSUBDIRECTIVES]+       Ie, declared accounts first, in the order they were declared,  followed+       by any undeclared accounts in alphabetic order. -              account assets:bank:checking   1110-                a comment-                some-tag:12345+       Note  that  sorting  is  done at each level of the account tree (within+       each group of sibling accounts under the same parent).  This directive: +              account other:zoo++       would  influence the position of zoo among other's subaccounts, but not+       the position of other among the top-level accounts.+    Rewriting accounts        You can define account alias rules which rewrite your account names, or        parts of them, before generating reports.  This can be useful for:@@ -976,7 +994,7 @@         A periodic transaction rule looks like a normal journal entry, with the        date replaced by a tilde (~) followed by a period expression (mnemonic:-       ~ looks like a repeating sine wave):+       ~ looks like a recurring sine wave.):                ~ monthly                   expenses:rent          $2000@@ -1003,56 +1021,71 @@        normal transactions, but with an extra tag named recur, whose value  is        the generating period expression. -       Forecast transactions begin on or after the day after the latest normal-       (non-periodic) transaction in the journal, or today if there are  none.+       Forecast  transactions  start  on  the first occurrence, and end on the+       last occurrence, of their interval within  the  forecast  period.   The+       forecast period: -       They  end  on  or  before the report end date if specified, or 180 days-       from today if unspecified.+       o begins on the later of -       Forecasting can be useful for estimating balances into the future,  and-       experimenting  with  different  scenarios.   Note  the start date logic+         o the report start date if specified with -b/-p/date:++         o the  day  after the latest normal (non-periodic) transaction in the+           journal, or today if there are no normal transactions.++       o ends on the report end date if specified  with  -e/-p/date:,  or  180+         days from today.++       where  "today"  means  the current date at report time.  The "later of"+       rule ensures that forecast transactions do not overlap normal  transac-+       tions in time; they will begin only after normal transactions end.++       Forecasting  can be useful for estimating balances into the future, and+       experimenting with different scenarios.   Note  the  start  date  logic        means that forecasted transactions are automatically replaced by normal        transactions as you add those.         Forecasting can also help with data entry: describe most of your trans--       actions with periodic rules, and every so  often  copy  the  output  of+       actions  with  periodic  rules,  and  every so often copy the output of        print --forecast to the journal.         You can generate one-time transactions too: just write a period expres--       sion specifying a date with no report interval.  (You could also  write-       a  normal  transaction  with  a future date, but remember this disables+       sion  specifying a date with no report interval.  (You could also write+       a normal transaction with a future date,  but  remember  this  disables        forecast transactions on previous dates.)     Budgeting with periodic transactions-       With the --budget flag, currently supported  by  the  balance  command,-       each  periodic transaction rule declares recurring budget goals for the-       specified accounts.  Eg the first example  above  declares  a  goal  of-       spending  $2000  on  rent  (and  also,  a goal of depositing $2000 into-       checking) every month.  Goals and actual performance can then  be  com-+       With  the  --budget  flag,  currently supported by the balance command,+       each periodic transaction rule declares recurring budget goals for  the+       specified  accounts.   Eg  the  first  example above declares a goal of+       spending $2000 on rent (and also,  a  goal  of  depositing  $2000  into+       checking)  every  month.  Goals and actual performance can then be com-        pared in budget reports. -       For  more  details, see: balance: Budget report and Cookbook: Budgeting+       For more details, see: balance: Budget report and  Cookbook:  Budgeting        and Forecasting. -   Automated postings-       Automated posting rules describe extra postings that should be added to-       certain transactions at report time, when the --auto flag is used. -       An automated posting rule looks like a normal journal entry, except the-       first line is an equal sign (=) followed by a query (mnemonic: =  looks-       like posting lines):+   Transaction Modifiers+       Transaction  modifier  rules  describe  changes  that should be applied+       automatically to certain transactions.  Currently,  this  means  adding+       extra postings (also known as "automated postings").  Transaction modi-+       fiers are enabled by the --auto flag. +       A transaction modifier rule looks a bit like a  normal  journal  entry,+       except  the  first  line  is  an  equal  sign  (=)  followed by a query+       (mnemonic: = suggests matching something.):+               = expenses:gifts                   budget:gifts  *-1                   assets:budget  *1 -       The  posting  amounts can be of the form *N, which means "the amount of-       the matched transaction's first posting, multiplied by  N".   They  can+       The posting amounts can be of the form *N, which means "the  amount  of+       the  matched  transaction's  first posting, multiplied by N".  They can        also be ordinary fixed amounts.  Fixed amounts with no commodity symbol-       will be given the same commodity as  the  matched  transaction's  first+       will  be  given  the  same commodity as the matched transaction's first        posting. -       This  example adds a corresponding (unbalanced) budget posting to every+       This example adds a corresponding (unbalanced) budget posting to  every        transaction involving the expenses:gifts account:                = expenses:gifts@@ -1068,16 +1101,16 @@                   (budget:gifts)            $-20                   assets -       Like postings recorded  by  hand,  automated  postings  participate  in+       Like  postings  recorded  by  hand,  automated  postings participate in        transaction balancing, missing amount inference and balance assertions.  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+       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+       These were written with Ledger in mind,  but  also  work  with  hledger        files:  @@ -1090,14 +1123,13 @@        Textmate       https://github.com/ledger/ledger/wiki/Using-TextMate-2        Text   Wran-   https://github.com/ledger/ledger/wiki/Edit-        gler           ing-Ledger-files-with-TextWrangler-        Visual  Stu-   https://marketplace.visualstudio.com/items?item-        dio Code       Name=mark-hansen.hledger-vscode    REPORTING BUGS-       Report bugs at http://bugs.hledger.org (or on the #hledger IRC  channel+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel        or hledger mail list)  @@ -1111,7 +1143,7 @@   SEE ALSO-       hledger(1),      hledger-ui(1),     hledger-web(1),     hledger-api(1),+       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) @@ -1119,4 +1151,4 @@   -hledger 1.9.99                     June 2018                hledger_journal(5)+hledger 1.10.99                 September 2018              hledger_journal(5)
hledger_timeclock.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_timeclock" "5" "June 2018" "hledger 1.9.99" "hledger User Manuals"+.TH "hledger_timeclock" "5" "September 2018" "hledger 1.10.99" "hledger User Manuals"   
hledger_timeclock.info view
@@ -4,8 +4,8 @@  File: hledger_timeclock.info,  Node: Top,  Up: (dir) -hledger_timeclock(5) hledger 1.9.99-***********************************+hledger_timeclock(5) hledger 1.10.99+************************************  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
hledger_timeclock.txt view
@@ -77,4 +77,4 @@   -hledger 1.9.99                     June 2018              hledger_timeclock(5)+hledger 1.10.99                 September 2018            hledger_timeclock(5)
hledger_timedot.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_timedot" "5" "June 2018" "hledger 1.9.99" "hledger User Manuals"+.TH "hledger_timedot" "5" "September 2018" "hledger 1.10.99" "hledger User Manuals"   
hledger_timedot.info view
@@ -4,8 +4,8 @@  File: hledger_timedot.info,  Node: Top,  Next: FILE FORMAT,  Up: (dir) -hledger_timedot(5) hledger 1.9.99-*********************************+hledger_timedot(5) hledger 1.10.99+**********************************  Timedot is a plain text format for logging dated, categorised quantities (of time, usually), supported by hledger.  It is convenient for@@ -110,7 +110,7 @@  Tag Table: Node: Top76-Node: FILE FORMAT811-Ref: #file-format912+Node: FILE FORMAT813+Ref: #file-format914  End Tag Table
hledger_timedot.txt view
@@ -124,4 +124,4 @@   -hledger 1.9.99                     June 2018                hledger_timedot(5)+hledger 1.10.99                 September 2018              hledger_timedot(5)
+ test/doctests.hs view
@@ -0,0 +1,63 @@+{- +Run doctests in Hledger source files under the current directory+(./Hledger.hs, ./Hledger/**, ./Text/**) using the doctest runner.++Arguments are case-insensitive file path substrings, to limit the files searched.+--verbose shows files being searched for doctests and progress while running.+--slow reloads ghci between each test (https://github.com/sol/doctest#a-note-on-performance).++Eg, in hledger source dir:+ +$ make ghci-doctest, :main [--verbose] [--slow] [CIFILEPATHSUBSTRINGS]++or:++$ stack test hledger-lib:test:doctests [--test-arguments '[--verbose] [--slow] [CIFILEPATHSUBSTRINGS]']++-}++{-# LANGUAGE PackageImports #-}++import Control.Monad+import Data.Char+import Data.List+import System.Environment+import "Glob" System.FilePath.Glob+import Test.DocTest++main = do+  args <- getArgs+  let+    verbose = "--verbose" `elem` args+    slow    = "--slow" `elem` args+    pats    = filter (not . ("-" `isPrefixOf`)) args++  -- find source files+  sourcefiles <- (filter (not . isInfixOf "/.") . concat) <$> sequence [+     glob "Hledger.hs"+    ,glob "Hledger/**/*.hs"+    ,glob "Text/**/*.hs"+    ]++  -- filter by patterns (case insensitive infix substring match)+  let +    fs | null pats = sourcefiles+       | otherwise = [f | f <- sourcefiles, let f' = map toLower f, any (`isInfixOf` f') pats']+          where pats' = map (map toLower) pats+    fslen = length fs+  +  if (null fs)+  then do+    putStrLn $ "No file paths found matching: " ++ unwords pats++  else do+    putStrLn $ +      "Loading and searching for doctests in " +      ++ show fslen +      ++ if fslen > 1 then " files, plus any files they import:" else " file, plus any files it imports:"+    when verbose $ putStrLn $ unwords fs++    doctest $+      (if verbose then ("--verbose" :) else id) $  -- doctest >= 0.15.0+      (if slow then id else ("--fast" :)) $        -- doctest >= 0.11.4+      fs
+ test/easytests.hs view
@@ -0,0 +1,5 @@+{-+Run hledger-lib's easytest tests using the easytest runner.+-}+import Hledger+main = run tests_Hledger
− tests/doctests.hs
@@ -1,14 +0,0 @@-{-# LANGUAGE PackageImports #-}--import Data.List-import "Glob" System.FilePath.Glob-import Test.DocTest--main = do-  fs1 <- glob "Hledger/**/*.hs"-  fs2 <- glob "Text/**/*.hs"-  --fs3 <- glob "other/ledger-parse/**/*.hs"-  let fs = filter (not . isInfixOf "/.") $ ["Hledger.hs"] ++ fs1 ++ fs2-  doctest $ -    "--fast" :  -- https://github.com/sol/doctest#a-note-on-performance -    fs
− tests/easytests.hs
@@ -1,39 +0,0 @@-#!/usr/bin/env stack exec -- ghcid -Tmain--- Run tests using project's resolver, whenever ghcid is happy.------ Experimental tests using easytest, an alternative to hunit (eg).--- https://hackage.haskell.org/package/easytest--{-# LANGUAGE OverloadedStrings #-}--import EasyTest-import Hledger--main :: IO ()-main = do-  run-  -- rerun "journal.standard account types.queries.assets"-  -- rerunOnly 2686786430487349354 "journal.standard account types.queries.assets"-    $ tests [--      scope "journal.standard account types.queries" $-        let-          j = samplejournal-          journalAccountNamesMatching :: Query -> Journal -> [AccountName]-          journalAccountNamesMatching q = filter (q `matchesAccount`) . journalAccountNames-          namesfrom qfunc = journalAccountNamesMatching (qfunc j) j-        in-          tests-            [ scope "assets" $-              expectEq (namesfrom journalAssetAccountQuery)     ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]-            , scope "liabilities" $-              expectEq (namesfrom journalLiabilityAccountQuery) ["liabilities","liabilities:debts"]-            , scope "equity" $-              expectEq (namesfrom journalEquityAccountQuery)    []-            , scope "income" $-              expectEq (namesfrom journalIncomeAccountQuery)    ["income","income:gifts","income:salary"]-            , scope "expenses" $-              expectEq (namesfrom journalExpenseAccountQuery)   ["expenses","expenses:food","expenses:supplies"]-            ]--    ]
− tests/hunittests.hs
@@ -1,10 +0,0 @@-import Hledger (tests_Hledger)-import System.Environment (getArgs)-import Test.Framework.Providers.HUnit (hUnitTestToTests)-import Test.Framework.Runners.Console (defaultMainWithArgs)--main :: IO ()-main = do-  args <- getArgs-  let args' = "--hide-successes" : args-  defaultMainWithArgs (hUnitTestToTests tests_Hledger) args'