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 +73/−0
- Hledger.hs +11/−11
- Hledger/Data.hs +17/−21
- Hledger/Data/Account.hs +51/−28
- Hledger/Data/AccountName.hs +58/−29
- Hledger/Data/Amount.hs +138/−116
- Hledger/Data/AutoTransaction.hs +0/−333
- Hledger/Data/Commodity.hs +0/−4
- Hledger/Data/Dates.hs +43/−22
- Hledger/Data/Journal.hs +49/−47
- Hledger/Data/Ledger.hs +29/−12
- Hledger/Data/MarketPrice.hs +0/−3
- Hledger/Data/PeriodicTransaction.hs +243/−0
- Hledger/Data/Posting.hs +21/−16
- Hledger/Data/RawOptions.hs +5/−1
- Hledger/Data/StringFormat.hs +43/−44
- Hledger/Data/Timeclock.hs +33/−33
- Hledger/Data/Transaction.hs +201/−203
- Hledger/Data/TransactionModifier.hs +131/−0
- Hledger/Data/Types.hs +60/−21
- Hledger/Query.hs +179/−190
- Hledger/Read.hs +35/−53
- Hledger/Read/Common.hs +140/−120
- Hledger/Read/CsvReader.hs +108/−111
- Hledger/Read/JournalReader.hs +294/−270
- Hledger/Read/TimeclockReader.hs +0/−5
- Hledger/Read/TimedotReader.hs +8/−15
- Hledger/Reports.hs +12/−16
- Hledger/Reports/BalanceHistoryReport.hs +0/−4
- Hledger/Reports/BalanceReport.hs +251/−232
- Hledger/Reports/BudgetReport.hs +59/−68
- Hledger/Reports/EntriesReport.hs +8/−13
- Hledger/Reports/MultiBalanceReports.hs +75/−95
- Hledger/Reports/PostingsReport.hs +208/−210
- Hledger/Reports/ReportOptions.hs +25/−33
- Hledger/Reports/TransactionsReports.hs +9/−8
- Hledger/Utils.hs +9/−8
- Hledger/Utils/Debug.hs +132/−131
- Hledger/Utils/Parse.hs +3/−1
- Hledger/Utils/Test.hs +126/−30
- Hledger/Utils/Text.hs +14/−11
- hledger-lib.cabal +28/−116
- hledger_csv.5 +1/−1
- hledger_csv.info +30/−30
- hledger_csv.txt +1/−1
- hledger_journal.5 +98/−49
- hledger_journal.info +183/−142
- hledger_journal.txt +117/−85
- hledger_timeclock.5 +1/−1
- hledger_timeclock.info +2/−2
- hledger_timeclock.txt +1/−1
- hledger_timedot.5 +1/−1
- hledger_timedot.info +4/−4
- hledger_timedot.txt +1/−1
- test/doctests.hs +63/−0
- test/easytests.hs +5/−0
- tests/doctests.hs +0/−14
- tests/easytests.hs +0/−39
- tests/hunittests.hs +0/−10
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'