penny-lib (empty) → 0.2.0.0
raw patch · 98 files changed
+12850/−0 lines, 98 filesdep +Decimaldep +basedep +containerssetup-changed
Dependencies added: Decimal, base, containers, explicit-exception, matchers, multiarg, old-locale, parsec, semigroups, split, text, time, transformers
Files
- LICENSE +30/−0
- Penny.hs +88/−0
- Penny/Cabin.hs +15/−0
- Penny/Cabin/Allocate.hs +72/−0
- Penny/Cabin/Balance.hs +21/−0
- Penny/Cabin/Balance/Help.hs +34/−0
- Penny/Cabin/Balance/Options.hs +37/−0
- Penny/Cabin/Balance/Parser.hs +148/−0
- Penny/Cabin/Balance/Tree.hs +448/−0
- Penny/Cabin/Chunk.hs +2408/−0
- Penny/Cabin/Chunk/Switch.hs +26/−0
- Penny/Cabin/Colors.hs +50/−0
- Penny/Cabin/Colors/DarkBackground.hs +38/−0
- Penny/Cabin/Colors/LightBackground.hs +35/−0
- Penny/Cabin/Interface.hs +55/−0
- Penny/Cabin/Meta.hs +15/−0
- Penny/Cabin/Options.hs +33/−0
- Penny/Cabin/Posts.hs +146/−0
- Penny/Cabin/Posts/Allocate.hs +65/−0
- Penny/Cabin/Posts/Allocated.hs +284/−0
- Penny/Cabin/Posts/BottomRows.hs +623/−0
- Penny/Cabin/Posts/Chunk.hs +104/−0
- Penny/Cabin/Posts/Fields.hs +166/−0
- Penny/Cabin/Posts/Growers.hs +610/−0
- Penny/Cabin/Posts/Help.hs +66/−0
- Penny/Cabin/Posts/Meta.hs +70/−0
- Penny/Cabin/Posts/Options.hs +247/−0
- Penny/Cabin/Posts/Parser.hs +326/−0
- Penny/Cabin/Posts/Spacers.hs +108/−0
- Penny/Cabin/Row.hs +113/−0
- Penny/Cabin/TextFormat.hs +173/−0
- Penny/Copper.hs +132/−0
- Penny/Copper/Account.hs +131/−0
- Penny/Copper/Amount.hs +96/−0
- Penny/Copper/Comments.hs +35/−0
- Penny/Copper/Commodity.hs +187/−0
- Penny/Copper/DateTime.hs +138/−0
- Penny/Copper/Entry.hs +43/−0
- Penny/Copper/Flag.hs +30/−0
- Penny/Copper/Item.hs +68/−0
- Penny/Copper/Memos/Posting.hs +39/−0
- Penny/Copper/Memos/Transaction.hs +45/−0
- Penny/Copper/Number.hs +29/−0
- Penny/Copper/Payees.hs +87/−0
- Penny/Copper/Posting.hs +126/−0
- Penny/Copper/Price.hs +97/−0
- Penny/Copper/Qty.hs +283/−0
- Penny/Copper/Tags.hs +37/−0
- Penny/Copper/TopLine.hs +59/−0
- Penny/Copper/Transaction.hs +75/−0
- Penny/Copper/Util.hs +150/−0
- Penny/Liberty.hs +692/−0
- Penny/Liberty/Expressions.hs +63/−0
- Penny/Liberty/Expressions/Infix.hs +218/−0
- Penny/Liberty/Expressions/RPN.hs +112/−0
- Penny/Lincoln.hs +210/−0
- Penny/Lincoln/Balance.hs +107/−0
- Penny/Lincoln/Bits.hs +74/−0
- Penny/Lincoln/Bits/Account.hs +12/−0
- Penny/Lincoln/Bits/Amount.hs +8/−0
- Penny/Lincoln/Bits/Commodity.hs +19/−0
- Penny/Lincoln/Bits/DateTime.hs +57/−0
- Penny/Lincoln/Bits/DrCr.hs +9/−0
- Penny/Lincoln/Bits/Entry.hs +9/−0
- Penny/Lincoln/Bits/Flag.hs +7/−0
- Penny/Lincoln/Bits/Memo.hs +9/−0
- Penny/Lincoln/Bits/Number.hs +6/−0
- Penny/Lincoln/Bits/Payee.hs +7/−0
- Penny/Lincoln/Bits/Price.hs +43/−0
- Penny/Lincoln/Bits/PricePoint.hs +10/−0
- Penny/Lincoln/Bits/Qty.hs +70/−0
- Penny/Lincoln/Bits/Tags.hs +10/−0
- Penny/Lincoln/Builders.hs +62/−0
- Penny/Lincoln/Family.hs +102/−0
- Penny/Lincoln/Family/Child.hs +9/−0
- Penny/Lincoln/Family/Family.hs +50/−0
- Penny/Lincoln/Family/Siblings.hs +54/−0
- Penny/Lincoln/HasText.hs +105/−0
- Penny/Lincoln/Meta.hs +71/−0
- Penny/Lincoln/NestedMap.hs +268/−0
- Penny/Lincoln/Predicates.hs +136/−0
- Penny/Lincoln/PriceDb.hs +139/−0
- Penny/Lincoln/Queries.hs +92/−0
- Penny/Lincoln/Serial.hs +105/−0
- Penny/Lincoln/TextNonEmpty.hs +25/−0
- Penny/Lincoln/Transaction.hs +278/−0
- Penny/Lincoln/Transaction/Unverified.hs +27/−0
- Penny/Shield.hs +88/−0
- Penny/Zinc.hs +29/−0
- Penny/Zinc/Error.hs +11/−0
- Penny/Zinc/Help.hs +125/−0
- Penny/Zinc/Parser.hs +86/−0
- Penny/Zinc/Parser/Defaults.hs +29/−0
- Penny/Zinc/Parser/Filter.hs +265/−0
- Penny/Zinc/Parser/Ledgers.hs +79/−0
- Penny/Zinc/Parser/Report.hs +30/−0
- Setup.hs +2/−0
- penny-lib.cabal +290/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2011-2012 Omari Norman.+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in+ the documentation and/or other materials provided with the+ distribution.++ * Neither the name of Omari Norman nor the names of its+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Penny.hs view
@@ -0,0 +1,88 @@+{- | Penny - double-entry accounting system++Penny is organized into a tree of modules, each with a name. Check out+the links for details on each component of Penny.++"Penny.Cabin" - Penny reports. Depends on Lincoln and Liberty.++"Penny.Copper" - the Penny parser. Depends on Lincoln.++"Penny.Liberty" - Penny command line parser helpers. Depends on+Lincoln and Copper.++"Penny.Lincoln" - the Penny core. Depends on no other Penny components.++"Penny.Shield" the Penny runtime environment++"Penny.Zinc" - the Penny command-line interface. Depends on Cabin,+Copper, Liberty, and Lincoln.++The dependencies are represented as a dot file in doc/dependencies.dot+in the Penny git repository.++This module exports a few functions that are useful for building a+simple command line program with default options. To make anything+more complicated you may need to import modules from deeper down+within the hierarchy, but for a program based on the defaults only you+should be able to import this module only (and nothing else).++-}+module Penny (+ -- * Default time zone+ Copper.DefaultTimeZone (DefaultTimeZone)+ , Copper.utcDefault+ , minsToDefaultTimeZone+ + -- * Radix and grouping characters+ , Copper.RadGroup+ , Copper.periodComma+ , Copper.periodSpace+ , Copper.commaPeriod+ , Copper.commaSpace+ + -- * Reports+ , Cabin.allReportsWithDefaults+ + -- * Parser defaults+ , Z.T(..)+ , Z.defaultFromRuntime+ + -- * Main function+ , defaultPenny+ , customPenny+ + -- * Other useful stuff - for custom reports+ ) where++import qualified Penny.Cabin as Cabin+import qualified Penny.Copper as Copper+import qualified Penny.Lincoln as L+import qualified Penny.Shield as S+import qualified Penny.Zinc as Z++-- | Use to make a DefaultTimeZone based on the number of minutes the+-- time zone is offset from UTC. Make sure the argument you supply is+-- between (-840) and 840; otherwise your program will crash at+-- runtime.+minsToDefaultTimeZone :: Int -> Copper.DefaultTimeZone+minsToDefaultTimeZone i = case L.minsToOffset i of+ Nothing -> error $ "penny: error: minutes out of range of "+ ++ "allowed values for time zone"+ Just tzo -> Copper.DefaultTimeZone tzo++defaultPenny :: Copper.DefaultTimeZone -> Copper.RadGroup -> IO ()+defaultPenny dtz rg = do+ rt <- S.runtime+ let df = Z.defaultFromRuntime dtz rg+ rs = Cabin.allReportsWithDefaults dtz rg+ Z.runPenny rt dtz rg df rs++customPenny ::+ Copper.DefaultTimeZone+ -> Copper.RadGroup+ -> (S.Runtime -> Z.T)+ -> [Cabin.Report]+ -> IO ()+customPenny dtz rg gd rs = do+ rt <- S.runtime+ Z.runPenny rt dtz rg gd rs
+ Penny/Cabin.hs view
@@ -0,0 +1,15 @@+-- | Cabin - Penny reports+module Penny.Cabin (allReportsWithDefaults, I.Report(..)) where++import qualified Penny.Cabin.Balance as B+import qualified Penny.Cabin.Posts as P+import qualified Penny.Copper as C+import qualified Penny.Cabin.Interface as I++allReportsWithDefaults ::+ C.DefaultTimeZone+ -> C.RadGroup+ -> [I.Report]+allReportsWithDefaults dtz rg =+ [B.defaultBalanceReport, P.defaultPostsReport dtz rg]+
+ Penny/Cabin/Allocate.hs view
@@ -0,0 +1,72 @@+module Penny.Cabin.Allocate (+ Allocation,+ allocation,+ unAllocation,+ allocate) where++import qualified Control.Monad.Trans.State as St+import qualified Data.Foldable as F+import qualified Data.Traversable as T+import qualified Data.Map as M++newtype Allocation = Allocation { unAllocation :: Int }+ deriving (Show, Eq, Ord)++allocation :: Int -> Allocation+allocation i =+ if i < 1+ then error "Allocations must be at least 1"+ else Allocation i++-- | Divide up a whole number proportionally into several parts. The+-- sum of the parts is guaranteed to add up to original number.+allocate ::+ Ord k+ => M.Map k Allocation+ -- ^ Maps arbitrary unique values to the allocations, so that you+ -- can look up the corresponding allocations.++ -> Int+ -- ^ Allocated values will add up to this number.+ + -> M.Map k Int+ -- ^ The given number, allocated into proportional parts.++allocate m t = let+ tot = F.sum . fmap (toDouble . unAllocation) $ m+ ratios = fmap ((/tot) . toDouble . unAllocation) m+ rounded = fmap (round . (* (toDouble t))) ratios+ toDouble = fromIntegral :: Int -> Double+ + in if M.null m+ then M.empty+ else adjust rounded t++adjust ::+ Ord k+ => M.Map k Int+ -> Int+ -> M.Map k Int+adjust ws w = let+ wsInts = fmap fromIntegral ws+ diff = (fromIntegral w) - F.sum wsInts in+ if M.null ws+ then M.empty+ else if diff == 0+ then ws+ else let+ ws' = St.evalState (T.mapM adjustMap ws) diff+ in adjust ws' w++-- | The state is the target number minus the current actual total.+adjustMap :: Int -> St.State Int Int+adjustMap w = do+ diff <- St.get+ case compare diff 0 of+ EQ -> return w+ GT -> do+ St.put (pred diff)+ return (succ w)+ LT -> do+ St.put (succ diff)+ return (pred w)
+ Penny/Cabin/Balance.hs view
@@ -0,0 +1,21 @@+-- | The Penny balance report+module Penny.Cabin.Balance (balanceReport, O.defaultOptions,+ O.Options(..),+ defaultBalanceReport) where++import qualified Penny.Cabin.Balance.Parser as P+import qualified Penny.Cabin.Balance.Options as O+import qualified Penny.Cabin.Balance.Help as H+import qualified Penny.Cabin.Interface as I+import qualified Penny.Shield as S++balanceReport :: (S.Runtime -> O.Options) -> I.Report+balanceReport getOpts = I.Report H.help "balance" pr+ where+ pr = do+ fn <- P.parser+ let f rt _ _ bs ps = fn rt (getOpts rt) bs ps+ return f++defaultBalanceReport :: I.Report+defaultBalanceReport = balanceReport O.defaultOptions
+ Penny/Cabin/Balance/Help.hs view
@@ -0,0 +1,34 @@+module Penny.Cabin.Balance.Help where++import qualified Data.Text as X++help :: X.Text+help = X.pack helpStr++helpStr :: String+helpStr = unlines [+ "balance, bal",+ " Show account balances. Accepts ONLY the following options:",+ "",+ " --color yes|no|auto|256",+ " yes: show 8 colors always",+ " no: never show colors",+ " auto: show 8 or 256 colors, but only if stdout is a terminal",+ " 256: show 256 colors always",+ " --background light|dark",+ " Use appropriate color scheme for terminal background",+ "",+ " --show-zero-balances",+ " Show balances that are zero",+ " --hide-zero-balances",+ " Hide balances that are zero",+ "",+ " --convert commodity dateTime",+ " Convert all commodities to the given commodity using their",+ " price at the given date (and, optionally, time.) Fails if",+ " any commodity does not have the necessary price.",+ " -c commodity",+ " Same as \"--convert commodity [right now]\"",+ ""+ ]+
+ Penny/Cabin/Balance/Options.hs view
@@ -0,0 +1,37 @@+-- | Options for the Balance report.+module Penny.Cabin.Balance.Options where++import qualified Data.Text as X+import qualified Penny.Lincoln as L+import qualified Penny.Cabin.Chunk as Chunk+import qualified Penny.Cabin.Colors as C+import qualified Penny.Cabin.Options as O+import qualified Penny.Shield as S+import qualified Penny.Cabin.Colors.DarkBackground as DB+import qualified Penny.Copper.DateTime as DT+import qualified Penny.Lincoln.Balance as Bal++data Options = Options {+ drCrColors :: C.DrCrColors+ , baseColors :: C.BaseColors+ , balanceFormat :: L.BottomLine -> X.Text+ , colorPref :: Chunk.Colors+ , showZeroBalances :: O.ShowZeroBalances+ , convert :: Maybe (L.Commodity, L.DateTime)+ , defaultTimeZone :: DT.DefaultTimeZone+ }++balanceAsIs :: L.BottomLine -> X.Text+balanceAsIs n = case n of+ L.Zero -> X.pack "--"+ L.NonZero c -> X.pack . show . L.unQty . Bal.qty $ c++defaultOptions :: S.Runtime -> Options+defaultOptions rt = Options {+ drCrColors = DB.drCrColors+ , baseColors = DB.baseColors+ , balanceFormat = balanceAsIs+ , colorPref = O.maxCapableColors rt+ , showZeroBalances = O.ShowZeroBalances False+ , convert = Nothing+ , defaultTimeZone = DT.utcDefault }
+ Penny/Cabin/Balance/Parser.hs view
@@ -0,0 +1,148 @@+module Penny.Cabin.Balance.Parser (parser) where++import qualified Data.Text as X+import qualified Data.Text.Lazy as XL+import Control.Applicative ((<|>), many)+import Control.Monad ((>=>))+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Penny.Cabin.Colors as Col+import qualified Penny.Cabin.Colors.DarkBackground as DB+import qualified Penny.Cabin.Colors.LightBackground as LB+import qualified Penny.Cabin.Chunk as Chk+import qualified Penny.Cabin.Balance.Options as O+import qualified Penny.Cabin.Balance.Tree as Tree+import qualified Penny.Cabin.Options as CO+import qualified Penny.Copper.Commodity as CC+import qualified Penny.Copper.DateTime as CD+import qualified Penny.Liberty as Ly+import qualified Penny.Lincoln as L+import qualified Penny.Shield as S+import System.Console.MultiArg.Prim (Parser)+import qualified System.Console.MultiArg.Combinator as C+import qualified Text.Parsec as Parsec++data Error = BadColorName String+ | BadBackground String+ | BadCommodity String+ | BadDate String+ deriving Show++parser ::+ Parser (S.Runtime+ -> O.Options+ -> [L.Box Ly.LibertyMeta]+ -> [L.PricePoint]+ -> Ex.Exceptional X.Text XL.Text)+parser = do+ ls <- many opts+ let f rt opInit bs ps = do+ let ls' = map (\fn -> fn rt) ls+ errOpParsed = (foldl (>=>) return ls') opInit+ opParsed <- case errOpParsed of+ Ex.Exception e -> Ex.throw . X.pack . show $ e+ Ex.Success g -> return g+ bits <- Tree.report opParsed bs ps+ return+ . Chk.chunksToText (O.colorPref opParsed)+ . concat+ $ bits+ return f++processColorArg ::+ S.Runtime+ -> String+ -> Maybe Chk.Colors+processColorArg rt x+ | x == "yes" = return Chk.Colors8+ | x == "no" = return Chk.Colors0+ | x == "auto" = return (CO.maxCapableColors rt)+ | x == "256" = return Chk.Colors256+ | otherwise = Nothing++parseOpt :: [String] -> [Char] -> C.ArgSpec a -> Parser a+parseOpt ss cs a = C.parseOption [C.OptSpec ss cs a]++color :: Parser (S.Runtime+ -> O.Options+ -> Ex.Exceptional Error O.Options)+color = parseOpt ["color"] "" (C.OneArg f)+ where+ f a1 rt op = case processColorArg rt a1 of+ Nothing -> Ex.throw . BadColorName $ a1+ Just c -> return (op { O.colorPref = c })++processBackgroundArg ::+ String+ -> Maybe (Col.DrCrColors, Col.BaseColors)+processBackgroundArg x+ | x == "light" = return (LB.drCrColors, LB.baseColors)+ | x == "dark" = return (DB.drCrColors, DB.baseColors)+ | otherwise = Nothing+++background :: Parser (O.Options -> Ex.Exceptional Error O.Options)+background = parseOpt ["background"] "" (C.OneArg f)+ where+ f a1 op = case processBackgroundArg a1 of+ Nothing -> Ex.throw . BadBackground $ a1+ Just (dc, base) ->+ return op { O.drCrColors = dc+ , O.baseColors = base }++showZeroBalances ::+ Parser (O.Options -> Ex.Exceptional a O.Options)+showZeroBalances = parseOpt ["show-zero-balances"] "" (C.NoArg f)+ where+ f op =+ return (op {O.showZeroBalances = CO.ShowZeroBalances True })++hideZeroBalances ::+ Parser (O.Options -> Ex.Exceptional a O.Options)+hideZeroBalances = parseOpt ["hide-zero-balances"] "" (C.NoArg f)+ where+ f op =+ return (op {O.showZeroBalances = CO.ShowZeroBalances False })++convertLong ::+ Parser (O.Options -> Ex.Exceptional Error O.Options)+convertLong = parseOpt ["convert"] "" (C.TwoArg f)+ where+ f a1 a2 op = do+ cty <- case Parsec.parse CC.lvl1Cmdty "" (X.pack a1) of+ Left _ -> Ex.throw . BadCommodity $ a1+ Right g -> return g+ let parseDate = CD.dateTime (O.defaultTimeZone op)+ dt <- case Parsec.parse parseDate "" (X.pack a2) of+ Left _ -> Ex.throw . BadDate $ a2+ Right g -> return g+ let op' = op { O.convert = Just (cty, dt) }+ return op'++convertShort :: Parser (S.Runtime+ -> O.Options+ -> Ex.Exceptional Error O.Options)+convertShort = parseOpt [] ['c'] (C.OneArg f)+ where+ f a1 rt op = do+ cty <- case Parsec.parse CC.lvl1Cmdty "" (X.pack a1) of+ Left _ -> Ex.throw . BadCommodity $ a1+ Right g -> return g+ let dt = S.currentTime rt+ op' = op { O.convert = Just (cty, dt) }+ return op'+ ++opts :: Parser (S.Runtime+ -> O.Options+ -> Ex.Exceptional Error O.Options)+opts =+ color+ <|> mkTwoArg background+ <|> mkTwoArg showZeroBalances+ <|> mkTwoArg hideZeroBalances+ <|> mkTwoArg convertLong+ <|> convertShort+ where+ mkTwoArg p = do+ f <- p+ return (\_ op -> f op)
+ Penny/Cabin/Balance/Tree.hs view
@@ -0,0 +1,448 @@+-- | Takes postings and places them into a tree for further+-- processing.+--+-- Steps:+--+-- * 1. [LT.PostingInfo] -> [PriceConverted]+--+-- * 2. [PriceConverted] -> FlatMap+--+-- * 3. FlatMap -> RawBals+--+-- * 4. RawBals -> (SummedBals, TotalBal)+--+-- * 5. (SummedBals, TotalBal) -> (SummedWithIsEven, TotalBal)+--+-- * 6. (SummedWithIsEven, TotalBal) -> (PreSpecMap, TotalBal)+--+-- * 7. (PreSpecMap, TotalBal) -> [Columns PreSpec]+--+-- * 8. [Columns PreSpec] -> [Columns R.ColumnSpec] (strict)+--+-- * 9. [Columns R.ColumnSpec] -> [[Chunk.Bit]] (lazy)+module Penny.Cabin.Balance.Tree (report) where++import Control.Applicative(Applicative(pure, (<*>)), (<$>))+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Control.Monad.Trans.State as St+import qualified Penny.Cabin.Row as R+import qualified Data.Foldable as Fdbl+import qualified Data.Functor.Identity as Id+import qualified Data.Map as M+import qualified Data.Monoid as Monoid+import qualified Penny.Lincoln.NestedMap as NM+import qualified Data.Text as X+import qualified Data.Traversable as Tr+import qualified Penny.Cabin.Options as CO+import qualified Penny.Cabin.Balance.Options as O+import qualified Penny.Cabin.Chunk as Chunk+import qualified Penny.Cabin.Colors as C+import qualified Penny.Liberty as Ly+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Lincoln.Balance as Bal+import qualified Data.Semigroup as S++-- Step 1. Convert prices.+data PriceConverted = PriceConverted {+ pcEntry :: L.Entry+ , pcAccount :: L.Account }++convertOne ::+ L.PriceDb+ -> (L.Commodity, L.DateTime)+ -> L.Entry+ -> Ex.Exceptional X.Text L.Entry+convertOne db (cty, dt) en@(L.Entry dc am@(L.Amount _ fr)) =+ if fr == cty+ then return en+ else do+ let to = L.To cty+ am' <- case L.convert db dt to am of+ Ex.Exception e -> Ex.throw (convertError cty am e)+ Ex.Success g -> return g+ return (L.Entry dc am')++convertError ::+ L.Commodity+ -> L.Amount+ -> L.PriceDbError+ -> X.Text+convertError to (L.Amount _ fr) e =+ let fromErr = L.text (L.Delimited (X.singleton ':')+ (Fdbl.toList . L.unCommodity $ fr))+ toErr = L.text (L.Delimited (X.singleton ':')+ (Fdbl.toList . L.unCommodity $ to))+ in case e of+ L.FromNotFound ->+ X.pack "no data to convert from commodity "+ `X.append` fromErr+ L.ToNotFound ->+ X.pack "no data to convert to commodity "+ `X.append` toErr+ L.CpuNotFound ->+ X.pack "no data to convert from commodity "+ `X.append` fromErr+ `X.append` (X.pack " to commodity ")+ `X.append` toErr+ `X.append` (X.pack " at given date and time")+ ++buildDb :: [L.PricePoint] -> L.PriceDb+buildDb = foldl f L.emptyDb where+ f db pb = L.addPrice db pb++converter ::+ O.Options+ -> [L.PricePoint]+ -> [L.Box Ly.LibertyMeta]+ -> Ex.Exceptional X.Text [PriceConverted]+converter os pbs bs = case O.convert os of+ Nothing ->+ let f b = PriceConverted en ac where+ p = L.boxPostFam b+ en = Q.entry p+ ac = Q.account p+ in return . map f $ bs+ Just pair ->+ let f b = let p = L.boxPostFam b+ en = Q.entry p+ ac = Q.account p+ in do+ en' <- convertOne db pair en+ return (PriceConverted en' ac)+ db = buildDb pbs+ in mapM f bs+++-- Step 2. This puts all the PriceConverteds into a flat map, where+-- the key is the full account name and the value is the balance. If+-- the user desired, zero balances are eliminated from the flat map.+newtype FlatMap = FlatMap { _unFlatMap :: M.Map L.Account Bal.Balance }++toFlatMap :: O.Options -> [PriceConverted] -> FlatMap+toFlatMap o = FlatMap . foldr f M.empty where+ remove = not . CO.unShowZeroBalances . O.showZeroBalances $ o+ f pc m =+ let a = pcAccount pc+ bal = Bal.entryToBalance . pcEntry $ pc+ in case M.lookup a m of+ Nothing -> M.insert a bal m+ Just oldBal ->+ let added = Bal.addBalances oldBal bal+ newBal = if remove+ then Bal.removeZeroCommodities added+ else Just added+ in case newBal of+ Nothing -> M.delete a m+ Just b' -> M.insert a b' m++-- Step 3+newtype RawBal = RawBal { unRawBal :: S.Option Bal.Balance }+instance Monoid.Monoid RawBal where+ mappend (RawBal b1) (RawBal b2) = RawBal $ b1 `Monoid.mappend` b2+ mempty = RawBal Monoid.mempty++type RawBals = NM.NestedMap L.SubAccountName RawBal++-- | Inserts a pair from the FlatMap into the Balances NestedMap.+insertBalance ::+ L.Account+ -> Bal.Balance+ -> RawBals+ -> RawBals+insertBalance a b rbs = let+ rb = RawBal . S.Option . Just $ b+ subs = Fdbl.toList . L.unAccount $ a+ in NM.insert rbs subs rb+ +rawBalances :: FlatMap -> RawBals+rawBalances (FlatMap m) = M.foldrWithKey insertBalance NM.empty m++-- Step 4+newtype SummedBal =+ SummedBal { unSummedBal :: S.Option Bal.Balance }+instance Monoid.Monoid SummedBal where+ mappend (SummedBal b1) (SummedBal b2) =+ SummedBal $ b1 `Monoid.mappend` b2+ mempty = SummedBal Monoid.mempty+type TotalBal = SummedBal+type SummedBals = NM.NestedMap L.SubAccountName SummedBal++sumBalances :: RawBals -> (SummedBals, TotalBal)+sumBalances rb = (sb, (SummedBal . unRawBal $ tb)) where+ (tb, rawBals) = NM.cumulativeTotal rb+ sb = fmap (SummedBal . unRawBal) rawBals++-- Step 5+type SummedWithIsEven = NM.NestedMap L.SubAccountName (SummedBal, Bool)++makeSummedWithIsEven ::+ (SummedBals, TotalBal)+ -> (SummedWithIsEven, TotalBal)+makeSummedWithIsEven (sb, tb) = (swie, tb) where+ swie = St.evalState (Tr.mapM f sb) False+ f lbl = do+ st <- St.get+ St.put (not st)+ return (lbl, st)++-- Step 6+data Columns a = Columns {+ account :: a+ , drCr :: a+ , commodity :: a+ , quantity :: a+ } deriving Show++instance Functor Columns where+ fmap f c = Columns {+ account = f (account c)+ , drCr = f (drCr c)+ , commodity = f (commodity c)+ , quantity = f (quantity c)+ }++instance Applicative Columns where+ pure a = Columns a a a a+ fn <*> fa = Columns {+ account = (account fn) (account fa)+ , drCr = (drCr fn) (drCr fa)+ , commodity = (commodity fn) (commodity fa)+ , quantity = (quantity fn) (quantity fa)+ }++data PreSpec = PreSpec {+ _justification :: R.Justification+ , _padSpec :: Chunk.TextSpec+ , bits :: [Chunk.Chunk] }++type PreSpecMap = NM.NestedMap L.SubAccountName (Columns PreSpec)++-- | Takes a list of triples from bottomLineChunks and creates three+-- Cells, one each for DrCr, Commodity, and Qty.+bottomLineBalCells ::+ Chunk.TextSpec -- ^ Fill colors+ -> [(Chunk.Chunk, Chunk.Chunk, Chunk.Chunk)]+ -> (PreSpec, PreSpec, PreSpec)+bottomLineBalCells spec ts = (mkSpec dc, mkSpec ct, mkSpec qt) where+ mkSpec ls = PreSpec R.LeftJustify spec ls+ (dc, ct, qt) = foldr f ([], [], []) ts+ f (da, ca, qa) (d, c, q) = (da:d, ca:c, qa:q)+++type IsEven = Bool++-- | Returns a triple (x, y, z), where x is the DrCr chunk, y is the+-- commodity chunk, and z is the qty chunk.+bottomLineBalChunks ::+ O.Options+ -> IsEven+ -> (L.Commodity, Bal.BottomLine)+ -> (Chunk.Chunk, Chunk.Chunk, Chunk.Chunk)+bottomLineBalChunks os isEven (comm, bl) = (dc, cty, qty) where+ dc = Chunk.chunk ts dcTxt+ cty = Chunk.chunk ts ctyTxt+ qty = Chunk.chunk ts qtyTxt+ ctyTxt = L.text (L.Delimited (X.singleton ':') (L.textList comm))+ (ts, dcTxt, qtyTxt) = case bl of+ Bal.Zero -> let+ getTs = if isEven then C.evenZero else C.oddZero+ dcT = X.pack "--"+ qtyT = dcT+ in (getTs . O.drCrColors $ os, dcT, qtyT)+ Bal.NonZero clm -> let+ (getTs, dcT) = case Bal.drCr clm of+ L.Debit ->+ (if isEven then C.evenDebit else C.oddDebit,+ X.pack "Dr")+ L.Credit ->+ (if isEven then C.evenCredit else C.oddCredit,+ X.pack "Cr")+ qTxt = (O.balanceFormat os) bl+ in (getTs . O.drCrColors $ os, dcT, qTxt)+++fillTextSpec ::+ O.Options+ -> IsEven+ -> Chunk.TextSpec+fillTextSpec os isEven = let+ getTs = if isEven then C.evenColors else C.oddColors+ in getTs . O.baseColors $ os+ ++bottomLineCells ::+ O.Options+ -> IsEven+ -> SummedBal+ -> (PreSpec, PreSpec, PreSpec)+bottomLineCells os isEven mayBal = let+ fill = fillTextSpec os isEven+ tsZero = if isEven+ then C.evenZero . O.drCrColors $ os+ else C.oddZero . O.drCrColors $ os+ zeroSpec =+ PreSpec R.LeftJustify+ tsZero [Chunk.chunk tsZero (X.pack "--")]+ zeroSpecs = (zeroSpec, zeroSpec, zeroSpec)+ in case S.getOption . unSummedBal $ mayBal of+ Nothing -> zeroSpecs+ Just bal -> bottomLineBalCells fill+ . map (bottomLineBalChunks os isEven)+ . M.assocs+ . Bal.unBalance+ $ bal++padding :: Int+padding = 2++accountPreSpec ::+ O.Options+ -> IsEven+ -> Int+ -> L.SubAccountName+ -> PreSpec+accountPreSpec os isEven lvl acct = PreSpec j ts [bit] where+ j = R.LeftJustify+ ts = if isEven+ then C.evenColors . O.baseColors $ os+ else C.oddColors . O.baseColors $ os+ bit = Chunk.chunk ts txt where+ txt = pad `X.append` (L.text acct)+ pad = X.replicate (padding * lvl) (X.singleton ' ')+++makePreSpec ::+ O.Options+ -> [(L.SubAccountName, (SummedBal, Bool))]+ -> L.SubAccountName+ -> (SummedBal, Bool)+ -> Columns PreSpec+makePreSpec os ps a (mayBal, isEven) = Columns act dc com qt where+ lvl = length ps + 1+ act = accountPreSpec os isEven lvl a+ (dc, com, qt) = bottomLineCells os isEven mayBal++traverser ::+ O.Options+ -> [(L.SubAccountName, (SummedBal, Bool))]+ -> L.SubAccountName+ -> (SummedBal, Bool)+ -> a+ -> Id.Identity (Maybe (Columns PreSpec))+traverser os hist a mayBal _ =+ return (Just $ makePreSpec os hist a mayBal)++makePreSpecMap ::+ O.Options+ -> (SummedWithIsEven, TotalBal)+ -> (PreSpecMap, TotalBal)+makePreSpecMap os (sb, tb) = (cim, tb) where+ cim = Id.runIdentity (NM.traverseWithTrail (traverser os) sb)++-- Step 7+makeTotalCells ::+ O.Options+ -> SummedBal+ -> Columns PreSpec+makeTotalCells os mayBal = Columns act dc com qt where+ act = accountPreSpec os True 0 tot+ tot = L.SubAccountName $ L.TextNonEmpty 'T' (X.pack "otal")+ (dc, com, qt) = bottomLineCells os True mayBal++makeColumnList :: O.Options -> (PreSpecMap, TotalBal) -> [Columns PreSpec]+makeColumnList os (cim, tb) = totCols : restCols where+ totCols = makeTotalCells os tb+ restCols = Fdbl.toList cim+++-- Step 8++-- | When given a list of columns, determine the widest row in each+-- column.+maxWidths :: [Columns PreSpec] -> Columns R.Width+maxWidths = Fdbl.foldl' maxWidthPerColumn (pure (R.Width 0))++-- | Applied to a Columns of PreSpec and a Colums of widths, return a+-- Columns that has the wider of the two values.+maxWidthPerColumn ::+ Columns R.Width+ -> Columns PreSpec+ -> Columns R.Width+maxWidthPerColumn w p = f <$> w <*> p where+ f old new = max old (maximum . map Chunk.chunkWidth . bits $ new)+ +-- | Changes a single set of Columns to a set of ColumnSpec of the+-- given width.+preSpecToSpec ::+ Columns R.Width+ -> Columns PreSpec+ -> Columns R.ColumnSpec+preSpecToSpec ws p = f <$> ws <*> p where+ f width (PreSpec j ps bs) = R.ColumnSpec j width ps bs++resizeColumnsInList :: [Columns PreSpec] -> [Columns R.ColumnSpec]+resizeColumnsInList cs = map (preSpecToSpec w) cs where+ w = maxWidths cs+++-- Step 9+widthSpacerAcct :: Int+widthSpacerAcct = 4++widthSpacerDrCr :: Int+widthSpacerDrCr = 1++widthSpacerCommodity :: Int+widthSpacerCommodity = 1++colsToBits ::+ O.Options+ -> IsEven+ -> Columns R.ColumnSpec+ -> [Chunk.Chunk]+colsToBits os isEven (Columns a dc c q) = let+ fillSpec = if isEven+ then C.evenColors . O.baseColors $ os+ else C.oddColors . O.baseColors $ os+ spacer w = R.ColumnSpec j (Chunk.Width w) fillSpec []+ j = R.LeftJustify+ cs = a+ : spacer widthSpacerAcct+ : dc+ : spacer widthSpacerDrCr+ : c+ : spacer widthSpacerCommodity+ : q+ : []+ in R.row cs++colsListToBits ::+ O.Options+ -> [Columns R.ColumnSpec]+ -> [[Chunk.Chunk]]+colsListToBits os = zipWith f bools where+ f b c = colsToBits os b c+ bools = iterate not True+++-- Tie it all together++report ::+ O.Options+ -> [L.Box Ly.LibertyMeta]+ -> [L.PricePoint]+ -> Ex.Exceptional X.Text [[Chunk.Chunk]]+report os ps rs = do+ pcs <- converter os rs ps+ return+ . colsListToBits os+ . resizeColumnsInList+ . makeColumnList os+ . makePreSpecMap os+ . makeSummedWithIsEven+ . sumBalances+ . rawBalances+ . toFlatMap os+ $ pcs
+ Penny/Cabin/Chunk.hs view
@@ -0,0 +1,2408 @@+-- | Handles colors and special effects for text. This module was+-- written using the control sequences documented for xterm in+-- ctlseqs.txt, which is included in the xterm source code (on Debian+-- GNU/Linux systems, it is at+-- \/usr\/share\/doc\/xterm\/ctlseqs.txt.gz). The code in here should also+-- work for any terminal which recognizes ISO 6429 escape sequences+-- (also known as ANSI escape sequences), but only for 8 colors. This+-- module also generates sequences for 256 color xterms; though this+-- works fine with xterm, it might not work on other terminals (I+-- believe it works for other terminals commonly found on Linux+-- systems, such as gnome-terminal and Konsole, but I have not tested+-- these terminals as I do not use them.) Perhaps it also works on+-- Windows or Mac OS X systems and their terminals, though I have not+-- tested them (in particular, Windows could be problematic as not all+-- Windows terminals support ISO 6429.)+--+-- In theory there are more portable ways to generate color codes,+-- such as through curses (or is it ncurses?) but support for ISO 6429+-- is widespread enough that I am prepared to say that if your+-- terminal does not support it, too bad; just use the colorless mode.+module Penny.Cabin.Chunk (+ -- * Colors+ Colors(Colors0, Colors8, Colors256),+ Background8,+ Background256,+ Foreground8,+ Foreground256,++ -- * Chunks+ Chunk,+ chunk,+ Width(Width, unWidth),+ chunkWidth,+ chunksToText,+ + -- * Effects+ Bold(Bold, unBold),+ Underline(Underline, unUnderline),+ Flash(Flash, unFlash),+ Inverse(Inverse, unInverse),++ -- * Style and TextSpec+ + -- | A style is a bundle of attributes that describes text+ -- attributes, such as its color and whether it is bold.+ StyleCommon (StyleCommon, bold, underline, flash, inverse),+ Style8 (Style8, foreground8, background8, common8),+ Style256 (Style256, foreground256, background256, common256),+ defaultStyleCommon,+ defaultStyle8,+ defaultStyle256,+ + TextSpec (TextSpec, style8, style256),+ defaultTextSpec,+ + -- * Specific colors+ -- * 8 color foreground colors+ color8_f_default,+ color8_f_black,+ color8_f_red,+ color8_f_green,+ color8_f_yellow,+ color8_f_blue,+ color8_f_magenta,+ color8_f_cyan,+ color8_f_white,++ -- ** 8 color background colors+ color8_b_default,+ color8_b_black,+ color8_b_red,+ color8_b_green,+ color8_b_yellow,+ color8_b_blue,+ color8_b_magenta,+ color8_b_cyan,+ color8_b_white,++ -- * 256 color foreground colors+ color256_f_default,+ color256_f_0,+ color256_f_1,+ color256_f_2,+ color256_f_3,+ color256_f_4,+ color256_f_5,+ color256_f_6,+ color256_f_7,+ color256_f_8,+ color256_f_9,+ color256_f_10,+ color256_f_11,+ color256_f_12,+ color256_f_13,+ color256_f_14,+ color256_f_15,+ color256_f_16,+ color256_f_17,+ color256_f_18,+ color256_f_19,+ color256_f_20,+ color256_f_21,+ color256_f_22,+ color256_f_23,+ color256_f_24,+ color256_f_25,+ color256_f_26,+ color256_f_27,+ color256_f_28,+ color256_f_29,+ color256_f_30,+ color256_f_31,+ color256_f_32,+ color256_f_33,+ color256_f_34,+ color256_f_35,+ color256_f_36,+ color256_f_37,+ color256_f_38,+ color256_f_39,+ color256_f_40,+ color256_f_41,+ color256_f_42,+ color256_f_43,+ color256_f_44,+ color256_f_45,+ color256_f_46,+ color256_f_47,+ color256_f_48,+ color256_f_49,+ color256_f_50,+ color256_f_51,+ color256_f_52,+ color256_f_53,+ color256_f_54,+ color256_f_55,+ color256_f_56,+ color256_f_57,+ color256_f_58,+ color256_f_59,+ color256_f_60,+ color256_f_61,+ color256_f_62,+ color256_f_63,+ color256_f_64,+ color256_f_65,+ color256_f_66,+ color256_f_67,+ color256_f_68,+ color256_f_69,+ color256_f_70,+ color256_f_71,+ color256_f_72,+ color256_f_73,+ color256_f_74,+ color256_f_75,+ color256_f_76,+ color256_f_77,+ color256_f_78,+ color256_f_79,+ color256_f_80,+ color256_f_81,+ color256_f_82,+ color256_f_83,+ color256_f_84,+ color256_f_85,+ color256_f_86,+ color256_f_87,+ color256_f_88,+ color256_f_89,+ color256_f_90,+ color256_f_91,+ color256_f_92,+ color256_f_93,+ color256_f_94,+ color256_f_95,+ color256_f_96,+ color256_f_97,+ color256_f_98,+ color256_f_99,+ color256_f_100,+ color256_f_101,+ color256_f_102,+ color256_f_103,+ color256_f_104,+ color256_f_105,+ color256_f_106,+ color256_f_107,+ color256_f_108,+ color256_f_109,+ color256_f_110,+ color256_f_111,+ color256_f_112,+ color256_f_113,+ color256_f_114,+ color256_f_115,+ color256_f_116,+ color256_f_117,+ color256_f_118,+ color256_f_119,+ color256_f_120,+ color256_f_121,+ color256_f_122,+ color256_f_123,+ color256_f_124,+ color256_f_125,+ color256_f_126,+ color256_f_127,+ color256_f_128,+ color256_f_129,+ color256_f_130,+ color256_f_131,+ color256_f_132,+ color256_f_133,+ color256_f_134,+ color256_f_135,+ color256_f_136,+ color256_f_137,+ color256_f_138,+ color256_f_139,+ color256_f_140,+ color256_f_141,+ color256_f_142,+ color256_f_143,+ color256_f_144,+ color256_f_145,+ color256_f_146,+ color256_f_147,+ color256_f_148,+ color256_f_149,+ color256_f_150,+ color256_f_151,+ color256_f_152,+ color256_f_153,+ color256_f_154,+ color256_f_155,+ color256_f_156,+ color256_f_157,+ color256_f_158,+ color256_f_159,+ color256_f_160,+ color256_f_161,+ color256_f_162,+ color256_f_163,+ color256_f_164,+ color256_f_165,+ color256_f_166,+ color256_f_167,+ color256_f_168,+ color256_f_169,+ color256_f_170,+ color256_f_171,+ color256_f_172,+ color256_f_173,+ color256_f_174,+ color256_f_175,+ color256_f_176,+ color256_f_177,+ color256_f_178,+ color256_f_179,+ color256_f_180,+ color256_f_181,+ color256_f_182,+ color256_f_183,+ color256_f_184,+ color256_f_185,+ color256_f_186,+ color256_f_187,+ color256_f_188,+ color256_f_189,+ color256_f_190,+ color256_f_191,+ color256_f_192,+ color256_f_193,+ color256_f_194,+ color256_f_195,+ color256_f_196,+ color256_f_197,+ color256_f_198,+ color256_f_199,+ color256_f_200,+ color256_f_201,+ color256_f_202,+ color256_f_203,+ color256_f_204,+ color256_f_205,+ color256_f_206,+ color256_f_207,+ color256_f_208,+ color256_f_209,+ color256_f_210,+ color256_f_211,+ color256_f_212,+ color256_f_213,+ color256_f_214,+ color256_f_215,+ color256_f_216,+ color256_f_217,+ color256_f_218,+ color256_f_219,+ color256_f_220,+ color256_f_221,+ color256_f_222,+ color256_f_223,+ color256_f_224,+ color256_f_225,+ color256_f_226,+ color256_f_227,+ color256_f_228,+ color256_f_229,+ color256_f_230,+ color256_f_231,+ color256_f_232,+ color256_f_233,+ color256_f_234,+ color256_f_235,+ color256_f_236,+ color256_f_237,+ color256_f_238,+ color256_f_239,+ color256_f_240,+ color256_f_241,+ color256_f_242,+ color256_f_243,+ color256_f_244,+ color256_f_245,+ color256_f_246,+ color256_f_247,+ color256_f_248,+ color256_f_249,+ color256_f_250,+ color256_f_251,+ color256_f_252,+ color256_f_253,+ color256_f_254,+ color256_f_255,++ -- ** 256 color background colors+ color256_b_default,+ color256_b_0,+ color256_b_1,+ color256_b_2,+ color256_b_3,+ color256_b_4,+ color256_b_5,+ color256_b_6,+ color256_b_7,+ color256_b_8,+ color256_b_9,+ color256_b_10,+ color256_b_11,+ color256_b_12,+ color256_b_13,+ color256_b_14,+ color256_b_15,+ color256_b_16,+ color256_b_17,+ color256_b_18,+ color256_b_19,+ color256_b_20,+ color256_b_21,+ color256_b_22,+ color256_b_23,+ color256_b_24,+ color256_b_25,+ color256_b_26,+ color256_b_27,+ color256_b_28,+ color256_b_29,+ color256_b_30,+ color256_b_31,+ color256_b_32,+ color256_b_33,+ color256_b_34,+ color256_b_35,+ color256_b_36,+ color256_b_37,+ color256_b_38,+ color256_b_39,+ color256_b_40,+ color256_b_41,+ color256_b_42,+ color256_b_43,+ color256_b_44,+ color256_b_45,+ color256_b_46,+ color256_b_47,+ color256_b_48,+ color256_b_49,+ color256_b_50,+ color256_b_51,+ color256_b_52,+ color256_b_53,+ color256_b_54,+ color256_b_55,+ color256_b_56,+ color256_b_57,+ color256_b_58,+ color256_b_59,+ color256_b_60,+ color256_b_61,+ color256_b_62,+ color256_b_63,+ color256_b_64,+ color256_b_65,+ color256_b_66,+ color256_b_67,+ color256_b_68,+ color256_b_69,+ color256_b_70,+ color256_b_71,+ color256_b_72,+ color256_b_73,+ color256_b_74,+ color256_b_75,+ color256_b_76,+ color256_b_77,+ color256_b_78,+ color256_b_79,+ color256_b_80,+ color256_b_81,+ color256_b_82,+ color256_b_83,+ color256_b_84,+ color256_b_85,+ color256_b_86,+ color256_b_87,+ color256_b_88,+ color256_b_89,+ color256_b_90,+ color256_b_91,+ color256_b_92,+ color256_b_93,+ color256_b_94,+ color256_b_95,+ color256_b_96,+ color256_b_97,+ color256_b_98,+ color256_b_99,+ color256_b_100,+ color256_b_101,+ color256_b_102,+ color256_b_103,+ color256_b_104,+ color256_b_105,+ color256_b_106,+ color256_b_107,+ color256_b_108,+ color256_b_109,+ color256_b_110,+ color256_b_111,+ color256_b_112,+ color256_b_113,+ color256_b_114,+ color256_b_115,+ color256_b_116,+ color256_b_117,+ color256_b_118,+ color256_b_119,+ color256_b_120,+ color256_b_121,+ color256_b_122,+ color256_b_123,+ color256_b_124,+ color256_b_125,+ color256_b_126,+ color256_b_127,+ color256_b_128,+ color256_b_129,+ color256_b_130,+ color256_b_131,+ color256_b_132,+ color256_b_133,+ color256_b_134,+ color256_b_135,+ color256_b_136,+ color256_b_137,+ color256_b_138,+ color256_b_139,+ color256_b_140,+ color256_b_141,+ color256_b_142,+ color256_b_143,+ color256_b_144,+ color256_b_145,+ color256_b_146,+ color256_b_147,+ color256_b_148,+ color256_b_149,+ color256_b_150,+ color256_b_151,+ color256_b_152,+ color256_b_153,+ color256_b_154,+ color256_b_155,+ color256_b_156,+ color256_b_157,+ color256_b_158,+ color256_b_159,+ color256_b_160,+ color256_b_161,+ color256_b_162,+ color256_b_163,+ color256_b_164,+ color256_b_165,+ color256_b_166,+ color256_b_167,+ color256_b_168,+ color256_b_169,+ color256_b_170,+ color256_b_171,+ color256_b_172,+ color256_b_173,+ color256_b_174,+ color256_b_175,+ color256_b_176,+ color256_b_177,+ color256_b_178,+ color256_b_179,+ color256_b_180,+ color256_b_181,+ color256_b_182,+ color256_b_183,+ color256_b_184,+ color256_b_185,+ color256_b_186,+ color256_b_187,+ color256_b_188,+ color256_b_189,+ color256_b_190,+ color256_b_191,+ color256_b_192,+ color256_b_193,+ color256_b_194,+ color256_b_195,+ color256_b_196,+ color256_b_197,+ color256_b_198,+ color256_b_199,+ color256_b_200,+ color256_b_201,+ color256_b_202,+ color256_b_203,+ color256_b_204,+ color256_b_205,+ color256_b_206,+ color256_b_207,+ color256_b_208,+ color256_b_209,+ color256_b_210,+ color256_b_211,+ color256_b_212,+ color256_b_213,+ color256_b_214,+ color256_b_215,+ color256_b_216,+ color256_b_217,+ color256_b_218,+ color256_b_219,+ color256_b_220,+ color256_b_221,+ color256_b_222,+ color256_b_223,+ color256_b_224,+ color256_b_225,+ color256_b_226,+ color256_b_227,+ color256_b_228,+ color256_b_229,+ color256_b_230,+ color256_b_231,+ color256_b_232,+ color256_b_233,+ color256_b_234,+ color256_b_235,+ color256_b_236,+ color256_b_237,+ color256_b_238,+ color256_b_239,+ color256_b_240,+ color256_b_241,+ color256_b_242,+ color256_b_243,+ color256_b_244,+ color256_b_245,+ color256_b_246,+ color256_b_247,+ color256_b_248,+ color256_b_249,+ color256_b_250,+ color256_b_251,+ color256_b_252,+ color256_b_253,+ color256_b_254,+ color256_b_255++ ) where+++import Data.Monoid (Monoid, mempty, mappend)+import Data.Text (Text)+import qualified Data.Text as X+import qualified Data.Text.Lazy as XL+import qualified Data.Text.Lazy.Builder as TB++--+-- Colors+--++-- | How many colors to actually show.+data Colors = Colors0 | Colors8 | Colors256+ deriving Show++-- | Background color in an 8 color setting.+newtype Background8 = Background8 { unBackground8 :: Code }++-- | Background color in a 256 color setting.+newtype Background256 = Background256 { unBackground256 :: Code }++-- | Foreground color in an 8 color setting.+newtype Foreground8 = Foreground8 { unForeground8 :: Code }++-- | Foreground color in a 256 color setting.+newtype Foreground256 = Foreground256 { unForeground256 :: Code }+++--+-- Chunks+--++-- | A chunk is some textual data coupled with a description of what+-- color the text is. The chunk knows what colors to use for both+-- foreground color and background color, in both an 8 color terminal+-- and a 256 color terminal. The chunk has only one set of color+-- descriptions. To change colors, you must use a new chunk.+--+-- There is no way to combine chunks. To print large numbers of+-- chunks, lazily build a list of them and then print them using+-- chunksToText.+data Chunk = Chunk TextSpec Text++chunk :: TextSpec -> Text -> Chunk+chunk = Chunk++-- | How wide the text of a chunk is.+newtype Width = Width { unWidth :: Int }+ deriving (Show, Eq, Ord)++instance Monoid Width where+ mempty = Width 0+ mappend (Width w1) (Width w2) = Width $ w1 + w2++chunkWidth :: Chunk -> Width+chunkWidth (Chunk _ t) = Width . X.length $ t++-- | Transforms chunks to a lazy Text. This function runs lazily and+-- in constant time and space.+chunksToText :: Colors -> [Chunk] -> XL.Text+chunksToText c = TB.toLazyText . foldr (builder c) (printReset c)+++--+-- Effects+--+newtype Bold = Bold { unBold :: Bool }+newtype Underline = Underline { unUnderline :: Bool }+newtype Flash = Flash { unFlash :: Bool }+newtype Inverse = Inverse { unInverse :: Bool }++--+-- Styles+--++-- | Style elements that apply in both 8 and 256 color+-- terminals. However, the elements are described separately for 8 and+-- 256 color terminals, so that the text appearance can change+-- depending on how many colors a terminal has.+data StyleCommon = StyleCommon {+ bold :: Bold+ , underline :: Underline+ , flash :: Flash+ , inverse :: Inverse }++-- | Describes text appearance (foreground and background colors, as+-- well as other attributes such as bold) for an 8 color terminal.+data Style8 = Style8 {+ foreground8 :: Foreground8+ , background8 :: Background8+ , common8 :: StyleCommon }++-- | Describes text appearance (foreground and background colors, as+-- well as other attributes such as bold) for a 256 color terminal.+data Style256 = Style256 {+ foreground256 :: Foreground256+ , background256 :: Background256+ , common256 :: StyleCommon }++-- | Has all bold, flash, underline, and inverse turned off.+defaultStyleCommon :: StyleCommon+defaultStyleCommon = StyleCommon {+ bold = Bold False+ , underline = Underline False+ , flash = Flash False+ , inverse = Inverse False }++-- | Uses the default terminal colors (which will vary depending on+-- the terminal).+defaultStyle8 :: Style8+defaultStyle8 = Style8 {+ foreground8 = color8_f_default+ , background8 = color8_b_default+ , common8 = defaultStyleCommon }++-- | Uses the default terminal colors (which will vary depending on+-- the terminal).+defaultStyle256 :: Style256+defaultStyle256 = Style256 {+ foreground256 = color256_f_default+ , background256 = color256_b_default+ , common256 = defaultStyleCommon }++--+-- TextSpec+--++-- | The TextSpec bundles together the styles for the 8 and 256 color+-- terminals, so that the text can be portrayed on any terminal.+data TextSpec = TextSpec {+ style8 :: Style8+ , style256 :: Style256 }++-- | A TextSpec with the default colors on 8 and 256 color terminals,+-- with all attributes turned off.+defaultTextSpec :: TextSpec+defaultTextSpec = TextSpec {+ style8 = defaultStyle8+ , style256 = defaultStyle256 }++--+-- Internal+--++(+++) :: TB.Builder -> TB.Builder -> TB.Builder+(+++) = mappend+infixr 5 +++++builder :: Colors -> Chunk -> TB.Builder -> TB.Builder+builder c (Chunk ts t) acc =+ printReset c+ +++ textSpecCodes c ts + +++ TB.fromText t+ +++ acc++textSpecCodes :: Colors -> TextSpec -> TB.Builder+textSpecCodes c s = case c of+ Colors0 -> mempty+ Colors8 -> style8Colors $ style8 s+ Colors256 -> style256Colors $ style256 s++style8Colors :: Style8 -> TB.Builder+style8Colors s =+ (unCode . unForeground8 . foreground8 $ s)+ +++ (unCode . unBackground8 . background8 $ s)+ +++ (common (common8 s))++style256Colors :: Style256 -> TB.Builder+style256Colors s =+ (unCode . unForeground256 . foreground256 $ s)+ +++ (unCode . unBackground256 . background256 $ s)+ +++ (common (common256 s))++common :: StyleCommon -> TB.Builder+common s =+ (if unBold . bold $ s then unCode boldOn else mempty)+ +++ (if unUnderline . underline $ s+ then unCode underlineOn else mempty)+ +++ (if unFlash . flash $ s+ then unCode flashOn else mempty)+ +++ (if unInverse . inverse $ s+ then unCode inverseOn else mempty)++newtype Code = Code { unCode :: TB.Builder }++code :: String -> Code+code s = Code $+ TB.fromString (toEnum 27:'[':[])+ +++ TB.fromString s+ +++ TB.singleton 'm'++boldOn :: Code+boldOn = code "1"++underlineOn :: Code+underlineOn = code "4"++flashOn :: Code+flashOn = code "5"++inverseOn :: Code+inverseOn = code "7"++resetOn :: Code+resetOn = code "0"++printReset :: Colors -> TB.Builder+printReset c = case c of+ Colors0 -> mempty+ _ -> unCode resetOn++--+-- Color basement+--+color8_f_default :: Foreground8+color8_f_default = Foreground8 . Code $ mempty++color8_f_black :: Foreground8+color8_f_black = Foreground8 . code $ "30"++color8_f_red :: Foreground8+color8_f_red = Foreground8 . code $ "31"++color8_f_green :: Foreground8+color8_f_green = Foreground8 . code $ "32"++color8_f_yellow :: Foreground8+color8_f_yellow = Foreground8 . code $ "33"++color8_f_blue :: Foreground8+color8_f_blue = Foreground8 . code $ "34"++color8_f_magenta :: Foreground8+color8_f_magenta = Foreground8 . code $ "35"++color8_f_cyan :: Foreground8+color8_f_cyan = Foreground8 . code $ "36"++color8_f_white :: Foreground8+color8_f_white = Foreground8 . code $ "37"++color8_b_default :: Background8+color8_b_default = Background8 . Code $ mempty++color8_b_black :: Background8+color8_b_black = Background8 . code $ "40"++color8_b_red :: Background8+color8_b_red = Background8 . code $ "41"++color8_b_green :: Background8+color8_b_green = Background8 . code $ "42"++color8_b_yellow :: Background8+color8_b_yellow = Background8 . code $ "43"++color8_b_blue :: Background8+color8_b_blue = Background8 . code $ "44"++color8_b_magenta :: Background8+color8_b_magenta = Background8 . code $ "45"++color8_b_cyan :: Background8+color8_b_cyan = Background8 . code $ "46"++color8_b_white :: Background8+color8_b_white = Background8 . code $ "47"++color256_f_default :: Foreground256+color256_f_default = Foreground256 . Code $ mempty++color256_f_0 :: Foreground256+color256_f_0 = Foreground256 . code $ "38;5;0"++color256_f_1 :: Foreground256+color256_f_1 = Foreground256 . code $ "38;5;1"++color256_f_2 :: Foreground256+color256_f_2 = Foreground256 . code $ "38;5;2"++color256_f_3 :: Foreground256+color256_f_3 = Foreground256 . code $ "38;5;3"++color256_f_4 :: Foreground256+color256_f_4 = Foreground256 . code $ "38;5;4"++color256_f_5 :: Foreground256+color256_f_5 = Foreground256 . code $ "38;5;5"++color256_f_6 :: Foreground256+color256_f_6 = Foreground256 . code $ "38;5;6"++color256_f_7 :: Foreground256+color256_f_7 = Foreground256 . code $ "38;5;7"++color256_f_8 :: Foreground256+color256_f_8 = Foreground256 . code $ "38;5;8"++color256_f_9 :: Foreground256+color256_f_9 = Foreground256 . code $ "38;5;9"++color256_f_10 :: Foreground256+color256_f_10 = Foreground256 . code $ "38;5;10"++color256_f_11 :: Foreground256+color256_f_11 = Foreground256 . code $ "38;5;11"++color256_f_12 :: Foreground256+color256_f_12 = Foreground256 . code $ "38;5;12"++color256_f_13 :: Foreground256+color256_f_13 = Foreground256 . code $ "38;5;13"++color256_f_14 :: Foreground256+color256_f_14 = Foreground256 . code $ "38;5;14"++color256_f_15 :: Foreground256+color256_f_15 = Foreground256 . code $ "38;5;15"++color256_f_16 :: Foreground256+color256_f_16 = Foreground256 . code $ "38;5;16"++color256_f_17 :: Foreground256+color256_f_17 = Foreground256 . code $ "38;5;17"++color256_f_18 :: Foreground256+color256_f_18 = Foreground256 . code $ "38;5;18"++color256_f_19 :: Foreground256+color256_f_19 = Foreground256 . code $ "38;5;19"++color256_f_20 :: Foreground256+color256_f_20 = Foreground256 . code $ "38;5;20"++color256_f_21 :: Foreground256+color256_f_21 = Foreground256 . code $ "38;5;21"++color256_f_22 :: Foreground256+color256_f_22 = Foreground256 . code $ "38;5;22"++color256_f_23 :: Foreground256+color256_f_23 = Foreground256 . code $ "38;5;23"++color256_f_24 :: Foreground256+color256_f_24 = Foreground256 . code $ "38;5;24"++color256_f_25 :: Foreground256+color256_f_25 = Foreground256 . code $ "38;5;25"++color256_f_26 :: Foreground256+color256_f_26 = Foreground256 . code $ "38;5;26"++color256_f_27 :: Foreground256+color256_f_27 = Foreground256 . code $ "38;5;27"++color256_f_28 :: Foreground256+color256_f_28 = Foreground256 . code $ "38;5;28"++color256_f_29 :: Foreground256+color256_f_29 = Foreground256 . code $ "38;5;29"++color256_f_30 :: Foreground256+color256_f_30 = Foreground256 . code $ "38;5;30"++color256_f_31 :: Foreground256+color256_f_31 = Foreground256 . code $ "38;5;31"++color256_f_32 :: Foreground256+color256_f_32 = Foreground256 . code $ "38;5;32"++color256_f_33 :: Foreground256+color256_f_33 = Foreground256 . code $ "38;5;33"++color256_f_34 :: Foreground256+color256_f_34 = Foreground256 . code $ "38;5;34"++color256_f_35 :: Foreground256+color256_f_35 = Foreground256 . code $ "38;5;35"++color256_f_36 :: Foreground256+color256_f_36 = Foreground256 . code $ "38;5;36"++color256_f_37 :: Foreground256+color256_f_37 = Foreground256 . code $ "38;5;37"++color256_f_38 :: Foreground256+color256_f_38 = Foreground256 . code $ "38;5;38"++color256_f_39 :: Foreground256+color256_f_39 = Foreground256 . code $ "38;5;39"++color256_f_40 :: Foreground256+color256_f_40 = Foreground256 . code $ "38;5;40"++color256_f_41 :: Foreground256+color256_f_41 = Foreground256 . code $ "38;5;41"++color256_f_42 :: Foreground256+color256_f_42 = Foreground256 . code $ "38;5;42"++color256_f_43 :: Foreground256+color256_f_43 = Foreground256 . code $ "38;5;43"++color256_f_44 :: Foreground256+color256_f_44 = Foreground256 . code $ "38;5;44"++color256_f_45 :: Foreground256+color256_f_45 = Foreground256 . code $ "38;5;45"++color256_f_46 :: Foreground256+color256_f_46 = Foreground256 . code $ "38;5;46"++color256_f_47 :: Foreground256+color256_f_47 = Foreground256 . code $ "38;5;47"++color256_f_48 :: Foreground256+color256_f_48 = Foreground256 . code $ "38;5;48"++color256_f_49 :: Foreground256+color256_f_49 = Foreground256 . code $ "38;5;49"++color256_f_50 :: Foreground256+color256_f_50 = Foreground256 . code $ "38;5;50"++color256_f_51 :: Foreground256+color256_f_51 = Foreground256 . code $ "38;5;51"++color256_f_52 :: Foreground256+color256_f_52 = Foreground256 . code $ "38;5;52"++color256_f_53 :: Foreground256+color256_f_53 = Foreground256 . code $ "38;5;53"++color256_f_54 :: Foreground256+color256_f_54 = Foreground256 . code $ "38;5;54"++color256_f_55 :: Foreground256+color256_f_55 = Foreground256 . code $ "38;5;55"++color256_f_56 :: Foreground256+color256_f_56 = Foreground256 . code $ "38;5;56"++color256_f_57 :: Foreground256+color256_f_57 = Foreground256 . code $ "38;5;57"++color256_f_58 :: Foreground256+color256_f_58 = Foreground256 . code $ "38;5;58"++color256_f_59 :: Foreground256+color256_f_59 = Foreground256 . code $ "38;5;59"++color256_f_60 :: Foreground256+color256_f_60 = Foreground256 . code $ "38;5;60"++color256_f_61 :: Foreground256+color256_f_61 = Foreground256 . code $ "38;5;61"++color256_f_62 :: Foreground256+color256_f_62 = Foreground256 . code $ "38;5;62"++color256_f_63 :: Foreground256+color256_f_63 = Foreground256 . code $ "38;5;63"++color256_f_64 :: Foreground256+color256_f_64 = Foreground256 . code $ "38;5;64"++color256_f_65 :: Foreground256+color256_f_65 = Foreground256 . code $ "38;5;65"++color256_f_66 :: Foreground256+color256_f_66 = Foreground256 . code $ "38;5;66"++color256_f_67 :: Foreground256+color256_f_67 = Foreground256 . code $ "38;5;67"++color256_f_68 :: Foreground256+color256_f_68 = Foreground256 . code $ "38;5;68"++color256_f_69 :: Foreground256+color256_f_69 = Foreground256 . code $ "38;5;69"++color256_f_70 :: Foreground256+color256_f_70 = Foreground256 . code $ "38;5;70"++color256_f_71 :: Foreground256+color256_f_71 = Foreground256 . code $ "38;5;71"++color256_f_72 :: Foreground256+color256_f_72 = Foreground256 . code $ "38;5;72"++color256_f_73 :: Foreground256+color256_f_73 = Foreground256 . code $ "38;5;73"++color256_f_74 :: Foreground256+color256_f_74 = Foreground256 . code $ "38;5;74"++color256_f_75 :: Foreground256+color256_f_75 = Foreground256 . code $ "38;5;75"++color256_f_76 :: Foreground256+color256_f_76 = Foreground256 . code $ "38;5;76"++color256_f_77 :: Foreground256+color256_f_77 = Foreground256 . code $ "38;5;77"++color256_f_78 :: Foreground256+color256_f_78 = Foreground256 . code $ "38;5;78"++color256_f_79 :: Foreground256+color256_f_79 = Foreground256 . code $ "38;5;79"++color256_f_80 :: Foreground256+color256_f_80 = Foreground256 . code $ "38;5;80"++color256_f_81 :: Foreground256+color256_f_81 = Foreground256 . code $ "38;5;81"++color256_f_82 :: Foreground256+color256_f_82 = Foreground256 . code $ "38;5;82"++color256_f_83 :: Foreground256+color256_f_83 = Foreground256 . code $ "38;5;83"++color256_f_84 :: Foreground256+color256_f_84 = Foreground256 . code $ "38;5;84"++color256_f_85 :: Foreground256+color256_f_85 = Foreground256 . code $ "38;5;85"++color256_f_86 :: Foreground256+color256_f_86 = Foreground256 . code $ "38;5;86"++color256_f_87 :: Foreground256+color256_f_87 = Foreground256 . code $ "38;5;87"++color256_f_88 :: Foreground256+color256_f_88 = Foreground256 . code $ "38;5;88"++color256_f_89 :: Foreground256+color256_f_89 = Foreground256 . code $ "38;5;89"++color256_f_90 :: Foreground256+color256_f_90 = Foreground256 . code $ "38;5;90"++color256_f_91 :: Foreground256+color256_f_91 = Foreground256 . code $ "38;5;91"++color256_f_92 :: Foreground256+color256_f_92 = Foreground256 . code $ "38;5;92"++color256_f_93 :: Foreground256+color256_f_93 = Foreground256 . code $ "38;5;93"++color256_f_94 :: Foreground256+color256_f_94 = Foreground256 . code $ "38;5;94"++color256_f_95 :: Foreground256+color256_f_95 = Foreground256 . code $ "38;5;95"++color256_f_96 :: Foreground256+color256_f_96 = Foreground256 . code $ "38;5;96"++color256_f_97 :: Foreground256+color256_f_97 = Foreground256 . code $ "38;5;97"++color256_f_98 :: Foreground256+color256_f_98 = Foreground256 . code $ "38;5;98"++color256_f_99 :: Foreground256+color256_f_99 = Foreground256 . code $ "38;5;99"++color256_f_100 :: Foreground256+color256_f_100 = Foreground256 . code $ "38;5;100"++color256_f_101 :: Foreground256+color256_f_101 = Foreground256 . code $ "38;5;101"++color256_f_102 :: Foreground256+color256_f_102 = Foreground256 . code $ "38;5;102"++color256_f_103 :: Foreground256+color256_f_103 = Foreground256 . code $ "38;5;103"++color256_f_104 :: Foreground256+color256_f_104 = Foreground256 . code $ "38;5;104"++color256_f_105 :: Foreground256+color256_f_105 = Foreground256 . code $ "38;5;105"++color256_f_106 :: Foreground256+color256_f_106 = Foreground256 . code $ "38;5;106"++color256_f_107 :: Foreground256+color256_f_107 = Foreground256 . code $ "38;5;107"++color256_f_108 :: Foreground256+color256_f_108 = Foreground256 . code $ "38;5;108"++color256_f_109 :: Foreground256+color256_f_109 = Foreground256 . code $ "38;5;109"++color256_f_110 :: Foreground256+color256_f_110 = Foreground256 . code $ "38;5;110"++color256_f_111 :: Foreground256+color256_f_111 = Foreground256 . code $ "38;5;111"++color256_f_112 :: Foreground256+color256_f_112 = Foreground256 . code $ "38;5;112"++color256_f_113 :: Foreground256+color256_f_113 = Foreground256 . code $ "38;5;113"++color256_f_114 :: Foreground256+color256_f_114 = Foreground256 . code $ "38;5;114"++color256_f_115 :: Foreground256+color256_f_115 = Foreground256 . code $ "38;5;115"++color256_f_116 :: Foreground256+color256_f_116 = Foreground256 . code $ "38;5;116"++color256_f_117 :: Foreground256+color256_f_117 = Foreground256 . code $ "38;5;117"++color256_f_118 :: Foreground256+color256_f_118 = Foreground256 . code $ "38;5;118"++color256_f_119 :: Foreground256+color256_f_119 = Foreground256 . code $ "38;5;119"++color256_f_120 :: Foreground256+color256_f_120 = Foreground256 . code $ "38;5;120"++color256_f_121 :: Foreground256+color256_f_121 = Foreground256 . code $ "38;5;121"++color256_f_122 :: Foreground256+color256_f_122 = Foreground256 . code $ "38;5;122"++color256_f_123 :: Foreground256+color256_f_123 = Foreground256 . code $ "38;5;123"++color256_f_124 :: Foreground256+color256_f_124 = Foreground256 . code $ "38;5;124"++color256_f_125 :: Foreground256+color256_f_125 = Foreground256 . code $ "38;5;125"++color256_f_126 :: Foreground256+color256_f_126 = Foreground256 . code $ "38;5;126"++color256_f_127 :: Foreground256+color256_f_127 = Foreground256 . code $ "38;5;127"++color256_f_128 :: Foreground256+color256_f_128 = Foreground256 . code $ "38;5;128"++color256_f_129 :: Foreground256+color256_f_129 = Foreground256 . code $ "38;5;129"++color256_f_130 :: Foreground256+color256_f_130 = Foreground256 . code $ "38;5;130"++color256_f_131 :: Foreground256+color256_f_131 = Foreground256 . code $ "38;5;131"++color256_f_132 :: Foreground256+color256_f_132 = Foreground256 . code $ "38;5;132"++color256_f_133 :: Foreground256+color256_f_133 = Foreground256 . code $ "38;5;133"++color256_f_134 :: Foreground256+color256_f_134 = Foreground256 . code $ "38;5;134"++color256_f_135 :: Foreground256+color256_f_135 = Foreground256 . code $ "38;5;135"++color256_f_136 :: Foreground256+color256_f_136 = Foreground256 . code $ "38;5;136"++color256_f_137 :: Foreground256+color256_f_137 = Foreground256 . code $ "38;5;137"++color256_f_138 :: Foreground256+color256_f_138 = Foreground256 . code $ "38;5;138"++color256_f_139 :: Foreground256+color256_f_139 = Foreground256 . code $ "38;5;139"++color256_f_140 :: Foreground256+color256_f_140 = Foreground256 . code $ "38;5;140"++color256_f_141 :: Foreground256+color256_f_141 = Foreground256 . code $ "38;5;141"++color256_f_142 :: Foreground256+color256_f_142 = Foreground256 . code $ "38;5;142"++color256_f_143 :: Foreground256+color256_f_143 = Foreground256 . code $ "38;5;143"++color256_f_144 :: Foreground256+color256_f_144 = Foreground256 . code $ "38;5;144"++color256_f_145 :: Foreground256+color256_f_145 = Foreground256 . code $ "38;5;145"++color256_f_146 :: Foreground256+color256_f_146 = Foreground256 . code $ "38;5;146"++color256_f_147 :: Foreground256+color256_f_147 = Foreground256 . code $ "38;5;147"++color256_f_148 :: Foreground256+color256_f_148 = Foreground256 . code $ "38;5;148"++color256_f_149 :: Foreground256+color256_f_149 = Foreground256 . code $ "38;5;149"++color256_f_150 :: Foreground256+color256_f_150 = Foreground256 . code $ "38;5;150"++color256_f_151 :: Foreground256+color256_f_151 = Foreground256 . code $ "38;5;151"++color256_f_152 :: Foreground256+color256_f_152 = Foreground256 . code $ "38;5;152"++color256_f_153 :: Foreground256+color256_f_153 = Foreground256 . code $ "38;5;153"++color256_f_154 :: Foreground256+color256_f_154 = Foreground256 . code $ "38;5;154"++color256_f_155 :: Foreground256+color256_f_155 = Foreground256 . code $ "38;5;155"++color256_f_156 :: Foreground256+color256_f_156 = Foreground256 . code $ "38;5;156"++color256_f_157 :: Foreground256+color256_f_157 = Foreground256 . code $ "38;5;157"++color256_f_158 :: Foreground256+color256_f_158 = Foreground256 . code $ "38;5;158"++color256_f_159 :: Foreground256+color256_f_159 = Foreground256 . code $ "38;5;159"++color256_f_160 :: Foreground256+color256_f_160 = Foreground256 . code $ "38;5;160"++color256_f_161 :: Foreground256+color256_f_161 = Foreground256 . code $ "38;5;161"++color256_f_162 :: Foreground256+color256_f_162 = Foreground256 . code $ "38;5;162"++color256_f_163 :: Foreground256+color256_f_163 = Foreground256 . code $ "38;5;163"++color256_f_164 :: Foreground256+color256_f_164 = Foreground256 . code $ "38;5;164"++color256_f_165 :: Foreground256+color256_f_165 = Foreground256 . code $ "38;5;165"++color256_f_166 :: Foreground256+color256_f_166 = Foreground256 . code $ "38;5;166"++color256_f_167 :: Foreground256+color256_f_167 = Foreground256 . code $ "38;5;167"++color256_f_168 :: Foreground256+color256_f_168 = Foreground256 . code $ "38;5;168"++color256_f_169 :: Foreground256+color256_f_169 = Foreground256 . code $ "38;5;169"++color256_f_170 :: Foreground256+color256_f_170 = Foreground256 . code $ "38;5;170"++color256_f_171 :: Foreground256+color256_f_171 = Foreground256 . code $ "38;5;171"++color256_f_172 :: Foreground256+color256_f_172 = Foreground256 . code $ "38;5;172"++color256_f_173 :: Foreground256+color256_f_173 = Foreground256 . code $ "38;5;173"++color256_f_174 :: Foreground256+color256_f_174 = Foreground256 . code $ "38;5;174"++color256_f_175 :: Foreground256+color256_f_175 = Foreground256 . code $ "38;5;175"++color256_f_176 :: Foreground256+color256_f_176 = Foreground256 . code $ "38;5;176"++color256_f_177 :: Foreground256+color256_f_177 = Foreground256 . code $ "38;5;177"++color256_f_178 :: Foreground256+color256_f_178 = Foreground256 . code $ "38;5;178"++color256_f_179 :: Foreground256+color256_f_179 = Foreground256 . code $ "38;5;179"++color256_f_180 :: Foreground256+color256_f_180 = Foreground256 . code $ "38;5;180"++color256_f_181 :: Foreground256+color256_f_181 = Foreground256 . code $ "38;5;181"++color256_f_182 :: Foreground256+color256_f_182 = Foreground256 . code $ "38;5;182"++color256_f_183 :: Foreground256+color256_f_183 = Foreground256 . code $ "38;5;183"++color256_f_184 :: Foreground256+color256_f_184 = Foreground256 . code $ "38;5;184"++color256_f_185 :: Foreground256+color256_f_185 = Foreground256 . code $ "38;5;185"++color256_f_186 :: Foreground256+color256_f_186 = Foreground256 . code $ "38;5;186"++color256_f_187 :: Foreground256+color256_f_187 = Foreground256 . code $ "38;5;187"++color256_f_188 :: Foreground256+color256_f_188 = Foreground256 . code $ "38;5;188"++color256_f_189 :: Foreground256+color256_f_189 = Foreground256 . code $ "38;5;189"++color256_f_190 :: Foreground256+color256_f_190 = Foreground256 . code $ "38;5;190"++color256_f_191 :: Foreground256+color256_f_191 = Foreground256 . code $ "38;5;191"++color256_f_192 :: Foreground256+color256_f_192 = Foreground256 . code $ "38;5;192"++color256_f_193 :: Foreground256+color256_f_193 = Foreground256 . code $ "38;5;193"++color256_f_194 :: Foreground256+color256_f_194 = Foreground256 . code $ "38;5;194"++color256_f_195 :: Foreground256+color256_f_195 = Foreground256 . code $ "38;5;195"++color256_f_196 :: Foreground256+color256_f_196 = Foreground256 . code $ "38;5;196"++color256_f_197 :: Foreground256+color256_f_197 = Foreground256 . code $ "38;5;197"++color256_f_198 :: Foreground256+color256_f_198 = Foreground256 . code $ "38;5;198"++color256_f_199 :: Foreground256+color256_f_199 = Foreground256 . code $ "38;5;199"++color256_f_200 :: Foreground256+color256_f_200 = Foreground256 . code $ "38;5;200"++color256_f_201 :: Foreground256+color256_f_201 = Foreground256 . code $ "38;5;201"++color256_f_202 :: Foreground256+color256_f_202 = Foreground256 . code $ "38;5;202"++color256_f_203 :: Foreground256+color256_f_203 = Foreground256 . code $ "38;5;203"++color256_f_204 :: Foreground256+color256_f_204 = Foreground256 . code $ "38;5;204"++color256_f_205 :: Foreground256+color256_f_205 = Foreground256 . code $ "38;5;205"++color256_f_206 :: Foreground256+color256_f_206 = Foreground256 . code $ "38;5;206"++color256_f_207 :: Foreground256+color256_f_207 = Foreground256 . code $ "38;5;207"++color256_f_208 :: Foreground256+color256_f_208 = Foreground256 . code $ "38;5;208"++color256_f_209 :: Foreground256+color256_f_209 = Foreground256 . code $ "38;5;209"++color256_f_210 :: Foreground256+color256_f_210 = Foreground256 . code $ "38;5;210"++color256_f_211 :: Foreground256+color256_f_211 = Foreground256 . code $ "38;5;211"++color256_f_212 :: Foreground256+color256_f_212 = Foreground256 . code $ "38;5;212"++color256_f_213 :: Foreground256+color256_f_213 = Foreground256 . code $ "38;5;213"++color256_f_214 :: Foreground256+color256_f_214 = Foreground256 . code $ "38;5;214"++color256_f_215 :: Foreground256+color256_f_215 = Foreground256 . code $ "38;5;215"++color256_f_216 :: Foreground256+color256_f_216 = Foreground256 . code $ "38;5;216"++color256_f_217 :: Foreground256+color256_f_217 = Foreground256 . code $ "38;5;217"++color256_f_218 :: Foreground256+color256_f_218 = Foreground256 . code $ "38;5;218"++color256_f_219 :: Foreground256+color256_f_219 = Foreground256 . code $ "38;5;219"++color256_f_220 :: Foreground256+color256_f_220 = Foreground256 . code $ "38;5;220"++color256_f_221 :: Foreground256+color256_f_221 = Foreground256 . code $ "38;5;221"++color256_f_222 :: Foreground256+color256_f_222 = Foreground256 . code $ "38;5;222"++color256_f_223 :: Foreground256+color256_f_223 = Foreground256 . code $ "38;5;223"++color256_f_224 :: Foreground256+color256_f_224 = Foreground256 . code $ "38;5;224"++color256_f_225 :: Foreground256+color256_f_225 = Foreground256 . code $ "38;5;225"++color256_f_226 :: Foreground256+color256_f_226 = Foreground256 . code $ "38;5;226"++color256_f_227 :: Foreground256+color256_f_227 = Foreground256 . code $ "38;5;227"++color256_f_228 :: Foreground256+color256_f_228 = Foreground256 . code $ "38;5;228"++color256_f_229 :: Foreground256+color256_f_229 = Foreground256 . code $ "38;5;229"++color256_f_230 :: Foreground256+color256_f_230 = Foreground256 . code $ "38;5;230"++color256_f_231 :: Foreground256+color256_f_231 = Foreground256 . code $ "38;5;231"++color256_f_232 :: Foreground256+color256_f_232 = Foreground256 . code $ "38;5;232"++color256_f_233 :: Foreground256+color256_f_233 = Foreground256 . code $ "38;5;233"++color256_f_234 :: Foreground256+color256_f_234 = Foreground256 . code $ "38;5;234"++color256_f_235 :: Foreground256+color256_f_235 = Foreground256 . code $ "38;5;235"++color256_f_236 :: Foreground256+color256_f_236 = Foreground256 . code $ "38;5;236"++color256_f_237 :: Foreground256+color256_f_237 = Foreground256 . code $ "38;5;237"++color256_f_238 :: Foreground256+color256_f_238 = Foreground256 . code $ "38;5;238"++color256_f_239 :: Foreground256+color256_f_239 = Foreground256 . code $ "38;5;239"++color256_f_240 :: Foreground256+color256_f_240 = Foreground256 . code $ "38;5;240"++color256_f_241 :: Foreground256+color256_f_241 = Foreground256 . code $ "38;5;241"++color256_f_242 :: Foreground256+color256_f_242 = Foreground256 . code $ "38;5;242"++color256_f_243 :: Foreground256+color256_f_243 = Foreground256 . code $ "38;5;243"++color256_f_244 :: Foreground256+color256_f_244 = Foreground256 . code $ "38;5;244"++color256_f_245 :: Foreground256+color256_f_245 = Foreground256 . code $ "38;5;245"++color256_f_246 :: Foreground256+color256_f_246 = Foreground256 . code $ "38;5;246"++color256_f_247 :: Foreground256+color256_f_247 = Foreground256 . code $ "38;5;247"++color256_f_248 :: Foreground256+color256_f_248 = Foreground256 . code $ "38;5;248"++color256_f_249 :: Foreground256+color256_f_249 = Foreground256 . code $ "38;5;249"++color256_f_250 :: Foreground256+color256_f_250 = Foreground256 . code $ "38;5;250"++color256_f_251 :: Foreground256+color256_f_251 = Foreground256 . code $ "38;5;251"++color256_f_252 :: Foreground256+color256_f_252 = Foreground256 . code $ "38;5;252"++color256_f_253 :: Foreground256+color256_f_253 = Foreground256 . code $ "38;5;253"++color256_f_254 :: Foreground256+color256_f_254 = Foreground256 . code $ "38;5;254"++color256_f_255 :: Foreground256+color256_f_255 = Foreground256 . code $ "38;5;255"++color256_b_default :: Background256+color256_b_default = Background256 . Code $ mempty++color256_b_0 :: Background256+color256_b_0 = Background256 . code $ "48;5;0"++color256_b_1 :: Background256+color256_b_1 = Background256 . code $ "48;5;1"++color256_b_2 :: Background256+color256_b_2 = Background256 . code $ "48;5;2"++color256_b_3 :: Background256+color256_b_3 = Background256 . code $ "48;5;3"++color256_b_4 :: Background256+color256_b_4 = Background256 . code $ "48;5;4"++color256_b_5 :: Background256+color256_b_5 = Background256 . code $ "48;5;5"++color256_b_6 :: Background256+color256_b_6 = Background256 . code $ "48;5;6"++color256_b_7 :: Background256+color256_b_7 = Background256 . code $ "48;5;7"++color256_b_8 :: Background256+color256_b_8 = Background256 . code $ "48;5;8"++color256_b_9 :: Background256+color256_b_9 = Background256 . code $ "48;5;9"++color256_b_10 :: Background256+color256_b_10 = Background256 . code $ "48;5;10"++color256_b_11 :: Background256+color256_b_11 = Background256 . code $ "48;5;11"++color256_b_12 :: Background256+color256_b_12 = Background256 . code $ "48;5;12"++color256_b_13 :: Background256+color256_b_13 = Background256 . code $ "48;5;13"++color256_b_14 :: Background256+color256_b_14 = Background256 . code $ "48;5;14"++color256_b_15 :: Background256+color256_b_15 = Background256 . code $ "48;5;15"++color256_b_16 :: Background256+color256_b_16 = Background256 . code $ "48;5;16"++color256_b_17 :: Background256+color256_b_17 = Background256 . code $ "48;5;17"++color256_b_18 :: Background256+color256_b_18 = Background256 . code $ "48;5;18"++color256_b_19 :: Background256+color256_b_19 = Background256 . code $ "48;5;19"++color256_b_20 :: Background256+color256_b_20 = Background256 . code $ "48;5;20"++color256_b_21 :: Background256+color256_b_21 = Background256 . code $ "48;5;21"++color256_b_22 :: Background256+color256_b_22 = Background256 . code $ "48;5;22"++color256_b_23 :: Background256+color256_b_23 = Background256 . code $ "48;5;23"++color256_b_24 :: Background256+color256_b_24 = Background256 . code $ "48;5;24"++color256_b_25 :: Background256+color256_b_25 = Background256 . code $ "48;5;25"++color256_b_26 :: Background256+color256_b_26 = Background256 . code $ "48;5;26"++color256_b_27 :: Background256+color256_b_27 = Background256 . code $ "48;5;27"++color256_b_28 :: Background256+color256_b_28 = Background256 . code $ "48;5;28"++color256_b_29 :: Background256+color256_b_29 = Background256 . code $ "48;5;29"++color256_b_30 :: Background256+color256_b_30 = Background256 . code $ "48;5;30"++color256_b_31 :: Background256+color256_b_31 = Background256 . code $ "48;5;31"++color256_b_32 :: Background256+color256_b_32 = Background256 . code $ "48;5;32"++color256_b_33 :: Background256+color256_b_33 = Background256 . code $ "48;5;33"++color256_b_34 :: Background256+color256_b_34 = Background256 . code $ "48;5;34"++color256_b_35 :: Background256+color256_b_35 = Background256 . code $ "48;5;35"++color256_b_36 :: Background256+color256_b_36 = Background256 . code $ "48;5;36"++color256_b_37 :: Background256+color256_b_37 = Background256 . code $ "48;5;37"++color256_b_38 :: Background256+color256_b_38 = Background256 . code $ "48;5;38"++color256_b_39 :: Background256+color256_b_39 = Background256 . code $ "48;5;39"++color256_b_40 :: Background256+color256_b_40 = Background256 . code $ "48;5;40"++color256_b_41 :: Background256+color256_b_41 = Background256 . code $ "48;5;41"++color256_b_42 :: Background256+color256_b_42 = Background256 . code $ "48;5;42"++color256_b_43 :: Background256+color256_b_43 = Background256 . code $ "48;5;43"++color256_b_44 :: Background256+color256_b_44 = Background256 . code $ "48;5;44"++color256_b_45 :: Background256+color256_b_45 = Background256 . code $ "48;5;45"++color256_b_46 :: Background256+color256_b_46 = Background256 . code $ "48;5;46"++color256_b_47 :: Background256+color256_b_47 = Background256 . code $ "48;5;47"++color256_b_48 :: Background256+color256_b_48 = Background256 . code $ "48;5;48"++color256_b_49 :: Background256+color256_b_49 = Background256 . code $ "48;5;49"++color256_b_50 :: Background256+color256_b_50 = Background256 . code $ "48;5;50"++color256_b_51 :: Background256+color256_b_51 = Background256 . code $ "48;5;51"++color256_b_52 :: Background256+color256_b_52 = Background256 . code $ "48;5;52"++color256_b_53 :: Background256+color256_b_53 = Background256 . code $ "48;5;53"++color256_b_54 :: Background256+color256_b_54 = Background256 . code $ "48;5;54"++color256_b_55 :: Background256+color256_b_55 = Background256 . code $ "48;5;55"++color256_b_56 :: Background256+color256_b_56 = Background256 . code $ "48;5;56"++color256_b_57 :: Background256+color256_b_57 = Background256 . code $ "48;5;57"++color256_b_58 :: Background256+color256_b_58 = Background256 . code $ "48;5;58"++color256_b_59 :: Background256+color256_b_59 = Background256 . code $ "48;5;59"++color256_b_60 :: Background256+color256_b_60 = Background256 . code $ "48;5;60"++color256_b_61 :: Background256+color256_b_61 = Background256 . code $ "48;5;61"++color256_b_62 :: Background256+color256_b_62 = Background256 . code $ "48;5;62"++color256_b_63 :: Background256+color256_b_63 = Background256 . code $ "48;5;63"++color256_b_64 :: Background256+color256_b_64 = Background256 . code $ "48;5;64"++color256_b_65 :: Background256+color256_b_65 = Background256 . code $ "48;5;65"++color256_b_66 :: Background256+color256_b_66 = Background256 . code $ "48;5;66"++color256_b_67 :: Background256+color256_b_67 = Background256 . code $ "48;5;67"++color256_b_68 :: Background256+color256_b_68 = Background256 . code $ "48;5;68"++color256_b_69 :: Background256+color256_b_69 = Background256 . code $ "48;5;69"++color256_b_70 :: Background256+color256_b_70 = Background256 . code $ "48;5;70"++color256_b_71 :: Background256+color256_b_71 = Background256 . code $ "48;5;71"++color256_b_72 :: Background256+color256_b_72 = Background256 . code $ "48;5;72"++color256_b_73 :: Background256+color256_b_73 = Background256 . code $ "48;5;73"++color256_b_74 :: Background256+color256_b_74 = Background256 . code $ "48;5;74"++color256_b_75 :: Background256+color256_b_75 = Background256 . code $ "48;5;75"++color256_b_76 :: Background256+color256_b_76 = Background256 . code $ "48;5;76"++color256_b_77 :: Background256+color256_b_77 = Background256 . code $ "48;5;77"++color256_b_78 :: Background256+color256_b_78 = Background256 . code $ "48;5;78"++color256_b_79 :: Background256+color256_b_79 = Background256 . code $ "48;5;79"++color256_b_80 :: Background256+color256_b_80 = Background256 . code $ "48;5;80"++color256_b_81 :: Background256+color256_b_81 = Background256 . code $ "48;5;81"++color256_b_82 :: Background256+color256_b_82 = Background256 . code $ "48;5;82"++color256_b_83 :: Background256+color256_b_83 = Background256 . code $ "48;5;83"++color256_b_84 :: Background256+color256_b_84 = Background256 . code $ "48;5;84"++color256_b_85 :: Background256+color256_b_85 = Background256 . code $ "48;5;85"++color256_b_86 :: Background256+color256_b_86 = Background256 . code $ "48;5;86"++color256_b_87 :: Background256+color256_b_87 = Background256 . code $ "48;5;87"++color256_b_88 :: Background256+color256_b_88 = Background256 . code $ "48;5;88"++color256_b_89 :: Background256+color256_b_89 = Background256 . code $ "48;5;89"++color256_b_90 :: Background256+color256_b_90 = Background256 . code $ "48;5;90"++color256_b_91 :: Background256+color256_b_91 = Background256 . code $ "48;5;91"++color256_b_92 :: Background256+color256_b_92 = Background256 . code $ "48;5;92"++color256_b_93 :: Background256+color256_b_93 = Background256 . code $ "48;5;93"++color256_b_94 :: Background256+color256_b_94 = Background256 . code $ "48;5;94"++color256_b_95 :: Background256+color256_b_95 = Background256 . code $ "48;5;95"++color256_b_96 :: Background256+color256_b_96 = Background256 . code $ "48;5;96"++color256_b_97 :: Background256+color256_b_97 = Background256 . code $ "48;5;97"++color256_b_98 :: Background256+color256_b_98 = Background256 . code $ "48;5;98"++color256_b_99 :: Background256+color256_b_99 = Background256 . code $ "48;5;99"++color256_b_100 :: Background256+color256_b_100 = Background256 . code $ "48;5;100"++color256_b_101 :: Background256+color256_b_101 = Background256 . code $ "48;5;101"++color256_b_102 :: Background256+color256_b_102 = Background256 . code $ "48;5;102"++color256_b_103 :: Background256+color256_b_103 = Background256 . code $ "48;5;103"++color256_b_104 :: Background256+color256_b_104 = Background256 . code $ "48;5;104"++color256_b_105 :: Background256+color256_b_105 = Background256 . code $ "48;5;105"++color256_b_106 :: Background256+color256_b_106 = Background256 . code $ "48;5;106"++color256_b_107 :: Background256+color256_b_107 = Background256 . code $ "48;5;107"++color256_b_108 :: Background256+color256_b_108 = Background256 . code $ "48;5;108"++color256_b_109 :: Background256+color256_b_109 = Background256 . code $ "48;5;109"++color256_b_110 :: Background256+color256_b_110 = Background256 . code $ "48;5;110"++color256_b_111 :: Background256+color256_b_111 = Background256 . code $ "48;5;111"++color256_b_112 :: Background256+color256_b_112 = Background256 . code $ "48;5;112"++color256_b_113 :: Background256+color256_b_113 = Background256 . code $ "48;5;113"++color256_b_114 :: Background256+color256_b_114 = Background256 . code $ "48;5;114"++color256_b_115 :: Background256+color256_b_115 = Background256 . code $ "48;5;115"++color256_b_116 :: Background256+color256_b_116 = Background256 . code $ "48;5;116"++color256_b_117 :: Background256+color256_b_117 = Background256 . code $ "48;5;117"++color256_b_118 :: Background256+color256_b_118 = Background256 . code $ "48;5;118"++color256_b_119 :: Background256+color256_b_119 = Background256 . code $ "48;5;119"++color256_b_120 :: Background256+color256_b_120 = Background256 . code $ "48;5;120"++color256_b_121 :: Background256+color256_b_121 = Background256 . code $ "48;5;121"++color256_b_122 :: Background256+color256_b_122 = Background256 . code $ "48;5;122"++color256_b_123 :: Background256+color256_b_123 = Background256 . code $ "48;5;123"++color256_b_124 :: Background256+color256_b_124 = Background256 . code $ "48;5;124"++color256_b_125 :: Background256+color256_b_125 = Background256 . code $ "48;5;125"++color256_b_126 :: Background256+color256_b_126 = Background256 . code $ "48;5;126"++color256_b_127 :: Background256+color256_b_127 = Background256 . code $ "48;5;127"++color256_b_128 :: Background256+color256_b_128 = Background256 . code $ "48;5;128"++color256_b_129 :: Background256+color256_b_129 = Background256 . code $ "48;5;129"++color256_b_130 :: Background256+color256_b_130 = Background256 . code $ "48;5;130"++color256_b_131 :: Background256+color256_b_131 = Background256 . code $ "48;5;131"++color256_b_132 :: Background256+color256_b_132 = Background256 . code $ "48;5;132"++color256_b_133 :: Background256+color256_b_133 = Background256 . code $ "48;5;133"++color256_b_134 :: Background256+color256_b_134 = Background256 . code $ "48;5;134"++color256_b_135 :: Background256+color256_b_135 = Background256 . code $ "48;5;135"++color256_b_136 :: Background256+color256_b_136 = Background256 . code $ "48;5;136"++color256_b_137 :: Background256+color256_b_137 = Background256 . code $ "48;5;137"++color256_b_138 :: Background256+color256_b_138 = Background256 . code $ "48;5;138"++color256_b_139 :: Background256+color256_b_139 = Background256 . code $ "48;5;139"++color256_b_140 :: Background256+color256_b_140 = Background256 . code $ "48;5;140"++color256_b_141 :: Background256+color256_b_141 = Background256 . code $ "48;5;141"++color256_b_142 :: Background256+color256_b_142 = Background256 . code $ "48;5;142"++color256_b_143 :: Background256+color256_b_143 = Background256 . code $ "48;5;143"++color256_b_144 :: Background256+color256_b_144 = Background256 . code $ "48;5;144"++color256_b_145 :: Background256+color256_b_145 = Background256 . code $ "48;5;145"++color256_b_146 :: Background256+color256_b_146 = Background256 . code $ "48;5;146"++color256_b_147 :: Background256+color256_b_147 = Background256 . code $ "48;5;147"++color256_b_148 :: Background256+color256_b_148 = Background256 . code $ "48;5;148"++color256_b_149 :: Background256+color256_b_149 = Background256 . code $ "48;5;149"++color256_b_150 :: Background256+color256_b_150 = Background256 . code $ "48;5;150"++color256_b_151 :: Background256+color256_b_151 = Background256 . code $ "48;5;151"++color256_b_152 :: Background256+color256_b_152 = Background256 . code $ "48;5;152"++color256_b_153 :: Background256+color256_b_153 = Background256 . code $ "48;5;153"++color256_b_154 :: Background256+color256_b_154 = Background256 . code $ "48;5;154"++color256_b_155 :: Background256+color256_b_155 = Background256 . code $ "48;5;155"++color256_b_156 :: Background256+color256_b_156 = Background256 . code $ "48;5;156"++color256_b_157 :: Background256+color256_b_157 = Background256 . code $ "48;5;157"++color256_b_158 :: Background256+color256_b_158 = Background256 . code $ "48;5;158"++color256_b_159 :: Background256+color256_b_159 = Background256 . code $ "48;5;159"++color256_b_160 :: Background256+color256_b_160 = Background256 . code $ "48;5;160"++color256_b_161 :: Background256+color256_b_161 = Background256 . code $ "48;5;161"++color256_b_162 :: Background256+color256_b_162 = Background256 . code $ "48;5;162"++color256_b_163 :: Background256+color256_b_163 = Background256 . code $ "48;5;163"++color256_b_164 :: Background256+color256_b_164 = Background256 . code $ "48;5;164"++color256_b_165 :: Background256+color256_b_165 = Background256 . code $ "48;5;165"++color256_b_166 :: Background256+color256_b_166 = Background256 . code $ "48;5;166"++color256_b_167 :: Background256+color256_b_167 = Background256 . code $ "48;5;167"++color256_b_168 :: Background256+color256_b_168 = Background256 . code $ "48;5;168"++color256_b_169 :: Background256+color256_b_169 = Background256 . code $ "48;5;169"++color256_b_170 :: Background256+color256_b_170 = Background256 . code $ "48;5;170"++color256_b_171 :: Background256+color256_b_171 = Background256 . code $ "48;5;171"++color256_b_172 :: Background256+color256_b_172 = Background256 . code $ "48;5;172"++color256_b_173 :: Background256+color256_b_173 = Background256 . code $ "48;5;173"++color256_b_174 :: Background256+color256_b_174 = Background256 . code $ "48;5;174"++color256_b_175 :: Background256+color256_b_175 = Background256 . code $ "48;5;175"++color256_b_176 :: Background256+color256_b_176 = Background256 . code $ "48;5;176"++color256_b_177 :: Background256+color256_b_177 = Background256 . code $ "48;5;177"++color256_b_178 :: Background256+color256_b_178 = Background256 . code $ "48;5;178"++color256_b_179 :: Background256+color256_b_179 = Background256 . code $ "48;5;179"++color256_b_180 :: Background256+color256_b_180 = Background256 . code $ "48;5;180"++color256_b_181 :: Background256+color256_b_181 = Background256 . code $ "48;5;181"++color256_b_182 :: Background256+color256_b_182 = Background256 . code $ "48;5;182"++color256_b_183 :: Background256+color256_b_183 = Background256 . code $ "48;5;183"++color256_b_184 :: Background256+color256_b_184 = Background256 . code $ "48;5;184"++color256_b_185 :: Background256+color256_b_185 = Background256 . code $ "48;5;185"++color256_b_186 :: Background256+color256_b_186 = Background256 . code $ "48;5;186"++color256_b_187 :: Background256+color256_b_187 = Background256 . code $ "48;5;187"++color256_b_188 :: Background256+color256_b_188 = Background256 . code $ "48;5;188"++color256_b_189 :: Background256+color256_b_189 = Background256 . code $ "48;5;189"++color256_b_190 :: Background256+color256_b_190 = Background256 . code $ "48;5;190"++color256_b_191 :: Background256+color256_b_191 = Background256 . code $ "48;5;191"++color256_b_192 :: Background256+color256_b_192 = Background256 . code $ "48;5;192"++color256_b_193 :: Background256+color256_b_193 = Background256 . code $ "48;5;193"++color256_b_194 :: Background256+color256_b_194 = Background256 . code $ "48;5;194"++color256_b_195 :: Background256+color256_b_195 = Background256 . code $ "48;5;195"++color256_b_196 :: Background256+color256_b_196 = Background256 . code $ "48;5;196"++color256_b_197 :: Background256+color256_b_197 = Background256 . code $ "48;5;197"++color256_b_198 :: Background256+color256_b_198 = Background256 . code $ "48;5;198"++color256_b_199 :: Background256+color256_b_199 = Background256 . code $ "48;5;199"++color256_b_200 :: Background256+color256_b_200 = Background256 . code $ "48;5;200"++color256_b_201 :: Background256+color256_b_201 = Background256 . code $ "48;5;201"++color256_b_202 :: Background256+color256_b_202 = Background256 . code $ "48;5;202"++color256_b_203 :: Background256+color256_b_203 = Background256 . code $ "48;5;203"++color256_b_204 :: Background256+color256_b_204 = Background256 . code $ "48;5;204"++color256_b_205 :: Background256+color256_b_205 = Background256 . code $ "48;5;205"++color256_b_206 :: Background256+color256_b_206 = Background256 . code $ "48;5;206"++color256_b_207 :: Background256+color256_b_207 = Background256 . code $ "48;5;207"++color256_b_208 :: Background256+color256_b_208 = Background256 . code $ "48;5;208"++color256_b_209 :: Background256+color256_b_209 = Background256 . code $ "48;5;209"++color256_b_210 :: Background256+color256_b_210 = Background256 . code $ "48;5;210"++color256_b_211 :: Background256+color256_b_211 = Background256 . code $ "48;5;211"++color256_b_212 :: Background256+color256_b_212 = Background256 . code $ "48;5;212"++color256_b_213 :: Background256+color256_b_213 = Background256 . code $ "48;5;213"++color256_b_214 :: Background256+color256_b_214 = Background256 . code $ "48;5;214"++color256_b_215 :: Background256+color256_b_215 = Background256 . code $ "48;5;215"++color256_b_216 :: Background256+color256_b_216 = Background256 . code $ "48;5;216"++color256_b_217 :: Background256+color256_b_217 = Background256 . code $ "48;5;217"++color256_b_218 :: Background256+color256_b_218 = Background256 . code $ "48;5;218"++color256_b_219 :: Background256+color256_b_219 = Background256 . code $ "48;5;219"++color256_b_220 :: Background256+color256_b_220 = Background256 . code $ "48;5;220"++color256_b_221 :: Background256+color256_b_221 = Background256 . code $ "48;5;221"++color256_b_222 :: Background256+color256_b_222 = Background256 . code $ "48;5;222"++color256_b_223 :: Background256+color256_b_223 = Background256 . code $ "48;5;223"++color256_b_224 :: Background256+color256_b_224 = Background256 . code $ "48;5;224"++color256_b_225 :: Background256+color256_b_225 = Background256 . code $ "48;5;225"++color256_b_226 :: Background256+color256_b_226 = Background256 . code $ "48;5;226"++color256_b_227 :: Background256+color256_b_227 = Background256 . code $ "48;5;227"++color256_b_228 :: Background256+color256_b_228 = Background256 . code $ "48;5;228"++color256_b_229 :: Background256+color256_b_229 = Background256 . code $ "48;5;229"++color256_b_230 :: Background256+color256_b_230 = Background256 . code $ "48;5;230"++color256_b_231 :: Background256+color256_b_231 = Background256 . code $ "48;5;231"++color256_b_232 :: Background256+color256_b_232 = Background256 . code $ "48;5;232"++color256_b_233 :: Background256+color256_b_233 = Background256 . code $ "48;5;233"++color256_b_234 :: Background256+color256_b_234 = Background256 . code $ "48;5;234"++color256_b_235 :: Background256+color256_b_235 = Background256 . code $ "48;5;235"++color256_b_236 :: Background256+color256_b_236 = Background256 . code $ "48;5;236"++color256_b_237 :: Background256+color256_b_237 = Background256 . code $ "48;5;237"++color256_b_238 :: Background256+color256_b_238 = Background256 . code $ "48;5;238"++color256_b_239 :: Background256+color256_b_239 = Background256 . code $ "48;5;239"++color256_b_240 :: Background256+color256_b_240 = Background256 . code $ "48;5;240"++color256_b_241 :: Background256+color256_b_241 = Background256 . code $ "48;5;241"++color256_b_242 :: Background256+color256_b_242 = Background256 . code $ "48;5;242"++color256_b_243 :: Background256+color256_b_243 = Background256 . code $ "48;5;243"++color256_b_244 :: Background256+color256_b_244 = Background256 . code $ "48;5;244"++color256_b_245 :: Background256+color256_b_245 = Background256 . code $ "48;5;245"++color256_b_246 :: Background256+color256_b_246 = Background256 . code $ "48;5;246"++color256_b_247 :: Background256+color256_b_247 = Background256 . code $ "48;5;247"++color256_b_248 :: Background256+color256_b_248 = Background256 . code $ "48;5;248"++color256_b_249 :: Background256+color256_b_249 = Background256 . code $ "48;5;249"++color256_b_250 :: Background256+color256_b_250 = Background256 . code $ "48;5;250"++color256_b_251 :: Background256+color256_b_251 = Background256 . code $ "48;5;251"++color256_b_252 :: Background256+color256_b_252 = Background256 . code $ "48;5;252"++color256_b_253 :: Background256+color256_b_253 = Background256 . code $ "48;5;253"++color256_b_254 :: Background256+color256_b_254 = Background256 . code $ "48;5;254"++color256_b_255 :: Background256+color256_b_255 = Background256 . code $ "48;5;255"+
+ Penny/Cabin/Chunk/Switch.hs view
@@ -0,0 +1,26 @@+module Penny.Cabin.Chunk.Switch where++import qualified Penny.Cabin.Chunk as C++-- | Switch the foreground colors for new ones.+switchForeground ::+ C.Foreground8+ -> C.Foreground256+ -> C.TextSpec+ -> C.TextSpec+switchForeground c8 c256 ts = ts' where+ ts' = C.TextSpec s8' s256'+ s8' = (C.style8 ts) { C.foreground8 = c8 }+ s256' = (C.style256 ts) { C.foreground256 = c256 }++-- | Switch the background colors for new ones.+switchBackground ::+ C.Background8+ -> C.Background256+ -> C.TextSpec+ -> C.TextSpec+switchBackground c8 c256 ts = ts' where+ ts' = C.TextSpec s8' s256'+ s8' = (C.style8 ts) { C.background8 = c8 }+ s256' = (C.style256 ts) { C.background256 = c256 }+
+ Penny/Cabin/Colors.hs view
@@ -0,0 +1,50 @@+module Penny.Cabin.Colors where++import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Balance as Bal+import qualified Penny.Lincoln.Bits as Bits+import qualified Penny.Cabin.Chunk as C+import qualified Penny.Cabin.Meta as M++-- | The colors for debits and credits. These are used for the entry's+-- amount and for the total amounts that accompany each entry. Here,+-- @even@ and @odd@ refer to the the posting's visible number. @Zero@+-- is for the balance entries whose balance is zero.+data DrCrColors =+ DrCrColors { evenDebit :: !C.TextSpec+ , evenCredit :: !C.TextSpec+ , oddDebit :: !C.TextSpec+ , oddCredit :: !C.TextSpec+ , evenZero :: !C.TextSpec+ , oddZero :: !C.TextSpec }++-- | The colors for all fields other than entries and totals.+data BaseColors =+ BaseColors { evenColors :: !C.TextSpec+ , oddColors :: !C.TextSpec }++-- | Change a BaseColors to a TextSpec.+colors :: M.VisibleNum -> BaseColors -> C.TextSpec+colors vn b = let n = L.forward . M.unVisibleNum $ vn in+ if odd n then oddColors b else evenColors b++-- | Change a DrCrColors to a BaseColors; you can then use 'colors' to+-- change that to a TextSpec.+drCrToBaseColors :: Bits.DrCr -> DrCrColors -> BaseColors+drCrToBaseColors dc col = case dc of+ Bits.Debit -> BaseColors (evenDebit col) (oddDebit col)+ Bits.Credit -> BaseColors (evenCredit col) (oddCredit col)++-- | Change a DrCrColors to a BaseColors, based on a balance (unlike+-- entries, balances might be zero.)+bottomLineToBaseColors :: DrCrColors -> Bal.BottomLine -> BaseColors+bottomLineToBaseColors col no = case no of+ Bal.Zero -> BaseColors (evenZero col) (oddZero col)+ Bal.NonZero column -> drCrToBaseColors (Bal.drCr column) col+ +-- | TextSpec to use when showing the lack of a balance.+noBalanceColors :: M.VisibleNum -> DrCrColors -> C.TextSpec+noBalanceColors vn dc =+ if odd . L.forward . M.unVisibleNum $ vn+ then oddZero dc+ else evenZero dc
+ Penny/Cabin/Colors/DarkBackground.hs view
@@ -0,0 +1,38 @@+-- | The default colors; the user may override.+module Penny.Cabin.Colors.DarkBackground where++import qualified Penny.Cabin.Colors as PC+import qualified Penny.Cabin.Chunk as CC+import qualified Penny.Cabin.Chunk.Switch as S++baseColors :: PC.BaseColors+baseColors = PC.BaseColors evenTextSpec oddTextSpec++drCrColors :: PC.DrCrColors+drCrColors = PC.DrCrColors { PC.evenDebit = debit evenTextSpec+ , PC.evenCredit = credit evenTextSpec+ , PC.oddDebit = debit oddTextSpec+ , PC.oddCredit = credit oddTextSpec+ , PC.evenZero = zero evenTextSpec+ , PC.oddZero = zero oddTextSpec }++evenTextSpec :: CC.TextSpec+evenTextSpec = CC.defaultTextSpec++oddTextSpec :: CC.TextSpec+oddTextSpec = S.switchBackground CC.color8_b_default+ CC.color256_b_235 evenTextSpec+ +-- | Debits in 256 colors are orange; in 8 colors, magenta+debit :: CC.TextSpec -> CC.TextSpec+debit = S.switchForeground CC.color8_f_magenta CC.color256_f_208++-- | Credits in 256 colors are cyan; in 8 colors, cyan+credit :: CC.TextSpec -> CC.TextSpec+credit = S.switchForeground CC.color8_f_cyan (CC.color256_f_45)++-- | Zero values are white+zero :: CC.TextSpec -> CC.TextSpec+zero = S.switchForeground CC.color8_f_white (CC.color256_f_15)++
+ Penny/Cabin/Colors/LightBackground.hs view
@@ -0,0 +1,35 @@+-- | The default colors; the user may override.+module Penny.Cabin.Colors.LightBackground where++import qualified Penny.Cabin.Colors as PC+import qualified Penny.Cabin.Chunk as CC+import qualified Penny.Cabin.Chunk.Switch as S++baseColors :: PC.BaseColors+baseColors = PC.BaseColors evenTextSpec oddTextSpec++drCrColors :: PC.DrCrColors+drCrColors = PC.DrCrColors { PC.evenDebit = debit evenTextSpec+ , PC.evenCredit = credit evenTextSpec+ , PC.oddDebit = debit oddTextSpec+ , PC.oddCredit = credit oddTextSpec+ , PC.evenZero = zero evenTextSpec+ , PC.oddZero = zero oddTextSpec }++evenTextSpec :: CC.TextSpec+evenTextSpec = CC.defaultTextSpec++oddTextSpec :: CC.TextSpec+oddTextSpec = S.switchBackground CC.color8_b_default+ (CC.color256_b_247) evenTextSpec+ +debit :: CC.TextSpec -> CC.TextSpec+debit = S.switchForeground CC.color8_f_magenta (CC.color256_f_13)++credit :: CC.TextSpec -> CC.TextSpec+credit = S.switchForeground CC.color8_f_cyan (CC.color256_f_14)++zero :: CC.TextSpec -> CC.TextSpec+zero = S.switchForeground CC.color8_f_white (CC.color256_f_15)++
+ Penny/Cabin/Interface.hs view
@@ -0,0 +1,55 @@+-- | An interface for other Penny components to use. A report is+-- anything that is a 'Report'.+module Penny.Cabin.Interface where++import Control.Monad.Exception.Synchronous (Exceptional)+import qualified Data.Text as X+import qualified Data.Text.Lazy as XL+import Text.Matchers.Text (CaseSensitive)+import System.Console.MultiArg.Prim (Parser)++import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import Penny.Shield (Runtime)++type ReportFunc =+ Runtime+ -- ^ Information only known at runtime, such as the+ -- environment. Does not include any information that is derived+ -- from parsing the command line.++ -> CaseSensitive+ -- ^ Result from previous parses indicating whether the user desires+ -- case sensitivity (this may have been changed in the filtering+ -- options)+ + -> (CaseSensitive -> X.Text -> Exceptional X.Text (X.Text -> Bool))+ -- ^ Result from previous parsers indicating the matcher factory the+ -- user wishes to use+ + -> [L.Box Ly.LibertyMeta]+ -- ^ Postings that will be included in the report+ + -> [L.PricePoint]+ -- ^ PricePoints to be included in the report+ + + -> Exceptional X.Text XL.Text+ -- ^ The exception type is a strict Text, containing the error+ -- message. The success type is a lazy Text, containing the+ -- resulting report.+++data Report =+ Report { help :: X.Text+ -- ^ A strict Text containing a help message.+ + , name :: String+ -- ^ The name of the report+ + , parseReport :: Parser ReportFunc+ -- ^ The parser must parse everything beginning with the+ -- first word after the name of the report (the parser does+ -- not parse the name of the report) up until, but not+ -- including, the first non-option word.+ }
+ Penny/Cabin/Meta.hs view
@@ -0,0 +1,15 @@+module Penny.Cabin.Meta (VisibleNum, unVisibleNum,+ visibleNums ) where++import qualified Penny.Lincoln as L++newtype VisibleNum = VisibleNum { unVisibleNum :: L.Serial }+ deriving (Eq, Show)++visibleNums ::+ (VisibleNum -> a -> b)+ -> [L.Box a]+ -> [L.Box b]+visibleNums f = L.serialItems s' where+ s' ser (L.Box m pf) = L.Box (f (VisibleNum ser) m) pf+
+ Penny/Cabin/Options.hs view
@@ -0,0 +1,33 @@+-- | Options applicable to multiple Cabin reports.++module Penny.Cabin.Options where++import qualified Penny.Cabin.Chunk as C+import qualified Penny.Shield as S++-- | The user's color preference.+data ColorPref = Pref0 | Pref8 | Pref256 | PrefAuto+ deriving Show++maxCapableColors :: S.Runtime -> C.Colors+maxCapableColors r = case S.output r of+ S.NotTTY -> C.Colors0+ S.IsTTY ->+ case lookup "TERM" (S.environment r) of+ Nothing -> C.Colors8+ (Just t) -> if t == "xterm-256color"+ then C.Colors256+ else C.Colors8++autoColors :: ColorPref -> S.Runtime -> C.Colors+autoColors c r = case c of+ Pref0 -> C.Colors0+ Pref8 -> C.Colors8+ Pref256 -> C.Colors256+ PrefAuto -> maxCapableColors r++-- | Whether to show zero balances in reports.+newtype ShowZeroBalances =+ ShowZeroBalances { unShowZeroBalances :: Bool }+ deriving (Show, Eq)+
+ Penny/Cabin/Posts.hs view
@@ -0,0 +1,146 @@+-- | The Penny Postings report+--+-- The Postings report displays postings in a tabular format designed+-- to be read by humans. Some terminology used in the Postings report:+--+-- [@row@] The smallest unit that spans from left to right. A row,+-- however, might consist of more than one screen line. For example,+-- the running balance is shown on the far right side of the Postings+-- report. The running balance might consist of more than one+-- commodity. Each commodity is displayed on its own screen+-- line. However, all these lines put together are displayed in a+-- single row.+--+-- [@column@] The smallest unit that spans from top to bottom.+--+-- [@tranche@] Each posting is displayed in several rows. The group of+-- rows that is displayed for a single posting is called a tranche.+--+-- [@tranche row@] Each tranche has a particular number of rows+-- (currently four); each of these rows is known as a tranche row.+--+-- [@field@] Corresponds to a particular element of the posting, such+-- as whether it is a debit or credit or its payee. The user can+-- select which fields to see.+--+-- [@allocation@] The width of the Payee and Account fields is+-- variable. Generally their width will adjust to fill the entire+-- width of the screen. The allocations of the Payee and Account+-- fields determine how much of the remaining space each field will+-- receive.+--+-- The Postings report is easily customized from the command line to+-- show various fields. However, the order of the fields is not+-- configurable without editing the source code (sorry).++module Penny.Cabin.Posts (+ -- * Defaults+ defaultPostsReport+ + -- * Custom report+ , customPostsReport++ -- * Options+ , O.T(..)+ , O.ReportWidth(..)+ , O.ymd, O.qtyAsIs, O.balanceAsIs, O.defaultWidth+ , O.columnsVarToWidth, O.defaultOptions, O.widthFromRuntime+ , O.defaultFields+ ) where++import Control.Applicative ((<$>))+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Text as X+import qualified Data.Text.Lazy as XL+import qualified Penny.Cabin.Chunk as Chk+import qualified Penny.Cabin.Interface as I+import qualified Penny.Cabin.Posts.Options as O+import qualified Penny.Cabin.Posts.Meta as M+import Penny.Cabin.Posts.Chunk (makeChunk)+import Penny.Cabin.Posts.Parser (parseOptions)+import Penny.Cabin.Posts.Help (help)+import qualified Penny.Copper as C+import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import qualified Penny.Shield as S+import System.Console.MultiArg.Prim (Parser)+import Text.Matchers.Text (CaseSensitive)++-- | When applied to a DefaultTimeZone and a RadGroup, returns a+-- report with the default options.+defaultPostsReport ::+ C.DefaultTimeZone+ -> C.RadGroup+ -> I.Report+defaultPostsReport dtz rg = makeReport f where+ f = parseReportOpts (\rt -> O.defaultOptions dtz rg rt)++-- | Generate a custom Posts report.+customPostsReport ::+ (S.Runtime -> O.T)+ -- ^ Function that, when applied to a Runtime, returns the default+ -- options for the posts report. The options will be overridden by+ -- any options on the command line.++ -> I.Report+customPostsReport = makeReport . parseReportOpts++-- | When passed a ParseReportOpts, makes a Report.+makeReport ::+ Parser I.ReportFunc+ -> I.Report+makeReport f =+ I.Report { I.help = help+ , I.name = "posts"+ , I.parseReport = f }++type Factory = CaseSensitive -> X.Text+ -> Ex.Exceptional X.Text (X.Text -> Bool)++parseReportOpts ::+ (S.Runtime -> O.T)+ -> Parser (S.Runtime+ -> CaseSensitive+ -> Factory+ -> [L.Box Ly.LibertyMeta]+ -> a+ -> Ex.Exceptional X.Text XL.Text)+parseReportOpts frt = do+ getOpts <- parseOptions+ let f rt cs fty bs _ = do+ let optsDefault = frt rt+ optsInit = optsDefault { O.sensitive = cs+ , O.factory = fty }+ optsParsed <- case getOpts rt optsInit of+ Ex.Exception e -> Ex.throw . X.pack . show $ e+ Ex.Success g -> return g+ makeReportTxt optsParsed bs+ return f+ ++-- | Using the options parsed from the command line, print out the+-- report.+makeReportTxt ::+ O.T+ -> [L.Box Ly.LibertyMeta]+ -> Ex.Exceptional X.Text XL.Text+makeReportTxt op bs = fmap mkText postMetaBoxes+ where+ e = X.pack "posts report: filter expression parse failure"+ postMetaBoxes = Ex.fromMaybe e (filterAndAssignMeta op bs)+ mkText = Chk.chunksToText (O.colorPref op) . makeChunk op+ ++-- | Takes a list of Box with LibertyMeta and options as processed+-- from the command line. Filters the list of Box and processes the+-- post filter, then assigns the post metadata. Fails if the+-- expression fails to produce a result.+filterAndAssignMeta ::+ O.T+ -> [L.Box Ly.LibertyMeta]+ -> Maybe [L.Box M.PostMeta]+filterAndAssignMeta op bs =+ M.addMetadata (O.showZeroBalances op)+ <$> M.filterBoxes (O.tokens op) (O.postFilter op) bs++
+ Penny/Cabin/Posts/Allocate.hs view
@@ -0,0 +1,65 @@+module Penny.Cabin.Posts.Allocate (+ Allocation,+ allocation,+ unAllocation,+ allocate) where++import qualified Control.Monad.Trans.State as St+import qualified Data.Foldable as F+import qualified Data.Traversable as T++-- | Allocations are integers. The absolute value of the integer is+-- used, so for practical purposes @-4@ is the same allocation as @4@.+newtype Allocation = Allocation { unAllocation :: Int }+ deriving (Show, Eq, Ord)++allocation :: Int -> Allocation+allocation = Allocation++-- | Properties of the allocated result:+--+-- * If the sum of the absolute values of the allocations is zero,+-- then each of the elements in the result will also be zero.+--+-- * Otherwise, if any allocation is zero, then the corresponding+-- amount in the result will also be zero. The sum of the results will+-- equal the total requested.+allocate ::+ (Functor f, F.Foldable f, T.Traversable f)+ => f Allocation+ -> Int+ -> f Int+allocate m t = let+ tot = F.sum . fmap (toDouble . abs . unAllocation) $ m+ ratios = fmap ((/tot) . toDouble . abs . unAllocation) m+ rounded = fmap (round . (* (toDouble t))) ratios+ toDouble = fromIntegral :: Int -> Double+ in if tot == 0+ then fmap (const 0) m+ else adjust rounded t++adjust :: (Functor f, F.Foldable f, T.Traversable f)+ => f Int+ -> Int+ -> f Int+adjust ws w = let+ wsInts = fmap fromIntegral ws+ diff = (fromIntegral w) - F.sum wsInts in+ if diff == 0+ then ws+ else let+ ws' = St.evalState (T.mapM adjustMap ws) diff+ in adjust ws' w++-- | The state is the target number minus the current actual total.+adjustMap :: Int -> St.State Int Int+adjustMap w = if w == 0 then return 0 else do+ diff <- St.get+ case compare diff 0 of+ EQ -> return w+ GT -> do+ St.put (pred diff)+ return (succ w)+ LT -> do+ St.put (succ diff)+ return (pred w)
+ Penny/Cabin/Posts/Allocated.hs view
@@ -0,0 +1,284 @@+-- | Calculates the allocated cells -- the Payee cell and the Account+-- cell. Here is the logic for this process:+--+-- 1. If neither Payee nor Account appears, do nothing.+--+-- 2. Obtain the width of the growing cells, including the+-- spacers. One of the spacers attached to a field might be omitted:+--+-- a. If the rightmost growing field is TotalQty, include all spacers.+--+-- b. If the rightmost growing field is to the left of Payee, include+-- all spacers.+--+-- c. If the rightmost growing field is to the right of Account but is+-- not TotalQty, omit its spacer.+--+-- 2. Subtract from this sum the width of the Payee and Account+-- spacers:+--+-- a. Subtract the width of Payee spacer if it appears.+--+-- b. Subtract the width of the Account spacer if it appears.+--+-- 3. If the remaining width is 0 or less, do nothing. Return, but+-- indicate in return value that neither Payee nor Account is showing.+--+-- 4. Allocate the remaining width. If only Payee or Account appears,+-- it gets all the width; otherwise, allocate the widths. No special+-- arrangements are made if either field gets an allocation of 0.+--+-- 5. Fill cell contents. Return filled cells.+module Penny.Cabin.Posts.Allocated (payeeAndAcct, Fields(..)) where++import Control.Applicative(Applicative((<*>), pure), (<$>))+import Data.Maybe (catMaybes, isJust)+import Data.List (intersperse)+import qualified Data.Foldable as Fdbl+import qualified Data.Sequence as Seq+import qualified Data.Traversable as T+import qualified Data.Text as X+import qualified Penny.Cabin.Chunk as C+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.Posts.Allocate as A+import qualified Penny.Cabin.Colors as PC+import qualified Penny.Cabin.Posts.Fields as F+import qualified Penny.Cabin.Posts.Growers as G+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Posts.Options as Options+import qualified Penny.Cabin.Posts.Options as O+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Cabin.Posts.Spacers as Spacers+import qualified Penny.Cabin.TextFormat as TF+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Lincoln.HasText as HT++type Box = L.Box M.PostMeta ++data Fields a = Fields {+ payee :: a+ , account :: a+ } deriving (Eq, Show)++-- | Creates Payee and Account cells. The user must have requested the+-- cells. In addition, no cells are created if there is not enough+-- space for them in the report. Returns a Fields; each element of the+-- Fields is Nothing if no cells were created (either because the user+-- did not ask for them, or because there was no room) or Just cs i,+-- where cs is a list of all the cells, and i is the width of all the+-- cells.+payeeAndAcct ::+ G.Fields (Maybe Int)+ -> Options.T+ -> [Box]+ -> Fields (Maybe ([R.ColumnSpec], Int))+payeeAndAcct fs os = allocateCells os ws where+ ws = fieldWidth os ss fs rw+ ss = O.spacers os+ rw = O.width os+++-- | Allocates cells. Returns a pair, with the first element being the+-- list of allocated cells, and the second indicating the width of the+-- cells, which will be greater than zero.+allocateCells ::+ Options.T+ -> Fields Int+ -> [Box]+ -> Fields (Maybe ([R.ColumnSpec], Int))+allocateCells os fs is = let+ cellMakers = Fields allocPayee allocAcct+ mkCells width maker =+ if width > 0+ then Just (map (maker width os) is)+ else Nothing+ unShrunkCells = mkCells <$> fs <*> cellMakers+ in fmap (fmap removeExtraSpace) unShrunkCells+++-- | After first being allocated by allocPayee and allocAcct, cells+-- are as wide as the total space allocated. This function removes the+-- extra space, making all the cells as wide as the widest+-- cell. Returns the resized cells and the new width.+removeExtraSpace :: [R.ColumnSpec] -> ([R.ColumnSpec], Int)+removeExtraSpace cs = (trimmed, len) where+ len = Fdbl.foldl' f 0 cs where+ f acc c = max acc (Fdbl.foldl' g 0 (R.bits c)) where+ g inAcc chk = max inAcc (C.unWidth . C.chunkWidth $ chk)+ trimmed = map f cs where+ f c = c { R.width = C.Width len }++-- | Gets the width of the two allocated fields.+fieldWidth ::+ Options.T+ -> Spacers.T Int+ -> G.Fields (Maybe Int)+ -> O.ReportWidth+ -> Fields Int+fieldWidth os ss fs (O.ReportWidth rw) = let+ flds = optionsToFields os+ grownWidth = sumGrowersAndSpacers fs ss+ widthForCells = rw - grownWidth - allocSpacerWidth+ payeeSpacerWidth = if payee flds then abs (S.payee ss) else 0+ acctSpacerWidth = if account flds then abs (S.account ss) else 0+ allocSpacerWidth = payeeSpacerWidth + acctSpacerWidth+ allocs = (\bool alloc -> if bool then alloc else A.allocation 0)+ <$> flds+ <*> Fields (O.payeeAllocation os) (O.accountAllocation os)+ in if widthForCells < 1+ then pure 0+ else A.allocate allocs widthForCells+++optionsToFields :: Options.T -> Fields Bool+optionsToFields os = let f = O.fields os in Fields {+ payee = F.payee f+ , account = F.account f }+++-- | Sums spacers for growing cells. This function is intended for use+-- only by the functions that allocate cells for the report, so it+-- assumes that either the Payee or the Account field is showing. Sums+-- all spacers, UNLESS the rightmost field is from PostingDrCr to+-- TotalCmdty, in which case the rightmost spacer is omitted. Apply to+-- the second element of the tuple returned by growCells (which+-- reflects which fields actually have width) and to the accompanying+-- Spacers.+sumSpacers ::+ G.Fields (Maybe a)+ -> Spacers.T Int+ -> Int+sumSpacers fs =+ sum+ . map fst+ . appearingSpacers+ . catMaybes+ . Fdbl.toList+ . fmap toWidth+ . pairedWithSpacers fs+ ++-- | Takes a triple:+--+-- * The first element is Just _ if the field appears in the report;+-- Nothing if not+--+-- * The second element is Maybe Int for the width of the spacer+-- (TotalQty has no spacer, so it will be Nothing)+--+-- * The third element is the EFields tag+--+-- Returns Nothing if the field does not appear in the report. Returns+-- Just a pair if the field does appear in the report, where the first+-- element is the width of the spacer, and the second element is the+-- EFields tag.+toWidth :: (Maybe a, Maybe Int, t) -> Maybe (Int, t)+toWidth (maybeShowing, maybeWidth, tag) =+ if isJust maybeShowing+ then case maybeWidth of+ Just w -> Just (w, tag)+ Nothing -> Just (0, tag)+ else Nothing+++-- | Given a list of all spacers that are attached to the fields that+-- are present in a report, return a list of the spacers that will+-- actually appear in the report. The rightmost spacer does not appear+-- if it is to the right of Account (unless there is a TotalQty field,+-- in which case, all spacers appear because TotalQty has no spacer.)+appearingSpacers :: [(Int, G.EFields)] -> [(Int, G.EFields)]+appearingSpacers ss = case ss of+ [] -> []+ l -> case snd $ last l of+ G.ETotalQty -> l+ t -> if t > G.ENumber+ then init l+ else l++-- | Applied to two arguments: first, a Fields, and second, a+-- Spacers. Combines each Field with its corresponding Spacer and with+-- the GFields, which indicates each particular field.+pairedWithSpacers ::+ G.Fields a+ -> Spacers.T b+ -> G.Fields (a, Maybe b, G.EFields)+pairedWithSpacers f s =+ (\(a, b) c -> (a, b, c))+ <$> G.pairWithSpacer f s+ <*> G.eFields++-- | Sums the contents of growing cells and their accompanying+-- spacers; makes the adjustments described in sumSpacers.+sumGrowersAndSpacers ::+ G.Fields (Maybe Int)+ -> Spacers.T Int+ -> Int+sumGrowersAndSpacers fs ss = spacers + flds where+ spacers = sumSpacers fs ss+ flds = Fdbl.foldr f 0 fs where+ f maybeI acc = case maybeI of+ Nothing -> acc+ Just i -> acc + i+++allocPayee :: Int -> Options.T -> Box -> R.ColumnSpec+allocPayee w os i = let+ pb = L.boxPostFam i+ ts = PC.colors (M.visibleNum . L.boxMeta $ i) (O.baseColors os)+ c = R.ColumnSpec j (C.Width w) ts sq+ j = R.LeftJustify+ sq = case Q.payee pb of+ Nothing -> []+ Just pye -> let+ wrapped =+ Fdbl.toList+ . TF.unLines + . TF.wordWrap w+ . TF.txtWords+ . HT.text+ $ pye+ toBit (TF.Words seqTxts) =+ C.chunk ts+ . X.unwords+ . Fdbl.toList+ $ seqTxts+ in fmap toBit wrapped+ in c+++allocAcct :: Int -> Options.T -> Box -> R.ColumnSpec+allocAcct aw os i = let+ pb = L.boxPostFam i+ ts = PC.colors (M.visibleNum . L.boxMeta $ i) (O.baseColors os) in+ R.ColumnSpec R.LeftJustify (C.Width aw) ts $ let+ target = TF.Target aw+ shortest = TF.Shortest . O.subAccountLength $ os+ a = Q.account pb+ ws = TF.Words . Seq.fromList . HT.textList $ a+ (TF.Words shortened) = TF.shorten shortest target ws+ in [C.chunk ts+ . X.concat+ . intersperse (X.singleton ':')+ . Fdbl.toList+ $ shortened]++instance Functor Fields where+ fmap f i = Fields {+ payee = f (payee i)+ , account = f (account i) }++instance Applicative Fields where+ pure a = Fields a a+ ff <*> fa = Fields {+ payee = payee ff (payee fa)+ , account = account ff (account fa) }++instance Fdbl.Foldable Fields where+ foldr f z flds =+ f (payee flds) (f (account flds) z)++instance T.Traversable Fields where+ traverse f flds =+ Fields <$> f (payee flds) <*> f (account flds)+
+ Penny/Cabin/Posts/BottomRows.hs view
@@ -0,0 +1,623 @@+-- | Fills the bottom rows, which contain the tags, memo, and+-- filename. These rows are formatted as follows:+--+-- * If the columns for TotalDrCr, TotalCmdty, and TotalQty are all+-- present, AND if there are at least TWO other columns present, then+-- there will be a hanging indent. The bottom rows will begin at the+-- SECOND column and end with the last column to the left of+-- TotalDrCr. In this case, each bottom row will have three cells: one+-- padding on the left, one main content, and one padding on the+-- right.+--+-- * Otherwise, if there are NO columns in the top row, these rows+-- will take the entire width of the report. Each bottom row will have+-- one cell.+--+-- * Otherwise, the bottom rows are as wide as all the top cells+-- combined. Each bottom row will have one cell.++module Penny.Cabin.Posts.BottomRows (+ bottomRows, Fields(..), TopRowCells(..), mergeWithSpacers,+ topRowCells) where++import Control.Applicative((<$>), Applicative(pure, (<*>)))+import qualified Data.Foldable as Fdbl+import Control.Monad (guard)+import Data.List (intersperse, find)+import qualified Data.List.NonEmpty as NE+import Data.Maybe (catMaybes)+import Data.Monoid (mappend, mempty, First(First, getFirst))+import qualified Data.Sequence as Seq+import qualified Data.Text as X+import qualified Data.Traversable as T+import qualified Penny.Cabin.Chunk as C+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.TextFormat as TF+import qualified Penny.Cabin.Posts.Allocated as A+import qualified Penny.Cabin.Colors as PC+import qualified Penny.Cabin.Posts.Fields as Fields+import qualified Penny.Cabin.Posts.Fields as F+import qualified Penny.Cabin.Posts.Growers as G+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Posts.Options as Options+import qualified Penny.Cabin.Posts.Options as O+import qualified Penny.Cabin.Posts.Spacers as Spacers+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.HasText as HT+import qualified Penny.Lincoln.Queries as Q++type Box = L.Box M.PostMeta ++bottomRows ::+ G.Fields (Maybe Int)+ -> A.Fields (Maybe Int)+ -> Options.T+ -> [Box]+ -> Fields (Maybe [[C.Chunk]])+bottomRows gf af os is = makeRows is pcs where+ pcs = infoProcessors topSpecs rw wanted+ wanted = requestedMakers os+ topSpecs = topCellSpecs gf af (O.spacers os)+ rw = O.width os+++data Fields a = Fields {+ tags :: a+ , memo :: a+ , filename :: a+ } deriving (Show, Eq)++instance Fdbl.Foldable Fields where+ foldr f z d =+ f (tags d)+ (f (memo d)+ (f (filename d) z))++instance Functor Fields where+ fmap f (Fields t m fn) =+ Fields (f t) (f m) (f fn)++instance Applicative Fields where+ pure a = Fields a a a+ ff <*> fa = Fields {+ tags = (tags ff) (tags fa)+ , memo = (memo ff) (memo fa)+ , filename = (filename ff) (filename fa)+ }++bottomRowsFields :: Fields.T a -> Fields a+bottomRowsFields f = Fields {+ tags = F.tags f+ , memo = F.memo f+ , filename = F.filename f }+++data Hanging a = Hanging {+ leftPad :: a+ , mainCell :: a+ , rightPad :: a+ } deriving (Show, Eq)+++newtype SpacerWidth = SpacerWidth Int deriving (Show, Eq)+newtype ContentWidth = ContentWidth Int deriving (Show, Eq)+++hanging ::+ [TopCellSpec]+ -> Maybe ((Box -> Int -> (C.TextSpec, R.ColumnSpec))+ -> Box -> [C.Chunk])+hanging specs = hangingWidths specs+ >>= return . hangingInfoProcessor++hangingInfoProcessor ::+ Hanging Int+ -> (Box -> Int -> (C.TextSpec, R.ColumnSpec))+ -> Box+ -> [C.Chunk]+hangingInfoProcessor widths mkr info = row where+ row = R.row [left, mid, right]+ (ts, mid) = mkr info (mainCell widths)+ mkPad w = R.ColumnSpec R.LeftJustify (C.Width w) ts []+ left = mkPad (leftPad widths)+ right = mkPad (rightPad widths)++widthOfTopColumns ::+ [TopCellSpec]+ -> Maybe ((Box -> Int -> (C.TextSpec, R.ColumnSpec))+ -> Box -> [C.Chunk])+widthOfTopColumns ts =+ if null ts+ then Nothing+ else Just $ makeSpecificWidth w where+ w = Fdbl.foldl' f 0 ts+ f acc (_, maySpcWidth, (ContentWidth cw)) =+ acc + cw + maybe 0 (\(SpacerWidth sw) -> sw) maySpcWidth+++widthOfReport ::+ O.ReportWidth+ -> (Box -> Int -> (C.TextSpec, R.ColumnSpec))+ -> Box+ -> [C.Chunk]+widthOfReport (O.ReportWidth rw) fn info =+ makeSpecificWidth rw fn info++chooseProcessor ::+ [TopCellSpec]+ -> O.ReportWidth+ -> (Box -> Int -> (C.TextSpec, R.ColumnSpec))+ -> Box+ -> [C.Chunk]+chooseProcessor specs rw fn = let+ firstTwo = First (hanging specs)+ `mappend` First (widthOfTopColumns specs)+ in case getFirst firstTwo of+ Nothing -> widthOfReport rw fn+ Just r -> r fn++infoProcessors ::+ [TopCellSpec]+ -> O.ReportWidth+ -> Fields (Maybe (Box -> Int -> (C.TextSpec, R.ColumnSpec)))+ -> Fields (Maybe (Box -> [C.Chunk]))+infoProcessors specs rw flds = let+ chooser = chooseProcessor specs rw+ mkProcessor mayFn = case mayFn of+ Nothing -> Nothing+ Just fn -> Just $ chooser fn+ in mkProcessor <$> flds+++makeRows ::+ [Box]+ -> Fields (Maybe (Box -> [C.Chunk]))+ -> Fields (Maybe [[C.Chunk]])+makeRows is flds = let+ mkRow fn = map fn is+ in fmap (fmap mkRow) flds+++-- | Calculates column widths for a Hanging report. If it cannot+-- calculate the widths (because these cells do not support hanging),+-- returns Nothing.+hangingWidths :: [TopCellSpec]+ -> Maybe (Hanging Int)+hangingWidths ls = do+ let len = length ls+ guard (len > 4)+ let matchColumn x (c, _, _) = x == c+ totDrCr <- find (matchColumn ETotalDrCr) ls+ totCmdty <- find (matchColumn ETotalCmdty) ls+ totQty <- find (matchColumn ETotalQty) ls+ let (first:middle) = take (len - 3) ls+ mid <- NE.nonEmpty middle+ return $ calcHangingWidths first mid (totDrCr, totCmdty, totQty)++type TopCellSpec = (ETopRowCells, Maybe SpacerWidth, ContentWidth)++-- | Given the first column in the top row, at least one middle+-- column, and the last three columns, calculate the width of the+-- three columns in the hanging report.+calcHangingWidths ::+ TopCellSpec+ -> NE.NonEmpty TopCellSpec+ -> (TopCellSpec, TopCellSpec, TopCellSpec)+ -> Hanging Int+calcHangingWidths l m r = Hanging left middle right where+ calcWidth (_, maybeSp, (ContentWidth c)) =+ c + maybe 0 (\(SpacerWidth w) -> abs w) maybeSp+ left = calcWidth l+ middle = Fdbl.foldl' f 0 m where+ f acc c = acc + calcWidth c+ (totDrCr, totCmdty, totQty) = r+ right = calcWidth totDrCr + calcWidth totCmdty+ + calcWidth totQty+++topCellSpecs :: G.Fields (Maybe Int)+ -> A.Fields (Maybe Int)+ -> Spacers.T Int+ -> [TopCellSpec]+topCellSpecs gFlds aFlds spcs = let+ allFlds = topRowCells gFlds aFlds+ cws = fmap (fmap ContentWidth) allFlds+ merged = mergeWithSpacers cws spcs+ tripler e (cw, maybeSpc) = (e, (fmap SpacerWidth maybeSpc), cw)+ list = Fdbl.toList $ tripler <$> eTopRowCells <*> merged+ toMaybe (e, maybeS, maybeC) = case maybeC of+ Nothing -> Nothing+ Just c -> Just (e, maybeS, c)+ in catMaybes (map toMaybe list)+ ++-- | Merges a TopRowCells with a Spacers. Returns Maybes because+-- totalQty has no spacer.+mergeWithSpacers ::+ TopRowCells a+ -> Spacers.T b+ -> TopRowCells (a, Maybe b)+mergeWithSpacers t s = TopRowCells {+ globalTransaction = (globalTransaction t, Just (S.globalTransaction s))+ , revGlobalTransaction = (revGlobalTransaction t, Just (S.revGlobalTransaction s))+ , globalPosting = (globalPosting t, Just (S.globalPosting s))+ , revGlobalPosting = (revGlobalPosting t, Just (S.revGlobalPosting s))+ , fileTransaction = (fileTransaction t, Just (S.fileTransaction s))+ , revFileTransaction = (revFileTransaction t, Just (S.revFileTransaction s))+ , filePosting = (filePosting t, Just (S.filePosting s))+ , revFilePosting = (revFilePosting t, Just (S.revFilePosting s))+ , filtered = (filtered t, Just (S.filtered s))+ , revFiltered = (revFiltered t, Just (S.revFiltered s))+ , sorted = (sorted t, Just (S.sorted s))+ , revSorted = (revSorted t, Just (S.revSorted s))+ , visible = (visible t, Just (S.visible s))+ , revVisible = (revVisible t, Just (S.revVisible s))+ , lineNum = (lineNum t, Just (S.lineNum s))+ , date = (date t, Just (S.date s))+ , flag = (flag t, Just (S.flag s))+ , number = (number t, Just (S.number s))+ , payee = (payee t, Just (S.payee s))+ , account = (account t, Just (S.account s))+ , postingDrCr = (postingDrCr t, Just (S.postingDrCr s))+ , postingCmdty = (postingCmdty t, Just (S.postingCmdty s))+ , postingQty = (postingQty t, Just (S.postingQty s))+ , totalDrCr = (totalDrCr t, Just (S.totalDrCr s))+ , totalCmdty = (totalCmdty t, Just (S.totalCmdty s))+ , totalQty = (totalQty t, Nothing) }+++-- | Applied to a function that, when applied to the width of a cell,+-- returns a cell filled with data, returns a Row with that cell.+makeSpecificWidth :: Int -> (Box -> Int -> (a, R.ColumnSpec))+ -> Box -> [C.Chunk]+makeSpecificWidth w f i = R.row [c] where+ (_, c) = f i w+++type Maker = Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)++makers :: Fields Maker+makers = Fields tagsCell memoCell filenameCell++-- | Applied to an Options, indicating which reports the user wants,+-- returns a Fields (Maybe Maker) with a Maker in each respective+-- field that the user wants to see.+requestedMakers ::+ Options.T+ -> Fields (Maybe (Box -> Int -> (C.TextSpec, R.ColumnSpec)))+requestedMakers os = let+ flds = bottomRowsFields (O.fields os)+ filler b mkr = if b then Just $ mkr os else Nothing+ in filler <$> flds <*> makers++tagsCell :: Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)+tagsCell os info w = (ts, cell) where+ vn = M.visibleNum . L.boxMeta $ info+ cell = R.ColumnSpec R.LeftJustify (C.Width w) ts cs+ ts = PC.colors vn (O.baseColors os)+ cs =+ Fdbl.toList+ . fmap toBit+ . TF.unLines+ . TF.wordWrap w+ . TF.Words+ . Seq.fromList+ . map (X.cons '*')+ . HT.textList+ . Q.tags+ . L.boxPostFam+ $ info+ toBit (TF.Words ws) = C.chunk ts t where+ t = X.concat . intersperse (X.singleton ' ') . Fdbl.toList $ ws+++memoBits :: C.TextSpec -> L.Memo -> C.Width -> [C.Chunk]+memoBits ts m (C.Width w) = cs where+ cs = Fdbl.toList+ . fmap toBit+ . TF.unLines+ . TF.wordWrap w+ . TF.Words+ . Seq.fromList+ . X.words+ . HT.text+ . HT.Delimited (X.singleton ' ')+ . HT.textList+ $ m+ toBit (TF.Words ws) = C.chunk ts (X.unwords . Fdbl.toList $ ws)+++memoCell :: Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)+memoCell os info width = (ts, cell) where+ w = C.Width width+ vn = M.visibleNum . L.boxMeta $ info+ cell = R.ColumnSpec R.LeftJustify w ts cs+ ts = PC.colors vn (O.baseColors os)+ pm = Q.postingMemo . L.boxPostFam $ info+ tm = Q.transactionMemo . L.boxPostFam $ info+ nullMemo (L.Memo m) = null m+ cs = case (nullMemo pm, nullMemo tm) of+ (True, True) -> mempty+ (False, True) -> memoBits ts pm w+ (True, False) -> memoBits ts tm w+ (False, False) -> memoBits ts pm w `mappend` memoBits ts tm w+ ++filenameCell :: Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)+filenameCell os info width = (ts, cell) where+ w = C.Width width+ vn = M.visibleNum . L.boxMeta $ info+ cell = R.ColumnSpec R.LeftJustify w ts cs+ toBit n = C.chunk ts+ . X.drop (max 0 (X.length n - width)) $ n+ cs = case Q.filename . L.boxPostFam $ info of+ Nothing -> []+ Just fn -> [toBit . L.unFilename $ fn]+ ts = PC.colors vn (O.baseColors os)++++data TopRowCells a = TopRowCells {+ globalTransaction :: a+ , revGlobalTransaction :: a+ , globalPosting :: a+ , revGlobalPosting :: a+ , fileTransaction :: a+ , revFileTransaction :: a+ , filePosting :: a+ , revFilePosting :: a+ , filtered :: a+ , revFiltered :: a+ , sorted :: a+ , revSorted :: a+ , visible :: a+ , revVisible :: a+ , lineNum :: a+ -- ^ The line number from the posting's metadata+ , date :: a+ , flag :: a+ , number :: a+ , payee :: a+ , account :: a+ , postingDrCr :: a+ , postingCmdty :: a+ , postingQty :: a+ , totalDrCr :: a+ , totalCmdty :: a+ , totalQty :: a }+ deriving (Show, Eq)++topRowCells :: G.Fields a -> A.Fields a -> TopRowCells a+topRowCells g a = TopRowCells {+ globalTransaction = G.globalTransaction g+ , revGlobalTransaction = G.revGlobalTransaction g+ , globalPosting = G.globalPosting g+ , revGlobalPosting = G.revGlobalPosting g+ , fileTransaction = G.fileTransaction g+ , revFileTransaction = G.revFileTransaction g+ , filePosting = G.filePosting g+ , revFilePosting = G.revFilePosting g+ , filtered = G.filtered g+ , revFiltered = G.revFiltered g+ , sorted = G.sorted g+ , revSorted = G.revSorted g+ , visible = G.visible g+ , revVisible = G.revVisible g+ , lineNum = G.lineNum g+ , date = G.date g+ , flag = G.flag g+ , number = G.number g+ , payee = A.payee a+ , account = A.account a+ , postingDrCr = G.postingDrCr g+ , postingCmdty = G.postingCmdty g+ , postingQty = G.postingQty g+ , totalDrCr = G.totalDrCr g+ , totalCmdty = G.totalCmdty g+ , totalQty = G.totalQty g }+++data ETopRowCells =+ EGlobalTransaction+ | ERevGlobalTransaction+ | EGlobalPosting+ | ERevGlobalPosting+ | EFileTransaction+ | ERevFileTransaction+ | EFilePosting+ | ERevFilePosting+ | EFiltered+ | ERevFiltered+ | ESorted+ | ERevSorted+ | EVisible+ | ERevVisible+ | ELineNum+ | EDate+ | EFlag+ | ENumber+ | EPayee+ | EAccount+ | EPostingDrCr+ | EPostingCmdty+ | EPostingQty+ | ETotalDrCr+ | ETotalCmdty+ | ETotalQty+ deriving (Show, Eq, Enum)++eTopRowCells :: TopRowCells ETopRowCells+eTopRowCells = TopRowCells {+ globalTransaction = EGlobalTransaction+ , revGlobalTransaction = ERevGlobalTransaction+ , globalPosting = EGlobalPosting+ , revGlobalPosting = ERevGlobalPosting+ , fileTransaction = EFileTransaction+ , revFileTransaction = ERevFileTransaction+ , filePosting = EFilePosting+ , revFilePosting = ERevFilePosting+ , filtered = EFiltered+ , revFiltered = ERevFiltered+ , sorted = ESorted+ , revSorted = ERevSorted+ , visible = EVisible+ , revVisible = ERevVisible+ , lineNum = ELineNum+ , date = EDate+ , flag = EFlag+ , number = ENumber+ , payee = EPayee+ , account = EAccount+ , postingDrCr = EPostingDrCr+ , postingCmdty = EPostingCmdty+ , postingQty = EPostingQty+ , totalDrCr = ETotalDrCr+ , totalCmdty = ETotalCmdty+ , totalQty = ETotalQty }++instance Functor TopRowCells where+ fmap f t = TopRowCells {+ globalTransaction = f (globalTransaction t)+ , revGlobalTransaction = f (revGlobalTransaction t)+ , globalPosting = f (globalPosting t)+ , revGlobalPosting = f (revGlobalPosting t)+ , fileTransaction = f (fileTransaction t)+ , revFileTransaction = f (revFileTransaction t)+ , filePosting = f (filePosting t)+ , revFilePosting = f (revFilePosting t)+ , filtered = f (filtered t)+ , revFiltered = f (revFiltered t)+ , sorted = f (sorted t)+ , revSorted = f (revSorted t)+ , visible = f (visible t)+ , revVisible = f (revVisible t)+ , lineNum = f (lineNum t)+ , date = f (date t)+ , flag = f (flag t)+ , number = f (number t)+ , payee = f (payee t)+ , account = f (account t)+ , postingDrCr = f (postingDrCr t)+ , postingCmdty = f (postingCmdty t)+ , postingQty = f (postingQty t)+ , totalDrCr = f (totalDrCr t)+ , totalCmdty = f (totalCmdty t)+ , totalQty = f (totalQty t) }++instance Applicative TopRowCells where+ pure a = TopRowCells {+ globalTransaction = a+ , revGlobalTransaction = a+ , globalPosting = a+ , revGlobalPosting = a+ , fileTransaction = a+ , revFileTransaction = a+ , filePosting = a+ , revFilePosting = a+ , filtered = a+ , revFiltered = a+ , sorted = a+ , revSorted = a+ , visible = a+ , revVisible = a+ , lineNum = a+ , date = a+ , flag = a+ , number = a+ , payee = a+ , account = a+ , postingDrCr = a+ , postingCmdty = a+ , postingQty = a+ , totalDrCr = a+ , totalCmdty = a+ , totalQty = a }++ ff <*> fa = TopRowCells {+ globalTransaction = globalTransaction ff (globalTransaction fa)+ , revGlobalTransaction = revGlobalTransaction ff (revGlobalTransaction fa)+ , globalPosting = globalPosting ff (globalPosting fa)+ , revGlobalPosting = revGlobalPosting ff (revGlobalPosting fa)+ , fileTransaction = fileTransaction ff (fileTransaction fa)+ , revFileTransaction = revFileTransaction ff (revFileTransaction fa)+ , filePosting = filePosting ff (filePosting fa)+ , revFilePosting = revFilePosting ff (revFilePosting fa)+ , filtered = filtered ff (filtered fa)+ , revFiltered = revFiltered ff (revFiltered fa)+ , sorted = sorted ff (sorted fa)+ , revSorted = revSorted ff (revSorted fa)+ , visible = visible ff (visible fa)+ , revVisible = revVisible ff (revVisible fa)+ , lineNum = lineNum ff (lineNum fa)+ , date = date ff (date fa)+ , flag = flag ff (flag fa)+ , number = number ff (number fa)+ , payee = payee ff (payee fa)+ , account = account ff (account fa)+ , postingDrCr = postingDrCr ff (postingDrCr fa)+ , postingCmdty = postingCmdty ff (postingCmdty fa)+ , postingQty = postingQty ff (postingQty fa)+ , totalDrCr = totalDrCr ff (totalDrCr fa)+ , totalCmdty = totalCmdty ff (totalCmdty fa)+ , totalQty = totalQty ff (totalQty fa) }++instance Fdbl.Foldable TopRowCells where+ foldr f z o =+ f (globalTransaction o)+ (f (revGlobalTransaction o)+ (f (globalPosting o)+ (f (revGlobalPosting o)+ (f (fileTransaction o)+ (f (revFileTransaction o)+ (f (filePosting o)+ (f (revFilePosting o)+ (f (filtered o)+ (f (revFiltered o)+ (f (sorted o)+ (f (revSorted o)+ (f (visible o)+ (f (revVisible o)+ (f (lineNum o)+ (f (date o)+ (f (flag o)+ (f (number o)+ (f (payee o)+ (f (account o)+ (f (postingDrCr o)+ (f (postingCmdty o)+ (f (postingQty o)+ (f (totalDrCr o)+ (f (totalCmdty o)+ (f (totalQty o) z)))))))))))))))))))))))))++instance T.Traversable TopRowCells where+ traverse f t =+ TopRowCells+ <$> f (globalTransaction t)+ <*> f (revGlobalTransaction t)+ <*> f (globalPosting t)+ <*> f (revGlobalPosting t)+ <*> f (fileTransaction t)+ <*> f (revFileTransaction t)+ <*> f (filePosting t)+ <*> f (revFilePosting t)+ <*> f (filtered t)+ <*> f (revFiltered t)+ <*> f (sorted t)+ <*> f (revSorted t)+ <*> f (visible t)+ <*> f (revVisible t)+ <*> f (lineNum t)+ <*> f (date t)+ <*> f (flag t)+ <*> f (number t)+ <*> f (payee t)+ <*> f (account t)+ <*> f (postingDrCr t)+ <*> f (postingCmdty t)+ <*> f (postingQty t)+ <*> f (totalDrCr t)+ <*> f (totalCmdty t)+ <*> f (totalQty t)+
+ Penny/Cabin/Posts/Chunk.hs view
@@ -0,0 +1,104 @@+module Penny.Cabin.Posts.Chunk (makeChunk) where++import qualified Data.Foldable as Fdbl+import Data.List (transpose)+import Data.Maybe (isNothing, catMaybes)+import qualified Penny.Cabin.Posts.Growers as G+import qualified Penny.Cabin.Posts.Allocated as A+import qualified Penny.Cabin.Posts.BottomRows as B+import qualified Penny.Cabin.Posts.Options as Options+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.Chunk as C+import qualified Penny.Cabin.Colors as PC+import qualified Penny.Lincoln as L+import qualified Penny.Cabin.Posts.Meta as M++type Box = L.Box M.PostMeta ++makeChunk :: Options.T -> [Box] -> [C.Chunk]+makeChunk os is = let+ fmapSnd flds = fmap (fmap snd) flds+ fmapFst flds = fmap (fmap fst) flds+ gFldW = fmapSnd gFlds+ aFldW = fmapSnd aFlds+ gFlds = G.growCells os is+ aFlds = A.payeeAndAcct gFldW os is+ bFlds = B.bottomRows gFldW aFldW os is+ topCells = B.topRowCells (fmapFst gFlds) (fmapFst aFlds)+ withSpacers = B.mergeWithSpacers topCells (Options.spacers os)+ topRows = makeTopRows (Options.baseColors os) withSpacers+ bottomRows = makeBottomRows bFlds+ in makeAllRows topRows bottomRows+++topRowsCells ::+ PC.BaseColors+ -> B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)+ -> [[(R.ColumnSpec, Maybe R.ColumnSpec)]]+topRowsCells bc t = let+ toWithSpc (mayCs, maySp) = case mayCs of+ Nothing -> Nothing+ Just cs -> Just (makeSpacers bc cs maySp)+ f mayPairList acc = case mayPairList of+ Nothing -> acc+ (Just pairList) -> pairList : acc+ in transpose $ Fdbl.foldr f [] (fmap toWithSpc t)++makeRow :: [(R.ColumnSpec, Maybe R.ColumnSpec)] -> [C.Chunk]+makeRow = R.row . foldr f [] where+ f (c, mayC) acc = case mayC of+ Nothing -> c:acc+ Just spcr -> c:spcr:acc+++makeSpacers ::+ PC.BaseColors+ -> [R.ColumnSpec]+ -> Maybe Int+ -> [(R.ColumnSpec, Maybe R.ColumnSpec)]+makeSpacers bc cs mayI = case mayI of+ Nothing -> map (\c -> (c, Nothing)) cs+ Just i -> makeEvenOddSpacers bc cs i++makeEvenOddSpacers ::+ PC.BaseColors+ -> [R.ColumnSpec]+ -> Int+ -> [(R.ColumnSpec, Maybe R.ColumnSpec)]+makeEvenOddSpacers bc cs i = let absI = abs i in+ if absI == 0+ then map (\c -> (c, Nothing)) cs+ else let+ spcrs = cycle [Just $ mkSpcr evenTs, Just $ mkSpcr oddTs]+ mkSpcr ts = R.ColumnSpec R.LeftJustify (C.Width absI) ts []+ evenTs = PC.evenColors bc+ oddTs = PC.oddColors bc+ in zip cs spcrs++makeTopRows ::+ PC.BaseColors+ -> B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)+ -> Maybe [[C.Chunk]]+makeTopRows bc trc =+ if Fdbl.all (isNothing . fst) trc+ then Nothing+ else Just $ map makeRow . topRowsCells bc $ trc+++makeBottomRows ::+ B.Fields (Maybe [[C.Chunk]])+ -> Maybe [[[C.Chunk]]]+makeBottomRows flds =+ if Fdbl.all isNothing flds+ then Nothing+ else Just . transpose . catMaybes . Fdbl.toList $ flds++makeAllRows :: Maybe [[C.Chunk]] -> Maybe [[[C.Chunk]]] -> [C.Chunk]+makeAllRows mayrs mayrrs = case (mayrs, mayrrs) of+ (Nothing, Nothing) -> []+ (Just rs, Nothing) -> concat rs+ (Nothing, Just rrs) -> concat . concat $ rrs+ (Just rs, Just rrs) -> concat $ zipWith f rs rrs where+ f topRow botRows = concat [topRow, concat botRows]++
+ Penny/Cabin/Posts/Fields.hs view
@@ -0,0 +1,166 @@+-- | Fields that can appear in the Posts report.+module Penny.Cabin.Posts.Fields where++import Control.Applicative(Applicative(pure, (<*>)))+import qualified Data.Foldable as F++data T a = T {+ globalTransaction :: a+ , revGlobalTransaction :: a+ , globalPosting :: a+ , revGlobalPosting :: a+ , fileTransaction :: a+ , revFileTransaction :: a+ , filePosting :: a+ , revFilePosting :: a+ , filtered :: a+ , revFiltered :: a+ , sorted :: a+ , revSorted :: a+ , visible :: a+ , revVisible :: a+ , lineNum :: a+ -- ^ The line number from the posting's metadata+ , date :: a+ , flag :: a+ , number :: a+ , payee :: a+ , account :: a+ , postingDrCr :: a+ , postingCmdty :: a+ , postingQty :: a+ , totalDrCr :: a+ , totalCmdty :: a+ , totalQty :: a+ , tags :: a+ , memo :: a+ , filename :: a }+ deriving (Show, Eq)++instance Functor T where+ fmap f fa = T {+ globalTransaction = f (globalTransaction fa)+ , revGlobalTransaction = f (revGlobalTransaction fa)+ , globalPosting = f (globalPosting fa)+ , revGlobalPosting = f (revGlobalPosting fa)+ , fileTransaction = f (fileTransaction fa)+ , revFileTransaction = f (revFileTransaction fa)+ , filePosting = f (filePosting fa)+ , revFilePosting = f (revFilePosting fa)+ , filtered = f (filtered fa)+ , revFiltered = f (revFiltered fa)+ , sorted = f (sorted fa)+ , revSorted = f (revSorted fa)+ , visible = f (visible fa)+ , revVisible = f (revVisible fa)+ , lineNum = f (lineNum fa)+ , date = f (date fa)+ , flag = f (flag fa)+ , number = f (number fa)+ , payee = f (payee fa)+ , account = f (account fa)+ , postingDrCr = f (postingDrCr fa)+ , postingCmdty = f (postingCmdty fa)+ , postingQty = f (postingQty fa)+ , totalDrCr = f (totalDrCr fa)+ , totalCmdty = f (totalCmdty fa)+ , totalQty = f (totalQty fa)+ , tags = f (tags fa)+ , memo = f (memo fa)+ , filename = f (filename fa) }++instance Applicative T where+ pure a = T {+ globalTransaction = a+ , revGlobalTransaction = a+ , globalPosting = a+ , revGlobalPosting = a+ , fileTransaction = a+ , revFileTransaction = a+ , filePosting = a+ , revFilePosting = a+ , filtered = a+ , revFiltered = a+ , sorted = a+ , revSorted = a+ , visible = a+ , revVisible = a+ , lineNum = a+ , date = a+ , flag = a+ , number = a+ , payee = a+ , account = a+ , postingDrCr = a+ , postingCmdty = a+ , postingQty = a+ , totalDrCr = a+ , totalCmdty = a+ , totalQty = a+ , tags = a+ , memo = a+ , filename = a }++ ff <*> fa = T {+ globalTransaction = globalTransaction ff (globalTransaction fa)+ , revGlobalTransaction = revGlobalTransaction ff+ (revGlobalTransaction fa)+ , globalPosting = globalPosting ff (globalPosting fa)+ , revGlobalPosting = revGlobalPosting ff (revGlobalPosting fa)+ , fileTransaction = fileTransaction ff (fileTransaction fa)+ , revFileTransaction = revFileTransaction ff (revFileTransaction fa)+ , filePosting = filePosting ff (filePosting fa)+ , revFilePosting = revFilePosting ff (revFilePosting fa)+ , filtered = filtered ff (filtered fa)+ , revFiltered = revFiltered ff (revFiltered fa)+ , sorted = sorted ff (sorted fa)+ , revSorted = revSorted ff (revSorted fa)+ , visible = visible ff (visible fa)+ , revVisible = revVisible ff (revVisible fa)+ , lineNum = lineNum ff (lineNum fa)+ , date = date ff (date fa)+ , flag = flag ff (flag fa)+ , number = number ff (number fa)+ , payee = payee ff (payee fa)+ , account = account ff (account fa)+ , postingDrCr = postingDrCr ff (postingDrCr fa)+ , postingCmdty = postingCmdty ff (postingCmdty fa)+ , postingQty = postingQty ff (postingQty fa)+ , totalDrCr = totalDrCr ff (totalDrCr fa)+ , totalCmdty = totalCmdty ff (totalCmdty fa)+ , totalQty = totalQty ff (totalQty fa)+ , tags = tags ff (tags fa)+ , memo = memo ff (memo fa)+ , filename = filename ff (filename fa) }++instance F.Foldable T where+ foldr f z t =+ f (globalTransaction t)+ (f (revGlobalTransaction t)+ (f (globalPosting t)+ (f (revGlobalPosting t)+ (f (fileTransaction t)+ (f (revFileTransaction t)+ (f (filePosting t)+ (f (revFilePosting t)+ (f (filtered t)+ (f (revFiltered t)+ (f (sorted t)+ (f (revSorted t)+ (f (visible t)+ (f (revVisible t)+ (f (lineNum t)+ (f (date t)+ (f (flag t)+ (f (number t)+ (f (payee t)+ (f (account t)+ (f (postingDrCr t)+ (f (postingCmdty t)+ (f (postingQty t)+ (f (totalDrCr t)+ (f (totalCmdty t)+ (f (totalQty t)+ (f (tags t)+ (f (memo t)+ (f (filename t) z))))))))))))))))))))))))))))
+ Penny/Cabin/Posts/Growers.hs view
@@ -0,0 +1,610 @@+-- | Calculates cells that "grow to fit." These cells grow to fit the+-- widest cell in the column. No information is ever truncated from+-- these cells (what use is a truncated dollar amount?)+module Penny.Cabin.Posts.Growers (+ growCells, Fields(..), grownWidth,+ eFields, EFields(..), pairWithSpacer) where++import Control.Applicative((<$>), Applicative(pure, (<*>)))+import qualified Data.Foldable as Fdbl+import Data.Map (elems, assocs)+import qualified Data.Semigroup as Semi+import Data.Semigroup ((<>))+import Data.Text (Text, pack, empty)+import qualified Data.Text as X+import qualified Penny.Cabin.Chunk as C+import qualified Penny.Cabin.Colors as PC+import qualified Penny.Cabin.Posts.Options as O+import qualified Penny.Cabin.Posts.Options as Options+import qualified Penny.Cabin.Posts.Fields as F+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Cabin.Posts.Spacers as Spacers+import qualified Penny.Cabin.Row as R+import qualified Penny.Liberty as Ly+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+++type Box = L.Box M.PostMeta ++-- | Grows the cells that will be GrowToFit cells in the report. First+-- this function fills in all visible cells with text, but leaves the+-- width undetermined. Then it determines the widest line in each+-- column. Finally it adjusts each cell in the column so that it is+-- that maximum width.+--+-- Returns a list of rows, and a Fields holding the width of each+-- cell. Each of these widths will be at least 1; fields that were in+-- the report but that ended up having no width are changed to+-- Nothing.+growCells ::+ Options.T+ -> [Box]+ -> Fields (Maybe ([R.ColumnSpec], Int))+growCells o infos = toPair <$> wanted <*> growers where+ toPair b gwr+ | b = let+ cs = map (gwr o) infos+ w = Fdbl.foldl' f 0 cs where+ f acc c = max acc (widestLine c)+ cs' = map (sizer (R.Width w)) cs+ in if w > 0 then Just (cs', w) else Nothing+ | otherwise = Nothing+ wanted = growingFields o++widestLine :: PreSpec -> Int+widestLine (PreSpec _ _ bs) =+ maximum . map (R.unWidth . C.chunkWidth) $ bs++data PreSpec = PreSpec {+ _justification :: R.Justification+ , _padSpec :: C.TextSpec+ , _bits :: [C.Chunk] }+++-- | Given a PreSpec and a width, create a ColumnSpec of the right+-- size.+sizer :: R.Width -> PreSpec -> R.ColumnSpec+sizer w (PreSpec j ts bs) = R.ColumnSpec j w ts bs++-- | Makes a left justified cell that is only one line long. The width+-- is unset.+oneLine :: Text -> Options.T -> Box -> PreSpec+oneLine t os b =+ let bc = Options.baseColors os+ ts = PC.colors (M.visibleNum . L.boxMeta $ b) bc+ j = R.LeftJustify+ bit = C.chunk ts t+ in PreSpec j ts [bit]++growers :: Fields (Options.T -> Box -> PreSpec)+growers = Fields {+ globalTransaction = getGlobalTransaction+ , revGlobalTransaction = getRevGlobalTransaction+ , globalPosting = getGlobalPosting+ , revGlobalPosting = getRevGlobalPosting+ , fileTransaction = getFileTransaction+ , revFileTransaction = getRevFileTransaction+ , filePosting = getFilePosting+ , revFilePosting = getRevFilePosting+ , filtered = getFiltered+ , revFiltered = getRevFiltered+ , sorted = getSorted+ , revSorted = getRevSorted+ , visible = getVisible+ , revVisible = getRevVisible+ , lineNum = getLineNum+ , date = getDate+ , flag = getFlag+ , number = getNumber+ , postingDrCr = getPostingDrCr+ , postingCmdty = getPostingCmdty+ , postingQty = getPostingQty+ , totalDrCr = getTotalDrCr+ , totalCmdty = getTotalCmdty+ , totalQty = getTotalQty }++-- | Make a left justified cell one line long that shows a serial.+serialCellMaybe ::+ (L.PostFam -> Maybe Int)+ -- ^ When applied to a Box, this function returns Just Int if the+ -- box has a serial, or Nothing if not.+ + -> Options.T -> Box -> PreSpec+serialCellMaybe f os b = oneLine t os b+ where+ t = case f (L.boxPostFam b) of+ Nothing -> X.empty+ Just i -> X.pack . show $ i++serialCell ::+ (M.PostMeta -> Int)+ -> Options.T -> Box -> PreSpec+serialCell f os b = oneLine t os b+ where+ t = pack . show . f . L.boxMeta $ b++getGlobalTransaction :: Options.T -> Box -> PreSpec+getGlobalTransaction =+ serialCellMaybe (fmap (L.forward . L.unGlobalTransaction)+ . Q.globalTransaction)++getRevGlobalTransaction :: Options.T -> Box -> PreSpec+getRevGlobalTransaction =+ serialCellMaybe (fmap (L.backward . L.unGlobalTransaction)+ . Q.globalTransaction)++getGlobalPosting :: Options.T -> Box -> PreSpec+getGlobalPosting =+ serialCellMaybe (fmap (L.forward . L.unGlobalPosting)+ . Q.globalPosting)++getRevGlobalPosting :: Options.T -> Box -> PreSpec+getRevGlobalPosting =+ serialCellMaybe (fmap (L.backward . L.unGlobalPosting)+ . Q.globalPosting)++getFileTransaction :: Options.T -> Box -> PreSpec+getFileTransaction =+ serialCellMaybe (fmap (L.forward . L.unFileTransaction)+ . Q.fileTransaction)++getRevFileTransaction :: Options.T -> Box -> PreSpec+getRevFileTransaction =+ serialCellMaybe (fmap (L.backward . L.unFileTransaction)+ . Q.fileTransaction)++getFilePosting :: Options.T -> Box -> PreSpec+getFilePosting =+ serialCellMaybe (fmap (L.forward . L.unFilePosting)+ . Q.filePosting)++getRevFilePosting :: Options.T -> Box -> PreSpec+getRevFilePosting =+ serialCellMaybe (fmap (L.backward . L.unFilePosting)+ . Q.filePosting)++getSorted :: Options.T -> Box -> PreSpec+getSorted =+ serialCell (L.forward . Ly.unSortedNum . M.sortedNum)++getRevSorted :: Options.T -> Box -> PreSpec+getRevSorted =+ serialCell (L.backward . Ly.unSortedNum . M.sortedNum)++getFiltered :: Options.T -> Box -> PreSpec+getFiltered =+ serialCell (L.forward . Ly.unFilteredNum . M.filteredNum)++getRevFiltered :: Options.T -> Box -> PreSpec+getRevFiltered =+ serialCell (L.backward . Ly.unFilteredNum . M.filteredNum)++getVisible :: Options.T -> Box -> PreSpec+getVisible =+ serialCell (L.forward . M.unVisibleNum . M.visibleNum)++getRevVisible :: Options.T -> Box -> PreSpec+getRevVisible =+ serialCell (L.backward . M.unVisibleNum . M.visibleNum)+++getLineNum :: Options.T -> Box -> PreSpec+getLineNum os b = oneLine t os b where+ lineTxt = pack . show . L.unPostingLine+ t = maybe empty lineTxt (Q.postingLine . L.boxPostFam $ b)++getDate :: Options.T -> Box -> PreSpec+getDate os i = oneLine t os i where+ t = O.dateFormat os i++getFlag :: Options.T -> Box -> PreSpec+getFlag os i = oneLine t os i where+ t = maybe empty L.text (Q.flag . L.boxPostFam $ i)++getNumber :: Options.T -> Box -> PreSpec+getNumber os i = oneLine t os i where+ t = maybe empty L.text (Q.number . L.boxPostFam $ i)++dcTxt :: L.DrCr -> Text+dcTxt L.Debit = pack "Dr"+dcTxt L.Credit = pack "Cr"++-- | Gives a one-line cell that is colored according to whether the+-- posting is a debit or credit.+coloredPostingCell :: Text -> Options.T -> Box -> PreSpec+coloredPostingCell t os i = PreSpec j ts [bit] where+ j = R.LeftJustify+ bit = C.chunk ts t+ dc = Q.drCr . L.boxPostFam $ i+ ts = PC.colors (M.visibleNum . L.boxMeta $ i)+ . PC.drCrToBaseColors dc+ . O.drCrColors+ $ os++getPostingDrCr :: Options.T -> Box -> PreSpec+getPostingDrCr os i = coloredPostingCell t os i where+ t = dcTxt . Q.drCr . L.boxPostFam $ i++getPostingCmdty :: Options.T -> Box -> PreSpec+getPostingCmdty os i = coloredPostingCell t os i where+ t = L.text . L.Delimited (X.singleton ':') + . L.textList . Q.commodity . L.boxPostFam $ i++getPostingQty :: Options.T -> Box -> PreSpec+getPostingQty os i = coloredPostingCell t os i where+ t = O.qtyFormat os i++getTotalDrCr :: Options.T -> Box -> PreSpec+getTotalDrCr os i = let+ vn = M.visibleNum . L.boxMeta $ i+ ts = PC.colors vn bc+ bc = PC.drCrToBaseColors dc (O.drCrColors os)+ dc = Q.drCr . L.boxPostFam $ i+ bits = case M.balance . L.boxMeta $ i of+ Nothing -> let+ spec = PC.noBalanceColors vn (O.drCrColors os)+ in [C.chunk spec (pack "--")]+ Just bal -> let+ toBit bl = let+ spec = + PC.colors vn+ . PC.bottomLineToBaseColors (O.drCrColors os)+ $ bl+ txt = case bl of+ L.Zero -> pack "--"+ L.NonZero (L.Column clmDrCr _) -> dcTxt clmDrCr+ in C.chunk spec txt+ in fmap toBit . elems . L.unBalance $ bal+ j = R.LeftJustify+ in PreSpec j ts bits++getTotalCmdty :: Options.T -> Box -> PreSpec+getTotalCmdty os i = let+ vn = M.visibleNum . L.boxMeta $ i+ j = R.RightJustify+ ts = PC.colors vn bc+ bc = PC.drCrToBaseColors dc (O.drCrColors os)+ dc = Q.drCr . L.boxPostFam $ i+ bits = case M.balance . L.boxMeta $ i of+ Nothing -> let+ spec = PC.noBalanceColors vn (O.drCrColors os)+ in [C.chunk spec (pack "--")]+ Just bal -> let+ toBit (com, nou) = let+ spec =+ PC.colors vn+ . PC.bottomLineToBaseColors (O.drCrColors os)+ $ nou+ txt = L.text+ . L.Delimited (X.singleton ':')+ . L.textList+ $ com+ in C.chunk spec txt+ in fmap toBit . assocs . L.unBalance $ bal+ in PreSpec j ts bits++getTotalQty :: Options.T -> Box -> PreSpec+getTotalQty os i = let+ vn = M.visibleNum . L.boxMeta $ i+ j = R.LeftJustify+ ts = PC.colors vn bc+ bc = PC.drCrToBaseColors dc (O.drCrColors os)+ dc = Q.drCr . L.boxPostFam $ i+ bits = case M.balance . L.boxMeta $ i of+ Nothing -> let+ spec = PC.noBalanceColors vn (O.drCrColors os)+ in [C.chunk spec (pack "--")]+ Just bal -> fmap toChunk . assocs . L.unBalance $ bal where+ toChunk (com, nou) = let+ spec = + PC.colors vn+ . PC.bottomLineToBaseColors (O.drCrColors os)+ $ nou+ txt = O.balanceFormat os com nou+ in C.chunk spec txt+ in PreSpec j ts bits++growingFields :: Options.T -> Fields Bool+growingFields o = let+ f = O.fields o in Fields {+ globalTransaction = F.globalTransaction f+ , revGlobalTransaction = F.revGlobalTransaction f+ , globalPosting = F.globalPosting f+ , revGlobalPosting = F.revGlobalPosting f+ , fileTransaction = F.fileTransaction f+ , revFileTransaction = F.revFileTransaction f+ , filePosting = F.filePosting f+ , revFilePosting = F.revFilePosting f+ , filtered = F.filtered f+ , revFiltered = F.revFiltered f+ , sorted = F.sorted f+ , revSorted = F.revSorted f+ , visible = F.visible f+ , revVisible = F.revVisible f+ , lineNum = F.lineNum f+ , date = F.date f+ , flag = F.flag f+ , number = F.number f+ , postingDrCr = F.postingDrCr f+ , postingCmdty = F.postingCmdty f+ , postingQty = F.postingQty f+ , totalDrCr = F.totalDrCr f+ , totalCmdty = F.totalCmdty f+ , totalQty = F.totalQty f }++-- | All growing fields, as an ADT.+data EFields =+ EGlobalTransaction+ | ERevGlobalTransaction+ | EGlobalPosting+ | ERevGlobalPosting+ | EFileTransaction+ | ERevFileTransaction+ | EFilePosting+ | ERevFilePosting+ | EFiltered+ | ERevFiltered+ | ESorted+ | ERevSorted+ | EVisible+ | ERevVisible+ | ELineNum+ | EDate+ | EFlag+ | ENumber+ | EPostingDrCr+ | EPostingCmdty+ | EPostingQty+ | ETotalDrCr+ | ETotalCmdty+ | ETotalQty+ deriving (Show, Eq, Ord, Enum)++-- | Returns a Fields where each record has its corresponding EField.+eFields :: Fields EFields+eFields = Fields {+ globalTransaction = EGlobalTransaction+ , revGlobalTransaction = ERevGlobalTransaction+ , globalPosting = EGlobalPosting+ , revGlobalPosting = ERevGlobalPosting+ , fileTransaction = EFileTransaction+ , revFileTransaction = ERevFileTransaction+ , filePosting = EFilePosting+ , revFilePosting = ERevFilePosting+ , filtered = EFiltered+ , revFiltered = ERevFiltered+ , sorted = ESorted+ , revSorted = ERevSorted+ , visible = EVisible+ , revVisible = ERevVisible+ , lineNum = ELineNum+ , date = EDate+ , flag = EFlag+ , number = ENumber+ , postingDrCr = EPostingDrCr+ , postingCmdty = EPostingCmdty+ , postingQty = EPostingQty+ , totalDrCr = ETotalDrCr+ , totalCmdty = ETotalCmdty+ , totalQty = ETotalQty }++-- | All growing fields.+data Fields a = Fields {+ globalTransaction :: a+ , revGlobalTransaction :: a+ , globalPosting :: a+ , revGlobalPosting :: a+ , fileTransaction :: a+ , revFileTransaction :: a+ , filePosting :: a+ , revFilePosting :: a+ , filtered :: a+ , revFiltered :: a+ , sorted :: a+ , revSorted :: a+ , visible :: a+ , revVisible :: a+ , lineNum :: a+ -- ^ The line number from the posting's metadata+ , date :: a+ , flag :: a+ , number :: a+ , postingDrCr :: a+ , postingCmdty :: a+ , postingQty :: a+ , totalDrCr :: a+ , totalCmdty :: a+ , totalQty :: a }+ deriving (Show, Eq)++instance Fdbl.Foldable Fields where+ foldr f z i =+ f (globalTransaction i)+ (f (revGlobalTransaction i)+ (f (globalPosting i)+ (f (revGlobalPosting i)+ (f (fileTransaction i)+ (f (revFileTransaction i)+ (f (filePosting i)+ (f (revFilePosting i)+ (f (filtered i)+ (f (revFiltered i)+ (f (sorted i)+ (f (revSorted i)+ (f (visible i)+ (f (revVisible i)+ (f (lineNum i)+ (f (date i)+ (f (flag i)+ (f (number i)+ (f (postingDrCr i)+ (f (postingCmdty i)+ (f (postingQty i)+ (f (totalDrCr i)+ (f (totalCmdty i)+ (f (totalQty i) z)))))))))))))))))))))))++instance Functor Fields where+ fmap f i = Fields {+ globalTransaction = f (globalTransaction i)+ , revGlobalTransaction = f (revGlobalTransaction i)+ , globalPosting = f (globalPosting i)+ , revGlobalPosting = f (revGlobalPosting i)+ , fileTransaction = f (fileTransaction i)+ , revFileTransaction = f (revFileTransaction i)+ , filePosting = f (filePosting i)+ , revFilePosting = f (revFilePosting i)+ , filtered = f (filtered i)+ , revFiltered = f (revFiltered i)+ , sorted = f (sorted i)+ , revSorted = f (revSorted i)+ , visible = f (visible i)+ , revVisible = f (revVisible i)+ , lineNum = f (lineNum i)+ , date = f (date i)+ , flag = f (flag i)+ , number = f (number i)+ , postingDrCr = f (postingDrCr i)+ , postingCmdty = f (postingCmdty i)+ , postingQty = f (postingQty i)+ , totalDrCr = f (totalDrCr i)+ , totalCmdty = f (totalCmdty i)+ , totalQty = f (totalQty i) }++instance Applicative Fields where+ pure a = Fields {+ globalTransaction = a+ , revGlobalTransaction = a+ , globalPosting = a+ , revGlobalPosting = a+ , fileTransaction = a+ , revFileTransaction = a+ , filePosting = a+ , revFilePosting = a+ , filtered = a+ , revFiltered = a+ , sorted = a+ , revSorted = a+ , visible = a+ , revVisible = a+ , lineNum = a+ , date = a+ , flag = a+ , number = a+ , postingDrCr = a+ , postingCmdty = a+ , postingQty = a+ , totalDrCr = a+ , totalCmdty = a+ , totalQty = a }++ fl <*> fa = Fields {+ globalTransaction = globalTransaction fl (globalTransaction fa)+ , revGlobalTransaction = revGlobalTransaction fl (revGlobalTransaction fa)+ , globalPosting = globalPosting fl (globalPosting fa)+ , revGlobalPosting = revGlobalPosting fl (revGlobalPosting fa)+ , fileTransaction = fileTransaction fl (fileTransaction fa)+ , revFileTransaction = revFileTransaction fl (revFileTransaction fa)+ , filePosting = filePosting fl (filePosting fa)+ , revFilePosting = revFilePosting fl (revFilePosting fa)+ , filtered = filtered fl (filtered fa)+ , revFiltered = revFiltered fl (revFiltered fa)+ , sorted = sorted fl (sorted fa)+ , revSorted = revSorted fl (revSorted fa)+ , visible = visible fl (visible fa)+ , revVisible = revVisible fl (revVisible fa)+ , lineNum = lineNum fl (lineNum fa)+ , date = date fl (date fa)+ , flag = flag fl (flag fa)+ , number = number fl (number fa)+ , postingDrCr = postingDrCr fl (postingDrCr fa)+ , postingCmdty = postingCmdty fl (postingCmdty fa)+ , postingQty = postingQty fl (postingQty fa)+ , totalDrCr = totalDrCr fl (totalDrCr fa)+ , totalCmdty = totalCmdty fl (totalCmdty fa)+ , totalQty = totalQty fl (totalQty fa) }++-- | Pairs data from a Fields with its matching spacer field. The+-- spacer field is returned in a Maybe because the TotalQty field does+-- not have a spacer.+pairWithSpacer :: Fields a -> Spacers.T b -> Fields (a, Maybe b)+pairWithSpacer f s = Fields {+ globalTransaction = (globalTransaction f, Just (S.globalTransaction s))+ , revGlobalTransaction = (revGlobalTransaction f, Just (S.revGlobalTransaction s))+ , globalPosting = (globalPosting f, Just (S.globalPosting s))+ , revGlobalPosting = (revGlobalPosting f, Just (S.revGlobalPosting s))+ , fileTransaction = (fileTransaction f, Just (S.fileTransaction s))+ , revFileTransaction = (revFileTransaction f, Just (S.revFileTransaction s))+ , filePosting = (filePosting f, Just (S.filePosting s))+ , revFilePosting = (revFilePosting f, Just (S.revFilePosting s))+ , filtered = (filtered f, Just (S.filtered s))+ , revFiltered = (revFiltered f, Just (S.revFiltered s))+ , sorted = (sorted f, Just (S.sorted s))+ , revSorted = (revSorted f, Just (S.revSorted s))+ , visible = (visible f, Just (S.visible s))+ , revVisible = (revVisible f, Just (S.revVisible s))+ , lineNum = (lineNum f, Just (S.lineNum s))+ , date = (date f, Just (S.date s))+ , flag = (flag f, Just (S.flag s))+ , number = (number f, Just (S.number s))+ , postingDrCr = (postingDrCr f, Just (S.postingDrCr s))+ , postingCmdty = (postingCmdty f, Just (S.postingCmdty s))+ , postingQty = (postingQty f, Just (S.postingQty s))+ , totalDrCr = (totalDrCr f, Just (S.totalDrCr s))+ , totalCmdty = (totalCmdty f, Just (S.totalCmdty s))+ , totalQty = (totalQty f, Nothing ) }++-- | Reduces a set of Fields to a single value.+reduce :: Semi.Semigroup s => Fields s -> s+reduce f =+ globalTransaction f+ <> revGlobalTransaction f+ <> globalPosting f+ <> revGlobalPosting f+ <> fileTransaction f+ <> revFileTransaction f+ <> filePosting f+ <> revFilePosting f+ <> filtered f+ <> revFiltered f+ <> sorted f+ <> revSorted f+ <> visible f+ <> revVisible f+ <> lineNum f+ <> date f+ <> flag f+ <> number f+ <> postingDrCr f+ <> postingCmdty f+ <> postingQty f+ <> totalDrCr f+ <> totalCmdty f+ <> totalQty f++-- | Compute the width of all Grown cells, including any applicable+-- spacer cells.+grownWidth ::+ Fields (Maybe Int)+ -> Spacers.T Int+ -> Int+grownWidth fs ss =+ Semi.getSum+ . reduce+ . fmap Semi.Sum+ . fmap fieldWidth+ $ pairWithSpacer fs ss++-- | Compute the field width of a single field and its spacer. The+-- first element of the tuple is the field width, if present; the+-- second element of the tuple is the width of the spacer. If there is+-- no field, returns 0.+fieldWidth :: (Maybe Int, Maybe Int) -> Int+fieldWidth (m1, m2) = case m1 of+ Nothing -> 0+ Just i1 -> case m2 of+ Just i2 -> if i2 > 0 then i1 + i2 else i1+ Nothing -> i1+
+ Penny/Cabin/Posts/Help.hs view
@@ -0,0 +1,66 @@+module Penny.Cabin.Posts.Help where++import qualified Data.Text as X++help :: X.Text+help = X.pack helpStr++helpStr :: String+helpStr = unlines [+ "postings, pos",+ " Show postings in order with a running balance.",+ " The postings report takes the same options shown",+ " from above in the categories \"Posting filters\" to",+ " \"Removing postings after sorting and filtering\",",+ " with the options affecting which postings are shown on screen.",+ " Postings not shown still affect the running balance.",+ "",+ " Additional sequence number filtering options",+ " Like the \"Sequence numbers\" options above, these",+ " options take the form --option cmp num.",+ "",+ " --filtered, --revFiltered",+ " All postings, after filters given in the filter",+ " specification portion of the command line are",+ " applied",+ " --sorted, --revSorted",+ " All postings remaining after filtering and after",+ " postings have been sorted",+ "",+ " Other additional options:",+ "",+ " --color yes|no|auto|256",+ " yes: show 8 colors always",+ " no: never show colors",+ " auto: show 8 or 256 colors, but only if stdout is a terminal",+ " 256: show 256 colors always",+ " --background light|dark",+ " Use appropriate color scheme for terminal background",+ "",+ " --width num",+ " Hint for roughly how wide the report should be in columns",+ " --show field, --hide field",+ " show or hide this field, where field is one of:",+ " globalTransaction, revGlobalTransaction,",+ " globalPosting, revGlobalPosting,",+ " fileTransaction, revFileTransaction,",+ " filePosting, revFilePosting,",+ " filtered, revFiltered,",+ " sorted, revSorted,",+ " visible, revVisible,",+ " lineNum,",+ " date, flag, number, payee, account,",+ " postingDrCr, postingCommodity, postingQty,",+ " totalDrCr, totalCommodity, totalQty,",+ " tags, memo, filename",+ " --show-all",+ " Show all fields",+ " --hide-all",+ " Hide all fields",+ "",+ " --show-zero-balances",+ " Show balances that are zero",+ " --hide-zero-balances",+ " Hide balances that are zero",+ ""+ ]
+ Penny/Cabin/Posts/Meta.hs view
@@ -0,0 +1,70 @@+module Penny.Cabin.Posts.Meta (+ M.VisibleNum(M.unVisibleNum)+ , PostMeta(filteredNum, sortedNum, visibleNum, balance)+ , filterBoxes+ , addMetadata+ ) where++import Data.List (mapAccumL)+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Liberty as Ly+import qualified Penny.Liberty.Expressions as Exp+import qualified Penny.Cabin.Meta as M+import qualified Penny.Cabin.Options as CO++data PostMeta =+ PostMeta { filteredNum :: Ly.FilteredNum+ , sortedNum :: Ly.SortedNum+ , visibleNum :: M.VisibleNum+ , balance :: Maybe L.Balance }+ deriving Show+++-- | Takes a list of Box with LibertyMeta, and a list of tokens from+-- the command line, and a list of post filters. Filters the list of+-- Box and processes the post filter. Fails if the expression fails to+-- produce a result.+filterBoxes ::+ [Exp.Token (L.Box Ly.LibertyMeta -> Bool)]+ -> [Ly.PostFilterFn]+ -> [L.Box Ly.LibertyMeta]+ -> Maybe [L.Box Ly.LibertyMeta]+filterBoxes tks pfs bs = fmap fltr (Ly.parsePredicate tks)+ where+ fltr p =+ Ly.processPostFilters pfs . filter p $ bs++++-- | Applied to a list of Box that have already been filtered, returns+-- a list of Box with posting metadata.+addMetadata ::+ CO.ShowZeroBalances+ -> [L.Box Ly.LibertyMeta]+ -> [L.Box PostMeta]+addMetadata szb = M.visibleNums f . addBalances szb where+ f vn (lm, mb) =+ PostMeta (Ly.filteredNum lm) (Ly.sortedNum lm) vn mb++addBalances ::+ CO.ShowZeroBalances+ -> [L.Box Ly.LibertyMeta]+ -> [(L.Box (Ly.LibertyMeta, Maybe L.Balance))]+addBalances szb = snd . mapAccumL (balanceAccum szb) Nothing++balanceAccum :: + CO.ShowZeroBalances+ -> Maybe L.Balance+ -> L.Box Ly.LibertyMeta+ -> (Maybe L.Balance, (L.Box (Ly.LibertyMeta, Maybe L.Balance)))+balanceAccum (CO.ShowZeroBalances szb) mb po =+ let balThis = L.entryToBalance . Q.entry . L.boxPostFam $ po+ balNew = case mb of+ Nothing -> balThis+ Just balOld -> L.addBalances balOld balThis+ balNoZeroes = L.removeZeroCommodities balNew+ bal' = if szb then Just balNew else balNoZeroes+ po' = L.Box (L.boxMeta po, bal') (L.boxPostFam po)+ in (bal', po')+
+ Penny/Cabin/Posts/Options.hs view
@@ -0,0 +1,247 @@+-- | Options for the Posts report.+module Penny.Cabin.Posts.Options where+++import Control.Monad.Exception.Synchronous (Exceptional)+import Data.Text (Text)+import qualified Data.Text as X+import Data.Time (formatTime)+import System.Locale (defaultTimeLocale)+import qualified Data.Time as Time+import qualified Text.Matchers.Text as M++import qualified Penny.Liberty as Ly+import qualified Penny.Liberty.Expressions as Ex+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Balance as Bal+import qualified Penny.Lincoln.Bits as Bits+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Cabin.Posts.Allocate as A+import qualified Penny.Cabin.Chunk as CC+import qualified Penny.Cabin.Colors as C+import qualified Penny.Cabin.Options as O+import qualified Penny.Cabin.Posts.Fields as Fields+import qualified Penny.Cabin.Posts.Fields as F+import qualified Penny.Cabin.Posts.Meta as Meta+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Cabin.Posts.Spacers as Spacers+import qualified Penny.Cabin.Colors.DarkBackground as Dark+import Penny.Copper.DateTime (DefaultTimeZone)+import Penny.Copper.Qty (RadGroup)+import qualified Penny.Shield as S+++type Box = L.Box Meta.PostMeta++data T =+ T { drCrColors :: C.DrCrColors+ -- ^ Colors to use when displaying debits, credits, and+ -- when displaying balance totals++ , baseColors :: C.BaseColors + -- ^ Colors to use when displaying everything else++ , dateFormat :: Box -> X.Text+ -- ^ How to display dates. This function is applied to the+ -- a PostingInfo so it has lots of information, but it+ -- should return a date for use in the Date field.+ + , qtyFormat :: Box -> X.Text+ -- ^ How to display the quantity of the posting. This+ -- function is applied to a PostingInfo so it has lots of+ -- information, but it should return a formatted string of+ -- the quantity. Allows you to format digit grouping,+ -- radix points, perform rounding, etc.+ + , balanceFormat :: Bits.Commodity -> Bal.BottomLine -> X.Text+ -- ^ How to display balance totals. Similar to+ -- balanceFormat.+ + , payeeAllocation :: A.Allocation+ -- ^ This and accountAllocation determine how much space+ -- payees and accounts receive. They divide up the+ -- remaining space after everything else is displayed. For+ -- instance if payeeAllocation is 60 and accountAllocation+ -- is 40, the payee takes about 60 percent of the+ -- remaining space and the account takes about 40 percent.+ + , accountAllocation :: A.Allocation + -- ^ See payeeAllocation above++ , width :: ReportWidth+ -- ^ Gives the default report width. This can be+ -- overridden on the command line. You can use the+ -- information from the Runtime to make this as wide as+ -- the as the current terminal.++ , subAccountLength :: Int+ -- ^ When shortening the names of sub accounts to make+ -- them fit, they will be this long.++ , colorPref :: CC.Colors+ -- ^ How many colors you want to see, or do it+ -- automatically.++ , timeZone :: DefaultTimeZone+ -- ^ When dates and times are given on the command line+ -- and they have no time zone, they are assumed to be in+ -- this time zone. This has no bearing on how dates are+ -- formatted in the output; for that, see dateFormat+ -- above.++ , radGroup :: RadGroup+ -- ^ The characters used for the radix point for numbers+ -- given on the command line (e.g. a full stop, or a+ -- comma) and for the digit group separator for+ -- numbers parsed from the command line (e.g. a full stop,+ -- or a comma). Affects how inputs are parsed. Has no bearing+ -- on how output is formatted; for that, see qtyFormat and+ -- balanceFormat above.+ + , sensitive :: M.CaseSensitive+ -- ^ Whether pattern matches are case sensitive by default.+ + , factory :: M.CaseSensitive+ -> Text -> Exceptional Text (Text -> Bool)+ -- ^ Default factory for pattern matching+ + , tokens :: [Ex.Token (L.Box Ly.LibertyMeta -> Bool)]+ -- ^ Default list of tokens used to filter postings.+ + , postFilter :: [Ly.PostFilterFn]+ -- ^ The entire posting list is transformed by this+ -- function after it is filtered according to the tokens.+ + , fields :: Fields.T Bool+ -- ^ Default fields to show in the report.+ + , spacers :: Spacers.T Int+ -- ^ Default width for spacer fields. If any of these Ints are+ -- less than or equal to zero, there will be no spacer. There is+ -- never a spacer for fields that do not appear in the report.+ + , showZeroBalances :: O.ShowZeroBalances+ }++newtype ReportWidth = ReportWidth { unReportWidth :: Int }+ deriving (Eq, Show, Ord)++ymd :: Box -> X.Text+ymd p = X.pack (formatTime defaultTimeLocale fmt d) where+ d = Time.localDay+ . Bits.localTime+ . Q.dateTime+ . L.boxPostFam+ $ p+ fmt = "%Y-%m-%d"++qtyAsIs :: Box -> X.Text+qtyAsIs p = X.pack . show . Bits.unQty . Q.qty . L.boxPostFam $ p++balanceAsIs :: Bits.Commodity -> Bal.BottomLine -> X.Text+balanceAsIs _ n = case n of+ Bal.Zero -> X.pack "--"+ Bal.NonZero c -> X.pack . show . Bits.unQty . Bal.qty $ c++defaultWidth :: ReportWidth+defaultWidth = ReportWidth 80++columnsVarToWidth :: Maybe String -> ReportWidth+columnsVarToWidth ms = case ms of+ Nothing -> defaultWidth+ Just str -> case reads str of+ [] -> defaultWidth+ (i, []):[] -> if i > 0 then ReportWidth i else defaultWidth+ _ -> defaultWidth++defaultOptions ::+ DefaultTimeZone+ -> RadGroup+ -> S.Runtime+ -> T+defaultOptions dtz rg rt =+ T { drCrColors = Dark.drCrColors+ , baseColors = Dark.baseColors+ , dateFormat = ymd+ , qtyFormat = qtyAsIs+ , balanceFormat = balanceAsIs+ , payeeAllocation = A.allocation 40+ , accountAllocation = A.allocation 60+ , width = widthFromRuntime rt+ , subAccountLength = 2+ , colorPref = O.autoColors O.PrefAuto rt+ , timeZone = dtz+ , radGroup = rg+ , sensitive = M.Insensitive+ , factory = \s t -> (return (M.within s t))+ , tokens = []+ , postFilter = []+ , fields = defaultFields+ , spacers = defaultSpacerWidth+ , showZeroBalances = O.ShowZeroBalances False }++widthFromRuntime :: S.Runtime -> ReportWidth+widthFromRuntime rt = case S.screenWidth rt of+ Nothing -> defaultWidth+ Just w -> ReportWidth . S.unScreenWidth $ w++defaultFields :: Fields.T Bool+defaultFields =+ Fields.T { F.globalTransaction = False+ , F.revGlobalTransaction = False+ , F.globalPosting = False+ , F.revGlobalPosting = False+ , F.fileTransaction = False+ , F.revFileTransaction = False+ , F.filePosting = False+ , F.revFilePosting = False+ , F.filtered = False+ , F.revFiltered = False+ , F.sorted = False+ , F.revSorted = False+ , F.visible = False+ , F.revVisible = False+ , F.lineNum = False+ , F.date = True+ , F.flag = False+ , F.number = False+ , F.payee = True+ , F.account = True+ , F.postingDrCr = True+ , F.postingCmdty = True+ , F.postingQty = True+ , F.totalDrCr = True+ , F.totalCmdty = True+ , F.totalQty = True+ , F.tags = False+ , F.memo = False+ , F.filename = False }++defaultSpacerWidth :: Spacers.T Int+defaultSpacerWidth =+ Spacers.T { S.globalTransaction = 1+ , S.revGlobalTransaction = 1+ , S.globalPosting = 1+ , S.revGlobalPosting = 1+ , S.fileTransaction = 1+ , S.revFileTransaction = 1+ , S.filePosting = 1+ , S.revFilePosting = 1+ , S.filtered = 1+ , S.revFiltered = 1+ , S.sorted = 1+ , S.revSorted = 1+ , S.visible = 1+ , S.revVisible = 1+ , S.lineNum = 1+ , S.date = 1+ , S.flag = 1+ , S.number = 1+ , S.payee = 4+ , S.account = 1+ , S.postingDrCr = 1+ , S.postingCmdty = 1+ , S.postingQty = 1+ , S.totalDrCr = 1+ , S.totalCmdty = 1 }+
+ Penny/Cabin/Posts/Parser.hs view
@@ -0,0 +1,326 @@+module Penny.Cabin.Posts.Parser (parseOptions) where++import Control.Applicative ((<|>), (<$>), pure, many, (<*>))+import Control.Monad ((>=>))+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Char (toLower)+import qualified Data.Foldable as F+import qualified System.Console.MultiArg.Combinator as C+import System.Console.MultiArg.Prim (Parser)++import qualified Penny.Cabin.Chunk as CC+import qualified Penny.Cabin.Colors as PC+import qualified Penny.Cabin.Posts.Fields as Fl+import qualified Penny.Cabin.Posts.Options as Op+import qualified Penny.Cabin.Colors.DarkBackground as DB+import qualified Penny.Cabin.Colors.LightBackground as LB+import qualified Penny.Cabin.Options as CO+import qualified Penny.Liberty as Ly+import qualified Penny.Liberty.Expressions as Exp+import qualified Penny.Lincoln as L+import qualified Penny.Shield as S++data Error = BadColorName String+ | BadBackgroundArg String+ | BadWidthArg String+ | NoMatchingFieldName+ | MultipleMatchingFieldNames [String]+ | LibertyError Ly.Error+ | BadNumber String+ | BadComparator String+ deriving Show++-- | Parses the command line from the first word remaining up until,+-- but not including, the first non-option argment.+parseOptions ::+ Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)+parseOptions = f <$> many parseOption where+ f ls =+ let g rt op =+ let ls' = map (\fn -> fn rt) ls+ in (foldl (>=>) return ls') op+ in g+++parseOption ::+ Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)+parseOption =+ operand+ <|> mkTwoArg boxFilters+ <|> mkTwoArg postFilter+ <|> mkTwoArg matcherSelect+ <|> mkTwoArg caseSelect+ <|> mkTwoArg operator+ <|> color+ <|> mkTwoArg background+ <|> mkTwoArg width+ <|> mkTwoArg showField+ <|> mkTwoArg hideField+ <|> mkTwoArg showAllFields+ <|> mkTwoArg hideAllFields+ <|> mkTwoArg showZeroBalances+ <|> mkTwoArg hideZeroBalances+ where+ mkTwoArg p = do+ f <- p+ return (\_ o -> f o)++operand :: Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)+operand = f <$> Ly.parseOperand+ where+ f lyFn rt op =+ let dtz = Op.timeZone op+ rg = Op.radGroup op+ dt = S.currentTime rt+ cs = Op.sensitive op+ fty = Op.factory op+ in case lyFn dt dtz rg cs fty of+ Ex.Exception e -> Ex.throw . LibertyError $ e+ Ex.Success (Exp.Operand g) ->+ let g' = g . L.boxPostFam+ ts' = Op.tokens op ++ [Exp.TokOperand g']+ in return op { Op.tokens = ts' }++-- | Processes a option for box-level serials.+optBoxSerial ::+ [String]+ -- ^ Long options+ + -> [Char]+ -- ^ Short options+ + -> (Ly.LibertyMeta -> Int)+ -- ^ Pulls the serial from the PostMeta+ + -> Parser (Op.T -> Ex.Exceptional Error Op.T)++optBoxSerial ls ss f = parseOpt ls ss (C.TwoArg g)+ where+ g a1 a2 op = do+ cmp <- Ex.fromMaybe (BadComparator a1) (Ly.parseComparer a1)+ i <- parseInt a2+ let h box =+ let ser = f . L.boxMeta $ box+ in ser `cmp` i+ tok = Exp.TokOperand h+ return op { Op.tokens = Op.tokens op ++ [tok] }++optFilteredNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)+optFilteredNum = optBoxSerial ["filtered"] "" f+ where+ f = L.forward . Ly.unFilteredNum . Ly.filteredNum++optRevFilteredNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)+optRevFilteredNum = optBoxSerial ["revFiltered"] "" f+ where+ f = L.backward . Ly.unFilteredNum . Ly.filteredNum++optSortedNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)+optSortedNum = optBoxSerial ["sorted"] "" f+ where+ f = L.forward . Ly.unSortedNum . Ly.sortedNum++optRevSortedNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)+optRevSortedNum = optBoxSerial ["revSorted"] "" f+ where+ f = L.backward . Ly.unSortedNum . Ly.sortedNum++parseInt :: String -> Ex.Exceptional Error Int+parseInt s = case reads s of+ (i, ""):[] -> return i+ _ -> Ex.throw . BadNumber $ s++boxFilters :: Parser (Op.T -> Ex.Exceptional Error Op.T)+boxFilters =+ optFilteredNum+ <|> optRevFilteredNum+ <|> optSortedNum+ <|> optRevSortedNum+++postFilter :: Parser (Op.T -> Ex.Exceptional Error Op.T)+postFilter = f <$> Ly.parsePostFilter+ where+ f ex op =+ case ex of+ Ex.Exception e -> Ex.throw . LibertyError $ e+ Ex.Success pf ->+ return op { Op.postFilter = Op.postFilter op ++ [pf] }++matcherSelect :: Parser (Op.T -> Ex.Exceptional Error Op.T)+matcherSelect = f <$> Ly.parseMatcherSelect+ where+ f mf op = return op { Op.factory = mf }++caseSelect :: Parser (Op.T -> Ex.Exceptional Error Op.T)+caseSelect = f <$> Ly.parseCaseSelect+ where+ f cs op = return op { Op.sensitive = cs }++operator :: Parser (Op.T -> Ex.Exceptional Error Op.T)+operator = f <$> Ly.parseOperator+ where+ f oo op = return op { Op.tokens = Op.tokens op ++ [oo] }++parseOpt :: [String] -> [Char] -> C.ArgSpec a -> Parser a+parseOpt ss cs a = C.parseOption [C.OptSpec ss cs a]++color :: Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)+color = parseOpt ["color"] "" (C.OneArg f)+ where+ f a1 rt op = case pickColorArg rt a1 of+ Nothing -> Ex.throw . BadColorName $ a1+ Just c -> return (op { Op.colorPref = c })++pickColorArg :: S.Runtime -> String -> Maybe CC.Colors+pickColorArg rt t+ | t == "yes" = Just CC.Colors8+ | t == "no" = Just CC.Colors0+ | t == "256" = Just CC.Colors256+ | t == "auto" = Just . CO.maxCapableColors $ rt+ | otherwise = Nothing++pickBackgroundArg :: String -> Maybe (PC.DrCrColors, PC.BaseColors)+pickBackgroundArg t+ | t == "light" = Just (LB.drCrColors, LB.baseColors)+ | t == "dark" = Just (DB.drCrColors, DB.baseColors)+ | otherwise = Nothing+++background :: Parser (Op.T -> Ex.Exceptional Error Op.T)+background = parseOpt ["background"] "" (C.OneArg f)+ where+ f a1 op = case pickBackgroundArg a1 of+ Nothing -> Ex.throw . BadBackgroundArg $ a1+ Just (dc, bc) -> return (op { Op.drCrColors = dc+ , Op.baseColors = bc } )+++width :: Parser (Op.T -> Ex.Exceptional Error Op.T)+width = parseOpt ["width"] "" (C.OneArg f)+ where+ f a1 op = case reads a1 of+ (i, ""):[] -> return (op { Op.width = Op.ReportWidth i })+ _ -> Ex.throw . BadWidthArg $ a1++showField :: Parser (Op.T -> Ex.Exceptional Error Op.T)+showField = parseOpt ["show"] "" (C.OneArg f)+ where+ f a1 op = do+ fl <- parseField a1+ let newFl = fieldOn (Op.fields op) fl+ return op { Op.fields = newFl }++hideField :: Parser (Op.T -> Ex.Exceptional Error Op.T)+hideField = parseOpt ["hide"] "" (C.OneArg f)+ where+ f a1 op = do+ fl <- parseField a1+ let newFl = fieldOff (Op.fields op) fl+ return op { Op.fields = newFl }++showAllFields :: Parser (Op.T -> Ex.Exceptional a Op.T)+showAllFields = parseOpt ["show-all"] "" (C.NoArg f)+ where+ f op = return (op {Op.fields = pure True})++hideAllFields :: Parser (Op.T -> Ex.Exceptional a Op.T)+hideAllFields = parseOpt ["hide-all"] "" (C.NoArg f)+ where+ f op = return (op {Op.fields = pure False})++showZeroBalances :: Parser (Op.T -> Ex.Exceptional a Op.T)+showZeroBalances = parseOpt ["show-zero-balances"] "" (C.NoArg f)+ where+ f op =+ return (op {Op.showZeroBalances = CO.ShowZeroBalances True })++hideZeroBalances :: Parser (Op.T -> Ex.Exceptional a Op.T)+hideZeroBalances = parseOpt ["hide-zero-balances"] "" (C.NoArg f)+ where+ f op =+ return (op {Op.showZeroBalances = CO.ShowZeroBalances False })++-- | Turns a field on if it is True.+fieldOn ::+ Fl.T Bool+ -- ^ Fields as seen so far++ -> Fl.T Bool+ -- ^ Record that should have one True element indicating a field+ -- name seen on the command line; other elements should be False+ + -> Fl.T Bool+ -- ^ Fields as seen so far, with new field added++fieldOn old new = (||) <$> old <*> new++-- | Turns off a field if it is True.+fieldOff ::+ Fl.T Bool+ -- ^ Fields seen so far+ + -> Fl.T Bool+ -- ^ Record that should have one True element indicating a field+ -- name seen on the command line; other elements should be False+ + -> Fl.T Bool+ -- ^ Fields as seen so far, with new field added++fieldOff old new = f <$> old <*> new+ where+ f o False = o+ f _ True = False++parseField :: String -> Ex.Exceptional Error (Fl.T Bool)+parseField str =+ let lower = map toLower str+ checkField s =+ if (map toLower s) == lower+ then (s, True)+ else (s, False)+ flds = checkField <$> fieldNames+ in checkFields flds++-- | Checks the fields with the True value to ensure there is only one.+checkFields :: Fl.T (String, Bool) -> Ex.Exceptional Error (Fl.T Bool)+checkFields fs =+ let f (s, b) ls = if b then s:ls else ls+ in case F.foldr f [] fs of+ [] -> Ex.throw NoMatchingFieldName+ _:[] -> return (snd <$> fs)+ ls -> Ex.throw . MultipleMatchingFieldNames $ ls++++fieldNames :: Fl.T String+fieldNames = Fl.T {+ Fl.globalTransaction = "globalTransaction"+ , Fl.revGlobalTransaction = "revGlobalTransaction"+ , Fl.globalPosting = "globalPosting"+ , Fl.revGlobalPosting = "revGlobalPosting"+ , Fl.fileTransaction = "fileTransaction"+ , Fl.revFileTransaction = "revFileTransaction"+ , Fl.filePosting = "filePosting"+ , Fl.revFilePosting = "revFilePosting"+ , Fl.filtered = "filtered"+ , Fl.revFiltered = "revFiltered"+ , Fl.sorted = "sorted"+ , Fl.revSorted = "revSorted"+ , Fl.visible = "visible"+ , Fl.revVisible = "revVisible"+ , Fl.lineNum = "lineNum"+ , Fl.date = "date"+ , Fl.flag = "flag"+ , Fl.number = "number"+ , Fl.payee = "payee"+ , Fl.account = "account"+ , Fl.postingDrCr = "postingDrCr"+ , Fl.postingCmdty = "postingCmdty"+ , Fl.postingQty = "postingQty"+ , Fl.totalDrCr = "totalDrCr"+ , Fl.totalCmdty = "totalCmdty"+ , Fl.totalQty = "totalQty"+ , Fl.tags = "tags"+ , Fl.memo = "memo"+ , Fl.filename = "filename" }
+ Penny/Cabin/Posts/Spacers.hs view
@@ -0,0 +1,108 @@+-- | Spacer fields in the report. They don't contain any data; they+-- just provide whitespace. Each spacer immediately follows the named+-- field.+module Penny.Cabin.Posts.Spacers where++data T a = T {+ globalTransaction :: a+ , revGlobalTransaction :: a+ , globalPosting :: a+ , revGlobalPosting :: a+ , fileTransaction :: a+ , revFileTransaction :: a+ , filePosting :: a+ , revFilePosting :: a+ , filtered :: a+ , revFiltered :: a+ , sorted :: a+ , revSorted :: a+ , visible :: a+ , revVisible :: a+ , lineNum :: a+ -- ^ The line number from the posting's metadata+ , date :: a+ , flag :: a+ , number :: a+ , payee :: a+ , account :: a+ , postingDrCr :: a+ , postingCmdty :: a+ , postingQty :: a+ , totalDrCr :: a+ , totalCmdty :: a }+ deriving (Show, Eq)++t_globalTransaction :: a -> T a -> T a+t_globalTransaction a f = f { globalTransaction = a }++t_revGlobalTransaction :: a -> T a -> T a+t_revGlobalTransaction a f = f { revGlobalTransaction = a }++t_globalPosting :: a -> T a -> T a+t_globalPosting a f = f { globalPosting = a }++t_revGlobalPosting :: a -> T a -> T a+t_revGlobalPosting a f = f { revGlobalPosting = a }++t_fileTransaction :: a -> T a -> T a+t_fileTransaction a f = f { fileTransaction = a }++t_revFileTransaction :: a -> T a -> T a+t_revFileTransaction a f = f { revFileTransaction = a }++t_filePosting :: a -> T a -> T a+t_filePosting a f = f { filePosting = a }++t_revFilePosting :: a -> T a -> T a+t_revFilePosting a f = f { revFilePosting = a }++t_filtered :: a -> T a -> T a+t_filtered a f = f { filtered = a }++t_revFiltered :: a -> T a -> T a+t_revFiltered a f = f { revFiltered = a }++t_sorted :: a -> T a -> T a+t_sorted a f = f { sorted = a }++t_revSorted :: a -> T a -> T a+t_revSorted a f = f { revSorted = a }++t_visible :: a -> T a -> T a+t_visible a f = f { visible = a }++t_revVisible :: a -> T a -> T a+t_revVisible a f = f { revVisible = a }++t_lineNum :: a -> T a -> T a+t_lineNum a f = f { lineNum = a }++t_date :: a -> T a -> T a+t_date a f = f { date = a }++t_flag :: a -> T a -> T a+t_flag a f = f { flag = a }++t_number :: a -> T a -> T a+t_number a f = f { number = a }++t_payee :: a -> T a -> T a+t_payee a f = f { payee = a }++t_account :: a -> T a -> T a+t_account a f = f { account = a }++t_postingDrCr :: a -> T a -> T a+t_postingDrCr a f = f { postingDrCr = a }++t_postingCmdty :: a -> T a -> T a+t_postingCmdty a f = f { postingCmdty = a }++t_postingQty :: a -> T a -> T a+t_postingQty a f = f { postingQty = a }++t_totalDrCr :: a -> T a -> T a+t_totalDrCr a f = f { totalDrCr = a }++t_totalCmdty :: a -> T a -> T a+t_totalCmdty a f = f { totalCmdty = a }
+ Penny/Cabin/Row.hs view
@@ -0,0 +1,113 @@+module Penny.Cabin.Row (+ Justification(LeftJustify, RightJustify),+ ColumnSpec(ColumnSpec, justification, width, padSpec, bits),+ C.Width(Width, unWidth),+ row ) where++import Data.List (transpose)+import qualified Data.Text as X+import qualified Penny.Cabin.Chunk as C++-- | How to justify cells. LeftJustify leaves the right side+-- ragged. RightJustify leaves the left side ragged.+data Justification =+ LeftJustify+ | RightJustify+ deriving Show++-- | A cell of text output. You tell the cell how to justify itself+-- and how wide it is. You also tell it the background colors to+-- use. The cell will be appropriately justified (that is, text+-- aligned between left and right margins) and padded (with lines of+-- blank text added on the bottom as needed) when joined with other+-- cells into a Row.+data ColumnSpec =+ ColumnSpec { justification :: Justification+ , width :: C.Width+ , padSpec :: C.TextSpec+ , bits :: [C.Chunk] }++newtype JustifiedCell = JustifiedCell (Either (C.Chunk, C.Chunk) C.Chunk)+data JustifiedColumn = JustifiedColumn {+ justifiedCells :: [JustifiedCell]+ , _justifiedWidth :: C.Width+ , _justifiedPadSpec :: C.TextSpec }++newtype PaddedColumns = PaddedColumns [[JustifiedCell]]+newtype CellsByRow = CellsByRow [[JustifiedCell]]+newtype CellRowsWithNewlines = CellRowsWithNewlines [[JustifiedCell]]+++justify ::+ C.TextSpec+ -> C.Width+ -> Justification+ -> C.Chunk+ -> JustifiedCell+justify ts (C.Width w) j b+ | origWidth < w = JustifiedCell . Left $ pair+ | otherwise = JustifiedCell . Right $ b+ where+ origWidth = C.unWidth . C.chunkWidth $ b+ pad = C.chunk ts t+ t = X.replicate (w - origWidth) (X.singleton ' ')+ pair = case j of+ LeftJustify -> (b, pad)+ RightJustify -> (pad, b)++newtype Height = Height { _unHeight :: Int }+ deriving (Show, Eq, Ord)++height :: [[a]] -> Height+height = Height . maximum . map length++row :: [ColumnSpec] -> [C.Chunk]+row =+ concat+ . concat+ . toBits+ . toCellRowsWithNewlines+ . toCellsByRow+ . bottomPad+ . map justifiedColumn++justifiedColumn :: ColumnSpec -> JustifiedColumn+justifiedColumn (ColumnSpec j w ts bs) = JustifiedColumn cs w ts where+ cs = map (justify ts w j) $ bs++bottomPad :: [JustifiedColumn] -> PaddedColumns+bottomPad jcs = PaddedColumns pcs where+ justCells = map justifiedCells jcs+ (Height h) = height justCells+ pcs = map toPaddedColumn jcs+ toPaddedColumn (JustifiedColumn cs (C.Width w) ts) = let+ l = length cs+ nPads = h - l+ pad = C.chunk ts t+ t = X.replicate w (X.singleton ' ')+ pads = replicate nPads . JustifiedCell . Right $ pad+ cs'+ | l < h = cs ++ pads+ | otherwise = cs+ in cs'+++toCellsByRow :: PaddedColumns -> CellsByRow+toCellsByRow (PaddedColumns cs) = CellsByRow (transpose cs)+++toCellRowsWithNewlines :: CellsByRow -> CellRowsWithNewlines+toCellRowsWithNewlines (CellsByRow bs) =+ CellRowsWithNewlines bs' where+ bs' = foldr f [] bs+ newline = JustifiedCell . Right+ $ C.chunk C.defaultTextSpec (X.singleton '\n')+ f cells acc = (cells ++ [newline]) : acc+ ++toBits :: CellRowsWithNewlines -> [[[C.Chunk]]]+toBits (CellRowsWithNewlines cs) = map (map toB) cs where+ toB (JustifiedCell c) = case c of+ Left (lb, rb) -> [lb, rb]+ Right b -> [b]+
+ Penny/Cabin/TextFormat.hs view
@@ -0,0 +1,173 @@+module Penny.Cabin.TextFormat (+ Lines(Lines, unLines),+ Words(Words, unWords),+ CharsPerLine(unCharsPerLine),+ txtWords,+ wordWrap,+ Target(Target, unTarget),+ Shortest(Shortest, unShortest),+ shorten) where++import qualified Control.Monad.Trans.State as St+import qualified Data.Foldable as F+import Data.Sequence ((|>), ViewR((:>)), ViewL((:<)))+import qualified Data.Sequence as S+import qualified Data.Text as X+import qualified Data.Traversable as T++data Lines = Lines { unLines :: S.Seq Words } deriving Show+data Words = Words { unWords :: S.Seq X.Text } deriving Show+newtype CharsPerLine =+ CharsPerLine { unCharsPerLine :: Int } deriving Show++-- | Splits a blank-separated text into words.+txtWords :: X.Text -> Words+txtWords = Words . S.fromList . X.words++-- | Wraps a sequence of words into a sequence of lines, where each+-- line is no more than a given maximum number of characters long.+--+-- If the maximum number of characters per line is less than 1,+-- returns a Lines that is empty.+--+-- An individual word will be split across multiple lines only if that+-- word is too long to fit into a single line. No hyphenation is done;+-- the word is simply broken across two lines.+wordWrap :: Int -> Words -> Lines+wordWrap l (Words wsq) =+ if l < 1+ then Lines (S.empty)+ else F.foldl f (Lines S.empty) wsq where+ f (Lines sws) w = let+ (back, ws) = case S.viewr sws of+ S.EmptyR -> (S.empty, Words S.empty)+ (b :> x) -> (b, x)+ in case addWord l ws w of+ (Just ws') -> Lines $ back |> ws'+ Nothing ->+ if X.length w > l+ then addPartialWords l (Lines sws) w+ else Lines (back |> ws |> (Words (S.singleton w)))++lenWords :: Words -> Int+lenWords (Words s) = case S.length s of+ 0 -> 0+ l -> (F.sum . fmap X.length $ s) + (l - 1)++-- | Adds a word to a Words, but only if it will not make the Words+-- exceed the given length.+addWord :: Int -> Words -> X.Text -> Maybe Words+addWord l (Words ws) w =+ let words' = Words (ws |> w)+ in if lenWords words' > l+ then Nothing+ else Just words'++-- | Adds a word to a Words. If the word is too long to fit, breaks it+-- and adds the longest portion possible. Returns the new Words, and a+-- Text with the part of the word that was not added (if any; if all+-- of the word was added, return an empty Text.)+addPartialWord :: Int -> Words -> X.Text -> (Words, X.Text)+addPartialWord l (Words ws) t = case addWord l (Words ws) t of+ (Just ws') -> (ws', X.empty)+ Nothing ->+ let maxChars =+ if S.null ws then l+ else max 0 (l - lenWords (Words ws) - 1)+ (begin, end) = X.splitAt maxChars t+ in (Words (if X.null begin then ws else ws |> begin), end)++addPartialWords :: Int -> Lines -> X.Text -> Lines+addPartialWords l (Lines wsq) t = let+ (back, ws) = case S.viewr wsq of+ S.EmptyR -> (S.empty, Words S.empty)+ (b :> x) -> (b, x)+ (rw, rt) = addPartialWord l ws t+ in if X.null rt+ then Lines (back |> rw)+ else addPartialWords l (Lines (back |> rw |> Words (S.empty))) rt++newtype Target = Target { unTarget :: Int } deriving Show+newtype Shortest = Shortest { unShortest :: Int } deriving Show++-- | Takes a list of words and shortens it so that it fits in the+-- space allotted. You specify the minimum length for each word, x. It+-- will shorten the farthest left word first, until it is only x+-- characters long; then it will shorten the next word until it is+-- only x characters long, etc. This proceeds until all words are just+-- x characters long. Then words are shortened to one+-- character. Then the leftmost words are deleted as necessary.+--+-- Assumes that the words will be printed with a separator, which+-- matters when lengths are calculated.+shorten :: Shortest -> Target -> Words -> Words+shorten (Shortest s) (Target t) wsa@(Words wsq) = let+ nToRemove = max (lenWords wsa - t) 0+ (allWords, _) = shortenUntilOne s nToRemove wsq+ in stripWordsUntil t (Words allWords)++-- | Shorten a word by x characters or until it is y characters long,+-- whichever comes first. Returns the word and the number of+-- characters removed.+shortenUntil :: Int -> Int -> X.Text -> (X.Text, Int)+shortenUntil by shortest t = let+ removable = max (X.length t - shortest) 0+ toRemove = min removable (max by 0)+ prefix = X.length t - toRemove+ in (X.take prefix t, toRemove)++-- | Shortens a word until it is x characters long or by the number of+-- characters indicated in the state, whichever is less. Subtracts the+-- number of characters removed from the state.+shortenSt :: Int -> X.Text -> St.State Int X.Text+shortenSt shortest t = do+ by <- St.get+ let (r, nRemoved) = shortenUntil by shortest t+ St.put (by - nRemoved)+ return r++-- | Shortens each word in a list, from left to right, until a+-- particular number of characters have been reduced or until each+-- word is x characters long, whichever happens first. Returns the new+-- list and the number of characters that still need to be reduced.+shortenEachInList ::+ T.Traversable t+ => Int -- ^ Shortest word length+ -> Int -- ^ Total number to remove+ -> t X.Text+ -> (t X.Text, Int)+shortenEachInList shortest by ts = (r, left) where+ k = T.mapM (shortenSt shortest) ts+ (r, left) = St.runState k by++shortenUntilOne ::+ T.Traversable t+ => Int -- ^ Shortest word length to start with+ -> Int -- ^ Total number of characters to remove+ -> t X.Text+ -> (t X.Text, Int)+shortenUntilOne shortest by ts = let+ r@(ts', left) = shortenEachInList shortest by ts+ in if shortest == 1 || left == 0+ then r+ else shortenUntilOne (pred shortest) left ts'++-- | Eliminates words until the length of the words, as indicated by+-- lenWords, is less than or equal to the value given.+stripWordsUntil :: Int -> Words -> Words+stripWordsUntil i wsa@(Words ws) = case S.viewl ws of+ S.EmptyL -> Words (S.empty)+ (_ :< rest) ->+ if lenWords wsa <= (max i 0)+ then wsa+ else stripWordsUntil (max i 0) (Words rest)++ +--+-- Testing+--+_words :: Words+_words = Words . S.fromList . map X.pack $ ws where + ws = [ "these", "are", "fragilisticwonderfulgood",+ "good", "", "x", "xy", "xyza",+ "longlonglongword" ]
+ Penny/Copper.hs view
@@ -0,0 +1,132 @@+-- | Copper - the Penny parser+module Penny.Copper (+ -- * Comments+ C.Comment(Comment),++ -- * Radix and grouping+ Q.RadGroup,+ Q.periodComma, Q.periodSpace, Q.commaPeriod, Q.commaSpace,+ Q.GroupingSpec(..),+ + -- * Default time zone+ DT.DefaultTimeZone(DefaultTimeZone),+ DT.utcDefault,+ + -- * FileContents+ FileContents(FileContents, unFileContents),+ + -- * Errors+ ErrorMsg (ErrorMsg, unErrorMsg),++ -- * Items+ I.Item(Transaction, Price, CommentItem, BlankLine),+ I.Line(unLine),++ -- * Parsing+ Ledger(Ledger, unLedger),+ parse,+ + -- * Rendering+ I.render+ ) where+++import Control.Applicative ((<$>))+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Text as X+import Text.Parsec ( manyTill, eof )+import qualified Text.Parsec as P++import qualified Penny.Copper.Comments as C+import qualified Penny.Copper.Qty as Q+import qualified Penny.Copper.DateTime as DT+import qualified Penny.Copper.Item as I+import qualified Penny.Lincoln as L++data Ledger =+ Ledger { unLedger :: [(I.Line, I.Item)] }+ deriving Show++newtype FileContents = FileContents { unFileContents :: X.Text }+ deriving (Eq, Show)++newtype ErrorMsg = ErrorMsg { unErrorMsg :: X.Text }+ deriving (Eq, Show)++parseFile ::+ DT.DefaultTimeZone+ -> Q.RadGroup+ -> (L.Filename, FileContents)+ -> Ex.Exceptional ErrorMsg+ [(I.Line, I.Item)]+parseFile dtz rg (fn, (FileContents c)) =+ let p = addFileMetadata fn+ <$> manyTill (I.itemWithLineNumber dtz rg) eof+ fnStr = X.unpack . L.unFilename $ fn+ in case P.parse p fnStr c of+ Left err -> Ex.throw (ErrorMsg . X.pack . show $ err)+ Right g -> return g++addFileMetadata ::+ L.Filename+ -> [(I.Line, I.Item)]+ -> [(I.Line, I.Item)]+addFileMetadata fn ls =+ let (lns, is) = (map fst ls, map snd ls)+ eis = map toEiItem is+ procTop s m =+ m { L.fileTransaction = Just (L.FileTransaction s)+ , L.filename = Just fn }+ procPstg s m =+ m { L.filePosting = Just (L.FilePosting s) }+ eis' = L.addSerialsToEithers procTop procPstg eis+ is' = map fromEiItem eis'+ in zip lns is'+++addGlobalMetadata ::+ [[(I.Line, I.Item)]]+ -> [(I.Line, I.Item)]+addGlobalMetadata lss =+ let ls = concat lss+ procTop s m =+ m { L.globalTransaction = Just (L.GlobalTransaction s) }+ procPstg s m =+ m { L.globalPosting = Just (L.GlobalPosting s) }+ (lns, is) = (map fst ls, map snd ls)+ eis = map toEiItem is+ eis' = L.addSerialsToEithers procTop procPstg eis+ is' = map fromEiItem eis'+ in zip lns is'++parse ::+ DT.DefaultTimeZone+ -> Q.RadGroup+ -> [(L.Filename, FileContents)]+ -> Ex.Exceptional ErrorMsg Ledger+parse dtz rg ps =+ mapM (parseFile dtz rg) ps+ >>= (return . Ledger . addGlobalMetadata)++data Other = OPrice L.PricePoint+ | OCommentItem C.Comment+ | OBlankLine+ deriving Show++type EiItem = Either Other L.Transaction++toEiItem :: I.Item -> EiItem+toEiItem i = case i of+ I.Transaction t -> Right t+ I.Price p -> Left (OPrice p)+ I.CommentItem c -> Left (OCommentItem c)+ I.BlankLine -> Left OBlankLine++fromEiItem :: EiItem -> I.Item+fromEiItem i = case i of+ Left l -> case l of+ OPrice p -> I.Price p+ OCommentItem c -> I.CommentItem c+ OBlankLine -> I.BlankLine+ Right t -> I.Transaction t+
+ Penny/Copper/Account.hs view
@@ -0,0 +1,131 @@+-- | Account parsers. Account names fall into one of three groups:+--+-- * Level 1 account. Can have nearly any character, including+-- spaces. However, when in a Ledger file they must be quoted.+--+-- * Level 2 account. The first sub-account begins with a letter. All+-- other characters may be nearly any character, except for a space.+module Penny.Copper.Account (+ lvl1Account+ , lvl1AccountQuoted+ , lvl2Account+ , render+ , lvl1Char+ , lvl2FirstChar+ , lvl2RemainingChar+ ) where++import Control.Applicative((<$>), (<*>), (*>))+import qualified Data.Foldable as F+import Data.Text ( snoc, cons, pack, Text )+import qualified Data.Traversable as T+import Text.Parsec (+ char, satisfy, many, (<?>),+ many1, between, sepBy1, option )+import Text.Parsec.Text ( Parser )++import Data.List.NonEmpty (NonEmpty((:|)))+import qualified Data.List.NonEmpty as NE+import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ),+ unsafeTextNonEmpty )+import qualified Penny.Lincoln.HasText as HT+import qualified Penny.Copper.Util as U++-- | Characters allowed in a Level 1 account. (Check the source code+-- to see what these are).+lvl1Char :: Char -> Bool+lvl1Char c = allowed && notBanned where+ allowed = U.rangeLettersToSymbols c || c == ' '+ notBanned = not $ c `elem` "}:"++lvl1Sub :: Parser B.SubAccountName+lvl1Sub = f <$> p <?> e where+ f = B.SubAccountName . unsafeTextNonEmpty+ p = many1 (satisfy lvl1Char)+ e = "sub account name"++lvl1Account :: Parser B.Account+lvl1Account = B.Account . NE.fromList <$> p <?> e where+ e = "account name"+ p = sepBy1 lvl1Sub (char ':')++lvl1AccountQuoted :: Parser B.Account+lvl1AccountQuoted = between (char '{') (char '}') lvl1Account++-- | Characters allowed for the first character of a Level 2 account.+lvl2FirstChar :: Char -> Bool+lvl2FirstChar = U.rangeLetters++-- | Characters allowed for the remaining characters of a Level 2+-- account.+lvl2RemainingChar :: Char -> Bool+lvl2RemainingChar c = allowed && notBanned where+ allowed = U.rangeLettersToSymbols c+ notBanned = not $ c `elem` "}:"++lvl2SubAccountFirst :: Parser B.SubAccountName+lvl2SubAccountFirst = f <$> c1 <*> cs <?> e where+ c1 = satisfy lvl2FirstChar+ cs = many (satisfy lvl2RemainingChar)+ f l1 lr = B.SubAccountName (TextNonEmpty l1 (pack lr))+ e = "sub account name beginning with a letter"+ +lvl2SubAccountRest :: Parser B.SubAccountName+lvl2SubAccountRest = f <$> cs <?> e where+ cs = many1 (satisfy p)+ p c = allowed && notBanned where+ allowed = U.rangeLettersToSymbols c+ notBanned = not $ c `elem` "}:"+ f = B.SubAccountName . unsafeTextNonEmpty+ e = "sub account name"++lvl2Account :: Parser B.Account+lvl2Account = f <$> p1 <*> p2 <?> e where+ f x y = B.Account (x :| y)+ p1 = lvl2SubAccountFirst+ p2 = option [] $+ char ':' *> sepBy1 lvl2SubAccountRest (char ':')+ e = "account name"++data Level = L1 | L2+ deriving (Eq, Ord, Show)++-- | Checks an account to see what level to render it at.+checkAccount :: B.Account -> Maybe Level+checkAccount (B.Account subs) = let+ checkFirst = checkFirstSubAccount (NE.head subs)+ checkRest = map checkFirstSubAccount (NE.tail subs)+ in F.minimum <$> T.sequenceA (checkFirst : checkRest)++-- | Checks the first sub account to see if it qualifies as a Level 1+-- or Level 2 sub account.+checkFirstSubAccount ::+ B.SubAccountName+ -> Maybe Level+checkFirstSubAccount s = do+ l <- checkOtherSubAccount s+ return $ case l of+ L1 -> L1+ L2 -> let (B.SubAccountName (TextNonEmpty c _)) = s+ in if lvl2FirstChar c then L2 else L1++-- | Checks a sub account other than the first one to see if it+-- qualifies as a Level 1 or Level 2 sub account.+checkOtherSubAccount ::+ B.SubAccountName+ -> Maybe Level+checkOtherSubAccount = U.checkText ls where+ ls = (lvl2RemainingChar, L2) :| [(lvl1Char, L1)]++-- | Shows an account, with the minimum level of quoting+-- possible. Fails with an error if any one of the characters in the+-- account name does not satisfy the 'lvl1Char' predicate. Otherwise+-- returns a rendered account, quoted if necessary.+render :: B.Account -> Maybe Text+render a = do+ l <- checkAccount a+ let t = HT.text . HT.Delimited (pack ":") . HT.textList $ a+ return $ case l of+ L1 -> cons '{' t `snoc` '}'+ L2 -> t
+ Penny/Copper/Amount.hs view
@@ -0,0 +1,96 @@+-- | Amount parsers. An amount is a commodity and a quantity. (An+-- entry is an amount and a debit or credit).+--+-- Possible combinations:+--+-- * quoted Level 1 commodity, optional whitespace, quantity+--+-- * Level 3 commodity, optional whitespace, quantity+--+-- * Quantity, optional whitespace, quoted Level 1 commodity+--+-- * Quantity, optional whitespace, Level 2 commodity+--+-- Each quantity may be quoted or unquoted.+module Penny.Copper.Amount (+ amount+ , render+ ) where++import Control.Applicative ((<$>), (<*>), (<|>))+import qualified Data.Text as X+import Text.Parsec ( char, many, (<?>) )+import Text.Parsec.Text ( Parser )++import qualified Penny.Copper.Commodity as C+import qualified Penny.Copper.Qty as Q+import qualified Penny.Lincoln as L++-- | Parse optional spaces, returns appropriate metadata.+spaces :: Parser L.SpaceBetween+spaces = f <$> many (char ' ') where+ f l = if null l then L.NoSpaceBetween else L.SpaceBetween++cmdtyQty :: Parser L.Commodity+ -> Q.RadGroup+ -> Parser (L.Amount, L.Format)+cmdtyQty p rg = let+ f c s q = (a, fmt) where+ a = L.Amount q c+ fmt = L.Format L.CommodityOnLeft s+ e = "amount, commodity on left"+ in f <$> p <*> spaces <*> Q.qty rg <?> e++lvl1CmdtyQty :: Q.RadGroup -> Parser (L.Amount, L.Format)+lvl1CmdtyQty = cmdtyQty C.quotedLvl1Cmdty++lvl3CmdtyQty :: Q.RadGroup -> Parser (L.Amount, L.Format)+lvl3CmdtyQty = cmdtyQty C.lvl3Cmdty++cmdtyOnRight :: Q.RadGroup -> Parser (L.Amount, L.Format)+cmdtyOnRight rg = let + f q s c = (a, fmt) where+ a = L.Amount q c+ fmt = L.Format L.CommodityOnRight s+ e = "amount, commodity on right"+ in f+ <$> Q.qty rg+ <*> spaces+ <*> (C.quotedLvl1Cmdty <|> C.lvl2Cmdty)+ <?> e++-- | Parses an amount with its metadata. Handles all combinations of+-- commodities and quantities.+amount :: Q.RadGroup -> Parser (L.Amount, L.Format)+amount rg = lvl1CmdtyQty rg+ <|> lvl3CmdtyQty rg+ <|> cmdtyOnRight rg+ <?> "amount"++-- | Render an Amount. The Format is required so that the commodity+-- can be displayed in the right place.+render ::+ (Q.GroupingSpec, Q.GroupingSpec)+ -- ^ Grouping+ -> Q.RadGroup+ -> L.Format+ -> L.Amount+ -> Maybe X.Text+render gs rg f a = let+ (q, c) = (L.qty a, L.commodity a)+ qty = Q.quote $ Q.renderUnquoted rg gs q+ ws = case L.between f of+ L.SpaceBetween -> X.singleton ' '+ L.NoSpaceBetween -> X.empty+ mayLvl3 = C.renderLvl3 c+ mayLvl2 = C.renderLvl2 c+ in do+ quotedLvl1 <- C.renderQuotedLvl1 c+ let (l, r) = case L.side f of+ L.CommodityOnLeft -> case mayLvl3 of+ Nothing -> (quotedLvl1, qty)+ Just l3 -> (l3, qty)+ L.CommodityOnRight -> case mayLvl2 of+ Nothing -> (qty, quotedLvl1)+ Just l2 -> (qty, l2)+ return $ X.concat [l, ws, r]
+ Penny/Copper/Comments.hs view
@@ -0,0 +1,35 @@+module Penny.Copper.Comments (+ Comment(Comment)+ , comment+ , isCommentChar+ , render+ ) where++import Control.Applicative ((<*>), (<*), (<$))+import Data.Text ( Text, pack, cons, snoc )+import qualified Data.Text as X+import Text.Parsec (many, char, satisfy, (<?>))+import Text.Parsec.Text ( Parser )++import Penny.Copper.Util (eol)+import qualified Penny.Copper.Util as U++data Comment = Comment Text+ deriving (Eq, Show)++isCommentChar :: Char -> Bool+isCommentChar c = inCategory || allowed where+ allowed = c `elem` " "+ inCategory = U.rangeLettersToSymbols c++comment :: Parser Comment+comment = Comment . pack+ <$ char '#'+ <*> many (satisfy isCommentChar)+ <* eol+ <?> "single line comment"++render :: Comment -> Maybe Text+render (Comment t) = case X.find (not . isCommentChar) t of+ Nothing -> return $ '#' `cons` t `snoc` '\n'+ Just _ -> Nothing
+ Penny/Copper/Commodity.hs view
@@ -0,0 +1,187 @@+-- | Commodity parsers. Commodity names fall into three groups:+--+-- * Level 1 commodities. These can have a broad selection of+-- characters, including spaces. The downside is that they need to be+-- quoted when they appear in a file. (They don't have to be quoted+-- from the command line, presuming that a single command line+-- argument captures the entire commodity name and nothing else.)+--+-- * Level 2 commodities. The first sub-commodity begins with a letter+-- or a symbol. All other characters may be nearly any other+-- character. Spaces however are not permitted.+--+-- * Level 3 commodities. All charcters must be letters or symbols. In+-- addition, the first character cannot be a @+@ or a @-@.+module Penny.Copper.Commodity (+ -- * Level 1 commodities+ lvl1Char,+ lvl1Cmdty,+ quotedLvl1Cmdty,+ commandLineCmdty,+ + -- * Level 2 commodities+ lvl2FirstChar,+ lvl2OtherChars,+ lvl2Cmdty,+ + -- * Level 3 commodities+ lvl3FirstChar,+ lvl3OtherChars,+ lvl3Cmdty,+ + -- * Helpers when parsing from a file+ leftSideCmdty,+ rightSideCmdty,+ + -- * Rendering+ renderQuotedLvl1,+ renderLvl2,+ renderLvl3+ ) where++import Control.Applicative ((<*>), (<$>), (*>), (<|>))+import Control.Monad (guard)+import Data.Text ( pack, Text, cons, snoc, singleton )+import Text.Parsec ( satisfy, many, char, sepBy1, many1, (<?>),+ between, option, sepBy )+import Text.Parsec.Text ( Parser )++import qualified Penny.Lincoln.Bits as B+import Data.List.NonEmpty (NonEmpty((:|)), fromList)+import Penny.Copper.Util (listIsOK, firstCharOfListIsOK)+import qualified Penny.Copper.Util as U+import qualified Penny.Lincoln.HasText as HT+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ),+ unsafeTextNonEmpty )++-- | Most liberal set of letters allowed in a commodity. +lvl1Char :: Char -> Bool+lvl1Char c = (category || specific) && notBanned where+ category = U.rangeLettersToSymbols c+ specific = c == ' '+ notBanned = not $ c `elem` ['"', ':']++-- | A sub commodity comprised of the most liberal characters.+lvl1SubCmdty :: Parser B.SubCommodity+lvl1SubCmdty = f <$> m <?> "sub commodity" where+ m = many1 (satisfy lvl1Char)+ f cs = B.SubCommodity (TextNonEmpty (head cs) (pack $ tail cs))++-- | A commodity that might have spaces inside of the name. To parse+-- this when it is in a ledger file, it must be quoted; use+-- quotedLvl1Cmdty for that. This parser can be used directly for values+-- entered from the command line.+lvl1Cmdty :: Parser B.Commodity+lvl1Cmdty = (B.Commodity . fromList)+ <$> sepBy1 lvl1SubCmdty (char ':')+ <?> "commodity with spaces"+++-- | A commodity that may have spaces in the name; is wrapped inside+-- of double quotes.+quotedLvl1Cmdty :: Parser B.Commodity+quotedLvl1Cmdty = between q q lvl1Cmdty+ <?> "quoted commodity" where+ q = char '"'++-- | Allows only letters and symbols.+lvl2FirstChar :: Char -> Bool+lvl2FirstChar c = U.rangeLetters c || U.rangeMathCurrency c++lvl2OtherChars :: Char -> Bool+lvl2OtherChars c = category && notBanned where+ category = U.rangeLettersToSymbols c+ notBanned = not $ c `elem` ['"', ':']++lvl2FirstSubCmdty :: Parser B.SubCommodity+lvl2FirstSubCmdty = f <$> firstLet <*> restLet <?> e where+ e = "sub commodity, first character is letter or symbol"+ firstLet = satisfy lvl2FirstChar+ restLet = many (satisfy lvl2OtherChars)+ f l1 lr = B.SubCommodity (TextNonEmpty l1 (pack lr))++lvl2OtherSubCmdty :: Parser B.SubCommodity+lvl2OtherSubCmdty = f <$> ls <?> e where+ e = "sub commodity"+ ls = many1 (satisfy lvl2OtherChars)+ f = B.SubCommodity . unsafeTextNonEmpty++lvl2Cmdty :: Parser B.Commodity+lvl2Cmdty = f <$> firstSub <*> restSubs <?> e where+ e = "commodity, first character is letter or symbol"+ firstSub = lvl2FirstSubCmdty+ restSubs = option []+ $ char ':'+ *> sepBy1 lvl2OtherSubCmdty (char ':')+ f s1 sr = B.Commodity (s1 :| sr)++lvl3OtherChars :: Char -> Bool+lvl3OtherChars c = U.rangeLetters c || U.rangeMathCurrency c++lvl3FirstChar :: Char -> Bool+lvl3FirstChar c = lvl3OtherChars c && (not $ c `elem` "+-")++lvl3FirstSubCmdty :: Parser B.SubCommodity+lvl3FirstSubCmdty = f <$> c <*> cs <?> e where+ e = "first sub commodity, letters and symbols only, "+ ++ "first character not a + or -"+ f c1 cr = B.SubCommodity (TextNonEmpty c1 (pack cr))+ c = satisfy lvl3FirstChar+ cs = many (satisfy lvl3OtherChars)++lvl3OtherSubCmdty :: Parser B.SubCommodity+lvl3OtherSubCmdty = f <$> ls <?> e where + e = "sub commodity, letters and symbols only"+ f = B.SubCommodity . unsafeTextNonEmpty+ ls = many1 (satisfy lvl3OtherChars)++lvl3Cmdty :: Parser B.Commodity+lvl3Cmdty = f <$> p1 <*> pr <?> e where+ f cf cs = B.Commodity (cf :| cs)+ p1 = lvl3FirstSubCmdty+ pr = option [] $ char ':' *> sepBy lvl3OtherSubCmdty (char ':')+ e = "commodity, letters and symbols only"++-- | A commodity being read in from the command line, where the+-- commodity is guaranteed to be the only thing to parse.+commandLineCmdty :: Parser B.Commodity+commandLineCmdty = lvl1Cmdty++-- | A commodity on the left side of a quantity in a ledger file.+leftSideCmdty :: Parser B.Commodity+leftSideCmdty =+ quotedLvl1Cmdty+ <|> lvl3Cmdty+ <?> "commodity to the left of the quantity"++-- | A commodity on the right side of a quantity in a ledger file.+rightSideCmdty :: Parser B.Commodity+rightSideCmdty =+ quotedLvl1Cmdty+ <|> lvl2Cmdty+ <?> "commodity to the right of the quantity"++-- | Render a quoted Level 1 commodity. Fails if any character does+-- not satisfy lvl1Char.+renderQuotedLvl1 :: B.Commodity -> Maybe Text+renderQuotedLvl1 ca@(B.Commodity c) = do+ guard $ listIsOK lvl1Char ca+ return $ '"'+ `cons` HT.text (HT.Delimited (singleton ':') (HT.textList c))+ `snoc` '"'++-- | Render a Level 2 commodity. Fails if the first character is not a+-- letter or a symbol, or if any other character is a space.+renderLvl2 :: B.Commodity -> Maybe Text+renderLvl2 (B.Commodity c) = do+ guard $ firstCharOfListIsOK lvl2FirstChar c+ guard $ listIsOK lvl2OtherChars c+ return $ HT.text (HT.Delimited (singleton ':') (HT.textList c))++-- | Render a Level 3 commodity. Fails if any character is not a+-- letter or a symbol.+renderLvl3 :: B.Commodity -> Maybe Text+renderLvl3 (B.Commodity c) = do+ guard $ listIsOK lvl3OtherChars c+ guard $ firstCharOfListIsOK lvl3FirstChar c+ return $ HT.text (HT.Delimited (singleton ':') (HT.textList c))
+ Penny/Copper/DateTime.hs view
@@ -0,0 +1,138 @@+module Penny.Copper.DateTime (+ DefaultTimeZone(DefaultTimeZone, unDefaultTimeZone)+ , dateTime+ , utcDefault+ , render+ ) where++import Control.Applicative ((<$>), optional, (<*>))+import qualified Data.Text as X+import Data.Time (fromGregorianValid)+import Data.Maybe (fromMaybe)+import qualified Data.Time as T+import Text.Parsec (char, digit, (<|>), (<?>))+import Text.Parsec.Text ( Parser )+import Control.Monad ( void, when )+import Data.Fixed ( Pico )+import System.Locale (defaultTimeLocale)++import Penny.Copper.Util (spaces)+import qualified Penny.Lincoln.Bits as B++newtype DefaultTimeZone =+ DefaultTimeZone { unDefaultTimeZone :: B.TimeZoneOffset }+ deriving (Eq, Show)++utcDefault :: DefaultTimeZone+utcDefault = DefaultTimeZone B.noOffset++charToDigit :: Char -> Int+charToDigit c = case c of+ '0' -> 0+ '1' -> 1+ '2' -> 2+ '3' -> 3+ '4' -> 4+ '5' -> 5+ '6' -> 6+ '7' -> 7+ '8' -> 8+ '9' -> 9+ _ -> error "unrecognized digit"++read2digits :: Parser Int+read2digits = f <$> digit <*> digit where+ f d1 d2 = charToDigit d1 * 10 + charToDigit d2++read4digits :: Parser Integer+read4digits = f <$> digit <*> digit <*> digit <*> digit where+ f d1 d2 d3 d4 = fromIntegral $+ charToDigit d1 * 1000+ + charToDigit d2 * 100+ + charToDigit d3 * 10+ + charToDigit d4++date :: Parser T.Day+date = do+ let slash = void $ char '/' <|> char '-'+ y <- read4digits+ slash+ m <- read2digits+ slash+ d <- read2digits+ case fromGregorianValid y m d of+ Nothing -> fail "invalid date"+ Just dt -> return dt++colon :: Parser ()+colon = void $ char ':'++hrs :: Parser Int+hrs = do+ h <- read2digits+ when (h > 23) $ fail "invalid hour"+ return h++mins :: Parser Int+mins = do+ m <- read2digits+ when (m > 59) $ fail "invalid minute"+ return m++secs :: Parser Pico+secs = do+ s <- fromIntegral <$> read2digits+ when (s > 59) $ fail "invalid seconds"+ return s++timeOfDay :: Parser T.TimeOfDay+timeOfDay = do+ h <- hrs+ colon+ m <- mins+ maybeS <- optional (colon >> secs)+ let s = fromMaybe (fromIntegral (0 :: Int)) maybeS+ return $ T.TimeOfDay h m s++timeZoneOffset :: Parser B.TimeZoneOffset+timeZoneOffset = do+ changeSign <-+ (char '+' >> return id)+ <|> (char '-' >> return (negate :: Int -> Int))+ <?> "time zone sign"+ h <- read2digits+ m <- read2digits+ let mi = h * 60 + m+ maybe (fail "invalid time zone offset") return+ $ B.minsToOffset (changeSign mi)++dateTime :: DefaultTimeZone -> Parser B.DateTime+dateTime (DefaultTimeZone dtz) = do+ d <- date+ spaces+ mayTod <- optional timeOfDay+ spaces+ mayTz <- optional timeZoneOffset+ let tod = fromMaybe T.midnight mayTod+ tz = fromMaybe dtz mayTz+ return (B.dateTime (T.LocalTime d tod) tz)++-- | Render a DateTime. If the DateTime is in the given+-- DefaultTimeZone, and the DateTime is midnight, then the time and+-- time zone will not be printed. Otherwise, the time and time zone+-- will both be printed. The test for time zone equality depends only+-- upon the time zone's offset from UTC.+render :: DefaultTimeZone -> B.DateTime -> X.Text+render (DefaultTimeZone dtz) dt = let+ lt = B.localTime dt+ off = B.timeZone dt+ fmtLong = "%F %T %z"+ fmtShort = "%F"+ sameZone = dtz == off+ local = T.localTimeOfDay lt+ isMidnight = local == T.midnight+ fmt = if sameZone && isMidnight+ then fmtShort+ else fmtLong+ zt = T.ZonedTime lt (T.minutesToTimeZone (B.offsetToMins off))+ in X.pack $ T.formatTime defaultTimeLocale fmt zt
+ Penny/Copper/Entry.hs view
@@ -0,0 +1,43 @@+module Penny.Copper.Entry (entry, render) where++import Control.Applicative ((<$>), (<*>))+import Control.Monad ( void )+import qualified Data.Text as X+import Text.Parsec ( char, string, optional, (<|>), (<?>) )+import Text.Parsec.Text ( Parser )++import Penny.Copper.Amount (amount)+import qualified Penny.Copper.Amount as A+import qualified Penny.Copper.Qty as Q+import Penny.Copper.Util (lexeme)+import qualified Penny.Lincoln as L++drCr :: Parser L.DrCr+drCr = let+ dr = do+ void (char 'D' <|> char 'd')+ void (char 'r') <|> void (string "ebit")+ return L.Debit+ cr = do+ void (char 'C' <|> char 'c')+ void (optional (char 'r' >> optional (string "edit")))+ return L.Credit+ in dr <|> cr++entry :: Q.RadGroup -> Parser (L.Entry, L.Format)+entry rg = f <$> lexeme drCr <*> amount rg <?> e where+ f dc (am, fmt) = (L.Entry dc am, fmt)+ e = "entry"++render ::+ (Q.GroupingSpec, Q.GroupingSpec)+ -> Q.RadGroup+ -> L.Format+ -> L.Entry+ -> Maybe X.Text+render gs rg f (L.Entry dc a) = do+ amt <- A.render gs rg f a+ let dcTxt = X.pack $ case dc of+ L.Debit -> "Dr"+ L.Credit -> "Cr"+ return $ X.append (X.snoc dcTxt ' ') amt
+ Penny/Copper/Flag.hs view
@@ -0,0 +1,30 @@+module Penny.Copper.Flag (flag, isFlagChar, render) where++import Control.Applicative ((<$>), (<*>))+import Data.Text ( pack, cons, snoc )+import qualified Data.Text as X+import Text.Parsec ( char, satisfy, many, between, (<?>))+import Text.Parsec.Text ( Parser )++import qualified Penny.Copper.Util as U+import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ) )+import qualified Penny.Lincoln.TextNonEmpty as TNE++isFlagChar :: Char -> Bool+isFlagChar c = allowed && not banned where+ allowed = U.rangeLettersToSymbols c ||+ c == ' '+ banned = c == ']'++flag :: Parser B.Flag+flag = between (char '[') (char ']') p <?> "flag" where+ p = (\c cs -> B.Flag (TextNonEmpty c (pack cs)))+ <$> satisfy isFlagChar+ <*> many (satisfy isFlagChar)++render :: B.Flag -> Maybe X.Text+render (B.Flag fl) =+ if TNE.all isFlagChar fl+ then Just $ '[' `cons` (TNE.toText fl) `snoc` ']'+ else Nothing
+ Penny/Copper/Item.hs view
@@ -0,0 +1,68 @@+module Penny.Copper.Item (+ itemWithLineNumber,+ Item(Transaction, Price, CommentItem, BlankLine),+ Line(unLine),+ render+ ) where++import Control.Applicative ((<$>), (<*>), (<$))+import qualified Data.Text as X+import Text.Parsec (getPosition, sourceLine, (<|>),+ (<?>))+import Text.Parsec.Text ( Parser )++import qualified Penny.Copper.Comments as C+import qualified Penny.Copper.DateTime as DT+import qualified Penny.Lincoln as L+import qualified Penny.Copper.Qty as Q+import Penny.Copper.Price ( price )+import qualified Penny.Copper.Price as P+import qualified Penny.Copper.Transaction as T+import Penny.Copper.Transaction ( transaction )+import Penny.Copper.Util (eol)+++-- | An Item is used both to hold the result of parsing an item from a+-- file and for rendering. It is parameterized on two types: the+-- metadata type for the TopLine, and the metadata type for the+-- Posting.+data Item = Transaction L.Transaction+ | Price L.PricePoint+ | CommentItem C.Comment+ | BlankLine+ deriving Show++newtype Line = Line { unLine :: Int }+ deriving (Eq, Show)++itemWithLineNumber ::+ DT.DefaultTimeZone+ -> Q.RadGroup+ -> Parser (Line, Item)+itemWithLineNumber dtz rg = (,)+ <$> ((Line . sourceLine) <$> getPosition)+ <*> parseItem dtz rg++parseItem ::+ DT.DefaultTimeZone+ -> Q.RadGroup+ -> Parser Item+parseItem dtz rg = let+ bl = BlankLine <$ eol <?> "blank line"+ t = Transaction <$> transaction dtz rg+ p = Price <$> price dtz rg+ c = CommentItem <$> C.comment+ in (bl <|> t <|> p <|> c)++render ::+ DT.DefaultTimeZone+ -> (Q.GroupingSpec, Q.GroupingSpec)+ -> Q.RadGroup+ -> Item+ -> Maybe X.Text+render dtz gs rg i = case i of+ Transaction t -> T.render dtz gs rg t+ Price pp -> P.render dtz gs rg pp+ CommentItem c -> C.render c+ BlankLine -> Just $ X.singleton '\n'+
+ Penny/Copper/Memos/Posting.hs view
@@ -0,0 +1,39 @@+module Penny.Copper.Memos.Posting (+ memo, render, isCommentChar) where++import Control.Applicative ((<*>), (<*), (<$), (<$>))+import qualified Data.Text as X+import Text.Parsec (char, satisfy, many, (<?>))+import Text.Parsec.Text ( Parser )++import Penny.Copper.Util (eol, rangeLettersToSymbols)+import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.TextNonEmpty as TNE++isCommentChar :: Char -> Bool+isCommentChar c = rangeLettersToSymbols c+ || c == ' '++memo :: Parser B.Memo+memo = B.Memo <$> many memoLine++memoLine :: Parser B.MemoLine+memoLine = (\c cs -> B.MemoLine $ TNE.TextNonEmpty c (X.pack cs))+ <$ char '\''+ <*> satisfy isCommentChar+ <*> many (satisfy isCommentChar)+ <* eol+ <?> "posting memo"++render :: B.Memo -> Maybe X.Text+render (B.Memo ls) = X.concat <$> mapM renderLine ls ++renderLine :: B.MemoLine -> Maybe X.Text+renderLine (B.MemoLine l) =+ if TNE.all isCommentChar l+ then Just $+ X.pack (replicate 8 ' ')+ `X.snoc` '\''+ `X.append` TNE.toText l+ `X.snoc` '\n'+ else Nothing
+ Penny/Copper/Memos/Transaction.hs view
@@ -0,0 +1,45 @@+module Penny.Copper.Memos.Transaction (+ memo, render, isCommentChar+ ) where++import Control.Applicative ((<*>), (<$>), (<*), (<$))+import qualified Data.Text as X+import Text.Parsec (+ char, satisfy, sourceLine, getPosition, (<?>),+ many)+import Text.Parsec.Text ( Parser )++import Penny.Copper.Util (rangeLettersToSymbols, eol)+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.TextNonEmpty as TNE++isCommentChar :: Char -> Bool+isCommentChar c = rangeLettersToSymbols c+ || c == ' '++memoLine :: Parser L.MemoLine+memoLine = L.MemoLine <$> (+ TNE.TextNonEmpty+ <$ char ';'+ <*> satisfy isCommentChar+ <*> (X.pack <$> many (satisfy isCommentChar))+ <* eol )+ <?> "posting memo line"++-- | Parses a transaction memo and associated whitespace afterward.+memo :: Parser (L.Memo, L.TopMemoLine)+memo =+ flip (,)+ <$> ((L.TopMemoLine . sourceLine) <$> getPosition)+ <*> (L.Memo <$> many memoLine)+ <?> "transaction memo"+ +-- | Renders a transaction memo. Fails if the memo is not renderable.+render :: L.Memo -> Maybe X.Text+render (L.Memo m) = X.concat <$> mapM renderLine m++renderLine :: L.MemoLine -> Maybe X.Text+renderLine (L.MemoLine l) =+ if TNE.all isCommentChar l+ then Just $ X.singleton ';' `X.append` TNE.toText l `X.snoc` '\n'+ else Nothing
+ Penny/Copper/Number.hs view
@@ -0,0 +1,29 @@+module Penny.Copper.Number (isNumChar, number, render) where++import Control.Applicative ((<$>), (<*>))+import Data.Text ( pack, cons, snoc, Text )+import Text.Parsec ( char, satisfy, many, between, (<?>))+import Text.Parsec.Text ( Parser )++import Penny.Copper.Util (rangeLettersToSymbols)+import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ) )+import qualified Penny.Lincoln.TextNonEmpty as TNE++isNumChar :: Char -> Bool+isNumChar c = allowed && not banned where+ allowed = rangeLettersToSymbols c ||+ c == ' '+ banned = c == ')'++number :: Parser B.Number+number = between (char '(') (char ')') p <?> "number" where+ p = (\c cs -> B.Number (TextNonEmpty c (pack cs)))+ <$> satisfy isNumChar+ <*> many (satisfy isNumChar)++render :: B.Number -> Maybe Text+render (B.Number tne) =+ if TNE.all isNumChar tne+ then Just $ '(' `cons` TNE.toText tne `snoc` ')'+ else Nothing
+ Penny/Copper/Payees.hs view
@@ -0,0 +1,87 @@+-- | Payee parsers. There are two types of payee parsers:+--+-- Quoted payees. These allow the most latitude in the characters+-- allowed. They are surrounded by @<@ and @>@.+--+-- Unquoted payees. These are not surrounded by @<@ and @>@. Their+-- first character must be a letter or number.++module Penny.Copper.Payees (+ -- * Parse any payee+ payee+ + -- * Quoted payees+ , quotedChar+ , quotedPayee+ + -- * Unquoted payees+ , unquotedFirstChar+ , unquotedRestChars+ , unquotedPayee+ + -- * Rendering+ , smartRender+ , quoteRender+ ) where++import Control.Applicative ((<$>), (<*>), (<|>))+import Data.Text (pack, Text, snoc, cons)+import qualified Data.Text as X+import Text.Parsec (char, satisfy, many, between, (<?>))+import Text.Parsec.Text ( Parser )++import Penny.Copper.Util (rangeLettersToSymbols, rangeLetters)+import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.TextNonEmpty as TNE++quotedChar :: Char -> Bool+quotedChar c = allowed && not banned where+ allowed = rangeLettersToSymbols c || c == ' '+ banned = c == '>'++payee :: Parser B.Payee+payee = quotedPayee <|> unquotedPayee++quotedPayee :: Parser B.Payee+quotedPayee = between (char '<') (char '>') p <?> "quoted payee" where+ p = (\c cs -> B.Payee (TextNonEmpty c (pack cs)))+ <$> satisfy quotedChar+ <*> many (satisfy quotedChar)++unquotedFirstChar :: Char -> Bool+unquotedFirstChar = rangeLetters++unquotedRestChars :: Char -> Bool+unquotedRestChars = quotedChar++unquotedPayee :: Parser B.Payee+unquotedPayee = let+ p c cs = B.Payee (TextNonEmpty c (pack cs))+ in p+ <$> satisfy unquotedFirstChar+ <*> many (satisfy unquotedRestChars)+ <?> "unquoted payee"++-- | Render a payee with a minimum of quoting. Fails if cannot be+-- rendered at all.+smartRender :: B.Payee -> Maybe Text+smartRender (B.Payee p) = let+ TextNonEmpty f r = p+ noQuoteNeeded = unquotedFirstChar f+ && X.all unquotedRestChars r+ renderable = TNE.all quotedChar p+ quoted = '<' `cons` TNE.toText p `snoc` '>'+ makeText+ | noQuoteNeeded = Just $ TNE.toText p+ | renderable = Just quoted+ | otherwise = Nothing+ in makeText++-- | Renders with quotes, whether the payee needs it or not.+quoteRender :: B.Payee -> Maybe Text+quoteRender (B.Payee p) = let+ renderable = TNE.all quotedChar p+ quoted = '<' `cons` TNE.toText p `snoc` '>'+ in if renderable+ then Just quoted+ else Nothing
+ Penny/Copper/Posting.hs view
@@ -0,0 +1,126 @@+module Penny.Copper.Posting (+ posting, render+ ) where++import Control.Applicative ((<$>), (<*>), (<*), (<|>))+import qualified Data.Text as X+import Text.Parsec (+ getParserState,+ statePos, optionMaybe, sourceLine, (<?>),+ State)+import Text.Parsec.Text ( Parser )++import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Bits as B+import qualified Penny.Copper.Account as Ac+import qualified Penny.Copper.Entry as En+import qualified Penny.Copper.Flag as Fl+import qualified Penny.Copper.Memos.Posting as Me+import qualified Penny.Copper.Number as Nu+import qualified Penny.Copper.Payees as Pa+import qualified Penny.Copper.Qty as Qt+import qualified Penny.Copper.Tags as Ta+import Penny.Copper.Util (lexeme, eol, renMaybe, txtWords)+import qualified Penny.Lincoln.Transaction as T+import qualified Penny.Lincoln.Transaction.Unverified as U++posting :: Qt.RadGroup+ -> Parser U.Posting+posting rg =+ makeUnverified+ <$> (L.PostingLine . sourceLine . statePos+ <$> getParserState)+ <*> (UnverifiedWithMeta+ <$> optionMaybe (lexeme Fl.flag)+ <*> optionMaybe (lexeme Nu.number)+ <*> optionMaybe (lexeme Pa.quotedPayee)+ <*> lexeme (Ac.lvl1AccountQuoted <|> Ac.lvl2Account)+ <*> lexeme Ta.tags+ <*> optionMaybe (lexeme (En.entry rg))+ <* eol+ <*> Me.memo)+ <?> "posting"++data UnverifiedWithMeta = UnverifiedWithMeta {+ flag :: Maybe B.Flag+ , number :: Maybe B.Number+ , payee :: Maybe B.Payee+ , account :: B.Account+ , tags :: B.Tags+ , entry :: Maybe (B.Entry, L.Format)+ , memo :: B.Memo+ } deriving (Eq, Show)+ +makeUnverified ::+ L.PostingLine+ -> UnverifiedWithMeta+ -> U.Posting+makeUnverified pl u = upo where+ upo = U.Posting (payee u) (number u) (flag u) (account u)+ (tags u) en (memo u) meta+ (en, fmt) = case entry u of+ Nothing -> (Nothing, Nothing)+ Just (e, f) -> (Just e, Just f)+ meta = L.PostingMeta (Just pl) fmt Nothing Nothing+++-- | Renders a Posting. Fails if any of the components+-- fail to render. In addition, if the unverified Posting has an+-- Entry, a Format must be provided, otherwise render fails.+--+-- The columns look like this. Column numbers begin with 0 (like they+-- do in Emacs) rather than with column 1 (like they do in+-- Vim). (Really Emacs is the strange one; most CLI utilities seem to+-- start with column 1 too...)+--+-- > ID COLUMN WIDTH WHAT+-- > ---------------------------------------------------+-- > A 0 4 Blank spaces for indentation+-- > B 4 50 Flag, Number, Payee, Account, Tags+-- > C 54 2 Blank spaces for padding+-- > D 56 NA Entry+--+-- Omit the padding after column B if there is no entry; also omit+-- columns C and D entirely if there is no Entry. (It is annoying to+-- have extraneous blank space in a file).+render ::+ (Qt.GroupingSpec, Qt.GroupingSpec)+ -> Qt.RadGroup+ -> T.Posting+ -> Maybe X.Text+render gs rg p = do+ fl <- renMaybe (T.pFlag p) Fl.render+ nu <- renMaybe (T.pNumber p) Nu.render+ pa <- renMaybe (T.pPayee p) Pa.quoteRender+ ac <- Ac.render (T.pAccount p)+ ta <- Ta.render (T.pTags p)+ me <- Me.render (T.pMemo p)+ maybePair <- case (T.pInferred p, L.postingFormat . T.pMeta $ p) of+ (T.Inferred, Nothing) -> return Nothing+ (T.NotInferred, Just f) -> return (Just (T.pEntry p, f))+ _ -> Nothing+ let renderEn (e, f) = En.render gs rg f e+ en <- renMaybe maybePair renderEn+ return $ formatter fl nu pa ac ta en me++formatter ::+ X.Text -- ^ Flag+ -> X.Text -- ^ Number+ -> X.Text -- ^ Payee+ -> X.Text -- ^ Account+ -> X.Text -- ^ Tags+ -> X.Text -- ^ Entry+ -> X.Text -- ^ Memo+ -> X.Text+formatter fl nu pa ac ta en me = let+ colA = X.pack (replicate 4 ' ')+ colBnoPad = txtWords [fl, nu, pa, ac, ta]+ colD = en+ colB = if X.null en+ then colBnoPad+ else X.justifyLeft 50 ' ' colBnoPad+ colC = if X.null en+ then X.empty+ else X.pack (replicate 2 ' ')+ rtn = X.singleton '\n'+ in X.concat [colA, colB, colC, colD, rtn, me]
+ Penny/Copper/Price.hs view
@@ -0,0 +1,97 @@+module Penny.Copper.Price (price, render) where++import Text.Parsec ( char, getPosition, sourceLine, (<?>),+ SourcePos )+import Text.Parsec.Text ( Parser )++import Control.Applicative ((<*>), (<*), (<|>), (<$))+import Data.Text (singleton, snoc, intercalate)+import qualified Data.Text as X++import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Bits.PricePoint as PP+import qualified Penny.Copper.Amount as A+import qualified Penny.Copper.Commodity as C+import qualified Penny.Copper.DateTime as DT+import qualified Penny.Copper.Qty as Q+import Penny.Copper.Util (lexeme, eol)++{-+BNF-style specification for prices:++<price> ::= "dateTime" <fromCmdty> <toAmount>+<fromCmdty> ::= "quotedLvl1Cmdty" | "lvl2Cmdty"+<toAmount> ::= "amount"+-}++mkPrice :: SourcePos+ -> L.DateTime+ -> L.Commodity+ -> (L.Amount, L.Format)+ -> Maybe L.PricePoint+mkPrice pos dt from (am, fmt) = let+ to = L.commodity am+ q = L.qty am+ pm = L.PriceMeta (Just pl) (Just fmt)+ pl = L.PriceLine . sourceLine $ pos+ in do+ p <- L.newPrice (L.From from) (L.To to) (L.CountPerUnit q)+ return $ L.PricePoint dt p pm++maybePrice ::+ DT.DefaultTimeZone+ -> Q.RadGroup+ -> Parser (Maybe L.PricePoint)+maybePrice dtz rg =+ mkPrice+ <$ lexeme (char '@')+ <*> getPosition+ <*> lexeme (DT.dateTime dtz)+ <*> lexeme (C.quotedLvl1Cmdty <|> C.lvl2Cmdty)+ <*> A.amount rg+ <* eol+ <?> "price"+ +-- | A price with an EOL and whitespace after the EOL. @price d r@+-- will parse a PriceBox, where @d@ is the DefaultTimeZone for the+-- DateTime in the PricePoint, and @r@ is the radix point and grouping+-- character to parse the Amount. Fails if the price is not valid+-- (e.g. the from and to commodities are the same).+price ::+ DT.DefaultTimeZone+ -> Q.RadGroup+ -> Parser L.PricePoint+price dtz rg = do+ b <- maybePrice dtz rg+ case b of+ Nothing -> fail "invalid price given"+ Just p -> return p++-- | @render dtz rg f pp@ renders a price point @pp@. @dtz@ is the+-- DefaultTimeZone for rendering of the DateTime in the Price+-- Point. @rg@ is the radix point and grouping character for the+-- amount. @f@ is the Format for how the price will be+-- formatted. Fails if either the From or the To commodity cannot be+-- rendered.+render ::+ DT.DefaultTimeZone+ -> (Q.GroupingSpec, Q.GroupingSpec)+ -> Q.RadGroup+ -> L.PricePoint+ -> Maybe X.Text+render dtz gs rg pp = let+ dateTxt = DT.render dtz (PP.dateTime pp)+ (L.From from) = L.from . L.price $ pp+ (L.To to) = L.to . L.price $ pp+ (L.CountPerUnit q) = L.countPerUnit . L.price $ pp+ mayFromTxt = C.renderLvl3 from <|> C.renderQuotedLvl1 from+ amt = L.Amount q to+ in do+ fmt <- L.priceFormat . L.ppMeta $ pp+ let mayAmtTxt = A.render gs rg fmt amt+ amtTxt <- mayAmtTxt+ fromTxt <- mayFromTxt+ return $+ (intercalate (singleton ' ')+ [singleton '@', dateTxt, fromTxt, amtTxt])+ `snoc` '\n'
+ Penny/Copper/Qty.hs view
@@ -0,0 +1,283 @@+module Penny.Copper.Qty (+ -- * Setting the radix and separator characters+ RadGroup, periodComma, periodSpace, commaPeriod,+ commaSpace,+ + -- * Rendering+ GroupingSpec(NoGrouping, GroupLarge, GroupAll),+ renderUnquoted,+ quote,++ -- * Parsing quantities+ qtyUnquoted,+ qtyQuoted,+ qty) where++import Control.Applicative ((<$>), (<*>), (<$), (*>), optional)+import qualified Data.Decimal as D+import Data.List (intercalate)+import Data.List.Split (splitEvery, splitOn)+import qualified Data.Text as X+import Data.Text (snoc, cons)+import Text.Parsec ( char, (<|>), many1, (<?>), + sepBy1, digit, between)+import qualified Text.Parsec as P+import Text.Parsec.Text ( Parser )++import Penny.Lincoln.Bits.Qty ( Qty, partialNewQty, unQty )++data Radix = RComma | RPeriod deriving (Eq, Show)+data Grouper = GComma | GPeriod | GSpace deriving (Eq, Show)++data RadGroup = RadGroup Radix Grouper deriving (Eq, Show)++-- | Radix is period, grouping is comma+periodComma :: RadGroup+periodComma = RadGroup RPeriod GComma++-- | Radix is period, grouping is space+periodSpace :: RadGroup+periodSpace = RadGroup RPeriod GSpace++-- | Radix is comma, grouping is period+commaPeriod :: RadGroup+commaPeriod = RadGroup RComma GPeriod++-- | Radix is comma, grouping is space+commaSpace :: RadGroup+commaSpace = RadGroup RComma GSpace++parseRadix :: Radix -> Parser ()+parseRadix r = () <$ char c <?> "radix point" where+ c = case r of RComma -> ','; RPeriod -> '.'++parseGrouper :: Grouper -> Parser ()+parseGrouper g = () <$ char c <?> "grouping character" where+ c = case g of+ GComma -> ','+ GPeriod -> '.'+ GSpace -> ' '++{- a BNF style specification for numbers.++<radix> ::= (as specified)+<grouper> ::= (as specified)+<digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"+<digits> ::= <digit> | <digit> <digits>+<firstGroups> ::= <digits> <grouper> <digits>+<nextGroup> ::= <grouper> <digits>+<nextGroups> ::= <nextGroup> | <nextGroup> <nextGroups>+<allGroups> ::= <firstGroups> | <firstGroups> <nextGroups>+<whole> ::= <allGroups> | <digits>+<fractional> ::= <digits>+<number> ::= <whole>+ | <whole> <radix>+ | <whole> <radix> <fractional>+ | <radix> <fractional>+-}++wholeGrouped :: Grouper -> Parser String+wholeGrouped g = p <$> group1 <*> optional groupRest <?> e where + e = "whole number"+ group1 = many1 digit+ groupRest = parseGrouper g *> sepBy1 (many1 digit) (parseGrouper g)+ p g1 gr = case gr of+ Nothing -> g1+ Just groups -> g1 ++ concat groups++fractionalGrouped :: Grouper -> Parser String+fractionalGrouped g =+ p <$> group1 <*> optional groupRest <?> e where+ e = "fractional number"+ group1 = many1 digit+ groupRest = parseGrouper g *> sepBy1 (many1 digit) (parseGrouper g)+ p g1 gr = case gr of+ Nothing -> g1+ Just groups -> g1 ++ concat groups++wholeNonGrouped :: Parser String+wholeNonGrouped = many1 digit++fractionalOnly :: Radix -> Parser String+fractionalOnly r = parseRadix r *> many1 P.digit++numberStrGrouped :: Radix -> Grouper -> Parser NumberStr+numberStrGrouped r g = startsWhole <|> fracOnly <?> e where+ e = "quantity, with optional grouping"+ startsWhole = p <?> "whole number" where+ p = do+ wholeStr <- wholeGrouped g+ mayRad <- optional (parseRadix r)+ case mayRad of+ Nothing -> return $ Whole wholeStr+ Just _ -> do+ mayFrac <- optional $ fractionalGrouped g+ case mayFrac of+ Nothing -> return $ WholeRad wholeStr+ Just frac -> return $ WholeRadFrac wholeStr frac+ fracOnly = RadFrac <$> fractionalOnly r++numberStrNonGrouped :: Radix -> Parser NumberStr+numberStrNonGrouped r = startsWhole <|> fracOnly <?> e where+ e = "quantity, no grouping"+ startsWhole = p <?> "whole number" where+ p = do+ wholeStr <- wholeNonGrouped+ mayRad <- optional (parseRadix r)+ case mayRad of+ Nothing -> return $ Whole wholeStr+ Just _ -> do+ mayFrac <- optional $ many1 P.digit+ case mayFrac of+ Nothing -> return $ WholeRad wholeStr+ Just frac -> return $ WholeRadFrac wholeStr frac+ fracOnly = RadFrac <$> fractionalOnly r+++-- | A number string after radix and grouping characters have been+-- stripped out.+data NumberStr =+ Whole String+ -- ^ A whole number only. No radix point.+ | WholeRad String+ -- ^ A whole number and a radix point, but nothing after the radix+ -- point.+ | WholeRadFrac String String+ -- ^ A whole number and something after the radix point.+ | RadFrac String+ -- ^ A radix point and a fractional value after it, but nothing+ -- before the radix point.+ deriving Show++-- | Do not use Prelude.read or Prelude.reads on whole decimal strings+-- like @232.72@. Sometimes it will fail, though sometimes it will+-- succeed; why is not clear to me. Hopefully reading integers won't+-- fail! However, in case it does, use read', whose error message will+-- at least tell you what number was being read.+--+-- Data.Decimal cannot handle decimals whose exponent would exceed+-- 255, which is the maximum that a Word8 can hold. A Word8 is used to+-- hold the exponent. If the exponent would exceed 255, this function+-- fails.+toDecimal :: NumberStr -> Maybe D.Decimal+toDecimal ns = case ns of+ Whole s -> Just $ D.Decimal 0 (readWithErr s)+ WholeRad s -> Just $ D.Decimal 0 (readWithErr s)+ WholeRadFrac w f -> fromWholeRadFrac w f+ RadFrac f -> fromWholeRadFrac "0" f+ where+ fromWholeRadFrac w f = let+ len = length f+ in if len > 255+ then Nothing+ else Just $ D.Decimal (fromIntegral len) (readWithErr (w ++ f))++readWithErr :: String -> Integer+readWithErr s = let+ readSresult = reads s+ in case readSresult of+ (i, ""):[] -> i+ _ -> error $ "readWithErr failed. String being read: " ++ s+ ++ " Result of reads: " ++ show readSresult+ +-- | Unquoted quantity. These include no spaces, regardless of what+-- the grouping character is.+qtyUnquoted :: RadGroup -> Parser Qty+qtyUnquoted (RadGroup r g) = do+ nStr <- case g of+ GSpace -> numberStrNonGrouped r+ _ -> numberStrGrouped r g+ d <- case toDecimal nStr of+ Nothing -> fail $ "fractional part too big: " ++ show nStr+ Just dec -> return dec+ return $ partialNewQty d+ +-- | Parse quoted quantity. It can include spaces, if the grouping+-- character is a space. However these must be quoted when in a Ledger+-- file (from the command line they need not be quoted). The quote+-- character is a caret, @^@.+qtyQuoted :: RadGroup -> Parser Qty+qtyQuoted (RadGroup r g) = between (char '^') (char '^') p where+ p = do+ nStr <- numberStrGrouped r g+ d <- case toDecimal nStr of+ Nothing -> fail $ "fractional part too big: " ++ show nStr+ Just dec -> return dec+ return $ partialNewQty d++-- | Parse a quoted quantity or, if that fails, an unquoted+-- quantity.+qty :: RadGroup -> Parser Qty+qty r = qtyQuoted r <|> qtyUnquoted r <?> "quantity"++-- | Specifies how to perform digit grouping when rendering a+-- quantity. All grouping groups into groups of 3 digits.+data GroupingSpec = + NoGrouping+ -- ^ Do not perform any digit grouping+ | GroupLarge+ -- ^ Group digits, but only if the number to be grouped is greater+ -- than 9,999 (if grouping the whole part) or if there are more+ -- than 4 decimal places (if grouping the fractional part).+ | GroupAll+ -- ^ Group digits whenever there are at least four decimal places.+ deriving (Eq, Show)++-- | Quotes a rendered Qty, but only if necessary; otherwise, simply+-- leaves it unquoted.+quote :: X.Text -> X.Text+quote t = case X.find (== ' ') t of+ Nothing -> t+ Just _ -> '^' `cons` t `snoc` '^'++-- | Renders an unquoted Qty. Performs digit grouping as requested. +renderUnquoted ::+ RadGroup+ -> (GroupingSpec, GroupingSpec)+ -- ^ Group for the portion to the left and right of the radix point?++ -> Qty+ -> X.Text+renderUnquoted (RadGroup r g) (gl, gr) q = let+ qs = show . unQty $ q+ in X.pack $ case splitOn "." qs of+ w:[] -> groupWhole g gl w+ w:d:[] ->+ groupWhole g gl w ++ renderRadix r ++ groupDecimal g gr d+ _ -> error "Qty.hs: rendering error"++renderGrouper :: Grouper -> String+renderGrouper g = case g of+ GComma -> ","+ GPeriod -> "."+ GSpace -> " "++renderRadix :: Radix -> String+renderRadix r = case r of+ RComma -> ","+ RPeriod -> "."++-- | Performs grouping for amounts to the left of the radix point.+groupWhole :: Grouper -> GroupingSpec -> String -> String+groupWhole g gs o = let+ grouped = intercalate (renderGrouper g)+ . reverse+ . map reverse+ . splitEvery 3+ . reverse+ $ o+ in case gs of+ NoGrouping -> o+ GroupLarge -> if length o > 4 then grouped else o+ GroupAll -> grouped++-- | Performs grouping for amounts to the right of the radix point.+groupDecimal :: Grouper -> GroupingSpec -> String -> String+groupDecimal g gs o = let+ grouped = intercalate (renderGrouper g)+ . splitEvery 3+ $ o+ in case gs of+ NoGrouping -> o+ GroupLarge -> if length o > 4 then grouped else o+ GroupAll -> grouped
+ Penny/Copper/Tags.hs view
@@ -0,0 +1,37 @@+module Penny.Copper.Tags (+ isTagChar, tags, render+ )where++import Control.Applicative ((<$>), (*>), (<*>))+import qualified Data.Text as X+import Text.Parsec (char, satisfy, many, (<?>))+import Text.Parsec.Text ( Parser )++import Penny.Copper.Util (lexeme, rangeLettersNumbers)+import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.TextNonEmpty as TNE++isTagChar :: Char -> Bool+isTagChar = rangeLettersNumbers++tagChar :: Parser Char+tagChar = satisfy isTagChar++tag :: Parser B.Tag+tag = (char '*' *> (f <$> tagChar <*> many tagChar)) <?> e where+ f t ts = B.Tag $ TNE.TextNonEmpty t (X.pack ts)+ e = "tag"++tags :: Parser B.Tags+tags = B.Tags <$> many (lexeme tag)++renderTag :: B.Tag -> Maybe X.Text+renderTag (B.Tag t) =+ if TNE.all isTagChar t+ then Just $ X.cons '*' (TNE.toText t)+ else Nothing++render :: B.Tags -> Maybe X.Text+render (B.Tags ts) =+ X.intercalate (X.singleton ' ')+ <$> mapM renderTag ts
+ Penny/Copper/TopLine.hs view
@@ -0,0 +1,59 @@+module Penny.Copper.TopLine (+ topLine+ , render+ ) where++import Control.Applicative ((<$>), (<*>), (<*), (<|>), pure)+import qualified Data.Text as X+import Text.Parsec ( optionMaybe, getPosition,+ sourceLine, optionMaybe )+import Text.Parsec.Text ( Parser )++import qualified Penny.Copper.DateTime as DT+import qualified Penny.Copper.Memos.Transaction as M+import qualified Penny.Copper.Flag as F+import qualified Penny.Copper.Number as N+import qualified Penny.Copper.Payees as P+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Transaction as T+import Penny.Copper.Util (lexeme, eol, renMaybe, txtWords)+import qualified Penny.Lincoln.Transaction.Unverified as U++topLine ::+ DT.DefaultTimeZone+ -- -> Parser (U.TopLine (Meta.TopMemoLine, Meta.TopLineLine))+ -> Parser U.TopLine+topLine dtz =+ f+ <$> M.memo+ <*> getPosition+ <*> lexeme (DT.dateTime dtz)+ <*> optionMaybe (lexeme F.flag)+ <*> optionMaybe (lexeme N.number)+ <*> optionMaybe (P.quotedPayee <|> P.unquotedPayee)+ <* eol+ where+ f (me, tml) pos dt fl nu pa = tl where+ tl = U.TopLine dt fl nu pa me meta+ meta = L.emptyTopLineMeta { L.topMemoLine = Just tml+ , L.topLineLine = Just tll }+ tll = L.TopLineLine . sourceLine $ pos++++render ::+ DT.DefaultTimeZone+ -> T.TopLine+ -> Maybe X.Text+render dtz tl =+ f+ <$> M.render (T.tMemo tl)+ <*> pure (DT.render dtz (T.tDateTime tl))+ <*> renMaybe (T.tFlag tl) F.render+ <*> renMaybe (T.tNumber tl) N.render+ <*> renMaybe (T.tPayee tl) P.smartRender+ where+ f meX dtX flX nuX paX =+ meX+ `X.append` (txtWords [dtX, flX, nuX, paX])+ `X.snoc` '\n'
+ Penny/Copper/Transaction.hs view
@@ -0,0 +1,75 @@+module Penny.Copper.Transaction (transaction, render) where++import Control.Applicative ((<$>), (<*>))+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Foldable (toList)+import qualified Data.Traversable as Tr+import qualified Data.Text as X+import Text.Parsec (many)+import Text.Parsec.Text ( Parser )++import qualified Penny.Copper.DateTime as DT+import qualified Penny.Copper.TopLine as TL+import Penny.Copper.TopLine ( topLine )+import qualified Penny.Copper.Posting as Po+import qualified Penny.Copper.Qty as Qt+import qualified Penny.Lincoln as L+import Penny.Lincoln.Family (orphans)+import qualified Penny.Lincoln.Family.Family as F+import Penny.Lincoln.Family.Family ( Family ( Family ) )+import qualified Penny.Lincoln.Transaction as T+import qualified Penny.Lincoln.Transaction.Unverified as U++errorStr :: T.Error -> String+errorStr e = case e of+ T.UnbalancedError -> "postings are not balanced"+ T.CouldNotInferError -> "could not infer entry for posting"++mkTransaction ::+ U.TopLine+ -> U.Posting+ -> U.Posting+ -> [U.Posting]+ -> Ex.Exceptional String L.Transaction+mkTransaction top p1 p2 ps = let+ famTrans = Family top p1 p2 ps+ errXact = T.transaction famTrans+ in case errXact of+ Ex.Exception err -> Ex.Exception . errorStr $ err+ Ex.Success x -> return x++maybeTransaction ::+ DT.DefaultTimeZone+ -> Qt.RadGroup+ -> Parser (Ex.Exceptional String L.Transaction)+maybeTransaction dtz rg =+ mkTransaction+ <$> topLine dtz+ <*> Po.posting rg+ <*> Po.posting rg+ <*> many (Po.posting rg)++transaction ::+ DT.DefaultTimeZone+ -> Qt.RadGroup+ -> Parser L.Transaction+transaction dtz rg = do+ ex <- maybeTransaction dtz rg+ case ex of+ Ex.Exception s -> fail s+ Ex.Success b -> return b++render ::+ DT.DefaultTimeZone+ -> (Qt.GroupingSpec, Qt.GroupingSpec)+ -> Qt.RadGroup+ -> T.Transaction+ -> Maybe X.Text+render dtz gs rg txn = do+ let txnFam = T.unTransaction txn+ tlX <- TL.render dtz (F.parent txnFam)+ pstgsX <- Tr.traverse (Po.render gs rg) (orphans txnFam)+ return $ tlX `X.append` (X.concat (toList pstgsX))+ ++
+ Penny/Copper/Util.hs view
@@ -0,0 +1,150 @@+module Penny.Copper.Util where++import Control.Applicative ((<*), pure, (<$))+import qualified Data.Char as C+import qualified Data.Foldable as F+import qualified Data.List.NonEmpty as NE+import qualified Data.Text as X+import qualified Penny.Lincoln.HasText as HT+import qualified Penny.Lincoln.TextNonEmpty as TNE+import Text.Parsec (char, many, skipMany)+import Text.Parsec.Text (Parser)++rangeLettersToSymbols :: Char -> Bool+rangeLettersToSymbols c = case C.generalCategory c of+ C.UppercaseLetter -> True+ C.LowercaseLetter -> True+ C.TitlecaseLetter -> True+ C.ModifierLetter -> True+ C.OtherLetter -> True+ C.DecimalNumber -> True+ C.LetterNumber -> True+ C.OtherNumber -> True+ C.ConnectorPunctuation -> True+ C.DashPunctuation -> True+ C.OpenPunctuation -> True+ C.ClosePunctuation -> True+ C.InitialQuote -> True+ C.FinalQuote -> True+ C.OtherPunctuation -> True+ C.MathSymbol -> True+ C.CurrencySymbol -> True+ C.ModifierSymbol -> True+ C.OtherSymbol -> True+ _ -> False++rangeLetters :: Char -> Bool+rangeLetters c = case C.generalCategory c of+ C.UppercaseLetter -> True+ C.LowercaseLetter -> True+ C.TitlecaseLetter -> True+ C.ModifierLetter -> True+ C.OtherLetter -> True+ _ -> False++rangeMathCurrency :: Char -> Bool+rangeMathCurrency c = case C.generalCategory c of+ C.MathSymbol -> True+ C.CurrencySymbol -> True+ _ -> False++rangeSymbols :: Char -> Bool+rangeSymbols c = case C.generalCategory c of+ C.MathSymbol -> True+ C.CurrencySymbol -> True+ C.ModifierSymbol -> True+ C.OtherSymbol -> True+ _ -> False++rangeLettersNumbers :: Char -> Bool+rangeLettersNumbers c = case C.generalCategory c of+ C.UppercaseLetter -> True+ C.LowercaseLetter -> True+ C.TitlecaseLetter -> True+ C.ModifierLetter -> True+ C.OtherLetter -> True+ C.DecimalNumber -> True+ C.LetterNumber -> True+ C.OtherNumber -> True+ _ -> False++-- | Creates a new parser that behaves like the old one, but also+-- parses any whitespace remaining afterward.+lexeme :: Parser a -> Parser a+lexeme p = p <* skipMany (char ' ')++-- | Parses any trailing whitespace followed by a newline followed by+-- additional whitespace.+eol :: Parser ()+eol = pure ()+ <* skipMany (char ' ')+ <* char '\n'+ <* skipMany (char ' ')++-- | Parses a run of spaces.+spaces :: Parser ()+spaces = () <$ many (char ' ')++-- | Applied to a non-empty list of pairs, with the first element of+-- the pair being a predicate that returns True if a character is OK+-- and the second element being something of an arbitrary type, and to+-- something that has a Text. The pairs must be ordered from most+-- restrictive to least restrictive predicates. If at least one of the+-- predicates indicates that the Text is valid, returns the leftmost b+-- associated with that predicate. If none of the predicates indicates+-- that the Text is valid, returns the rightmost error.+--+-- Here, most restrictive means the predicate that indicates True for+-- the narrowest range of characters, while least restrictive means+-- the predicate that indicates True for the widest range of+-- characters.+checkText ::+ HT.HasText a+ => NE.NonEmpty ((Char -> Bool), b)+ -> a+ -> Maybe b+checkText ps a = let+ t = HT.text a+ results = fmap (g . f) ps where+ f (p, b) = (X.find (not . p) t, b)+ g (p, b) = case p of+ Nothing -> Right b+ Just c -> Left c+ folder x y = case x of+ Right b -> Right b+ Left _ -> y+ in case F.foldr1 folder results of+ Left _ -> Nothing+ Right b -> return b++listIsOK ::+ HT.HasTextNonEmptyList a+ => (Char -> Bool) -- ^ Returns True for characters that are allowed+ -> a+ -> Bool+listIsOK p = F.all (TNE.all p) . HT.textNonEmptyList++firstCharOfListIsOK ::+ HT.HasTextNonEmptyList a+ => (Char -> Bool) -- ^ Returns True if the first character is allowed+ -> a+ -> Bool+firstCharOfListIsOK p ls = let+ firstText = NE.head . HT.textNonEmptyList $ ls+ in p (TNE.first firstText)++-- | Takes a field that may or may not be present and a function that+-- renders it. If the field is not present at all, returns an empty+-- Text. Otherwise will succeed or fail depending upon whether the+-- rendering function succeeds or fails.+renMaybe :: Maybe a -> (a -> Maybe X.Text) -> Maybe X.Text+renMaybe mx f = case mx of+ Nothing -> Just X.empty+ Just a -> f a++-- | Merges a list of words into one Text; however, if any given Text+-- is empty, that Text is first dropped from the list.+txtWords :: [X.Text] -> X.Text+txtWords xs = case filter (not . X.null) xs of+ [] -> X.empty+ rs -> X.unwords rs
+ Penny/Liberty.hs view
@@ -0,0 +1,692 @@+-- | Liberty - Penny command line parsing utilities+--+-- Both Cabin and Zinc share various functions that aid in parsing+-- command lines. For instance both the Postings report and the Zinc+-- postings filter use common command-line options. However, Zinc+-- already depends on Cabin. To avoid a cyclic dependency whereby+-- Cabin would also depend on Zinc, functions formerly in Zinc that+-- Cabin will also find useful are relocated here, to Liberty.++module Penny.Liberty (+ Error(..),+ MatcherFactory,+ X.evaluate,+ X.Token,+ FilteredNum(FilteredNum, unFilteredNum),+ SortedNum(SortedNum, unSortedNum),+ LibertyMeta(filteredNum, sortedNum),+ xactionsToFiltered,+ ListLength(ListLength, unListLength),+ ItemIndex(ItemIndex, unItemIndex),+ PostFilterFn,+ parseComparer,+ processPostFilters,+ parseTokenList,+ parsePredicate,+ + -- * Parsers+ Operand,+ parseOperand,+ parsePostFilter,+ parseMatcherSelect,+ parseCaseSelect,+ parseOperator,+ Orderer,+ parseSort+ ) where++import Control.Applicative ((<|>))+import qualified Control.Monad.Exception.Synchronous as Ex+import Control.Monad.Exception.Synchronous (+ Exceptional(Exception))+import Data.Char (toUpper)+import Data.List (isPrefixOf, sortBy)+import Data.Text (Text, pack)+import qualified Data.Text as Text+import qualified System.Console.MultiArg.Combinator as C+import System.Console.MultiArg.Prim (Parser)+import Text.Parsec (parse)+ +import Penny.Copper.DateTime (DefaultTimeZone, dateTime)+import Penny.Copper.Qty (RadGroup, qty)++import Penny.Lincoln.Family.Child (child, parent)+import qualified Penny.Lincoln.Predicates as P+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Liberty.Expressions as X++import Text.Matchers.CaseSensitive (+ CaseSensitive(Sensitive, Insensitive))+import qualified Text.Matchers.Text as TM++-- | A serial indicating how a post relates to all other postings that+-- made it through the filtering phase.+newtype FilteredNum = FilteredNum { unFilteredNum :: L.Serial }+ deriving Show++-- | A serial indicating how a posting relates to all other postings+-- that have been sorted.+newtype SortedNum = SortedNum { unSortedNum :: L.Serial }+ deriving Show++-- | All metadata from Liberty.+data LibertyMeta =+ LibertyMeta { filteredNum :: FilteredNum+ , sortedNum :: SortedNum }+ deriving Show+++-- | Parses a list of tokens. Returns Nothing if the token sequence is+-- invalid (e.g. if two operators are next to each other) or if the+-- resulting RPN expression is bad (there are tokens left on the stack+-- after parsing). Otherwise, returns the expression.+--+-- An empty list will fail to parse. The function calling this one+-- must deal with empty lists if those are a possibility.+parseTokenList :: [X.Token a] -> Maybe a+parseTokenList = X.evaluate++-- | Parses a list of tokens to obtain a predicate. Deals with an+-- empty list of tokens by returning a predicate that is always+-- True. Fails if the list of tokens is not empty and the parse fails.+parsePredicate :: [X.Token (a -> Bool)] -> Maybe (a -> Bool)+parsePredicate ls = case ls of+ [] -> return (const True)+ ts -> parseTokenList ts++-- | Takes a list of transactions, splits them into PostingChild+-- instances, filters them, post-filters them, sorts them, and places+-- them in Box instances with Filtered serials.+xactionsToFiltered ::+ + (L.PostFam -> Bool)+ -- ^ The predicate to filter the transactions++ -> [PostFilterFn]+ -- ^ Post filter specs++ -> (L.PostFam -> L.PostFam -> Ordering)+ -- ^ The sorter++ -> [L.Transaction]+ -- ^ The transactions to work on (probably parsed in from Copper)+ + -> [L.Box LibertyMeta]+ -- ^ Sorted, filtered postings++xactionsToFiltered pdct pfs s =+ addSortedNum+ . processPostFilters pfs+ . sortBy (sorter s)+ . addFilteredNum+ . map toBox+ . filter pdct+ . concatMap L.postFam+++-- | Transforms a PostingChild into a Box.+toBox :: L.PostFam -> L.Box ()+toBox = L.Box ()++-- | Takes a list of filtered boxes and adds the Filtered serials.++addFilteredNum :: [L.Box a] -> [L.Box FilteredNum]+addFilteredNum = L.serialItems f where+ f ser = fmap (const (FilteredNum ser))++-- | Wraps a PostingChild sorter to change it to a Box sorter.+sorter :: (L.PostFam -> L.PostFam -> Ordering)+ -> L.Box a+ -> L.Box b+ -> Ordering+sorter f b1 b2 = f (L.boxPostFam b1) (L.boxPostFam b2)++-- | Takes a list of Boxes with metadata and adds a Serial for the+-- Sorted.+addSortedNum ::+ [L.Box FilteredNum]+ -> [L.Box LibertyMeta]+addSortedNum = L.serialItems f where+ f ser = fmap g where+ g filtNum = LibertyMeta filtNum (SortedNum ser)++type MatcherFactory =+ CaseSensitive+ -> Text+ -> Ex.Exceptional Text (Text -> Bool)+type Operand = X.Operand (L.PostFam -> Bool)++newtype ListLength = ListLength { unListLength :: Int }+ deriving (Eq, Ord, Show)+newtype ItemIndex = ItemIndex { unItemIndex :: Int }+ deriving (Eq, Ord, Show)++-- | Specifies options for the post-filter stage.+type PostFilterFn = ListLength -> ItemIndex -> Bool+++processPostFilters :: [PostFilterFn] -> [a] -> [a]+processPostFilters pfs ls = foldl processPostFilter ls pfs+++processPostFilter :: [a] -> PostFilterFn -> [a]+processPostFilter as fn = map fst . filter fn' $ zipped where+ len = ListLength $ length as+ fn' (_, idx) = fn len (ItemIndex idx)+ zipped = zip as [0..]+ ++data Error = MakeMatcherFactoryError Text+ | DateParseError+ | BadPatternError Text+ | BadNumberError Text+ | BadQtyError Text+ | BadSortKeyError Text+ | BadComparator Text+ | BadExpression+ | BadColorName Text+ | BadFieldName Text+ | BadBackgroundArg Text+ | UnexpectedWord Text Text+ | BadCommodityError Text+ deriving Show+++------------------------------------------------------------+-- Operands+------------------------------------------------------------++-- | Given a String from the command line which represents a pattern,+-- and a MatcherFactor, return a Matcher. Fails if the pattern is bad+-- (e.g. it is not a valid regular expression).+getMatcher ::+ String+ -> CaseSensitive+ -> MatcherFactory+ -> Ex.Exceptional Error (Text -> Bool)++getMatcher s cs f = case f cs (pack s) of+ Ex.Exception e -> Ex.Exception $ BadPatternError e+ Ex.Success m -> return m++-- | Parses comparers given on command line to a function.+parseComparer ::+ (Eq a, Ord a)+ => String+ -> Maybe (a -> a -> Bool)+parseComparer t+ | t == "<" = Just (<)+ | t == "<=" = Just (<=)+ | t == "==" = Just (==)+ | t == "=" = Just (==)+ | t == ">" = Just (>)+ | t == ">=" = Just (>=)+ | t == "/=" = Just (/=)+ | t == "!=" = Just (/=)+ | otherwise = Nothing++parseDate :: DefaultTimeZone -> Text -> Exceptional Error L.DateTime+parseDate dtz t = case parse (dateTime dtz) "" t of+ Left _ -> Exception DateParseError+ Right d -> return d++date :: Parser (DefaultTimeZone -> Ex.Exceptional Error Operand)+date =+ let os = C.OptSpec ["date"] ['d'] (C.TwoArg f)+ f a1 a2 dtz = do+ cmp <- Ex.fromMaybe (BadComparator (pack a1))+ (parseComparer a1)+ dt <- parseDate dtz (pack a2)+ return $ X.Operand (P.date (`cmp` dt))+ in C.parseOption [os]++current :: Parser (L.DateTime -> Operand)+current =+ let os = C.OptSpec ["current"] [] (C.NoArg f)+ f dt = X.Operand (P.date (<= dt))+ in C.parseOption [os]++-- | Creates options that match against fields that are+-- colon-separated. The operand added will return True if the+-- posting's colon-separated option matches the pattern given, or+-- False if it does not.+sepOption ::+ String+ -- ^ Long option name+ + -> Maybe Char+ -- ^ Short option name, if there is one++ -> (Text -> (Text -> Bool) -> L.PostFam -> Bool)+ -- ^ When applied to a text that separates the different segments of+ -- the field, a matcher, and a posting, this function returns True+ -- if the posting matches the matcher or False if it does not.+ + -> Parser (CaseSensitive+ -> MatcherFactory+ -> Ex.Exceptional Error Operand)+sepOption str mc f =+ let so = case mc of+ Nothing -> []+ Just c -> [c]+ os = C.OptSpec [str] so (C.OneArg g)+ g a cs fty = do+ m <- getMatcher a cs fty+ return $ X.Operand (f sep m)+ in C.parseOption [os]++sep :: Text+sep = Text.singleton ':'++-- | Parses exactly one integer; fails if it cannot read exactly one.+parseInt :: String -> Exceptional Error Int+parseInt t = let ps = reads t in+ case ps of+ [] -> Exception $ BadNumberError (pack t)+ ((i, s):[]) -> if length s /= 0+ then Exception $ BadNumberError (pack t)+ else return i+ _ -> Exception $ BadNumberError (pack t)++-- | Creates options that take two arguments, with the first argument+-- being the level to match, and the second being the pattern that+-- should match against that level. Adds an operand that returns True+-- if the pattern matches.+levelOption ::+ String+ -- ^ Long option name+ + -> (Int -> (Text -> Bool) -> L.PostFam -> Bool)+ -- ^ Applied to an integer, a matcher, and a PostingBox, this+ -- function returns True if a particular field in the posting+ -- matches the matcher given at the given level, or False otherwise.+ + -> Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+ +levelOption str f =+ let os = C.OptSpec [str] [] (C.TwoArg g)+ g a1 a2 cs fty = do+ n <- parseInt a1+ m <- getMatcher a2 cs fty+ return $ X.Operand (f n m)+ in C.parseOption [os]++-- | Creates options that add an operand that matches the posting if a+-- particluar field matches the pattern given.+patternOption ::+ String+ -- ^ Long option+ + -> Maybe Char+ -- ^ Short option, if included++ -> ((Text -> Bool) -> L.PostFam -> Bool)+ -- ^ When applied to a matcher and a PostingBox, this function+ -- returns True if the posting matches, or False if it does not.+ + -> Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+patternOption str mc f =+ let so = maybe [] (\c -> [c]) mc+ os = C.OptSpec [str] so (C.OneArg g)+ g a1 cs fty = do+ m <- getMatcher a1 cs fty+ return $ X.Operand (f m)+ in C.parseOption [os]++-- | The account option; matches if the pattern given matches the+-- colon-separated account name.+account :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+account = sepOption "account" (Just 'a') P.account++-- | The account-level option; matches if the account at the given+-- level matches.+accountLevel :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+accountLevel = levelOption "account-level" P.accountLevel++-- | The accountAny option; returns True if the matcher given matches+-- a single sub-account name at any level.+accountAny :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+accountAny = patternOption "account-any" Nothing P.accountAny++-- | The payee option; returns True if the matcher matches the payee+-- name.+payee :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+payee = patternOption "payee" (Just 'p') P.payee++tag :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+tag = patternOption "tag" (Just 't') P.tag++number :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+number = patternOption "number" Nothing P.number++flag :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+flag = patternOption "flag" Nothing P.flag++commodity :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+commodity = sepOption "commodity" Nothing P.commodity++commodityLevel :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+commodityLevel = levelOption "commodity-level" P.commodityLevel++commodityAny :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+commodityAny = patternOption "commodity" Nothing P.commodityAny++postingMemo :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+postingMemo = patternOption "posting-memo" Nothing P.postingMemo++transactionMemo :: Parser (CaseSensitive+ -> MatcherFactory -> Exceptional Error Operand)+transactionMemo = patternOption "transaction-memo"+ Nothing P.transactionMemo++debit :: Parser Operand+debit =+ let os = C.OptSpec ["debit"] [] (C.NoArg (X.Operand P.debit))+ in C.parseOption [os]++credit :: Parser Operand+credit =+ let os = C.OptSpec ["credit"] [] (C.NoArg (X.Operand P.credit))+ in C.parseOption [os]++qtyOption :: Parser (RadGroup -> Exceptional Error Operand)+qtyOption =+ let os = C.OptSpec ["qty"] [] (C.TwoArg f)+ f a1 a2 rg = do+ q <- case parse (qty rg) "" (pack a2) of+ Left _ -> Ex.throw $ BadQtyError (pack a2)+ Right g -> return g+ cmp <- Ex.fromMaybe (BadComparator (pack a1))+ (parseComparer a1)+ return $ X.Operand (P.qty (`cmp` q))+ in C.parseOption [os]++-- | Creates two options suitable for comparison of serial numbers,+-- one for ascending, one for descending.+serialOption ::++ (L.PostFam -> Maybe L.Serial)+ -- ^ Function that, when applied to a PostingChild, returns the serial+ -- you are interested in.++ -> String+ -- ^ Name of the command line option, such as @global-transaction@++ -> Parser (Exceptional Error Operand)+ -- ^ Parses both descending and ascending serial options.++serialOption getSerial n =+ let osA = C.OptSpec [n] []+ (C.TwoArg (f L.forward))+ osD = C.OptSpec [addPrefix "rev" n] []+ (C.TwoArg (f L.backward))+ f getInt a1 a2 = do+ cmp <- Ex.fromMaybe (BadComparator (pack a1))+ (parseComparer a1)+ i <- parseInt a2+ let op pf = case getSerial pf of+ Nothing -> False+ Just ser -> getInt ser `cmp` i+ return $ X.Operand op+ in C.parseOption [osA, osD]++-- | Takes a string, adds a prefix and capitalizes the first letter of+-- the old string. e.g. applied to "rev" and "globalTransaction",+-- returns "revGlobalTransaction".+addPrefix :: String -> String -> String+addPrefix pre suf = pre ++ suf' where+ suf' = case suf of+ "" -> ""+ x:xs -> toUpper x : xs++globalTransaction :: Parser (Exceptional Error Operand)+globalTransaction =+ let f = fmap L.unGlobalTransaction+ . L.globalTransaction+ . L.tMeta+ . parent+ . L.unPostFam+ in serialOption f "globalTransaction"++globalPosting :: Parser (Exceptional Error Operand)+globalPosting =+ let f = fmap L.unGlobalPosting+ . L.globalPosting+ . L.pMeta+ . child+ . L.unPostFam+ in serialOption f "globalPosting"++filePosting :: Parser (Exceptional Error Operand)+filePosting =+ let f = fmap L.unFilePosting+ . L.filePosting+ . L.pMeta+ . child+ . L.unPostFam+ in serialOption f "filePosting"++fileTransaction :: Parser (Exceptional Error Operand)+fileTransaction =+ let f = fmap L.unFileTransaction+ . L.fileTransaction+ . L.tMeta+ . parent+ . L.unPostFam+ in serialOption f "fileTransaction"++-- | Parses operands.+parseOperand ::+ Parser (L.DateTime+ -> DefaultTimeZone+ -> RadGroup+ -> CaseSensitive+ -> MatcherFactory+ -> Ex.Exceptional Error Operand)++parseOperand =+ (do { f <- date; return (\_ dtz _ _ _ -> f dtz)})+ <|> (do { f <- current; return (\dt _ _ _ _ -> return (f dt)) })+ <|> wrapFactArg account+ <|> wrapFactArg accountLevel+ <|> wrapFactArg accountAny+ <|> wrapFactArg payee+ <|> wrapFactArg tag+ <|> wrapFactArg number+ <|> wrapFactArg flag+ <|> wrapFactArg commodity+ <|> wrapFactArg commodityLevel+ <|> wrapFactArg commodityAny+ <|> wrapFactArg postingMemo+ <|> wrapFactArg transactionMemo+ <|> (do { o <- debit; return (\_ _ _ _ _ -> return o) })+ <|> (do { o <- credit; return (\_ _ _ _ _ -> return o) })+ <|> (do { f <- qtyOption; return (\ _ _ rg _ _ -> f rg)})+ <|> wrapNoArg globalTransaction+ <|> wrapNoArg globalPosting+ <|> wrapNoArg filePosting+ <|> wrapNoArg fileTransaction+++wrapNoArg ::+ Parser (Ex.Exceptional Error Operand)+ -> Parser (L.DateTime+ -> DefaultTimeZone+ -> RadGroup+ -> CaseSensitive+ -> MatcherFactory+ -> Ex.Exceptional Error Operand)+wrapNoArg p = do+ o <- p+ return (\_ _ _ _ _ -> o)++wrapFactArg ::+ Parser (CaseSensitive -> MatcherFactory -> Exceptional Error Operand)+ -> Parser (L.DateTime+ -> DefaultTimeZone+ -> RadGroup+ -> CaseSensitive+ -> MatcherFactory+ -> Ex.Exceptional Error Operand)+wrapFactArg p = do+ f <- p+ return (\_ _ _ cs fact -> f cs fact)++------------------------------------------------------------+-- Post filters+------------------------------------------------------------++optHead :: Parser (Exceptional Error PostFilterFn)+optHead =+ let os = C.OptSpec ["head"] [] (C.OneArg f)+ f a = do+ i <- fmap ItemIndex (parseInt a)+ return (\_ idx -> idx < i)+ in C.parseOption [os]++optTail :: Parser (Exceptional Error PostFilterFn)+optTail =+ let os = C.OptSpec ["tail"] [] (C.OneArg f)+ f a = do+ i <- parseInt a+ let g (ListLength len) (ItemIndex idx) = idx >= len - i+ return g+ in C.parseOption [os]++parsePostFilter :: Parser (Exceptional Error PostFilterFn)+parsePostFilter = optHead <|> optTail++------------------------------------------------------------+-- Matcher control+------------------------------------------------------------++noArg :: a -> String -> Parser a+noArg a s = let os = C.OptSpec [s] "" (C.NoArg a)+ in C.parseOption [os]++parseInsensitive :: Parser CaseSensitive+parseInsensitive =+ let os = C.OptSpec ["case-insensitive"] ['i'] (C.NoArg Insensitive)+ in C.parseOption [os]++parseSensitive :: Parser CaseSensitive+parseSensitive =+ let os = C.OptSpec ["case-sensitive"] ['I'] (C.NoArg Sensitive)+ in C.parseOption [os]+++within :: Parser MatcherFactory+within = noArg (\c t -> return (TM.within c t)) "within"++pcre :: Parser MatcherFactory+pcre = noArg TM.pcre "pcre"++posix :: Parser MatcherFactory+posix = noArg TM.tdfa "posix"++exact :: Parser MatcherFactory+exact = noArg (\c t -> return (TM.exact c t)) "exact"++parseMatcherSelect :: Parser MatcherFactory+parseMatcherSelect = within <|> pcre <|> posix <|> exact++parseCaseSelect :: Parser CaseSensitive+parseCaseSelect = parseInsensitive <|> parseSensitive++------------------------------------------------------------+-- Operators+------------------------------------------------------------++-- | Open parentheses+open :: Parser (X.Token a)+open = noArg X.TokOpenParen "open"++-- | Close parentheses+close :: Parser (X.Token a)+close = noArg X.TokCloseParen "close"++-- | and operator+parseAnd :: Parser (X.Token (a -> Bool))+parseAnd = noArg X.tokAnd "and"++-- | or operator+parseOr :: Parser (X.Token (a -> Bool))+parseOr = noArg X.tokOr "or"++-- | not operator+parseNot :: Parser (X.Token (a -> Bool))+parseNot = noArg X.tokNot "not" where++parseOperator :: Parser (X.Token (a -> Bool))+parseOperator =+ open <|> close <|> parseAnd <|> parseOr <|> parseNot++------------------------------------------------------------+-- Sorting+------------------------------------------------------------+type Orderer = L.PostFam -> L.PostFam -> Ordering++ordering ::+ (Ord b)+ => (a -> b)+ -> (a -> a -> Ordering)+ordering q = f where+ f p1 p2 = compare (q p1) (q p2)+++flipOrder :: (a -> a -> Ordering) -> (a -> a -> Ordering)+flipOrder f = f' where+ f' p1 p2 = case f p1 p2 of+ LT -> GT+ GT -> LT+ EQ -> EQ++capitalizeFirstLetter :: String -> String+capitalizeFirstLetter s = case s of+ [] -> []+ (x:xs) -> toUpper x : xs++ordPairs :: [(String, Orderer)]+ordPairs = + [ ("payee", ordering Q.payee)+ , ("date", ordering Q.dateTime)+ , ("flag", ordering Q.flag)+ , ("number", ordering Q.number)+ , ("account", ordering Q.account)+ , ("drCr", ordering Q.drCr)+ , ("qty", ordering Q.qty)+ , ("commodity", ordering Q.commodity)+ , ("postingMemo", ordering Q.postingMemo)+ , ("transactionMemo", ordering Q.transactionMemo) ]++ords :: [(String, Orderer)]+ords = ordPairs ++ uppers where+ uppers = map toReversed ordPairs+ toReversed (s, f) =+ (capitalizeFirstLetter s, flipOrder f)+++parseSort :: Parser (Exceptional Error Orderer)+parseSort =+ let os = C.OptSpec ["sort"] ['s'] (C.OneArg f)+ f a =+ let matches = filter (\p -> a `isPrefixOf` (fst p)) ords+ in case matches of+ [] -> Ex.throw $ BadSortKeyError (pack a)+ x:[] -> return $ snd x+ _ -> Ex.throw $ BadSortKeyError (pack a)+ in C.parseOption [os]
+ Penny/Liberty/Expressions.hs view
@@ -0,0 +1,63 @@+module Penny.Liberty.Expressions (+ I.Precedence(Precedence),+ + I.Associativity(ALeft, ARight),+ + I.Token(TokOperand,+ TokUnaryPostfix,+ TokUnaryPrefix,+ TokBinary,+ TokOpenParen,+ TokCloseParen),++ R.Operand(Operand),+ tokAnd,+ tokOr,+ tokNot,+ evaluate) where++import qualified Data.Foldable as Fdbl+import Penny.Liberty.Expressions.Infix as I+import Penny.Liberty.Expressions.RPN as R+++-- | Tokens should be enqueued from left to right, so that tokens on+-- the left side of the sequence are those at the beginning of the+-- expression.+evaluate ::+ Fdbl.Foldable l+ => l (I.Token a)+ -> Maybe a+evaluate i = I.infixToRPN i >>= R.process++-- | An And token which is left associative with precedence 3.+tokAnd :: I.Token (a -> Bool)+tokAnd = I.TokBinary (I.Precedence 3) I.ALeft f where+ f x y = \a -> x a && y a++-- | An Or token which is left associative with precedence 2.+tokOr :: I.Token (a -> Bool)+tokOr = I.TokBinary (I.Precedence 2) I.ALeft f where+ f x y = \a -> x a || y a++-- | A unary prefix Not token with precedence 4.+tokNot :: I.Token (a -> Bool)+tokNot = I.TokUnaryPrefix (I.Precedence 4) (not .)++--+-- Testing+--+_plus :: I.Token Double+_plus = I.TokBinary (I.Precedence 4) I.ALeft (+)++_minus :: I.Token Double+_minus = I.TokBinary (I.Precedence 4) I.ALeft (-)++_times :: I.Token Double+_times = I.TokBinary (I.Precedence 5) I.ALeft (*)++_divide :: I.Token Double+_divide = I.TokBinary (I.Precedence 5) I.ALeft (/)++_neg :: I.Token Double+_neg = I.TokUnaryPrefix (I.Precedence 5) negate
+ Penny/Liberty/Expressions/Infix.hs view
@@ -0,0 +1,218 @@+-- | Creates RPN expressions from infix inputs.+--+-- Penny accepts only infix expressions, but RPN expressions are+-- easier to process. This module converts infix expressions to RPN+-- expressions for further processing.+--+-- Uses the shunting-yard algorithm, best described at+-- http://www.chris-j.co.uk/parsing.php (be sure to use the \"click to+-- display\" links).+module Penny.Liberty.Expressions.Infix (++ Precedence(Precedence),+ + Associativity(ALeft,+ ARight),+ + Token(TokOperand,+ TokUnaryPostfix,+ TokUnaryPrefix,+ TokBinary,+ TokOpenParen,+ TokCloseParen),+ + infixToRPN+ ) where++import qualified Data.Foldable as Fdbl+import qualified Penny.Liberty.Expressions.RPN as R+import qualified Data.Sequence as Seq+import Data.Sequence((|>), Seq)++-- | Precedence can be any integer; the greater the number, the higher+-- the precedence.+newtype Precedence = Precedence Int deriving (Show, Eq, Ord)++data Associativity = ALeft | ARight deriving Show++data Token a =+ TokOperand a+ | TokUnaryPostfix (a -> a)+ | TokUnaryPrefix Precedence (a -> a)+ | TokBinary Precedence Associativity (a -> a -> a)+ | TokOpenParen+ | TokCloseParen++instance (Show a) => Show (Token a) where+ show (TokOperand a) = "<operand " ++ show a ++ ">"+ show (TokUnaryPostfix _) = "<unary postfix>"+ show (TokUnaryPrefix (Precedence i) _) =+ "<unary prefix, precedence " ++ (show i) ++ ">"+ show (TokBinary (Precedence i) a _) =+ "<binary, precedence " ++ show i ++ " "+ ++ show a ++ ">"+ show TokOpenParen = "<OpenParen>"+ show TokCloseParen = "<CloseParen>"++data StackVal a =+ StkUnaryPrefix Precedence (a -> a)+ | StkBinary Precedence (a -> a -> a)+ | StkOpenParen++instance Show (StackVal a) where+ show (StkUnaryPrefix p _) =+ "<unary prefix, " ++ show p ++ ">"+ show (StkBinary p _) =+ "<binary, " ++ show p ++ ">"+ show StkOpenParen = "<OpenParen>"++-- | Converts an infix expression to an RPN expression.+infixToRPN ::+ Fdbl.Foldable l+ => l (Token a)+ -- ^ Input tokens. These should be in the sequence from left to+ -- right in ordinary infix order. The easiest choice is a list,+ -- though you might want to use Data.Sequence if many appends will+ -- be needed to build the sequence.++ -> Maybe (Seq (R.Token a))+ -- ^ The resulting RPN expression. The token type here is a token+ -- from Penny.Liberty.Expressions.RPN, which is a different type+ -- than the Token in this module. Fails only if there is a close+ -- parenthesis without a matching open parenthesis, or if there is+ -- an open parenthesis without a matching close parenthesis. Other+ -- nonsensical expressions will still parse to an RPN expression+ -- successfully, so the RPN parser has to catch these errors.++infixToRPN ls =+ Fdbl.foldlM processToken ([], Seq.empty) ls+ >>= popRemainingOperators+++-- | Process a single input token. Fails if the token is a close+-- parenthesis and a matching open parenthesis is not+-- found. Otherwise, succeeds and adjusts the stack and the output+-- queue accordingly.+processToken ::+ ([StackVal a], Seq (R.Token a))+ -> Token a+ -> Maybe ([StackVal a], Seq (R.Token a))+processToken (ss, ts) tok =+ case tok of+ TokOperand a -> Just (ss, processOperand a ts)+ TokUnaryPostfix f ->+ Just (ss, processUnaryPostfix f ts)+ TokUnaryPrefix p f ->+ Just (processUnaryPrefix p f ss, ts)+ TokBinary p a f ->+ Just (processBinary p a f (ss, ts))+ TokOpenParen -> Just (processOpenParen ss, ts)+ TokCloseParen -> processCloseParen (ss, ts)++-- | If a token is an operand, append it to the postfix output.+processOperand :: a -> Seq (R.Token a) -> Seq (R.Token a)+processOperand a sq = sq |> (R.TokOperand (R.Operand a))++-- | If a token is a unary postfix operator, append it to the postfix+-- output.+processUnaryPostfix ::+ (a -> a)+ -> Seq (R.Token a)+ -> Seq (R.Token a)+processUnaryPostfix f sq =+ sq |> (R.TokOperator (R.Unary f))++-- | If a token is a unary prefix operator, push it onto the stack.+processUnaryPrefix ::+ Precedence+ -> (a -> a)+ -> [StackVal a]+ -> [StackVal a]+processUnaryPrefix p f s = (StkUnaryPrefix p f):s++-- | Pops tokens from the stack and appends them to the ouptut, as+-- long as the token at the top of the stack is and operator and its+-- precedence meets the given predicate.+popTokens ::+ (Precedence -> Bool)+ -> ([StackVal a], Seq (R.Token a))+ -> ([StackVal a], Seq (R.Token a))+popTokens f (ss, os) =+ case ss of+ [] -> (ss, os)+ x:xs -> case x of+ StkOpenParen -> (ss, os)+ StkUnaryPrefix p g -> popper (R.Unary g) p+ StkBinary p g -> popper (R.Binary g) p+ where+ popper tok pr =+ if f pr+ then+ let output' = os |> (R.TokOperator tok)+ in popTokens f (xs, output')+ else (ss, os)+ ++-- | If the token is a binary operator A, then:+--+-- If A is left associative, while there is an operator B of higher or+-- equal precedence than A at the top of the stack, pop B off the+-- stack and append it to the output.+--+-- If A is right associative, while there is an operator B of higher+-- precedence than A at the top of the stack, pop B off the stack and+-- append it to the output.+--+-- Push A onto the stack.+processBinary ::+ Precedence+ -> Associativity+ -> (a -> a -> a)+ -> ([StackVal a], Seq (R.Token a))+ -> ([StackVal a], Seq (R.Token a))+processBinary p a f pair =+ let pdct = case a of+ ALeft -> (>= p)+ ARight -> (> p)+ (ss, os) = popTokens pdct pair+ in ((StkBinary p f):ss, os)++-- | If the token is an opening parenthesis, push it onto the stack.+processOpenParen :: [StackVal a] -> [StackVal a]+processOpenParen = (StkOpenParen :)++-- | If the token is a closing parenthesis, pop operators off the top+-- of the stack and append them to the output until the operator at+-- the top of the stack is an opening bracket. Pop the opening bracket+-- off the stack.+--+-- Fails if no open paren is found.+processCloseParen ::+ ([StackVal a], Seq (R.Token a))+ -> Maybe ([StackVal a], Seq (R.Token a))+processCloseParen (ss, os) = case ss of+ [] -> Nothing+ (x:xs) ->+ let popper op = processCloseParen (xs, output')+ where+ output' = os |> (R.TokOperator op)+ in case x of+ StkUnaryPrefix _ f -> popper (R.Unary f) + StkBinary _ f -> popper (R.Binary f)+ StkOpenParen -> Just (xs, os)++-- | Removes all remaining operators from the stack and puts them on+-- the output queue. Fails if the stack has an open parenthesis; as+-- this is unmatched.+popRemainingOperators ::+ ([StackVal a], Seq (R.Token a))+ -> Maybe (Seq (R.Token a))+popRemainingOperators (s, os) = case s of+ [] -> Just os+ x:xs -> case x of+ StkOpenParen -> Nothing+ StkUnaryPrefix _ f -> pusher (R.Unary f)+ StkBinary _ f -> pusher (R.Binary f)+ where+ pusher op = popRemainingOperators (xs, output') where+ output' = os |> (R.TokOperator op)
+ Penny/Liberty/Expressions/RPN.hs view
@@ -0,0 +1,112 @@+-- | Parses reverse polish notation expressions. This module needs+-- much better error messages (right now it has none).+--+-- An RPN expression consists of operands and operators; a token is+-- either an operand or an operator. For example, in the expression @5+-- 4 +@, the @5@ and the @4@ are operands; the @+@ is an operator;+-- each of these three is a token.+module Penny.Liberty.Expressions.RPN (+ Operand(Operand),+ Operator(Unary, Binary),+ Token(TokOperand, TokOperator),+ process) where++import qualified Data.Foldable as Fdbl++-- | An operand; for example, in the expression @5 4 +@, @5@ and @4@+-- are operands.+newtype Operand a = Operand a deriving Show++-- | Operators; for example, in the expression @5 4 +@, @+@ is an+-- operator. Because this is RPN, there is no operator precedence.+data Operator a =+ Unary (a -> a)+ -- ^ Unary operators take only one operand (for example, a factorial+ -- operator).++ | Binary (a -> a -> a)+ -- ^ Binary operators take two operands (for example, an addition+ -- operator).++instance Show (Operator a) where+ show (Unary _) = "<unary operator>"+ show (Binary _) = "<binary operator>"++-- | A token is either an operator or an operand.+data Token a =+ TokOperand (Operand a)+ | TokOperator (Operator a)+ deriving Show+++-- | Given an operator, and the stack of operands, process the+-- operator. When parsing an RPN expression, encountering an operator+-- at the front of the queue of tokens to be processed means that the+-- correct number of tokens (one, for a unary operator, or two, for a+-- binary operator) must be popped off the stack of operands. The+-- operator is then applied to the operands. For a binary operator,+-- the binary function is applied first to the operand that was lowest+-- on the stack, and then to the operand that was higher up on the+-- stack. The result of the operator is then pushed onto the top of+-- the stack.+--+-- This function fails if there were insufficient operands on the+-- stack to process the operator. If successful, returns the new+-- stack, with the processed operands removed and the result of the+-- operator pushed onto the top of the stack.+processOperator ::+ Operator a+ -> [Operand a]+ -> Maybe ([Operand a])+processOperator t ds = case t of+ (Unary f) -> case ds of+ [] -> Nothing+ (Operand x):xs -> return $ (Operand (f x)) : xs+ (Binary f) -> case ds of+ [] -> Nothing+ (Operand x):dss -> case dss of+ (Operand y):dsss ->+ return $ (Operand (f y x)) : dsss+ [] -> Nothing++-- | Adds an operand to the top of the stack.+processOperand ::+ Operand a+ -> [Operand a]+ -> [Operand a]+processOperand = (:)++-- | Processes the next token. Fails if the next token is an operator+-- and fails; otherwise, returns the new stack of operands.+processToken ::+ [Operand a]+ -> Token a+ -> Maybe ([Operand a])+processToken s tok = case tok of+ TokOperand d -> return (processOperand d s)+ TokOperator t -> processOperator t s++-- | Processes an entire input sequence of RPN tokens.+process ::+ Fdbl.Foldable l+ => l (Token a)+ -- ^ The tokens must be in the sequence from left to right in+ -- postfix order; for example, @5 4 -@ will yield @1@. Typically+ -- many appends will be required in order to build this sequence. If+ -- performance is a concern, you can use a Data.Sequence; if the+ -- list is small (as these lists will typically be) a regular list+ -- will do just fine.++ -> Maybe a+ -- ^ Fails if there is not exactly one operand remaining on the+ -- stack at the end of the parse, or if at any time there are+ -- insufficient operands on the stack to parse an+ -- operator. Otherwise, succeeds and returns the result.+process ls = do+ os <- Fdbl.foldlM processToken [] ls+ (top, rest) <- case os of+ (Operand x) : oss -> return (x, oss)+ _ -> Nothing+ case rest of+ [] -> return top+ _ -> Nothing
+ Penny/Lincoln.hs view
@@ -0,0 +1,210 @@+-- | Lincoln - the Penny core+--+-- Penny's core types and classes are here. This module re-exports the+-- most useful things. For more details you will want to look at the+-- sub-modules. Also, not all types and functions are re-exported due+-- to naming conflicts. In particular, neither+-- "Penny.Lincoln.Predicates" nor "Penny.Lincoln.Queries" is exported+-- from here due to the blizzard of name conflicts that would result.+module Penny.Lincoln (+ -- * Balances+ B.Balance+ , B.unBalance+ , B.Balanced(Balanced, Inferable, NotInferable)+ , B.isBalanced+ , B.entryToBalance+ , B.addBalances+ , B.removeZeroCommodities+ , B.BottomLine (Zero, NonZero)+ , B.Column (Column)+ + -- * Bits+ -- ** Accounts+ , I.SubAccountName (SubAccountName, unSubAccountName)+ , I.Account(Account, unAccount)+ + -- ** Amounts+ , I.Amount (Amount, qty, commodity)+ + -- ** Commodities+ , I.Commodity (Commodity, unCommodity)+ , I.SubCommodity (SubCommodity, unSubCommodity)+ , I.charCommodity+ + -- ** DateTime+ , I.DateTime+ , I.dateTime+ , I.localTime+ , I.timeZone+ , I.TimeZoneOffset+ , I.minsToOffset+ , I.offsetToMins+ , I.noOffset+ + -- ** Debits and credits+ , I.DrCr(Debit, Credit)+ , I.opposite+ + -- ** Entries+ , I.Entry (Entry, drCr, amount)+ + -- ** Flag+ , I.Flag (Flag, unFlag)+ + -- ** Memos+ , I.MemoLine (MemoLine, unMemoLine)+ , I.Memo (Memo, unMemo)+ + -- ** Number+ , I.Number (Number, unNumber)+ + -- ** Payee+ , I.Payee (Payee, unPayee)+ + -- ** Prices and price points+ , I.From(From, unFrom)+ , I.To(To, unTo)+ , I.CountPerUnit(CountPerUnit, unCountPerUnit)+ , I.Price(from, to, countPerUnit)+ , I.newPrice+ , I.PricePoint(PricePoint, price, ppMeta)++ -- ** Quantities + , I.Qty+ , I.unQty+ , I.partialNewQty+ , I.newQty+ , I.add+ , I.subt+ , I.mult+ , I.zero+ , I.difference+ , I.Difference(LeftBiggerBy, RightBiggerBy, Equal)+ + -- ** Tags+ , I.Tag(Tag, unTag)+ , I.Tags(Tags, unTags)+ + + -- * Builders+ , Bd.crashy+ , Bd.account+ + -- * Families+ -- ** Family types+ , F.Family(Family)+ , F.Child(Child)+ , F.Siblings(Siblings)+ + -- ** Manipulating families+ , F.children+ , F.orphans+ , F.adopt+ , F.marryWith+ , F.marry+ , F.divorceWith+ , F.divorce+ + -- * HasText+ , HT.HasText(text)+ , HT.Delimited(Delimited)+ , HT.HasTextList(textList)+ , HT.HasTextNonEmpty(textNonEmpty)+ , HT.HasTextNonEmptyList(textNonEmptyList)++ -- * TextNonEmpty+ , TNE.TextNonEmpty(TextNonEmpty)+ + -- * Transactions+ -- ** Postings and transactions+ , T.Posting+ , T.Transaction+ , T.PostFam++ -- ** Making transactions+ , T.transaction+ , T.Error ( UnbalancedError, CouldNotInferError)+ + -- ** Querying postings+ , T.Inferred(Inferred, NotInferred)+ , T.pPayee+ , T.pNumber+ , T.pFlag+ , T.pAccount+ , T.pTags+ , T.pEntry+ , T.pMemo+ , T.pInferred+ , T.pMeta+ , T.changePostingMeta++ -- ** Querying transactions+ , T.TopLine+ , T.tDateTime+ , T.tFlag+ , T.tNumber+ , T.tPayee+ , T.tMemo+ , T.tMeta+ , T.changeTransactionMeta+ , T.postFam+ + -- ** Adding serials to transactions+ , T.addSerialsToList+ , T.addSerialsToEithers+ + -- ** Unwrapping Transactions+ , T.unTransaction+ , T.unPostFam+ + -- ** Transaction boxes+ , T.Box (Box, boxMeta, boxPostFam)+ + -- * Metadata+ , M.TopLineLine(TopLineLine, unTopLineLine)+ , M.TopMemoLine(TopMemoLine, unTopMemoLine)+ , M.Side(CommodityOnLeft, CommodityOnRight)+ , M.SpaceBetween(SpaceBetween, NoSpaceBetween)+ , M.Format(Format, side, between)+ , M.Filename(Filename, unFilename)+ , M.PriceLine(PriceLine, unPriceLine)+ , M.PostingLine(PostingLine, unPostingLine)+ , M.PriceMeta(PriceMeta, priceLine, priceFormat)+ , M.GlobalPosting(GlobalPosting, unGlobalPosting)+ , M.FilePosting(FilePosting, unFilePosting)+ , M.GlobalTransaction(GlobalTransaction, unGlobalTransaction)+ , M.FileTransaction(FileTransaction, unFileTransaction)+ , M.PostingMeta(PostingMeta, postingLine, postingFormat,+ globalPosting, filePosting)+ , M.emptyPostingMeta+ , M.TopLineMeta(TopLineMeta, topMemoLine, topLineLine, filename,+ globalTransaction, fileTransaction)+ , M.emptyTopLineMeta+ + -- * PriceDb+ , DB.PriceDb+ , DB.emptyDb+ , DB.addPrice+ , DB.getPrice+ , DB.PriceDbError(FromNotFound, ToNotFound, CpuNotFound)+ , DB.convert+ + -- * Serials+ , S.Serial+ , S.forward+ , S.backward+ , S.serials+ , S.serialItems++ ) where++import qualified Penny.Lincoln.Balance as B+import qualified Penny.Lincoln.Bits as I+import qualified Penny.Lincoln.Builders as Bd+import qualified Penny.Lincoln.Family as F+import qualified Penny.Lincoln.HasText as HT+import qualified Penny.Lincoln.Meta as M+import qualified Penny.Lincoln.PriceDb as DB+import qualified Penny.Lincoln.Serial as S+import qualified Penny.Lincoln.TextNonEmpty as TNE+import qualified Penny.Lincoln.Transaction as T
+ Penny/Lincoln/Balance.hs view
@@ -0,0 +1,107 @@+module Penny.Lincoln.Balance ( + Balance,+ unBalance,+ Balanced(Balanced, Inferable, NotInferable),+ isBalanced,+ entryToBalance,+ addBalances,+ removeZeroCommodities,+ BottomLine(Zero, NonZero),+ Column(Column, drCr, qty)+ ) where++import Data.Map ( Map )+import qualified Data.Map as M+import Data.Monoid ( Monoid, mempty, mappend )+import qualified Data.Semigroup as Semi++import Penny.Lincoln.Bits (+ add, difference, Difference(LeftBiggerBy, RightBiggerBy, Equal))+import qualified Penny.Lincoln.Bits as B++-- | A balance summarizes several entries. You do not create a Balance+-- directly. Instead, use 'entryToBalance'. Balance used to be a+-- monoid, but there is nothing appropriate for mempty. Instead,+-- Balance is really a semigroup, but not a monoid.+newtype Balance = Balance (Map B.Commodity BottomLine)+ deriving (Show, Eq)++-- | The map returned by unBalance is never empty.+unBalance :: Balance -> Map B.Commodity BottomLine+unBalance (Balance m) = m++-- | Returned by 'isBalanced'.+data Balanced = Balanced+ | Inferable B.Entry+ | NotInferable+ deriving (Show, Eq)++-- | Is this balance balanced?+isBalanced :: Balance -> Balanced+isBalanced (Balance m) = M.foldrWithKey f Balanced m where+ f c n b = case n of+ Zero -> b+ (NonZero col) -> case b of+ Balanced -> let+ e = B.Entry dc a+ dc = case drCr col of+ B.Debit -> B.Credit+ B.Credit -> B.Debit+ q = qty col+ a = B.Amount q c+ in Inferable e+ _ -> NotInferable++-- | Converts an Entry to a Balance.+entryToBalance :: B.Entry -> Balance+entryToBalance (B.Entry dc am) = Balance $ M.singleton c no where+ c = B.commodity am+ no = NonZero (Column dc (B.qty am))++data BottomLine = Zero+ | NonZero Column+ deriving (Show, Eq)++instance Monoid BottomLine where+ mempty = Zero+ mappend n1 n2 = case (n1, n2) of+ (Zero, Zero) -> Zero+ (Zero, (NonZero c)) -> NonZero c+ ((NonZero c), Zero) -> NonZero c+ ((NonZero c1), (NonZero c2)) ->+ let (Column dc1 q1) = c1+ (Column dc2 q2) = c2+ in if dc1 == dc2+ then NonZero $ Column dc1 (q1 `add` q2)+ else case difference q1 q2 of+ LeftBiggerBy diff ->+ NonZero $ Column dc1 diff+ RightBiggerBy diff ->+ NonZero $ Column dc2 diff+ Equal -> Zero++data Column = Column { drCr :: B.DrCr+ , qty :: B.Qty }+ deriving (Show, Eq)++-- | Add two Balances together. Commodities are never removed from the+-- balance, even if their balance is zero. Instead, they are left in+-- the balance. Sometimes you want to know that a commodity was in the+-- account but its balance is now zero.+addBalances :: Balance -> Balance -> Balance+addBalances (Balance t1) (Balance t2) = + Balance $ M.unionWith mappend t1 t2++instance Semi.Semigroup Balance where+ (<>) = addBalances++-- | Removes zero balances from a Balance. Will not return a Balance+-- with no commodities; instead, returns Nothing if there would be a+-- balance with no commodities.+removeZeroCommodities :: Balance -> Maybe Balance+removeZeroCommodities (Balance m) =+ let p b = case b of+ Zero -> False+ _ -> True+ m' = M.filter p m+ in if M.null m' then Nothing else Just (Balance m')
+ Penny/Lincoln/Bits.hs view
@@ -0,0 +1,74 @@+-- | Essential data types used to make Transactions and Postings.+module Penny.Lincoln.Bits (+ -- * Accounts+ Ac.SubAccountName(SubAccountName, unSubAccountName),+ Ac.Account(Account, unAccount),++ -- * Amounts+ Am.Amount(Amount, qty, commodity),++ -- * Commodities+ C.Commodity(Commodity, unCommodity),+ C.SubCommodity(SubCommodity, unSubCommodity),+ C.charCommodity,++ -- * DateTime+ DT.DateTime,+ DT.dateTime,+ DT.localTime,+ DT.timeZone,+ DT.TimeZoneOffset,+ DT.minsToOffset, DT.offsetToMins, DT.noOffset,++ -- * Debits and Credits+ DC.DrCr(Debit, Credit),+ DC.opposite,+ + -- * Entries+ E.Entry(Entry, drCr, amount),++ -- * Flag+ F.Flag(Flag, unFlag),++ -- * Memos+ M.MemoLine(MemoLine, unMemoLine),+ M.Memo(Memo, unMemo),++ -- * Number+ N.Number(Number, unNumber),++ -- * Payee+ Pa.Payee(Payee, unPayee),++ -- * Prices and price points+ Pr.From(From, unFrom), Pr.To(To, unTo),+ Pr.CountPerUnit(CountPerUnit, unCountPerUnit),+ Pr.Price(from, to, countPerUnit),+ Pr.convert, Pr.newPrice,+ PP.PricePoint(PricePoint, price, ppMeta),++ -- * Quantities+ Q.Qty, Q.unQty, Q.partialNewQty,+ Q.newQty, Q.add, Q.subt, Q.mult, Q.zero,+ Q.difference,+ Q.Difference(Q.LeftBiggerBy, Q.RightBiggerBy, Q.Equal),++ -- * Tags+ T.Tag(Tag, unTag),+ T.Tags(Tags, unTags)) where+++import qualified Penny.Lincoln.Bits.Account as Ac+import qualified Penny.Lincoln.Bits.Amount as Am+import qualified Penny.Lincoln.Bits.Commodity as C+import qualified Penny.Lincoln.Bits.DateTime as DT+import qualified Penny.Lincoln.Bits.DrCr as DC+import qualified Penny.Lincoln.Bits.Entry as E+import qualified Penny.Lincoln.Bits.Flag as F+import qualified Penny.Lincoln.Bits.Memo as M+import qualified Penny.Lincoln.Bits.Number as N+import qualified Penny.Lincoln.Bits.Payee as Pa+import qualified Penny.Lincoln.Bits.Price as Pr+import qualified Penny.Lincoln.Bits.PricePoint as PP+import qualified Penny.Lincoln.Bits.Qty as Q+import qualified Penny.Lincoln.Bits.Tags as T
+ Penny/Lincoln/Bits/Account.hs view
@@ -0,0 +1,12 @@+module Penny.Lincoln.Bits.Account where++import Penny.Lincoln.TextNonEmpty (TextNonEmpty)+import Data.List.NonEmpty (NonEmpty)++newtype SubAccountName =+ SubAccountName { unSubAccountName :: TextNonEmpty }+ deriving (Eq, Ord, Show)++newtype Account = Account { unAccount :: NonEmpty SubAccountName }+ deriving (Eq, Show, Ord)+
+ Penny/Lincoln/Bits/Amount.hs view
@@ -0,0 +1,8 @@+module Penny.Lincoln.Bits.Amount where++import Penny.Lincoln.Bits.Qty ( Qty )+import Penny.Lincoln.Bits.Commodity ( Commodity )++data Amount = Amount { qty :: Qty+ , commodity :: Commodity }+ deriving (Eq, Show, Ord)
+ Penny/Lincoln/Bits/Commodity.hs view
@@ -0,0 +1,19 @@+module Penny.Lincoln.Bits.Commodity where++import Penny.Lincoln.TextNonEmpty (+ TextNonEmpty ( TextNonEmpty ) )+import Data.List.NonEmpty (NonEmpty((:|)))+import Data.Text ( empty )++newtype Commodity =+ Commodity { unCommodity :: NonEmpty SubCommodity }+ deriving (Eq, Ord, Show)++newtype SubCommodity =+ SubCommodity { unSubCommodity :: TextNonEmpty }+ deriving (Eq, Ord, Show)++-- | Creates a Commodity whose name is only a single character.+charCommodity :: Char -> Commodity+charCommodity c =+ Commodity ((SubCommodity (TextNonEmpty c empty)) :| [])
+ Penny/Lincoln/Bits/DateTime.hs view
@@ -0,0 +1,57 @@+-- | Perhaps this could be called @moment@, as it aims to identify a+-- moment in time. A DateTime is a combination of a LocalTime from+-- Data.Time and a TimeZoneOffset. Previously a DateTime was simply a+-- ZonedTime from Data.Time but ZonedTime has data that Penny does not+-- need.+module Penny.Lincoln.Bits.DateTime (+ DateTime+ , dateTime+ , localTime+ , timeZone+ , TimeZoneOffset+ , offsetToMins+ , minsToOffset+ , noOffset+ ) where++import qualified Data.Time as T++-- | The number of minutes that this timezone is offset from UTC. Can+-- be positive, negative, or zero.+newtype TimeZoneOffset = TimeZoneOffset { offsetToMins :: Int }+ deriving (Eq, Ord, Show)++-- | Convert minutes to a time zone offset. I'm having a hard time+-- deciding whether to be liberal or strict in what to accept+-- here. Currently it is somewhat strict in that it will fail if+-- absolute value is greater than 840 minutes; currently the article+-- at http://en.wikipedia.org/wiki/List_of_time_zones_by_UTC_offset+-- says there is no offset greater than 14 hours, or 840 minutes.+minsToOffset :: Int -> Maybe TimeZoneOffset+minsToOffset m = if abs m > 840+ then Nothing+ else Just $ TimeZoneOffset m++noOffset :: TimeZoneOffset+noOffset = TimeZoneOffset 0++-- | A DateTime is a UTC time that also remembers the local time from+-- which it was set. The Eq and Ord instances will compare two+-- DateTimes based on their equivalent UTC times.+data DateTime = DateTime { localTime :: T.LocalTime+ , timeZone :: TimeZoneOffset }+ deriving Show++-- | Construct a DateTime.+dateTime :: T.LocalTime -> TimeZoneOffset -> DateTime+dateTime = DateTime++toUTC :: DateTime -> T.UTCTime+toUTC (DateTime lt (TimeZoneOffset tzo)) = T.localTimeToUTC tz lt where+ tz = T.minutesToTimeZone tzo++instance Eq DateTime where+ l == r = toUTC l == toUTC r++instance Ord DateTime where+ compare l r = compare (toUTC l) (toUTC r)
+ Penny/Lincoln/Bits/DrCr.hs view
@@ -0,0 +1,9 @@+module Penny.Lincoln.Bits.DrCr where++data DrCr = Debit | Credit deriving (Eq, Show, Ord)++-- | Debit returns Credit; Credit returns Debit+opposite :: DrCr -> DrCr+opposite d = case d of+ Debit -> Credit+ Credit -> Debit
+ Penny/Lincoln/Bits/Entry.hs view
@@ -0,0 +1,9 @@+module Penny.Lincoln.Bits.Entry where++import Penny.Lincoln.Bits.Amount (Amount)+import Penny.Lincoln.Bits.DrCr (DrCr)++data Entry = Entry { drCr :: DrCr+ , amount :: Amount }+ deriving (Eq, Show, Ord)+
+ Penny/Lincoln/Bits/Flag.hs view
@@ -0,0 +1,7 @@+module Penny.Lincoln.Bits.Flag where++import Penny.Lincoln.TextNonEmpty (TextNonEmpty)++newtype Flag = Flag { unFlag :: TextNonEmpty }+ deriving (Eq, Show, Ord)+
+ Penny/Lincoln/Bits/Memo.hs view
@@ -0,0 +1,9 @@+module Penny.Lincoln.Bits.Memo where++import Penny.Lincoln.TextNonEmpty (TextNonEmpty)++newtype MemoLine = MemoLine { unMemoLine :: TextNonEmpty }+ deriving (Eq, Ord, Show)++newtype Memo = Memo { unMemo :: [MemoLine] }+ deriving (Eq, Show, Ord)
+ Penny/Lincoln/Bits/Number.hs view
@@ -0,0 +1,6 @@+module Penny.Lincoln.Bits.Number where++import Penny.Lincoln.TextNonEmpty (TextNonEmpty)++newtype Number = Number { unNumber :: TextNonEmpty }+ deriving (Eq, Show, Ord)
+ Penny/Lincoln/Bits/Payee.hs view
@@ -0,0 +1,7 @@+module Penny.Lincoln.Bits.Payee where++import Penny.Lincoln.TextNonEmpty ( TextNonEmpty )++newtype Payee = Payee { unPayee :: TextNonEmpty }+ deriving (Eq, Show, Ord)+
+ Penny/Lincoln/Bits/Price.hs view
@@ -0,0 +1,43 @@+module Penny.Lincoln.Bits.Price (+ From ( From, unFrom ),+ To ( To, unTo ),+ CountPerUnit ( CountPerUnit, unCountPerUnit ),+ Price ( from, to, countPerUnit ),+ convert,+ newPrice) where++import Penny.Lincoln.Bits.Amount (Amount(Amount))+import Penny.Lincoln.Bits.Commodity (Commodity)+import Penny.Lincoln.Bits.Qty (Qty, mult)++newtype From = From { unFrom :: Commodity }+ deriving (Eq, Ord, Show)++newtype To = To { unTo :: Commodity }+ deriving (Eq, Ord, Show)++newtype CountPerUnit = CountPerUnit { unCountPerUnit :: Qty }+ deriving (Eq, Ord, Show)++data Price = Price { from :: From+ , to :: To+ , countPerUnit :: CountPerUnit }+ deriving (Eq, Ord, Show)++-- | Convert an amount from the From price to the To price. Fails if+-- the From commodity in the Price is not the same as the commodity in+-- the Amount.+convert :: Price -> Amount -> Maybe Amount+convert p (Amount q c) =+ if (unFrom . from $ p) /= c+ then Nothing+ else let q' = q `mult` (unCountPerUnit . countPerUnit $ p)+ in Just (Amount q' (unTo . to $ p))++-- | Succeeds only if From and To are different commodities.+newPrice :: From -> To -> CountPerUnit -> Maybe Price+newPrice f t cpu =+ if unFrom f == unTo t+ then Nothing+ else Just $ Price f t cpu+
+ Penny/Lincoln/Bits/PricePoint.hs view
@@ -0,0 +1,10 @@+module Penny.Lincoln.Bits.PricePoint where++import Penny.Lincoln.Bits.Price (Price)+import Penny.Lincoln.Bits.DateTime (DateTime)+import qualified Penny.Lincoln.Meta as M++data PricePoint = PricePoint { dateTime :: DateTime+ , price :: Price+ , ppMeta :: M.PriceMeta }+ deriving Show
+ Penny/Lincoln/Bits/Qty.hs view
@@ -0,0 +1,70 @@+-- | Penny quantities. A quantity is simply a count (possibly+-- fractional) of something. It does not have a commodity or a+-- Debit/Credit.+module Penny.Lincoln.Bits.Qty (+ Qty, unQty, partialNewQty,+ newQty, add, subt, mult,+ zero, Difference(LeftBiggerBy, RightBiggerBy, Equal),+ difference) where++import Data.Decimal ( DecimalRaw ( Decimal ), Decimal )++-- | A quantity is always greater than zero. Various odd questions+-- happen if quantities can be zero. For instance, what if you have a+-- debit whose quantity is zero? Does it require a balancing credit+-- that is also zero? And how can you have a debit of zero anyway?+--+-- I can imagine situations where a quantity of zero might be useful;+-- for instance maybe you want to specifically indicate that a+-- particular posting in a transaction did not happen (for instance,+-- that a paycheck deduction did not take place). I think the better+-- way to handle that though would be through an addition to+-- Debit/Credit - maybe Debit/Credit/Zero. Barring the addition of+-- that, though, the best way to indicate a situation such as this+-- would be through transaction memos.+newtype Qty = Qty Decimal+ deriving (Eq, Ord, Show)++data Difference =+ LeftBiggerBy Qty+ | RightBiggerBy Qty+ | Equal++-- | Subtract the second Qty from the first.+difference :: Qty -> Qty -> Difference+difference (Qty q1) (Qty q2) = case compare q1 q2 of+ GT -> LeftBiggerBy (Qty $ q1 - q2)+ LT -> RightBiggerBy (Qty $ q2 - q1)+ EQ -> Equal++-- | Unwrap a Qty to get the underlying Decimal. This Decimal will+-- always be greater than zero.+unQty :: Qty -> Decimal+unQty (Qty d) = d++-- | Make a new Qty. This function is partial. It will call error if+-- its argument is less than or equal to zero.+partialNewQty :: Decimal -> Qty+partialNewQty d =+ if d <= 0+ then error+ $ "partialNewQty: argument less than or equal to zero: "+ ++ show d+ else Qty d++-- | Make a new Qty. Returns Nothing if its argument is less than+-- zero.+newQty :: Decimal -> Maybe Qty+newQty d = if d <= 0 then Nothing else Just (Qty d)++add :: Qty -> Qty -> Qty+add (Qty q1) (Qty q2) = Qty $ q1 + q2++subt :: Qty -> Qty -> Maybe Qty+subt (Qty q1) (Qty q2) = if q2 > q1 then Nothing else Just $ Qty (q1 - q2)++mult :: Qty -> Qty -> Qty+mult (Qty q1) (Qty q2) = Qty $ q1 * q2++zero :: Qty+zero = Qty $ Decimal 0 0
+ Penny/Lincoln/Bits/Tags.hs view
@@ -0,0 +1,10 @@+module Penny.Lincoln.Bits.Tags where++import Penny.Lincoln.TextNonEmpty (TextNonEmpty)++newtype Tag = Tag { unTag :: TextNonEmpty }+ deriving (Eq, Show, Ord)++newtype Tags = Tags { unTags :: [Tag] }+ deriving (Eq, Show, Ord)+
+ Penny/Lincoln/Builders.hs view
@@ -0,0 +1,62 @@+-- | Partial functions that make common types in Lincoln. Some data+-- types in Lincoln are deeply nested, with TextNonEmpty nested inside+-- of a newtype, nested inside of a NonEmptyList, nested inside+-- of... :) All the nesting ensures to the maximum extent possible+-- that the type system reflects the restrictions that exist on+-- Penny's data. For example, it would make no sense to have an empty+-- account (that is, an account with no sub-accounts) or a sub-account+-- whose name is an empty Text.+--+-- The disadvantage of the nesting is that building these data types+-- can be tedious if, for example, you want to build some data within+-- a short custom Haskell program. Thus, this module.++module Penny.Lincoln.Builders ( + crashy+ , account+ , commodity+ ) where++import Control.Monad.Exception.Synchronous as Ex+import qualified Data.List.Split as S+import Data.List.NonEmpty (NonEmpty((:|)))+import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.TextNonEmpty (TextNonEmpty(TextNonEmpty))+import Data.Text (pack)+import qualified Data.Traversable as T++-- | Makes a function partial. Use if you don't want to bother dealing+-- with the Exceptional type.+crashy :: Show e => Ex.Exceptional e a -> a+crashy = Ex.resolve (error . show)++-- | Create an Account. You supply a single String, with colons to+-- separate the different sub-accounts.+account :: String -> Ex.Exceptional String B.Account+account input = do+ subStrs <- case S.splitOn ":" input of+ [] -> error "splitOn returned an empty list; should never happen"+ []:[] -> Ex.throw "account name is null"+ (s:ss) -> return $ s :| ss+ let makeSub s = case s of+ [] -> Ex.throw $+ "sub account name is null from account: " ++ input+ (c:cs) -> return $ B.SubAccountName (TextNonEmpty c (pack cs))+ subs <- T.traverse makeSub subStrs+ return $ B.Account subs+ +-- | Create a Commodity. You supply a single String, with colons to+-- separate the different sub-commodities.+commodity :: String -> Ex.Exceptional String B.Commodity+commodity input = do+ subStrs <- case S.splitOn ":" input of+ [] -> error "splitOn returned an empty list; should never happen"+ []:[] -> Ex.throw "account name is null"+ (s:ss) -> return $ s :| ss+ let makeSub s = case s of+ [] -> Ex.throw $+ "sub account name is null from account: " ++ input+ (c:cs) -> return $ B.SubCommodity (TextNonEmpty c (pack cs))+ subs <- T.traverse makeSub subStrs+ return $ B.Commodity subs+
+ Penny/Lincoln/Family.hs view
@@ -0,0 +1,102 @@+-- | A Transaction consists of a TopLine (information common to all+-- postings, such as the DateTime) and of at least two Postings. This+-- data relationship is so important and useful that it is expressed+-- in these modules. This module has its own functions and re-exports+-- functions and types from other modules in this hierarchy. There are+-- handy accessor functions for the records within each of the family+-- types, but these are not exported due to name conflicts; if you+-- want these simply import the necessary module (maybe qualified).+module Penny.Lincoln.Family (+ -- * Family types+ F.Family(Family),+ C.Child(Child),+ S.Siblings(Siblings),+ + -- * Mapping families+ F.mapChildrenM,+ F.mapChildren,+ F.mapParentM,+ F.mapParent,++ -- * Functions to manipulate families+ children,+ orphans,+ adopt,+ marryWith,+ marry,+ divorceWith,+ divorce,+ S.collapse ) where++import qualified Penny.Lincoln.Family.Family as F+import qualified Penny.Lincoln.Family.Child as C+import qualified Penny.Lincoln.Family.Siblings as S++-- | Gets a family's children. The Child type contains information on+-- the parent, and each Child contains information on the other+-- Siblings.+children :: F.Family p c -> S.Siblings (C.Child p c)+children (F.Family p c1 c2 cRest) = S.Siblings fc sc rc where+ fc = C.Child c1 c2 cRest p+ sc = C.Child c2 c1 cRest p+ rc = map toChild rest+ rest = others cRest+ toChild (c, cs) = C.Child c c1 (c2:cs) p++-- | Separates the children from their parent.+orphans :: F.Family p c -> S.Siblings c+orphans (F.Family _ c1 c2 cs) = S.Siblings c1 c2 cs++-- | Unites a parent and some siblings into one family; the dual of+-- orphans.+adopt :: p -> S.Siblings c -> F.Family p c+adopt p (S.Siblings c1 c2 cs) = F.Family p c1 c2 cs++-- | Marries two families into one. This function is rather cruel: if+-- one family has more children than the other family, then the extra+-- children are discarded. That is, all children must pair one-by-one.+marryWith :: (p1 -> p2 -> p3)+ -> (c1 -> c2 -> c3)+ -> F.Family p1 c1+ -> F.Family p2 c2+ -> F.Family p3 c3+marryWith fp fc (F.Family lp lc1 lc2 lcs) (F.Family rp rc1 rc2 rcs) =+ F.Family (fp lp rp) (fc lc1 rc1) (fc lc2 rc2)+ (zipWith fc lcs rcs)++-- | marryWith a tupling function.+marry :: F.Family p1 c1+ -> F.Family p2 c2+ -> F.Family (p1, p2) (c1, c2)+marry = marryWith (,) (,)+ +-- | Splits up a family.+divorceWith :: (p1 -> (p2, p3))+ -> (c1 -> (c2, c3))+ -> F.Family p1 c1+ -> (F.Family p2 c2, F.Family p3 c3)+divorceWith fp fc (F.Family p c1 c2 cs) = (f2, f3) where+ f2 = F.Family p2 c21 c22 c2s+ f3 = F.Family p3 c31 c32 c3s+ (p2, p3) = fp p+ (c21, c31) = fc c1+ (c22, c32) = fc c2+ cps = map fc cs+ (c2s, c3s) = (map fst cps, map snd cps)++-- | divorceWith an untupling function.+divorce :: F.Family (p1, p2) (c1, c2)+ -> (F.Family p1 c1, F.Family p2 c2)+divorce = divorceWith id id++others :: [a] -> [(a, [a])]+others = map yank . allIndexes++allIndexes :: [a] -> [(Int, [a])]+allIndexes as = zip [0..] (replicate (length as) as)++yank :: (Int, [a]) -> (a, [a])+yank (i, as) = let+ (ys, zs) = splitAt i as+ in (head zs, ys ++ (tail zs))+
+ Penny/Lincoln/Family/Child.hs view
@@ -0,0 +1,9 @@+module Penny.Lincoln.Family.Child where++-- | A Child has at least one sibling and a parent.+data Child p c =+ Child { child :: c+ , sibling1 :: c+ , siblings :: [c]+ , parent :: p }+ deriving Show
+ Penny/Lincoln/Family/Family.hs view
@@ -0,0 +1,50 @@+module Penny.Lincoln.Family.Family where++import qualified Data.Functor.Identity as I++-- | A Family has one parent (ah, the anomie, sorry) and at least two+-- children.+data Family p c =+ Family { parent :: p+ , child1 :: c+ , child2 :: c+ , children :: [c] }+ deriving (Eq, Show)++-- | Maps over all children in a monad, in order starting with child+-- 1, then child 2, then the children in the list from left to right.+mapChildrenM ::+ Monad m+ => (a -> m b)+ -> Family p a+ -> m (Family p b)+mapChildrenM f (Family p c1 c2 cs) = do+ c1' <- f c1+ c2' <- f c2+ cs' <- mapM f cs+ return $ Family p c1' c2' cs'+++-- | Maps over all children, in order starting with child+-- 1, then child 2, then the children in the list from left to right.+mapChildren ::+ (a -> b)+ -> Family p a+ -> Family p b+mapChildren f fam = I.runIdentity (mapChildrenM f' fam) where+ f' = return . f++-- | Maps over the parent in a monad.+mapParentM ::+ Monad m+ => (a -> m b)+ -> Family a c+ -> m (Family b c)+mapParentM f (Family p c1 c2 cs) = do+ p' <- f p+ return $ Family p' c1 c2 cs++-- | Maps over the parent.+mapParent :: (a -> b) -> Family a c -> Family b c+mapParent f fam = I.runIdentity (mapParentM f' fam) where+ f' = return . f
+ Penny/Lincoln/Family/Siblings.hs view
@@ -0,0 +1,54 @@+module Penny.Lincoln.Family.Siblings (+ Siblings(Siblings, first, second, rest),+ collapse+ ) where++import qualified Prelude as P+import Prelude hiding (concat)+import qualified Data.Semigroup as S+import qualified Data.List.NonEmpty as NE+import Data.List.NonEmpty (NonEmpty((:|)))+import qualified Data.Foldable as Foldable+import qualified Data.Traversable as T+import Control.Applicative ((<*>), (<$>))++-- | Describes the siblings of a family, but tells you nothing about+-- the parent. There are always at least two Siblings.+data Siblings a = Siblings { first :: a+ , second :: a+ , rest :: [a] }+ deriving (Eq, Show)++instance S.Semigroup (Siblings a) where+ (Siblings a1 a2 ar) <> (Siblings b1 b2 br) =+ Siblings a1 a2 (ar ++ (b1:b2:br))++instance Functor Siblings where+ fmap g (Siblings f s rs) = Siblings (g f) (g s) (map g rs)++instance Foldable.Foldable Siblings where+ foldr g b (Siblings f s rs) = g f (g s (foldr g b rs))++instance T.Traversable Siblings where+ -- traverse :: Applicative f => (a -> f b) -> t a -> f (t b)+ traverse g (Siblings f s rs) =+ Siblings+ <$> g f+ <*> g s+ <*> T.traverse g rs++-- | Change a Siblings of NonEmpty lists to a Siblings. The original+-- order of the elements contained in the Siblings and within the+-- NonEmpty lists is preserved.+collapse :: Siblings (NE.NonEmpty a)+ -> Siblings a+collapse (Siblings (s1_1:|s1_r) s2@(s2_1:|s2_r) sr) =+ Siblings r1 r2 rr where+ r1 = s1_1+ (r2, rr) = case s1_r of+ [] -> (s2_1, (s2_r ++ concatNE sr))+ x:xs -> (x, xs ++ concatNE (s2 : sr))++concatNE :: [NE.NonEmpty a] -> [a]+concatNE = foldr f [] where+ f (a :| as) soFar = (a:as) ++ soFar
+ Penny/Lincoln/HasText.hs view
@@ -0,0 +1,105 @@+module Penny.Lincoln.HasText where++import Data.Foldable (toList)+import Data.List (intersperse)+import Data.List.NonEmpty (NonEmpty)+import Data.Text (Text, cons)+import qualified Data.Text as X++import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.TextNonEmpty (TextNonEmpty(TextNonEmpty))++class HasText a where+ text :: a -> Text++instance HasText Text where+ text = id++instance HasText TextNonEmpty where+ text (TextNonEmpty f r) = f `cons` r++instance HasText B.SubAccountName where+ text = text . B.unSubAccountName++instance HasText B.SubCommodity where+ text = text . B.unSubCommodity++instance HasText B.Flag where+ text = text . B.unFlag++instance HasText B.MemoLine where+ text = text . B.unMemoLine++instance HasText B.Number where+ text = text . B.unNumber++instance HasText B.Payee where+ text = text . B.unPayee++instance HasText B.Tag where+ text = text . B.unTag+ +-- | Applying 'text' to a Delimited type will give you a single Text+-- with the delimiter interspersed between the values of the list.+data Delimited a = Delimited Text [a]+ deriving Show++instance HasText a => HasText (Delimited a) where+ text (Delimited sep ts) = X.concat . intersperse sep . map text $ ts++class HasTextList a where+ textList :: a -> [Text]++instance HasText a => HasTextList (NonEmpty a) where+ textList = map text . toList++instance HasTextList B.Account where+ textList = textList . B.unAccount++instance HasTextList B.Commodity where+ textList = textList . B.unCommodity++instance HasTextList B.Tags where+ textList = map text . B.unTags++instance HasTextList B.Memo where+ textList = map text . B.unMemo++instance HasText a => HasTextList [a] where+ textList = map text++class HasTextNonEmpty a where+ textNonEmpty :: a -> TextNonEmpty++instance HasTextNonEmpty TextNonEmpty where+ textNonEmpty = id++instance HasTextNonEmpty B.SubAccountName where+ textNonEmpty = B.unSubAccountName++instance HasTextNonEmpty B.SubCommodity where+ textNonEmpty = B.unSubCommodity++instance HasTextNonEmpty B.Flag where+ textNonEmpty = B.unFlag+ +instance HasTextNonEmpty B.Number where+ textNonEmpty = B.unNumber++instance HasTextNonEmpty B.Payee where+ textNonEmpty = B.unPayee++instance HasTextNonEmpty B.Tag where+ textNonEmpty = B.unTag++class HasTextNonEmptyList a where+ textNonEmptyList :: a -> NonEmpty TextNonEmpty++instance HasTextNonEmpty a => HasTextNonEmptyList (NonEmpty a) where+ textNonEmptyList = fmap textNonEmpty++instance HasTextNonEmptyList B.Account where+ textNonEmptyList = fmap textNonEmpty . B.unAccount++instance HasTextNonEmptyList B.Commodity where+ textNonEmptyList = fmap textNonEmpty . B.unCommodity
+ Penny/Lincoln/Meta.hs view
@@ -0,0 +1,71 @@+module Penny.Lincoln.Meta where++import qualified Penny.Lincoln.Serial as S+import qualified Data.Text as X++newtype TopLineLine = TopLineLine { unTopLineLine :: Int }+ deriving (Eq, Show)++newtype TopMemoLine = TopMemoLine { unTopMemoLine :: Int }+ deriving (Eq, Show)++data Side = CommodityOnLeft | CommodityOnRight deriving (Eq, Show)+data SpaceBetween = SpaceBetween | NoSpaceBetween deriving (Eq, Show)++data Format =+ Format { side :: Side+ , between :: SpaceBetween }+ deriving (Eq, Show)++newtype Filename = Filename { unFilename :: X.Text }+ deriving (Eq, Show)++newtype PriceLine = PriceLine { unPriceLine :: Int }+ deriving (Eq, Show)++newtype PostingLine = PostingLine { unPostingLine :: Int }+ deriving (Eq, Show)++data PriceMeta =+ PriceMeta { priceLine :: Maybe PriceLine+ , priceFormat :: Maybe Format }+ deriving (Eq, Show)++newtype GlobalPosting =+ GlobalPosting { unGlobalPosting :: S.Serial }+ deriving (Eq, Show)++newtype FilePosting =+ FilePosting { unFilePosting :: S.Serial }+ deriving (Eq, Show)++emptyPostingMeta :: PostingMeta+emptyPostingMeta = PostingMeta Nothing Nothing Nothing Nothing++data PostingMeta =+ PostingMeta { postingLine :: Maybe PostingLine+ , postingFormat :: Maybe Format+ , globalPosting :: Maybe GlobalPosting+ , filePosting :: Maybe FilePosting }+ deriving (Eq, Show)++newtype GlobalTransaction =+ GlobalTransaction { unGlobalTransaction :: S.Serial }+ deriving (Eq, Show)++newtype FileTransaction =+ FileTransaction { unFileTransaction :: S.Serial }+ deriving (Eq, Show)+++emptyTopLineMeta :: TopLineMeta+emptyTopLineMeta = TopLineMeta Nothing Nothing Nothing Nothing Nothing++data TopLineMeta =+ TopLineMeta { topMemoLine :: Maybe TopMemoLine+ , topLineLine :: Maybe TopLineLine+ , filename :: Maybe Filename+ , globalTransaction :: Maybe GlobalTransaction+ , fileTransaction :: Maybe FileTransaction }+ deriving (Eq, Show)+
+ Penny/Lincoln/NestedMap.hs view
@@ -0,0 +1,268 @@+-- | A nested map. The values in each NestedMap are tuples, with the+-- first element of the tuple being a label that you select and the+-- second value being another NestedMap. Functions are provided so you+-- may query the map at any level or insert new labels (and,+-- therefore, new keys) at any level.+module Penny.Lincoln.NestedMap (+ NestedMap ( NestedMap ),+ unNestedMap,+ empty,+ relabel,+ descend,+ insert,+ cumulativeTotal,+ traverse,+ traverseWithTrail ) where++import Control.Applicative ((<*>), (<$>))+import Data.Map ( Map )+import qualified Data.Foldable as F+import qualified Data.Traversable as T+import qualified Data.Map as M+import Data.Monoid ( Monoid, mconcat, mappend, mempty )++newtype NestedMap k l =+ NestedMap { unNestedMap :: Map k (l, NestedMap k l) }+ deriving (Eq, Show, Ord)++instance Functor (NestedMap k) where+ fmap f (NestedMap m) = let+ g (l, s) = (f l, fmap f s)+ in NestedMap $ M.map g m++instance (Ord k) => F.Foldable (NestedMap k) where+ foldMap = T.foldMapDefault++instance (Ord k) => T.Traversable (NestedMap k) where+ -- traverse :: Applicative f+ -- => (a -> f b)+ -- -> NestedMap k a+ -- -> f (NestedMap k b)+ traverse f (NestedMap m) = let+ f' (l, m') = (,) <$> f l <*> T.traverse f m'+ in NestedMap <$> T.traverse f' m+ +-- | An empty NestedMap.+empty :: NestedMap k l+empty = NestedMap (M.empty)++-- | Helper function for relabel. For a given key and function+-- that modifies the label, return the new submap to insert into the+-- given map. Does not actually insert the submap though. That way,+-- relabel can then modify the returned submap before+-- inserting it into the mother map with the given label.+newSubmap ::+ (Ord k)+ => NestedMap k l+ -> k+ -> (Maybe l -> l)+ -> (l, NestedMap k l)+newSubmap (NestedMap m) k g = (newL, NestedMap newM) where+ (newL, newM) = case M.lookup k m of+ Nothing -> (g Nothing, M.empty)+ (Just (oldL, (NestedMap oldM))) -> (g (Just oldL), oldM)++-- | Descends through a NestedMap with successive keys in the list,+-- proceeding from left to right. At any given level, if the key+-- given does not already exist, then inserts an empty submap and+-- applies the given label modification function to Nothing to+-- determine the new label. If the given key already does exist, then+-- preserves the existing submap and applies the given label+-- modification function to (Just oldlabel) to determine the new+-- label.+relabel ::+ (Ord k)+ => NestedMap k l+ -> [(k, (Maybe l -> l))]+ -> NestedMap k l+relabel m [] = m+relabel (NestedMap m) ((k, f):vs) = let+ (newL, newM) = newSubmap (NestedMap m) k f+ newM' = relabel newM vs+ in NestedMap $ M.insert k (newL, newM') m++-- | Given a list of keys, find the key that is furthest down in the+-- map that matches the requested list of keys. Returns [(k, l)],+-- where the first item in the list is the topmost key found and its+-- matching label, and the last item in the list is the deepest key+-- found and its matching label. (Often you will be most interested+-- in the deepest key.)+descend ::+ Ord k+ => [k]+ -> NestedMap k l+ -> [(k, l)]+descend keys (NestedMap mi) = descend' keys mi where+ descend' [] _ = []+ descend' (k:ks) m = case M.lookup k m of+ Nothing -> []+ Just (l, (NestedMap im)) -> (k, l) : descend' ks im+++-- | Descends through the NestedMap one level at a time, proceeding+-- key by key from left to right through the list of keys given. At+-- the last key, appends the given label to the labels already+-- present; if no label is present, uses mempty and mappend to create+-- a new label. If the list of keys is empty, does nothing.+insert ::+ (Ord k, Monoid l)+ => NestedMap k l+ -> [k]+ -> l+ -> NestedMap k l+insert m [] _ = m+insert m ks l = relabel m ts where+ ts = firsts ++ [end]+ firsts = map (\k -> (k, keepOld)) (init ks) where+ keepOld mk = case mk of+ (Just old) -> old+ Nothing -> mempty+ end = (key, newL) where+ key = last ks+ newL mk = case mk of+ (Just old) -> old `mappend` l+ Nothing -> mempty `mappend` l+ +totalMap ::+ (Monoid l)+ => NestedMap k l+ -> l+totalMap (NestedMap m) =+ if M.null m+ then mempty+ else mconcat . map totalTuple . M.elems $ m++totalTuple ::+ (Monoid l)+ => (l, NestedMap k l)+ -> l+totalTuple (l, (NestedMap top)) =+ if M.null top+ then l+ else mappend l (totalMap (NestedMap top))++remapWithTotals ::+ (Monoid l)+ => NestedMap k l+ -> NestedMap k l+remapWithTotals (NestedMap top) =+ if M.null top+ then NestedMap M.empty+ else NestedMap $ M.map f top where+ f a@(_, m) = (totalTuple a, remapWithTotals m)++-- | Leaves all keys of the map and submaps the same. Changes each+-- label to reflect the total of that label and of all the labels of+-- the maps within the NestedMap accompanying the label. Returns the+-- total of the entire NestedMap.+cumulativeTotal ::+ (Monoid l)+ => NestedMap k l+ -> (l, NestedMap k l)+cumulativeTotal m = (totalMap m, remapWithTotals m)++-- | Supply a function that takes a key, a label, and a+-- NestedMap. traverse will traverse the NestedMap. For each (label,+-- NestedMap) pair, traverse will first apply the given function to+-- the label before descending through the NestedMap. The function is+-- applied to the present key and label and the accompanying+-- NestedMap. The function you supply must return a Maybe. If the+-- result is Nothing, then the pair is deleted as a value from its+-- parent NestedMap. If the result is (Just s), then the label of this+-- level of the NestedMap is changed to s before descending to the+-- next level of the NestedMap.+--+-- All this is done in a monad, so you can carry out arbitrary side+-- effects such as inspecting or changing a state or doing IO. If you+-- don't need a monad, just use Identity.+--+-- Thus this function can be used to inspect, modify, and prune a+-- NestedMap.+--+-- For a simpler traverse that does not provide you with so much+-- information, NestedMap is also an instance of Data.Traversable.+traverse ::+ (Monad m, Ord k)+ => (k -> l -> NestedMap k l -> m (Maybe a))+ -> NestedMap k l+ -> m (NestedMap k a)+traverse f m = traverseWithTrail (\_ -> f) m++-- | Like traverse, but the supplied function is also applied to a+-- list that tells it about the levels of NestedMap that are parents+-- to this NestedMap.+traverseWithTrail ::+ (Monad m, Ord k)+ => ( [(k, l)] -> k -> l -> NestedMap k l -> m (Maybe a) )+ -> NestedMap k l+ -> m (NestedMap k a)+traverseWithTrail f = traverseWithTrail' f []++traverseWithTrail' ::+ (Monad m, Ord k)+ => ([(k, l)] -> k -> l -> NestedMap k l -> m (Maybe a))+ -> [(k, l)]+ -> NestedMap k l+ -> m (NestedMap k a)+traverseWithTrail' f ts (NestedMap m) =+ if M.null m+ then return $ NestedMap M.empty+ else do+ let ps = M.assocs m+ mlsMaybes <- mapM (traversePairWithTrail f ts) ps+ let ps' = zip (M.keys m) mlsMaybes+ folder (k, ma) rs = case ma of+ (Just r) -> (k, r):rs+ Nothing -> rs+ ps'' = foldr folder [] ps'+ return (NestedMap (M.fromList ps''))++traversePairWithTrail ::+ (Monad m, Ord k)+ => ( [(k, l)] -> k -> l -> NestedMap k l -> m (Maybe a) )+ -> [(k, l)]+ -> (k, (l, NestedMap k l))+ -> m (Maybe (a, NestedMap k a))+traversePairWithTrail f ls (k, (l, m)) = do+ ma <- f ls k l m+ case ma of+ Nothing -> return Nothing+ (Just a) -> do+ m' <- traverseWithTrail' f ((k, l):ls) m+ return (Just (a, m'))++-- For testing+_new :: (k, l) -> (k, (Maybe l -> l))+_new (k, l) = (k, const l)++_map1, _map2, _map3, _map4 :: NestedMap Int String+_map1 = NestedMap M.empty+_map2 = relabel _map1 [_new (5, "hello"), _new (66, "goodbye"), _new (777, "yeah")]+_map3 = relabel _map2 [_new (6, "what"), _new (77, "zeke"), _new (888, "foo")]+_map4 = relabel _map3+ [ (6, (\m -> case m of Nothing -> "_new"; (Just s) -> s ++ "_new"))+ , (77, (\m -> case m of Nothing -> "_new"; (Just s) -> s ++ "more _new")) ]++_printer :: Int -> String -> a -> IO (Maybe ())+_printer i s _ = do+ putStrLn (show i)+ putStrLn s+ return $ Just ()++_printerWithTrail :: [(Int, String)] -> Int -> String -> a -> IO (Maybe ())+_printerWithTrail ps n str _ = do+ let ptr (i, s) = putStr ("(" ++ show i ++ ", " ++ s ++ ") ")+ mapM_ ptr . reverse $ ps+ ptr (n, str)+ putStrLn ""+ return $ Just ()++_showMap4 :: IO ()+_showMap4 = do+ _ <- traverse _printer _map4+ return ()++_showMapWithTrail :: IO ()+_showMapWithTrail = do+ _ <- traverseWithTrail _printerWithTrail _map4+ return ()
+ Penny/Lincoln/Predicates.hs view
@@ -0,0 +1,136 @@+-- | Functions that return a boolean based upon some criterion that+-- matches something, often a PostingBox. Useful when filtering+-- Postings.+module Penny.Lincoln.Predicates where++import Data.Text (Text, singleton)++import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.HasText (HasText, text, HasTextList, textList,+ Delimited(Delimited))+import qualified Penny.Lincoln.Queries as Q+import Penny.Lincoln.Transaction (PostFam)++-- * Matching helpers++match :: HasText a => (Text -> Bool) -> a -> Bool+match f = f . text++matchMaybe :: HasText a => (Text -> Bool) -> Maybe a -> Bool+matchMaybe f a = case a of+ Nothing -> False+ (Just t) -> match f t++matchAny :: HasTextList a => (Text -> Bool) -> a -> Bool+matchAny f = any f . textList++matchAnyMaybe :: HasTextList a => (Text -> Bool) -> Maybe a -> Bool+matchAnyMaybe f ma = case ma of+ Nothing -> False+ (Just ls) -> matchAny f ls++matchLevel :: HasTextList a => Int -> (Text -> Bool) -> a -> Bool+matchLevel i f a = let ts = textList a in+ if i < 0 || i >= length ts+ then False+ else f (ts !! i)++matchMemo :: (Text -> Bool) -> B.Memo -> Bool+matchMemo f m = f m' where+ m' = text $ Delimited (singleton ' ') (textList m)+ ++matchMaybeLevel ::+ HasTextList a+ => Int+ -> (Text -> Bool)+ -> Maybe a+ -> Bool+matchMaybeLevel i f ma = case ma of+ Nothing -> False+ (Just l) -> matchLevel i f l++-- * Pattern matching fields++payee :: (Text -> Bool) -> PostFam -> Bool+payee f = matchMaybe f . Q.payee++number :: (Text -> Bool) -> PostFam -> Bool+number f = matchMaybe f . Q.number++flag :: (Text -> Bool) -> PostFam -> Bool+flag f = matchMaybe f . Q.flag++postingMemo :: (Text -> Bool) -> PostFam -> Bool+postingMemo f = matchMemo f . Q.postingMemo++transactionMemo :: (Text -> Bool) -> PostFam -> Bool+transactionMemo f = matchMemo f . Q.transactionMemo++-- * Date++date ::+ (B.DateTime -> Bool)+ -> PostFam+ -> Bool+date f c = f (Q.dateTime c)+++-- * Qty++qty ::+ (B.Qty -> Bool)+ -> PostFam+ -> Bool+qty f c = f (Q.qty c)+++-- * DrCr+drCr :: B.DrCr -> PostFam -> Bool+drCr dc p = dc == Q.drCr p++debit :: PostFam -> Bool+debit p = Q.drCr p == B.Debit++credit :: PostFam -> Bool+credit p = Q.drCr p == B.Credit++-- * Matching delimited fields++matchDelimited ::+ HasTextList a+ => Text+ -> (Text -> Bool)+ -> a -> Bool+matchDelimited d f = f . text . Delimited d . textList++-- * Commodity++commodity :: Text -> (Text -> Bool) -> PostFam -> Bool+commodity t f = matchDelimited t f+ . B.unCommodity+ . Q.commodity++commodityLevel :: Int -> (Text -> Bool) -> PostFam -> Bool+commodityLevel i f = matchLevel i f . Q.commodity++commodityAny :: (Text -> Bool) -> PostFam -> Bool+commodityAny f = matchAny f . Q.commodity+++-- * Account+account :: Text -> (Text -> Bool) -> PostFam -> Bool+account t f = matchDelimited t f+ . B.unAccount+ . Q.account++accountLevel :: Int -> (Text -> Bool) -> PostFam -> Bool+accountLevel i f = matchLevel i f . Q.account++accountAny :: (Text -> Bool) -> PostFam -> Bool+accountAny f = matchAny f . Q.account++-- * Tags+tag :: (Text -> Bool) -> PostFam -> Bool+tag f = matchAny f . Q.tags+
+ Penny/Lincoln/PriceDb.hs view
@@ -0,0 +1,139 @@+-- | A database of price information. A PricePoint has a DateTime, a+-- From commodity, a To commodity, and a QtyPerUnit. The PriceDb holds+-- this information for several prices. You can query the database by+-- supplying a from commodity, a to commodity, and a DateTime, and the+-- database will give you the QtyPerUnit, if there is one.+module Penny.Lincoln.PriceDb (+ PriceDb,+ emptyDb,+ addPrice,+ getPrice,+ PriceDbError(FromNotFound, ToNotFound, CpuNotFound),+ convert+ ) where++import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Foldable as Fdbl+import qualified Data.Map as M+import qualified Data.Time as T+import qualified Penny.Lincoln.NestedMap as NM+import qualified Penny.Lincoln.Bits as B++type CpuMap = M.Map T.UTCTime B.CountPerUnit+type ToMap = M.Map B.To CpuMap++-- | The PriceDb holds information about prices. Create an empty one+-- using 'emptyDb' then fill it with values using foldl or similar.+newtype PriceDb = PriceDb (NM.NestedMap B.SubCommodity ToMap)++-- | An empty PriceDb+emptyDb :: PriceDb+emptyDb = PriceDb NM.empty++-- | Add a single price to the PriceDb.+addPrice :: PriceDb -> B.PricePoint -> PriceDb+addPrice (PriceDb db) pp@(B.PricePoint _ pr _) =+ PriceDb $ NM.relabel db ls where+ ls = firsts ++ [lst]+ cmdtyList = Fdbl.toList . B.unCommodity . B.unFrom . B.from $ pr+ firsts = map toFst (init cmdtyList) where+ toFst cty = (cty, f)+ f maybeL = case maybeL of+ Nothing -> M.empty+ Just m -> m+ lst = (last cmdtyList, insertIntoToMap pp)+++-- | Returns a function to use when inserting a new value into the+-- ToMap label of a PriceDb.+insertIntoToMap ::+ B.PricePoint+ -> Maybe ToMap+ -> ToMap+insertIntoToMap (B.PricePoint dt pr _) =+ let toCmdty = B.to pr+ newToMap oldMap = M.alter alterTo toCmdty oldMap+ alterTo mayCpuMap =+ let newKey = dateTimeToUTC dt+ newVal = B.countPerUnit pr+ in Just $ case mayCpuMap of+ Nothing -> M.singleton newKey newVal+ Just m -> M.insert newKey newVal m+ relabeler maybeToMap =+ newToMap (maybe M.empty id maybeToMap)+ in relabeler++dateTimeToUTC :: B.DateTime -> T.UTCTime+dateTimeToUTC dt = T.localTimeToUTC tz lt where+ tz = T.minutesToTimeZone . B.offsetToMins $ tzo+ lt = B.localTime dt+ tzo = B.timeZone dt+++-- | Getting prices can fail; if it fails, an Error is returned.+data PriceDbError = FromNotFound | ToNotFound | CpuNotFound++-- | Looks up values from the PriceDb. Throws "Error" if something+-- fails.+--+-- First, tries to find the best possible From match. For example, if+-- From is LUV:2001, first tries to see if there is a From match for+-- LUV:2001. If there is not an exact match for LUV:2001 but there+-- is a match for LUV, then LUV is used. If there is not a match+-- for either LUV:2001 or for LUV, then FromNotFound is thrown.+--+-- The To commodity must match exactly. So, if the TO commodity is+-- LUV:2001, only LUV:2001 will do. If the To commodity is not+-- found, ToNotFound is thrown.+--+-- The DateTime is the time at which to find a price. If a price+-- exists for that exact DateTime, that price is returned. If no price+-- exists for that exact DateTime, but there is a price for an earlier+-- DateTime, the latest possible price is returned. If there are no+-- earlier prices, CpuNotFound is thrown.+--+-- There is no backtracking on earlier decisions. For example, if From+-- is LUV:2001, and there is indeed at least one From price in the+-- PriceDb and CpuNotFound occurs, getPrice does not check to see if+-- the computation would have succeeded had it used LUV rather than+-- LUV:2001.++getPrice ::+ PriceDb+ -> B.From+ -> B.To+ -> B.DateTime+ -> Ex.Exceptional PriceDbError B.CountPerUnit+getPrice (PriceDb db) fr to dt = do+ let utc = dateTimeToUTC dt+ subs = Fdbl.toList . B.unCommodity . B.unFrom $ fr+ toMap <- case NM.descend subs db of+ [] -> Ex.throw FromNotFound+ xs -> return . snd . last $ xs+ cpuMap <- case M.lookup to toMap of+ Nothing -> Ex.throw ToNotFound+ Just m -> return m+ let (lower, exact, _) = M.splitLookup utc cpuMap+ cpu <- case exact of+ Just c -> return c+ Nothing ->+ if M.null lower+ then Ex.throw CpuNotFound+ else return . snd . M.findMax $ lower+ return cpu++-- | Given an Amount and a Commodity to convert the amount to,+-- converts the Amount to the given commodity. If the Amount given is+-- already in the To commodity, simply returns what was passed in. Can+-- fail and throw PriceDbError. Internally uses 'getPrice', so read its+-- documentation for details on how price lookup works.+convert ::+ PriceDb+ -> B.DateTime+ -> B.To+ -> B.Amount+ -> Ex.Exceptional PriceDbError B.Amount+convert db dt to (B.Amount qt fr) = do+ cpu <- fmap B.unCountPerUnit (getPrice db (B.From fr) to dt)+ let qt' = B.mult cpu qt+ return $ B.Amount qt' (B.unTo to)
+ Penny/Lincoln/Queries.hs view
@@ -0,0 +1,92 @@+module Penny.Lincoln.Queries where++import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.Meta as M+import Penny.Lincoln.Family.Child (child, parent)+import qualified Penny.Lincoln.Transaction as T+import Penny.Lincoln.Balance (Balance, entryToBalance)++best ::+ (T.Posting -> Maybe a)+ -> (T.TopLine -> Maybe a)+ -> T.PostFam+ -> Maybe a+best fp ft c = case fp . child . T.unPostFam $ c of+ Just r -> Just r+ Nothing -> ft . parent . T.unPostFam $ c+++payee :: T.PostFam -> Maybe B.Payee+payee = best T.pPayee T.tPayee++number :: T.PostFam -> Maybe B.Number+number = best T.pNumber T.tNumber++flag :: T.PostFam -> Maybe B.Flag+flag = best T.pFlag T.tFlag++postingMemo :: T.PostFam -> B.Memo+postingMemo = T.pMemo . child . T.unPostFam++transactionMemo :: T.PostFam -> B.Memo+transactionMemo = T.tMemo . parent . T.unPostFam++dateTime :: T.PostFam -> B.DateTime+dateTime = T.tDateTime . parent . T.unPostFam++account :: T.PostFam -> B.Account+account = T.pAccount . child . T.unPostFam++tags :: T.PostFam -> B.Tags+tags = T.pTags . child . T.unPostFam++entry :: T.PostFam -> B.Entry+entry = T.pEntry . child . T.unPostFam++balance :: T.PostFam -> Balance+balance = entryToBalance . entry++drCr :: T.PostFam -> B.DrCr+drCr = B.drCr . entry++amount :: T.PostFam -> B.Amount+amount = B.amount . entry++qty :: T.PostFam -> B.Qty+qty = B.qty . amount++commodity :: T.PostFam -> B.Commodity+commodity = B.commodity . amount++postingMeta :: T.PostFam -> M.PostingMeta+postingMeta = T.pMeta . child . T.unPostFam++topLineMeta :: T.PostFam -> M.TopLineMeta+topLineMeta = T.tMeta . parent . T.unPostFam++topMemoLine :: T.PostFam -> Maybe M.TopMemoLine+topMemoLine = M.topMemoLine . topLineMeta++topLineLine :: T.PostFam -> Maybe M.TopLineLine+topLineLine = M.topLineLine . topLineMeta++filename :: T.PostFam -> Maybe M.Filename+filename = M.filename . topLineMeta++globalTransaction :: T.PostFam -> Maybe M.GlobalTransaction+globalTransaction = M.globalTransaction . topLineMeta++fileTransaction :: T.PostFam -> Maybe M.FileTransaction+fileTransaction = M.fileTransaction . topLineMeta++postingLine :: T.PostFam -> Maybe M.PostingLine+postingLine = M.postingLine . postingMeta++postingFormat :: T.PostFam -> Maybe M.Format+postingFormat = M.postingFormat . postingMeta++globalPosting :: T.PostFam -> Maybe M.GlobalPosting+globalPosting = M.globalPosting . postingMeta++filePosting :: T.PostFam -> Maybe M.FilePosting+filePosting = M.filePosting . postingMeta
+ Penny/Lincoln/Serial.hs view
@@ -0,0 +1,105 @@+module Penny.Lincoln.Serial (+ Serial, forward, backward, serials,+ serialItems, serialItemsM,+ serialChildrenInFamily, serialEithers,+ NextFwd, NextBack, initNexts) where++import Control.Monad (zipWithM)+import Control.Monad.Trans.Class (lift)+import qualified Data.Either as E+import qualified Penny.Lincoln.Family as F+import qualified Control.Monad.Trans.State as St++-- | A type for serial numbers, used widely for different purposes+-- throughout Penny.++data Serial = Serial {+ -- | Numbered from first to last, beginning at zero.+ forward :: !Int+ + -- | Numbered from last to first, ending at zero.+ , backward :: !Int++ } deriving (Eq, Show)+ ++-- | Label a list of items with serials.+serialItems :: (Serial -> a -> b)+ -> [a]+ -> [b]+serialItems f as =+ let ss = serials as+ in zipWith f ss as++-- | Label a list of items with serials, in a monad.+serialItemsM ::+ Monad m+ => (Serial -> a -> m b)+ -> [a]+ -> m [b]+serialItemsM f as =+ let ss = serials as+ in zipWithM f ss as++-- | Applied to a list of items, return a list of Serials usable to+-- identify the list of items.+serials :: [a] -> [Serial]+serials as = zipWith Serial fs rs where+ len = length as+ fs = take len . iterate succ $ 0+ rs = take len . iterate pred $ (len - 1)+++serialChildrenInFamily ::+ (Serial -> cOld -> cNew)+ -> F.Family p cOld+ -> St.State (NextFwd, NextBack) (F.Family p cNew)+serialChildrenInFamily f = F.mapChildrenM (serialItemSt f)++newtype NextFwd = NextFwd Int deriving Show+newtype NextBack = NextBack Int deriving Show++serialItemSt ::+ (Serial -> cOld -> cNew)+ -> cOld+ -> St.State (NextFwd, NextBack) cNew+serialItemSt f old = do+ (NextFwd fwd, NextBack bak) <- St.get+ let s = Serial fwd bak+ St.put (NextFwd $ succ fwd, NextBack $ pred bak)+ return (f s old)++newtype Index = Index Int deriving (Eq, Ord, Show)+newtype Total = Total Int deriving (Eq, Ord, Show)++serialEithers ::+ Monad m+ => (Serial -> a -> m c)+ -> (Serial -> b -> m d)+ -> [Either a b]+ -> m [Either c d]+serialEithers fa fb ls =+ let allA = E.lefts ls+ allB = E.rights ls+ totA = Total . length $ allA+ totB = Total . length $ allB+ initState = (0 :: Int, 0 :: Int)+ k e = do+ (nextA, nextB) <- St.get+ case e of+ Left a -> do+ c <- lift $ fa (serial totA (Index nextA)) a+ St.put (succ nextA, nextB)+ return $ Left c+ Right b -> do+ d <- lift $ fb (serial totB (Index nextB)) b+ St.put (nextA, succ nextB)+ return $ Right d+ in St.evalStateT (mapM k ls) initState++serial :: Total -> Index -> Serial+serial (Total t) (Index i) = Serial i b where+ b = t - i - 1++initNexts :: Int -> (NextFwd, NextBack)+initNexts i = (NextFwd 0, NextBack $ i - 1)
+ Penny/Lincoln/TextNonEmpty.hs view
@@ -0,0 +1,25 @@+module Penny.Lincoln.TextNonEmpty (+ TextNonEmpty (TextNonEmpty, first, rest),+ unsafeTextNonEmpty,+ any, all,+ toText) where++import Prelude hiding (any, all)+import Data.Text ( Text, pack, cons )+import qualified Data.Text as X++data TextNonEmpty = TextNonEmpty { first :: Char+ , rest :: Text }+ deriving (Eq, Ord, Show)++unsafeTextNonEmpty :: String -> TextNonEmpty+unsafeTextNonEmpty s = TextNonEmpty (head s) (pack . tail $ s)++any :: (Char -> Bool) -> TextNonEmpty -> Bool+any f (TextNonEmpty c ts) = f c || X.any f ts++all :: (Char -> Bool) -> TextNonEmpty -> Bool+all f (TextNonEmpty c ts) = f c && X.all f ts++toText :: TextNonEmpty -> Text+toText (TextNonEmpty t ts) = t `cons` ts
+ Penny/Lincoln/Transaction.hs view
@@ -0,0 +1,278 @@+-- | Transactions, the heart of Penny. The Transaction data type is+-- abstract, so that only this module can create Transactions. This+-- provides assurance that if a Transaction exists, it is a valid,+-- balanced Transaction. In addition, the Posting data type is+-- abstract as well, so you know that if you have a Posting, it was+-- created as part of a balanced Transaction.+--+-- Functions prefixed with a @p@ query a particular posting for its+-- properties. Functions prefixed with a @t@ query transactions. Every+-- transaction has a single DateTime, and all the postings have this+-- same DateTime, so there is no function to query a posting's+-- DateTime. Just query the parent transaction. For other things such+-- as Number and Flag, the transaction might have data and the posting+-- might have data as well, so functions are provided to query both.+--+-- Often you will want to query a single posting and have a function+-- that gives you, for example, the posting's flag if it has one, or+-- the transaction's flag if it has one, or Nothing if neither the+-- posting nor the transaction has a flag. The functions in+-- "Penny.Lincoln.Queries" do that.+module Penny.Lincoln.Transaction (+ + -- * Postings and transactions+ Posting,+ Transaction,+ PostFam (unPostFam),++ -- * Making transactions+ transaction,+ Error ( UnbalancedError, CouldNotInferError),+ + -- * Querying postings+ Inferred(Inferred, NotInferred),+ pPayee, pNumber, pFlag, pAccount, pTags,+ pEntry, pMemo, pInferred, pMeta, changePostingMeta,++ -- * Querying transactions+ TopLine,+ tDateTime, tFlag, tNumber, tPayee, tMemo, tMeta,+ unTransaction, postFam, changeTransactionMeta,+ + -- * Adding serials+ addSerialsToList, addSerialsToEithers,+ + -- * Box+ Box ( Box, boxMeta, boxPostFam )+ + ) where++import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.Family ( children, orphans, adopt )+import qualified Penny.Lincoln.Meta as M+import qualified Penny.Lincoln.Family.Family as F+import qualified Penny.Lincoln.Family.Child as C+import qualified Penny.Lincoln.Family.Siblings as S+import qualified Penny.Lincoln.Transaction.Unverified as U+import qualified Penny.Lincoln.Balance as Bal+import qualified Penny.Lincoln.Serial as Ser++import Control.Monad.Exception.Synchronous (+ Exceptional (Exception, Success) , throw )+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Either as E+import qualified Data.Foldable as Fdbl+import Data.Maybe ( catMaybes )+import qualified Data.Traversable as Tr+import qualified Control.Monad.Trans.State.Lazy as St+import Control.Monad.Trans.Class ( lift )++-- | Indicates whether the entry for this posting was inferred. That+-- is, if the user did not supply an entry for this posting, then it+-- was inferred.+data Inferred = Inferred | NotInferred deriving (Eq, Show)++-- | Each Transaction consists of at least two Postings.+data Posting =+ Posting { pPayee :: (Maybe B.Payee)+ , pNumber :: (Maybe B.Number)+ , pFlag :: (Maybe B.Flag)+ , pAccount :: B.Account+ , pTags :: B.Tags+ , pEntry :: B.Entry+ , pMemo :: B.Memo+ , pInferred :: Inferred+ , pMeta :: M.PostingMeta }+ deriving (Eq, Show)++-- | The TopLine holds information that applies to all the postings in+-- a transaction (so named because in a ledger file, this information+-- appears on the top line.)+data TopLine =+ TopLine { tDateTime :: B.DateTime+ , tFlag :: (Maybe B.Flag)+ , tNumber :: (Maybe B.Number)+ , tPayee :: (Maybe B.Payee)+ , tMemo :: B.Memo+ , tMeta :: M.TopLineMeta }+ deriving (Eq, Show)++-- | All the Postings in a Transaction must produce a Total whose+-- debits and credits are equal. That is, the Transaction must be+-- balanced. No Transactions are created that are not balanced.+newtype Transaction =+ Transaction { unTransaction :: F.Family TopLine Posting }+ deriving (Eq, Show)+ +-- | Errors that can arise when making a Transaction.+data Error = UnbalancedError+ | CouldNotInferError+ deriving (Eq, Show)++newtype PostFam = PostFam { unPostFam :: C.Child TopLine Posting }+ deriving Show++-- | Get the Postings from a Transaction, with information on the+-- sibling Postings.+postFam :: Transaction -> [PostFam]+postFam (Transaction ps) = map PostFam . Fdbl.toList . children $ ps++{- BNF-like grammar for the various sorts of allowed postings.++postingGroup ::= (inferGroup balancedGroup*) | balancedGroup++inferGroup ::= "at least 1 posting. All postings have same account and+ commodity. The balance is inferable."+balancedGroup ::= "at least 2 postings. All postings have the same+ account and commodity. The balance is balanced."++-}++-- | Makes transactions.+transaction ::+ F.Family U.TopLine U.Posting+ -> Exceptional Error Transaction+transaction f@(F.Family p _ _ _) = do+ let os = orphans f+ t = totalAll os+ p' = toTopLine p+ a2 <- inferAll os t+ return $ Transaction (adopt p' a2)++totalAll :: S.Siblings U.Posting+ -> Bal.Balance+totalAll =+ Fdbl.foldr1 Bal.addBalances+ . catMaybes+ . Fdbl.toList+ . fmap (fmap Bal.entryToBalance . U.entry)++infer ::+ U.Posting+ -> Ex.ExceptionalT Error+ (St.State (Maybe B.Entry)) Posting+infer po =+ case U.entry po of+ Nothing -> do+ st <- lift St.get+ case st of+ Nothing -> Ex.throwT CouldNotInferError+ (Just e) -> do+ lift $ St.put Nothing+ return $ toPosting po e Inferred+ (Just e) -> return $ toPosting po e NotInferred+ +runInfer ::+ Maybe B.Entry+ -> S.Siblings U.Posting+ -> Exceptional Error (S.Siblings Posting)+runInfer me pos = do+ let (res, finalSt) = St.runState ext me+ ext = Ex.runExceptionalT (Tr.mapM infer pos)+ case finalSt of+ (Just _) -> throw UnbalancedError+ Nothing -> case res of + (Exception e) -> throw e+ (Success g) -> return g++inferAll ::+ S.Siblings U.Posting+ -> Bal.Balance+ -> Exceptional Error (S.Siblings Posting)+inferAll pos t = do+ en <- case Bal.isBalanced t of+ Bal.Balanced -> return Nothing+ (Bal.Inferable e) -> return $ Just e+ Bal.NotInferable -> throw UnbalancedError+ runInfer en pos++toPosting :: U.Posting+ -> B.Entry+ -> Inferred+ -> Posting+toPosting (U.Posting p n f a t _ m mt) e i =+ Posting p n f a t e m i mt++toTopLine :: U.TopLine -> TopLine+toTopLine (U.TopLine d f n p m mt) =+ TopLine d f n p m mt+++-- | Change the metadata on a transaction.+changeTransactionMeta ::+ (M.TopLineMeta -> M.TopLineMeta)+ -- ^ Function that, when applied to the old top line meta, returns+ -- the new meta.+ + -> Transaction+ -- ^ The old transaction with metadata++ -> Transaction+ -- ^ Transaction with new metadata++changeTransactionMeta fm (Transaction f) = Transaction f' where+ f' = F.Family tl c1 c2 cs+ (F.Family p c1 c2 cs) = f+ tl = p { tMeta = fm (tMeta tl) }++-- | Change the metadata on a posting.+changePostingMeta ::+ (M.PostingMeta -> M.PostingMeta)+ -> Transaction+ -> Transaction+changePostingMeta f (Transaction fam) =+ Transaction . F.mapChildren g $ fam+ where+ g p = p { pMeta = f (pMeta p) }++addSerials ::+ (Ser.Serial -> M.TopLineMeta -> M.TopLineMeta)+ -> (Ser.Serial -> M.PostingMeta -> M.PostingMeta)+ -> Ser.Serial+ -> Transaction+ -> St.State (Ser.NextFwd, Ser.NextBack) Transaction+addSerials ft fp s (Transaction fam) = do+ let topMapper pm = pm { tMeta = ft s (tMeta pm) }+ pstgMapper ser pstg = pstg { pMeta = fp ser (pMeta pstg) }+ fam' = F.mapParent topMapper fam+ fam'' <- Ser.serialChildrenInFamily pstgMapper fam'+ return $ Transaction fam''++addSerialsToList ::+ (Ser.Serial -> M.TopLineMeta -> M.TopLineMeta)+ -> (Ser.Serial -> M.PostingMeta -> M.PostingMeta)+ -> [Transaction]+ -> [Transaction]+addSerialsToList ft fp ls =+ let nPstgs = length . concatMap Fdbl.toList . map orphans+ . map unTransaction $ ls+ initState = Ser.initNexts nPstgs+ processor = addSerials ft fp+ in St.evalState (Ser.serialItemsM processor ls) initState+++addSerialsToEithers ::+ (Ser.Serial -> M.TopLineMeta -> M.TopLineMeta)+ -> (Ser.Serial -> M.PostingMeta -> M.PostingMeta)+ -> [Either a Transaction]+ -> [Either a Transaction]+addSerialsToEithers ft fp ls =+ let txns = E.rights ls+ nPstgs = length . concatMap Fdbl.toList . map orphans+ . map unTransaction $ txns+ initState = Ser.initNexts nPstgs+ processA _ a = return a+ processTxn = addSerials ft fp+ k = Ser.serialEithers processA processTxn ls+ in St.evalState k initState++-- | A box stores a family of transaction data along with+-- metadata. The transaction is stored in child form, indicating a+-- particular posting of interest. The metadata is in addition to the+-- metadata associated with the TopLine and with each posting.+data Box m =+ Box { boxMeta :: m+ , boxPostFam :: PostFam }+ deriving Show++instance Functor Box where+ fmap f (Box m pf) = Box (f m) pf
+ Penny/Lincoln/Transaction/Unverified.hs view
@@ -0,0 +1,27 @@+module Penny.Lincoln.Transaction.Unverified where++import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.Meta as M++data TopLine = TopLine+ B.DateTime+ (Maybe B.Flag)+ (Maybe B.Number)+ (Maybe B.Payee)+ B.Memo+ M.TopLineMeta+ deriving (Eq, Show)++data Posting = Posting+ (Maybe B.Payee)+ (Maybe B.Number) + (Maybe B.Flag) + B.Account+ B.Tags+ (Maybe B.Entry)+ B.Memo+ M.PostingMeta+ deriving (Eq, Show)++entry :: Posting -> Maybe B.Entry+entry (Posting _ _ _ _ _ e _ _) = e
+ Penny/Shield.hs view
@@ -0,0 +1,88 @@+-- | Shield - the Penny runtime environment+--+-- Both Cabin and Copper can benefit from knowing information about+-- the Penny runtime environment, such as environment variables and+-- whether standard output is a terminal. That information is provided+-- by the Runtime type. In the future this module may also provide+-- information about the POSIX locale configuration. For now, that+-- information would require reaching into the FFI and so it is not+-- implemented.++module Penny.Shield (+ ScreenLines,+ unScreenLines,+ ScreenWidth,+ unScreenWidth,+ Output(IsTTY, NotTTY),+ Term,+ unTerm,+ Runtime,+ environment,+ currentTime,+ output,+ screenLines,+ screenWidth,+ term,+ runtime)+ where++import Control.Applicative ((<$>), (<*>))+import qualified Data.Time as T+import System.Environment (getEnvironment)+import System.IO (hIsTerminalDevice, stdout)++import qualified Penny.Lincoln.Bits as B++data ScreenLines = ScreenLines { unScreenLines :: Int }+ deriving Show++newtype ScreenWidth = ScreenWidth { unScreenWidth :: Int }+ deriving Show++data Output = IsTTY | NotTTY++newtype Term = Term { unTerm :: String } deriving Show++-- | Information about the runtime environment.+data Runtime = Runtime { environment :: [(String, String)]+ , currentTime :: B.DateTime+ , output :: Output }++zonedToDateTime :: T.ZonedTime -> B.DateTime+zonedToDateTime (T.ZonedTime lt tz) = B.dateTime lt off where+ off = maybe (error "could not convert time to local time")+ id (B.minsToOffset (T.timeZoneMinutes tz))++runtime :: IO Runtime+runtime = Runtime+ <$> getEnvironment+ <*> (zonedToDateTime <$> T.getZonedTime)+ <*> findOutput++findOutput :: IO Output+findOutput = do+ isTerm <- hIsTerminalDevice stdout+ return $ if isTerm then IsTTY else NotTTY++screenLines :: Runtime -> Maybe ScreenLines+screenLines r = + (lookup "LINES" . environment $ r)+ >>= safeRead+ >>= return . ScreenLines++screenWidth :: Runtime -> Maybe ScreenWidth+screenWidth r =+ (lookup "COLUMNS" . environment $ r)+ >>= safeRead+ >>= return . ScreenWidth++term :: Runtime -> Maybe Term+term r =+ (lookup "TERM" . environment $ r)+ >>= return . Term++-- | Read, but without crashes.+safeRead :: (Read a) => String -> Maybe a+safeRead s = case reads s of+ (a, []):[] -> Just a+ _ -> Nothing
+ Penny/Zinc.hs view
@@ -0,0 +1,29 @@+-- | Zinc - the Penny command-line interface+module Penny.Zinc (runPenny, P.T(..),+ P.defaultFromRuntime) where++import qualified Control.Monad.Exception.Synchronous as Ex+import qualified System.Console.MultiArg.Prim as M+import System.Console.MultiArg.GetArgs (getArgs)+import System.Exit (exitFailure)+import System.IO (stderr, hPutStrLn)++import qualified Penny.Cabin.Interface as I+import qualified Penny.Copper as Cop+import qualified Penny.Shield as S+import qualified Penny.Zinc.Parser as P++runPenny ::+ S.Runtime+ -> Cop.DefaultTimeZone+ -> Cop.RadGroup+ -> (S.Runtime -> P.T)+ -> [I.Report]+ -> IO ()+runPenny rt dtz rg df rs = do+ as <- getArgs+ case M.parse as (P.parser rt dtz rg df rs) of+ Ex.Exception e -> do+ hPutStrLn stderr . show $ e+ exitFailure+ Ex.Success g -> g
+ Penny/Zinc/Error.hs view
@@ -0,0 +1,11 @@+module Penny.Zinc.Error where++import Data.Text (Text, pack)++data Error =+ ParseError Text+ | ReportError Text+ deriving Show++printError :: Error -> Text+printError = pack . show
+ Penny/Zinc/Help.hs view
@@ -0,0 +1,125 @@+module Penny.Zinc.Help where++import Data.Text (Text, pack)++help :: Text+help = pack $ unlines [+ "usage: zinc [posting filters] report [report options] file . . .",+ "",+ "Posting filters",+ "------------------------------------------",+ "",+ "Dates",+ "-----",+ "",+ "--date cmp timespec, -d cmp timespec",+ " Date must be within the time frame given. timespec",+ " is a day or a day and a time. Valid values for cmp:",+ " <, >, <=, >=, ==, /=, !=",+ "--current",+ " Same as \"--date <= (right now) \"",+ "",+ "Serials",+ "----------------",+ "These options take the form --option cmp num; the given",+ "sequence number must fall within the given range. \"rev\"",+ "in the option name indicates numbering is from end to beginning.",+ "",+ "--globalTransaction, --revGlobalTransaction",+ " All transactions, after reading the ledger files",+ "--globalPosting, --revGlobalPosting",+ " All postings, after reading the leder files",+ "--fileTransaction, --revFileTransaction",+ " Transactions in each ledger file, after reading the files",+ " (numbering restarts with each file)",+ "--filePosting, --revFilePosting",+ " Postings in each ledger file, after reading the files",+ " (numbering restarts with each file)",+ "",+ "Pattern matching",+ "----------------",+ "",+ "-a pattern, --account pattern",+ " Pattern must match colon-separated account name",+ "--account-level num pat",+ " Pattern must match sub account at given level",+ "--account-any pat",+ " Pattern must match sub account at any level",+ "-p pattern, --payee pattern",+ " Payee must match pattern",+ "-t pattern, --tag pattern",+ " Tag must match pattern",+ "--number pattern",+ " Number must match pattern",+ "--flag pattern",+ " Flag must match pattern",+ "--commodity pattern",+ " Pattern must match colon-separated commodity name",+ "--commodity-level num pattern",+ " Pattern must match sub commodity at given level",+ "--commodity-any pattern",+ " Pattern must match sub commodity at any level",+ "--posting-memo pattern",+ " Posting memo must match pattern",+ "--transaction-memo pattern",+ " Transaction memo must match pattern",+ "",+ "Other posting characteristics",+ "-----------------------------",+ "--debit",+ " Entry must be a debit",+ "--credit",+ " Entry must be a credit",+ "--qty cmp number",+ " Entry quantity must fall within given range",+ "",+ "Operators - from highest to lowest precedence",+ "(all are left associative)",+ "--------------------------",+ "--open expr --close",+ " Force precedence (as in \"open\" and \"close\" parentheses)",+ "--not expr",+ " True if expr is false",+ "expr1 --and expr2 ",+ " True if expr and expr2 are both true",+ "expr1 --or expr2",+ " True if either expr1 or expr2 is true",+ "",+ "Options affecting patterns",+ "--------------------------",+ "",+ "-i, --case-insensitive",+ " Be case insensitive (default)",+ "-I, --case-sensitive",+ " Be case sensitive",+ "",+ "--within",+ " Use \"within\" matcher (default)",+ "--pcre",+ " Use \"pcre\" matcher",+ "--posix",+ " Use \"posix\" matcher",+ "--exact",+ " Use \"exact\" matcher",+ "",+ "Removing postings after sorting and filtering",+ "---------------------------------------------",+ "--head n",+ " Keep only the first n postings",+ "--tail n",+ " Keep only the last n postings",+ "",+ "Sorting",+ "-------",+ "",+ "-s key, --sort key",+ " Sort postings according to key",+ "",+ "Keys:",+ " payee, date, flag, number, account, drCr,",+ " qty, commodity, postingMemo, transactionMemo",+ "",+ " Ascending order by default; for descending order,",+ " capitalize the name of the key.",+ ""+ ]
+ Penny/Zinc/Parser.hs view
@@ -0,0 +1,86 @@+module Penny.Zinc.Parser (+ Defaults.T(..)+ , Defaults.defaultFromRuntime+ , parser+ ) where++import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Monoid (mappend, mconcat)+import qualified Data.Text as X+import qualified Data.Text.IO as StrictIO+import qualified Data.Text.Lazy.IO as LazyIO+import System.Console.MultiArg.Prim (Parser)+import System.Exit (exitSuccess, exitFailure)+import System.IO (stderr, hPutStrLn)+import qualified Penny.Cabin.Interface as I+import qualified Penny.Shield as S+import qualified Penny.Zinc.Help as Help+import qualified Penny.Zinc.Parser.Filter as F+import qualified Penny.Zinc.Parser.Report as R+import qualified Penny.Zinc.Parser.Ledgers as L+import qualified Penny.Zinc.Parser.Defaults as Defaults++import Penny.Copper.DateTime (DefaultTimeZone)+import Penny.Copper.Qty (RadGroup)++-- | Parses all command line options and arguments. Returns an IO+-- action which will print appropriate error messages if something+-- failed, or will print a report and exit successfully if everything+-- went well.+parser ::+ S.Runtime+ -> DefaultTimeZone+ -> RadGroup+ -> (S.Runtime -> Defaults.T)+ -> [I.Report]+ -> Parser (IO ())+parser rt dtz rg getDf rs = do+ let df = getDf rt+ errFilt <- F.parseFilter df+ case errFilt of+ Ex.Exception e -> return $ do+ hPutStrLn stderr $ ("penny: error: " ++ show e)+ exitFailure+ Ex.Success r -> case r of+ Left F.NeedsHelp -> return $ do+ StrictIO.putStrLn . helpText $ rs+ exitSuccess+ Right rslt -> parseReportsAndFilesAndPrint rt dtz rg rs rslt+ ++-- | Returns an IO action that will parse the report options and the+-- files on the command line and print the resulting report.+parseReportsAndFilesAndPrint ::+ S.Runtime+ -> DefaultTimeZone+ -> RadGroup+ -> [I.Report]+ -> F.Result+ -> Parser (IO ())+parseReportsAndFilesAndPrint rt dtz rg rs rslt = do+ let (F.Result factory sensitive sortFilt) = rslt+ parserFunc <- R.report rs+ let reportFunc = parserFunc rt sensitive factory+ filenames <- L.filenames+ return $ do+ ledgers <- L.readLedgers filenames+ let parsed = L.parseLedgers dtz rg ledgers+ case parsed of+ Ex.Exception e -> do+ hPutStrLn stderr . show $ e+ exitFailure+ Ex.Success (txns, prices) -> do+ let boxes = sortFilt txns+ case reportFunc boxes prices of+ Ex.Exception bad -> do+ StrictIO.putStrLn bad+ exitFailure+ Ex.Success good -> do+ LazyIO.putStr good+ exitSuccess++helpText ::+ [I.Report]+ -> X.Text+helpText = mappend Help.help . mconcat . fmap I.help+
+ Penny/Zinc/Parser/Defaults.hs view
@@ -0,0 +1,29 @@+module Penny.Zinc.Parser.Defaults where++import qualified Text.Matchers.Text as M+import qualified Penny.Lincoln as L+import qualified Penny.Copper as Cop+import qualified Penny.Shield as S+import qualified Data.Text as X+import qualified Control.Monad.Exception.Synchronous as Ex++data T =+ T { sensitive :: M.CaseSensitive+ , factory :: M.CaseSensitive -> X.Text+ -> Ex.Exceptional X.Text (X.Text -> Bool)+ , currentTime :: L.DateTime+ , defaultTimeZone :: Cop.DefaultTimeZone+ , radGroup :: Cop.RadGroup }++defaultFromRuntime ::+ Cop.DefaultTimeZone+ -> Cop.RadGroup+ -> S.Runtime+ -> T+defaultFromRuntime dtz rg rt =+ T { sensitive = M.Insensitive+ , factory = (\c t -> return (M.within c t))+ , currentTime = S.currentTime rt+ , defaultTimeZone = dtz+ , radGroup = rg }+
+ Penny/Zinc/Parser/Filter.hs view
@@ -0,0 +1,265 @@+module Penny.Zinc.Parser.Filter (+ parseFilter+ , Error(LibertyError, TokenParseError)+ , NeedsHelp(NeedsHelp)+ , Result(Result, resultFactory, resultSensitive, sorterFilterer)+ ) where++import Control.Applicative ((<|>), (<$>), Applicative, pure, many)+import Control.Monad ((>=>))+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Monoid (mempty, mappend)+import Data.Text (Text)+import qualified Text.Matchers.Text as M+import qualified System.Console.MultiArg.Combinator as C+import System.Console.MultiArg.Prim (Parser)++import qualified Penny.Copper as Cop+import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import qualified Penny.Liberty.Expressions as X++import qualified Penny.Zinc.Parser.Defaults as D+import qualified Penny.Zinc.Parser.Defaults as Defaults++-- | Parses all filtering options. Returns a parser that contains an+-- Exception if some error occurred after parsing the options, or a+-- Success with a result if the parse was successful.+parseFilter ::+ Defaults.T+ -> Parser (Ex.Exceptional Error (Either NeedsHelp Result))+parseFilter d = fmap f (many parser) where+ f ls =+ let k = foldl (>=>) return ls+ in case k (newState d) of+ Ex.Success st' ->+ if help st'+ then return . Left $ NeedsHelp+ else+ case Ly.parsePredicate . tokens $ st' of+ Nothing -> Ex.throw TokenParseError+ Just pdct ->+ let fn = Ly.xactionsToFiltered pdct+ (postFilter st') (orderer st')+ r = Result { resultFactory = factory st'+ , resultSensitive = sensitive st'+ , sorterFilterer = fn }+ in return . Right $ r+ Ex.Exception e -> Ex.Exception e++data Error = LibertyError Ly.Error+ | TokenParseError+ deriving Show++-- | Returned if the user requested help.+data NeedsHelp = NeedsHelp+ deriving Show++-- | Indicates the result of a successful parse of filtering options.+data Result =+ Result { resultFactory :: M.CaseSensitive+ -> Text -> Ex.Exceptional Text (Text -> Bool)+ -- ^ The factory indicated, so that it can be used in+ -- subsequent parses of the same command line.++ , resultSensitive :: M.CaseSensitive+ -- ^ Indicated case sensitivity, so that it can be used in+ -- subsequent parses of the command line.+ + , sorterFilterer :: [L.Transaction] -> [L.Box Ly.LibertyMeta]+ -- ^ Applied to a list of Transaction, will sort and filter+ -- the transactions and assign them LibertyMeta.+ }+++data State =+ State { sensitive :: M.CaseSensitive+ , factory :: M.CaseSensitive+ -> Text -> Ex.Exceptional Text (Text -> Bool)+ , tokens :: [X.Token (L.PostFam -> Bool)]+ , postFilter :: [Ly.PostFilterFn]+ , orderer :: Ly.Orderer+ , help :: Bool+ , currentTime :: L.DateTime+ , defaultTimeZone :: Cop.DefaultTimeZone+ , radGroup :: Cop.RadGroup }++newState ::+ Defaults.T+ -> State+newState d =+ State { sensitive = D.sensitive d+ , factory = D.factory d+ , tokens = []+ , postFilter = []+ , orderer = mempty+ , help = False+ , currentTime = D.currentTime d+ , defaultTimeZone = D.defaultTimeZone d+ , radGroup = D.radGroup d }++parser :: Parser (State -> Ex.Exceptional Error State)+parser =+ operand+ <|> parsePostFilter+ <|> impurify parseMatcherSelect+ <|> impurify parseCaseSelect+ <|> impurify parseOperator+ <|> parseSort+ <|> impurify parseHelp++option :: [String] -> [Char] -> C.ArgSpec a -> Parser a+option ss cs a = C.parseOption [C.OptSpec ss cs a]++operand :: Parser (State -> Ex.Exceptional Error State)+operand = f <$> Ly.parseOperand+ where+ f lyFn =+ let g st =+ let r = lyFn (currentTime st) (defaultTimeZone st)+ (radGroup st) (sensitive st) (factory st)+ in case r of+ Ex.Exception e -> Ex.throw . LibertyError $ e+ Ex.Success (X.Operand o) ->+ let tok' = tokens st ++ [X.TokOperand o]+ in return st { tokens = tok' }+ in g+ +parsePostFilter :: Parser (State -> Ex.Exceptional Error State)+parsePostFilter = f <$> Ly.parsePostFilter+ where+ f lyResult =+ let g st = case lyResult of+ Ex.Exception e -> Ex.throw . LibertyError $ e+ Ex.Success pf ->+ let ls' = postFilter st ++ [pf]+ in return st { postFilter = ls' }+ in g++impurify ::+ (Functor f, Applicative a)+ => f (b -> b)+ -> f (b -> a b)+impurify = fmap (pure .)++parseMatcherSelect :: Parser (State -> State)+parseMatcherSelect = f <$> Ly.parseMatcherSelect+ where+ f fty = g+ where+ g st = st { factory = fty }++parseCaseSelect :: Parser (State -> State)+parseCaseSelect = f <$> Ly.parseCaseSelect+ where+ f sel = g+ where+ g st = st { sensitive = sel }++parseOperator :: Parser (State -> State)+parseOperator = f <$> Ly.parseOperator+ where+ f tok = g+ where+ g st = st { tokens = tokens st ++ [tok] }++parseSort :: Parser (State -> Ex.Exceptional Error State)+parseSort = f <$> Ly.parseSort+ where+ f exOrd = g+ where+ g st = case exOrd of+ Ex.Exception e -> Ex.throw . LibertyError $ e+ Ex.Success o ->+ return st { orderer = mappend o (orderer st) }++parseHelp :: Parser (State -> State)+parseHelp = option ["help"] ['h'] (C.NoArg f)+ where+ f st = st { help = True }++{-+wrapLiberty ::+ DefaultTimeZone+ -> DateTime+ -> RadGroup+ -> State+ -> ParserE Error State+wrapLiberty dtz dt rg st = let+ toLibSt = LF.State { LF.sensitive = sensitive st+ , LF.factory = factory st+ , LF.tokens = tokens st+ , LF.postFilter = postFilter st }+ fromLibSt libSt = State { sensitive = LF.sensitive libSt+ , factory = LF.factory libSt+ , tokens = LF.tokens libSt+ , postFilter = LF.postFilter libSt+ , orderer = orderer st+ , help = help st }+ in fromLibSt <$> LF.parseOption dtz dt rg toLibSt++wrapOrderer :: State -> ParserE Error State+wrapOrderer st = mkSt <$> S.sort where+ mkSt o = st { orderer = o `mappend` (orderer st) }++helpOpt :: ParserE Error ()+helpOpt = do+ let lo = makeLongOpt . pack $ "help"+ so = makeShortOpt 'h'+ _ <- mixedNoArg lo [] [so]+ return ()++wrapHelp :: State -> ParserE Error State+wrapHelp st = (\_ -> st { help = Help }) <$> helpOpt++parseOption ::+ DefaultTimeZone+ -> DateTime+ -> RadGroup+ -> State+ -> ParserE Error State+parseOption dtz dt rg st =+ wrapLiberty dtz dt rg st+ <|> wrapOrderer st+ <|> wrapHelp st++parseOptions ::+ DefaultTimeZone+ -> DateTime+ -> RadGroup+ -> State+ -> ParserE Error State+parseOptions dtz dt rg st =+ option st $ do+ rs <- runUntilFailure (parseOption dtz dt rg) st+ if null rs then return st else return (last rs)++parseFilter ::+ DefaultTimeZone+ -> DateTime+ -> RadGroup+ -> ParserE Error (Either NeedsHelp Result)+parseFilter dtz dt rg = do+ st' <- parseOptions dtz dt rg newState+ case help st' of+ Help -> return . Left $ NeedsHelp+ NoHelp -> do+ p <- case Oo.getPredicate (tokens st') of+ Just pr -> return pr+ Nothing -> throw E.BadExpression+ let f = sortFilterAndPostFilter (orderer st') p (postFilter st')+ r = Result { resultFactory = factory st'+ , resultSensitive = sensitive st'+ , sorterFilterer = f }+ return . Right $ r++sortFilterAndPostFilter ::+ S.Orderer+ -> (T.PostingInfo -> Bool)+ -> ([T.PostingInfo] -> [T.PostingInfo])+ -> [PostingBox] -> [T.PostingInfo]+sortFilterAndPostFilter o p pf =+ pf+ . filter p+ . PSq.sortedPostingInfos o+-}
+ Penny/Zinc/Parser/Ledgers.hs view
@@ -0,0 +1,79 @@+module Penny.Zinc.Parser.Ledgers (+ filenames+ , parseLedgers+ , readLedgers+ ) where++import Control.Applicative ((<$>), many, optional)+import Control.Monad (when)+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Text (Text, pack, unpack)+import qualified Data.Text.IO as TIO++import qualified Penny.Copper as C+import qualified Penny.Lincoln as L+import qualified Penny.Zinc.Error as ZE+import System.Console.MultiArg.Prim (Parser, nextArg)+import System.IO (hIsTerminalDevice, stdin, stderr, hPutStrLn)+++warnTerminal :: IO ()+warnTerminal =+ hPutStrLn stderr $ "zinc: warning: reading from standard input, "+ ++ "which is a terminal"++data Filename =+ Filename Text+ | Stdin++-- | Converts a Ledgers filename to a Lincoln filename.+convertFilename :: Filename -> L.Filename+convertFilename (Filename x) = L.Filename x+convertFilename Stdin = L.Filename . pack $ "<stdin>"++-- | Actually reads the file off disk. For now just let this crash if+-- any of the IO errors occur.+ledgerText :: Filename -> IO Text+ledgerText f = case f of+ Stdin -> do+ isTerm <- hIsTerminalDevice stdin+ when isTerm warnTerminal+ TIO.hGetContents stdin+ Filename fn -> TIO.readFile (unpack fn)++readLedgers :: [Filename] -> IO [(Filename, Text)]+readLedgers = mapM f where+ f fn = (\txt -> (fn, txt)) <$> ledgerText fn++parseLedgers ::+ C.DefaultTimeZone+ -> C.RadGroup+ -> [(Filename, Text)]+ -> Ex.Exceptional ZE.Error ([L.Transaction], [L.PricePoint])+parseLedgers dtz rg ls =+ let toPair (f, t) = (convertFilename f, C.FileContents t)+ parsed = C.parse dtz rg (map toPair ls)+ folder i (ts, ps) = case snd i of+ C.Transaction t -> (t:ts, ps)+ C.Price p -> (ts, p:ps)+ _ -> (ts, ps)+ toResult (C.Ledger is) = foldr folder ([], []) is+ toErr (C.ErrorMsg x) = ZE.ParseError x+ in Ex.mapExceptional toErr toResult parsed+++filename :: Parser Filename+filename = f <$> nextArg+ where+ f a = if a == "-"+ then Stdin+ else Filename . pack $ a++filenames :: Parser [Filename]+filenames = do+ fn1 <- optional filename+ case fn1 of+ Nothing -> return [Stdin]+ Just fn -> do+ fns <- many filename+ return (fn:fns)
+ Penny/Zinc/Parser/Report.hs view
@@ -0,0 +1,30 @@+module Penny.Zinc.Parser.Report (report) where++import System.Console.MultiArg.Prim (Parser)+import qualified System.Console.MultiArg.Combinator as C++import qualified Penny.Cabin.Interface as I+import qualified Data.Set as Set++-- | Given a Runtime and a list of available Reports, returns a Parser+-- that will return a function corresponding to the report the user+-- desires. The parser returned assumes that the filtering options+-- have already been parsed. The returned parser parses all options+-- corresponding to report options in order to return the function.+report ::+ [I.Report]+ -- ^ List of available Reports++ -> Parser I.ReportFunc+report rs = do+ let toPair r = (I.name r, I.parseReport r)+ alist = map toPair rs+ set = Set.fromList . map fst $ alist+ (_, n) <- C.matchApproxWord set+ case lookup n alist of+ Nothing -> error $ "Penny.Zinc.Parser.Report: error: "+ ++ "report not found"+ Just rptFunc -> rptFunc+++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ penny-lib.cabal view
@@ -0,0 +1,290 @@+Name: penny-lib+Version: 0.2.0.0+Cabal-version: >=1.8+Build-Type: Simple+License: MIT+Copyright: 2012 Omari Norman.+author: Omari Norman+maintainer: omari@smileystation.com+stability: Experimental+homepage: http://www.github.com/massysett/penny+bug-reports: omari@smileystation.com+Category: Console, Finance+License-File: LICENSE+synopsis: Extensible double-entry accounting system - library++description: Penny is a double-entry accounting system. It is inspired+ by, but incompatible with, John Wiegley's Ledger, which is available+ at <http://ledger-cli.org/>. You will want to install the package+ penny-bin by using cabal install penny-bin, which will install the+ executable program and this library as well. Features:++ .++ * Penny is a double-entry accounting system. It uses traditional+ accounting terminology, such as the terms \"Debit\" and+ \"Credit\". If you need a refresher on the basics of double-entry+ accounting, pick up a used accounting textbook from your favorite+ bookseller (they can be had cheaply, for less than ten U.S. dollars+ including shipping) or check out+ <http://www.principlesofaccounting.com/>, a great free online text.++ .++ * Penny is based around "Penny.Lincoln", a core library to represent+ transactions and postings and their components, such as their+ amounts and whether they are debits and credits. You can use+ Lincoln all by itself even if you don't use the other components+ of Penny, which you may find handy if you are a Haskell+ programmer. I wrote Penny because I wanted a precise library to+ represent my accounting data so I could analyze it programatically+ and verify its consistency. I wrote it in Haskell not because I+ wanted to write something in Haskell but because Haskell is the+ best tool for this job (I used to use the shell, combined with+ Ledger, which is a messy combination.)++ .++ * Penny's command line interface, Zinc, and its reports, Cabin, give+ you unparalleled flexibility to filter and sort postings. Each+ posting within a transaction may have its own flags assigned+ (e.g. to indicate whether the posting is cleared) and each posting+ may have infinite \"tags\" assigned to it, giving you another way+ to categorize your postings. For instance, you might have vacation+ related postings in several different accounts, but you can give+ them all a \"vacation\" tag.++ .++ * Full Unicode support. Also, you may set which characters you wish+ to use to represent the radix point and the digit grouping+ character in your ledger file.++ .++ * Penny's reports, in "Penny.Cabin", have color baked in from the+ beginning. You do not have to use color, though, which is handy if+ you are sending output to a file or if, well, you just don't like+ color.++ .++ * Penny's reports are customizable in Haskell, giving you an easy+ and powerful way to make your own reports without writing cryptic+ formatting strings.++ .++ * Penny handles multiple commodities (for example, multiple+ currencies, stocks and bonds, tracking other assets, etc.) in an+ easy and transparent way that is consistent with double-entry+ accounting principles. It embraces the philosophy outlined in this+ tutorial on multiple commodity accounting:+ <http://www.mscs.dal.ca/~selinger/accounting/tutorial.html>.++ .++ * Penny stores amounts using only integers, building from the+ Data.Decimal library available at+ <http://hackage.haskell.org/package/Decimal>. This ensures the+ accuracy of your data, as using floating point values to represent+ money is a bad idea. Here is one explanation:+ <http://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency>. The+ use of integer arithmetic also makes Penny simpler internally, as+ there is no need for arbitrary rounding to compensate for the+ bizarre and inaccurate results that sometimes arise from the use of+ floating-point values to represent currencies.++ .++ * Freely licensed under the MIT license. If you take this code,+ improve it, lock it up and make it proprietary, and sell it,+ AWESOME! I haven't lost anything because I still have my code and,+ what's more, then maybe I can buy your product and not have to+ maintain this one any more!++ .++ * Uses no GHC extensions. However, the code is only tested under GHC+ and for all practical purposes it will only run under GHC at this+ time because it uses libraries such as Data.Text that are+ available only under GHC. Despite this I expect I will continue to+ avoid language extensions.++ .++ Non-features / disadvantages:++ .++ * Written in Haskell. Yes, I think Haskell is the best tool ever,+ but its compiler is not as commonly installed as compilers for C+ or C++, and non-Haskellers will probably find Penny to be more+ difficult to install than Ledger, as the latter is written in C++.++ .++ * Handling commodities requires that you set up multiple accounts;+ some might find this cumbersome.++ .++ * Young and not well tested yet. Also, only tested on Unix. It+ probably would not be difficult to make Penny run on Windows; if+ someone wants to do that, go ahead.++ .++ * Full Penny functionality is available without a Haskell compiler;+ you could even use a pre-compiled binary. However, to fully+ customize Penny, you will need a Haskell compiler installed.++ .++ * Can be slow and memory hungry with large data sets. I have a+ ledger file with about 28,000 lines. On my least capable machine+ (which has an Intel Core 2 Duo at 1.6 GHz) this takes about 1.4+ seconds to parse. Not horrible but not instantaneous+ either. Generating a report about all these transactions can take+ about seven seconds and a little less than 300 MB of memory. I+ have eliminated all the obvious slowness from the code and+ attempted a rewrite of the parser, which made no difference; other+ ideas to speed up Penny with large data sets would involve+ substantial changes and this is not at the top of my list because+ the program is currently usable with relatively recent hardware.++ .++ More details about the organization of the Penny modules is+ available by examining the top "Penny" module.++ .++ This is only a library. For the executable package, which also+ includes more documentation, search for the penny-bin package on+ Hackage.++source-repository head+ type: git+ location: git://github.com/massysett/penny.git++Library+ Build-depends:+ base ==4.*,+ text ==0.11.*,+ explicit-exception ==0.1.*,+ containers ==0.4.*,+ transformers == 0.2.*,+ time ==1.2.*,+ Decimal >= 0.2.2 && < 0.3,+ parsec >= 3.1.2 && < 3.2,+ multiarg ==0.4.*,+ matchers ==0.2.*,+ old-locale ==1.0.*,+ semigroups ==0.8.*,+ split ==0.1.*++ Exposed-modules:+ Penny,+ Penny.Cabin,+ Penny.Cabin.Allocate,+ Penny.Cabin.Balance,+ Penny.Cabin.Balance.Parser,+ Penny.Cabin.Balance.Help,+ Penny.Cabin.Balance.Options,+ Penny.Cabin.Balance.Tree,+ Penny.Cabin.Chunk,+ Penny.Cabin.Chunk.Switch,+ Penny.Cabin.Colors,+ Penny.Cabin.Colors.DarkBackground,+ Penny.Cabin.Colors.LightBackground,+ Penny.Cabin.Interface,+ Penny.Cabin.Meta,+ Penny.Cabin.Options,+ Penny.Cabin.Posts,+ Penny.Cabin.Posts.Allocate,+ Penny.Cabin.Posts.Allocated,+ Penny.Cabin.Posts.BottomRows,+ Penny.Cabin.Posts.Fields,+ Penny.Cabin.Posts.Growers,+ Penny.Cabin.Posts.Chunk,+ Penny.Cabin.Posts.Help,+ Penny.Cabin.Posts.Meta,+ Penny.Cabin.Posts.Options,+ Penny.Cabin.Posts.Parser,+ Penny.Cabin.Posts.Spacers,+ Penny.Cabin.Row,+ Penny.Cabin.TextFormat,+ Penny.Copper,+ Penny.Copper.Account,+ Penny.Copper.Amount,+ Penny.Copper.Comments,+ Penny.Copper.Commodity,+ Penny.Copper.DateTime,+ Penny.Copper.Entry,+ Penny.Copper.Flag,+ Penny.Copper.Item,+ Penny.Copper.Memos.Posting,+ Penny.Copper.Memos.Transaction,+ Penny.Copper.Number,+ Penny.Copper.Payees,+ Penny.Copper.Posting,+ Penny.Copper.Price,+ Penny.Copper.Qty,+ Penny.Copper.Tags,+ Penny.Copper.TopLine,+ Penny.Copper.Transaction,+ Penny.Copper.Util,+ Penny.Liberty,+ Penny.Liberty.Expressions,+ Penny.Liberty.Expressions.Infix,+ Penny.Liberty.Expressions.RPN,+ Penny.Lincoln,+ Penny.Lincoln.Balance,+ Penny.Lincoln.Bits,+ Penny.Lincoln.Bits.Account,+ Penny.Lincoln.Bits.Amount,+ Penny.Lincoln.Bits.Commodity,+ Penny.Lincoln.Bits.DateTime,+ Penny.Lincoln.Bits.DrCr,+ Penny.Lincoln.Bits.Entry,+ Penny.Lincoln.Bits.Flag,+ Penny.Lincoln.Bits.Memo,+ Penny.Lincoln.Bits.Number,+ Penny.Lincoln.Bits.Payee,+ Penny.Lincoln.Bits.Price,+ Penny.Lincoln.Bits.PricePoint,+ Penny.Lincoln.Bits.Qty,+ Penny.Lincoln.Bits.Tags,+ Penny.Lincoln.Builders,+ Penny.Lincoln.Family,+ Penny.Lincoln.Family.Child,+ Penny.Lincoln.Family.Family,+ Penny.Lincoln.Family.Siblings,+ Penny.Lincoln.HasText,+ Penny.Lincoln.Meta,+ Penny.Lincoln.NestedMap,+ Penny.Lincoln.Predicates,+ Penny.Lincoln.PriceDb,+ Penny.Lincoln.Queries,+ Penny.Lincoln.Serial,+ Penny.Lincoln.TextNonEmpty,+ Penny.Lincoln.Transaction,+ Penny.Lincoln.Transaction.Unverified,+ Penny.Shield,+ Penny.Zinc,+ Penny.Zinc.Error,+ Penny.Zinc.Help,+ Penny.Zinc.Parser,+ Penny.Zinc.Parser.Defaults,+ Penny.Zinc.Parser.Filter,+ Penny.Zinc.Parser.Ledgers,+ Penny.Zinc.Parser.Report++ ghc-options: -Wall+ if flag(debug)+ ghc-options: -auto-all -caf-all++Flag debug+ Description: turns on some debugging options+ Default: False