packages feed

hledger-lib 1.34 → 1.40

raw patch · 30 files changed

+1274/−1110 lines, 30 filesdep +luciddep ~megaparsecPVP ok

version bump matches the API change (PVP)

Dependencies added: lucid

Dependency ranges changed: megaparsec

API changes (from Hackage documentation)

- Hledger.Data: DayOfMonth :: Int -> Interval
- Hledger.Data: DayOfYear :: Int -> Int -> Interval
- Hledger.Data: WeekdayOfMonth :: Int -> Int -> Interval
- Hledger.Data.Dates: showDateSpanMonthAbbrev :: DateSpan -> Text
- Hledger.Data.Period: showPeriodMonthAbbrev :: Period -> Text
- Hledger.Data.Types: DayOfMonth :: Int -> Interval
- Hledger.Data.Types: DayOfYear :: Int -> Int -> Interval
- Hledger.Data.Types: WeekdayOfMonth :: Int -> Int -> Interval
- Hledger.Read.CsvUtils: printCSV :: [CsvRecord] -> Text
- Hledger.Read.CsvUtils: printTSV :: [CsvRecord] -> Text
- Hledger.Read.CsvUtils: tests_CsvUtils :: TestTree
- Hledger.Read.CsvUtils: type CSV = [CsvRecord]
- Hledger.Read.CsvUtils: type CsvRecord = [CsvValue]
- Hledger.Read.CsvUtils: type CsvValue = Text
- Hledger.Reports.BudgetReport: budgetReportAsCsv :: ReportOpts -> BudgetReport -> [[Text]]
- Hledger.Reports.BudgetReport: budgetReportAsTable :: ReportOpts -> BudgetReport -> Table Text Text WideBuilder
- Hledger.Reports.BudgetReport: budgetReportAsText :: ReportOpts -> BudgetReport -> Text
- Hledger.Reports.MultiBalanceReport: balanceReportTableAsText :: ReportOpts -> Table Text Text WideBuilder -> Builder
+ Hledger.Data: MonthAndDay :: Int -> Int -> Interval
+ Hledger.Data: MonthDay :: Int -> Interval
+ Hledger.Data: NthWeekdayOfMonth :: Int -> Int -> Interval
+ Hledger.Data.Amount: showMixedAmountLinesPartsB :: AmountFormat -> MixedAmount -> [(WideBuilder, Amount)]
+ Hledger.Data.Dates: showDateSpanAbbrev :: DateSpan -> Text
+ Hledger.Data.Journal: journalDbg :: Journal -> String
+ Hledger.Data.Period: showPeriodAbbrev :: Period -> Text
+ Hledger.Data.Types: MonthAndDay :: Int -> Int -> Interval
+ Hledger.Data.Types: MonthDay :: Int -> Interval
+ Hledger.Data.Types: NthWeekdayOfMonth :: Int -> Int -> Interval
+ Hledger.Read.Common: emptyorcommentlinep2 :: [Char] -> TextParser m ()
+ Hledger.Reports.PostingsReport: defsortspec :: SortSpec
+ Hledger.Reports.PostingsReport: type SortSpec = [SortField]
+ Hledger.Reports.ReportOptions: AbsAmount' :: Bool -> SortField
+ Hledger.Reports.ReportOptions: Account' :: Bool -> SortField
+ Hledger.Reports.ReportOptions: Amount' :: Bool -> SortField
+ Hledger.Reports.ReportOptions: Date' :: Bool -> SortField
+ Hledger.Reports.ReportOptions: Description' :: Bool -> SortField
+ Hledger.Reports.ReportOptions: [sortspec_] :: ReportOpts -> SortSpec
+ Hledger.Reports.ReportOptions: data SortField
+ Hledger.Reports.ReportOptions: defsortspec :: SortSpec
+ Hledger.Reports.ReportOptions: instance GHC.Classes.Eq Hledger.Reports.ReportOptions.SortField
+ Hledger.Reports.ReportOptions: instance GHC.Show.Show Hledger.Reports.ReportOptions.SortField
+ Hledger.Reports.ReportOptions: sortKeysDescription :: [Char]
+ Hledger.Reports.ReportOptions: sortspec :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH SortSpec
+ Hledger.Reports.ReportOptions: type SortSpec = [SortField]
+ Hledger.Utils.Parse: attachSource :: FilePath -> Text -> FinalParseError' e -> FinalParseErrorBundle' e
+ Hledger.Utils.Parse: data FinalParseError' e
+ Hledger.Utils.Parse: data FinalParseErrorBundle' e
+ Hledger.Utils.Parse: data SourceExcerpt
+ Hledger.Utils.Parse: excerpt_ :: MonadParsec HledgerParseErrorData Text m => m a -> m SourceExcerpt
+ Hledger.Utils.Parse: finalCustomFailure :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => e -> m a
+ Hledger.Utils.Parse: finalError :: ParseError Text e -> FinalParseError' e
+ Hledger.Utils.Parse: finalErrorBundlePretty :: FinalParseErrorBundle' HledgerParseErrorData -> String
+ Hledger.Utils.Parse: finalFail :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => String -> m a
+ Hledger.Utils.Parse: finalFancyFailure :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => Set (ErrorFancy e) -> m a
+ Hledger.Utils.Parse: getExcerptText :: SourceExcerpt -> Text
+ Hledger.Utils.Parse: instance GHC.Base.Monoid (Hledger.Utils.Parse.FinalParseError' e)
+ Hledger.Utils.Parse: instance GHC.Base.Semigroup (Hledger.Utils.Parse.FinalParseError' e)
+ Hledger.Utils.Parse: instance GHC.Classes.Eq Hledger.Utils.Parse.HledgerParseErrorData
+ Hledger.Utils.Parse: instance GHC.Classes.Ord (Text.Megaparsec.Error.ParseError Data.Text.Internal.Text Hledger.Utils.Parse.HledgerParseErrorData)
+ Hledger.Utils.Parse: instance GHC.Classes.Ord Hledger.Utils.Parse.HledgerParseErrorData
+ Hledger.Utils.Parse: instance GHC.Show.Show Hledger.Utils.Parse.HledgerParseErrorData
+ Hledger.Utils.Parse: instance GHC.Show.Show e => GHC.Show.Show (Hledger.Utils.Parse.FinalParseError' e)
+ Hledger.Utils.Parse: instance GHC.Show.Show e => GHC.Show.Show (Hledger.Utils.Parse.FinalParseErrorBundle' e)
+ Hledger.Utils.Parse: instance Text.Megaparsec.Error.ShowErrorComponent Hledger.Utils.Parse.HledgerParseErrorData
+ Hledger.Utils.Parse: parseErrorAt :: Int -> String -> HledgerParseErrorData
+ Hledger.Utils.Parse: parseErrorAtRegion :: Int -> Int -> String -> HledgerParseErrorData
+ Hledger.Utils.Parse: parseIncludeFile :: Monad m => StateT st (ParsecT HledgerParseErrorData Text (ExceptT FinalParseError m)) a -> st -> FilePath -> Text -> StateT st (ParsecT HledgerParseErrorData Text (ExceptT FinalParseError m)) a
+ Hledger.Utils.Parse: reparseExcerpt :: Monad m => SourceExcerpt -> ParsecT HledgerParseErrorData Text m a -> ParsecT HledgerParseErrorData Text m a
+ Hledger.Utils.Parse: type FinalParseError = FinalParseError' HledgerParseErrorData
+ Hledger.Utils.Parse: type FinalParseErrorBundle = FinalParseErrorBundle' HledgerParseErrorData
+ Hledger.Write.Csv: printCSV :: CSV -> Text
+ Hledger.Write.Csv: printTSV :: CSV -> Text
+ Hledger.Write.Csv: tests_CsvUtils :: TestTree
+ Hledger.Write.Csv: type CSV = [CsvRecord]
+ Hledger.Write.Csv: type CsvRecord = [CsvValue]
+ Hledger.Write.Csv: type CsvValue = Text
+ Hledger.Write.Html: printHtml :: [[Cell (Html ())]] -> Html ()
+ Hledger.Write.Ods: printFods :: TextEncoding -> Map Text ((Maybe Int, Maybe Int), [[Cell Text]]) -> Text
+ Hledger.Write.Spreadsheet: Body :: Emphasis -> Style
+ Hledger.Write.Spreadsheet: Cell :: Type -> Style -> text -> Cell text
+ Hledger.Write.Spreadsheet: Head :: Style
+ Hledger.Write.Spreadsheet: Item :: Emphasis
+ Hledger.Write.Spreadsheet: Total :: Emphasis
+ Hledger.Write.Spreadsheet: TypeAmount :: !Amount -> Type
+ Hledger.Write.Spreadsheet: TypeDate :: Type
+ Hledger.Write.Spreadsheet: TypeMixedAmount :: Type
+ Hledger.Write.Spreadsheet: TypeString :: Type
+ Hledger.Write.Spreadsheet: [cellContent] :: Cell text -> text
+ Hledger.Write.Spreadsheet: [cellStyle] :: Cell text -> Style
+ Hledger.Write.Spreadsheet: [cellType] :: Cell text -> Type
+ Hledger.Write.Spreadsheet: data Cell text
+ Hledger.Write.Spreadsheet: data Emphasis
+ Hledger.Write.Spreadsheet: data Style
+ Hledger.Write.Spreadsheet: data Type
+ Hledger.Write.Spreadsheet: defaultCell :: text -> Cell text
+ Hledger.Write.Spreadsheet: emptyCell :: Monoid text => Cell text
+ Hledger.Write.Spreadsheet: instance GHC.Base.Functor Hledger.Write.Spreadsheet.Cell
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Eq Hledger.Write.Spreadsheet.Emphasis
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Eq Hledger.Write.Spreadsheet.Style
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Eq Hledger.Write.Spreadsheet.Type
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Ord Hledger.Write.Spreadsheet.Emphasis
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Ord Hledger.Write.Spreadsheet.Style
+ Hledger.Write.Spreadsheet: instance GHC.Classes.Ord Hledger.Write.Spreadsheet.Type
+ Hledger.Write.Spreadsheet: instance GHC.Show.Show Hledger.Write.Spreadsheet.Emphasis
+ Hledger.Write.Spreadsheet: instance GHC.Show.Show Hledger.Write.Spreadsheet.Style
+ Hledger.Write.Spreadsheet: instance GHC.Show.Show Hledger.Write.Spreadsheet.Type
- Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a3GBG => Lens' c_a3GBG BalancingOpts
+ Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP BalancingOpts
- Hledger.Data.Balancing: class HasBalancingOpts c_a3GBG
+ Hledger.Data.Balancing: class HasBalancingOpts c_a3IqP
- Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a3GBG => Lens' c_a3GBG (Maybe (Map CommoditySymbol AmountStyle))
+ Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP (Maybe (Map CommoditySymbol AmountStyle))
- Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a3GBG => Lens' c_a3GBG Bool
+ Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP Bool
- Hledger.Data.Balancing: infer_balancing_costs :: HasBalancingOpts c_a3GBG => Lens' c_a3GBG Bool
+ Hledger.Data.Balancing: infer_balancing_costs :: HasBalancingOpts c_a3IqP => Lens' c_a3IqP Bool
- Hledger.Read.Common: aliases :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb [String]
+ Hledger.Read.Common: aliases :: HasInputOpts c_a3X2k => Lens' c_a3X2k [String]
- Hledger.Read.Common: anon__ :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: anon__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: auto__ :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: auto__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: balancingopts :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb BalancingOpts
+ Hledger.Read.Common: balancingopts :: HasInputOpts c_a3X2k => Lens' c_a3X2k BalancingOpts
- Hledger.Read.Common: class HasInputOpts c_a3Vdb
+ Hledger.Read.Common: class HasInputOpts c_a3X2k
- Hledger.Read.Common: defer :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: defer :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: forecast :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb (Maybe DateSpan)
+ Hledger.Read.Common: forecast :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe DateSpan)
- Hledger.Read.Common: infer_costs :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: infer_costs :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: infer_equity :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: infer_equity :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: inputOpts :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb InputOpts
+ Hledger.Read.Common: inputOpts :: HasInputOpts c_a3X2k => Lens' c_a3X2k InputOpts
- Hledger.Read.Common: ioDay :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Day
+ Hledger.Read.Common: ioDay :: HasInputOpts c_a3X2k => Lens' c_a3X2k Day
- Hledger.Read.Common: mformat :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb (Maybe StorageFormat)
+ Hledger.Read.Common: mformat :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe StorageFormat)
- Hledger.Read.Common: mrules_file :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb (Maybe FilePath)
+ Hledger.Read.Common: mrules_file :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe FilePath)
- Hledger.Read.Common: new__ :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: new__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: new_save :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: new_save :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: pivot :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb String
+ Hledger.Read.Common: pivot :: HasInputOpts c_a3X2k => Lens' c_a3X2k String
- Hledger.Read.Common: reportspan :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb DateSpan
+ Hledger.Read.Common: reportspan :: HasInputOpts c_a3X2k => Lens' c_a3X2k DateSpan
- Hledger.Read.Common: strict :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: strict :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.Common: verbose_tags :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.Common: verbose_tags :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb [String]
+ Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3X2k => Lens' c_a3X2k [String]
- Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb BalancingOpts
+ Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3X2k => Lens' c_a3X2k BalancingOpts
- Hledger.Read.InputOptions: class HasInputOpts c_a3Vdb
+ Hledger.Read.InputOptions: class HasInputOpts c_a3X2k
- Hledger.Read.InputOptions: defer :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: defer :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb (Maybe DateSpan)
+ Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe DateSpan)
- Hledger.Read.InputOptions: infer_costs :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: infer_costs :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb InputOpts
+ Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3X2k => Lens' c_a3X2k InputOpts
- Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Day
+ Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3X2k => Lens' c_a3X2k Day
- Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb (Maybe StorageFormat)
+ Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe StorageFormat)
- Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb (Maybe FilePath)
+ Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3X2k => Lens' c_a3X2k (Maybe FilePath)
- Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb String
+ Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3X2k => Lens' c_a3X2k String
- Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb DateSpan
+ Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3X2k => Lens' c_a3X2k DateSpan
- Hledger.Read.InputOptions: strict :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: strict :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Read.InputOptions: verbose_tags :: HasInputOpts c_a3Vdb => Lens' c_a3Vdb Bool
+ Hledger.Read.InputOptions: verbose_tags :: HasInputOpts c_a3X2k => Lens' c_a3X2k Bool
- Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Maybe ConversionOp -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> Bool -> [Text] -> Bool -> Bool -> Bool -> BalanceCalculation -> BalanceAccumulation -> Maybe Text -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> Layout -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Maybe ConversionOp -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> Bool -> [Text] -> Bool -> Bool -> SortSpec -> Bool -> BalanceCalculation -> BalanceAccumulation -> Maybe Text -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> Layout -> ReportOpts
- Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd AccountListMode
+ Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH AccountListMode
- Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd BalanceAccumulation
+ Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH BalanceAccumulation
- Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd BalanceCalculation
+ Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH BalanceCalculation
- Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd (Maybe Text)
+ Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe Text)
- Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a43Kd
+ Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a45GH
- Hledger.Reports.ReportOptions: class HasReportSpec c_a44NN
+ Hledger.Reports.ReportOptions: class HasReportSpec c_a46Nj
- Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd (Maybe ConversionOp)
+ Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe ConversionOp)
- Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd (Maybe Int)
+ Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe Int)
- Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Int
+ Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Int
- Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd StringFormat
+ Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH StringFormat
- Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Interval
+ Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Interval
- Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Layout
+ Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Layout
- Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd (Maybe NormalSign)
+ Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe NormalSign)
- Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Period
+ Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Period
- Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd [Text]
+ Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH [Text]
- Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd ReportOpts
+ Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH ReportOpts
- Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a44NN => Lens' c_a44NN ReportSpec
+ Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a46Nj => Lens' c_a46Nj ReportSpec
- Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a44NN => Lens' c_a44NN Day
+ Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a46Nj => Lens' c_a46Nj Day
- Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a44NN => Lens' c_a44NN Query
+ Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a46Nj => Lens' c_a46Nj Query
- Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a44NN => Lens' c_a44NN [QueryOpt]
+ Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a46Nj => Lens' c_a46Nj [QueryOpt]
- Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a44NN => Lens' c_a44NN ReportOpts
+ Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a46Nj => Lens' c_a46Nj ReportOpts
- Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd [Status]
+ Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH [Status]
- Hledger.Reports.ReportOptions: summary_only :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: summary_only :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd Bool
+ Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH Bool
- Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a43Kd => Lens' c_a43Kd (Maybe ValuationType)
+ Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a45GH => Lens' c_a45GH (Maybe ValuationType)

Files

CHANGES.md view
@@ -7,20 +7,34 @@  Breaking changes -Improvements- Fixes +Improvements      + --> Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. ++# 1.40 2024-09-09++Breaking changes++- Some constructors of the Interval type have been renamed for clarity.+- Hledger.Read.CsvUtils has moved to Hledger.Write.Csv. (Henning Thielemann)+- Tabular report rendering code has been added/reworked to allow new output formats and more reuse. (Henning Thielemann)++Improvements++- Added `journalDbg` debug output helper.++- Allow doclayout 0.5.  # 1.34 2024-06-01 
Hledger/Data/Amount.hs view
@@ -155,6 +155,7 @@   showMixedAmountWithZeroCommodity,   showMixedAmountB,   showMixedAmountLinesB,+  showMixedAmountLinesPartsB,   wbToText,   wbUnpack,   mixedAmountSetPrecision,@@ -763,7 +764,7 @@     negate = maNegate     (+)    = maPlus     (*)    = error "error, mixed amounts do not support multiplication" -- PARTIAL:-    abs    = error "error, mixed amounts do not support abs"+    abs    = mapMixedAmount (\amt -> amt { aquantity = abs (aquantity amt)})     signum = error "error, mixed amounts do not support signum"  -- | Calculate the key used to store an Amount within a MixedAmount.@@ -1120,10 +1121,17 @@ -- This returns the list of WideBuilders: one for each Amount, and padded/elided to the appropriate width. -- This does not honour displayOneLine; all amounts will be displayed as if displayOneLine were False. showMixedAmountLinesB :: AmountFormat -> MixedAmount -> [WideBuilder]-showMixedAmountLinesB opts@AmountFormat{displayMaxWidth=mmax,displayMinWidth=mmin} ma =-    map (adBuilder . pad) elided+showMixedAmountLinesB opts ma =+    map fst $ showMixedAmountLinesPartsB opts ma++-- | Like 'showMixedAmountLinesB' but also returns+-- the amounts associated with each text builder.+showMixedAmountLinesPartsB :: AmountFormat -> MixedAmount -> [(WideBuilder, Amount)]+showMixedAmountLinesPartsB opts@AmountFormat{displayMaxWidth=mmax,displayMinWidth=mmin} ma =+    zip (map (adBuilder . pad) elided) amts   where-    astrs = amtDisplayList (wbWidth sep) (showAmountB opts) . orderedAmounts opts $+    astrs = amtDisplayList (wbWidth sep) (showAmountB opts) amts+    amts = orderedAmounts opts $               if displayCost opts then ma else mixedAmountStripCosts ma     sep   = WideBuilder (TB.singleton '\n') 0     width = maximum $ map (wbWidth . adBuilder) elided
Hledger/Data/Dates.hs view
@@ -43,7 +43,7 @@   showEFDate,   showDateSpan,   showDateSpanDebug,-  showDateSpanMonthAbbrev,+  showDateSpanAbbrev,   elapsedSeconds,   prevday,   periodexprp,@@ -139,8 +139,8 @@  -- | Like showDateSpan, but show month spans as just the abbreviated month name -- in the current locale.-showDateSpanMonthAbbrev :: DateSpan -> Text-showDateSpanMonthAbbrev = showPeriodMonthAbbrev . dateSpanAsPeriod+showDateSpanAbbrev :: DateSpan -> Text+showDateSpanAbbrev = showPeriodAbbrev . dateSpanAsPeriod  -- | Get the current local date. getCurrentDay :: IO Day@@ -188,14 +188,20 @@ spansSpan spans = DateSpan (spanStartDate =<< headMay spans) (spanEndDate =<< lastMay spans)  -- | Split a DateSpan into consecutive exact spans of the specified Interval.--- If the first argument is true and the interval is Weeks, Months, Quarters or Years,--- the start date will be adjusted backward if needed to nearest natural interval boundary--- (a monday, first of month, first of quarter or first of year). -- If no interval is specified, the original span is returned. -- If the original span is the null date span, ie unbounded, the null date span is returned. -- If the original span is empty, eg if the end date is <= the start date, no spans are returned. ----- ==== Examples:+-- ==== Date adjustment+-- Some intervals respect the "adjust" flag (years, quarters, months, weeks, every Nth weekday+-- of month seem to be the ones that need it). This will move the start date earlier, if needed,+-- to the previous natural interval boundary (first of year, first of quarter, first of month,+-- monday, previous Nth weekday of month). Related: #1982 #2218+--+-- The end date is always moved later if needed to the next natural interval boundary,+-- so that the last period is the same length as the others.+--+-- ==== Examples -- >>> let t i y1 m1 d1 y2 m2 d2 = splitSpan True i $ DateSpan (Just $ Flex $ fromGregorian y1 m1 d1) (Just $ Flex $ fromGregorian y2 m2 d2) -- >>> t NoInterval 2008 01 01 2009 01 01 -- [DateSpan 2008]@@ -212,38 +218,38 @@ -- >>> t (Months 2) 2008 01 01 2008 04 01 -- [DateSpan 2008-01-01..2008-02-29,DateSpan 2008-03-01..2008-04-30] -- >>> t (Weeks 1) 2008 01 01 2008 01 15--- [DateSpan 2007-12-31W01,DateSpan 2008-01-07W02,DateSpan 2008-01-14W03]+-- [DateSpan 2007-W01,DateSpan 2008-W02,DateSpan 2008-W03] -- >>> t (Weeks 2) 2008 01 01 2008 01 15 -- [DateSpan 2007-12-31..2008-01-13,DateSpan 2008-01-14..2008-01-27]--- >>> t (DayOfMonth 2) 2008 01 01 2008 04 01--- [DateSpan 2007-12-02..2008-01-01,DateSpan 2008-01-02..2008-02-01,DateSpan 2008-02-02..2008-03-01,DateSpan 2008-03-02..2008-04-01]--- >>> t (WeekdayOfMonth 2 4) 2011 01 01 2011 02 15+-- >>> t (MonthDay 2) 2008 01 01 2008   04 01+-- [DateSpan 2008-01-02..2008-02-01,DateSpan 2008-02-02..2008-03-01,DateSpan 2008-03-02..2008-04-01]+-- >>> t (NthWeekdayOfMonth 2 4) 2011 01 01 2011 02 15 -- [DateSpan 2010-12-09..2011-01-12,DateSpan 2011-01-13..2011-02-09,DateSpan 2011-02-10..2011-03-09] -- >>> t (DaysOfWeek [2]) 2011 01 01 2011 01 15 -- [DateSpan 2010-12-28..2011-01-03,DateSpan 2011-01-04..2011-01-10,DateSpan 2011-01-11..2011-01-17]--- >>> t (DayOfYear 11 29) 2011 10 01 2011 10 15--- [DateSpan 2010-11-29..2011-11-28]--- >>> t (DayOfYear 11 29) 2011 12 01 2012 12 15--- [DateSpan 2011-11-29..2012-11-28,DateSpan 2012-11-29..2013-11-28]+-- >>> t (MonthAndDay 11 29) 2012 10 01 2013 10 15+-- [DateSpan 2012-11-29..2013-11-28] -- splitSpan :: Bool -> Interval -> DateSpan -> [DateSpan]-splitSpan _ _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]-splitSpan _ _ ds | isEmptySpan ds = []-splitSpan _ _ ds@(DateSpan (Just s) (Just e)) | s == e = [ds]-splitSpan _ NoInterval ds = [ds]-splitSpan _ (Days n) ds = splitspan id addDays n                    ds-splitSpan adjust (Weeks n)    ds = splitspan (if adjust then startofweek    else id) addDays (7*n)                ds-splitSpan adjust (Months n)   ds = splitspan (if adjust then startofmonth   else id) addGregorianMonthsClip n     ds-splitSpan adjust (Quarters n) ds = splitspan (if adjust then startofquarter else id) addGregorianMonthsClip (3*n) ds-splitSpan adjust (Years n)    ds = splitspan (if adjust then startofyear    else id) addGregorianYearsClip n      ds-splitSpan _ (DayOfMonth dom)  ds = splitspan (nthdayofmonthcontaining dom) (addGregorianMonthsToMonthday dom) 1 ds-splitSpan _ (DayOfYear m n)   ds = splitspan (nthdayofyearcontaining m n) (addGregorianYearsClip) 1 ds-splitSpan _ (WeekdayOfMonth n wd) ds = splitspan (nthweekdayofmonthcontaining n wd) advancemonths 1 ds+splitSpan _      _                        (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]+splitSpan _      _                        ds | isEmptySpan ds = []+splitSpan _      _                        ds@(DateSpan (Just s) (Just e)) | s == e = [ds]+splitSpan _      NoInterval               ds = [ds]+splitSpan _      (Days n)                 ds = splitspan id addDays n ds+splitSpan adjust (Weeks n)                ds = splitspan (if adjust then startofweek    else id) addDays                 (7*n) ds+splitSpan adjust (Months n)               ds = splitspan (if adjust then startofmonth   else id) addGregorianMonthsClip  n     ds+splitSpan adjust (Quarters n)             ds = splitspan (if adjust then startofquarter else id) addGregorianMonthsClip  (3*n) ds+splitSpan adjust (Years n)                ds = splitspan (if adjust then startofyear    else id) addGregorianYearsClip   n     ds+splitSpan adjust (NthWeekdayOfMonth n wd) ds = splitspan (if adjust then prevstart else nextstart) advancemonths          1     ds   where+    prevstart = prevNthWeekdayOfMonth n wd+    nextstart = nextNthWeekdayOfMonth n wd     advancemonths 0 = id-    advancemonths w = advancetonthweekday n wd . startofmonth . addGregorianMonthsClip w-splitSpan _ (DaysOfWeek [])         ds = [ds]-splitSpan _ (DaysOfWeek days@(n:_)) ds = spansFromBoundaries e bdrys+    advancemonths m = advanceToNthWeekday n wd . startofmonth . addGregorianMonthsClip m+splitSpan _      (MonthDay dom)           ds = splitspan (nextnthdayofmonth dom) (addGregorianMonthsToMonthday dom) 1 ds+splitSpan _      (MonthAndDay m d)        ds = splitspan (nextmonthandday m d)   (addGregorianYearsClip)            1 ds+splitSpan _      (DaysOfWeek [])          ds = [ds]+splitSpan _      (DaysOfWeek days@(n:_))  ds = spansFromBoundaries e bdrys   where     (s, e) = dateSpanSplitLimits (nthdayofweekcontaining n) nextday ds     bdrys = concatMap (flip map starts . addDays) [0,7..]@@ -260,16 +266,18 @@   in fromGregorian y m dom  -- Split the given span into exact spans using the provided helper functions:--- 1. The start function is applied to the span's start date to get the first sub-span's start date.--- 2. The addInterval function is used to calculate the subsequent spans' start dates,--- possibly with stride increased by the mult multiplier.--- It should adapt to spans of varying length, eg if splitting on "every 31st of month"--- addInterval should adjust to 28/29/30 in short months but return to 31 in the long months.+--+-- 1. The start function is used to adjust the provided span's start date to get the first sub-span's start date.+--+-- 2. The next function is used to calculate subsequent sub-spans' start dates, possibly with stride increased by a multiplier.+--    It should handle spans of varying length, eg when splitting on "every 31st of month",+--    it adjusts to 28/29/30 in short months but returns to 31 in the long months.+-- splitspan :: (Day -> Day) -> (Integer -> Day -> Day) -> Int -> DateSpan -> [DateSpan]-splitspan start addInterval mult ds = spansFromBoundaries e bdrys+splitspan start next mult ds = spansFromBoundaries e bdrys   where-    (s, e) = dateSpanSplitLimits start (addInterval (toInteger mult)) ds-    bdrys = mapM (addInterval . toInteger) [0,mult..] $ start s+    (s, e) = dateSpanSplitLimits start (next (toInteger mult)) ds+    bdrys = mapM (next . toInteger) [0,mult..] $ start s  -- | Fill in missing start/end dates for calculating 'splitSpan'. dateSpanSplitLimits :: (Day -> Day) -> (Day -> Day) -> DateSpan -> (Day, Day)@@ -490,8 +498,7 @@ fixSmartDateStrEither :: Day -> Text -> Either HledgerParseErrors Text fixSmartDateStrEither d = fmap showEFDate . fixSmartDateStrEither' d -fixSmartDateStrEither'-  :: Day -> Text -> Either HledgerParseErrors EFDay+fixSmartDateStrEither' :: Day -> Text -> Either HledgerParseErrors EFDay fixSmartDateStrEither' d s = case parsewith smartdateonly (T.toLower s) of                                Right sd -> Right $ fixSmartDate d sd                                Left e -> Left e@@ -621,7 +628,7 @@       firstmonthofquarter m2 = ((m2-1) `div` 3) * 3 + 1  thisyear = startofyear-prevyear = startofyear . addGregorianYearsClip (-1)+-- prevyear = startofyear . addGregorianYearsClip (-1) nextyear = startofyear . addGregorianYearsClip 1 startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day @@ -633,65 +640,50 @@     (DateSpan (Just start) _:_) -> fromEFDay start     _ -> d --- | 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.+-- | Find the next occurrence of the specified month and day of month, on or after the given date.+-- The month should be 1-12 and the day of month should be 1-31, or an error will be raised. ----- Examples: lets take 2017-11-22. Year-long intervals covering it that--- starts before Nov 22 will start in 2017. However--- intervals that start after Nov 23rd should start in 2016: -- >>> let wed22nd = fromGregorian 2017 11 22--- >>> nthdayofyearcontaining 11 21 wed22nd--- 2017-11-21--- >>> nthdayofyearcontaining 11 22 wed22nd+-- >>> nextmonthandday 11 21 wed22nd+-- 2018-11-21+-- >>> nextmonthandday 11 22 wed22nd -- 2017-11-22--- >>> nthdayofyearcontaining 11 23 wed22nd--- 2016-11-23--- >>> nthdayofyearcontaining 12 02 wed22nd--- 2016-12-02--- >>> nthdayofyearcontaining 12 31 wed22nd--- 2016-12-31--- >>> nthdayofyearcontaining 1 1 wed22nd--- 2017-01-01-nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day-nthdayofyearcontaining m mdy date+-- >>> nextmonthandday 11 23 wed22nd+-- 2017-11-23+nextmonthandday :: Month -> MonthDay -> Day -> Day+nextmonthandday m n date   -- PARTIAL:-  | not (validMonth m)  = error' $ "nthdayofyearcontaining: invalid month "++show m-  | not (validDay   mdy) = error' $ "nthdayofyearcontaining: invalid day "  ++show mdy-  | mmddOfSameYear <= date = mmddOfSameYear-  | otherwise = mmddOfPrevYear-  where mmddOfSameYear = addDays (toInteger mdy-1) $ applyN (m-1) nextmonth s-        mmddOfPrevYear = addDays (toInteger mdy-1) $ applyN (m-1) nextmonth $ prevyear s-        s = startofyear date+  | not (validMonth m) = error' $ "nextmonthandday: month should be 1..12, not "++show m+  | not (validDay   n) = error' $ "nextmonthandday: day should be 1..31, not "  ++show n+  | mdthisyear >= date = mdthisyear+  | otherwise          = mdnextyear+  where+    s = startofyear date+    advancetomonth = applyN (m-1) nextmonth+    advancetoday = addDays (toInteger n-1)+    mdthisyear = advancetoday $ advancetomonth s+    mdnextyear = advancetoday $ advancetomonth $ nextyear s --- | For a given date d find the month-long period that starts on day n of a month--- that includes d. (It will begin on day n or either d's month or the previous month.)--- The given day of month should be in the range 1-31, or an error will be raised.+-- | Find the next occurrence of the specified day of month, on or after the given date.+-- The day of month should be 1-31, or an error will be raised. ----- Examples: lets take 2017-11-22. Month-long intervals covering it that--- start on 1st-22nd of month will start in Nov. However--- intervals that start on 23rd-30th of month should start in Oct: -- >>> let wed22nd = fromGregorian 2017 11 22--- >>> nthdayofmonthcontaining 1 wed22nd--- 2017-11-01--- >>> nthdayofmonthcontaining 12 wed22nd--- 2017-11-12--- >>> nthdayofmonthcontaining 22 wed22nd+-- >>> nextnthdayofmonth 21 wed22nd+-- 2017-12-21+-- >>> nextnthdayofmonth 22 wed22nd -- 2017-11-22--- >>> nthdayofmonthcontaining 23 wed22nd--- 2017-10-23--- >>> nthdayofmonthcontaining 30 wed22nd--- 2017-10-30-nthdayofmonthcontaining :: MonthDay -> Day -> Day-nthdayofmonthcontaining mdy date+-- >>> nextnthdayofmonth 23 wed22nd+-- 2017-11-23+nextnthdayofmonth :: MonthDay -> Day -> Day+nextnthdayofmonth n date   -- PARTIAL:-  | not (validDay mdy) = error' $ "nthdayofmonthcontaining: invalid day "  ++show mdy-  | nthOfSameMonth <= date = nthOfSameMonth-  | otherwise = nthOfPrevMonth-  where nthOfSameMonth = nthdayofmonth mdy s-        nthOfPrevMonth = nthdayofmonth mdy $ prevmonth s-        s = startofmonth date+  | not (validDay n)       = error' $ "nextnthdayofmonth: day should be 1..31, not "  ++show n+  | nthofthismonth >= date = nthofthismonth+  | otherwise              = nthofnextmonth+  where+    s = startofmonth date+    nthofthismonth = nthdayofmonth n s+    nthofnextmonth = nthdayofmonth n $ nextmonth s  -- | For given date d find week-long interval that starts on nth day of week -- and covers it.@@ -717,37 +709,66 @@           nthOfPrevWeek = addDays (toInteger n-1) $ prevweek s           s = startofweek d --- | For given date d find month-long interval that starts on nth weekday of month--- and covers it.+-- -- | Find the next occurrence of some weekday, on or after the given date d.+-- --+-- -- >>> let wed22nd = fromGregorian 2017 11 22+-- -- >>> nextnthdayofweek 1 wed22nd+-- -- 2017-11-20+-- -- >>> nextnthdayofweek 2 wed22nd+-- -- 2017-11-21+-- -- >>> nextnthdayofweek 3 wed22nd+-- -- 2017-11-22+-- -- >>> nextnthdayofweek 4 wed22nd+-- -- 2017-11-16+-- -- >>> nextnthdayofweek 5 wed22nd+-- -- 2017-11-17+-- nextdayofweek :: WeekDay -> Day -> Day+-- nextdayofweek n d | nthOfSameWeek <= d = nthOfSameWeek+--                            | otherwise = nthOfPrevWeek+--     where nthOfSameWeek = addDays (toInteger n-1) s+--           nthOfPrevWeek = addDays (toInteger n-1) $ prevweek s+--           s = startofweek d++-- | Find the next occurrence of some nth weekday of a month, on or after the given date d. ----- Examples: 2017-11-22 is 3rd Wed of Nov. Month-long intervals that cover it and--- start on 1st-4th Wed will start in Nov. However--- intervals that start on 4th Thu or Fri or later should start in Oct: -- >>> let wed22nd = fromGregorian 2017 11 22--- >>> nthweekdayofmonthcontaining 1 3 wed22nd--- 2017-11-01--- >>> nthweekdayofmonthcontaining 3 2 wed22nd--- 2017-11-21--- >>> nthweekdayofmonthcontaining 4 3 wed22nd+-- >>> nextNthWeekdayOfMonth 3 3 wed22nd  -- next third wednesday+-- 2017-12-20+-- >>> nextNthWeekdayOfMonth 4 3 wed22nd  -- next fourth wednesday -- 2017-11-22--- >>> nthweekdayofmonthcontaining 4 4 wed22nd--- 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+-- >>> nextNthWeekdayOfMonth 5 3 wed22nd  -- next fifth wednesday+-- 2017-11-29+nextNthWeekdayOfMonth :: Int -> WeekDay -> Day -> Day+nextNthWeekdayOfMonth n wd d+  | nthweekdaythismonth >= d = nthweekdaythismonth+  | otherwise                = nthweekdaynextmonth+  where+    nthweekdaythismonth = advanceToNthWeekday n wd $ startofmonth d+    nthweekdaynextmonth = advanceToNthWeekday n wd $ nextmonth d --- | Advance to nth weekday wd after given start day s+-- | Find the previous occurrence of some nth weekday of a month, on or before the given date d.+--+-- >>> let wed22nd = fromGregorian 2017 11 22+-- >>> prevNthWeekdayOfMonth 4 3 wed22nd+-- 2017-11-22+-- >>> prevNthWeekdayOfMonth 5 2 wed22nd+-- 2017-10-31+prevNthWeekdayOfMonth :: Int -> WeekDay -> Day -> Day+prevNthWeekdayOfMonth n wd d+  | nthweekdaythismonth <= d = nthweekdaythismonth+  | otherwise                = nthweekdayprevmonth+  where+    nthweekdaythismonth = advanceToNthWeekday n wd $ startofmonth d+    nthweekdayprevmonth = advanceToNthWeekday n wd $ prevmonth d++-- | Advance to the nth occurrence of the given weekday, on or after the given date. -- Can call error.-advancetonthweekday :: Int -> WeekDay -> Day -> Day-advancetonthweekday n wd s =+advanceToNthWeekday :: Int -> WeekDay -> Day -> Day+advanceToNthWeekday n wd s =   -- PARTIAL:   maybe err (addWeeks (n-1)) $ firstMatch (>=s) $ iterate (addWeeks 1) $ firstweekday s   where-    err = error' "advancetonthweekday: should not happen"+    err = error' "advanceToNthWeekday: should not happen"     addWeeks k = addDays (7 * toInteger k)     firstMatch p = headMay . dropWhile (not . p)     firstweekday = addDays (toInteger wd-1) . startofweek@@ -966,41 +987,41 @@ -- >>> p "every week to 2009" -- Right (Weeks 1,DateSpan ..2008-12-31) -- >>> p "every 2nd day of month"--- Right (DayOfMonth 2,DateSpan ..)+-- Right (MonthDay 2,DateSpan ..) -- >>> p "every 2nd day"--- Right (DayOfMonth 2,DateSpan ..)+-- Right (MonthDay 2,DateSpan ..) -- >>> p "every 2nd day 2009.."--- Right (DayOfMonth 2,DateSpan 2009-01-01..)+-- Right (MonthDay 2,DateSpan 2009-01-01..) -- >>> p "every 2nd day 2009-"--- Right (DayOfMonth 2,DateSpan 2009-01-01..)+-- Right (MonthDay 2,DateSpan 2009-01-01..) -- >>> p "every 29th Nov"--- Right (DayOfYear 11 29,DateSpan ..)+-- Right (MonthAndDay 11 29,DateSpan ..) -- >>> p "every 29th nov ..2009"--- Right (DayOfYear 11 29,DateSpan ..2008-12-31)+-- Right (MonthAndDay 11 29,DateSpan ..2008-12-31) -- >>> p "every nov 29th"--- Right (DayOfYear 11 29,DateSpan ..)+-- Right (MonthAndDay 11 29,DateSpan ..) -- >>> p "every Nov 29th 2009.."--- Right (DayOfYear 11 29,DateSpan 2009-01-01..)+-- Right (MonthAndDay 11 29,DateSpan 2009-01-01..) -- >>> p "every 11/29 from 2009"--- Right (DayOfYear 11 29,DateSpan 2009-01-01..)+-- Right (MonthAndDay 11 29,DateSpan 2009-01-01..) -- >>> p "every 11/29 since 2009"--- Right (DayOfYear 11 29,DateSpan 2009-01-01..)+-- Right (MonthAndDay 11 29,DateSpan 2009-01-01..) -- >>> p "every 2nd Thursday of month to 2009"--- Right (WeekdayOfMonth 2 4,DateSpan ..2008-12-31)+-- Right (NthWeekdayOfMonth 2 4,DateSpan ..2008-12-31) -- >>> p "every 1st monday of month to 2009"--- Right (WeekdayOfMonth 1 1,DateSpan ..2008-12-31)+-- Right (NthWeekdayOfMonth 1 1,DateSpan ..2008-12-31) -- >>> p "every tue" -- Right (DaysOfWeek [2],DateSpan ..) -- >>> p "every 2nd day of week" -- Right (DaysOfWeek [2],DateSpan ..) -- >>> p "every 2nd day of month"--- Right (DayOfMonth 2,DateSpan ..)+-- Right (MonthDay 2,DateSpan ..) -- >>> p "every 2nd day"--- Right (DayOfMonth 2,DateSpan ..)+-- Right (MonthDay 2,DateSpan ..) -- >>> p "every 2nd day 2009.."--- Right (DayOfMonth 2,DateSpan 2009-01-01..)+-- Right (MonthDay 2,DateSpan 2009-01-01..) -- >>> p "every 2nd day of month 2009.."--- Right (DayOfMonth 2,DateSpan 2009-01-01..)+-- Right (MonthDay 2,DateSpan 2009-01-01..) periodexprp :: Day -> TextParser m (Interval, DateSpan) periodexprp rdate = do   skipNonNewlineSpaces@@ -1029,9 +1050,9 @@     , Months 2 <$ string' "bimonthly"     , string' "every" *> skipNonNewlineSpaces *> choice'         [ DaysOfWeek . pure <$> (nth <* skipNonNewlineSpaces <* string' "day" <* of_ "week")-        , DayOfMonth <$> (nth <* skipNonNewlineSpaces <* string' "day" <* optOf_ "month")-        , liftA2 WeekdayOfMonth nth $ skipNonNewlineSpaces *> weekday <* optOf_ "month"-        , uncurry DayOfYear <$> (md <* optOf_ "year")+        , MonthDay <$> (nth <* skipNonNewlineSpaces <* string' "day" <* optOf_ "month")+        , liftA2 NthWeekdayOfMonth nth $ skipNonNewlineSpaces *> weekday <* optOf_ "month"+        , uncurry MonthAndDay <$> (md <* optOf_ "year")         , DaysOfWeek <$> weekdaysp         , DaysOfWeek [1..5] <$ string' "weekday"         , DaysOfWeek [6..7] <$ string' "weekendday"@@ -1050,8 +1071,8 @@     optOf_ period = optional . try $ of_ period      nth = decimal <* choice (map string' ["st","nd","rd","th"])-    d_o_y = runPermutation $ liftA2 DayOfYear (toPermutation $ (month <|> mon) <* skipNonNewlineSpaces)-                                              (toPermutation $ nth <* skipNonNewlineSpaces)+    d_o_y = runPermutation $ liftA2 MonthAndDay (toPermutation $ (month <|> mon) <* skipNonNewlineSpaces)+                                                (toPermutation $ nth <* skipNonNewlineSpaces)      -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".     tryinterval :: Text -> Text -> (Int -> Interval) -> TextParser m Interval
Hledger/Data/Journal.hs view
@@ -21,6 +21,7 @@   addTransactionModifier,   addPeriodicTransaction,   addTransaction,+  journalDbg,   journalInferMarketPricesFromTransactions,   journalInferCommodityStyles,   journalStyleAmounts,@@ -134,7 +135,6 @@ import Data.Tree (Tree(..), flatten) import Text.Printf (printf) import Text.Megaparsec (ParsecT)-import Text.Megaparsec.Custom (FinalParseError)  import Hledger.Utils import Hledger.Data.Types@@ -182,17 +182,39 @@              -- ++ (show $ journalTransactions l)              where accounts = filter (/= "root") $ flatten $ journalAccountNameTree j --- showJournalDebug j = unlines [---                       show j---                      ,show (jtxns j)---                      ,show (jtxnmodifiers j)---                      ,show (jperiodictxns j)---                      ,show $ jparsetimeclockentries j---                      ,show $ jpricedirectives j---                      ,show $ jfinalcommentlines j---                      ,show $ jparsestate j---                      ,show $ map fst $ jfiles j---                      ]+journalDbg j@Journal{..} = chomp $ unlines $+  ("Journal " ++ takeFileName (journalFilePath j)++":") :  --  ++ " {"+  map (" "<>) [+   "jparsedefaultyear: "         <> shw jparsedefaultyear+  ,"jparsedefaultcommodity: "    <> shw jparsedefaultcommodity+  ,"jparsedecimalmark: "         <> shw jparsedecimalmark+  ,"jparseparentaccounts: "      <> shw jparseparentaccounts+  ,"jparsealiases: "             <> shw jparsealiases+  -- ,"jparsetimeclockentries: " <> shw jparsetimeclockentries+  ,"jincludefilestack: "         <> shw jincludefilestack+  ,"jdeclaredpayees: "           <> shw jdeclaredpayees+  ,"jdeclaredtags: "             <> shw jdeclaredtags+  ,"jdeclaredaccounts: "         <> shw jdeclaredaccounts+  ,"jdeclaredaccounttags: "      <> shw jdeclaredaccounttags+  ,"jdeclaredaccounttypes: "     <> shw jdeclaredaccounttypes+  ,"jaccounttypes: "             <> shw jaccounttypes+  ,"jglobalcommoditystyles: "    <> shw jglobalcommoditystyles+  ,"jcommodities: "              <> shw jcommodities+  ,"jinferredcommodities: "      <> shw jinferredcommodities+  ,"jpricedirectives: "          <> shw jpricedirectives+  ,"jinferredmarketprices: "     <> shw jinferredmarketprices+  ,"jtxnmodifiers: "             <> shw jtxnmodifiers+  -- ,"jperiodictxns: "          <> shw jperiodictxns+  ,"jtxns: "                     <> shw jtxns+  ,"jfinalcommentlines: "        <> shw jfinalcommentlines+  ,"jfiles: "                    <> shw jfiles+  ,"jlastreadtime: "             <> shw jlastreadtime+  ]+  -- ++ ["}"]+  where+    shw :: Show a => a -> String+    shw = show+    -- shw = pshow  -- The semigroup instance for Journal is useful for two situations. --@@ -232,12 +254,32 @@     ,jdeclaredpayees            = jdeclaredpayees            j1 <> jdeclaredpayees            j2     ,jdeclaredtags              = jdeclaredtags              j1 <> jdeclaredtags              j2     ,jdeclaredaccounts          = jdeclaredaccounts          j1 <> jdeclaredaccounts          j2-    ,jdeclaredaccounttags       = jdeclaredaccounttags       j1 <> jdeclaredaccounttags       j2-    ,jdeclaredaccounttypes      = jdeclaredaccounttypes      j1 <> jdeclaredaccounttypes      j2-    ,jaccounttypes              = jaccounttypes              j1 <> jaccounttypes              j2-    ,jglobalcommoditystyles     = jglobalcommoditystyles     j1 <> jglobalcommoditystyles     j2-    ,jcommodities               = jcommodities               j1 <> jcommodities               j2-    ,jinferredcommodities       = jinferredcommodities       j1 <> jinferredcommodities       j2+    --+    -- The next six fields are Maps, which need to be merged carefully for correct semantics,+    -- especially the first two, which have list values. There may still be room for improvement here.+    --+    -- ,jdeclaredaccounttags   :: M.Map AccountName [Tag]+    -- jdeclaredaccounttags can have multiple duplicated/conflicting values for an account's tag.+    ,jdeclaredaccounttags       = M.unionWith (<>) (jdeclaredaccounttags j1) (jdeclaredaccounttags j2)+    --+    -- ,jdeclaredaccounttypes  :: M.Map AccountType [AccountName]+    -- jdeclaredaccounttypes can have multiple duplicated/conflicting values for an account's type.+    ,jdeclaredaccounttypes      = M.unionWith (<>) (jdeclaredaccounttypes j1) (jdeclaredaccounttypes j2)+    --+    -- ,jaccounttypes          :: M.Map AccountName AccountType+    -- jaccounttypes has a single type for any given account. When it had multiple type declarations, the last/rightmost wins.+    ,jaccounttypes              = M.unionWith (const id) (jaccounttypes j1) (jaccounttypes j2)+    --+    -- ,jglobalcommoditystyles :: M.Map CommoditySymbol AmountStyle+    ,jglobalcommoditystyles     = (<>) (jglobalcommoditystyles j1) (jglobalcommoditystyles j2)+    --+    -- ,jcommodities           :: M.Map CommoditySymbol Commodity+    ,jcommodities               = (<>) (jcommodities j1) (jcommodities j2)+    --+    -- ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle+    ,jinferredcommodities       = (<>) (jinferredcommodities j1) (jinferredcommodities j2)+    --+    --     ,jpricedirectives           = jpricedirectives           j1 <> jpricedirectives           j2     ,jinferredmarketprices      = jinferredmarketprices      j1 <> jinferredmarketprices      j2     ,jtxnmodifiers              = jtxnmodifiers              j1 <> jtxnmodifiers              j2
Hledger/Data/JournalChecks.hs view
@@ -289,18 +289,19 @@       "%s\n",       "The recentassertions check is enabled, so accounts with balance assertions must",       "have a balance assertion within %d days of their latest posting.",-      "In account \"%s\", this posting is %d days later",-      "than the last balance assertion, which was on %s.",       "",+      "In %s,",+      "this posting is %d days later than the balance assertion on %s.",+      "",       "Consider adding a more recent balance assertion for this account. Eg:",       "",-      "%s\n    %s    %s0 = %s0  ; (adjust asserted amount)"+      "%s\n    %s    %s0 = %sAMT"       ])     f     l     (textChomp ex)     maxlag-    acct+    (bold' $ T.unpack acct)     lag     (showDate latestassertdate)     (show today)
Hledger/Data/Period.hs view
@@ -15,7 +15,7 @@   ,isStandardPeriod   ,periodTextWidth   ,showPeriod-  ,showPeriodMonthAbbrev+  ,showPeriodAbbrev   ,periodStart   ,periodEnd   ,periodNext@@ -173,10 +173,10 @@ -- | Render a period as a compact display string suitable for user output. -- -- >>> showPeriod (WeekPeriod (fromGregorian 2016 7 25))--- "2016-07-25W30"+-- "2016-W30" showPeriod :: Period -> Text showPeriod (DayPeriod b)       = T.pack $ formatTime defaultTimeLocale "%F" b              -- DATE-showPeriod (WeekPeriod b)      = T.pack $ formatTime defaultTimeLocale "%FW%V" b           -- STARTDATEWYEARWEEK+showPeriod (WeekPeriod b)      = T.pack $ formatTime defaultTimeLocale "%0Y-W%V" b         -- YYYY-Www showPeriod (MonthPeriod y m)   = T.pack $ printf "%04d-%02d" y m                           -- YYYY-MM showPeriod (QuarterPeriod y q) = T.pack $ printf "%04dQ%d" y q                             -- YYYYQN showPeriod (YearPeriod y)      = T.pack $ printf "%04d" y                                  -- YYYY@@ -186,13 +186,16 @@ showPeriod (PeriodTo e)        = T.pack $ formatTime defaultTimeLocale "..%F" (addDays (-1) e)    -- ..INCLUSIVEENDDATE showPeriod PeriodAll           = ".." --- | Like showPeriod, but if it's a month period show just--- the 3 letter month name abbreviation for the current locale.-showPeriodMonthAbbrev :: Period -> Text-showPeriodMonthAbbrev (MonthPeriod _ m)                           -- Jan+-- | Like showPeriod, but if it's a month or week period show+-- an abbreviated form.+-- >>> showPeriodAbbrev (WeekPeriod (fromGregorian 2016 7 25))+-- "W30"+showPeriodAbbrev :: Period -> Text+showPeriodAbbrev (MonthPeriod _ m)                                              -- Jan   | m > 0 && m <= length monthnames = T.pack . snd $ monthnames !! (m-1)   where monthnames = months defaultTimeLocale-showPeriodMonthAbbrev p = showPeriod p+showPeriodAbbrev (WeekPeriod b) = T.pack $ formatTime defaultTimeLocale "W%V" b -- Www+showPeriodAbbrev p = showPeriod p  periodStart :: Period -> Maybe Day periodStart p = fromEFDay <$> mb
Hledger/Data/PeriodicTransaction.hs view
@@ -114,10 +114,6 @@ -- <BLANKLINE> -- -- >>> _ptgen "every 2nd day of month from 2017/02 to 2017/04"--- 2017-01-02---     ; generated-transaction: ~ every 2nd day of month from 2017/02 to 2017/04---     a           $1.00--- <BLANKLINE> -- 2017-02-02 --     ; generated-transaction: ~ every 2nd day of month from 2017/02 to 2017/04 --     a           $1.00@@ -128,10 +124,6 @@ -- <BLANKLINE> -- -- >>> _ptgen "every 30th day of month from 2017/1 to 2017/5"--- 2016-12-30---     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5---     a           $1.00--- <BLANKLINE> -- 2017-01-30 --     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5 --     a           $1.00@@ -150,10 +142,6 @@ -- <BLANKLINE> -- -- >>> _ptgen "every 2nd Thursday of month from 2017/1 to 2017/4"--- 2016-12-08---     ; generated-transaction: ~ every 2nd Thursday of month from 2017/1 to 2017/4---     a           $1.00--- <BLANKLINE> -- 2017-01-12 --     ; generated-transaction: ~ every 2nd Thursday of month from 2017/1 to 2017/4 --     a           $1.00@@ -168,10 +156,6 @@ -- <BLANKLINE> -- -- >>> _ptgen "every nov 29th from 2017 to 2019"--- 2016-11-29---     ; generated-transaction: ~ every nov 29th from 2017 to 2019---     a           $1.00--- <BLANKLINE> -- 2017-11-29 --     ; generated-transaction: ~ every nov 29th from 2017 to 2019 --     a           $1.00
Hledger/Data/Types.hs view
@@ -116,7 +116,7 @@  instance Default DateSpan where def = DateSpan Nothing Nothing --- Typical report periods (spans of time), both finite and open-ended.+-- Some common report subperiods, both finite and open-ended. -- A higher-level abstraction than DateSpan. data Period =     DayPeriod Day@@ -132,16 +132,8 @@  instance Default Period where def = PeriodAll ----- Typical report period/subperiod durations, from a day to a year.---data Duration =---    DayLong---   WeekLong---   MonthLong---   QuarterLong---   YearLong---  deriving (Eq,Ord,Show,Generic)---- Ways in which a period can be divided into subperiods.+-- All the kinds of report interval allowed in a period expression+-- (to generate periodic reports or periodic transactions). data Interval =     NoInterval   | Days Int@@ -149,13 +141,10 @@   | Months Int   | Quarters Int   | Years Int-  | DayOfMonth Int-  | WeekdayOfMonth Int Int-  | DaysOfWeek [Int]-  | DayOfYear Int Int -- Month, Day-  -- WeekOfYear Int-  -- MonthOfYear Int-  -- QuarterOfYear Int+  | NthWeekdayOfMonth Int Int  -- n,              weekday 1-7+  | MonthDay Int               -- 1-31+  | MonthAndDay Int Int        -- month 1-12,     monthday 1-31+  | DaysOfWeek [Int]           -- [weekday 1-7]   deriving (Eq,Show,Ord,Generic)  instance Default Interval where def = NoInterval
Hledger/Read/Common.hs view
@@ -96,6 +96,7 @@   isSameLineCommentStart,   multilinecommentp,   emptyorcommentlinep,+  emptyorcommentlinep2,   followingcommentp,   transactioncommentp,   commenttagsp,@@ -150,9 +151,6 @@ import Text.Megaparsec import Text.Megaparsec.Char (char, char', digitChar, newline, string) import Text.Megaparsec.Char.Lexer (decimal)-import Text.Megaparsec.Custom-  (FinalParseError, attachSource, finalErrorBundlePretty, parseErrorAt, parseErrorAtRegion)--- import Text.Megaparsec.Debug (dbg)  -- from megaparsec 9.3+  import Hledger.Data import Hledger.Query (Query(..), filterQuery, parseQueryTerm, queryEndDate, queryStartDate, queryIsDate, simplifyQuery)@@ -211,7 +209,7 @@     in definputopts{        -- files_             = listofstringopt "file" rawopts        mformat_           = Nothing-      ,mrules_file_       = maybestringopt "rules-file" rawopts+      ,mrules_file_       = maybestringopt "rules" rawopts       ,aliases_           = listofstringopt "alias" rawopts       ,anon_              = boolopt "obfuscate" rawopts       ,new_               = boolopt "new" rawopts@@ -361,7 +359,7 @@        -- >>= Right . dbg0With (concatMap (T.unpack.showTransaction).jtxns)        -- >>= \j -> deepseq (concatMap (T.unpack.showTransaction).jtxns $ j) (return j)       <&> dbg9With (lbl "amounts after styling, forecasting, auto-posting".showJournalAmountsDebug)-      >>= (\j -> if checkordereddates then journalCheckOrdereddates j <&> const j else Right j)  -- check ordereddates before assertions. The outer parentheses are needed.+      >>= (\j -> if checkordereddates then journalCheckOrdereddates j $> j else Right j)  -- check ordereddates before assertions. The outer parentheses are needed.       >>= journalBalanceTransactions balancingopts_{ignore_assertions_=not checkassertions}  -- infer balance assignments and missing amounts, and maybe check balance assertions.       <&> dbg9With (lbl "amounts after transaction-balancing".showJournalAmountsDebug)       -- <&> dbg9With (("journalFinalise amounts after styling, forecasting, auto postings, transaction balancing"<>).showJournalAmountsDebug)@@ -409,6 +407,10 @@ getYear :: JournalParser m (Maybe Year) getYear = fmap jparsedefaultyear get +dp :: String -> TextParser m ()+dp = const $ return ()  -- no-op+-- dp = dbgparse 1  -- trace parse state at this --debug level+ -- | Get the decimal mark that has been specified for parsing, if any -- (eg by the CSV decimal-mark rule, or possibly a future journal directive). -- Return it as an AmountStyle that amount parsers can use.@@ -1262,9 +1264,10 @@  -- | A blank or comment line in journal format: a line that's empty or -- containing only whitespace or whose first non-whitespace character--- is semicolon, hash, or star.+-- is semicolon, hash, or star. See also emptyorcommentlinep2. emptyorcommentlinep :: TextParser m () emptyorcommentlinep = do+  dp "emptyorcommentlinep"   skipNonNewlineSpaces   skiplinecommentp <|> void newline   where@@ -1276,6 +1279,19 @@       pure ()  {-# INLINABLE emptyorcommentlinep #-}++-- | A newer comment line parser.+-- Parses a line which is empty, all blanks, or whose first non-blank character is one of those provided.+emptyorcommentlinep2 :: [Char] -> TextParser m ()+emptyorcommentlinep2 cs =+  label ("empty line or comment line beginning with "++cs) $ do+    dp "emptyorcommentlinep2"+    skipNonNewlineSpaces+    void newline <|> void commentp+    where+      commentp = do+        choice (map (some.char) cs)+        takeWhileP Nothing (/='\n') <* newline  -- | Is this a character that, as the first non-whitespace on a line, -- starts a comment line ?
− Hledger/Read/CsvUtils.hs
@@ -1,56 +0,0 @@---- * -*- outline-regexp:"--- \\*"; -*----- ** doc-{-|--CSV utilities.---}----- ** language-{-# LANGUAGE OverloadedStrings    #-}----- ** exports-module Hledger.Read.CsvUtils (-  CSV, CsvRecord, CsvValue,-  printCSV,-  printTSV,-  -- * Tests-  tests_CsvUtils,-)-where----- ** imports-import Prelude hiding (Applicative(..))-import Data.List (intersperse)-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Builder as TB--import Hledger.Utils----- ** doctest setup--- $setup--- >>> :set -XOverloadedStrings--type CSV       = [CsvRecord]-type CsvRecord = [CsvValue]-type CsvValue  = Text--printCSV :: [CsvRecord] -> TL.Text-printCSV = TB.toLazyText . unlinesB . map printRecord-    where printRecord = foldMap TB.fromText . intersperse "," . map printField-          printField = wrap "\"" "\"" . T.replace "\"" "\"\""--printTSV :: [CsvRecord] -> TL.Text-printTSV = TB.toLazyText . unlinesB . map printRecord-    where printRecord = foldMap TB.fromText . intersperse "\t" . map printField-          printField = T.map replaceWhitespace-          replaceWhitespace c | c `elem` ['\t', '\n', '\r'] = ' '-          replaceWhitespace c = c----- ** tests--tests_CsvUtils :: TestTree-tests_CsvUtils = testGroup "CsvUtils" [-  ]
Hledger/Read/JournalReader.hs view
@@ -91,7 +91,6 @@ import Safe 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)
Hledger/Read/RulesReader.hs view
@@ -72,13 +72,12 @@ import Data.Foldable (asum, toList) import Text.Megaparsec hiding (match, parse) import Text.Megaparsec.Char (char, newline, string, digitChar)-import Text.Megaparsec.Custom (parseErrorAt) import Text.Printf (printf)  import Hledger.Data import Hledger.Utils import Hledger.Read.Common (aliasesFromOpts, Reader(..), InputOpts(..), amountp, statusp, journalFinalise, accountnamep, commenttagsp )-import Hledger.Read.CsvUtils+import Hledger.Write.Csv import System.Directory (doesFileExist, getHomeDirectory) import Data.Either (fromRight) @@ -113,7 +112,7 @@ -- file's directory. When a glob pattern matches multiple files, the alphabetically -- last is used. (Eg in case of multiple numbered downloads, the highest-numbered -- will be used.)--- The provided text, or a --rules-file option, are ignored by this reader.+-- The provided text, or a --rules option, are ignored by this reader. -- Balance assertions are not checked. parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal parse iopts f _ = do@@ -368,7 +367,7 @@  QUOTED-FIELD-NAME: " (any CHAR except double-quote)+ " -BARE-FIELD-NAME: any CHAR except space, tab, #, ;+BARE-FIELD-NAME: (any CHAR except space, tab, #, ;)+  FIELD-ASSIGNMENT: JOURNAL-FIELD ASSIGNMENT-SEPARATOR FIELD-VALUE @@ -884,7 +883,7 @@ -- 4. Return the transactions as a Journal. -- readJournalFromCsv :: Maybe (Either CsvRules FilePath) -> FilePath -> Text -> Maybe SepFormat -> ExceptT String IO Journal-readJournalFromCsv Nothing "-" _ _ = throwError "please use --rules-file when reading CSV from stdin"+readJournalFromCsv Nothing "-" _ _ = throwError "please use --rules when reading CSV from stdin" readJournalFromCsv merulesfile csvfile csvtext sep = do     -- for now, correctness is the priority here, efficiency not so much 
Hledger/Read/TimedotReader.hs view
@@ -50,7 +50,7 @@ import Text.Megaparsec.Char  import Hledger.Data-import Hledger.Read.Common hiding (emptyorcommentlinep)+import Hledger.Read.Common import Hledger.Utils import Data.Decimal (roundTo) import Data.Functor ((<&>))@@ -80,12 +80,10 @@  --- ** utilities -traceparse, traceparse' :: String -> TextParser m ()-traceparse  = const $ return ()-traceparse' = const $ return ()--- for debugging:--- traceparse  s = traceParse (s++"?")--- traceparse' s = trace s $ return ()+-- Trace parser state above a certain --debug level ?+tracelevel = 9+dp :: String -> JournalParser m ()+dp = if tracelevel >= 0 then lift . dbgparse tracelevel else const $ return ()  --- ** parsers {-@@ -113,9 +111,8 @@  preamblep :: JournalParser m () preamblep = do-  lift $ traceparse "preamblep"-  many $ notFollowedBy datelinep >> (lift $ emptyorcommentlinep "#;*")-  lift $ traceparse' "preamblep"+  dp "preamblep"+  void $ many $ notFollowedBy datelinep >> (lift $ emptyorcommentlinep2 "#;*")  -- | Parse timedot day entries to multi-posting time transactions for that day. -- @@@ -126,11 +123,13 @@ -- @ dayp :: JournalParser m () dayp = label "timedot day entry" $ do-  lift $ traceparse "dayp"+  dp "dayp"   pos <- getSourcePos   (date,desc,comment,tags) <- datelinep+  dp "dayp1"   commentlinesp-  ps <- (many $ timedotentryp <* commentlinesp) <&> concat+  dp "dayp2"+  ps <- (many $ dp "dayp3" >> timedotentryp <* commentlinesp) <&> concat   endpos <- getSourcePos   let t = txnTieKnot $ nulltransaction{     tsourcepos   = (pos, endpos),@@ -145,7 +144,7 @@  datelinep :: JournalParser m (Day,Text,Text,[Tag]) datelinep = do-  lift $ traceparse "datelinep"+  dp "datelinep"   lift $ optional orgheadingprefixp   date <- datep   desc <- T.strip <$> lift descriptionp@@ -156,16 +155,15 @@ -- or org headlines which do not start a new day. commentlinesp :: JournalParser m () commentlinesp = do-  lift $ traceparse "commentlinesp"-  void $ many $ try $ lift $ emptyorcommentlinep "#;"+  dp "commentlinesp"+  void $ many $ try $ lift $ emptyorcommentlinep2 "#;"  -- orgnondatelinep :: JournalParser m () -- orgnondatelinep = do---   lift $ traceparse "orgnondatelinep"+--   dp "orgnondatelinep" --   lift orgheadingprefixp --   notFollowedBy datelinep --   void $ lift restofline---   lift $ traceparse' "orgnondatelinep"  orgheadingprefixp = skipSome (char '*') >> skipNonNewlineSpaces1 @@ -175,7 +173,7 @@ -- @ timedotentryp :: JournalParser m [Posting] timedotentryp = do-  lift $ traceparse "timedotentryp"+  dp "timedotentryp"   notFollowedBy datelinep   lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1]   a <- modifiedaccountnamep@@ -225,7 +223,7 @@ -- @ numericquantityp :: TextParser m Hours numericquantityp = do-  -- lift $ traceparse "numericquantityp"+  -- dp "numericquantityp"   (q, _, _, _) <- numberp Nothing   msymbol <- optional $ choice $ map (string . fst) timeUnits   skipNonNewlineSpaces@@ -257,7 +255,7 @@ -- @ dotquantityp :: TextParser m Hours dotquantityp = do-  -- lift $ traceparse "dotquantityp"+  -- dp "dotquantityp"   char '.'   dots <- many (oneOf ['.', ' ']) <&> filter (not.isSpace)   return $ fromIntegral (1 + length dots) / 4@@ -267,7 +265,7 @@ -- ignoring any interspersed spaces after the first letter. letterquantitiesp :: TextParser m [(Hours, TagValue)] letterquantitiesp =-  -- dbg "letterquantitiesp" $ +  -- dp "letterquantitiesp"   do     letter1 <- letterChar     letters <- many (letterChar <|> spacenonewline) <&> filter (not.isSpace)@@ -276,19 +274,3 @@           | t@(c:_) <- group $ sort $ letter1:letters           ]     return groups---- | XXX new comment line parser, move to Hledger.Read.Common.emptyorcommentlinep--- Parse empty lines, all-blank lines, and lines beginning with any of the provided--- comment-beginning characters.-emptyorcommentlinep :: [Char] -> TextParser m ()-emptyorcommentlinep cs =-  label ("empty line or comment line beginning with "++cs) $ do-    traceparse "emptyorcommentlinep" -- XXX possible to combine label and traceparse ?-    skipNonNewlineSpaces-    void newline <|> void commentp-    traceparse' "emptyorcommentlinep"-    where-      commentp = do-        choice (map (some.char) cs)-        takeWhileP Nothing (/='\n') <* newline-
Hledger/Reports/BudgetReport.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedStrings   #-}-{-# LANGUAGE RecordWildCards     #-} {-# LANGUAGE ScopedTypeVariables #-}  module Hledger.Reports.BudgetReport (@@ -10,9 +9,6 @@   BudgetReportRow,   BudgetReport,   budgetReport,-  budgetReportAsTable,-  budgetReportAsText,-  budgetReportAsCsv,   -- * Helpers   combineBudgetAndActual,   -- * Tests@@ -21,25 +17,16 @@ where  import Control.Applicative ((<|>))-import Control.Arrow ((***))-import Data.Decimal (roundTo)-import Data.Function (on) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM-import Data.List (find, partition, transpose, foldl', maximumBy, intercalate)+import Data.List (find, partition, maximumBy, intercalate) import Data.List.Extra (nubSort)-import Data.Maybe (fromMaybe, catMaybes, isJust)+import Data.Maybe (fromMaybe, isJust) import Data.Map (Map) import qualified Data.Map as Map import qualified Data.Set as S-import Data.Text (Text) import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Builder as TB import Safe (minimumDef)---import System.Console.CmdArgs.Explicit as C---import Lucid as L-import qualified Text.Tabular.AsciiWide as Tab  import Hledger.Data import Hledger.Utils@@ -62,17 +49,6 @@ -- | A full budget report table. type BudgetReport    = PeriodicReport    DisplayName BudgetCell --- A BudgetCell's data values rendered for display - the actual change amount,--- the budget goal amount if any, and the corresponding goal percentage if possible.-type BudgetDisplayCell = (WideBuilder, Maybe (WideBuilder, Maybe WideBuilder))--- | A row of rendered budget data cells.-type BudgetDisplayRow  = [BudgetDisplayCell]---- | An amount render helper for the budget report. Renders each commodity separately.-type BudgetShowAmountsFn   = MixedAmount -> [WideBuilder]--- | A goal percentage calculating helper for the budget report.-type BudgetCalcPercentagesFn  = Change -> BudgetGoal -> [Maybe Percentage]- _brrShowDebug :: BudgetReportRow -> String _brrShowDebug (PeriodicReportRow dname budgetpairs _tot _avg) =   unwords [@@ -278,280 +254,6 @@         totGoalByPeriod = Map.fromList $ zip budgetperiods budgettots :: Map DateSpan BudgetTotal         totActualByPeriod = Map.fromList $ zip actualperiods actualtots :: Map DateSpan Change         budget b = if mixedAmountLooksZero b then Nothing else Just b---- | Render a budget report as plain text suitable for console output.-budgetReportAsText :: ReportOpts -> BudgetReport -> TL.Text-budgetReportAsText ropts@ReportOpts{..} budgetr = TB.toLazyText $-    TB.fromText title <> TB.fromText "\n\n" <>-      balanceReportTableAsText ropts (budgetReportAsTable ropts budgetr)-  where-    title = "Budget performance in " <> showDateSpan (periodicReportSpan budgetr)-           <> (case conversionop_ of-                 Just ToCost -> ", converted to cost"-                 _           -> "")-           <> (case value_ of-                 Just (AtThen _mc)   -> ", valued at posting date"-                 Just (AtEnd _mc)    -> ", valued at period ends"-                 Just (AtNow _mc)    -> ", current value"-                 Just (AtDate d _mc) -> ", valued at " <> showDate d-                 Nothing             -> "")-           <> ":"---- | Build a 'Table' from a multi-column balance report.-budgetReportAsTable :: ReportOpts -> BudgetReport -> Tab.Table Text Text WideBuilder-budgetReportAsTable ReportOpts{..} (PeriodicReport spans items totrow) =-  maybetransposetable $-  addtotalrow $-    Tab.Table-      (Tab.Group Tab.NoLine $ map Tab.Header accts)-      (Tab.Group Tab.NoLine $ map Tab.Header colheadings)-      rows-  where-    maybetransposetable-      | transpose_ = \(Tab.Table rh ch vals) -> Tab.Table ch rh (transpose vals)-      | otherwise  = id--    addtotalrow-      | no_total_ = id-      | otherwise = let rh = Tab.Group Tab.NoLine . replicate (length totalrows) $ Tab.Header ""-                        ch = Tab.Header [] -- ignored-                     in (flip (Tab.concatTables Tab.SingleLine) $ Tab.Table rh ch totalrows)--    colheadings = ["Commodity" | layout_ == LayoutBare]-                  ++ map (reportPeriodName balanceaccum_ spans) spans-                  ++ ["  Total" | row_total_]-                  ++ ["Average" | average_]--    (accts, rows, totalrows) =-      (accts'-      ,maybecommcol itemscs  $ showcells  texts-      ,maybecommcol totrowcs $ showtotrow totrowtexts)-      where-        -- If --layout=bare, prepend a commodities column.-        maybecommcol :: [WideBuilder] -> [[WideBuilder]] -> [[WideBuilder]]-        maybecommcol cs-          | layout_ == LayoutBare = zipWith (:) cs-          | otherwise             = id--        showcells, showtotrow :: [[BudgetDisplayCell]] -> [[WideBuilder]]-        (showcells, showtotrow) =-          (maybetranspose . map (zipWith showBudgetDisplayCell widths)       . maybetranspose-          ,maybetranspose . map (zipWith showBudgetDisplayCell totrowwidths) . maybetranspose)-          where-            -- | Combine a BudgetDisplayCell's rendered values into a "[PERCENT of GOAL]" rendering,-            -- respecting the given widths.-            showBudgetDisplayCell :: (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder-            showBudgetDisplayCell (actualwidth, budgetwidth, percentwidth) (actual, mbudget) =-              flip WideBuilder (actualwidth + totalbudgetwidth) $-                toPadded actual <> maybe emptycell showBudgetGoalAndPercentage mbudget--              where-                toPadded (WideBuilder b w) = (TB.fromText . flip T.replicate " " $ actualwidth - w) <> b--                (totalpercentwidth, totalbudgetwidth) =-                  let totalpercentwidth' = if percentwidth == 0 then 0 else percentwidth + 5-                   in ( totalpercentwidth'-                      , if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth' + 3-                      )--                emptycell :: TB.Builder-                emptycell = TB.fromText $ T.replicate totalbudgetwidth " "--                showBudgetGoalAndPercentage :: (WideBuilder, Maybe WideBuilder) -> TB.Builder-                showBudgetGoalAndPercentage (goal, perc) =-                  let perct = case perc of-                        Nothing  -> T.replicate totalpercentwidth " "-                        Just pct -> T.replicate (percentwidth - wbWidth pct) " " <> wbToText pct <> "% of "-                   in TB.fromText $ " [" <> perct <> T.replicate (budgetwidth - wbWidth goal) " " <> wbToText goal <> "]"--            -- | Build a list of widths for each column.-            -- When --transpose is used, the totals row must be included in this list.-            widths :: [(Int, Int, Int)]-            widths = zip3 actualwidths budgetwidths percentwidths-              where-                actualwidths  = map (maximum' . map first3 ) $ cols-                budgetwidths  = map (maximum' . map second3) $ cols-                percentwidths = map (maximum' . map third3 ) $ cols-                catcolumnwidths = foldl' (zipWith (++)) $ repeat []-                cols = maybetranspose $ catcolumnwidths $ map (cellswidth . rowToBudgetCells) items ++ [cellswidth $ rowToBudgetCells totrow]--                cellswidth :: [BudgetCell] -> [[(Int, Int, Int)]]-                cellswidth row =-                  let cs = budgetCellsCommodities row-                      (showmixed, percbudget) = mkBudgetDisplayFns cs-                      disp = showcell showmixed percbudget-                      budgetpercwidth = wbWidth *** maybe 0 wbWidth-                      cellwidth (am, bm) = let (bw, pw) = maybe (0, 0) budgetpercwidth bm in (wbWidth am, bw, pw)-                   in map (map cellwidth . disp) row--            totrowwidths :: [(Int, Int, Int)]-            totrowwidths-              | transpose_ = drop (length texts) widths-              | otherwise = widths--            maybetranspose-              | transpose_ = transpose-              | otherwise  = id--        (accts', itemscs, texts) = unzip3 $ concat shownitems-          where-            shownitems :: [[(AccountName, WideBuilder, BudgetDisplayRow)]]-            shownitems =-              map (\i ->-                let-                  addacctcolumn = map (\(cs, cvals) -> (renderacct i, cs, cvals))-                  isunbudgetedrow = displayFull (prrName i) == unbudgetedAccountName-                in addacctcolumn $ showrow isunbudgetedrow $ rowToBudgetCells i)-              items-              where-                -- FIXME. Have to check explicitly for which to render here, since-                -- budgetReport sets accountlistmode to ALTree. Find a principled way to do-                -- this.-                renderacct row = case accountlistmode_ of-                  ALTree -> T.replicate ((prrDepth row - 1)*2) " " <> prrDisplayName row-                  ALFlat -> accountNameDrop (drop_) $ prrFullName row--        (totrowcs, totrowtexts)  = unzip  $ concat showntotrow-          where-            showntotrow :: [[(WideBuilder, BudgetDisplayRow)]]-            showntotrow = [showrow False $ rowToBudgetCells totrow]--        -- | Get the data cells from a row or totals row, maybe adding -        -- the row total and/or row average depending on options.-        rowToBudgetCells :: PeriodicReportRow a BudgetCell -> [BudgetCell]-        rowToBudgetCells (PeriodicReportRow _ as rowtot rowavg) = as-            ++ [rowtot | row_total_ && not (null as)]-            ++ [rowavg | average_   && not (null as)]--        -- | Render a row's data cells as "BudgetDisplayCell"s, and a rendered list of commodity symbols.-        -- Also requires a flag indicating whether this is the special <unbudgeted> row.-        -- (The types make that hard to check here.)-        showrow :: Bool -> [BudgetCell] -> [(WideBuilder, BudgetDisplayRow)]-        showrow isunbudgetedrow cells =-          let-            cs = budgetCellsCommodities cells-            -- #2071 If there are no commodities - because there are no actual or goal amounts --            -- the zipped list would be empty, causing this row not to be shown.-            -- But rows like this sometimes need to be shown to preserve the account tree structure.-            -- So, ensure 0 will be shown as actual amount(s).-            -- Unfortunately this disables boring parent eliding, as if --no-elide had been used.-            -- (Just turning on --no-elide higher up doesn't work right.)-            -- Note, no goal amount will be shown for these rows,-            -- whereas --no-elide is likely to show a goal amount aggregated from children.-            cs1 = if null cs && not isunbudgetedrow then [""] else cs-            (showmixed, percbudget) = mkBudgetDisplayFns cs1-          in-            zip (map wbFromText cs1) $-            transpose $-            map (showcell showmixed percbudget)-            cells--        budgetCellsCommodities :: [BudgetCell] -> [CommoditySymbol]-        budgetCellsCommodities = S.toList . foldl' S.union mempty . map budgetCellCommodities-          where-            budgetCellCommodities :: BudgetCell -> S.Set CommoditySymbol-            budgetCellCommodities (am, bm) = f am `S.union` f bm-              where f = maybe mempty maCommodities--        -- | Render a "BudgetCell"'s amounts as "BudgetDisplayCell"s (one per commodity).-        showcell :: BudgetShowAmountsFn -> BudgetCalcPercentagesFn -> BudgetCell -> BudgetDisplayRow-        showcell showCommodityAmounts calcCommodityPercentages (mactual, mbudget) =-          zip actualamts budgetinfos-          where-            actual = fromMaybe nullmixedamt mactual-            actualamts = showCommodityAmounts actual-            budgetinfos =-              case mbudget of-                Nothing   -> repeat Nothing-                Just goal -> map Just $ showGoalAmountsAndPercentages goal-                where-                  showGoalAmountsAndPercentages :: MixedAmount -> [(WideBuilder, Maybe WideBuilder)]-                  showGoalAmountsAndPercentages goal = zip amts mpcts-                    where-                      amts  = showCommodityAmounts goal-                      mpcts = map (showrounded <$>) $ calcCommodityPercentages actual goal-                        where showrounded = wbFromText . T.pack . show . roundTo 0--        -- | Make budget info display helpers that adapt to --layout=wide.-        mkBudgetDisplayFns :: [CommoditySymbol] -> (BudgetShowAmountsFn, BudgetCalcPercentagesFn)-        mkBudgetDisplayFns cs = case layout_ of-          LayoutWide width ->-               ( pure . showMixedAmountB oneLineNoCostFmt{displayMaxWidth=width, displayColour=color_}-               , \a -> pure . percentage a)-          _ -> ( showMixedAmountLinesB noCostFmt{displayCommodity=layout_/=LayoutBare, displayCommodityOrder=Just cs, displayMinWidth=Nothing, displayColour=color_}-               , \a b -> map (percentage' a b) cs)-          where-            -- | Calculate the percentage of actual change to budget goal to show, if any.-            -- If valuing at cost, both amounts are converted to cost before comparing.-            -- A percentage will not be shown if:-            ---            -- - actual or goal are not the same, single, commodity-            ---            -- - the goal is zero-            ---            percentage :: Change -> BudgetGoal -> Maybe Percentage-            percentage actual budget =-              case (costedAmounts actual, costedAmounts budget) of-                ([a], [b]) | (acommodity a == acommodity b || amountLooksZero a) && not (amountLooksZero b)-                    -> Just $ 100 * aquantity a / aquantity b-                _   -> Nothing-              where-                costedAmounts = case conversionop_ of-                    Just ToCost -> amounts . mixedAmountCost-                    _           -> amounts--            -- | Like percentage, but accept multicommodity actual and budget amounts,-            -- and extract the specified commodity from both.-            percentage' :: Change -> BudgetGoal -> CommoditySymbol -> Maybe Percentage-            percentage' am bm c = case ((,) `on` find ((==) c . acommodity) . amounts) am bm of-                (Just a, Just b) -> percentage (mixedAmount a) (mixedAmount b)-                _                -> Nothing---- XXX generalise this with multiBalanceReportAsCsv ?--- | Render a budget report as CSV. Like multiBalanceReportAsCsv,--- but includes alternating actual and budget amount columns.-budgetReportAsCsv :: ReportOpts -> BudgetReport -> [[Text]]-budgetReportAsCsv-  ReportOpts{..}-  (PeriodicReport colspans items totrow)-  = (if transpose_ then transpose else id) $--  -- heading row-  ("Account" :-  ["Commodity" | layout_ == LayoutBare ]-   ++ concatMap (\spn -> [showDateSpan spn, "budget"]) colspans-   ++ concat [["Total"  ,"budget"] | row_total_]-   ++ concat [["Average","budget"] | average_]-  ) :--  -- account rows-  concatMap (rowAsTexts prrFullName) items--  -- totals row-  ++ concat [ rowAsTexts (const "Total:") totrow | not no_total_ ]--  where-    flattentuples tups = concat [[a,b] | (a,b) <- tups]-    showNorm = maybe "" (wbToText . showMixedAmountB oneLineNoCostFmt)--    rowAsTexts :: (PeriodicReportRow a BudgetCell -> Text)-               -> PeriodicReportRow a BudgetCell-               -> [[Text]]-    rowAsTexts render row@(PeriodicReportRow _ as (rowtot,budgettot) (rowavg, budgetavg))-      | layout_ /= LayoutBare = [render row : map showNorm vals]-      | otherwise =-            joinNames . zipWith (:) cs  -- add symbols and names-          . transpose                   -- each row becomes a list of Text quantities-          . map (map wbToText . showMixedAmountLinesB dopts . fromMaybe nullmixedamt)-          $ vals-      where-        cs = S.toList . foldl' S.union mempty . map maCommodities $ catMaybes vals-        dopts = oneLineNoCostFmt{displayCommodity=layout_ /= LayoutBare, displayCommodityOrder=Just cs, displayMinWidth=Nothing}-        vals = flattentuples as-            ++ concat [[rowtot, budgettot] | row_total_]-            ++ concat [[rowavg, budgetavg] | average_]--        joinNames = map (render row :)  -- tests 
Hledger/Reports/MultiBalanceReport.hs view
@@ -28,7 +28,6 @@   getPostings,   startingPostings,   generateMultiBalanceReport,-  balanceReportTableAsText,    -- -- * Tests   tests_MultiBalanceReport@@ -39,7 +38,7 @@ import Data.Bifunctor (second) import Data.Foldable (toList) import Data.List (sortOn, transpose)-import Data.List.NonEmpty (NonEmpty(..))+import Data.List.NonEmpty (NonEmpty((:|))) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM import Data.Map (Map)@@ -52,11 +51,6 @@ import Data.Time.Calendar (fromGregorian) import Safe (lastDef, minimumMay) -import Data.Default (def)-import qualified Data.Text as T-import qualified Data.Text.Lazy.Builder as TB-import qualified Text.Tabular.AsciiWide as Tab- import Hledger.Data import Hledger.Query import Hledger.Utils hiding (dbg3,dbg4,dbg5)@@ -594,33 +588,13 @@ cumulativeSum :: Account -> Map DateSpan Account -> Map DateSpan Account cumulativeSum start = snd . M.mapAccum (\a b -> let s = sumAcct a b in (s, s)) start --- | Given a table representing a multi-column balance report (for example,--- made using 'balanceReportAsTable'), render it in a format suitable for--- console output. Amounts with more than two commodities will be elided--- unless --no-elide is used.-balanceReportTableAsText :: ReportOpts -> Tab.Table T.Text T.Text WideBuilder -> TB.Builder-balanceReportTableAsText ReportOpts{..} =-    Tab.renderTableByRowsB def{Tab.tableBorders=False, Tab.prettyTable=pretty_} renderCh renderRow-  where-    renderCh-      | layout_ /= LayoutBare || transpose_ = fmap (Tab.textCell Tab.TopRight)-      | otherwise = zipWith ($) (Tab.textCell Tab.TopLeft : repeat (Tab.textCell Tab.TopRight))--    renderRow (rh, row)-      | layout_ /= LayoutBare || transpose_ =-          (Tab.textCell Tab.TopLeft rh, fmap (Tab.Cell Tab.TopRight . pure) row)-      | otherwise =-          (Tab.textCell Tab.TopLeft rh, zipWith ($) (Tab.Cell Tab.TopLeft : repeat (Tab.Cell Tab.TopRight)) (fmap pure row))--- -- tests  tests_MultiBalanceReport = testGroup "MultiBalanceReport" [    let-    amt0 = Amount {acommodity="$", aquantity=0, acost=Nothing, -      astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asdigitgroups = Nothing, +    amt0 = Amount {acommodity="$", aquantity=0, acost=Nothing,+      astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asdigitgroups = Nothing,       asdecimalmark = Just '.', asprecision = Precision 2, asrounding = NoRounding}}     (rspec,journal) `gives` r = do       let rspec' = rspec{_rsQuery=And [queryFromFlags $ _rsReportOpts rspec, _rsQuery rspec]}
Hledger/Reports/PostingsReport.hs view
@@ -15,15 +15,18 @@   PostingsReportItem,   postingsReport,   mkpostingsReportItem,+  SortSpec,+  defsortspec,    -- * Tests   tests_PostingsReport ) where -import Data.List (nub, sortOn)+import Data.List (nub, sortBy, sortOn) import Data.List.Extra (nubSort)-import Data.Maybe (isJust, isNothing)+import Data.Maybe (isJust, isNothing, fromMaybe)+import Data.Ord import Data.Text (Text) import Data.Time.Calendar (Day) import Safe (headMay)@@ -82,10 +85,12 @@           summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans           showempty = empty_ || average_ +      sortedps = if sortspec_ /= defsortspec then sortPostings ropts sortspec_ displayps else displayps+       -- Posting report items ready for display.       items =         dbg4 "postingsReport items" $-        postingsReportItems displayps (nullposting,Nothing) whichdate mdepth startbal runningcalc startnum+        postingsReportItems postings (nullposting,Nothing) whichdate mdepth startbal runningcalc startnum         where           -- In historical mode we'll need a starting balance, which we           -- may be converting to value per hledger_options.m4.md "Effect@@ -100,6 +105,10 @@            runningcalc = registerRunningCalculationFn ropts           startnum = if historical then length precedingps + 1 else 1+          postings | historical = if sortspec_ /= defsortspec +                        then error "--historical and --sort should not be used together" +                        else sortedps+                   | otherwise = sortedps  -- | Based on the given report options, return a function that does the appropriate -- running calculation for the register report, ie a running average or running total.@@ -109,6 +118,34 @@ registerRunningCalculationFn ropts   | average_ ropts = \i avg amt -> avg `maPlus` divideMixedAmount (fromIntegral i) (amt `maMinus` avg)   | otherwise      = \_ bal amt -> bal `maPlus` amt++-- | Sort two postings by the current list of value expressions (given in SortSpec).+comparePostings :: ReportOpts -> SortSpec -> (Posting, Maybe Period) -> (Posting, Maybe Period) -> Ordering+comparePostings _ [] _ _ = EQ+comparePostings ropts (ex:es) (a, pa) (b, pb) = +    let +    getDescription p = +        let tx = ptransaction p +            description = fmap (\t -> tdescription t) tx+        -- If there's no transaction attached, then use empty text for the description+        in fromMaybe "" description+    comparison = case ex of+          AbsAmount' False -> compare (abs (pamount a)) (abs (pamount b))+          Amount' False -> compare (pamount a) (pamount b)+          Account' False -> compare (paccount a) (paccount b)+          Date' False -> compare (postingDateOrDate2 (whichDate ropts) a) (postingDateOrDate2 (whichDate ropts) b)+          Description' False -> compare (getDescription a) (getDescription b)+          AbsAmount' True -> compare (Down (abs (pamount a))) (Down (abs (pamount b)))+          Amount' True -> compare (Down (pamount a)) (Down (pamount b))+          Account' True -> compare (Down (paccount a)) (Down (paccount b))+          Date' True -> compare (Down (postingDateOrDate2 (whichDate ropts) a)) (Down (postingDateOrDate2 (whichDate ropts) b))+          Description' True -> compare (Down (getDescription a)) (Down (getDescription b))+    in +    if comparison == EQ then comparePostings ropts es (a, pa) (b, pb) else comparison++-- | Sort postings by the current SortSpec.+sortPostings :: ReportOpts -> SortSpec -> [(Posting, Maybe Period)] -> [(Posting, Maybe Period)]+sortPostings ropts sspec = sortBy (comparePostings ropts sspec)  -- | Find postings matching a given query, within a given date span, -- and also any similarly-matched postings before that date span.
Hledger/Reports/ReportOptions.hs view
@@ -21,6 +21,9 @@   HasReportOpts(..),   ReportSpec(..),   HasReportSpec(..),+  SortField(..),+  SortSpec,+  sortKeysDescription,   overEither,   setEither,   BalanceCalculation(..),@@ -31,6 +34,7 @@   defreportopts,   rawOptsToReportOpts,   defreportspec,+  defsortspec,   setDefaultConversionOp,   reportOptsToSpec,   updateReportSpec,@@ -71,15 +75,13 @@ import Data.Either (fromRight) import Data.Either.Extra (eitherToMaybe) import Data.Functor.Identity (Identity(..))-import Data.List.Extra (find, isPrefixOf, nubSort)+import Data.List.Extra (find, isPrefixOf, nubSort, stripPrefix) import Data.Maybe (fromMaybe, isJust, isNothing) import qualified Data.Text as T import Data.Time.Calendar (Day, addDays) import Data.Default (Default(..)) import Safe (headMay, lastDef, lastMay, maximumMay, readMay) -import Text.Megaparsec.Custom- import Hledger.Data import Hledger.Query import Hledger.Utils@@ -144,6 +146,8 @@     ,average_          :: Bool     -- for posting reports (register)     ,related_          :: Bool+    -- for sorting reports (register)+    ,sortspec_             :: SortSpec     -- for account transactions reports (aregister)     ,txn_dates_        :: Bool     -- for balance reports (bal, bs, cf, is)@@ -199,6 +203,7 @@     , querystring_      = []     , average_          = False     , related_          = False+    , sortspec_         = defsortspec      , txn_dates_        = False     , balancecalc_      = def     , balanceaccum_     = def@@ -253,6 +258,7 @@           ,querystring_      = querystring           ,average_          = boolopt "average" rawopts           ,related_          = boolopt "related" rawopts+          ,sortspec_         = getSortSpec rawopts           ,txn_dates_        = boolopt "txn-dates" rawopts           ,balancecalc_      = balancecalcopt rawopts           ,balanceaccum_     = balanceaccumopt rawopts@@ -665,6 +671,47 @@     consIf f b = if b then (f True:) else id     consJust f = maybe id ((:) . f) +-- Methods/types needed for --sort argument++-- Possible arguments taken by the --sort command+-- Each of these takes a bool, which shows if it has been inverted+-- (True -> has been inverted, reverse the order)+data SortField+    = AbsAmount' Bool+    | Account' Bool+    | Amount' Bool+    | Date' Bool+    | Description' Bool+    deriving (Show, Eq)+type SortSpec = [SortField]++-- By default, sort by date in ascending order+defsortspec :: SortSpec+defsortspec = [Date' False]++-- Load a SortSpec from the argument given to --sort+-- If there is no spec given, then sort by [Date' False] by default+getSortSpec :: RawOpts -> SortSpec+getSortSpec opts = +    let opt = maybestringopt "sort" opts+        optParser s = +          let terms = map strip $ splitAtElement ',' s +              termParser t = case trimmed of+                "date"        -> Date'        isNegated+                "desc"        -> Description' isNegated+                "description" -> Description' isNegated+                "account"     -> Account'     isNegated+                "amount"      -> Amount'      isNegated+                "absamount"   -> AbsAmount'   isNegated+                _ -> error' $ "unknown --sort key " ++ t ++ ". Supported keys are: " <> sortKeysDescription <> "."+                where isNegated = isPrefixOf "-" t+                      trimmed = fromMaybe t (stripPrefix "-" t)+          in map termParser terms+    in maybe defsortspec optParser opt ++-- for option's help and parse error message+sortKeysDescription = "date, desc, account, amount, absamount"  -- 'description' is also accepted+ -- Report dates.  -- | The effective report span is the start and end dates specified by@@ -766,7 +813,7 @@ reportPeriodName :: BalanceAccumulation -> [DateSpan] -> DateSpan -> T.Text reportPeriodName balanceaccumulation spans =   case balanceaccumulation of-    PerPeriod -> if multiyear then showDateSpan else showDateSpanMonthAbbrev+    PerPeriod -> if multiyear then showDateSpan else showDateSpanAbbrev       where         multiyear = (>1) $ length $ nubSort $ map spanStartYear spans     _ -> maybe "" (showDate . prevday) . spanEnd@@ -819,7 +866,7 @@ -- >>> _rsQuery <$> setEither querystring ["assets"] defreportspec -- Right (Acct (RegexpCI "assets")) -- >>> _rsQuery <$> setEither querystring ["(assets"] defreportspec--- Left "This regular expression is malformed, please correct it:\n(assets"+-- Left "This regular expression is invalid or unsupported, please correct it:\n(assets" -- >>> _rsQuery $ set querystring ["assets"] defreportspec -- Acct (RegexpCI "assets") -- >>> _rsQuery $ set querystring ["(assets"] defreportspec
Hledger/Utils/Debug.hs view
@@ -53,7 +53,7 @@ Debug level:  What to show: ------------  --------------------------------------------------------- 0             normal command output only (no warnings, eg)-1             useful warnings, most common troubleshooting info, eg valuation+1             useful warnings, most common troubleshooting info (config file args, valuation..) 2             common troubleshooting info, more detail 3             report options selection 4             report generation
Hledger/Utils/IO.hs view
@@ -97,8 +97,10 @@ import           Data.Colour.RGBSpace (RGB(RGB)) import           Data.Colour.RGBSpace.HSL (lightness) import           Data.FileEmbed (makeRelativeToProject, embedStringFile)+import           Data.Functor ((<&>)) import           Data.List hiding (uncons) import           Data.Maybe (isJust)+import           Data.Ord (comparing, Down (Down)) import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Lazy as TL@@ -115,6 +117,7 @@ import           System.Directory (getHomeDirectory, getModificationTime) import           System.Environment (getArgs, lookupEnv, setEnv) import           System.FilePath (isRelative, (</>))+import "Glob"    System.FilePath.Glob (glob) import           System.IO   (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode,    openFile, stdin, stdout, stderr, universalNewlineMode, utf8_bom, hIsTerminalDevice)@@ -127,15 +130,14 @@   defaultOutputOptionsDarkBg, defaultOutputOptionsNoColor, pShowOpt, pPrintOpt)  import Hledger.Utils.Text (WideBuilder(WideBuilder))-import "Glob" System.FilePath.Glob (glob)-import Data.Functor ((<&>)) + -- Pretty showing/printing with pretty-simple  -- https://hackage.haskell.org/package/pretty-simple/docs/Text-Pretty-Simple.html#t:OutputOptions  -- | pretty-simple options with colour enabled if allowed.-prettyopts = +prettyopts =   (if useColorOnStderr then defaultOutputOptionsDarkBg else defaultOutputOptionsNoColor)     { outputOptionsIndentAmount = 2     -- , outputOptionsCompact      = True  -- fills lines, but does not respect page width (https://github.com/cdepillabout/pretty-simple/issues/126)@@ -225,7 +227,7 @@ -- | Read the value of the -o/--output-file command line option provided at program startup, -- if any, using unsafePerformIO. outputFileOption :: Maybe String-outputFileOption = +outputFileOption =   -- keep synced with output-file flag definition in hledger:CliOptions.   let args = progArgs in   case dropWhile (not . ("-o" `isPrefixOf`)) args of@@ -315,7 +317,7 @@ -- | Read the value of the --color or --colour command line option provided at program startup -- using unsafePerformIO. If this option was not provided, returns the empty string. colorOption :: String-colorOption = +colorOption =   -- similar to debugLevel   -- keep synced with color/colour flag definition in hledger:CliOptions   let args = progArgs in@@ -462,7 +464,7 @@ sortByModTime :: [FilePath] -> IO [FilePath] sortByModTime fs = do   ftimes <- forM fs $ \f -> do {t <- getModificationTime f; return (t,f)}-  return $ map snd $ reverse $ sort ftimes+  return $ map snd $ sortBy (comparing Data.Ord.Down) ftimes  -- | Like readFilePortably, but read all of the file before proceeding. readFileStrictly :: FilePath -> IO T.Text@@ -515,4 +517,5 @@   t <- getCurrentTime   tz <- getCurrentTimeZone   return $ utcToZonedTime tz t+ 
Hledger/Utils/Parse.hs view
@@ -1,8 +1,15 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}  module Hledger.Utils.Parse (++  -- * Some basic hledger parser flavours   SimpleStringParser,   SimpleTextParser,   TextParser,@@ -15,6 +22,7 @@   sourcePosPretty,   sourcePosPairPretty, +  -- * Parsers and helpers   choice',   choiceInState,   surroundedBy,@@ -32,7 +40,6 @@   isNonNewlineSpace,   restofline,   eolof,-   spacenonewline,   skipNonNewlineSpaces,   skipNonNewlineSpaces1,@@ -42,10 +49,44 @@   dbgparse,   traceOrLogParse, -  -- * re-exports-  HledgerParseErrors,+  -- * More helpers, previously in Text.Megaparsec.Custom++  -- ** Custom parse error types   HledgerParseErrorData,+  HledgerParseErrors,++  -- ** Failing with an arbitrary source position+  parseErrorAt,+  parseErrorAtRegion,++  -- ** Re-parsing+  SourceExcerpt,+  getExcerptText,+  excerpt_,+  reparseExcerpt,++  -- ** Pretty-printing custom parse errors   customErrorBundlePretty,++  -- ** "Final" parse errors+  FinalParseError,+  FinalParseError',+  FinalParseErrorBundle,+  FinalParseErrorBundle',++  -- *** Constructing "final" parse errors+  finalError,+  finalFancyFailure,+  finalFail,+  finalCustomFailure,++  -- *** Pretty-printing "final" parse errors+  finalErrorBundlePretty,+  attachSource,++  -- *** Handling parse errors from include files with "final" parse errors+  parseIncludeFile,+ ) where @@ -61,7 +102,15 @@ import Data.List import Data.Text (Text) import Text.Megaparsec.Char-import Text.Megaparsec.Custom+-- import Text.Megaparsec.Debug (dbg)  -- from megaparsec 9.3+++import Control.Monad.Except (ExceptT, MonadError, catchError, throwError)+-- import Control.Monad.State.Strict (StateT, evalStateT)+import Control.Monad.Trans.Class (lift)+import qualified Data.List.NonEmpty as NE+import Data.Monoid (Alt(..))+import qualified Data.Set as S+ import Hledger.Utils.Debug (debugLevel, traceOrLog)  -- | A parser of string to some type.@@ -200,3 +249,384 @@  eolof :: TextParser m () eolof = void newline <|> eof++++-- A bunch of megaparsec helpers, eg for re-parsing (formerly in Text.Megaparsec.Custom).+-- I think these are generic apart from the HledgerParseError name.++--- * Custom parse error types++-- | Custom error data for hledger parsers. Specialised for a 'Text' parse stream.+-- ReparseableTextParseErrorData ?+data HledgerParseErrorData+  -- | Fail with a message at a specific source position interval. The+  -- interval must be contained within a single line.+  = ErrorFailAt Int -- Starting offset+                Int -- Ending offset+                String -- Error message+  -- | Re-throw parse errors obtained from the "re-parsing" of an excerpt+  -- of the source text.+  | ErrorReparsing+      (NE.NonEmpty (ParseError Text HledgerParseErrorData)) -- Source fragment parse errors+  deriving (Show, Eq, Ord)++-- | A specialised version of ParseErrorBundle: +-- a non-empty collection of hledger parse errors, +-- equipped with PosState to help pretty-print them.+-- Specialised for a 'Text' parse stream.+type HledgerParseErrors = ParseErrorBundle Text HledgerParseErrorData++-- We require an 'Ord' instance for 'CustomError' so that they may be+-- stored in a 'Set'. The actual instance is inconsequential, so we just+-- derive it, but the derived instance requires an (orphan) instance for+-- 'ParseError'. Hopefully this does not cause any trouble.++deriving instance Ord (ParseError Text HledgerParseErrorData)++-- Note: the pretty-printing of our 'HledgerParseErrorData' type is only partally+-- defined in its 'ShowErrorComponent' instance; we perform additional+-- adjustments in 'customErrorBundlePretty'.++instance ShowErrorComponent HledgerParseErrorData where+  showErrorComponent (ErrorFailAt _ _ errMsg) = errMsg+  showErrorComponent (ErrorReparsing _) = "" -- dummy value++  errorComponentLen (ErrorFailAt startOffset endOffset _) =+    endOffset - startOffset+  errorComponentLen (ErrorReparsing _) = 1 -- dummy value+++--- * Failing with an arbitrary source position++-- | Fail at a specific source position, given by the raw offset from the+-- start of the input stream (the number of tokens processed at that+-- point).++parseErrorAt :: Int -> String -> HledgerParseErrorData+parseErrorAt offset = ErrorFailAt offset (offset+1)++-- | Fail at a specific source interval, given by the raw offsets of its+-- endpoints from the start of the input stream (the numbers of tokens+-- processed at those points).+--+-- Note that care must be taken to ensure that the specified interval does+-- not span multiple lines of the input source. This will not be checked.++parseErrorAtRegion+  :: Int    -- ^ Start offset+  -> Int    -- ^ End end offset+  -> String -- ^ Error message+  -> HledgerParseErrorData+parseErrorAtRegion startOffset endOffset msg =+  if startOffset < endOffset+    then ErrorFailAt startOffset endOffset msg'+    else ErrorFailAt startOffset (startOffset+1) msg'+  where+    msg' = "\n" ++ msg+++--- * Re-parsing++-- | A fragment of source suitable for "re-parsing". The purpose of this+-- data type is to preserve the content and source position of the excerpt+-- so that parse errors raised during "re-parsing" may properly reference+-- the original source.++data SourceExcerpt = SourceExcerpt Int  -- Offset of beginning of excerpt+                                   Text -- Fragment of source file++-- | Get the raw text of a source excerpt.++getExcerptText :: SourceExcerpt -> Text+getExcerptText (SourceExcerpt _ txt) = txt++-- | 'excerpt_ p' applies the given parser 'p' and extracts the portion of+-- the source consumed by 'p', along with the source position of this+-- portion. This is the only way to create a source excerpt suitable for+-- "re-parsing" by 'reparseExcerpt'.++-- This function could be extended to return the result of 'p', but we don't+-- currently need this.++excerpt_ :: MonadParsec HledgerParseErrorData Text m => m a -> m SourceExcerpt+excerpt_ p = do+  offset <- getOffset+  (!txt, _) <- match p+  pure $ SourceExcerpt offset txt++-- | 'reparseExcerpt s p' "re-parses" the source excerpt 's' using the+-- parser 'p'. Parse errors raised by 'p' will be re-thrown at the source+-- position of the source excerpt.+--+-- In order for the correct source file to be displayed when re-throwing+-- parse errors, we must ensure that the source file during the use of+-- 'reparseExcerpt s p' is the same as that during the use of 'excerpt_'+-- that generated the source excerpt 's'. However, we can usually expect+-- this condition to be satisfied because, at the time of writing, the+-- only changes of source file in the codebase take place through include+-- files, and the parser for include files neither accepts nor returns+-- 'SourceExcerpt's.++reparseExcerpt+  :: Monad m+  => SourceExcerpt+  -> ParsecT HledgerParseErrorData Text m a+  -> ParsecT HledgerParseErrorData Text m a+reparseExcerpt (SourceExcerpt offset txt) p = do+  (_, res) <- lift $ runParserT' p (offsetInitialState offset txt)+  case res of+    Right result -> pure result+    Left errBundle -> customFailure $ ErrorReparsing $ bundleErrors errBundle++  where+    offsetInitialState :: Int -> s ->+#if MIN_VERSION_megaparsec(8,0,0)+      State s e+#else+      State s+#endif+    offsetInitialState initialOffset s = State+      { stateInput  = s+      , stateOffset = initialOffset+      , statePosState = PosState+        { pstateInput = s+        , pstateOffset = initialOffset+        , pstateSourcePos = initialPos ""+        , pstateTabWidth = defaultTabWidth+        , pstateLinePrefix = ""+        }+#if MIN_VERSION_megaparsec(8,0,0)+      , stateParseErrors = []+#endif+      }++--- * Pretty-printing custom parse errors++-- | Pretty-print our custom parse errors. It is necessary to use this+-- instead of 'errorBundlePretty' when custom parse errors are thrown.+--+-- This function intercepts our custom parse errors and applies final+-- adjustments ('finalizeCustomError') before passing them to+-- 'errorBundlePretty'. These adjustments are part of the implementation+-- of the behaviour of our custom parse errors.+--+-- Note: We must ensure that the offset of the 'PosState' of the provided+-- 'ParseErrorBundle' is no larger than the offset specified by a+-- 'ErrorFailAt' constructor. This is guaranteed if this offset is set to+-- 0 (that is, the beginning of the source file), which is the+-- case for 'ParseErrorBundle's returned from 'runParserT'.++customErrorBundlePretty :: HledgerParseErrors -> String+customErrorBundlePretty errBundle =+  let errBundle' = errBundle { bundleErrors =+        NE.sortWith errorOffset $ -- megaparsec requires that the list of errors be sorted by their offsets+        bundleErrors errBundle >>= finalizeCustomError }+  in  errorBundlePretty errBundle'++  where+    finalizeCustomError+      :: ParseError Text HledgerParseErrorData -> NE.NonEmpty (ParseError Text HledgerParseErrorData)+    finalizeCustomError err = case findCustomError err of+      Nothing -> pure err++      Just errFailAt@(ErrorFailAt startOffset _ _) ->+        -- Adjust the offset+        pure $ FancyError startOffset $ S.singleton $ ErrorCustom errFailAt++      Just (ErrorReparsing errs) ->+        -- Extract and finalize the inner errors+        errs >>= finalizeCustomError++    -- If any custom errors are present, arbitrarily take the first one+    -- (since only one custom error should be used at a time).+    findCustomError :: ParseError Text HledgerParseErrorData -> Maybe HledgerParseErrorData+    findCustomError err = case err of+      FancyError _ errSet ->+        finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet+      _ -> Nothing++    finds :: (Foldable t) => (a -> Maybe b) -> t a -> Maybe b+    finds f = getAlt . foldMap (Alt . f)+++--- * "Final" parse errors+--+-- | A type representing "final" parse errors that cannot be backtracked+-- from and are guaranteed to halt parsing. The anti-backtracking+-- behaviour is implemented by an 'ExceptT' layer in the parser's monad+-- stack, using this type as the 'ExceptT' error type.+--+-- We have three goals for this type:+-- (1) it should be possible to convert any parse error into a "final"+-- parse error,+-- (2) it should be possible to take a parse error thrown from an include+-- file and re-throw it in the parent file, and+-- (3) the pretty-printing of "final" parse errors should be consistent+-- with that of ordinary parse errors, but should also report a stack of+-- files for errors thrown from include files.+--+-- In order to pretty-print a "final" parse error (goal 3), it must be+-- bundled with include filepaths and its full source text. When a "final"+-- parse error is thrown from within a parser, we do not have access to+-- the full source, so we must hold the parse error until it can be joined+-- with its source (and include filepaths, if it was thrown from an+-- include file) by the parser's caller.+--+-- A parse error with include filepaths and its full source text is+-- represented by the 'FinalParseErrorBundle' type, while a parse error in+-- need of either include filepaths, full source text, or both is+-- represented by the 'FinalParseError' type.++data FinalParseError' e+  -- a parse error thrown as a "final" parse error+  = FinalError           (ParseError Text e)+  -- a parse error obtained from running a parser, e.g. using 'runParserT'+  | FinalBundle          (ParseErrorBundle Text e)+  -- a parse error thrown from an include file+  | FinalBundleWithStack (FinalParseErrorBundle' e)+  deriving (Show)++type FinalParseError = FinalParseError' HledgerParseErrorData++-- We need a 'Monoid' instance for 'FinalParseError' so that 'ExceptT+-- FinalParseError m' is an instance of Alternative and MonadPlus, which+-- is needed to use some parser combinators, e.g. 'many'.+--+-- This monoid instance simply takes the first (left-most) error.++instance Semigroup (FinalParseError' e) where+  e <> _ = e++instance Monoid (FinalParseError' e) where+  mempty = FinalError $ FancyError 0 $+            S.singleton (ErrorFail "default parse error")+  mappend = (<>)++-- | A type bundling a 'ParseError' with its full source text, filepath,+-- and stack of include files. Suitable for pretty-printing.+--+-- Megaparsec's 'ParseErrorBundle' type already bundles a parse error with+-- its full source text and filepath, so we just add a stack of include+-- files.++data FinalParseErrorBundle' e = FinalParseErrorBundle'+  { finalErrorBundle :: ParseErrorBundle Text e+  , includeFileStack :: [FilePath]+  } deriving (Show)++type FinalParseErrorBundle = FinalParseErrorBundle' HledgerParseErrorData+++--- * Constructing and throwing final parse errors++-- | Convert a "regular" parse error into a "final" parse error.++finalError :: ParseError Text e -> FinalParseError' e+finalError = FinalError++-- | Like megaparsec's 'fancyFailure', but as a "final" parse error.++finalFancyFailure+  :: (MonadParsec e s m, MonadError (FinalParseError' e) m)+  => S.Set (ErrorFancy e) -> m a+finalFancyFailure errSet = do+  offset <- getOffset+  throwError $ FinalError $ FancyError offset errSet++-- | Like 'fail', but as a "final" parse error.++finalFail+  :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => String -> m a+finalFail = finalFancyFailure . S.singleton . ErrorFail++-- | Like megaparsec's 'customFailure', but as a "final" parse error.++finalCustomFailure+  :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => e -> m a+finalCustomFailure = finalFancyFailure . S.singleton . ErrorCustom+++--- * Pretty-printing "final" parse errors++-- | Pretty-print a "final" parse error: print the stack of include files,+-- then apply the pretty-printer for parse error bundles. Note that+-- 'attachSource' must be used on a "final" parse error before it can be+-- pretty-printed.++finalErrorBundlePretty :: FinalParseErrorBundle' HledgerParseErrorData -> String+finalErrorBundlePretty bundle =+     concatMap showIncludeFilepath (includeFileStack bundle)+  <> customErrorBundlePretty (finalErrorBundle bundle)+  where+    showIncludeFilepath path = "in file included from " <> path <> ",\n"++-- | Supply a filepath and source text to a "final" parse error so that it+-- can be pretty-printed. You must ensure that you provide the appropriate+-- source text and filepath.++attachSource+  :: FilePath -> Text -> FinalParseError' e -> FinalParseErrorBundle' e+attachSource filePath sourceText finalParseError = case finalParseError of++  -- A parse error thrown directly with the 'FinalError' constructor+  -- requires both source and filepath.+  FinalError err ->+    let bundle = ParseErrorBundle+          { bundleErrors = err NE.:| []+          , bundlePosState = initialPosState filePath sourceText }+    in  FinalParseErrorBundle'+          { finalErrorBundle = bundle+          , includeFileStack  = [] }++  -- A 'ParseErrorBundle' already has the appropriate source and filepath+  -- and so needs neither.+  FinalBundle peBundle -> FinalParseErrorBundle'+    { finalErrorBundle = peBundle+    , includeFileStack = [] }++  -- A parse error from a 'FinalParseErrorBundle' was thrown from an+  -- include file, so we add the filepath to the stack.+  FinalBundleWithStack fpeBundle -> fpeBundle+    { includeFileStack = filePath : includeFileStack fpeBundle }+++--- * Handling parse errors from include files with "final" parse errors++-- | Parse a file with the given parser and initial state, discarding the+-- final state and re-throwing any parse errors as "final" parse errors.++parseIncludeFile+  :: Monad m+  => StateT st (ParsecT HledgerParseErrorData Text (ExceptT FinalParseError m)) a+  -> st+  -> FilePath+  -> Text+  -> StateT st (ParsecT HledgerParseErrorData Text (ExceptT FinalParseError m)) a+parseIncludeFile parser initialState filepath text =+  catchError parser' handler+  where+    parser' = do+      eResult <- lift $ lift $+                  runParserT (evalStateT parser initialState) filepath text+      case eResult of+        Left parseErrorBundle -> throwError $ FinalBundle parseErrorBundle+        Right result -> pure result++    -- Attach source and filepath of the include file to its parse errors+    handler e = throwError $ FinalBundleWithStack $ attachSource filepath text e+++--- * Helpers++-- Like megaparsec's 'initialState', but instead for 'PosState'. Used when+-- constructing 'ParseErrorBundle's. The values for "tab width" and "line+-- prefix" are taken from 'initialState'.++initialPosState :: FilePath -> Text -> PosState Text+initialPosState filePath sourceText = PosState+  { pstateInput      = sourceText+  , pstateOffset     = 0+  , pstateSourcePos  = initialPos filePath+  , pstateTabWidth   = defaultTabWidth+  , pstateLinePrefix = "" }
Hledger/Utils/Regex.hs view
@@ -136,7 +136,7 @@ -- | Make a nice error message for a regexp error. mkRegexErr :: Text -> Maybe a -> Either RegexError a mkRegexErr s = maybe (Left errmsg) Right-  where errmsg = T.unpack $ "This regular expression is malformed, please correct it:\n" <> s+  where errmsg = T.unpack $ "This regular expression is invalid or unsupported, please correct it:\n" <> s  -- Convert a Regexp string to a compiled Regex, throw an error toRegex' :: Text -> Regexp
Hledger/Utils/String.hs view
@@ -190,7 +190,7 @@ -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails. words' :: String -> [String] words' "" = []-words' s  = map stripquotes $ fromparse $ parsewithString p s+words' s  = map stripquotes $ fromparse $ parsewithString p s  -- PARTIAL     where       p = (singleQuotedPattern <|> doubleQuotedPattern <|> patterns) `sepBy` skipNonNewlineSpaces1           -- eof
Hledger/Utils/Test.hs view
@@ -31,15 +31,15 @@ -- import Test.Tasty.QuickCheck as QC -- import Test.Tasty.SmallCheck as SC import Text.Megaparsec-import Text.Megaparsec.Custom++import Hledger.Utils.IO (pshow)+import Hledger.Utils.Parse   ( HledgerParseErrorData,     FinalParseError,     attachSource,     customErrorBundlePretty,     finalErrorBundlePretty,   )--import Hledger.Utils.IO (pshow)  -- * tasty helpers 
+ Hledger/Write/Csv.hs view
@@ -0,0 +1,56 @@+--- * -*- outline-regexp:"--- \\*"; -*-+--- ** doc+{-|++CSV utilities.++-}++--- ** language+{-# LANGUAGE OverloadedStrings    #-}++--- ** exports+module Hledger.Write.Csv (+  CSV, CsvRecord, CsvValue,+  printCSV,+  printTSV,+  -- * Tests+  tests_CsvUtils,+)+where++--- ** imports+import Prelude hiding (Applicative(..))+import Data.List (intersperse)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TB++import Hledger.Utils++--- ** doctest setup+-- $setup+-- >>> :set -XOverloadedStrings++type CSV       = [CsvRecord]+type CsvRecord = [CsvValue]+type CsvValue  = Text++printCSV :: CSV -> TL.Text+printCSV = TB.toLazyText . unlinesB . map printRecord+    where printRecord = foldMap TB.fromText . intersperse "," . map printField+          printField = wrap "\"" "\"" . T.replace "\"" "\"\""++printTSV :: CSV -> TL.Text+printTSV = TB.toLazyText . unlinesB . map printRecord+    where printRecord = foldMap TB.fromText . intersperse "\t" . map printField+          printField = T.map replaceWhitespace+          replaceWhitespace c | c `elem` ['\t', '\n', '\r'] = ' '+          replaceWhitespace c = c++--- ** tests++tests_CsvUtils :: TestTree+tests_CsvUtils = testGroup "CsvUtils" [+  ]
+ Hledger/Write/Html.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+Export spreadsheet table data as HTML table.++This is derived from <https://hackage.haskell.org/package/classify-frog-0.2.4.3/src/src/Spreadsheet/Format.hs>+-}+module Hledger.Write.Html (+    printHtml,+    ) where++import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))++import qualified Lucid.Base as LucidBase+import qualified Lucid+import Data.Foldable (for_)+++printHtml :: [[Cell (Lucid.Html ())]] -> Lucid.Html ()+printHtml table =+    Lucid.table_ $ for_ table $ \row ->+    Lucid.tr_ $ for_ row $ \cell ->+    formatCell cell++formatCell :: Cell (Lucid.Html ()) -> Lucid.Html ()+formatCell cell =+    let str = cellContent cell in+    case cellStyle cell of+        Head -> Lucid.th_ str+        Body emph ->+            let align =+                    case cellType cell of+                        TypeString -> []+                        TypeDate -> []+                        _ -> [LucidBase.makeAttribute "align" "right"]+                withEmph =+                    case emph of+                        Item -> id+                        Total -> Lucid.b_+            in  Lucid.td_ align $ withEmph str
+ Hledger/Write/Ods.hs view
@@ -0,0 +1,259 @@+{- |+Export table data as OpenDocument Spreadsheet+<https://docs.oasis-open.org/office/OpenDocument/v1.3/>.+This format supports character encodings, fixed header rows and columns,+number formatting, text styles, merged cells, formulas, hyperlinks.+Currently we support Flat ODS, a plain uncompressed XML format.++This is derived from <https://hackage.haskell.org/package/classify-frog-0.2.4.3/src/src/Spreadsheet/Format.hs>++-}+module Hledger.Write.Ods (+    printFods,+    ) where++import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..))+import Hledger.Data.Types (CommoditySymbol, AmountPrecision(..))+import Hledger.Data.Types (acommodity, aquantity, astyle, asprecision)++import qualified Data.Text.Lazy as TL+import qualified Data.Text as T+import Data.Text (Text)++import qualified Data.Map as Map+import qualified Data.Set as Set+import Data.Foldable (fold)+import Data.Map (Map)+import Data.Set (Set)+import Data.Maybe (mapMaybe)++import qualified System.IO as IO+import Text.Printf (printf)+++printFods ::+    IO.TextEncoding ->+    Map Text ((Maybe Int, Maybe Int), [[Cell Text]]) -> TL.Text+printFods encoding tables =+    let fileOpen customStyles =+          map (map (\c -> case c of '\'' -> '"'; _ -> c)) $+          printf "<?xml version='1.0' encoding='%s'?>" (show encoding) :+          "<office:document" :+          "  office:mimetype='application/vnd.oasis.opendocument.spreadsheet'" :+          "  office:version='1.3'" :+          "  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" :+          "  xmlns:xsd='http://www.w3.org/2001/XMLSchema'" :+          "  xmlns:text='urn:oasis:names:tc:opendocument:xmlns:text:1.0'" :+          "  xmlns:style='urn:oasis:names:tc:opendocument:xmlns:style:1.0'" :+          "  xmlns:meta='urn:oasis:names:tc:opendocument:xmlns:meta:1.0'" :+          "  xmlns:config='urn:oasis:names:tc:opendocument:xmlns:config:1.0'" :+          "  xmlns:xlink='http://www.w3.org/1999/xlink'" :+          "  xmlns:fo='urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'" :+          "  xmlns:ooo='http://openoffice.org/2004/office'" :+          "  xmlns:office='urn:oasis:names:tc:opendocument:xmlns:office:1.0'" :+          "  xmlns:table='urn:oasis:names:tc:opendocument:xmlns:table:1.0'" :+          "  xmlns:number='urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'" :+          "  xmlns:of='urn:oasis:names:tc:opendocument:xmlns:of:1.2'" :+          "  xmlns:field='urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0'" :+          "  xmlns:form='urn:oasis:names:tc:opendocument:xmlns:form:1.0'>" :+          "<office:styles>" :+          "  <style:style style:name='head' style:family='table-cell'>" :+          "    <style:paragraph-properties fo:text-align='center'/>" :+          "    <style:text-properties fo:font-weight='bold'/>" :+          "  </style:style>" :+          "  <style:style style:name='foot' style:family='table-cell'>" :+          "    <style:text-properties fo:font-weight='bold'/>" :+          "  </style:style>" :+          "  <style:style style:name='amount' style:family='table-cell'>" :+          "    <style:paragraph-properties fo:text-align='end'/>" :+          "  </style:style>" :+          "  <style:style style:name='total-amount' style:family='table-cell'>" :+          "    <style:paragraph-properties fo:text-align='end'/>" :+          "    <style:text-properties fo:font-weight='bold'/>" :+          "  </style:style>" :+          "  <number:date-style style:name='iso-date'>" :+          "    <number:year number:style='long'/>" :+          "    <number:text>-</number:text>" :+          "    <number:month number:style='long'/>" :+          "    <number:text>-</number:text>" :+          "    <number:day number:style='long'/>" :+          "  </number:date-style>" :+          "  <style:style style:name='date' style:family='table-cell'" :+          "      style:data-style-name='iso-date'/>" :+          "  <style:style style:name='foot-date' style:family='table-cell'" :+          "      style:data-style-name='iso-date'>" :+          "    <style:text-properties fo:font-weight='bold'/>" :+          "  </style:style>" :+          customStyles +++          "</office:styles>" :+          []++        fileClose =+          "</office:document>" :+          []++        tableConfig tableNames =+          " <office:settings>" :+          "  <config:config-item-set config:name='ooo:view-settings'>" :+          "   <config:config-item-map-indexed config:name='Views'>" :+          "    <config:config-item-map-entry>" :+          "     <config:config-item-map-named config:name='Tables'>" :+          (fold $+           flip Map.mapWithKey tableNames $ \tableName (mTopRow,mLeftColumn) ->+             printf "      <config:config-item-map-entry config:name='%s'>" tableName :+             (flip foldMap mLeftColumn $ \leftColumn ->+                "       <config:config-item config:name='HorizontalSplitMode' config:type='short'>2</config:config-item>" :+                printf "       <config:config-item config:name='HorizontalSplitPosition' config:type='int'>%d</config:config-item>" leftColumn :+                printf "       <config:config-item config:name='PositionRight' config:type='int'>%d</config:config-item>" leftColumn :+                []) +++             (flip foldMap mTopRow $ \topRow ->+                "       <config:config-item config:name='VerticalSplitMode' config:type='short'>2</config:config-item>" :+                printf "       <config:config-item config:name='VerticalSplitPosition' config:type='int'>%d</config:config-item>" topRow :+                printf "       <config:config-item config:name='PositionBottom' config:type='int'>%d</config:config-item>" topRow :+                []) +++             "      </config:config-item-map-entry>" :+             []) +++          "     </config:config-item-map-named>" :+          "    </config:config-item-map-entry>" :+          "   </config:config-item-map-indexed>" :+          "  </config:config-item-set>" :+          " </office:settings>" :+          []++        tableOpen name =+          "<office:body>" :+          "<office:spreadsheet>" :+          printf "<table:table table:name='%s'>" name :+          []++        tableClose =+          "</table:table>" :+          "</office:spreadsheet>" :+          "</office:body>" :+          []++    in  TL.unlines $ map (TL.fromStrict . T.pack) $+        fileOpen+          (let styles = cellStyles (foldMap (concat.snd) tables) in+           (numberConfig =<< Set.toList (Set.map snd styles))+           +++           (cellConfig =<< Set.toList styles)) +++        tableConfig (fmap fst tables) +++        (Map.toAscList tables >>= \(name,(_,table)) ->+            tableOpen name +++            (table >>= \row ->+                "<table:table-row>" :+                (row >>= formatCell) +++                "</table:table-row>" :+                []) +++            tableClose) +++        fileClose+++cellStyles :: [Cell Text] -> Set (Emphasis, (CommoditySymbol, AmountPrecision))+cellStyles =+    Set.fromList .+    mapMaybe (\cell ->+        case cellType cell of+            TypeAmount amt ->+                Just+                    (case cellStyle cell of+                        Body emph -> emph+                        Head -> Total,+                     (acommodity amt, asprecision $ astyle amt))+            _ -> Nothing)++numberStyleName :: (CommoditySymbol, AmountPrecision) -> String+numberStyleName (comm, prec) =+    printf "%s-%s" comm $+    case prec of+        NaturalPrecision -> "natural"+        Precision k -> show k++numberConfig :: (CommoditySymbol, AmountPrecision) -> [String]+numberConfig (comm, prec) =+    let precStr =+            case prec of+                NaturalPrecision -> ""+                Precision k -> printf " number:decimal-places='%d'" k+        name = numberStyleName (comm, prec)+    in+    printf "  <number:number-style style:name='number-%s'>" name :+    printf "    <number:number number:min-integer-digits='1'%s/>" precStr :+    printf "    <number:text>%s%s</number:text>"+      (if T.null comm then "" else " ") comm :+    "  </number:number-style>" :+    []++emphasisName :: Emphasis -> String+emphasisName emph =+    case emph of+        Item -> "item"+        Total -> "total"++cellConfig :: (Emphasis, (CommoditySymbol, AmountPrecision)) -> [String]+cellConfig (emph, numParam) =+    let name = numberStyleName numParam in+    let style :: String+        style =+            printf "style:name='%s-%s' style:data-style-name='number-%s'"+                (emphasisName emph) name name in+    case emph of+        Item ->+            printf "  <style:style style:family='table-cell' %s/>" style :+            []+        Total ->+            printf "  <style:style style:family='table-cell' %s>" style :+            "    <style:text-properties fo:font-weight='bold'/>" :+            "  </style:style>" :+            []+++formatCell :: Cell Text -> [String]+formatCell cell =+    let style, valueType :: String+        style =+          case (cellStyle cell, cellType cell) of+            (Body emph, TypeAmount amt) -> tableStyle $ numberStyle emph amt+            (Body Item, TypeString) -> ""+            (Body Item, TypeMixedAmount) -> tableStyle "amount"+            (Body Item, TypeDate) -> tableStyle "date"+            (Body Total, TypeString) -> tableStyle "foot"+            (Body Total, TypeMixedAmount) -> tableStyle "total-amount"+            (Body Total, TypeDate) -> tableStyle "foot-date"+            (Head, _) -> tableStyle "head"+        numberStyle emph amt =+            printf "%s-%s"+                (emphasisName emph)+                (numberStyleName (acommodity amt, asprecision $ astyle amt))+        tableStyle = printf " table:style-name='%s'"++        valueType =+            case cellType cell of+                TypeAmount amt ->+                    printf+                        "office:value-type='float' office:value='%s'"+                        (show $ aquantity amt)+                TypeDate ->+                    printf+                        "office:value-type='date' office:date-value='%s'"+                        (cellContent cell)+                _ -> "office:value-type='string'"++    in+    printf "<table:table-cell%s %s>" style valueType :+    printf "<text:p>%s</text:p>" (escape $ T.unpack $ cellContent cell) :+    "</table:table-cell>" :+    []++escape :: String -> String+escape =+    concatMap $ \c ->+        case c of+            '\n' -> "&#10;"+            '&' -> "&amp;"+            '<' -> "&lt;"+            '>' -> "&gt;"+            '"' -> "&quot;"+            '\'' -> "&apos;"+            _ -> [c]
+ Hledger/Write/Spreadsheet.hs view
@@ -0,0 +1,49 @@+{- |+Rich data type to describe data in a table.+This is the basis for ODS and HTML export.+-}+module Hledger.Write.Spreadsheet (+    Type(..),+    Style(..),+    Emphasis(..),+    Cell(..),+    defaultCell,+    emptyCell,+    ) where++import Hledger.Data.Types (Amount)+++data Type =+      TypeString+    | TypeAmount !Amount+    | TypeMixedAmount+    | TypeDate+    deriving (Eq, Ord, Show)++data Style = Body Emphasis | Head+    deriving (Eq, Ord, Show)++data Emphasis = Item | Total+    deriving (Eq, Ord, Show)++data Cell text =+    Cell {+        cellType :: Type,+        cellStyle :: Style,+        cellContent :: text+    }++instance Functor Cell where+    fmap f (Cell typ style content) = Cell typ style $ f content++defaultCell :: text -> Cell text+defaultCell text =+    Cell {+        cellType = TypeString,+        cellStyle = Body Item,+        cellContent = text+    }++emptyCell :: (Monoid text) => Cell text+emptyCell = defaultCell mempty
− Text/Megaparsec/Custom.hs
@@ -1,437 +0,0 @@--- A bunch of megaparsec helpers for re-parsing etc.--- I think these are generic apart from the HledgerParseError name.--{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} -- new-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-} -- new--module Text.Megaparsec.Custom (-  -- * Custom parse error types-  HledgerParseErrorData,-  HledgerParseErrors,--  -- * Failing with an arbitrary source position-  parseErrorAt,-  parseErrorAtRegion,--  -- * Re-parsing-  SourceExcerpt,-  getExcerptText,--  excerpt_,-  reparseExcerpt,--  -- * Pretty-printing custom parse errors-  customErrorBundlePretty,---  -- * "Final" parse errors-  FinalParseError,-  FinalParseError',-  FinalParseErrorBundle,-  FinalParseErrorBundle',--  -- * Constructing "final" parse errors-  finalError,-  finalFancyFailure,-  finalFail,-  finalCustomFailure,--  -- * Pretty-printing "final" parse errors-  finalErrorBundlePretty,-  attachSource,--  -- * Handling parse errors from include files with "final" parse errors-  parseIncludeFile,-)-where--import Control.Monad.Except (ExceptT, MonadError, catchError, throwError)-import Control.Monad.State.Strict (StateT, evalStateT)-import Control.Monad.Trans.Class (lift)-import qualified Data.List.NonEmpty as NE-import Data.Monoid (Alt(..))-import qualified Data.Set as S-import Data.Text (Text)-import Text.Megaparsec------ * Custom parse error types---- | Custom error data for hledger parsers. Specialised for a 'Text' parse stream.--- ReparseableTextParseErrorData ?-data HledgerParseErrorData-  -- | Fail with a message at a specific source position interval. The-  -- interval must be contained within a single line.-  = ErrorFailAt Int -- Starting offset-                Int -- Ending offset-                String -- Error message-  -- | Re-throw parse errors obtained from the "re-parsing" of an excerpt-  -- of the source text.-  | ErrorReparsing-      (NE.NonEmpty (ParseError Text HledgerParseErrorData)) -- Source fragment parse errors-  deriving (Show, Eq, Ord)---- | A specialised version of ParseErrorBundle: --- a non-empty collection of hledger parse errors, --- equipped with PosState to help pretty-print them.--- Specialised for a 'Text' parse stream.-type HledgerParseErrors = ParseErrorBundle Text HledgerParseErrorData---- We require an 'Ord' instance for 'CustomError' so that they may be--- stored in a 'Set'. The actual instance is inconsequential, so we just--- derive it, but the derived instance requires an (orphan) instance for--- 'ParseError'. Hopefully this does not cause any trouble.--deriving instance Ord (ParseError Text HledgerParseErrorData)---- Note: the pretty-printing of our 'HledgerParseErrorData' type is only partally--- defined in its 'ShowErrorComponent' instance; we perform additional--- adjustments in 'customErrorBundlePretty'.--instance ShowErrorComponent HledgerParseErrorData where-  showErrorComponent (ErrorFailAt _ _ errMsg) = errMsg-  showErrorComponent (ErrorReparsing _) = "" -- dummy value--  errorComponentLen (ErrorFailAt startOffset endOffset _) =-    endOffset - startOffset-  errorComponentLen (ErrorReparsing _) = 1 -- dummy value------ * Failing with an arbitrary source position---- | Fail at a specific source position, given by the raw offset from the--- start of the input stream (the number of tokens processed at that--- point).--parseErrorAt :: Int -> String -> HledgerParseErrorData-parseErrorAt offset = ErrorFailAt offset (offset+1)---- | Fail at a specific source interval, given by the raw offsets of its--- endpoints from the start of the input stream (the numbers of tokens--- processed at those points).------ Note that care must be taken to ensure that the specified interval does--- not span multiple lines of the input source. This will not be checked.--parseErrorAtRegion-  :: Int    -- ^ Start offset-  -> Int    -- ^ End end offset-  -> String -- ^ Error message-  -> HledgerParseErrorData-parseErrorAtRegion startOffset endOffset msg =-  if startOffset < endOffset-    then ErrorFailAt startOffset endOffset msg'-    else ErrorFailAt startOffset (startOffset+1) msg'-  where-    msg' = "\n" ++ msg------ * Re-parsing---- | A fragment of source suitable for "re-parsing". The purpose of this--- data type is to preserve the content and source position of the excerpt--- so that parse errors raised during "re-parsing" may properly reference--- the original source.--data SourceExcerpt = SourceExcerpt Int  -- Offset of beginning of excerpt-                                   Text -- Fragment of source file---- | Get the raw text of a source excerpt.--getExcerptText :: SourceExcerpt -> Text-getExcerptText (SourceExcerpt _ txt) = txt---- | 'excerpt_ p' applies the given parser 'p' and extracts the portion of--- the source consumed by 'p', along with the source position of this--- portion. This is the only way to create a source excerpt suitable for--- "re-parsing" by 'reparseExcerpt'.---- This function could be extended to return the result of 'p', but we don't--- currently need this.--excerpt_ :: MonadParsec HledgerParseErrorData Text m => m a -> m SourceExcerpt-excerpt_ p = do-  offset <- getOffset-  (!txt, _) <- match p-  pure $ SourceExcerpt offset txt---- | 'reparseExcerpt s p' "re-parses" the source excerpt 's' using the--- parser 'p'. Parse errors raised by 'p' will be re-thrown at the source--- position of the source excerpt.------ In order for the correct source file to be displayed when re-throwing--- parse errors, we must ensure that the source file during the use of--- 'reparseExcerpt s p' is the same as that during the use of 'excerpt_'--- that generated the source excerpt 's'. However, we can usually expect--- this condition to be satisfied because, at the time of writing, the--- only changes of source file in the codebase take place through include--- files, and the parser for include files neither accepts nor returns--- 'SourceExcerpt's.--reparseExcerpt-  :: Monad m-  => SourceExcerpt-  -> ParsecT HledgerParseErrorData Text m a-  -> ParsecT HledgerParseErrorData Text m a-reparseExcerpt (SourceExcerpt offset txt) p = do-  (_, res) <- lift $ runParserT' p (offsetInitialState offset txt)-  case res of-    Right result -> pure result-    Left errBundle -> customFailure $ ErrorReparsing $ bundleErrors errBundle--  where-    offsetInitialState :: Int -> s ->-#if MIN_VERSION_megaparsec(8,0,0)-      State s e-#else-      State s-#endif-    offsetInitialState initialOffset s = State-      { stateInput  = s-      , stateOffset = initialOffset-      , statePosState = PosState-        { pstateInput = s-        , pstateOffset = initialOffset-        , pstateSourcePos = initialPos ""-        , pstateTabWidth = defaultTabWidth-        , pstateLinePrefix = ""-        }-#if MIN_VERSION_megaparsec(8,0,0)-      , stateParseErrors = []-#endif-      }----- * Pretty-printing custom parse errors---- | Pretty-print our custom parse errors. It is necessary to use this--- instead of 'errorBundlePretty' when custom parse errors are thrown.------ This function intercepts our custom parse errors and applies final--- adjustments ('finalizeCustomError') before passing them to--- 'errorBundlePretty'. These adjustments are part of the implementation--- of the behaviour of our custom parse errors.------ Note: We must ensure that the offset of the 'PosState' of the provided--- 'ParseErrorBundle' is no larger than the offset specified by a--- 'ErrorFailAt' constructor. This is guaranteed if this offset is set to--- 0 (that is, the beginning of the source file), which is the--- case for 'ParseErrorBundle's returned from 'runParserT'.--customErrorBundlePretty :: HledgerParseErrors -> String-customErrorBundlePretty errBundle =-  let errBundle' = errBundle { bundleErrors =-        NE.sortWith errorOffset $ -- megaparsec requires that the list of errors be sorted by their offsets-        bundleErrors errBundle >>= finalizeCustomError }-  in  errorBundlePretty errBundle'--  where-    finalizeCustomError-      :: ParseError Text HledgerParseErrorData -> NE.NonEmpty (ParseError Text HledgerParseErrorData)-    finalizeCustomError err = case findCustomError err of-      Nothing -> pure err--      Just errFailAt@(ErrorFailAt startOffset _ _) ->-        -- Adjust the offset-        pure $ FancyError startOffset $ S.singleton $ ErrorCustom errFailAt--      Just (ErrorReparsing errs) ->-        -- Extract and finalize the inner errors-        errs >>= finalizeCustomError--    -- If any custom errors are present, arbitrarily take the first one-    -- (since only one custom error should be used at a time).-    findCustomError :: ParseError Text HledgerParseErrorData -> Maybe HledgerParseErrorData-    findCustomError err = case err of-      FancyError _ errSet ->-        finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet-      _ -> Nothing--    finds :: (Foldable t) => (a -> Maybe b) -> t a -> Maybe b-    finds f = getAlt . foldMap (Alt . f)------ * "Final" parse errors------ | A type representing "final" parse errors that cannot be backtracked--- from and are guaranteed to halt parsing. The anti-backtracking--- behaviour is implemented by an 'ExceptT' layer in the parser's monad--- stack, using this type as the 'ExceptT' error type.------ We have three goals for this type:--- (1) it should be possible to convert any parse error into a "final"--- parse error,--- (2) it should be possible to take a parse error thrown from an include--- file and re-throw it in the parent file, and--- (3) the pretty-printing of "final" parse errors should be consistent--- with that of ordinary parse errors, but should also report a stack of--- files for errors thrown from include files.------ In order to pretty-print a "final" parse error (goal 3), it must be--- bundled with include filepaths and its full source text. When a "final"--- parse error is thrown from within a parser, we do not have access to--- the full source, so we must hold the parse error until it can be joined--- with its source (and include filepaths, if it was thrown from an--- include file) by the parser's caller.------ A parse error with include filepaths and its full source text is--- represented by the 'FinalParseErrorBundle' type, while a parse error in--- need of either include filepaths, full source text, or both is--- represented by the 'FinalParseError' type.--data FinalParseError' e-  -- a parse error thrown as a "final" parse error-  = FinalError           (ParseError Text e)-  -- a parse error obtained from running a parser, e.g. using 'runParserT'-  | FinalBundle          (ParseErrorBundle Text e)-  -- a parse error thrown from an include file-  | FinalBundleWithStack (FinalParseErrorBundle' e)-  deriving (Show)--type FinalParseError = FinalParseError' HledgerParseErrorData---- We need a 'Monoid' instance for 'FinalParseError' so that 'ExceptT--- FinalParseError m' is an instance of Alternative and MonadPlus, which--- is needed to use some parser combinators, e.g. 'many'.------ This monoid instance simply takes the first (left-most) error.--instance Semigroup (FinalParseError' e) where-  e <> _ = e--instance Monoid (FinalParseError' e) where-  mempty = FinalError $ FancyError 0 $-            S.singleton (ErrorFail "default parse error")-  mappend = (<>)---- | A type bundling a 'ParseError' with its full source text, filepath,--- and stack of include files. Suitable for pretty-printing.------ Megaparsec's 'ParseErrorBundle' type already bundles a parse error with--- its full source text and filepath, so we just add a stack of include--- files.--data FinalParseErrorBundle' e = FinalParseErrorBundle'-  { finalErrorBundle :: ParseErrorBundle Text e-  , includeFileStack :: [FilePath]-  } deriving (Show)--type FinalParseErrorBundle = FinalParseErrorBundle' HledgerParseErrorData------ * Constructing and throwing final parse errors---- | Convert a "regular" parse error into a "final" parse error.--finalError :: ParseError Text e -> FinalParseError' e-finalError = FinalError---- | Like megaparsec's 'fancyFailure', but as a "final" parse error.--finalFancyFailure-  :: (MonadParsec e s m, MonadError (FinalParseError' e) m)-  => S.Set (ErrorFancy e) -> m a-finalFancyFailure errSet = do-  offset <- getOffset-  throwError $ FinalError $ FancyError offset errSet---- | Like 'fail', but as a "final" parse error.--finalFail-  :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => String -> m a-finalFail = finalFancyFailure . S.singleton . ErrorFail---- | Like megaparsec's 'customFailure', but as a "final" parse error.--finalCustomFailure-  :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => e -> m a-finalCustomFailure = finalFancyFailure . S.singleton . ErrorCustom------ * Pretty-printing "final" parse errors---- | Pretty-print a "final" parse error: print the stack of include files,--- then apply the pretty-printer for parse error bundles. Note that--- 'attachSource' must be used on a "final" parse error before it can be--- pretty-printed.--finalErrorBundlePretty :: FinalParseErrorBundle' HledgerParseErrorData -> String-finalErrorBundlePretty bundle =-     concatMap showIncludeFilepath (includeFileStack bundle)-  <> customErrorBundlePretty (finalErrorBundle bundle)-  where-    showIncludeFilepath path = "in file included from " <> path <> ",\n"---- | Supply a filepath and source text to a "final" parse error so that it--- can be pretty-printed. You must ensure that you provide the appropriate--- source text and filepath.--attachSource-  :: FilePath -> Text -> FinalParseError' e -> FinalParseErrorBundle' e-attachSource filePath sourceText finalParseError = case finalParseError of--  -- A parse error thrown directly with the 'FinalError' constructor-  -- requires both source and filepath.-  FinalError err ->-    let bundle = ParseErrorBundle-          { bundleErrors = err NE.:| []-          , bundlePosState = initialPosState filePath sourceText }-    in  FinalParseErrorBundle'-          { finalErrorBundle = bundle-          , includeFileStack  = [] }--  -- A 'ParseErrorBundle' already has the appropriate source and filepath-  -- and so needs neither.-  FinalBundle peBundle -> FinalParseErrorBundle'-    { finalErrorBundle = peBundle-    , includeFileStack = [] }--  -- A parse error from a 'FinalParseErrorBundle' was thrown from an-  -- include file, so we add the filepath to the stack.-  FinalBundleWithStack fpeBundle -> fpeBundle-    { includeFileStack = filePath : includeFileStack fpeBundle }------ * Handling parse errors from include files with "final" parse errors---- | Parse a file with the given parser and initial state, discarding the--- final state and re-throwing any parse errors as "final" parse errors.--parseIncludeFile-  :: Monad m-  => StateT st (ParsecT HledgerParseErrorData Text (ExceptT FinalParseError m)) a-  -> st-  -> FilePath-  -> Text-  -> StateT st (ParsecT HledgerParseErrorData Text (ExceptT FinalParseError m)) a-parseIncludeFile parser initialState filepath text =-  catchError parser' handler-  where-    parser' = do-      eResult <- lift $ lift $-                  runParserT (evalStateT parser initialState) filepath text-      case eResult of-        Left parseErrorBundle -> throwError $ FinalBundle parseErrorBundle-        Right result -> pure result--    -- Attach source and filepath of the include file to its parse errors-    handler e = throwError $ FinalBundleWithStack $ attachSource filepath text e------ * Helpers---- Like megaparsec's 'initialState', but instead for 'PosState'. Used when--- constructing 'ParseErrorBundle's. The values for "tab width" and "line--- prefix" are taken from 'initialState'.--initialPosState :: FilePath -> Text -> PosState Text-initialPosState filePath sourceText = PosState-  { pstateInput      = sourceText-  , pstateOffset     = 0-  , pstateSourcePos  = initialPos filePath-  , pstateTabWidth   = defaultTabWidth-  , pstateLinePrefix = "" }
Text/Tabular/AsciiWide.hs view
@@ -145,9 +145,7 @@  -- | A version of renderRow which returns the underlying Builder. renderRowB:: TableOpts -> Header Cell -> Builder-renderRowB topts h = renderColumns topts is h-  where is = map cellWidth $ headerContents h-+renderRowB topts h = renderColumns topts ws h where ws = map cellWidth $ headerContents h  verticalBar :: Bool -> Char verticalBar pretty = if pretty then '│' else '|'
hledger-lib.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.36.0.+-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        1.34+version:        1.40 synopsis:       A library providing the core functionality of hledger description:    This library contains hledger's core functionality.                 It is used by most hledger* packages so that they support the same@@ -80,12 +80,15 @@       Hledger.Read       Hledger.Read.Common       Hledger.Read.CsvReader-      Hledger.Read.CsvUtils       Hledger.Read.InputOptions       Hledger.Read.JournalReader       Hledger.Read.RulesReader       Hledger.Read.TimedotReader       Hledger.Read.TimeclockReader+      Hledger.Write.Csv+      Hledger.Write.Ods+      Hledger.Write.Html+      Hledger.Write.Spreadsheet       Hledger.Reports       Hledger.Reports.ReportOptions       Hledger.Reports.ReportTypes@@ -105,7 +108,6 @@       Hledger.Utils.Text       Text.Tabular.AsciiWide   other-modules:-      Text.Megaparsec.Custom       Text.WideString       Paths_hledger_lib   hs-source-dirs:@@ -131,11 +133,12 @@     , data-default >=0.5     , deepseq     , directory-    , doclayout >=0.3 && <0.5+    , doclayout >=0.3 && <0.6     , extra >=1.6.3     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1+    , lucid     , megaparsec >=7.0.0 && <9.7     , microlens >=0.4     , microlens-th >=0.4@@ -193,12 +196,13 @@     , data-default >=0.5     , deepseq     , directory-    , doclayout >=0.3 && <0.5+    , doclayout >=0.3 && <0.6     , doctest >=0.18.1     , extra >=1.6.3     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1+    , lucid     , megaparsec >=7.0.0 && <9.7     , microlens >=0.4     , microlens-th >=0.4@@ -258,12 +262,13 @@     , data-default >=0.5     , deepseq     , directory-    , doclayout >=0.3 && <0.5+    , doclayout >=0.3 && <0.6     , extra >=1.6.3     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1     , hledger-lib+    , lucid     , megaparsec >=7.0.0 && <9.7     , microlens >=0.4     , microlens-th >=0.4