packages feed

penny-lib 0.12.0.0 → 0.14.0.0

raw patch · 144 files changed

+13828/−13809 lines, 144 filesdep +QuickCheckdep +action-permutationsdep +binarydep ~multiargnew-component:exe:penny-gibberishnew-component:exe:penny-testPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, action-permutations, binary, ofx, random, random-shuffle

Dependency ranges changed: multiarg

API changes (from Hackage documentation)

- Penny: type Box = Box PostMeta
- Penny.Brenner.Amex: parser :: (String, FitFileLocation -> IO (Exceptional String [Posting]))
- Penny.Brenner.BofA: getPayee :: Desc -> Payee
- Penny.Brenner.BofA: instance Eq Label
- Penny.Brenner.BofA: instance Eq TagData
- Penny.Brenner.BofA: instance Eq TagName
- Penny.Brenner.BofA: instance Show Label
- Penny.Brenner.BofA: instance Show TagData
- Penny.Brenner.BofA: instance Show TagName
- Penny.Brenner.BofA: parser :: (String, FitFileLocation -> IO (Exceptional String [Posting]))
- Penny.Brenner.Clear: instance Eq Arg
- Penny.Brenner.Clear: instance Show Arg
- Penny.Brenner.Database: instance Eq Arg
- Penny.Brenner.Database: instance Show Arg
- Penny.Brenner.Import: instance Eq Arg
- Penny.Brenner.Import: instance Show Arg
- Penny.Brenner.Merge: instance Show Arg
- Penny.Brenner.Print: instance Eq Arg
- Penny.Brenner.Print: instance Show Arg
- Penny.Brenner.Types: Name :: Text -> Name
- Penny.Brenner.Types: instance Eq Name
- Penny.Brenner.Types: instance Show Name
- Penny.Brenner.Types: newtype Name
- Penny.Brenner.Types: unName :: Name -> Text
- Penny.Cabin.Meta: data VisibleNum
- Penny.Cabin.Meta: visibleNumBoxes :: (VisibleNum -> a -> b) -> [Box a] -> [Box b]
- Penny.Cabin.Meta: visibleNums :: (VisibleNum -> a -> b) -> [a] -> [b]
- Penny.Cabin.Posts.Meta: type Box = Box PostMeta
- Penny.Copper: BlankLine :: Item
- Penny.Copper: Comment :: Text -> Comment
- Penny.Copper: FileContents :: Text -> FileContents
- Penny.Copper: IComment :: Comment -> Item
- Penny.Copper: Ledger :: [Item] -> Ledger
- Penny.Copper: PricePoint :: PricePoint -> Item
- Penny.Copper: Transaction :: Transaction -> Item
- Penny.Copper: data ErrorMsg
- Penny.Copper: data Item
- Penny.Copper: instance Eq ErrorMsg
- Penny.Copper: instance Eq FileContents
- Penny.Copper: instance Show ErrorMsg
- Penny.Copper: instance Show FileContents
- Penny.Copper: ledger :: GroupSpecs -> Ledger -> Maybe Text
- Penny.Copper: mapItem :: (Comment -> Comment) -> (PricePoint -> PricePoint) -> (Transaction -> Transaction) -> Item -> Item
- Penny.Copper: mapItemA :: Applicative a => (Comment -> a Comment) -> (PricePoint -> a PricePoint) -> (Transaction -> a Transaction) -> Item -> a Item
- Penny.Copper: mapLedger :: (Item -> Item) -> Ledger -> Ledger
- Penny.Copper: mapLedgerA :: Applicative a => (Item -> a Item) -> Ledger -> a Ledger
- Penny.Copper: newtype Comment
- Penny.Copper: newtype FileContents
- Penny.Copper: newtype Ledger
- Penny.Copper: parse :: [(Filename, FileContents)] -> Exceptional ErrorMsg Ledger
- Penny.Copper: unComment :: Comment -> Text
- Penny.Copper: unFileContents :: FileContents -> Text
- Penny.Copper: unLedger :: Ledger -> [Item]
- Penny.Copper.Parsec: flagFirst :: Parser (Flag, Maybe Number, Maybe Payee)
- Penny.Copper.Parsec: ledger :: Parser Ledger
- Penny.Copper.Parsec: numberFirst :: Parser (Number, Maybe Flag, Maybe Payee)
- Penny.Copper.Parsec: pairedMaybes :: Parser (a, Maybe b) -> Parser (Maybe a, b) -> Parser (Maybe a, Maybe b)
- Penny.Copper.Parsec: parsePair :: Parser a -> Parser b -> Parser (Maybe a, Maybe b)
- Penny.Copper.Parsec: parseTriple :: Parser a -> Parser b -> Parser c -> Parser (a, Maybe b, Maybe c)
- Penny.Copper.Parsec: payeeFirst :: Parser (Payee, Maybe Flag, Maybe Number)
- Penny.Copper.Render: ledger :: GroupSpecs -> Ledger -> Maybe Text
- Penny.Copper.Types: BlankLine :: Item
- Penny.Copper.Types: Comment :: Text -> Comment
- Penny.Copper.Types: IComment :: Comment -> Item
- Penny.Copper.Types: Ledger :: [Item] -> Ledger
- Penny.Copper.Types: PricePoint :: PricePoint -> Item
- Penny.Copper.Types: Transaction :: Transaction -> Item
- Penny.Copper.Types: data Item
- Penny.Copper.Types: instance Eq Comment
- Penny.Copper.Types: instance Monoid Ledger
- Penny.Copper.Types: instance Show Comment
- Penny.Copper.Types: instance Show Item
- Penny.Copper.Types: instance Show Ledger
- Penny.Copper.Types: mapItem :: (Comment -> Comment) -> (PricePoint -> PricePoint) -> (Transaction -> Transaction) -> Item -> Item
- Penny.Copper.Types: mapItemA :: Applicative a => (Comment -> a Comment) -> (PricePoint -> a PricePoint) -> (Transaction -> a Transaction) -> Item -> a Item
- Penny.Copper.Types: mapLedger :: (Item -> Item) -> Ledger -> Ledger
- Penny.Copper.Types: mapLedgerA :: Applicative a => (Item -> a Item) -> Ledger -> a Ledger
- Penny.Copper.Types: newtype Comment
- Penny.Copper.Types: newtype Ledger
- Penny.Copper.Types: unComment :: Comment -> Text
- Penny.Copper.Types: unLedger :: Ledger -> [Item]
- Penny.Lincoln: Account :: [SubAccount] -> Account
- Penny.Lincoln: Amount :: Qty -> Commodity -> Maybe Side -> Maybe SpaceBetween -> Amount
- Penny.Lincoln: Balanced :: Balanced
- Penny.Lincoln: Box :: m -> PostFam -> Box m
- Penny.Lincoln: Child :: c -> c -> [c] -> p -> Child p c
- Penny.Lincoln: Column :: DrCr -> Qty -> Column
- Penny.Lincoln: Commodity :: Text -> Commodity
- Penny.Lincoln: CommodityOnLeft :: Side
- Penny.Lincoln: CommodityOnRight :: Side
- Penny.Lincoln: CouldNotInferError :: Error
- Penny.Lincoln: CountPerUnit :: Qty -> CountPerUnit
- Penny.Lincoln: CpuNotFound :: PriceDbError
- Penny.Lincoln: Credit :: DrCr
- Penny.Lincoln: DateTime :: Day -> Hours -> Minutes -> Seconds -> TimeZoneOffset -> DateTime
- Penny.Lincoln: Debit :: DrCr
- Penny.Lincoln: Entry :: DrCr -> Amount -> Entry
- Penny.Lincoln: Equal :: Difference
- Penny.Lincoln: Family :: p -> c -> c -> [c] -> Family p c
- Penny.Lincoln: FilePosting :: Serial -> FilePosting
- Penny.Lincoln: FileTransaction :: Serial -> FileTransaction
- Penny.Lincoln: Filename :: Text -> Filename
- Penny.Lincoln: Flag :: Text -> Flag
- Penny.Lincoln: From :: Commodity -> From
- Penny.Lincoln: FromNotFound :: PriceDbError
- Penny.Lincoln: GlobalPosting :: Serial -> GlobalPosting
- Penny.Lincoln: GlobalTransaction :: Serial -> GlobalTransaction
- Penny.Lincoln: Inferable :: Entry -> Balanced
- Penny.Lincoln: Inferred :: Inferred
- Penny.Lincoln: LeftBiggerBy :: Qty -> Difference
- Penny.Lincoln: Memo :: [Text] -> Memo
- Penny.Lincoln: NoSpaceBetween :: SpaceBetween
- Penny.Lincoln: NonZero :: Column -> BottomLine
- Penny.Lincoln: NotInferable :: Balanced
- Penny.Lincoln: NotInferred :: Inferred
- Penny.Lincoln: Number :: Text -> Number
- Penny.Lincoln: Payee :: Text -> Payee
- Penny.Lincoln: PostingChangeData :: Maybe (Maybe Payee) -> Maybe (Maybe Number) -> Maybe (Maybe Flag) -> Maybe Account -> Maybe Tags -> Maybe (Maybe Memo) -> Maybe (Maybe Side) -> Maybe (Maybe SpaceBetween) -> Maybe (Maybe PostingLine) -> Maybe (Maybe GlobalPosting) -> Maybe (Maybe FilePosting) -> PostingChangeData
- Penny.Lincoln: PostingLine :: Int -> PostingLine
- Penny.Lincoln: PriceLine :: Int -> PriceLine
- Penny.Lincoln: PricePoint :: DateTime -> Price -> Maybe Side -> Maybe SpaceBetween -> Maybe PriceLine -> PricePoint
- Penny.Lincoln: RTransaction :: Commodity -> Maybe Side -> Maybe SpaceBetween -> DrCr -> TopLine -> RPosting -> [RPosting] -> IPosting -> RTransaction
- Penny.Lincoln: RadFrac :: String -> NumberStr
- Penny.Lincoln: RightBiggerBy :: Qty -> Difference
- Penny.Lincoln: Siblings :: a -> a -> [a] -> Siblings a
- Penny.Lincoln: SpaceBetween :: SpaceBetween
- Penny.Lincoln: SubAccount :: Text -> SubAccount
- Penny.Lincoln: Tag :: Text -> Tag
- Penny.Lincoln: Tags :: [Tag] -> Tags
- Penny.Lincoln: To :: Commodity -> To
- Penny.Lincoln: ToNotFound :: PriceDbError
- Penny.Lincoln: TopLineChangeData :: Maybe DateTime -> Maybe (Maybe Flag) -> Maybe (Maybe Number) -> Maybe (Maybe Payee) -> Maybe (Maybe Memo) -> Maybe (Maybe TopLineLine) -> Maybe (Maybe TopMemoLine) -> Maybe (Maybe Filename) -> Maybe (Maybe GlobalTransaction) -> Maybe (Maybe FileTransaction) -> TopLineChangeData
- Penny.Lincoln: TopLineLine :: Int -> TopLineLine
- Penny.Lincoln: TopMemoLine :: Int -> TopMemoLine
- Penny.Lincoln: UnbalancedError :: Error
- Penny.Lincoln: Whole :: String -> NumberStr
- Penny.Lincoln: WholeRad :: String -> NumberStr
- Penny.Lincoln: WholeRadFrac :: String -> String -> NumberStr
- Penny.Lincoln: Zero :: BottomLine
- Penny.Lincoln: account :: Text -> Account
- Penny.Lincoln: add :: Qty -> Qty -> Qty
- Penny.Lincoln: addBalances :: Balance -> Balance -> Balance
- Penny.Lincoln: addPrice :: PriceDb -> PricePoint -> PriceDb
- Penny.Lincoln: adopt :: p -> Siblings c -> Family p c
- Penny.Lincoln: allocate :: Qty -> NonEmpty Qty -> NonEmpty Qty
- Penny.Lincoln: amount :: Entry -> Amount
- Penny.Lincoln: backward :: Serial -> Int
- Penny.Lincoln: boxMeta :: Box m -> m
- Penny.Lincoln: boxPostFam :: Box m -> PostFam
- Penny.Lincoln: changeTransaction :: Family TopLineChangeData PostingChangeData -> Transaction -> Transaction
- Penny.Lincoln: children :: Family p c -> Siblings (Child p c)
- Penny.Lincoln: class HasText a
- Penny.Lincoln: class HasTextList a
- Penny.Lincoln: commodity :: Amount -> Commodity
- Penny.Lincoln: convert :: PriceDb -> DateTime -> To -> Amount -> Exceptional PriceDbError Qty
- Penny.Lincoln: data Amount
- Penny.Lincoln: data Balance
- Penny.Lincoln: data Balanced
- Penny.Lincoln: data BottomLine
- Penny.Lincoln: data Box m
- Penny.Lincoln: data Child p c
- Penny.Lincoln: data Column
- Penny.Lincoln: data DateTime
- Penny.Lincoln: data Difference
- Penny.Lincoln: data DrCr
- Penny.Lincoln: data Entry
- Penny.Lincoln: data Error
- Penny.Lincoln: data Family p c
- Penny.Lincoln: data GenSerial a
- Penny.Lincoln: data Hours
- Penny.Lincoln: data Inferred
- Penny.Lincoln: data Minutes
- Penny.Lincoln: data NumberStr
- Penny.Lincoln: data PostFam
- Penny.Lincoln: data Posting
- Penny.Lincoln: data PostingChangeData
- Penny.Lincoln: data Price
- Penny.Lincoln: data PriceDb
- Penny.Lincoln: data PriceDbError
- Penny.Lincoln: data PricePoint
- Penny.Lincoln: data Qty
- Penny.Lincoln: data RTransaction
- Penny.Lincoln: data Seconds
- Penny.Lincoln: data Serial
- Penny.Lincoln: data Siblings a
- Penny.Lincoln: data Side
- Penny.Lincoln: data SpaceBetween
- Penny.Lincoln: data TimeZoneOffset
- Penny.Lincoln: data TopLine
- Penny.Lincoln: data TopLineChangeData
- Penny.Lincoln: data Transaction
- Penny.Lincoln: dateTime :: PricePoint -> DateTime
- Penny.Lincoln: dateTimeMidnightUTC :: Day -> DateTime
- Penny.Lincoln: day :: DateTime -> Day
- Penny.Lincoln: difference :: Qty -> Qty -> Difference
- Penny.Lincoln: divorce :: Family (p1, p2) (c1, c2) -> (Family p1 c1, Family p2 c2)
- Penny.Lincoln: divorceWith :: (p1 -> (p2, p3)) -> (c1 -> (c2, c3)) -> Family p1 c1 -> (Family p2 c2, Family p3 c3)
- Penny.Lincoln: drCr :: Entry -> DrCr
- Penny.Lincoln: emptyDb :: PriceDb
- Penny.Lincoln: emptyPostingChangeData :: PostingChangeData
- Penny.Lincoln: emptyTopLineChangeData :: TopLineChangeData
- Penny.Lincoln: entryToBalance :: Entry -> Balance
- Penny.Lincoln: equivalent :: Qty -> Qty -> Bool
- Penny.Lincoln: filterChildren :: (a -> Bool) -> Family p a -> Maybe (Family p a)
- Penny.Lincoln: find :: (p -> c -> Bool) -> Family p c -> Maybe c
- Penny.Lincoln: forward :: Serial -> Int
- Penny.Lincoln: fromZonedTime :: ZonedTime -> Maybe DateTime
- Penny.Lincoln: getPrice :: PriceDb -> From -> To -> DateTime -> Exceptional PriceDbError CountPerUnit
- Penny.Lincoln: getSerial :: GenSerial Serial
- Penny.Lincoln: hours :: DateTime -> Hours
- Penny.Lincoln: incrementBack :: GenSerial ()
- Penny.Lincoln: intToHours :: Int -> Maybe Hours
- Penny.Lincoln: intToMinutes :: Int -> Maybe Minutes
- Penny.Lincoln: intToSeconds :: Int -> Maybe Seconds
- Penny.Lincoln: isBalanced :: Balance -> Balanced
- Penny.Lincoln: makeSerials :: GenSerial a -> a
- Penny.Lincoln: mantissa :: Qty -> Integer
- Penny.Lincoln: mapChildren :: (a -> b) -> Family p a -> Family p b
- Penny.Lincoln: mapChildrenA :: Applicative m => (a -> m b) -> Family p a -> m (Family p b)
- Penny.Lincoln: mapParent :: (a -> b) -> Family a c -> Family b c
- Penny.Lincoln: mapParentA :: Applicative m => (a -> m b) -> Family a c -> m (Family b c)
- Penny.Lincoln: marry :: Family p1 c1 -> Family p2 c2 -> Family (p1, p2) (c1, c2)
- Penny.Lincoln: marryWith :: (p1 -> p2 -> p3) -> (c1 -> c2 -> c3) -> Family p1 c1 -> Family p2 c2 -> Family p3 c3
- Penny.Lincoln: midnight :: (Hours, Minutes, Seconds)
- Penny.Lincoln: minsToOffset :: Int -> Maybe TimeZoneOffset
- Penny.Lincoln: minutes :: DateTime -> Minutes
- Penny.Lincoln: mult :: Qty -> Qty -> Qty
- Penny.Lincoln: nSerials :: Int -> [Serial]
- Penny.Lincoln: newPrice :: From -> To -> CountPerUnit -> Maybe Price
- Penny.Lincoln: newQty :: Mantissa -> Places -> Maybe Qty
- Penny.Lincoln: newtype Account
- Penny.Lincoln: newtype Commodity
- Penny.Lincoln: newtype CountPerUnit
- Penny.Lincoln: newtype FilePosting
- Penny.Lincoln: newtype FileTransaction
- Penny.Lincoln: newtype Filename
- Penny.Lincoln: newtype Flag
- Penny.Lincoln: newtype From
- Penny.Lincoln: newtype GlobalPosting
- Penny.Lincoln: newtype GlobalTransaction
- Penny.Lincoln: newtype Memo
- Penny.Lincoln: newtype Number
- Penny.Lincoln: newtype Payee
- Penny.Lincoln: newtype PostingLine
- Penny.Lincoln: newtype PriceLine
- Penny.Lincoln: newtype SubAccount
- Penny.Lincoln: newtype Tag
- Penny.Lincoln: newtype Tags
- Penny.Lincoln: newtype To
- Penny.Lincoln: newtype TopLineLine
- Penny.Lincoln: newtype TopMemoLine
- Penny.Lincoln: noOffset :: TimeZoneOffset
- Penny.Lincoln: opposite :: DrCr -> DrCr
- Penny.Lincoln: orphans :: Family p c -> Siblings c
- Penny.Lincoln: pAccount :: Posting -> Account
- Penny.Lincoln: pEntry :: Posting -> Entry
- Penny.Lincoln: pFilePosting :: Posting -> Maybe FilePosting
- Penny.Lincoln: pFlag :: Posting -> Maybe Flag
- Penny.Lincoln: pGlobalPosting :: Posting -> Maybe GlobalPosting
- Penny.Lincoln: pInferred :: Posting -> Inferred
- Penny.Lincoln: pMemo :: Posting -> Maybe Memo
- Penny.Lincoln: pNumber :: Posting -> Maybe Number
- Penny.Lincoln: pPayee :: Posting -> Maybe Payee
- Penny.Lincoln: pPostingLine :: Posting -> Maybe PostingLine
- Penny.Lincoln: pTags :: Posting -> Tags
- Penny.Lincoln: pcAccount :: PostingChangeData -> Maybe Account
- Penny.Lincoln: pcFilePosting :: PostingChangeData -> Maybe (Maybe FilePosting)
- Penny.Lincoln: pcFlag :: PostingChangeData -> Maybe (Maybe Flag)
- Penny.Lincoln: pcGlobalPosting :: PostingChangeData -> Maybe (Maybe GlobalPosting)
- Penny.Lincoln: pcMemo :: PostingChangeData -> Maybe (Maybe Memo)
- Penny.Lincoln: pcNumber :: PostingChangeData -> Maybe (Maybe Number)
- Penny.Lincoln: pcPayee :: PostingChangeData -> Maybe (Maybe Payee)
- Penny.Lincoln: pcPostingLine :: PostingChangeData -> Maybe (Maybe PostingLine)
- Penny.Lincoln: pcSide :: PostingChangeData -> Maybe (Maybe Side)
- Penny.Lincoln: pcSpaceBetween :: PostingChangeData -> Maybe (Maybe SpaceBetween)
- Penny.Lincoln: pcTags :: PostingChangeData -> Maybe Tags
- Penny.Lincoln: places :: Qty -> Integer
- Penny.Lincoln: postFam :: Transaction -> [PostFam]
- Penny.Lincoln: ppSide :: PricePoint -> Maybe Side
- Penny.Lincoln: ppSpaceBetween :: PricePoint -> Maybe SpaceBetween
- Penny.Lincoln: price :: PricePoint -> Price
- Penny.Lincoln: priceLine :: PricePoint -> Maybe PriceLine
- Penny.Lincoln: qty :: Amount -> Qty
- Penny.Lincoln: rTransaction :: RTransaction -> Transaction
- Penny.Lincoln: removeZeroCommodities :: Balance -> Balance
- Penny.Lincoln: rtCommodity :: RTransaction -> Commodity
- Penny.Lincoln: rtDrCr :: RTransaction -> DrCr
- Penny.Lincoln: rtIPosting :: RTransaction -> IPosting
- Penny.Lincoln: rtMorePostings :: RTransaction -> [RPosting]
- Penny.Lincoln: rtPosting :: RTransaction -> RPosting
- Penny.Lincoln: rtSide :: RTransaction -> Maybe Side
- Penny.Lincoln: rtSpaceBetween :: RTransaction -> Maybe SpaceBetween
- Penny.Lincoln: rtTopLine :: RTransaction -> TopLine
- Penny.Lincoln: sameInstant :: DateTime -> DateTime -> Bool
- Penny.Lincoln: seconds :: DateTime -> Seconds
- Penny.Lincoln: serialItems :: (Serial -> a -> b) -> [a] -> [b]
- Penny.Lincoln: showDateTime :: DateTime -> String
- Penny.Lincoln: side :: Amount -> Maybe Side
- Penny.Lincoln: spaceBetween :: Amount -> Maybe SpaceBetween
- Penny.Lincoln: tDateTime :: TopLine -> DateTime
- Penny.Lincoln: tFileTransaction :: TopLine -> Maybe FileTransaction
- Penny.Lincoln: tFilename :: TopLine -> Maybe Filename
- Penny.Lincoln: tFlag :: TopLine -> Maybe Flag
- Penny.Lincoln: tGlobalTransaction :: TopLine -> Maybe GlobalTransaction
- Penny.Lincoln: tMemo :: TopLine -> Maybe Memo
- Penny.Lincoln: tNumber :: TopLine -> Maybe Number
- Penny.Lincoln: tPayee :: TopLine -> Maybe Payee
- Penny.Lincoln: tTopLineLine :: TopLine -> Maybe TopLineLine
- Penny.Lincoln: tTopMemoLine :: TopLine -> Maybe TopMemoLine
- Penny.Lincoln: tcDateTime :: TopLineChangeData -> Maybe DateTime
- Penny.Lincoln: tcFileTransaction :: TopLineChangeData -> Maybe (Maybe FileTransaction)
- Penny.Lincoln: tcFilename :: TopLineChangeData -> Maybe (Maybe Filename)
- Penny.Lincoln: tcFlag :: TopLineChangeData -> Maybe (Maybe Flag)
- Penny.Lincoln: tcGlobalTransaction :: TopLineChangeData -> Maybe (Maybe GlobalTransaction)
- Penny.Lincoln: tcMemo :: TopLineChangeData -> Maybe (Maybe Memo)
- Penny.Lincoln: tcNumber :: TopLineChangeData -> Maybe (Maybe Number)
- Penny.Lincoln: tcPayee :: TopLineChangeData -> Maybe (Maybe Payee)
- Penny.Lincoln: tcTopLineLine :: TopLineChangeData -> Maybe (Maybe TopLineLine)
- Penny.Lincoln: tcTopMemoLine :: TopLineChangeData -> Maybe (Maybe TopMemoLine)
- Penny.Lincoln: text :: HasText a => a -> Text
- Penny.Lincoln: textList :: HasTextList a => a -> [Text]
- Penny.Lincoln: timeZone :: DateTime -> TimeZoneOffset
- Penny.Lincoln: toQty :: NumberStr -> Maybe Qty
- Penny.Lincoln: toUTC :: DateTime -> UTCTime
- Penny.Lincoln: toUnverified :: Transaction -> Family TopLine Posting
- Penny.Lincoln: toZonedTime :: DateTime -> ZonedTime
- Penny.Lincoln: transaction :: Family TopLine Posting -> Exceptional Error Transaction
- Penny.Lincoln: type Factory = CaseSensitive -> Text -> Exceptional Text Matcher
- Penny.Lincoln: type Mantissa = Integer
- Penny.Lincoln: type Places = Integer
- Penny.Lincoln: unAccount :: Account -> [SubAccount]
- Penny.Lincoln: unBalance :: Balance -> Map Commodity BottomLine
- Penny.Lincoln: unCommodity :: Commodity -> Text
- Penny.Lincoln: unCountPerUnit :: CountPerUnit -> Qty
- Penny.Lincoln: unFilePosting :: FilePosting -> Serial
- Penny.Lincoln: unFileTransaction :: FileTransaction -> Serial
- Penny.Lincoln: unFilename :: Filename -> Text
- Penny.Lincoln: unFlag :: Flag -> Text
- Penny.Lincoln: unFrom :: From -> Commodity
- Penny.Lincoln: unGlobalPosting :: GlobalPosting -> Serial
- Penny.Lincoln: unGlobalTransaction :: GlobalTransaction -> Serial
- Penny.Lincoln: unMemo :: Memo -> [Text]
- Penny.Lincoln: unNumber :: Number -> Text
- Penny.Lincoln: unPayee :: Payee -> Text
- Penny.Lincoln: unPostFam :: PostFam -> Child TopLine Posting
- Penny.Lincoln: unPostingLine :: PostingLine -> Int
- Penny.Lincoln: unPriceLine :: PriceLine -> Int
- Penny.Lincoln: unSubAccount :: SubAccount -> Text
- Penny.Lincoln: unTag :: Tag -> Text
- Penny.Lincoln: unTags :: Tags -> [Tag]
- Penny.Lincoln: unTo :: To -> Commodity
- Penny.Lincoln: unTopLineLine :: TopLineLine -> Int
- Penny.Lincoln: unTopMemoLine :: TopMemoLine -> Int
- Penny.Lincoln: unTransaction :: Transaction -> Family TopLine Posting
- Penny.Lincoln: zeroSeconds :: Seconds
- Penny.Lincoln.Balance: drCr :: Column -> DrCr
- Penny.Lincoln.Balance: qty :: Column -> Qty
- Penny.Lincoln.Bits: Account :: [SubAccount] -> Account
- Penny.Lincoln.Bits: Amount :: Qty -> Commodity -> Maybe Side -> Maybe SpaceBetween -> Amount
- Penny.Lincoln.Bits: Commodity :: Text -> Commodity
- Penny.Lincoln.Bits: CommodityOnLeft :: Side
- Penny.Lincoln.Bits: CommodityOnRight :: Side
- Penny.Lincoln.Bits: CountPerUnit :: Qty -> CountPerUnit
- Penny.Lincoln.Bits: Credit :: DrCr
- Penny.Lincoln.Bits: DateTime :: Day -> Hours -> Minutes -> Seconds -> TimeZoneOffset -> DateTime
- Penny.Lincoln.Bits: Debit :: DrCr
- Penny.Lincoln.Bits: Entry :: DrCr -> Amount -> Entry
- Penny.Lincoln.Bits: Equal :: Difference
- Penny.Lincoln.Bits: FilePosting :: Serial -> FilePosting
- Penny.Lincoln.Bits: FileTransaction :: Serial -> FileTransaction
- Penny.Lincoln.Bits: Filename :: Text -> Filename
- Penny.Lincoln.Bits: Flag :: Text -> Flag
- Penny.Lincoln.Bits: From :: Commodity -> From
- Penny.Lincoln.Bits: GlobalPosting :: Serial -> GlobalPosting
- Penny.Lincoln.Bits: GlobalTransaction :: Serial -> GlobalTransaction
- Penny.Lincoln.Bits: LeftBiggerBy :: Qty -> Difference
- Penny.Lincoln.Bits: Memo :: [Text] -> Memo
- Penny.Lincoln.Bits: NoSpaceBetween :: SpaceBetween
- Penny.Lincoln.Bits: Number :: Text -> Number
- Penny.Lincoln.Bits: Payee :: Text -> Payee
- Penny.Lincoln.Bits: PostingLine :: Int -> PostingLine
- Penny.Lincoln.Bits: PriceLine :: Int -> PriceLine
- Penny.Lincoln.Bits: RadFrac :: String -> NumberStr
- Penny.Lincoln.Bits: RightBiggerBy :: Qty -> Difference
- Penny.Lincoln.Bits: SpaceBetween :: SpaceBetween
- Penny.Lincoln.Bits: SubAccount :: Text -> SubAccount
- Penny.Lincoln.Bits: Tag :: Text -> Tag
- Penny.Lincoln.Bits: Tags :: [Tag] -> Tags
- Penny.Lincoln.Bits: To :: Commodity -> To
- Penny.Lincoln.Bits: TopLineLine :: Int -> TopLineLine
- Penny.Lincoln.Bits: TopMemoLine :: Int -> TopMemoLine
- Penny.Lincoln.Bits: Whole :: String -> NumberStr
- Penny.Lincoln.Bits: WholeRad :: String -> NumberStr
- Penny.Lincoln.Bits: WholeRadFrac :: String -> String -> NumberStr
- Penny.Lincoln.Bits: add :: Qty -> Qty -> Qty
- Penny.Lincoln.Bits: allocate :: Qty -> NonEmpty Qty -> NonEmpty Qty
- Penny.Lincoln.Bits: amount :: Entry -> Amount
- Penny.Lincoln.Bits: commodity :: Amount -> Commodity
- Penny.Lincoln.Bits: convert :: Price -> Amount -> Maybe Amount
- Penny.Lincoln.Bits: data Amount
- Penny.Lincoln.Bits: data DateTime
- Penny.Lincoln.Bits: data Difference
- Penny.Lincoln.Bits: data DrCr
- Penny.Lincoln.Bits: data Entry
- Penny.Lincoln.Bits: data Hours
- Penny.Lincoln.Bits: data Minutes
- Penny.Lincoln.Bits: data NumberStr
- Penny.Lincoln.Bits: data Price
- Penny.Lincoln.Bits: data Qty
- Penny.Lincoln.Bits: data Seconds
- Penny.Lincoln.Bits: data Side
- Penny.Lincoln.Bits: data SpaceBetween
- Penny.Lincoln.Bits: data TimeZoneOffset
- Penny.Lincoln.Bits: dateTimeMidnightUTC :: Day -> DateTime
- Penny.Lincoln.Bits: day :: DateTime -> Day
- Penny.Lincoln.Bits: difference :: Qty -> Qty -> Difference
- Penny.Lincoln.Bits: drCr :: Entry -> DrCr
- Penny.Lincoln.Bits: equivalent :: Qty -> Qty -> Bool
- Penny.Lincoln.Bits: fromZonedTime :: ZonedTime -> Maybe DateTime
- Penny.Lincoln.Bits: hours :: DateTime -> Hours
- Penny.Lincoln.Bits: intToHours :: Int -> Maybe Hours
- Penny.Lincoln.Bits: intToMinutes :: Int -> Maybe Minutes
- Penny.Lincoln.Bits: intToSeconds :: Int -> Maybe Seconds
- Penny.Lincoln.Bits: mantissa :: Qty -> Integer
- Penny.Lincoln.Bits: midnight :: (Hours, Minutes, Seconds)
- Penny.Lincoln.Bits: minsToOffset :: Int -> Maybe TimeZoneOffset
- Penny.Lincoln.Bits: minutes :: DateTime -> Minutes
- Penny.Lincoln.Bits: mult :: Qty -> Qty -> Qty
- Penny.Lincoln.Bits: newPrice :: From -> To -> CountPerUnit -> Maybe Price
- Penny.Lincoln.Bits: newQty :: Mantissa -> Places -> Maybe Qty
- Penny.Lincoln.Bits: newtype Account
- Penny.Lincoln.Bits: newtype Commodity
- Penny.Lincoln.Bits: newtype CountPerUnit
- Penny.Lincoln.Bits: newtype FilePosting
- Penny.Lincoln.Bits: newtype FileTransaction
- Penny.Lincoln.Bits: newtype Filename
- Penny.Lincoln.Bits: newtype Flag
- Penny.Lincoln.Bits: newtype From
- Penny.Lincoln.Bits: newtype GlobalPosting
- Penny.Lincoln.Bits: newtype GlobalTransaction
- Penny.Lincoln.Bits: newtype Memo
- Penny.Lincoln.Bits: newtype Number
- Penny.Lincoln.Bits: newtype Payee
- Penny.Lincoln.Bits: newtype PostingLine
- Penny.Lincoln.Bits: newtype PriceLine
- Penny.Lincoln.Bits: newtype SubAccount
- Penny.Lincoln.Bits: newtype Tag
- Penny.Lincoln.Bits: newtype Tags
- Penny.Lincoln.Bits: newtype To
- Penny.Lincoln.Bits: newtype TopLineLine
- Penny.Lincoln.Bits: newtype TopMemoLine
- Penny.Lincoln.Bits: noOffset :: TimeZoneOffset
- Penny.Lincoln.Bits: opposite :: DrCr -> DrCr
- Penny.Lincoln.Bits: places :: Qty -> Integer
- Penny.Lincoln.Bits: qty :: Amount -> Qty
- Penny.Lincoln.Bits: sameInstant :: DateTime -> DateTime -> Bool
- Penny.Lincoln.Bits: seconds :: DateTime -> Seconds
- Penny.Lincoln.Bits: showDateTime :: DateTime -> String
- Penny.Lincoln.Bits: side :: Amount -> Maybe Side
- Penny.Lincoln.Bits: spaceBetween :: Amount -> Maybe SpaceBetween
- Penny.Lincoln.Bits: timeZone :: DateTime -> TimeZoneOffset
- Penny.Lincoln.Bits: toQty :: NumberStr -> Maybe Qty
- Penny.Lincoln.Bits: toUTC :: DateTime -> UTCTime
- Penny.Lincoln.Bits: toZonedTime :: DateTime -> ZonedTime
- Penny.Lincoln.Bits: type Mantissa = Integer
- Penny.Lincoln.Bits: type Places = Integer
- Penny.Lincoln.Bits: unAccount :: Account -> [SubAccount]
- Penny.Lincoln.Bits: unCommodity :: Commodity -> Text
- Penny.Lincoln.Bits: unCountPerUnit :: CountPerUnit -> Qty
- Penny.Lincoln.Bits: unFilePosting :: FilePosting -> Serial
- Penny.Lincoln.Bits: unFileTransaction :: FileTransaction -> Serial
- Penny.Lincoln.Bits: unFilename :: Filename -> Text
- Penny.Lincoln.Bits: unFlag :: Flag -> Text
- Penny.Lincoln.Bits: unFrom :: From -> Commodity
- Penny.Lincoln.Bits: unGlobalPosting :: GlobalPosting -> Serial
- Penny.Lincoln.Bits: unGlobalTransaction :: GlobalTransaction -> Serial
- Penny.Lincoln.Bits: unMemo :: Memo -> [Text]
- Penny.Lincoln.Bits: unNumber :: Number -> Text
- Penny.Lincoln.Bits: unPayee :: Payee -> Text
- Penny.Lincoln.Bits: unPostingLine :: PostingLine -> Int
- Penny.Lincoln.Bits: unPriceLine :: PriceLine -> Int
- Penny.Lincoln.Bits: unSubAccount :: SubAccount -> Text
- Penny.Lincoln.Bits: unTag :: Tag -> Text
- Penny.Lincoln.Bits: unTags :: Tags -> [Tag]
- Penny.Lincoln.Bits: unTo :: To -> Commodity
- Penny.Lincoln.Bits: unTopLineLine :: TopLineLine -> Int
- Penny.Lincoln.Bits: unTopMemoLine :: TopMemoLine -> Int
- Penny.Lincoln.Bits: zeroHours :: Hours
- Penny.Lincoln.Bits: zeroMinutes :: Minutes
- Penny.Lincoln.Bits: zeroSeconds :: Seconds
- Penny.Lincoln.Bits.Open: side :: Amount -> Maybe Side
- Penny.Lincoln.Bits.Open: spaceBetween :: Amount -> Maybe SpaceBetween
- Penny.Lincoln.Bits.Price: convert :: Price -> Amount -> Maybe Amount
- Penny.Lincoln.Bits.Qty: equivalent :: Qty -> Qty -> Bool
- Penny.Lincoln.Family: Child :: c -> c -> [c] -> p -> Child p c
- Penny.Lincoln.Family: Family :: p -> c -> c -> [c] -> Family p c
- Penny.Lincoln.Family: Siblings :: a -> a -> [a] -> Siblings a
- Penny.Lincoln.Family: adopt :: p -> Siblings c -> Family p c
- Penny.Lincoln.Family: children :: Family p c -> Siblings (Child p c)
- Penny.Lincoln.Family: collapse :: Siblings (NonEmpty a) -> Siblings a
- Penny.Lincoln.Family: data Child p c
- Penny.Lincoln.Family: data Family p c
- Penny.Lincoln.Family: data Siblings a
- Penny.Lincoln.Family: divorce :: Family (p1, p2) (c1, c2) -> (Family p1 c1, Family p2 c2)
- Penny.Lincoln.Family: divorceWith :: (p1 -> (p2, p3)) -> (c1 -> (c2, c3)) -> Family p1 c1 -> (Family p2 c2, Family p3 c3)
- Penny.Lincoln.Family: filterChildren :: (a -> Bool) -> Family p a -> Maybe (Family p a)
- Penny.Lincoln.Family: find :: (p -> c -> Bool) -> Family p c -> Maybe c
- Penny.Lincoln.Family: mapChildren :: (a -> b) -> Family p a -> Family p b
- Penny.Lincoln.Family: mapChildrenA :: Applicative m => (a -> m b) -> Family p a -> m (Family p b)
- Penny.Lincoln.Family: mapParent :: (a -> b) -> Family a c -> Family b c
- Penny.Lincoln.Family: mapParentA :: Applicative m => (a -> m b) -> Family a c -> m (Family b c)
- Penny.Lincoln.Family: marry :: Family p1 c1 -> Family p2 c2 -> Family (p1, p2) (c1, c2)
- Penny.Lincoln.Family: marryWith :: (p1 -> p2 -> p3) -> (c1 -> c2 -> c3) -> Family p1 c1 -> Family p2 c2 -> Family p3 c3
- Penny.Lincoln.Family: orphans :: Family p c -> Siblings c
- Penny.Lincoln.Family.Child: Child :: c -> c -> [c] -> p -> Child p c
- Penny.Lincoln.Family.Child: child :: Child p c -> c
- Penny.Lincoln.Family.Child: data Child p c
- Penny.Lincoln.Family.Child: instance (Show p, Show c) => Show (Child p c)
- Penny.Lincoln.Family.Child: parent :: Child p c -> p
- Penny.Lincoln.Family.Child: sibling1 :: Child p c -> c
- Penny.Lincoln.Family.Child: siblings :: Child p c -> [c]
- Penny.Lincoln.Family.Family: Family :: p -> c -> c -> [c] -> Family p c
- Penny.Lincoln.Family.Family: child1 :: Family p c -> c
- Penny.Lincoln.Family.Family: child2 :: Family p c -> c
- Penny.Lincoln.Family.Family: children :: Family p c -> [c]
- Penny.Lincoln.Family.Family: data Family p c
- Penny.Lincoln.Family.Family: filterChildren :: (a -> Bool) -> Family p a -> Maybe (Family p a)
- Penny.Lincoln.Family.Family: find :: (p -> c -> Bool) -> Family p c -> Maybe c
- Penny.Lincoln.Family.Family: instance (Eq p, Eq c) => Eq (Family p c)
- Penny.Lincoln.Family.Family: instance (Show p, Show c) => Show (Family p c)
- Penny.Lincoln.Family.Family: mapChildren :: (a -> b) -> Family p a -> Family p b
- Penny.Lincoln.Family.Family: mapChildrenA :: Applicative m => (a -> m b) -> Family p a -> m (Family p b)
- Penny.Lincoln.Family.Family: mapParent :: (a -> b) -> Family a c -> Family b c
- Penny.Lincoln.Family.Family: mapParentA :: Applicative m => (a -> m b) -> Family a c -> m (Family b c)
- Penny.Lincoln.Family.Family: parent :: Family p c -> p
- Penny.Lincoln.Family.Siblings: Siblings :: a -> a -> [a] -> Siblings a
- Penny.Lincoln.Family.Siblings: collapse :: Siblings (NonEmpty a) -> Siblings a
- Penny.Lincoln.Family.Siblings: data Siblings a
- Penny.Lincoln.Family.Siblings: first :: Siblings a -> a
- Penny.Lincoln.Family.Siblings: instance Eq a => Eq (Siblings a)
- Penny.Lincoln.Family.Siblings: instance Foldable Siblings
- Penny.Lincoln.Family.Siblings: instance Functor Siblings
- Penny.Lincoln.Family.Siblings: instance Semigroup (Siblings a)
- Penny.Lincoln.Family.Siblings: instance Show a => Show (Siblings a)
- Penny.Lincoln.Family.Siblings: instance Traversable Siblings
- Penny.Lincoln.Family.Siblings: rest :: Siblings a -> [a]
- Penny.Lincoln.Family.Siblings: second :: Siblings a -> a
- Penny.Lincoln.Predicates: clonedPostings :: Posting -> Posting -> Bool
- Penny.Lincoln.Predicates: clonedTopLines :: TopLine -> TopLine -> Bool
- Penny.Lincoln.Predicates: clonedTransactions :: Transaction -> Transaction -> Bool
- Penny.Lincoln.PriceDb: convert :: PriceDb -> DateTime -> To -> Amount -> Exceptional PriceDbError Qty
- Penny.Lincoln.Serial: data GenSerial a
- Penny.Lincoln.Serial: getSerial :: GenSerial Serial
- Penny.Lincoln.Serial: incrementBack :: GenSerial ()
- Penny.Lincoln.Serial: makeSerials :: GenSerial a -> a
- Penny.Lincoln.Serial: nSerials :: Int -> [Serial]
- Penny.Lincoln.Transaction: Box :: m -> PostFam -> Box m
- Penny.Lincoln.Transaction: CouldNotInferError :: Error
- Penny.Lincoln.Transaction: Inferred :: Inferred
- Penny.Lincoln.Transaction: NotInferred :: Inferred
- Penny.Lincoln.Transaction: PostingChangeData :: Maybe (Maybe Payee) -> Maybe (Maybe Number) -> Maybe (Maybe Flag) -> Maybe Account -> Maybe Tags -> Maybe (Maybe Memo) -> Maybe (Maybe Side) -> Maybe (Maybe SpaceBetween) -> Maybe (Maybe PostingLine) -> Maybe (Maybe GlobalPosting) -> Maybe (Maybe FilePosting) -> PostingChangeData
- Penny.Lincoln.Transaction: RTransaction :: Commodity -> Maybe Side -> Maybe SpaceBetween -> DrCr -> TopLine -> RPosting -> [RPosting] -> IPosting -> RTransaction
- Penny.Lincoln.Transaction: TopLineChangeData :: Maybe DateTime -> Maybe (Maybe Flag) -> Maybe (Maybe Number) -> Maybe (Maybe Payee) -> Maybe (Maybe Memo) -> Maybe (Maybe TopLineLine) -> Maybe (Maybe TopMemoLine) -> Maybe (Maybe Filename) -> Maybe (Maybe GlobalTransaction) -> Maybe (Maybe FileTransaction) -> TopLineChangeData
- Penny.Lincoln.Transaction: UnbalancedError :: Error
- Penny.Lincoln.Transaction: boxMeta :: Box m -> m
- Penny.Lincoln.Transaction: boxPostFam :: Box m -> PostFam
- Penny.Lincoln.Transaction: changeTransaction :: Family TopLineChangeData PostingChangeData -> Transaction -> Transaction
- Penny.Lincoln.Transaction: data Box m
- Penny.Lincoln.Transaction: data Error
- Penny.Lincoln.Transaction: data Inferred
- Penny.Lincoln.Transaction: data PostFam
- Penny.Lincoln.Transaction: data Posting
- Penny.Lincoln.Transaction: data PostingChangeData
- Penny.Lincoln.Transaction: data RTransaction
- Penny.Lincoln.Transaction: data TopLine
- Penny.Lincoln.Transaction: data TopLineChangeData
- Penny.Lincoln.Transaction: data Transaction
- Penny.Lincoln.Transaction: emptyPostingChangeData :: PostingChangeData
- Penny.Lincoln.Transaction: emptyTopLineChangeData :: TopLineChangeData
- Penny.Lincoln.Transaction: instance Eq Error
- Penny.Lincoln.Transaction: instance Eq Inferred
- Penny.Lincoln.Transaction: instance Eq Posting
- Penny.Lincoln.Transaction: instance Eq TopLine
- Penny.Lincoln.Transaction: instance Eq Transaction
- Penny.Lincoln.Transaction: instance Functor Box
- Penny.Lincoln.Transaction: instance Show Error
- Penny.Lincoln.Transaction: instance Show Inferred
- Penny.Lincoln.Transaction: instance Show PostFam
- Penny.Lincoln.Transaction: instance Show Posting
- Penny.Lincoln.Transaction: instance Show PostingChangeData
- Penny.Lincoln.Transaction: instance Show RTransaction
- Penny.Lincoln.Transaction: instance Show TopLine
- Penny.Lincoln.Transaction: instance Show TopLineChangeData
- Penny.Lincoln.Transaction: instance Show Transaction
- Penny.Lincoln.Transaction: instance Show m => Show (Box m)
- Penny.Lincoln.Transaction: pAccount :: Posting -> Account
- Penny.Lincoln.Transaction: pEntry :: Posting -> Entry
- Penny.Lincoln.Transaction: pFilePosting :: Posting -> Maybe FilePosting
- Penny.Lincoln.Transaction: pFlag :: Posting -> Maybe Flag
- Penny.Lincoln.Transaction: pGlobalPosting :: Posting -> Maybe GlobalPosting
- Penny.Lincoln.Transaction: pInferred :: Posting -> Inferred
- Penny.Lincoln.Transaction: pMemo :: Posting -> Maybe Memo
- Penny.Lincoln.Transaction: pNumber :: Posting -> Maybe Number
- Penny.Lincoln.Transaction: pPayee :: Posting -> Maybe Payee
- Penny.Lincoln.Transaction: pPostingLine :: Posting -> Maybe PostingLine
- Penny.Lincoln.Transaction: pTags :: Posting -> Tags
- Penny.Lincoln.Transaction: pcAccount :: PostingChangeData -> Maybe Account
- Penny.Lincoln.Transaction: pcFilePosting :: PostingChangeData -> Maybe (Maybe FilePosting)
- Penny.Lincoln.Transaction: pcFlag :: PostingChangeData -> Maybe (Maybe Flag)
- Penny.Lincoln.Transaction: pcGlobalPosting :: PostingChangeData -> Maybe (Maybe GlobalPosting)
- Penny.Lincoln.Transaction: pcMemo :: PostingChangeData -> Maybe (Maybe Memo)
- Penny.Lincoln.Transaction: pcNumber :: PostingChangeData -> Maybe (Maybe Number)
- Penny.Lincoln.Transaction: pcPayee :: PostingChangeData -> Maybe (Maybe Payee)
- Penny.Lincoln.Transaction: pcPostingLine :: PostingChangeData -> Maybe (Maybe PostingLine)
- Penny.Lincoln.Transaction: pcSide :: PostingChangeData -> Maybe (Maybe Side)
- Penny.Lincoln.Transaction: pcSpaceBetween :: PostingChangeData -> Maybe (Maybe SpaceBetween)
- Penny.Lincoln.Transaction: pcTags :: PostingChangeData -> Maybe Tags
- Penny.Lincoln.Transaction: postFam :: Transaction -> [PostFam]
- Penny.Lincoln.Transaction: rTransaction :: RTransaction -> Transaction
- Penny.Lincoln.Transaction: rtCommodity :: RTransaction -> Commodity
- Penny.Lincoln.Transaction: rtDrCr :: RTransaction -> DrCr
- Penny.Lincoln.Transaction: rtIPosting :: RTransaction -> IPosting
- Penny.Lincoln.Transaction: rtMorePostings :: RTransaction -> [RPosting]
- Penny.Lincoln.Transaction: rtPosting :: RTransaction -> RPosting
- Penny.Lincoln.Transaction: rtSide :: RTransaction -> Maybe Side
- Penny.Lincoln.Transaction: rtSpaceBetween :: RTransaction -> Maybe SpaceBetween
- Penny.Lincoln.Transaction: rtTopLine :: RTransaction -> TopLine
- Penny.Lincoln.Transaction: tDateTime :: TopLine -> DateTime
- Penny.Lincoln.Transaction: tFileTransaction :: TopLine -> Maybe FileTransaction
- Penny.Lincoln.Transaction: tFilename :: TopLine -> Maybe Filename
- Penny.Lincoln.Transaction: tFlag :: TopLine -> Maybe Flag
- Penny.Lincoln.Transaction: tGlobalTransaction :: TopLine -> Maybe GlobalTransaction
- Penny.Lincoln.Transaction: tMemo :: TopLine -> Maybe Memo
- Penny.Lincoln.Transaction: tNumber :: TopLine -> Maybe Number
- Penny.Lincoln.Transaction: tPayee :: TopLine -> Maybe Payee
- Penny.Lincoln.Transaction: tTopLineLine :: TopLine -> Maybe TopLineLine
- Penny.Lincoln.Transaction: tTopMemoLine :: TopLine -> Maybe TopMemoLine
- Penny.Lincoln.Transaction: tcDateTime :: TopLineChangeData -> Maybe DateTime
- Penny.Lincoln.Transaction: tcFileTransaction :: TopLineChangeData -> Maybe (Maybe FileTransaction)
- Penny.Lincoln.Transaction: tcFilename :: TopLineChangeData -> Maybe (Maybe Filename)
- Penny.Lincoln.Transaction: tcFlag :: TopLineChangeData -> Maybe (Maybe Flag)
- Penny.Lincoln.Transaction: tcGlobalTransaction :: TopLineChangeData -> Maybe (Maybe GlobalTransaction)
- Penny.Lincoln.Transaction: tcMemo :: TopLineChangeData -> Maybe (Maybe Memo)
- Penny.Lincoln.Transaction: tcNumber :: TopLineChangeData -> Maybe (Maybe Number)
- Penny.Lincoln.Transaction: tcPayee :: TopLineChangeData -> Maybe (Maybe Payee)
- Penny.Lincoln.Transaction: tcTopLineLine :: TopLineChangeData -> Maybe (Maybe TopLineLine)
- Penny.Lincoln.Transaction: tcTopMemoLine :: TopLineChangeData -> Maybe (Maybe TopMemoLine)
- Penny.Lincoln.Transaction: toUnverified :: Transaction -> Family TopLine Posting
- Penny.Lincoln.Transaction: transaction :: Family TopLine Posting -> Exceptional Error Transaction
- Penny.Lincoln.Transaction: unPostFam :: PostFam -> Child TopLine Posting
- Penny.Lincoln.Transaction: unTransaction :: Transaction -> Family TopLine Posting
- Penny.Lincoln.Transaction.Unverified: IPosting :: Maybe Payee -> Maybe Number -> Maybe Flag -> Account -> Tags -> Maybe Memo -> Maybe PostingLine -> Maybe GlobalPosting -> Maybe FilePosting -> IPosting
- Penny.Lincoln.Transaction.Unverified: Posting :: Maybe Payee -> Maybe Number -> Maybe Flag -> Account -> Tags -> Maybe Entry -> Maybe Memo -> Maybe PostingLine -> Maybe GlobalPosting -> Maybe FilePosting -> Posting
- Penny.Lincoln.Transaction.Unverified: RPosting :: Maybe Payee -> Maybe Number -> Maybe Flag -> Account -> Tags -> Qty -> Maybe Memo -> Maybe PostingLine -> Maybe GlobalPosting -> Maybe FilePosting -> RPosting
- Penny.Lincoln.Transaction.Unverified: TopLine :: DateTime -> Maybe Flag -> Maybe Number -> Maybe Payee -> Maybe Memo -> Maybe TopLineLine -> Maybe TopMemoLine -> Maybe Filename -> Maybe GlobalTransaction -> Maybe FileTransaction -> TopLine
- Penny.Lincoln.Transaction.Unverified: data IPosting
- Penny.Lincoln.Transaction.Unverified: data Posting
- Penny.Lincoln.Transaction.Unverified: data RPosting
- Penny.Lincoln.Transaction.Unverified: data TopLine
- Penny.Lincoln.Transaction.Unverified: emptyIPosting :: Account -> IPosting
- Penny.Lincoln.Transaction.Unverified: emptyPosting :: Account -> Posting
- Penny.Lincoln.Transaction.Unverified: emptyRPosting :: Account -> Qty -> RPosting
- Penny.Lincoln.Transaction.Unverified: emptyTopLine :: DateTime -> TopLine
- Penny.Lincoln.Transaction.Unverified: iAccount :: IPosting -> Account
- Penny.Lincoln.Transaction.Unverified: iFilePosting :: IPosting -> Maybe FilePosting
- Penny.Lincoln.Transaction.Unverified: iFlag :: IPosting -> Maybe Flag
- Penny.Lincoln.Transaction.Unverified: iGlobalPosting :: IPosting -> Maybe GlobalPosting
- Penny.Lincoln.Transaction.Unverified: iMemo :: IPosting -> Maybe Memo
- Penny.Lincoln.Transaction.Unverified: iNumber :: IPosting -> Maybe Number
- Penny.Lincoln.Transaction.Unverified: iPayee :: IPosting -> Maybe Payee
- Penny.Lincoln.Transaction.Unverified: iPostingLine :: IPosting -> Maybe PostingLine
- Penny.Lincoln.Transaction.Unverified: iTags :: IPosting -> Tags
- Penny.Lincoln.Transaction.Unverified: instance Eq IPosting
- Penny.Lincoln.Transaction.Unverified: instance Eq Posting
- Penny.Lincoln.Transaction.Unverified: instance Eq RPosting
- Penny.Lincoln.Transaction.Unverified: instance Eq TopLine
- Penny.Lincoln.Transaction.Unverified: instance Show IPosting
- Penny.Lincoln.Transaction.Unverified: instance Show Posting
- Penny.Lincoln.Transaction.Unverified: instance Show RPosting
- Penny.Lincoln.Transaction.Unverified: instance Show TopLine
- Penny.Lincoln.Transaction.Unverified: pAccount :: Posting -> Account
- Penny.Lincoln.Transaction.Unverified: pEntry :: Posting -> Maybe Entry
- Penny.Lincoln.Transaction.Unverified: pFilePosting :: Posting -> Maybe FilePosting
- Penny.Lincoln.Transaction.Unverified: pFlag :: Posting -> Maybe Flag
- Penny.Lincoln.Transaction.Unverified: pGlobalPosting :: Posting -> Maybe GlobalPosting
- Penny.Lincoln.Transaction.Unverified: pMemo :: Posting -> Maybe Memo
- Penny.Lincoln.Transaction.Unverified: pNumber :: Posting -> Maybe Number
- Penny.Lincoln.Transaction.Unverified: pPayee :: Posting -> Maybe Payee
- Penny.Lincoln.Transaction.Unverified: pPostingLine :: Posting -> Maybe PostingLine
- Penny.Lincoln.Transaction.Unverified: pTags :: Posting -> Tags
- Penny.Lincoln.Transaction.Unverified: rAccount :: RPosting -> Account
- Penny.Lincoln.Transaction.Unverified: rFilePosting :: RPosting -> Maybe FilePosting
- Penny.Lincoln.Transaction.Unverified: rFlag :: RPosting -> Maybe Flag
- Penny.Lincoln.Transaction.Unverified: rGlobalPosting :: RPosting -> Maybe GlobalPosting
- Penny.Lincoln.Transaction.Unverified: rMemo :: RPosting -> Maybe Memo
- Penny.Lincoln.Transaction.Unverified: rNumber :: RPosting -> Maybe Number
- Penny.Lincoln.Transaction.Unverified: rPayee :: RPosting -> Maybe Payee
- Penny.Lincoln.Transaction.Unverified: rPostingLine :: RPosting -> Maybe PostingLine
- Penny.Lincoln.Transaction.Unverified: rQty :: RPosting -> Qty
- Penny.Lincoln.Transaction.Unverified: rTags :: RPosting -> Tags
- Penny.Lincoln.Transaction.Unverified: tDateTime :: TopLine -> DateTime
- Penny.Lincoln.Transaction.Unverified: tFileTransaction :: TopLine -> Maybe FileTransaction
- Penny.Lincoln.Transaction.Unverified: tFilename :: TopLine -> Maybe Filename
- Penny.Lincoln.Transaction.Unverified: tFlag :: TopLine -> Maybe Flag
- Penny.Lincoln.Transaction.Unverified: tGlobalTransaction :: TopLine -> Maybe GlobalTransaction
- Penny.Lincoln.Transaction.Unverified: tMemo :: TopLine -> Maybe Memo
- Penny.Lincoln.Transaction.Unverified: tNumber :: TopLine -> Maybe Number
- Penny.Lincoln.Transaction.Unverified: tPayee :: TopLine -> Maybe Payee
- Penny.Lincoln.Transaction.Unverified: tTopLineLine :: TopLine -> Maybe TopLineLine
- Penny.Lincoln.Transaction.Unverified: tTopMemoLine :: TopLine -> Maybe TopMemoLine
+ Penny.Brenner: fitAcctDesc :: FitAcct -> String
+ Penny.Brenner: fitAcctName :: FitAcct -> String
+ Penny.Brenner: ofxParser :: (ParserDesc, ParserFn)
+ Penny.Brenner.Info: mode :: Mode (Maybe ConfigLocation -> Config -> IO ())
+ Penny.Brenner.OFX: parser :: (ParserDesc, ParserFn)
+ Penny.Brenner.Types: ConfigLocation :: Text -> ConfigLocation
+ Penny.Brenner.Types: FitAcctDesc :: Text -> FitAcctDesc
+ Penny.Brenner.Types: FitAcctName :: Text -> FitAcctName
+ Penny.Brenner.Types: ParserDesc :: Text -> ParserDesc
+ Penny.Brenner.Types: fitAcctDesc :: FitAcct -> FitAcctDesc
+ Penny.Brenner.Types: fitAcctName :: FitAcct -> FitAcctName
+ Penny.Brenner.Types: instance Eq ConfigLocation
+ Penny.Brenner.Types: instance Eq FitAcctDesc
+ Penny.Brenner.Types: instance Eq FitAcctName
+ Penny.Brenner.Types: instance Eq ParserDesc
+ Penny.Brenner.Types: instance HasText ConfigLocation
+ Penny.Brenner.Types: instance HasText Currency
+ Penny.Brenner.Types: instance HasText DbLocation
+ Penny.Brenner.Types: instance HasText FitAcctDesc
+ Penny.Brenner.Types: instance HasText FitAcctName
+ Penny.Brenner.Types: instance HasText ParserDesc
+ Penny.Brenner.Types: instance HasTextList DefaultAcct
+ Penny.Brenner.Types: instance HasTextList PennyAcct
+ Penny.Brenner.Types: instance Show ConfigLocation
+ Penny.Brenner.Types: instance Show FitAcctDesc
+ Penny.Brenner.Types: instance Show FitAcctName
+ Penny.Brenner.Types: instance Show ParserDesc
+ Penny.Brenner.Types: newtype ConfigLocation
+ Penny.Brenner.Types: newtype FitAcctDesc
+ Penny.Brenner.Types: newtype FitAcctName
+ Penny.Brenner.Types: newtype ParserDesc
+ Penny.Brenner.Types: type ParserFn = FitFileLocation -> IO (Exceptional String [Posting])
+ Penny.Brenner.Types: unConfigLocation :: ConfigLocation -> Text
+ Penny.Brenner.Types: unFitAcctDesc :: FitAcctDesc -> Text
+ Penny.Brenner.Types: unFitAcctName :: FitAcctName -> Text
+ Penny.Brenner.Types: unParserDesc :: ParserDesc -> Text
+ Penny.Brenner.Util: errExit :: String -> IO a
+ Penny.Brenner.Util: getFitAcct :: Maybe FitAcct -> IO FitAcct
+ Penny.Cabin.Meta: VisibleNum :: Serial -> VisibleNum
+ Penny.Cabin.Meta: newtype VisibleNum
+ Penny.Cabin.Posts.Parser: type Error = Text
+ Penny.Copper: item :: GroupSpecs -> S4 (TopLineCore, Ents PostingCore) PricePoint Comment BlankLine -> Maybe Text
+ Penny.Copper.Interface: BlankLine :: BlankLine
+ Penny.Copper.Interface: Comment :: Text -> Comment
+ Penny.Copper.Interface: ParsedTopLine :: DateTime -> Maybe Number -> Maybe Flag -> Maybe Payee -> Maybe (Memo, TopMemoLine) -> TopLineLine -> ParsedTopLine
+ Penny.Copper.Interface: data BlankLine
+ Penny.Copper.Interface: data ParsedTopLine
+ Penny.Copper.Interface: instance Binary BlankLine
+ Penny.Copper.Interface: instance Binary Comment
+ Penny.Copper.Interface: instance Binary ParsedTopLine
+ Penny.Copper.Interface: instance Constructor C1_0BlankLine
+ Penny.Copper.Interface: instance Constructor C1_0ParsedTopLine
+ Penny.Copper.Interface: instance Datatype D1BlankLine
+ Penny.Copper.Interface: instance Datatype D1ParsedTopLine
+ Penny.Copper.Interface: instance Eq BlankLine
+ Penny.Copper.Interface: instance Eq Comment
+ Penny.Copper.Interface: instance Generic BlankLine
+ Penny.Copper.Interface: instance Generic ParsedTopLine
+ Penny.Copper.Interface: instance Selector S1_0_0ParsedTopLine
+ Penny.Copper.Interface: instance Selector S1_0_1ParsedTopLine
+ Penny.Copper.Interface: instance Selector S1_0_2ParsedTopLine
+ Penny.Copper.Interface: instance Selector S1_0_3ParsedTopLine
+ Penny.Copper.Interface: instance Selector S1_0_4ParsedTopLine
+ Penny.Copper.Interface: instance Selector S1_0_5ParsedTopLine
+ Penny.Copper.Interface: instance Show BlankLine
+ Penny.Copper.Interface: instance Show Comment
+ Penny.Copper.Interface: instance Show ParsedTopLine
+ Penny.Copper.Interface: newtype Comment
+ Penny.Copper.Interface: ptlDateTime :: ParsedTopLine -> DateTime
+ Penny.Copper.Interface: ptlFlag :: ParsedTopLine -> Maybe Flag
+ Penny.Copper.Interface: ptlMemo :: ParsedTopLine -> Maybe (Memo, TopMemoLine)
+ Penny.Copper.Interface: ptlNumber :: ParsedTopLine -> Maybe Number
+ Penny.Copper.Interface: ptlPayee :: ParsedTopLine -> Maybe Payee
+ Penny.Copper.Interface: ptlTopLineLine :: ParsedTopLine -> TopLineLine
+ Penny.Copper.Interface: stripMeta :: LedgerItem -> S4 (TopLineCore, Ents PostingCore) PricePoint Comment BlankLine
+ Penny.Copper.Interface: toTopLineCore :: ParsedTopLine -> TopLineCore
+ Penny.Copper.Interface: type LedgerItem = S4 Transaction PricePoint Comment BlankLine
+ Penny.Copper.Interface: type ParsedItem = S4 ParsedTxn PricePoint Comment BlankLine
+ Penny.Copper.Interface: type ParsedTxn = (ParsedTopLine, Ents (PostingCore, PostingLine))
+ Penny.Copper.Interface: type Parser = String -> IO (Filename, [ParsedItem])
+ Penny.Copper.Interface: unComment :: Comment -> Text
+ Penny.Copper.Parsec: getFileContentsStdin :: String -> IO (Filename, Text)
+ Penny.Copper.Parsec: parse :: String -> IO (Filename, [ParsedItem])
+ Penny.Liberty: output :: OptSpec (Text -> IO ())
+ Penny.Liberty: processOutput :: [Text -> IO ()] -> Text -> IO ()
+ Penny.Lincoln.Balance: colDrCr :: Column -> DrCr
+ Penny.Lincoln.Balance: colQty :: Column -> Qty
+ Penny.Lincoln.Bits: PostingCore :: Maybe Payee -> Maybe Number -> Maybe Flag -> Account -> Tags -> Maybe Memo -> Maybe Side -> Maybe SpaceBetween -> PostingCore
+ Penny.Lincoln.Bits: PostingData :: PostingCore -> Maybe PostingFileMeta -> Maybe GlobalPosting -> PostingData
+ Penny.Lincoln.Bits: PostingFileMeta :: PostingLine -> FilePosting -> PostingFileMeta
+ Penny.Lincoln.Bits: TopLineCore :: DateTime -> Maybe Number -> Maybe Flag -> Maybe Payee -> Maybe Memo -> TopLineCore
+ Penny.Lincoln.Bits: TopLineData :: TopLineCore -> Maybe TopLineFileMeta -> Maybe GlobalTransaction -> TopLineData
+ Penny.Lincoln.Bits: TopLineFileMeta :: Filename -> TopLineLine -> Maybe TopMemoLine -> FileTransaction -> TopLineFileMeta
+ Penny.Lincoln.Bits: data PostingCore
+ Penny.Lincoln.Bits: data PostingData
+ Penny.Lincoln.Bits: data PostingFileMeta
+ Penny.Lincoln.Bits: data TopLineCore
+ Penny.Lincoln.Bits: data TopLineData
+ Penny.Lincoln.Bits: data TopLineFileMeta
+ Penny.Lincoln.Bits: emptyPostingCore :: Account -> PostingCore
+ Penny.Lincoln.Bits: emptyPostingData :: Account -> PostingData
+ Penny.Lincoln.Bits: emptyTopLineCore :: DateTime -> TopLineCore
+ Penny.Lincoln.Bits: emptyTopLineData :: DateTime -> TopLineData
+ Penny.Lincoln.Bits: instance Binary PostingCore
+ Penny.Lincoln.Bits: instance Binary PostingData
+ Penny.Lincoln.Bits: instance Binary PostingFileMeta
+ Penny.Lincoln.Bits: instance Binary PricePoint
+ Penny.Lincoln.Bits: instance Binary TopLineCore
+ Penny.Lincoln.Bits: instance Binary TopLineData
+ Penny.Lincoln.Bits: instance Binary TopLineFileMeta
+ Penny.Lincoln.Bits: instance Constructor C1_0PostingCore
+ Penny.Lincoln.Bits: instance Constructor C1_0PostingData
+ Penny.Lincoln.Bits: instance Constructor C1_0PostingFileMeta
+ Penny.Lincoln.Bits: instance Constructor C1_0PricePoint
+ Penny.Lincoln.Bits: instance Constructor C1_0TopLineCore
+ Penny.Lincoln.Bits: instance Constructor C1_0TopLineData
+ Penny.Lincoln.Bits: instance Constructor C1_0TopLineFileMeta
+ Penny.Lincoln.Bits: instance Datatype D1PostingCore
+ Penny.Lincoln.Bits: instance Datatype D1PostingData
+ Penny.Lincoln.Bits: instance Datatype D1PostingFileMeta
+ Penny.Lincoln.Bits: instance Datatype D1PricePoint
+ Penny.Lincoln.Bits: instance Datatype D1TopLineCore
+ Penny.Lincoln.Bits: instance Datatype D1TopLineData
+ Penny.Lincoln.Bits: instance Datatype D1TopLineFileMeta
+ Penny.Lincoln.Bits: instance Eq PostingCore
+ Penny.Lincoln.Bits: instance Eq PostingData
+ Penny.Lincoln.Bits: instance Eq PostingFileMeta
+ Penny.Lincoln.Bits: instance Eq TopLineCore
+ Penny.Lincoln.Bits: instance Eq TopLineData
+ Penny.Lincoln.Bits: instance Eq TopLineFileMeta
+ Penny.Lincoln.Bits: instance Equivalent PostingCore
+ Penny.Lincoln.Bits: instance Equivalent PricePoint
+ Penny.Lincoln.Bits: instance Equivalent TopLineCore
+ Penny.Lincoln.Bits: instance Generic PostingCore
+ Penny.Lincoln.Bits: instance Generic PostingData
+ Penny.Lincoln.Bits: instance Generic PostingFileMeta
+ Penny.Lincoln.Bits: instance Generic PricePoint
+ Penny.Lincoln.Bits: instance Generic TopLineCore
+ Penny.Lincoln.Bits: instance Generic TopLineData
+ Penny.Lincoln.Bits: instance Generic TopLineFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_0PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_0PostingData
+ Penny.Lincoln.Bits: instance Selector S1_0_0PostingFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_0PricePoint
+ Penny.Lincoln.Bits: instance Selector S1_0_0TopLineCore
+ Penny.Lincoln.Bits: instance Selector S1_0_0TopLineData
+ Penny.Lincoln.Bits: instance Selector S1_0_0TopLineFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_1PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_1PostingData
+ Penny.Lincoln.Bits: instance Selector S1_0_1PostingFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_1PricePoint
+ Penny.Lincoln.Bits: instance Selector S1_0_1TopLineCore
+ Penny.Lincoln.Bits: instance Selector S1_0_1TopLineData
+ Penny.Lincoln.Bits: instance Selector S1_0_1TopLineFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_2PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_2PostingData
+ Penny.Lincoln.Bits: instance Selector S1_0_2PricePoint
+ Penny.Lincoln.Bits: instance Selector S1_0_2TopLineCore
+ Penny.Lincoln.Bits: instance Selector S1_0_2TopLineData
+ Penny.Lincoln.Bits: instance Selector S1_0_2TopLineFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_3PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_3PricePoint
+ Penny.Lincoln.Bits: instance Selector S1_0_3TopLineCore
+ Penny.Lincoln.Bits: instance Selector S1_0_3TopLineFileMeta
+ Penny.Lincoln.Bits: instance Selector S1_0_4PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_4PricePoint
+ Penny.Lincoln.Bits: instance Selector S1_0_4TopLineCore
+ Penny.Lincoln.Bits: instance Selector S1_0_5PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_6PostingCore
+ Penny.Lincoln.Bits: instance Selector S1_0_7PostingCore
+ Penny.Lincoln.Bits: instance Show PostingCore
+ Penny.Lincoln.Bits: instance Show PostingData
+ Penny.Lincoln.Bits: instance Show PostingFileMeta
+ Penny.Lincoln.Bits: instance Show TopLineCore
+ Penny.Lincoln.Bits: instance Show TopLineData
+ Penny.Lincoln.Bits: instance Show TopLineFileMeta
+ Penny.Lincoln.Bits: pAccount :: PostingCore -> Account
+ Penny.Lincoln.Bits: pFilePosting :: PostingFileMeta -> FilePosting
+ Penny.Lincoln.Bits: pFlag :: PostingCore -> Maybe Flag
+ Penny.Lincoln.Bits: pMemo :: PostingCore -> Maybe Memo
+ Penny.Lincoln.Bits: pNumber :: PostingCore -> Maybe Number
+ Penny.Lincoln.Bits: pPayee :: PostingCore -> Maybe Payee
+ Penny.Lincoln.Bits: pPostingLine :: PostingFileMeta -> PostingLine
+ Penny.Lincoln.Bits: pSide :: PostingCore -> Maybe Side
+ Penny.Lincoln.Bits: pSpaceBetween :: PostingCore -> Maybe SpaceBetween
+ Penny.Lincoln.Bits: pTags :: PostingCore -> Tags
+ Penny.Lincoln.Bits: pdCore :: PostingData -> PostingCore
+ Penny.Lincoln.Bits: pdFileMeta :: PostingData -> Maybe PostingFileMeta
+ Penny.Lincoln.Bits: pdGlobal :: PostingData -> Maybe GlobalPosting
+ Penny.Lincoln.Bits: tDateTime :: TopLineCore -> DateTime
+ Penny.Lincoln.Bits: tFileTransaction :: TopLineFileMeta -> FileTransaction
+ Penny.Lincoln.Bits: tFilename :: TopLineFileMeta -> Filename
+ Penny.Lincoln.Bits: tFlag :: TopLineCore -> Maybe Flag
+ Penny.Lincoln.Bits: tMemo :: TopLineCore -> Maybe Memo
+ Penny.Lincoln.Bits: tNumber :: TopLineCore -> Maybe Number
+ Penny.Lincoln.Bits: tPayee :: TopLineCore -> Maybe Payee
+ Penny.Lincoln.Bits: tTopLineLine :: TopLineFileMeta -> TopLineLine
+ Penny.Lincoln.Bits: tTopMemoLine :: TopLineFileMeta -> Maybe TopMemoLine
+ Penny.Lincoln.Bits: tlCore :: TopLineData -> TopLineCore
+ Penny.Lincoln.Bits: tlFileMeta :: TopLineData -> Maybe TopLineFileMeta
+ Penny.Lincoln.Bits: tlGlobal :: TopLineData -> Maybe GlobalTransaction
+ Penny.Lincoln.Bits.DateTime: instance Binary DateTime
+ Penny.Lincoln.Bits.DateTime: instance Binary Hours
+ Penny.Lincoln.Bits.DateTime: instance Binary Minutes
+ Penny.Lincoln.Bits.DateTime: instance Binary Seconds
+ Penny.Lincoln.Bits.DateTime: instance Binary TimeZoneOffset
+ Penny.Lincoln.Bits.DateTime: instance Constructor C1_0Hours
+ Penny.Lincoln.Bits.DateTime: instance Constructor C1_0Minutes
+ Penny.Lincoln.Bits.DateTime: instance Constructor C1_0Seconds
+ Penny.Lincoln.Bits.DateTime: instance Constructor C1_0TimeZoneOffset
+ Penny.Lincoln.Bits.DateTime: instance Datatype D1Hours
+ Penny.Lincoln.Bits.DateTime: instance Datatype D1Minutes
+ Penny.Lincoln.Bits.DateTime: instance Datatype D1Seconds
+ Penny.Lincoln.Bits.DateTime: instance Datatype D1TimeZoneOffset
+ Penny.Lincoln.Bits.DateTime: instance Equivalent DateTime
+ Penny.Lincoln.Bits.DateTime: instance Generic Hours
+ Penny.Lincoln.Bits.DateTime: instance Generic Minutes
+ Penny.Lincoln.Bits.DateTime: instance Generic Seconds
+ Penny.Lincoln.Bits.DateTime: instance Generic TimeZoneOffset
+ Penny.Lincoln.Bits.DateTime: instance Selector S1_0_0Hours
+ Penny.Lincoln.Bits.DateTime: instance Selector S1_0_0Minutes
+ Penny.Lincoln.Bits.DateTime: instance Selector S1_0_0Seconds
+ Penny.Lincoln.Bits.DateTime: instance Selector S1_0_0TimeZoneOffset
+ Penny.Lincoln.Bits.Open: instance Binary Account
+ Penny.Lincoln.Bits.Open: instance Binary Amount
+ Penny.Lincoln.Bits.Open: instance Binary Commodity
+ Penny.Lincoln.Bits.Open: instance Binary DrCr
+ Penny.Lincoln.Bits.Open: instance Binary Entry
+ Penny.Lincoln.Bits.Open: instance Binary FilePosting
+ Penny.Lincoln.Bits.Open: instance Binary FileTransaction
+ Penny.Lincoln.Bits.Open: instance Binary Filename
+ Penny.Lincoln.Bits.Open: instance Binary Flag
+ Penny.Lincoln.Bits.Open: instance Binary GlobalPosting
+ Penny.Lincoln.Bits.Open: instance Binary GlobalTransaction
+ Penny.Lincoln.Bits.Open: instance Binary Memo
+ Penny.Lincoln.Bits.Open: instance Binary Number
+ Penny.Lincoln.Bits.Open: instance Binary Payee
+ Penny.Lincoln.Bits.Open: instance Binary PostingLine
+ Penny.Lincoln.Bits.Open: instance Binary PriceLine
+ Penny.Lincoln.Bits.Open: instance Binary Side
+ Penny.Lincoln.Bits.Open: instance Binary SpaceBetween
+ Penny.Lincoln.Bits.Open: instance Binary SubAccount
+ Penny.Lincoln.Bits.Open: instance Binary Tag
+ Penny.Lincoln.Bits.Open: instance Binary Tags
+ Penny.Lincoln.Bits.Open: instance Binary TopLineLine
+ Penny.Lincoln.Bits.Open: instance Binary TopMemoLine
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0Account
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0Amount
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0DrCr
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0Entry
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0FilePosting
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0FileTransaction
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0GlobalPosting
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0GlobalTransaction
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0PostingLine
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0PriceLine
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0Side
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0SpaceBetween
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0Tags
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0TopLineLine
+ Penny.Lincoln.Bits.Open: instance Constructor C1_0TopMemoLine
+ Penny.Lincoln.Bits.Open: instance Constructor C1_1DrCr
+ Penny.Lincoln.Bits.Open: instance Constructor C1_1Side
+ Penny.Lincoln.Bits.Open: instance Constructor C1_1SpaceBetween
+ Penny.Lincoln.Bits.Open: instance Datatype D1Account
+ Penny.Lincoln.Bits.Open: instance Datatype D1Amount
+ Penny.Lincoln.Bits.Open: instance Datatype D1DrCr
+ Penny.Lincoln.Bits.Open: instance Datatype D1Entry
+ Penny.Lincoln.Bits.Open: instance Datatype D1FilePosting
+ Penny.Lincoln.Bits.Open: instance Datatype D1FileTransaction
+ Penny.Lincoln.Bits.Open: instance Datatype D1GlobalPosting
+ Penny.Lincoln.Bits.Open: instance Datatype D1GlobalTransaction
+ Penny.Lincoln.Bits.Open: instance Datatype D1PostingLine
+ Penny.Lincoln.Bits.Open: instance Datatype D1PriceLine
+ Penny.Lincoln.Bits.Open: instance Datatype D1Side
+ Penny.Lincoln.Bits.Open: instance Datatype D1SpaceBetween
+ Penny.Lincoln.Bits.Open: instance Datatype D1Tags
+ Penny.Lincoln.Bits.Open: instance Datatype D1TopLineLine
+ Penny.Lincoln.Bits.Open: instance Datatype D1TopMemoLine
+ Penny.Lincoln.Bits.Open: instance Equivalent Amount
+ Penny.Lincoln.Bits.Open: instance Equivalent Entry
+ Penny.Lincoln.Bits.Open: instance Equivalent Tags
+ Penny.Lincoln.Bits.Open: instance Generic Account
+ Penny.Lincoln.Bits.Open: instance Generic Amount
+ Penny.Lincoln.Bits.Open: instance Generic DrCr
+ Penny.Lincoln.Bits.Open: instance Generic Entry
+ Penny.Lincoln.Bits.Open: instance Generic FilePosting
+ Penny.Lincoln.Bits.Open: instance Generic FileTransaction
+ Penny.Lincoln.Bits.Open: instance Generic GlobalPosting
+ Penny.Lincoln.Bits.Open: instance Generic GlobalTransaction
+ Penny.Lincoln.Bits.Open: instance Generic PostingLine
+ Penny.Lincoln.Bits.Open: instance Generic PriceLine
+ Penny.Lincoln.Bits.Open: instance Generic Side
+ Penny.Lincoln.Bits.Open: instance Generic SpaceBetween
+ Penny.Lincoln.Bits.Open: instance Generic Tags
+ Penny.Lincoln.Bits.Open: instance Generic TopLineLine
+ Penny.Lincoln.Bits.Open: instance Generic TopMemoLine
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0Account
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0Amount
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0Entry
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0FilePosting
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0FileTransaction
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0GlobalPosting
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0GlobalTransaction
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0PostingLine
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0PriceLine
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0Tags
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0TopLineLine
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_0TopMemoLine
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_1Amount
+ Penny.Lincoln.Bits.Open: instance Selector S1_0_1Entry
+ Penny.Lincoln.Bits.Price: instance Binary CountPerUnit
+ Penny.Lincoln.Bits.Price: instance Binary From
+ Penny.Lincoln.Bits.Price: instance Binary Price
+ Penny.Lincoln.Bits.Price: instance Binary To
+ Penny.Lincoln.Bits.Price: instance Constructor C1_0CountPerUnit
+ Penny.Lincoln.Bits.Price: instance Constructor C1_0From
+ Penny.Lincoln.Bits.Price: instance Constructor C1_0Price
+ Penny.Lincoln.Bits.Price: instance Constructor C1_0To
+ Penny.Lincoln.Bits.Price: instance Datatype D1CountPerUnit
+ Penny.Lincoln.Bits.Price: instance Datatype D1From
+ Penny.Lincoln.Bits.Price: instance Datatype D1Price
+ Penny.Lincoln.Bits.Price: instance Datatype D1To
+ Penny.Lincoln.Bits.Price: instance Equivalent CountPerUnit
+ Penny.Lincoln.Bits.Price: instance Equivalent Price
+ Penny.Lincoln.Bits.Price: instance Generic CountPerUnit
+ Penny.Lincoln.Bits.Price: instance Generic From
+ Penny.Lincoln.Bits.Price: instance Generic Price
+ Penny.Lincoln.Bits.Price: instance Generic To
+ Penny.Lincoln.Bits.Price: instance Selector S1_0_0CountPerUnit
+ Penny.Lincoln.Bits.Price: instance Selector S1_0_0From
+ Penny.Lincoln.Bits.Price: instance Selector S1_0_0Price
+ Penny.Lincoln.Bits.Price: instance Selector S1_0_0To
+ Penny.Lincoln.Bits.Price: instance Selector S1_0_1Price
+ Penny.Lincoln.Bits.Price: instance Selector S1_0_2Price
+ Penny.Lincoln.Bits.Qty: compareQty :: Qty -> Qty -> Ordering
+ Penny.Lincoln.Bits.Qty: instance Binary Qty
+ Penny.Lincoln.Bits.Qty: instance Constructor C1_0Qty
+ Penny.Lincoln.Bits.Qty: instance Datatype D1Qty
+ Penny.Lincoln.Bits.Qty: instance Equivalent Qty
+ Penny.Lincoln.Bits.Qty: instance Generic Qty
+ Penny.Lincoln.Bits.Qty: instance Selector S1_0_0Qty
+ Penny.Lincoln.Bits.Qty: instance Selector S1_0_1Qty
+ Penny.Lincoln.Bits.Qty: prettyShowQty :: Qty -> String
+ Penny.Lincoln.Bits.Qty: qtyOne :: Qty
+ Penny.Lincoln.Ents: Inferred :: Inferred
+ Penny.Lincoln.Ents: NotInferred :: Inferred
+ Penny.Lincoln.Ents: Posting :: (TopLineData, Ents PostingData) -> Posting
+ Penny.Lincoln.Ents: Transaction :: (TopLineData, Ents PostingData) -> Transaction
+ Penny.Lincoln.Ents: data Ent m
+ Penny.Lincoln.Ents: data Ents m
+ Penny.Lincoln.Ents: data Inferred
+ Penny.Lincoln.Ents: entry :: Ent m -> Entry
+ Penny.Lincoln.Ents: ents :: [(Maybe Entry, m)] -> Maybe (Ents m)
+ Penny.Lincoln.Ents: headEnt :: Ents m -> Ent m
+ Penny.Lincoln.Ents: inferred :: Ent m -> Inferred
+ Penny.Lincoln.Ents: instance Binary Inferred
+ Penny.Lincoln.Ents: instance Binary m => Binary (Ent m)
+ Penny.Lincoln.Ents: instance Binary m => Binary (Ents m)
+ Penny.Lincoln.Ents: instance Constructor C1_0Ent
+ Penny.Lincoln.Ents: instance Constructor C1_0Ents
+ Penny.Lincoln.Ents: instance Constructor C1_0Inferred
+ Penny.Lincoln.Ents: instance Constructor C1_1Inferred
+ Penny.Lincoln.Ents: instance Datatype D1Ent
+ Penny.Lincoln.Ents: instance Datatype D1Ents
+ Penny.Lincoln.Ents: instance Datatype D1Inferred
+ Penny.Lincoln.Ents: instance Eq Inferred
+ Penny.Lincoln.Ents: instance Eq Posting
+ Penny.Lincoln.Ents: instance Eq Transaction
+ Penny.Lincoln.Ents: instance Eq m => Eq (Ent m)
+ Penny.Lincoln.Ents: instance Eq m => Eq (Ents m)
+ Penny.Lincoln.Ents: instance Equivalent m => Equivalent (Ent m)
+ Penny.Lincoln.Ents: instance Equivalent m => Equivalent (Ents m)
+ Penny.Lincoln.Ents: instance Foldable Ents
+ Penny.Lincoln.Ents: instance Functor Ent
+ Penny.Lincoln.Ents: instance Functor Ents
+ Penny.Lincoln.Ents: instance Generic (Ent m)
+ Penny.Lincoln.Ents: instance Generic (Ents m)
+ Penny.Lincoln.Ents: instance Generic Inferred
+ Penny.Lincoln.Ents: instance Ord Inferred
+ Penny.Lincoln.Ents: instance Ord m => Ord (Ent m)
+ Penny.Lincoln.Ents: instance Ord m => Ord (Ents m)
+ Penny.Lincoln.Ents: instance Selector S1_0_0Ent
+ Penny.Lincoln.Ents: instance Selector S1_0_0Ents
+ Penny.Lincoln.Ents: instance Selector S1_0_1Ent
+ Penny.Lincoln.Ents: instance Selector S1_0_2Ent
+ Penny.Lincoln.Ents: instance Show Inferred
+ Penny.Lincoln.Ents: instance Show Posting
+ Penny.Lincoln.Ents: instance Show Transaction
+ Penny.Lincoln.Ents: instance Show m => Show (Ent m)
+ Penny.Lincoln.Ents: instance Show m => Show (Ents m)
+ Penny.Lincoln.Ents: instance Traversable Ents
+ Penny.Lincoln.Ents: mapEnts :: (Ent a -> b) -> Ents a -> Ents b
+ Penny.Lincoln.Ents: meta :: Ent m -> m
+ Penny.Lincoln.Ents: newtype Posting
+ Penny.Lincoln.Ents: newtype Transaction
+ Penny.Lincoln.Ents: rEnts :: Commodity -> DrCr -> (Qty, m) -> [(Qty, m)] -> m -> Ents m
+ Penny.Lincoln.Ents: tailEnts :: Ents m -> (Ent m, [Ent m])
+ Penny.Lincoln.Ents: transactionToPostings :: Transaction -> [Posting]
+ Penny.Lincoln.Ents: traverseEnts :: Applicative f => (Ent a -> f b) -> Ents a -> f (Ents b)
+ Penny.Lincoln.Ents: tupleEnts :: Ents m -> (Ent m, Ent m, [Ent m])
+ Penny.Lincoln.Ents: unEnts :: Ents m -> [Ent m]
+ Penny.Lincoln.Ents: unPosting :: Posting -> (TopLineData, Ents PostingData)
+ Penny.Lincoln.Ents: unTransaction :: Transaction -> (TopLineData, Ents PostingData)
+ Penny.Lincoln.Ents: unrollSnd :: (a, [b]) -> [(a, b)]
+ Penny.Lincoln.Ents: views :: Ents m -> [Ents m]
+ Penny.Lincoln.Equivalent: (==~) :: Equivalent a => a -> a -> Bool
+ Penny.Lincoln.Equivalent: class Equivalent a
+ Penny.Lincoln.Equivalent: compareEv :: Equivalent a => a -> a -> Ordering
+ Penny.Lincoln.Equivalent: equivalent :: Equivalent a => a -> a -> Bool
+ Penny.Lincoln.Equivalent: instance (Equivalent a, Equivalent b) => Equivalent (a, b)
+ Penny.Lincoln.HasText: Delimited :: Text -> a -> Delimited a
+ Penny.Lincoln.HasText: data Delimited a
+ Penny.Lincoln.HasText: delimited :: Delimited a -> a
+ Penny.Lincoln.HasText: delimiter :: Delimited a -> Text
+ Penny.Lincoln.HasText: instance Eq a => Eq (Delimited a)
+ Penny.Lincoln.HasText: instance HasTextList a => HasText (Delimited a)
+ Penny.Lincoln.HasText: instance HasTextList a => HasTextList (Delimited a)
+ Penny.Lincoln.HasText: instance Show a => Show (Delimited a)
+ Penny.Lincoln.Predicates: backFilePosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates: backFileTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates: backGlobalPosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates: backGlobalTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates: fwdFilePosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates: fwdFileTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates: fwdGlobalPosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates: fwdGlobalTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates: serialPdct :: Text -> (a -> Maybe Int) -> Int -> Ordering -> Pdct a
+ Penny.Lincoln.Predicates: type MakeSerialPdct = Int -> Ordering -> Pdct Posting
+ Penny.Lincoln.Predicates.Siblings: backFilePosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: backFileTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: backGlobalPosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: backGlobalTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: fwdFilePosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: fwdFileTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: fwdGlobalPosting :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: fwdGlobalTransaction :: MakeSerialPdct
+ Penny.Lincoln.Predicates.Siblings: serialPdct :: Text -> ((TopLineData, Ent PostingData) -> Maybe Int) -> Int -> Ordering -> Pdct Posting
+ Penny.Lincoln.Predicates.Siblings: type MakeSerialPdct = Int -> Ordering -> Pdct Posting
+ Penny.Lincoln.PriceDb: convertAsOf :: PriceDb -> DateTime -> To -> Amount -> Exceptional PriceDbError Qty
+ Penny.Lincoln.Queries.Siblings: globalTransaction :: Posting -> [Maybe GlobalTransaction]
+ Penny.Lincoln.Serial: instance Binary Serial
+ Penny.Lincoln.Serial: instance Constructor C1_0Serial
+ Penny.Lincoln.Serial: instance Datatype D1Serial
+ Penny.Lincoln.Serial: instance Generic Serial
+ Penny.Lincoln.Serial: instance Selector S1_0_0Serial
+ Penny.Lincoln.Serial: instance Selector S1_0_1Serial
+ Penny.Lincoln.Serial: serialNestedItems :: Traversable f => (a -> Either b (f c, Serial -> c -> d, f d -> b)) -> [a] -> [b]
+ Penny.Lincoln.Serial: serialSomeItems :: (a -> Either b (Serial -> b)) -> [a] -> [b]
+ Penny.Shield: data Term
+ Penny.Steel.Sums: S3a :: a -> S3 a b c
+ Penny.Steel.Sums: S3b :: b -> S3 a b c
+ Penny.Steel.Sums: S3c :: c -> S3 a b c
+ Penny.Steel.Sums: S4a :: a -> S4 a b c d
+ Penny.Steel.Sums: S4b :: b -> S4 a b c d
+ Penny.Steel.Sums: S4c :: c -> S4 a b c d
+ Penny.Steel.Sums: S4d :: d -> S4 a b c d
+ Penny.Steel.Sums: caseS3 :: (a -> d) -> (b -> d) -> (c -> d) -> S3 a b c -> d
+ Penny.Steel.Sums: caseS4 :: (a -> e) -> (b -> e) -> (c -> e) -> (d -> e) -> S4 a b c d -> e
+ Penny.Steel.Sums: data S3 a b c
+ Penny.Steel.Sums: data S4 a b c d
+ Penny.Steel.Sums: instance (Binary a, Binary b, Binary c) => Binary (S3 a b c)
+ Penny.Steel.Sums: instance (Binary a, Binary b, Binary c, Binary d) => Binary (S4 a b c d)
+ Penny.Steel.Sums: instance (Eq a, Eq b, Eq c) => Eq (S3 a b c)
+ Penny.Steel.Sums: instance (Eq a, Eq b, Eq c, Eq d) => Eq (S4 a b c d)
+ Penny.Steel.Sums: instance (Ord a, Ord b, Ord c) => Ord (S3 a b c)
+ Penny.Steel.Sums: instance (Ord a, Ord b, Ord c, Ord d) => Ord (S4 a b c d)
+ Penny.Steel.Sums: instance (Show a, Show b, Show c) => Show (S3 a b c)
+ Penny.Steel.Sums: instance (Show a, Show b, Show c, Show d) => Show (S4 a b c d)
+ Penny.Steel.Sums: instance Constructor C1_0S3
+ Penny.Steel.Sums: instance Constructor C1_0S4
+ Penny.Steel.Sums: instance Constructor C1_1S3
+ Penny.Steel.Sums: instance Constructor C1_1S4
+ Penny.Steel.Sums: instance Constructor C1_2S3
+ Penny.Steel.Sums: instance Constructor C1_2S4
+ Penny.Steel.Sums: instance Constructor C1_3S4
+ Penny.Steel.Sums: instance Datatype D1S3
+ Penny.Steel.Sums: instance Datatype D1S4
+ Penny.Steel.Sums: instance Generic (S3 a b c)
+ Penny.Steel.Sums: instance Generic (S4 a b c d)
+ Penny.Steel.Sums: mapS3 :: (a -> a1) -> (b -> b1) -> (c -> c1) -> S3 a b c -> S3 a1 b1 c1
+ Penny.Steel.Sums: mapS3a :: Functor f => (a -> f a1) -> (b -> f b1) -> (c -> f c1) -> S3 a b c -> f (S3 a1 b1 c1)
+ Penny.Steel.Sums: mapS4 :: (a -> a1) -> (b -> b1) -> (c -> c1) -> (d -> d1) -> S4 a b c d -> S4 a1 b1 c1 d1
+ Penny.Steel.Sums: mapS4a :: Functor f => (a -> f a1) -> (b -> f b1) -> (c -> f c1) -> (d -> f d1) -> S4 a b c d -> f (S4 a1 b1 c1 d1)
+ Penny.Steel.Sums: partitionS3 :: [S3 a b c] -> ([a], [b], [c])
+ Penny.Steel.Sums: partitionS4 :: [S4 a b c d] -> ([a], [b], [c], [d])
- Penny: Defaults :: Bool -> Matcher -> Bool -> ExprDesc -> Maybe Scheme -> [Scheme] -> [(SortField, SortOrder)] -> (Commodity -> Qty -> Text) -> Bool -> SortOrder -> Bool -> Target -> SortOrder -> SortBy -> (Commodity -> Qty -> Text) -> Fields Bool -> Int -> Bool -> (Box -> Text) -> (Box -> Text) -> (Commodity -> Qty -> Text) -> Int -> Int -> Int -> Spacers Int -> Defaults
+ Penny: Defaults :: Bool -> Matcher -> Bool -> ExprDesc -> Maybe Scheme -> [Scheme] -> [(SortField, SortOrder)] -> (Commodity -> Qty -> Text) -> Bool -> SortOrder -> Bool -> Target -> SortOrder -> SortBy -> (Commodity -> Qty -> Text) -> Fields Bool -> Int -> Bool -> ((PostMeta, Posting) -> Text) -> ((PostMeta, Posting) -> Text) -> (Commodity -> Qty -> Text) -> Int -> Int -> Int -> Spacers Int -> Defaults
- Penny: postingsDateFormat :: Defaults -> Box -> Text
+ Penny: postingsDateFormat :: Defaults -> (PostMeta, Posting) -> Text
- Penny: postingsQtyFormat :: Defaults -> Box -> Text
+ Penny: postingsQtyFormat :: Defaults -> (PostMeta, Posting) -> Text
- Penny: qtyAsIs :: Box -> Text
+ Penny: qtyAsIs :: (PostMeta, Posting) -> Text
- Penny: yearMonthDay :: Box -> Text
+ Penny: yearMonthDay :: (PostMeta, Posting) -> Text
- Penny.Brenner: Config :: Maybe FitAcct -> [(String, FitAcct)] -> Config
+ Penny.Brenner: Config :: Maybe FitAcct -> [FitAcct] -> Config
- Penny.Brenner: FitAcct :: String -> String -> String -> String -> GroupSpecs -> Translator -> Side -> SpaceBetween -> (String, FitFileLocation -> IO (Exceptional String [Posting])) -> (Desc -> Payee -> Payee) -> FitAcct
+ Penny.Brenner: FitAcct :: String -> String -> String -> String -> String -> String -> GroupSpecs -> Translator -> Side -> SpaceBetween -> (ParserDesc, FitFileLocation -> IO (Exceptional String [Posting])) -> (Desc -> Payee -> Payee) -> FitAcct
- Penny.Brenner: moreFitAccts :: Config -> [(String, FitAcct)]
+ Penny.Brenner: moreFitAccts :: Config -> [FitAcct]
- Penny.Brenner: parser :: FitAcct -> (String, FitFileLocation -> IO (Exceptional String [Posting]))
+ Penny.Brenner: parser :: FitAcct -> (ParserDesc, FitFileLocation -> IO (Exceptional String [Posting]))
- Penny.Brenner.Clear: mode :: Maybe FitAcct -> Mode (IO ())
+ Penny.Brenner.Clear: mode :: Mode (Maybe FitAcct -> IO ())
- Penny.Brenner.Database: mode :: Maybe FitAcct -> Mode (IO ())
+ Penny.Brenner.Database: mode :: Mode (Maybe FitAcct -> IO ())
- Penny.Brenner.Import: mode :: Maybe FitAcct -> Mode (IO ())
+ Penny.Brenner.Import: mode :: Mode (Maybe FitAcct -> IO ())
- Penny.Brenner.Merge: mode :: Maybe FitAcct -> Mode (IO ())
+ Penny.Brenner.Merge: mode :: Mode (Maybe FitAcct -> IO ())
- Penny.Brenner.Print: mode :: Maybe FitAcct -> Mode (IO ())
+ Penny.Brenner.Print: mode :: Mode (Maybe FitAcct -> IO ())
- Penny.Brenner.Types: Config :: Maybe FitAcct -> [(Name, FitAcct)] -> Config
+ Penny.Brenner.Types: Config :: Maybe FitAcct -> [FitAcct] -> Config
- Penny.Brenner.Types: FitAcct :: DbLocation -> PennyAcct -> DefaultAcct -> Currency -> GroupSpecs -> Translator -> Side -> SpaceBetween -> (String, FitFileLocation -> IO (Exceptional String [Posting])) -> (Desc -> Payee -> Payee) -> FitAcct
+ Penny.Brenner.Types: FitAcct :: FitAcctName -> FitAcctDesc -> DbLocation -> PennyAcct -> DefaultAcct -> Currency -> GroupSpecs -> Translator -> Side -> SpaceBetween -> (ParserDesc, FitFileLocation -> IO (Exceptional String [Posting])) -> (Desc -> Payee -> Payee) -> FitAcct
- Penny.Brenner.Types: moreFitAccts :: Config -> [(Name, FitAcct)]
+ Penny.Brenner.Types: moreFitAccts :: Config -> [FitAcct]
- Penny.Brenner.Types: parser :: FitAcct -> (String, FitFileLocation -> IO (Exceptional String [Posting]))
+ Penny.Brenner.Types: parser :: FitAcct -> (ParserDesc, FitFileLocation -> IO (Exceptional String [Posting]))
- Penny.Cabin.Balance.Convert: report :: Opts -> [PricePoint] -> [Box a] -> Exceptional Text [Chunk]
+ Penny.Cabin.Balance.Convert: report :: Opts -> [PricePoint] -> [(a, Posting)] -> Exceptional Text [Chunk]
- Penny.Cabin.Balance.MultiCommodity: report :: Opts -> [Box a] -> [Chunk]
+ Penny.Cabin.Balance.MultiCommodity: report :: Opts -> [(a, Posting)] -> [Chunk]
- Penny.Cabin.Balance.Util: balances :: ShowZeroBalances -> [Box a] -> Forest (SubAccount, Balance)
+ Penny.Cabin.Balance.Util: balances :: ShowZeroBalances -> [(a, Posting)] -> Forest (SubAccount, Balance)
- Penny.Cabin.Balance.Util: boxesBalance :: [Box a] -> Balance
+ Penny.Cabin.Balance.Util: boxesBalance :: [(a, Posting)] -> Balance
- Penny.Cabin.Balance.Util: tieredPostings :: [Box a] -> Forest (SubAccount, [Box a])
+ Penny.Cabin.Balance.Util: tieredPostings :: [(a, Posting)] -> Forest (SubAccount, [(a, Posting)])
- Penny.Cabin.Interface: type MkReport = CaseSensitive -> (CaseSensitive -> Text -> Exceptional Text Matcher) -> Changers -> ExprDesc -> ([Transaction] -> [Box LibertyMeta]) -> Mode ParseResult
+ Penny.Cabin.Interface: type MkReport = CaseSensitive -> (CaseSensitive -> Text -> Exceptional Text Matcher) -> Changers -> ExprDesc -> ([Transaction] -> [(LibertyMeta, Posting)]) -> Mode ParseResult
- Penny.Cabin.Posts: ZincOpts :: Fields Bool -> ReportWidth -> ShowZeroBalances -> (Box -> Text) -> (Box -> Text) -> (Commodity -> Qty -> Text) -> SubAccountLength -> Alloc -> Alloc -> Spacers Int -> ZincOpts
+ Penny.Cabin.Posts: ZincOpts :: Fields Bool -> ReportWidth -> ShowZeroBalances -> ((PostMeta, Posting) -> Text) -> ((PostMeta, Posting) -> Text) -> (Commodity -> Qty -> Text) -> SubAccountLength -> Alloc -> Alloc -> Spacers Int -> ZincOpts
- Penny.Cabin.Posts: dateFormat :: ZincOpts -> Box -> Text
+ Penny.Cabin.Posts: dateFormat :: ZincOpts -> (PostMeta, Posting) -> Text
- Penny.Cabin.Posts: postsReport :: Changers -> ShowZeroBalances -> (Pdct (Box LibertyMeta)) -> [PostFilterFn] -> ChunkOpts -> [Box LibertyMeta] -> [Chunk]
+ Penny.Cabin.Posts: postsReport :: Changers -> ShowZeroBalances -> (Pdct (LibertyMeta, Posting)) -> [PostFilterFn] -> ChunkOpts -> [(LibertyMeta, Posting)] -> [Chunk]
- Penny.Cabin.Posts: qtyAsIs :: Box -> Text
+ Penny.Cabin.Posts: qtyAsIs :: (PostMeta, Posting) -> Text
- Penny.Cabin.Posts: qtyFormat :: ZincOpts -> Box -> Text
+ Penny.Cabin.Posts: qtyFormat :: ZincOpts -> (PostMeta, Posting) -> Text
- Penny.Cabin.Posts: yearMonthDay :: Box -> Text
+ Penny.Cabin.Posts: yearMonthDay :: (PostMeta, Posting) -> Text
- Penny.Cabin.Posts.Allocated: payeeAndAcct :: Changers -> AllocatedOpts -> [Box] -> Fields (Maybe ([ColumnSpec], Int))
+ Penny.Cabin.Posts.Allocated: payeeAndAcct :: Changers -> AllocatedOpts -> [(PostMeta, Posting)] -> Fields (Maybe ([ColumnSpec], Int))
- Penny.Cabin.Posts.BottomRows: bottomRows :: Changers -> BottomOpts -> [Box] -> Fields (Maybe [[Chunk]])
+ Penny.Cabin.Posts.BottomRows: bottomRows :: Changers -> BottomOpts -> [(PostMeta, Posting)] -> Fields (Maybe [[Chunk]])
- Penny.Cabin.Posts.Chunk: ChunkOpts :: (Box -> Text) -> (Box -> Text) -> (Commodity -> Qty -> Text) -> Fields Bool -> SubAccountLength -> Alloc -> Alloc -> Spacers Int -> ReportWidth -> ChunkOpts
+ Penny.Cabin.Posts.Chunk: ChunkOpts :: ((PostMeta, Posting) -> Text) -> ((PostMeta, Posting) -> Text) -> (Commodity -> Qty -> Text) -> Fields Bool -> SubAccountLength -> Alloc -> Alloc -> Spacers Int -> ReportWidth -> ChunkOpts
- Penny.Cabin.Posts.Chunk: dateFormat :: ChunkOpts -> Box -> Text
+ Penny.Cabin.Posts.Chunk: dateFormat :: ChunkOpts -> (PostMeta, Posting) -> Text
- Penny.Cabin.Posts.Chunk: makeChunk :: Changers -> ChunkOpts -> [Box] -> [Chunk]
+ Penny.Cabin.Posts.Chunk: makeChunk :: Changers -> ChunkOpts -> [(PostMeta, Posting)] -> [Chunk]
- Penny.Cabin.Posts.Chunk: qtyFormat :: ChunkOpts -> Box -> Text
+ Penny.Cabin.Posts.Chunk: qtyFormat :: ChunkOpts -> (PostMeta, Posting) -> Text
- Penny.Cabin.Posts.Growers: GrowOpts :: (Box -> Text) -> (Box -> Text) -> (Commodity -> Qty -> Text) -> Fields Bool -> GrowOpts
+ Penny.Cabin.Posts.Growers: GrowOpts :: ((PostMeta, Posting) -> Text) -> ((PostMeta, Posting) -> Text) -> (Commodity -> Qty -> Text) -> Fields Bool -> GrowOpts
- Penny.Cabin.Posts.Growers: dateFormat :: GrowOpts -> Box -> Text
+ Penny.Cabin.Posts.Growers: dateFormat :: GrowOpts -> (PostMeta, Posting) -> Text
- Penny.Cabin.Posts.Growers: growCells :: Changers -> GrowOpts -> [Box] -> Fields (Maybe ([ColumnSpec], Int))
+ Penny.Cabin.Posts.Growers: growCells :: Changers -> GrowOpts -> [(PostMeta, Posting)] -> Fields (Maybe ([ColumnSpec], Int))
- Penny.Cabin.Posts.Growers: qtyFormat :: GrowOpts -> Box -> Text
+ Penny.Cabin.Posts.Growers: qtyFormat :: GrowOpts -> (PostMeta, Posting) -> Text
- Penny.Cabin.Posts.Meta: toBoxList :: ShowZeroBalances -> Pdct (Box LibertyMeta) -> [PostFilterFn] -> [Box LibertyMeta] -> [Box]
+ Penny.Cabin.Posts.Meta: toBoxList :: ShowZeroBalances -> Pdct (LibertyMeta, Posting) -> [PostFilterFn] -> [(LibertyMeta, Posting)] -> [(PostMeta, Posting)]
- Penny.Cabin.Posts.Parser: State :: CaseSensitive -> Factory -> [Token (Box LibertyMeta)] -> [PostFilterFn] -> Fields Bool -> ReportWidth -> ShowZeroBalances -> ExprDesc -> VerboseFilter -> ShowExpression -> State
+ Penny.Cabin.Posts.Parser: State :: CaseSensitive -> Factory -> [Token (LibertyMeta, Posting)] -> [PostFilterFn] -> Fields Bool -> ReportWidth -> ShowZeroBalances -> ExprDesc -> VerboseFilter -> ShowExpression -> State
- Penny.Cabin.Posts.Parser: tokens :: State -> [Token (Box LibertyMeta)]
+ Penny.Cabin.Posts.Parser: tokens :: State -> [Token (LibertyMeta, Posting)]
- Penny.Copper: open :: [String] -> IO Ledger
+ Penny.Copper: open :: [String] -> IO [LedgerItem]
- Penny.Copper.Parsec: amount :: Parser Amount
+ Penny.Copper.Parsec: amount :: Parser (Amount, Side, SpaceBetween)
- Penny.Copper.Parsec: blankLine :: Parser Item
+ Penny.Copper.Parsec: blankLine :: Parser ()
- Penny.Copper.Parsec: entry :: Parser Entry
+ Penny.Copper.Parsec: entry :: Parser (Entry, Side, SpaceBetween)
- Penny.Copper.Parsec: item :: Parser Item
+ Penny.Copper.Parsec: item :: Parser ParsedItem
- Penny.Copper.Parsec: leftCmdtyLvl1Amt :: Parser Amount
+ Penny.Copper.Parsec: leftCmdtyLvl1Amt :: Parser (Amount, Side, SpaceBetween)
- Penny.Copper.Parsec: leftCmdtyLvl3Amt :: Parser Amount
+ Penny.Copper.Parsec: leftCmdtyLvl3Amt :: Parser (Amount, Side, SpaceBetween)
- Penny.Copper.Parsec: leftSideCmdtyAmt :: Parser Amount
+ Penny.Copper.Parsec: leftSideCmdtyAmt :: Parser (Amount, Side, SpaceBetween)
- Penny.Copper.Parsec: posting :: Parser Posting
+ Penny.Copper.Parsec: posting :: Parser (PostingCore, PostingLine, Maybe Entry)
- Penny.Copper.Parsec: rightSideCmdtyAmt :: Parser Amount
+ Penny.Copper.Parsec: rightSideCmdtyAmt :: Parser (Amount, Side, SpaceBetween)
- Penny.Copper.Parsec: topLine :: Parser TopLine
+ Penny.Copper.Parsec: topLine :: Parser ParsedTopLine
- Penny.Copper.Parsec: transaction :: Parser Transaction
+ Penny.Copper.Parsec: transaction :: Parser ParsedTxn
- Penny.Copper.Render: amount :: GroupSpecs -> Amount -> Maybe Text
+ Penny.Copper.Render: amount :: GroupSpecs -> Maybe Side -> Maybe SpaceBetween -> Amount -> Maybe Text
- Penny.Copper.Render: entry :: GroupSpecs -> Entry -> Maybe Text
+ Penny.Copper.Render: entry :: GroupSpecs -> Maybe Side -> Maybe SpaceBetween -> Entry -> Maybe Text
- Penny.Copper.Render: item :: GroupSpecs -> Item -> Maybe Text
+ Penny.Copper.Render: item :: GroupSpecs -> S4 (TopLineCore, Ents PostingCore) PricePoint Comment BlankLine -> Maybe Text
- Penny.Copper.Render: posting :: GroupSpecs -> Bool -> Posting -> Maybe Text
+ Penny.Copper.Render: posting :: GroupSpecs -> Bool -> Ent PostingCore -> Maybe Text
- Penny.Copper.Render: topLine :: TopLine -> Maybe Text
+ Penny.Copper.Render: topLine :: TopLineCore -> Maybe Text
- Penny.Copper.Render: transaction :: GroupSpecs -> Transaction -> Maybe Text
+ Penny.Copper.Render: transaction :: GroupSpecs -> (TopLineCore, Ents PostingCore) -> Maybe Text
- Penny.Liberty: type Operand = Pdct PostFam
+ Penny.Liberty: type Operand = Pdct Posting
- Penny.Liberty: xactionsToFiltered :: LPdct -> [PostFilterFn] -> (PostFam -> PostFam -> Ordering) -> [Transaction] -> ([Chunk], [Box LibertyMeta])
+ Penny.Liberty: xactionsToFiltered :: LPdct -> [PostFilterFn] -> (Posting -> Posting -> Ordering) -> [Transaction] -> ([Chunk], [(LibertyMeta, Posting)])
- Penny.Lincoln: display :: PostFam -> Text
+ Penny.Lincoln: display :: Posting -> Text
- Penny.Lincoln.Bits.Open: Amount :: Qty -> Commodity -> Maybe Side -> Maybe SpaceBetween -> Amount
+ Penny.Lincoln.Bits.Open: Amount :: Qty -> Commodity -> Amount
- Penny.Lincoln.Bits.Qty: allocate :: Qty -> NonEmpty Qty -> NonEmpty Qty
+ Penny.Lincoln.Bits.Qty: allocate :: Qty -> (Qty, [Qty]) -> (Qty, [Qty])
- Penny.Lincoln.Predicates: filename :: MakePdct
+ Penny.Lincoln.Predicates: filename :: Matcher -> LPdct
- Penny.Lincoln.Predicates: type LPdct = Pdct PostFam
+ Penny.Lincoln.Predicates: type LPdct = Pdct Posting
- Penny.Lincoln.Predicates.Siblings: type LPdct = Pdct PostFam
+ Penny.Lincoln.Predicates.Siblings: type LPdct = Pdct Posting
- Penny.Lincoln.Queries: account :: PostFam -> Account
+ Penny.Lincoln.Queries: account :: Posting -> Account
- Penny.Lincoln.Queries: amount :: PostFam -> Amount
+ Penny.Lincoln.Queries: amount :: Posting -> Amount
- Penny.Lincoln.Queries: balance :: PostFam -> Balance
+ Penny.Lincoln.Queries: balance :: Posting -> Balance
- Penny.Lincoln.Queries: best :: (Posting -> Maybe a) -> (TopLine -> Maybe a) -> PostFam -> Maybe a
+ Penny.Lincoln.Queries: best :: (TopLineData -> Maybe a) -> (Ents PostingData -> Maybe a) -> Posting -> Maybe a
- Penny.Lincoln.Queries: commodity :: PostFam -> Commodity
+ Penny.Lincoln.Queries: commodity :: Posting -> Commodity
- Penny.Lincoln.Queries: dateTime :: PostFam -> DateTime
+ Penny.Lincoln.Queries: dateTime :: Posting -> DateTime
- Penny.Lincoln.Queries: drCr :: PostFam -> DrCr
+ Penny.Lincoln.Queries: drCr :: Posting -> DrCr
- Penny.Lincoln.Queries: entry :: PostFam -> Entry
+ Penny.Lincoln.Queries: entry :: Posting -> Entry
- Penny.Lincoln.Queries: filePosting :: PostFam -> Maybe FilePosting
+ Penny.Lincoln.Queries: filePosting :: Posting -> Maybe FilePosting
- Penny.Lincoln.Queries: fileTransaction :: PostFam -> Maybe FileTransaction
+ Penny.Lincoln.Queries: fileTransaction :: Posting -> Maybe FileTransaction
- Penny.Lincoln.Queries: filename :: PostFam -> Maybe Filename
+ Penny.Lincoln.Queries: filename :: Posting -> Maybe Filename
- Penny.Lincoln.Queries: flag :: PostFam -> Maybe Flag
+ Penny.Lincoln.Queries: flag :: Posting -> Maybe Flag
- Penny.Lincoln.Queries: globalPosting :: PostFam -> Maybe GlobalPosting
+ Penny.Lincoln.Queries: globalPosting :: Posting -> Maybe GlobalPosting
- Penny.Lincoln.Queries: globalTransaction :: PostFam -> Maybe GlobalTransaction
+ Penny.Lincoln.Queries: globalTransaction :: Posting -> Maybe GlobalTransaction
- Penny.Lincoln.Queries: localDay :: PostFam -> Day
+ Penny.Lincoln.Queries: localDay :: Posting -> Day
- Penny.Lincoln.Queries: number :: PostFam -> Maybe Number
+ Penny.Lincoln.Queries: number :: Posting -> Maybe Number
- Penny.Lincoln.Queries: payee :: PostFam -> Maybe Payee
+ Penny.Lincoln.Queries: payee :: Posting -> Maybe Payee
- Penny.Lincoln.Queries: postingLine :: PostFam -> Maybe PostingLine
+ Penny.Lincoln.Queries: postingLine :: Posting -> Maybe PostingLine
- Penny.Lincoln.Queries: postingMemo :: PostFam -> Maybe Memo
+ Penny.Lincoln.Queries: postingMemo :: Posting -> Maybe Memo
- Penny.Lincoln.Queries: qty :: PostFam -> Qty
+ Penny.Lincoln.Queries: qty :: Posting -> Qty
- Penny.Lincoln.Queries: side :: PostFam -> Maybe Side
+ Penny.Lincoln.Queries: side :: Posting -> Maybe Side
- Penny.Lincoln.Queries: spaceBetween :: PostFam -> Maybe SpaceBetween
+ Penny.Lincoln.Queries: spaceBetween :: Posting -> Maybe SpaceBetween
- Penny.Lincoln.Queries: tags :: PostFam -> Tags
+ Penny.Lincoln.Queries: tags :: Posting -> Tags
- Penny.Lincoln.Queries: topLineLine :: PostFam -> Maybe TopLineLine
+ Penny.Lincoln.Queries: topLineLine :: Posting -> Maybe TopLineLine
- Penny.Lincoln.Queries: topMemoLine :: PostFam -> Maybe TopMemoLine
+ Penny.Lincoln.Queries: topMemoLine :: Posting -> Maybe TopMemoLine
- Penny.Lincoln.Queries: transactionMemo :: PostFam -> Maybe Memo
+ Penny.Lincoln.Queries: transactionMemo :: Posting -> Maybe Memo
- Penny.Lincoln.Queries.Siblings: account :: PostFam -> [Account]
+ Penny.Lincoln.Queries.Siblings: account :: Posting -> [Account]
- Penny.Lincoln.Queries.Siblings: amount :: PostFam -> [Amount]
+ Penny.Lincoln.Queries.Siblings: amount :: Posting -> [Amount]
- Penny.Lincoln.Queries.Siblings: balance :: PostFam -> [Balance]
+ Penny.Lincoln.Queries.Siblings: balance :: Posting -> [Balance]
- Penny.Lincoln.Queries.Siblings: bestSibs :: (Posting -> Maybe a) -> (TopLine -> Maybe a) -> PostFam -> [Maybe a]
+ Penny.Lincoln.Queries.Siblings: bestSibs :: (PostingCore -> Maybe a) -> (TopLineCore -> Maybe a) -> Posting -> [Maybe a]
- Penny.Lincoln.Queries.Siblings: commodity :: PostFam -> [Commodity]
+ Penny.Lincoln.Queries.Siblings: commodity :: Posting -> [Commodity]
- Penny.Lincoln.Queries.Siblings: drCr :: PostFam -> [DrCr]
+ Penny.Lincoln.Queries.Siblings: drCr :: Posting -> [DrCr]
- Penny.Lincoln.Queries.Siblings: entry :: PostFam -> [Entry]
+ Penny.Lincoln.Queries.Siblings: entry :: Posting -> [Entry]
- Penny.Lincoln.Queries.Siblings: filePosting :: PostFam -> [Maybe FilePosting]
+ Penny.Lincoln.Queries.Siblings: filePosting :: Posting -> [Maybe FilePosting]
- Penny.Lincoln.Queries.Siblings: flag :: PostFam -> [Maybe Flag]
+ Penny.Lincoln.Queries.Siblings: flag :: Posting -> [Maybe Flag]
- Penny.Lincoln.Queries.Siblings: globalPosting :: PostFam -> [Maybe GlobalPosting]
+ Penny.Lincoln.Queries.Siblings: globalPosting :: Posting -> [Maybe GlobalPosting]
- Penny.Lincoln.Queries.Siblings: number :: PostFam -> [Maybe Number]
+ Penny.Lincoln.Queries.Siblings: number :: Posting -> [Maybe Number]
- Penny.Lincoln.Queries.Siblings: payee :: PostFam -> [Maybe Payee]
+ Penny.Lincoln.Queries.Siblings: payee :: Posting -> [Maybe Payee]
- Penny.Lincoln.Queries.Siblings: postingLine :: PostFam -> [Maybe PostingLine]
+ Penny.Lincoln.Queries.Siblings: postingLine :: Posting -> [Maybe PostingLine]
- Penny.Lincoln.Queries.Siblings: postingMemo :: PostFam -> [Maybe Memo]
+ Penny.Lincoln.Queries.Siblings: postingMemo :: Posting -> [Maybe Memo]
- Penny.Lincoln.Queries.Siblings: qty :: PostFam -> [Qty]
+ Penny.Lincoln.Queries.Siblings: qty :: Posting -> [Qty]
- Penny.Lincoln.Queries.Siblings: sibs :: (Posting -> a) -> PostFam -> [a]
+ Penny.Lincoln.Queries.Siblings: sibs :: (Ent PostingData -> a) -> Posting -> [a]
- Penny.Lincoln.Queries.Siblings: side :: PostFam -> [Maybe Side]
+ Penny.Lincoln.Queries.Siblings: side :: Posting -> [Maybe Side]
- Penny.Lincoln.Queries.Siblings: spaceBetween :: PostFam -> [Maybe SpaceBetween]
+ Penny.Lincoln.Queries.Siblings: spaceBetween :: Posting -> [Maybe SpaceBetween]
- Penny.Lincoln.Queries.Siblings: tags :: PostFam -> [Tags]
+ Penny.Lincoln.Queries.Siblings: tags :: Posting -> [Tags]
- Penny.Wheat: WheatConf :: String -> [String] -> [UTCTime -> TestTree PostFam] -> IndentAmt -> Verbosity -> Verbosity -> (Name -> Bool) -> (Name -> Bool) -> Bool -> GroupVerbosity -> Bool -> Bool -> UTCTime -> [String] -> WheatConf
+ Penny.Wheat: WheatConf :: String -> [String] -> [UTCTime -> TestTree Posting] -> IndentAmt -> Verbosity -> Verbosity -> (Name -> Bool) -> (Name -> Bool) -> Bool -> GroupVerbosity -> Bool -> Bool -> UTCTime -> [String] -> WheatConf
- Penny.Wheat: atLeastNPostings :: Int -> Name -> Pdct PostFam -> TestTree PostFam
+ Penny.Wheat: atLeastNPostings :: Int -> Name -> Pdct Posting -> TestTree Posting
- Penny.Wheat: eachPostingMustBeTrue :: Name -> Pdct PostFam -> TestTree PostFam
+ Penny.Wheat: eachPostingMustBeTrue :: Name -> Pdct Posting -> TestTree Posting
- Penny.Wheat: tests :: WheatConf -> [UTCTime -> TestTree PostFam]
+ Penny.Wheat: tests :: WheatConf -> [UTCTime -> TestTree Posting]

Files

− Penny.hs
@@ -1,457 +0,0 @@--- | Penny - extensible double-entry accounting system--module Penny-  ( -- * Building a custom Penny binary--    -- | Everything you need to create a custom Penny program is-    -- available by importing only this module.-    Version(..)-  , Defaults(..)-  , Z.Matcher(..)--  -- ** Color schemes-  , E.Scheme(..)-  , E.Changers-  , E.Labels(..)-  , E.EvenAndOdd(..)-  , module System.Console.Rainbow--  -- ** Sorting-  , Z.SortField(..)-  , CabP.SortOrder(..)--  -- ** Expression type-  , Exp.ExprDesc(..)--  -- ** Formatting quantities-  , defaultQtyFormat--  -- ** Convert report options-  , Target(..)-  , CP.SortBy(..)--  -- ** Postings report options-  , M.Box-  , Fields(..)-  , Spacers(..)-  , widthFromRuntime-  , Ps.yearMonthDay-  , Ps.qtyAsIs-  , Ps.balanceAsIs--  -- ** Runtime-  , S.Runtime-  , S.environment--  -- ** Text-  , X.Text-  , X.pack--  -- ** Main function-  , runPenny--    -- * Developer overview--    -- | Penny is organized into a tree of modules, each with a-    -- name. Check out the links for details on each component of-    -- Penny.-    ---    -- "Penny.Brenner" - Penny financial institution transaction-    -- handling. Depends on Lincoln and Copper.-    ---    -- "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. Depends on-    -- Lincoln.-    ---    -- "Penny.Steel" - independent utilities. Depends on no other-    -- Penny components.-    ---    -- "Penny.Wheat" - tools to use with-    -- "Penny.Steel.Prednote". Depends on Steel, Lincoln, and Copper.-    ---    -- "Penny.Zinc" - the Penny command-line interface. Depends on-    -- Cabin, Copper, Liberty, and Lincoln.-    ---    -- The dependencies are represented as a dot file in-    -- bin/doc/dependencies.dot in the Penny git repository.-  ) where--import qualified Data.Text as X-import Data.Version (Version(..))-import qualified Penny.Cabin.Balance.Convert as Conv-import qualified Penny.Cabin.Balance.Convert.Parser as CP-import qualified Penny.Cabin.Balance.Convert.Options as ConvOpts-import qualified Penny.Cabin.Balance.MultiCommodity as MC-import qualified Penny.Cabin.Balance.MultiCommodity.Parser as MP-import System.Console.Rainbow-import qualified Penny.Cabin.Interface as I-import qualified Penny.Cabin.Options as CO-import qualified Penny.Cabin.Parsers as CabP-import qualified Penny.Cabin.Posts as Ps-import qualified Penny.Cabin.Posts.Fields as PF-import qualified Penny.Cabin.Posts.Spacers as PS-import qualified Penny.Cabin.Posts.Meta as M-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Lincoln as L-import qualified Data.Prednote.Expressions as Exp-import qualified Penny.Zinc as Z-import qualified Penny.Shield as S-import qualified Text.Matchers as Mr---- | This type contains settings for all the reports, as well as--- default settings for the global options. Some of these can be--- overridden on the command line.-data Defaults = Defaults-  { caseSensitive :: Bool-    -- ^ Whether the matcher is case sensitive by default--  , matcher :: Z.Matcher-    -- ^ Which matcher to use--  , colorToFile :: Bool-    -- ^ Use colors when standard output is not a terminal?--  , expressionType :: Exp.ExprDesc-    -- ^ Use RPN or infix expressions? This affects both the posting-    -- filter and the filter for the Postings report.--  , defaultScheme :: Maybe E.Scheme-    -- ^ Default color scheme. If Nothing, there is no default color-    -- scheme. If there is no default color scheme and the user does-    -- not pick one on the command line, no colors will be used.--  , additionalSchemes :: [E.Scheme]-    -- ^ Additional color schemes the user can pick from on the-    -- command line.--  , sorter :: [(Z.SortField, CabP.SortOrder)]-    -- ^ Postings are sorted in this order by default. For example, if-    -- the first pair is (Date, Ascending), then postings are first-    -- sorted by date in ascending order. If the second pair is-    -- (Payee, Ascending), then postings with the same date are then-    -- sorted by payee.-    ---    -- If this list is empty, then by default postings are left in the-    -- same order as they appear in the ledger files.--  , balanceFormat :: L.Commodity -> L.Qty -> X.Text-    -- ^ How to format balances in the balance report. Change this-    -- function if, for example, you want to allow for digit grouping.--  , balanceShowZeroBalances :: Bool-    -- ^ Show zero balances in the balance report? If True, show them;-    -- if False, hide them.--  , balanceOrder :: CabP.SortOrder-    -- ^ Whether to sort the accounts in ascending or descending order-    -- by account name in the balance report.--  , convertShowZeroBalances :: Bool-    -- ^ Show zero balances in the convert report? If True, show them;-    -- if False, hide them.--  , convertTarget :: Target-    -- ^ The commodity to which to convert the commodities in the-    -- convert report.--  , convertOrder :: CabP.SortOrder-    -- ^ Sort the convert report in ascending or descending order.--  , convertSortBy :: CP.SortBy-    -- ^ Sort by account or by quantity in the convert report.--  , convertFormat :: L.Commodity -> L.Qty -> X.Text-    -- ^ How to format balances in the convert report. For instance,-    -- this function might perform digit grouping.--  , postingsFields :: Fields Bool-    -- ^ Fields to show by default in the postings report.--  , postingsWidth :: Int-    -- ^ The postings report is roughly this wide by-    -- default. Typically this will be as wide as your terminal.--  , postingsShowZeroBalances :: Bool-    -- ^ Show zero balances in the postings report? If True, show-    -- them; if False, hide them.--  , postingsDateFormat :: M.Box -> X.Text-    -- ^ How to format dates in the postings report.--  , postingsQtyFormat :: M.Box -> X.Text-    -- ^ How to format quantities in the balance report. This function-    -- is used when showing the quantity for the posting itself, and-    -- not the quantity for the totals columns (for that, see-    -- postingsBalanceFormat.) For example this function might perform-    -- digit grouping.--  , postingsBalanceFormat :: L.Commodity -> L.Qty -> X.Text-    -- ^ How to format balance totals in the postings report.--  , postingsSubAccountLength :: Int-    -- ^ Account names in the postings report are shortened if-    -- necessary in order to help the report fit within the allotted-    -- width (see postingsWidth). Account names are only shortened as-    -- much as is necessary for them to fit; however, each sub-account-    -- name will not be shortened any more than the amount given here.--  , postingsPayeeAllocation :: Int-    -- ^ postingsPayeeAllocation and postingsAccountAllocation-    -- determine how much space is allotted to the payee and account-    -- fields in the postings report. These fields are variable-    -- width. After space for most other fields is allotted, space is-    -- allotted for these two fields. The two fields divide the space-    -- proportionally depending on postingsPayeeAllocation and-    -- postingsAccountAllocation. For example, if-    -- postingsPayeeAllocation is 60 and postingsAccountAllocation is-    -- 40, then the payee field gets 60 percent of the leftover space-    -- and the account field gets 40 percent of the leftover space.-    ---    -- Both postingsPayeeAllocation and postingsAccountAllocation-    -- must be positive integers; if either one is less than 1, your-    -- program will crash at runtime.--  , postingsAccountAllocation :: Int-    -- ^ See postingsPayeeAllocation above for an explanation--  , postingsSpacers :: Spacers Int-    -- ^ Determines the number of spaces that appears to the right of-    -- each named field; for example, sPayee indicates how many spaces-    -- will appear to the right of the payee field. Each field of the-    -- Spacers should be a non-negative integer (although currently-    -- the absolute value of the field is taken.)-  }---- | Creates an IO action that you can use for the main function.-runPenny-  :: Version-  -- ^ Version of the executable-  -> (S.Runtime -> Defaults)-     -- ^ runPenny will apply this function to the Runtime. This way-     -- the defaults you use can vary depending on environment-     -- variables, the terminal type, the date, etc.-  -> IO ()-runPenny ver getDefaults = do-  rt <- S.runtime-  let df = getDefaults rt-      rs = allReports df-  Z.runZinc ver (toZincDefaults df) rt rs---- | The commodity to which to convert the commodities in the convert--- report.-data Target-  = AutoTarget-    -- ^ Selects a target commodity automatically, based on which-    -- commodity is the most common target commodity in the prices in-    -- your ledger files. If there is a tie for most common target-    -- commodity, the target that appears later in your ledger files-    -- is used.-  | ManualTarget String-    -- ^ Always uses the commodity named by the string given.-  deriving Show---- | Gets the current screen width from the runtime. If the COLUMNS--- environment variable is not set, uses 80.-widthFromRuntime :: S.Runtime -> Int-widthFromRuntime rt = case S.screenWidth rt of-  Nothing -> 80-  Just sw -> S.unScreenWidth sw--convTarget :: Target -> CP.Target-convTarget t = case t of-  AutoTarget -> CP.AutoTarget-  ManualTarget s -> CP.ManualTarget . L.To . L.Commodity . X.pack $ s--allReports-  :: Defaults-  -> [I.Report]-allReports df =-  let bd = toBalanceDefaults df-      cd = toConvertDefaults df-      pd = toPostingsDefaults df-  in [ Ps.zincReport pd-     , MC.parseReport (balanceFormat df) bd-     , Conv.cmdLineReport cd-     ]--toZincDefaults :: Defaults -> Z.Defaults-toZincDefaults d = Z.Defaults-  { Z.sensitive =-      if caseSensitive d then Mr.Sensitive else Mr.Insensitive-  , Z.matcher = matcher d-  , Z.colorToFile = Z.ColorToFile . colorToFile $ d-  , Z.defaultScheme = defaultScheme d-  , Z.moreSchemes = additionalSchemes d-  , Z.sorter = sorter d-  , Z.exprDesc = expressionType d-  }--toBalanceDefaults :: Defaults -> MP.ParseOpts-toBalanceDefaults d = MP.ParseOpts-  { MP.showZeroBalances =-      CO.ShowZeroBalances . balanceShowZeroBalances $ d-  , MP.order = balanceOrder d-  }--toConvertDefaults :: Defaults -> ConvOpts.DefaultOpts-toConvertDefaults d = ConvOpts.DefaultOpts-  { ConvOpts.showZeroBalances =-      CO.ShowZeroBalances . convertShowZeroBalances $ d-  , ConvOpts.target = convTarget . convertTarget $ d-  , ConvOpts.sortOrder = convertOrder d-  , ConvOpts.sortBy = convertSortBy d-  , ConvOpts.format = convertFormat d-  }--toPostingsDefaults :: Defaults -> Ps.ZincOpts-toPostingsDefaults d = Ps.ZincOpts-  { Ps.fields = convFields . postingsFields $ d-  , Ps.width = Ps.ReportWidth . postingsWidth $ d-  , Ps.showZeroBalances =-      CO.ShowZeroBalances . postingsShowZeroBalances $ d-  , Ps.dateFormat = postingsDateFormat d-  , Ps.qtyFormat = postingsQtyFormat d-  , Ps.balanceFormat = postingsBalanceFormat d-  , Ps.subAccountLength =-      Ps.SubAccountLength . postingsSubAccountLength $ d-  , Ps.payeeAllocation =-      Ps.alloc . postingsPayeeAllocation $ d-  , Ps.accountAllocation =-      Ps.alloc . postingsAccountAllocation $ d-  , Ps.spacers = convSpacers . postingsSpacers $ d-  }--defaultQtyFormat :: L.Qty -> X.Text-defaultQtyFormat = X.pack . show--data Spacers a = Spacers-  { sGlobalTransaction :: a-  , sRevGlobalTransaction :: a-  , sGlobalPosting :: a-  , sRevGlobalPosting :: a-  , sFileTransaction :: a-  , sRevFileTransaction :: a-  , sFilePosting :: a-  , sRevFilePosting :: a-  , sFiltered :: a-  , sRevFiltered :: a-  , sSorted :: a-  , sRevSorted :: a-  , sVisible :: a-  , sRevVisible :: a-  , sLineNum :: a-  , sDate :: a-  , sFlag :: a-  , sNumber :: a-  , sPayee :: a-  , sAccount :: a-  , sPostingDrCr :: a-  , sPostingCmdty :: a-  , sPostingQty :: a-  , sTotalDrCr :: a-  , sTotalCmdty :: a-  } deriving (Show, Eq)--data Fields a = Fields-  { fGlobalTransaction :: a-  , fRevGlobalTransaction :: a-  , fGlobalPosting :: a-  , fRevGlobalPosting :: a-  , fFileTransaction :: a-  , fRevFileTransaction :: a-  , fFilePosting :: a-  , fRevFilePosting :: a-  , fFiltered :: a-  , fRevFiltered :: a-  , fSorted :: a-  , fRevSorted :: a-  , fVisible :: a-  , fRevVisible :: a-  , fLineNum :: a-  , fDate :: a-  , fFlag :: a-  , fNumber :: a-  , fPayee :: a-  , fAccount :: a-  , fPostingDrCr :: a-  , fPostingCmdty :: a-  , fPostingQty :: a-  , fTotalDrCr :: a-  , fTotalCmdty :: a-  , fTotalQty :: a-  , fTags :: a-  , fMemo :: a-  , fFilename :: a-  } deriving (Show, Eq)--convSpacers :: Spacers a -> PS.Spacers a-convSpacers s = PS.Spacers-  { PS.globalTransaction = sGlobalTransaction s-  , PS.revGlobalTransaction = sRevGlobalTransaction s-  , PS.globalPosting = sGlobalPosting s-  , PS.revGlobalPosting = sRevGlobalPosting s-  , PS.fileTransaction = sFileTransaction s-  , PS.revFileTransaction = sRevFileTransaction s-  , PS.filePosting = sFilePosting s-  , PS.revFilePosting = sRevFilePosting s-  , PS.filtered = sFiltered s-  , PS.revFiltered = sRevFiltered s-  , PS.sorted = sSorted s-  , PS.revSorted = sRevSorted s-  , PS.visible = sVisible s-  , PS.revVisible = sRevVisible s-  , PS.lineNum = sLineNum s-  , PS.date = sDate s-  , PS.flag = sFlag s-  , PS.number = sNumber s-  , PS.payee = sPayee s-  , PS.account = sAccount s-  , PS.postingDrCr = sPostingDrCr s-  , PS.postingCmdty = sPostingCmdty s-  , PS.postingQty = sPostingQty s-  , PS.totalDrCr = sTotalDrCr s-  , PS.totalCmdty = sTotalCmdty s-  }--convFields :: Fields a -> PF.Fields a-convFields f = PF.Fields-  { PF.globalTransaction = fGlobalTransaction f-  , PF.revGlobalTransaction = fRevGlobalTransaction f-  , PF.globalPosting = fGlobalPosting f-  , PF.revGlobalPosting = fRevGlobalPosting f-  , PF.fileTransaction = fFileTransaction f-  , PF.revFileTransaction = fRevFileTransaction f-  , PF.filePosting = fFilePosting f-  , PF.revFilePosting = fRevFilePosting f-  , PF.filtered = fFiltered f-  , PF.revFiltered = fRevFiltered f-  , PF.sorted = fSorted f-  , PF.revSorted = fRevSorted f-  , PF.visible = fVisible f-  , PF.revVisible = fRevVisible f-  , PF.lineNum = fLineNum f-  , PF.date = fDate f-  , PF.flag = fFlag f-  , PF.number = fNumber f-  , PF.payee = fPayee f-  , PF.account = fAccount f-  , PF.postingDrCr = fPostingDrCr f-  , PF.postingCmdty = fPostingCmdty f-  , PF.postingQty = fPostingQty f-  , PF.totalDrCr = fTotalDrCr f-  , PF.totalCmdty = fTotalCmdty f-  , PF.totalQty = fTotalQty f-  , PF.tags = fTags f-  , PF.memo = fMemo f-  , PF.filename = fFilename f-  }-
− Penny/Brenner.hs
@@ -1,259 +0,0 @@--- | Brenner - Penny financial institution interfaces------ Brenner provides a uniform way to interact with downloaded data--- from financial Given a parser, Brenner will import the transactions--- and store them in a database. From there it is easy to merge the--- transactions (without duplicates) into a ledger file, and then to--- clear transactions from statements in an automated fashion.-module Penny.Brenner-  ( FitAcct(..)-  , Config(..)-  , R.GroupSpecs(..)-  , R.GroupSpec(..)-  , Y.Translator(..)-  , L.Side(..)-  , L.SpaceBetween(..)-  , usePayeeOrDesc-  , brennerMain-  ) where--import qualified Penny.Brenner.Types as Y-import Control.Monad (join)-import Data.Either (partitionEithers)-import qualified Data.Text as X-import qualified Data.Version as V-import qualified Penny.Liberty as Ly-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Builders as Bd-import qualified Penny.Copper.Render as R-import qualified Penny.Brenner.Clear as C-import qualified Penny.Brenner.Database as D-import qualified Penny.Brenner.Import as I-import qualified Penny.Brenner.Merge as M-import qualified Penny.Brenner.Print as P-import Control.Applicative ((<*>))-import qualified System.Console.MultiArg as MA-import qualified Control.Monad.Exception.Synchronous as Ex--brennerMain-  :: V.Version-  -- ^ Binary version-  -> Config-  -> IO ()-brennerMain v cf = do-  let cf' = convertConfig cf-  join $ MA.modesWithHelp (help cf') (globalOpts v)-    (preProcessor cf')--globalOpts-  :: V.Version-  -- ^ Binary version-  -> [MA.OptSpec (Either (IO ()) String)]-globalOpts v =-  [ MA.OptSpec ["fit-account"] "f" (MA.OneArg Right)-  , fmap Left (Ly.version v)-  ]--preProcessor-  :: Y.Config-  -> [Either (IO ()) String]-  -> Either (a -> IO ()) [MA.Mode (IO ())]-preProcessor cf args =-  let (vers, as) = partitionEithers args-  in case vers of-      [] -> makeModes cf as-      x:_ -> Left (const x)--makeModes-  :: Y.Config-  -> [String]-  -- ^ Names of financial institutions given on command line-  -> Either (a -> IO ()) [MA.Mode (IO ())]-makeModes cf as = Ex.toEither . Ex.mapException (const . fail) $ do-  fi <- case as of-    [] -> return $ Y.defaultFitAcct cf-    _ ->-      let pdct (Y.Name n, _) = n == X.pack s-          s = last as-      in case filter pdct (Y.moreFitAccts cf) of-           [] -> Ex.throw $-              "financial institution account "-              ++ s ++ " not configured."-           (_, c):[] -> return $ Just c-           _ -> Ex.throw $-              "more than one financial institution account "-              ++ "named " ++ s ++ " configured."-  return $ [C.mode, I.mode, M.mode, P.mode, D.mode] <*> [fi]--help-  :: Y.Config-  -> String-  -- ^ Program name--  -> String-help c n = unlines ls ++ cs-  where-    ls = [ "usage: " ++ n ++ " [global-options]"-            ++ " COMMAND [local-options]"-            ++ " ARGS..."-         , ""-         , "where COMMAND is one of:"-         , "import, merge, clear, database, print"-         , ""-         , "For help on an individual command and its"-           ++ " local options, use "-         , n ++ " COMMAND --help"-         , ""-         , "Global Options:"-         , "-f, --fit-account ACCOUNT"-         , "  Use one of the Additional Financial Institution"-         , "  Accounts shown below. If this option does not appear,"-         , "  the default account is used if there is one."-         , "-h, --help"-         , "  Show help and exit"-         , ""-         ]-    showPair (Y.Name a, cd) = "Additional financial institution "-      ++ "account: " ++ X.unpack a ++ "\n" ++ showFitAcct cd-    cs = showDefaultFitAcct (Y.defaultFitAcct c)-         ++ more-    more = if null (Y.moreFitAccts c)-           then "No additional financial institution accounts\n"-           else concatMap showPair . Y.moreFitAccts $ c--showDefaultFitAcct :: Maybe Y.FitAcct -> String-showDefaultFitAcct mc = case mc of-  Nothing -> "No default financial institution account\n"-  Just c -> "Default financial institution account:\n" ++ showFitAcct c--label :: String -> String -> String-label l o = "  " ++ l ++ ": " ++ o ++ "\n"--showAccount :: L.Account -> String-showAccount =-  X.unpack-  . X.intercalate (X.singleton ':')-  . map L.unSubAccount-  . L.unAccount--showFitAcct :: Y.FitAcct -> String-showFitAcct c =-  label "Database location"-    (X.unpack . Y.unDbLocation . Y.dbLocation $ c)--  ++ label "Penny account"-     (showAccount . Y.unPennyAcct . Y.pennyAcct $ c)--  ++ label "Account for new offsetting postings"-     (showAccount . Y.unDefaultAcct . Y.defaultAcct $ c)--  ++ label "Currency"-     (X.unpack . L.unCommodity . Y.unCurrency . Y.currency $ c)--  ++ "\n"--  ++ "More information about the parser:\n"-  ++ (fst . Y.parser $ c)-  ++ "\n\n"----- | Information to configure a single financial institution account.-data FitAcct = FitAcct-  { dbLocation :: String-    -- ^ Path and filename to where the database is kept. You can use-    -- an absolute or relative path (if it is relative, it will be-    -- resolved relative to the current directory at runtime.)--  , pennyAcct :: String-    -- ^ The account that you use in your Penny file to hold-    -- transactions for this card. Separate each sub-account with-    -- colons (as you do in the Penny file.)--  , defaultAcct :: String-    -- ^ When new transactions are created, one of the postings will-    -- be in the amexAcct given above. The other posting will be in-    -- this account.--  , currency :: String-    -- ^ The commodity for the currency of your card (e.g. @$@).--  , groupSpecs :: R.GroupSpecs-    -- ^ How to group digits when printing the resulting ledger. All-    -- quantities (not just those affected by this program) will be-    -- formatted using this specification.--  , translator :: Y.Translator-    -- ^ See the documentation under the 'Translator' type for-    -- details.--  , side :: L.Side-  -- ^ When creating new transactions, the commodity will be on this-  -- side--  , spaceBetween :: L.SpaceBetween-  -- ^ When creating new transactions, is there a space between the-  -- commodity and the quantity--  , parser :: ( String-              , Y.FitFileLocation -> IO (Ex.Exceptional String [Y.Posting]))-  -- ^ Parses a file of transactions from the financial-  -- institution. The function must open the file and parse it. This-  -- is in the IO monad not only because the function must open the-  -- file itself, but also so the function can perform arbitrary IO-  -- (run pdftotext, maybe?) If there is failure, the function can-  -- return an Exceptional String, which is the error-  -- message. Alternatively the function can raise an exception in the-  -- IO monad (currently Brenner makes no attempt to catch these) so-  -- if any of the IO functions throw you can simply not handle the-  -- exceptions.-  ---  -- The first element of the pair is a help string which should-  -- indicate how to download the data, as a helpful reminder.--  , toLincolnPayee :: Y.Desc -> Y.Payee -> L.Payee-  -- ^ Sometimes the financial institution provides Payee information,-  -- sometimes it does not. Sometimes the Desc might have additional-  -- information that you might want to remove. This function can be-  -- used to do that. The resulting Lincoln Payee is used for any-  -- transactions that are created by the merge command. The resulting-  -- payee is also used when comparing new financial institution-  -- postings to already existing ledger transactions in order to-  -- guess at which payee and accounts to create in the transactions-  -- created by the merge command.---  } deriving Show--convertFitAcct :: FitAcct -> Y.FitAcct-convertFitAcct (FitAcct db ax df cy gs tl sd sb ps tlp) = Y.FitAcct-  { Y.dbLocation = Y.DbLocation . X.pack $ db-  , Y.pennyAcct = Y.PennyAcct . Bd.account . X.pack $ ax-  , Y.defaultAcct = Y.DefaultAcct . Bd.account . X.pack $ df-  , Y.currency = Y.Currency . L.Commodity . X.pack $ cy-  , Y.groupSpecs = gs-  , Y.translator = tl-  , Y.side = sd-  , Y.spaceBetween = sb-  , Y.parser = ps-  , Y.toLincolnPayee = tlp-  }--data Config = Config-  { defaultFitAcct :: Maybe FitAcct-  , moreFitAccts :: [(String, FitAcct)]-  } deriving Show--convertConfig :: Config -> Y.Config-convertConfig (Config d m) = Y.Config-  { Y.defaultFitAcct = fmap convertFitAcct d-  , Y.moreFitAccts =-      let f (n, c) = (Y.Name (X.pack n), convertFitAcct c)-      in map f m-  }---- | A simple function to use for 'toLincolnPayee'. Uses the financial--- institution payee if it is available; otherwise, uses the financial--- institution description.-usePayeeOrDesc :: Y.Desc -> Y.Payee -> L.Payee-usePayeeOrDesc (Y.Desc d) (Y.Payee p) = L.Payee $-  if X.null p then d else p
− Penny/Brenner/Amex.hs
@@ -1,121 +0,0 @@--- | Parses Amex credit card data. See the help text in the 'help'--- function for more details. Also, the file format is documented in--- the file @doc\/amex-file-format.org@.-module Penny.Brenner.Amex (parser) where--import Control.Applicative ((<$>), (<*>), (<$), (<*), (*>), pure,-                            (<|>), optional)-import qualified Data.Time as Time-import qualified Penny.Brenner.Types as Y-import Text.Parsec.Text (Parser)-import qualified Text.Parsec as P-import Text.Parsec (many1, char, many, satisfy)-import qualified Data.Text as X-import qualified Data.Text.IO as TIO-import qualified Control.Monad.Exception.Synchronous as Ex--parser :: (String, Y.FitFileLocation-                   -> IO (Ex.Exceptional String [Y.Posting]))-parser = (help, loadIncoming)--help :: String-help = unlines-  [ "Parses American Express transaction data in CSV format."-  , "Not tested with American Express deposit accounts."-  , "To download, click the \"Download\" link which is visible"-  , "above and to the right of the transaction list. Under"-  , "\"Download Current View\" Select \"CSV\""-  , "and be sure to click"-  , "\"Include all additional Transaction Details.\""-  ]---- | Loads incoming Amex transactions.-loadIncoming :: Y.FitFileLocation-             -> IO (Ex.Exceptional String [Y.Posting])-loadIncoming (Y.FitFileLocation loc) = do-  txt <- TIO.readFile loc-  let parsed = P.parse (P.many posting <* P.eof) "" txt-      err s = "could not parse incoming postings: " ++ show s-  return (Ex.mapException err . Ex.fromEither $ parsed)--skipThrough :: Char -> Parser ()-skipThrough c = () <$ many (satisfy (/= c)) <* char c--readThrough :: Char -> Parser String-readThrough c = many (satisfy (/= c)) <* char c--date :: Parser Y.Date-date = p >>= failOnErr-  where-    p = (,,)-        <$> fmap read (many1 P.digit)-        <*  char '/'-        <*> fmap read (many1 P.digit)-        <* char '/'-        <*> fmap read (many1 P.digit)-        <*  skipThrough ','-    failOnErr (m, d, y) = maybe (fail "could not parse date")-      (return . Y.Date)-      $ Time.fromGregorianValid y m d--incDecAmount :: Parser (Y.IncDec, Y.Amount)-incDecAmount = do-  incDec <- (Y.Decrease <$ char '-') <|> pure Y.Increase-  amtStr <- readThrough ','-  case Y.mkAmount amtStr of-    Nothing -> fail $ "could not parse amount: " ++ amtStr-    Just a -> return (incDec, a)--doubleQuoted :: Parser String-doubleQuoted = char '"' *> readThrough '"'--desc :: Parser Y.Desc-desc = fmap (Y.Desc . X.pack) $ doubleQuoted <* char ','--payee :: Parser Y.Payee-payee = fmap (Y.Payee . X.pack) $ doubleQuoted <* char ','--amexId :: Parser Y.FitId-amexId = fmap (Y.FitId . X.pack)-         $ char '"' *> char '\'' *> readThrough '\''-           <* char '"' <* char ','----- | Skips a field. Will skip a quoted field or,--- alternatively, skip everything through to the next comma. Do not--- use for the last field, as it looks for a trailing comma.-skipField :: Parser ()-skipField =-  ()-  <$ skipper-  <* char ','-  where-    skipper =     (() <$ optional doubleQuoted)-              <|> (() <$ many (satisfy (/= ',')))----- | Parses last field (currently unknown). Parsers the EOL character.-skipLast :: Parser ()-skipLast = skipThrough '\n'--posting :: Parser Y.Posting-posting =-  f-  <$> date                                        -- 1-  <*  skipField                                   -- 2 Unknown-  <*> desc                                        -- 3 Description-  <*  skipField                                   -- 4 Unknown-  <*  skipField                                   -- 5 Unknown-  <*  skipField                                   -- 6 Unknown-  <*  skipField                                   -- 7 Unknown-  <*> incDecAmount                                -- 8-  <*  skipField                                   -- 9 Unknown-  <*  skipField                                   -- 10 Category-  <*> payee                                       -- 11 D.B.A.-  <*  skipField                                   -- 12 Address-  <*  skipField                                   -- 13 Postcode-  <*> amexId                                      -- 14-  <*  skipField                                   -- 15 Unknown-  <*  skipLast                                    -- 16-  where-    f dt ds (t, a) p i = Y.Posting dt ds t a p i
− Penny/Brenner/BofA.hs
@@ -1,226 +0,0 @@--- | Parses statements for Bank of America deposit accounts. See the--- help text in the 'help' function for more details. Also, the file--- format is documented in the file @doc\/bofa-file-format.org@.-module Penny.Brenner.BofA (parser, getPayee) where--import Control.Applicative ((<$>), (<*), (<$), (<*>))-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.Char (isUpper)-import qualified Data.Time as T-import qualified Text.Parsec as P-import Text.Parsec (char, string, many, many1, satisfy, manyTill,-                    (<?>), try)-import Text.Parsec.String (Parser)-import qualified Data.Tree as T-import Data.Tree (Tree(Node))-import qualified Penny.Brenner.Types as Y-import qualified Penny.Lincoln as L-import qualified Data.Text as X--newtype TagName = TagName { unTagName :: String }-  deriving (Eq, Show)--newtype TagData = TagData { unTagData :: String }-  deriving (Eq, Show)--data Label-  = Parent TagName-  | Terminal TagName TagData-  deriving (Eq, Show)--type ExS = Ex.Exceptional String--bOfAFile :: Parser ([(TagName, TagData)], Tree Label)-bOfAFile =-  (,)-  <$> many headerLine-  <*  string "\r\n"-  <*> node--notReturn :: Parser Char-notReturn = satisfy (/= '\r')--headerLine :: Parser (TagName, TagData)-headerLine =-  (,)-  <$> (TagName <$> manyTill (satisfy isUpper) (char ':'))-  <*> (TagData <$> manyTill notReturn (char '\r')-               <*  char '\n')--openTag :: Parser String-openTag = do-  { let pc = (satisfy (\c -> c /= '/' && c /= '>'))-  ; c <- try (char '<' >> pc)-  ; cs <- many pc-  ; _ <- char '>'-  ; return (c:cs)-  } <?> "open tag"--closeTag :: String -> Parser ()-closeTag s = () <$ string "</" <* string s <* char '>'-             <?> "close tag named " ++ s---- | Reads in a tag, then examine what's next. If a backslash-r is--- next, then this is the end of the line. That means it a nested--- tag. Parse some more child nodes, then parse a closing node. If--- anything else is next, this is a data node. Parse the data, then--- return that node.-node :: Parser (Tree Label)-node = do-  tagName <- openTag-  next <- P.anyChar-  case next of-    '\r' -> do-      _ <- char '\n'-      kids <- many1 node-      closeTag tagName-      _ <- string "\r\n"-      return $ T.Node (Parent (TagName tagName)) kids-    o -> do-      rs <- manyTill notReturn (char '\r')-      _ <- char '\n'-      return $-        T.Node (Terminal (TagName tagName) (TagData $ o:rs)) []--findNodes :: Eq a => a -> Tree a -> [Tree a]-findNodes a = findNodesBy (== a)---findNodesBy :: (a -> Bool) -> Tree a -> [Tree a]-findNodesBy f t@(Node l cs)-  | f l = [t]-  | otherwise = concatMap (findNodesBy f) cs--safeRead :: (Read r) => String -> Maybe r-safeRead s = case reads s of-  (i,""):[] -> Just i-  _ -> Nothing---- | Parses a B of A date-time. The format is YYYYMMDDHHMMSS. Discards--- the HHMMSS.-parseDateStr :: String -> ExS Y.Date-parseDateStr s =-  let (yr, r1) = splitAt 4 s-      (mo, r2) = splitAt 2 r1-      (da, _) = splitAt 2 r2-  in Ex.fromMaybe ("could not parse date: " ++ s) $ do-      yi <- safeRead yr-      ym <- safeRead mo-      yd <- safeRead da-      Y.Date <$> T.fromGregorianValid yi ym yd--parseAmountStr :: String -> ExS (Y.IncDec, Y.Amount)-parseAmountStr s = do-  (f, rs) <- case s of-    "" -> Ex.throw "empty string for amount"-    x:xs -> return (x, xs)-  let (amtStr, incDec) = case f of-        '-' -> (rs, Y.Decrease)-        _ -> (s, Y.Increase)-  amt <- Ex.fromMaybe ("could not parse amount: " ++ s)-         $ Y.mkAmount amtStr-  return (incDec, amt)--postings :: Tree Label -> ExS [Y.Posting]-postings t =-  let match = Parent (TagName "STMTTRN")-  in mapM posting .findNodes match $ t--posting :: Tree Label -> ExS Y.Posting-posting (Node l cs) = do-  tag <- case l of-    Parent n -> return n-    _ -> Ex.throw "did not find posting tree"-  Ex.assert "did not find STMTTRN tag" $ unTagName tag == "STMTTRN"-  tPosted <- findTerminal "DTPOSTED" cs-  tAmt <- findTerminal "TRNAMT" cs-  tId <- findTerminal "FITID" cs-  tName <- findTerminal "NAME" cs-  pPosted <- parseDateStr (X.unpack tPosted)-  (amtIncDec, pAmt) <- parseAmountStr (X.unpack tAmt)-  let pId = Y.FitId tId-      pName = Y.Desc tName-      pPayee = Y.Payee (X.empty)-  return $ Y.Posting pPosted pName amtIncDec pAmt pPayee pId---- | Removes the TagData from a tree, after ensuring that the TagName--- is correct and that the tree has no children.-terminalData-  :: String-  -- ^ The name of the terminal--  -> Tree Label--  -> ExS X.Text-  -- ^ Returns the data from the tag, or an error if this is not a-  -- terminal or if the terminal has children.-terminalData n (Node l cs) = do-  (tn, td) <- case l of-    Parent _ -> Ex.throw $ "looking for data tag named " ++ n-                           ++ ", but that tag does not have data"-    Terminal x y -> return (x, y)-  let tagErr = "looking for tag named " ++ n-        ++ ", but found tag named " ++ unTagName tn-  Ex.assert tagErr $ tn == TagName n-  let kidsErr = "data tag " ++ n ++ " should have no children,"-                ++ " but does"-  Ex.assert kidsErr $ null cs-  return . X.pack . unTagData $ td---- | Finds a terminal amongst a list of Trees; returns the data. Fails--- if there is no terminal by the given name or of the terminal has--- children (in which case it is not a terminal!)-findTerminal-  :: String-  -- ^ The name of the terminal--  -> [Tree Label]-  -> ExS X.Text-  -- ^ Returns the data from the terminal, or an error if there is no-  -- tag by this name or if it has chlidren.--findTerminal n ts = do-  let pdct lbl = case lbl of-        Terminal (TagName x) _ -> x == n-        _ -> False-  t <- case concatMap (findNodesBy pdct) ts of-    [] -> Ex.throw $ "looking for terminal named "-          ++ n ++ "; none found"-    x:[] -> return x-    _ -> Ex.throw $ "looking for terminal named "-         ++ n ++ "; multiple matches found"-  terminalData n t--help :: String-help = unlines-  [ "Parses Bank of America postings for deposit accounts, like checking"-  , "or savings. This parser is not tested with credit card accounts."-  , "To download the data, from the account activity screen click on"-  , "\"Download\", which is just above all the transaction information."-  , "Then download the \"WEB Connect for Quicken 2010 and above.\""-  ]--parser :: (String, Y.FitFileLocation-                   -> IO (Ex.Exceptional String [Y.Posting]))-parser = (help, psr)-  where-    psr (Y.FitFileLocation path) = do-      str <- readFile path-      return $ case P.parse bOfAFile "" str of-        Left e -> Ex.throw-                  $ "could not parse Bank of America transactions: "-                    ++ show e-        Right (_, t) -> postings t---- | For check card transactions, Bank of America changes the--- description so that it begins with @CHECKCARD MMDD@, where MM is--- the two-digit month of the transaction date, and DD is the--- two-digit day of the transaction date. (The posting date is used as--- the main date, and it is typically two days later than the--- transaction date.) This function strips off the @CHECKCARD MMDD@--- portion.-getPayee :: Y.Desc -> L.Payee-getPayee (Y.Desc d) = L.Payee $-  if X.isPrefixOf (X.pack "CHECKCARD") d-  then X.drop (length "CHECKCARD XXXX ") d-  else d
− Penny/Brenner/Clear.hs
@@ -1,175 +0,0 @@-module Penny.Brenner.Clear (mode) where--import qualified Control.Monad.Exception.Synchronous as Ex-import Control.Applicative (pure)-import Control.Monad (guard, mzero, when)-import Data.Maybe (mapMaybe, fromMaybe)-import Data.Monoid (mconcat, First(..))-import qualified Data.Set as Set-import qualified Data.Map as M-import qualified Data.Text as X-import qualified Data.Text.IO as TIO-import qualified System.Console.MultiArg as MA-import qualified Penny.Lincoln as L-import qualified Control.Monad.Trans.State as St-import qualified Control.Monad.Trans.Maybe as MT-import Control.Monad.Trans.Class (lift)-import qualified Penny.Copper.Types as Y-import qualified Penny.Copper as C-import qualified Penny.Copper.Render as R-import Text.Show.Pretty (ppShow)-import qualified Penny.Brenner.Types as Y-import qualified Penny.Brenner.Util as U---help :: String -> String-help pn = unlines-  [ "usage: " ++ pn ++ " clear clear [options] FIT_FILE LEDGER_FILE..."-  , "Parses all postings that are in FIT_FILE. Then marks all"-  , "postings that are in the FILEs given that correspond to one"-  , "of the postings in the FIT_FILE as being cleared."-  , "Quits if one of the postings found in FIT_FILE is not found"-  , "in the database, if one of the postings in the database"-  , "is not found in one of the FILEs, or if any of the postings found"-  , "in one of the FILEs already has a flag."-  , ""-  , "Results are printed to standard output. If no FILE, or FILE is \"-\","-  , "read standard input."-  , ""-  , "Options:"-  , "  -h, --help - show help and exit"-  ]--data Arg-  = APosArg String-  deriving (Eq, Show)--toPosArg :: Arg -> Maybe String-toPosArg a = case a of { APosArg s -> Just s }--data Opts = Opts-  { csvLocation :: Y.FitFileLocation-  , ledgerLocations :: [String]-  } deriving Show---mode :: Maybe Y.FitAcct -> MA.Mode (IO ())-mode c = MA.Mode-  { MA.mName = "clear"-  , MA.mIntersperse = MA.Intersperse-  , MA.mOpts = [ ]-  , MA.mPosArgs = APosArg-  , MA.mProcess = process c-  , MA.mHelp = help-  }--process :: Maybe Y.FitAcct -> [Arg] -> IO ()-process mayC as = do-  c <- case mayC of-    Just cd -> return cd-    Nothing -> fail $ "no financial institution account given"-               ++ " on command line, and no default financial"-               ++ " institution configured."-  (csv, ls) <- case mapMaybe toPosArg as of-    [] -> fail "clear: you must provide a postings file."-    x:xs -> return (Y.FitFileLocation x, xs)-  let os = Opts csv ls-  runClear c os--runClear :: Y.FitAcct -> Opts -> IO ()-runClear c os = do-  dbList <- U.loadDb (Y.AllowNew False) (Y.dbLocation c)-  let db = M.fromList dbList-      (_, prsr) = Y.parser c-  txns <- fmap (Ex.switch fail return) $ prsr (csvLocation os)-  leds <- C.open (ledgerLocations os)-  toClear <- case mapM (findUNumber db) (concat txns) of-    Nothing -> fail $ "at least one posting was not found in the"-                       ++ " database. Ensure all postings have "-                       ++ "been imported and merged."-    Just ls -> return $ Set.fromList ls-  let (led', left) = changeLedger (Y.pennyAcct c) toClear leds-  when (not (Set.null left))-    (fail $ "some postings were not cleared. "-      ++ "Those not cleared:\n" ++ ppShow left)-  case R.ledger (Y.groupSpecs c) led' of-    Nothing ->-      fail "could not render resulting ledger."-    Just txt -> TIO.putStr txt----- | Examines an financial institution transaction and the DbMap to--- find a matching UNumber. Fails if the financial institution--- transaction is not in the Db.-findUNumber :: Y.DbMap -> Y.Posting -> Maybe Y.UNumber-findUNumber m pstg =-  let atn = Y.fitId pstg-      p ap = Y.fitId ap == atn-      filteredMap = M.filter p m-      ls = M.toList filteredMap-  in case ls of-      (n, _):[] -> Just n-      _ -> Nothing---clearedFlag :: L.Flag-clearedFlag = L.Flag . X.singleton $ 'C'---- | Changes a ledger to clear postings. Returns postings still not--- cleared.-changeLedger-  :: Y.PennyAcct-  -> Set.Set Y.UNumber-  -> Y.Ledger-  -> (Y.Ledger, Set.Set Y.UNumber)-changeLedger ax s l = St.runState k s-  where-    k = Y.mapLedgerA f l-    f = Y.mapItemA pure pure (changeTxn ax)--changeTxn-  :: Y.PennyAcct-  -> L.Transaction-  -> St.State (Set.Set Y.UNumber) L.Transaction-changeTxn ax t = do-  let fam = L.unTransaction t-      fam' = L.mapParent (const L.emptyTopLineChangeData) fam-  fam'' <- L.mapChildrenA (changePstg ax) fam'-  return $ L.changeTransaction fam'' t----- | Sees if this posting is a posting in the right account and has a--- UNumber that needs to be cleared. If so, clears it. If this posting--- already has a flag, skips it.-changePstg-  :: Y.PennyAcct-  -> L.Posting-  -> St.State (Set.Set Y.UNumber) L.PostingChangeData-changePstg ax p =-  fmap (fromMaybe L.emptyPostingChangeData) . MT.runMaybeT $ do-    guard (L.pAccount p == (Y.unPennyAcct ax))-    let tags = L.pTags p-    un <- maybe mzero return $ parseUNumberFromTags tags-    guard (L.pFlag p == Nothing)-    set <- lift St.get-    guard (Set.member un set)-    lift $ St.put (Set.delete un set)-    return $ L.emptyPostingChangeData-             { L.pcFlag = Just (Just clearedFlag) }--parseUNumberFromTags :: L.Tags -> Maybe Y.UNumber-parseUNumberFromTags =-  getFirst-  . mconcat-  . map First-  . map parseUNumberFromTag-  . L.unTags--parseUNumberFromTag :: L.Tag -> Maybe Y.UNumber-parseUNumberFromTag (L.Tag x) = do-  (f, xs) <- X.uncons x-  guard (f == 'U')-  case reads . X.unpack $ xs of-    (u, ""):[] -> Just (Y.UNumber u)-    _ -> Nothing-
− Penny/Brenner/Database.hs
@@ -1,49 +0,0 @@-module Penny.Brenner.Database (mode) where--import qualified Penny.Brenner.Types as Y-import qualified Penny.Brenner.Util as U-import qualified System.Console.MultiArg as MA--help :: String -> String-help pn = unlines-  [ "usage: " ++ pn ++ " [global-options] database [local-options]"-  , "Shows the database of financial institution transactions."-  , "Does not accept any non-option arguments."-  , ""-  , "Local options:"-  , "  --help, -h Show this help and exit."-  ]--data Arg = ArgPos String deriving (Eq, Show)--mode-  :: Maybe Y.FitAcct-  -> MA.Mode (IO ())-mode mayFa = MA.Mode-  { MA.mName = "database"-  , MA.mIntersperse = MA.Intersperse-  , MA.mOpts = [ ]-  , MA.mPosArgs = ArgPos-  , MA.mProcess = processor mayFa-  , MA.mHelp = help-  }--processor-  :: Maybe Y.FitAcct-  -> [Arg]-  -> IO ()-processor mayFa ls-  | any isArgPos ls = fail $-        "penny-fit database: error: this command does"-        ++ " not accept non-option arguments."-  | otherwise = case mayFa of-      Nothing -> fail $ "no financial institution account"-        ++ " selected on command line, and no default"-        ++ " financial instititution account configured."-      Just fa -> do-        let dbLoc = Y.dbLocation fa-        db <- U.loadDb (Y.AllowNew False) dbLoc-        mapM_ putStr . map U.showDbPair $ db--isArgPos :: Arg -> Bool-isArgPos (ArgPos _) = True
− Penny/Brenner/Import.hs
@@ -1,106 +0,0 @@-module Penny.Brenner.Import (mode) where--import Control.Monad.Exception.Synchronous as Ex-import Data.Maybe (mapMaybe)-import qualified System.Console.MultiArg as MA-import qualified Penny.Brenner.Types as Y-import qualified Penny.Brenner.Util as U---data Arg-  = AFitFile String-  | AAllowNew-  deriving (Eq, Show)--toFitFile :: Arg -> Maybe String-toFitFile a = case a of-  AFitFile s -> Just s-  _ -> Nothing--data ImportOpts = ImportOpts-  { fitFile :: Y.FitFileLocation-  , allowNew :: Y.AllowNew-  , parser :: Y.FitFileLocation-              -> IO (Ex.Exceptional String [Y.Posting])-  }--mode-  :: Maybe Y.FitAcct-  -> MA.Mode (IO ())-mode mayFa = MA.Mode-  { MA.mName = "import"-  , MA.mIntersperse = MA.Intersperse-  , MA.mOpts =-      [ MA.OptSpec ["new"] "n" (MA.NoArg AAllowNew) ]-  , MA.mPosArgs = AFitFile-  , MA.mProcess = processor mayFa-  , MA.mHelp = help-  }--processor-  :: Maybe Y.FitAcct-  -> [Arg]-  -> IO ()-processor mayFa as = do-  (dbLoc, prsr) <- case mayFa of-    Nothing -> fail $ "no financial institution account provided"-      ++ " on command line, and no default financial institution"-      ++ " account is configured."-    Just fa -> return (Y.dbLocation fa, snd . Y.parser $ fa)-  loc <- case mapMaybe toFitFile as of-    [] -> fail "you must provide a postings file to read"-    x:[] -> return (Y.FitFileLocation x)-    _ -> fail "you cannot provide more than one postings file to read"-  let aNew = Y.AllowNew $ any (== AAllowNew) as-  doImport dbLoc (ImportOpts loc aNew prsr)----- | Appends new Amex transactions to the existing list.-appendNew-  :: [(Y.UNumber, Y.Posting)]-  -- ^ Existing transactions--  -> [Y.Posting]-  -- ^ New transactions--  -> ([(Y.UNumber, Y.Posting)], Int)-  -- ^ New list, and number of transactions added--appendNew db new = (db ++ newWithU, length newWithU)-  where-    nextUNum = if null db-               then 0-               else (Y.unUNumber . maximum . map fst $ db) + 1-    currFitIds = map (Y.fitId . snd) db-    isNew p = not (any (== Y.fitId p) currFitIds)-    newPstgs = filter isNew new-    mkPair i p = (Y.UNumber i, p)-    newWithU = zipWith mkPair [nextUNum..] newPstgs---doImport :: Y.DbLocation -> ImportOpts -> IO ()-doImport dbLoc os = do-  txnsOld <- U.loadDb (allowNew os) dbLoc-  parseResult <- parser os (fitFile os)-  ins <- case parseResult of-    Ex.Exception e -> fail e-    Ex.Success g -> return g-  let (new, len) = appendNew txnsOld ins-  U.saveDb dbLoc new-  putStrLn $ "imported " ++ show len ++ " new transactions."--help :: String -> String-help pn = unlines-  [ "usage: " ++ pn ++ "  [global-options] import [local-options] FIT_FILE"-  , "where FIT_FILE is the file downloaded from the financial"-  , "institution."-  , ""-  , "Local Options:"-  , ""-  , "-n, --new - Allows creation of new database. Without this option,"-  , "if the database file is not found, quits with an error."-  , ""-  , "-h, --help - Show this help."-  , ""-  ]-
− Penny/Brenner/Merge.hs
@@ -1,347 +0,0 @@-module Penny.Brenner.Merge (mode) where--import Control.Applicative (pure)-import Control.Monad (guard)-import qualified Control.Monad.Trans.State as St-import Data.List (find, sortBy, foldl')-import qualified Data.Map as M-import Data.Maybe (mapMaybe, isNothing, fromMaybe)-import Data.Monoid (First(..), mconcat)-import qualified Data.Text as X-import qualified Data.Text.IO as TIO-import qualified System.Console.MultiArg as MA-import qualified Penny.Copper as C-import qualified Penny.Copper.Render as R-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Transaction.Unverified as U-import qualified Penny.Lincoln.Queries as Q-import qualified Penny.Brenner.Types as Y-import qualified Penny.Brenner.Util as U--type NoAuto = Bool--data Arg-  = APos String-  | ANoAuto-  deriving (Eq, Show)--toPosArg :: Arg -> Maybe String-toPosArg a = case a of { APos s -> Just s; _ -> Nothing }--mode :: Maybe Y.FitAcct -> MA.Mode (IO ())-mode maybeC = MA.Mode-  { MA.mName = "merge"-  , MA.mIntersperse = MA.Intersperse-  , MA.mOpts = [MA.OptSpec ["no-auto"] "n" (MA.NoArg ANoAuto)]-  , MA.mPosArgs = APos-  , MA.mProcess = processor maybeC-  , MA.mHelp = help-  }--processor :: Maybe Y.FitAcct -> [Arg] -> IO ()-processor maybeC as =-  doMerge maybeC (ANoAuto `elem` as) (mapMaybe toPosArg as)--doMerge :: Maybe Y.FitAcct -> NoAuto -> [String] -> IO ()-doMerge maybeAcct noAuto ss = do-  acct <- case maybeAcct of-    Nothing -> do-      fail $ "no financial"-        ++ " institution account provided on command line, and"-        ++ " no default account configured."-    Just ac -> return ac-  dbLs <- U.loadDb (Y.AllowNew False) (Y.dbLocation acct)-  l <- C.open ss-  let dbWithEntry = fmap (pairWithEntry acct) . M.fromList $ dbLs-      (l', db') = changeItems acct-                  l (filterDb (Y.pennyAcct acct) dbWithEntry l)-      newTxns = createTransactions noAuto acct l dbLs db'-      final = C.Ledger (C.unLedger l' ++ newTxns)-  case R.ledger (Y.groupSpecs acct) final of-    Nothing -> fail "Could not render final ledger."-    Just x -> TIO.putStr x---help :: String -> String-help pn = unlines-  [ "usage: " ++ pn ++ " merge: merges new transactions from database"-  , "to ledger file."-  , "usage: penny-fit merge [options] FILE..."-  , "Results are printed to standard output. If no FILE, or if FILE is -,"-  , "read standard input."-  , ""-  , "Options:"-  , "  -h, --help - show help and exit"-  , "  -n, --no-auto - do not automatically assign payees and accounts"-  ]---- | Removes all Brenner postings that already have a Penny posting--- with the correct uNumber.-filterDb :: Y.PennyAcct -> DbWithEntry -> C.Ledger -> DbWithEntry-filterDb ax m l = M.difference m ml-  where-    ml = M.fromList-       . flip zip (repeat ())-       . mapMaybe toUNum-       . filter inPennyAcct-       . concatMap L.postFam-       . mapMaybe toTxn-       . C.unLedger-       $ l-    toTxn t = case t of-      C.Transaction x -> Just x-      _ -> Nothing-    inPennyAcct p = Q.account p == (Y.unPennyAcct ax)-    toUNum p = getUNumberFromTags . Q.tags $ p---- | Gets the first UNumber from a list of Tags.-getUNumberFromTags :: L.Tags -> Maybe Y.UNumber-getUNumberFromTags =-  getFirst-  . mconcat-  . map First-  . map getUNumberFromTag-  . L.unTags---- | Examines a tag to see if it is a uNumber. If so, returns the--- UNumber. Otherwise, returns Nothing.-getUNumberFromTag :: L.Tag -> Maybe Y.UNumber-getUNumberFromTag (L.Tag x) = do-  (f, r) <- X.uncons x-  guard (f == 'U')-  case reads . X.unpack $ r of-    (y, ""):[] -> return $ Y.UNumber y-    _ -> Nothing----- | Changes a single Item.-changeItem-  :: Y.FitAcct-  -> C.Item-  -> St.State DbWithEntry C.Item-changeItem acct =-  C.mapItemA pure pure (changeTransaction acct)----- | Changes all postings that match an AmexTxn to assign them the--- proper UNumber. Returns a list of changed items, and the DbMap of--- still-unassigned AmexTxns.-changeItems-  :: Y.FitAcct-  -> C.Ledger-  -> DbWithEntry-  -> (C.Ledger, DbWithEntry)-changeItems acct l =-  St.runState (C.mapLedgerA (changeItem acct) l)---changeTransaction-  :: Y.FitAcct-  -> L.Transaction-  -> St.State DbWithEntry L.Transaction-changeTransaction acct txn = do-  let fam = L.unTransaction txn-      fam' = L.mapParent (const L.emptyTopLineChangeData) fam-  fam'' <- L.mapChildrenA (inspectAndChange acct txn) fam'-  return $ L.changeTransaction fam'' txn---- | Inspects a posting to see if it is an Amex posting and, if so,--- whether it matches one of the remaining AmexTxns. If so, then--- changes the transaction's UNumber, and remove that UNumber from the--- DbMap. If the posting alreay has a Number (UNumber or otherwise)--- skips it.-inspectAndChange-  :: Y.FitAcct-  -> L.Transaction-  -> L.Posting-  -> St.State DbWithEntry L.PostingChangeData-inspectAndChange acct t p = do-  m <- St.get-  case findMatch acct t p m of-    Nothing -> return L.emptyPostingChangeData-    Just (n, m') ->-      let L.Tags oldTags = L.pTags p-          tags' = L.Tags (oldTags ++ [newLincolnUNumber n])-          pcd = L.emptyPostingChangeData-                  { L.pcTags = Just tags' }-      in St.put m' >> return pcd--newLincolnUNumber :: Y.UNumber -> L.Tag-newLincolnUNumber a =-  L.Tag ('U' `X.cons` (X.pack . show . Y.unUNumber $ a))----- | Searches a DbMap for an AmexTxn that matches a given posting. If--- a match is found, returns the matching UNumber and a new DbMap that--- has the match removed.-findMatch-  :: Y.FitAcct-  -> L.Transaction-  -> L.Posting-  -> DbWithEntry-  -> Maybe (Y.UNumber, DbWithEntry)-findMatch acct t p m = fmap toResult findResult-  where-    findResult = find (pennyTxnMatches acct t p)-                 . M.toList $ m-    toResult (u, (_, _)) = (u, M.delete u m)---- | Pairs each association in a DbMap with an Entry representing the--- transaction's entry in the ledger.-pairWithEntry :: Y.FitAcct -> Y.Posting -> (Y.Posting, L.Entry)-pairWithEntry acct p = (p, en)-  where-    en = L.Entry dc (L.Amount qty cty (Just (Y.side acct))-                                      (Just (Y.spaceBetween acct)))-    dc = Y.translate (Y.incDec p) (Y.translator acct)-    qty = U.parseQty (Y.amount p)-    cty = Y.unCurrency . Y.currency $ acct--type DbWithEntry = M.Map Y.UNumber (Y.Posting, L.Entry)---- | Does the given Penny transaction match this posting? Makes sure--- that the account, quantity, date, commodity, and DrCr match, and--- that the posting does not have a number (it's OK if the transaction--- has a number.)-pennyTxnMatches-  :: Y.FitAcct-  -> L.Transaction-  -> L.Posting-  -> (a, (Y.Posting, L.Entry))-  -> Bool-pennyTxnMatches acct t p (_, (a, e)) =-  mA && noFlag && mQ && mDC && mDate && mCmdty-  where-    mA = L.pAccount p == (Y.unPennyAcct . Y.pennyAcct $ acct)-    mQ = L.equivalent (L.qty . L.amount . L.pEntry $ p)-                      (L.qty . L.amount $ e)-    mDC = (L.drCr e) == (L.drCr . L.pEntry $ p)-    (L.Family tl _ _ _) = L.unTransaction t-    mDate = (L.day . L.tDateTime $ tl) == (Y.unDate . Y.date $ a)-    noFlag = isNothing . L.pNumber $ p-    mCmdty = (L.commodity . L.amount $ e)-              == (Y.unCurrency . Y.currency $ acct)----- | Creates a new transaction corresponding to a given AmexTxn. Uses--- the Amex payee if that string is non empty; otherwise, uses the--- Amex description for the payee.-newTransaction-  :: NoAuto-  -> Y.FitAcct-  -> UNumberLookupMap-  -> PyeLookupMap-  -> (Y.UNumber, (Y.Posting, L.Entry))-  -> L.Transaction-newTransaction noAuto acct mu mp (u, (a, e)) = L.rTransaction rt-  where-    rt = L.RTransaction-      { L.rtCommodity = Y.unCurrency . Y.currency $ acct-      , L.rtSide = Just . Y.side $ acct-      , L.rtSpaceBetween = Just . Y.spaceBetween $ acct-      , L.rtDrCr = L.drCr e-      , L.rtTopLine = tl-      , L.rtPosting = p1-      , L.rtMorePostings = []-      , L.rtIPosting = p2-      }-    tl = (U.emptyTopLine ( L.dateTimeMidnightUTC . Y.unDate-                           . Y.date $ a))-         { U.tPayee = Just pa }-    getPye = Y.toLincolnPayee acct-    (guessedPye, guessedAcct) = guessInfo getPye mu mp a-    dfltPye = getPye (Y.desc a) (Y.payee a)-    dfltAcct = Y.unDefaultAcct . Y.defaultAcct $ acct-    (pa, ac) =-      if noAuto-      then (dfltPye, dfltAcct)-      else ( fromMaybe dfltPye guessedPye,-             fromMaybe dfltAcct guessedAcct)-    pennyAcct = Y.unPennyAcct . Y.pennyAcct $ acct-    p1 = (U.emptyRPosting pennyAcct (L.qty . L.amount $ e))-          { U.rTags = L.Tags [newLincolnUNumber u] }-    p2 = U.emptyIPosting ac---- | Creates new transactions for all the items remaining in the--- DbMap. Appends a blank line after each one.-createTransactions-  :: NoAuto-  -> Y.FitAcct-  -> C.Ledger-  -> Y.DbList-  -> DbWithEntry-  -> [C.Item]-createTransactions noAuto acct led dbLs db =-  concatMap (\i -> [i, C.BlankLine])-  . map C.Transaction-  . map (newTransaction noAuto acct mu mp)-  . M.assocs-  $ db-  where-    mu = makeUNumberLookup (Y.toLincolnPayee acct) dbLs-    mp = makePyeLookupMap (Y.pennyAcct acct) led---- | Maps financial institution postings to UNumbers. The key is the--- Lincoln Payee of the financial institution posting, which is--- computed using the toLincolnPayee function in the FitAcct.  The--- UNumbers are in a list, with UNumbers from most recent financial--- institution postings first.-type UNumberLookupMap = M.Map L.Payee [Y.UNumber]---- | Create a UNumberLookupMap from a DbWithEntry. Financial--- institution postings with higher U-numbers will come first.-makeUNumberLookup-  :: (Y.Desc -> Y.Payee -> L.Payee)-  -> Y.DbList-  -> UNumberLookupMap-makeUNumberLookup toPye = foldl' ins M.empty . map f . sortBy g-  where-    ins m (k, v) = M.alter alterer k m-      where alterer Nothing = Just [v]-            alterer (Just ls) = Just $ v:ls-    f (u, p) = (toPye (Y.desc p) (Y.payee p), u)-    g (_, p1) (_, p2) = compare (Y.date p1) (Y.date p2)---- | Given a list of keys, find the first key that is in the--- map. Returns Nothing if no key is in the map.-findFirstKey :: Ord k => M.Map k v -> [k] -> Maybe v-findFirstKey _ [] = Nothing-findFirstKey m (k:ks) = case M.lookup k m of-  Nothing -> findFirstKey m ks-  Just v -> Just v---- | Maps UNumbers to payees and accounts from the ledger.-type PyeLookupMap = M.Map Y.UNumber (Maybe L.Payee, Maybe L.Account)---- | Makes a payee lookup map. Puts those postings which match the--- PennyAcct and have a UNumber into the map. (If two postings match--- the PennyAcct and have the same UNumber, the one that appears later--- in the ledger file will be in the map.)-makePyeLookupMap :: Y.PennyAcct -> C.Ledger -> PyeLookupMap-makePyeLookupMap a l-  = M.fromList . mapMaybe f . concatMap L.postFam . mapMaybe toPstg-    . C.unLedger $ l-  where-    f pstg = do-      guard $ (Q.account pstg) == Y.unPennyAcct a-      u <- getUNumberFromTags . Q.tags $ pstg-      let (L.Child _ sib sibs _) = L.unPostFam pstg-          ac = if null sibs-               then Just (L.pAccount sib)-               else Nothing-      return (u, (Q.payee pstg, ac))-    toPstg i = case i of { C.Transaction t -> Just t; _ -> Nothing }---- | Given a UNumber and the maps, looks up the payee and account--- information from previous transactions if this information is--- available.-guessInfo-  :: (Y.Desc -> Y.Payee -> L.Payee)-  -> UNumberLookupMap-  -> PyeLookupMap-  -> Y.Posting-  -> (Maybe L.Payee, Maybe L.Account)-guessInfo getPye mu mp p = fromMaybe (Nothing, Nothing) $ do-  let pstgPayee = getPye (Y.desc p) (Y.payee p)-  unums <- M.lookup pstgPayee mu-  findFirstKey mp unums
− Penny/Brenner/Print.hs
@@ -1,65 +0,0 @@--- | Prints parsed transactions.------ TODO add support to this and other Brenner components for reading--- from stdin.-module Penny.Brenner.Print (mode) where--import qualified Penny.Brenner.Types as Y-import qualified Penny.Brenner.Util as U-import qualified System.Console.MultiArg as MA-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.Maybe (mapMaybe)--help :: String -> String-help pn = unlines-  [ "usage: " ++ pn ++ "  [global-options] print [local-options] FILE..."-  , "Parses the transactions in each FILE using the appropriate parser"-  , "and prints the parse result to standard output."-  , ""-  , "Local options:"-  , "  --help, -h Show this help and exit."-  ]--data Arg-  = ArgFile String-  deriving (Eq, Show)--mode-  :: Maybe Y.FitAcct-  -> MA.Mode (IO ())-mode mayFa = MA.Mode-  { MA.mName = "print"-  , MA.mIntersperse = MA.Intersperse-  , MA.mOpts = []-  , MA.mPosArgs = ArgFile-  , MA.mProcess = processor mayFa-  , MA.mHelp = help-  }--processor-  :: Maybe Y.FitAcct-  -> [Arg]-  -> IO ()-processor mayFa ls =-  case mayFa of-    Nothing -> fail $-      "no financial institution account"-      ++ " provided on command line, and no account"-      ++ " configured by default."-    Just fa -> doPrint (snd . Y.parser $ fa) ls--doPrint-  :: (Y.FitFileLocation -> IO (Ex.Exceptional String [Y.Posting]))-  -> [Arg]-  -> IO ()-doPrint prsr ls = mapM_ f . mapMaybe toFile $ ls-  where-    f file = do-      r <- prsr file-      case r of-        Ex.Exception s -> do-          fail $ "penny-fit print: error: " ++ s-        Ex.Success ps -> mapM putStr . map U.showPosting $ ps-    toFile a = case a of-      ArgFile s -> Just (Y.FitFileLocation s)-
− Penny/Brenner/Types.hs
@@ -1,298 +0,0 @@-module Penny.Brenner.Types-  ( Date(..)-  , IncDec(..)-  , UNumber(..)-  , FitId(..)-  , Payee(..)-  , Desc(..)-  , Amount(unAmount)-  , mkAmount-  , translate-  , DbMap-  , DbList-  , Posting(..)-  , DbLocation(..)-  , Name(..)-  , PennyAcct(..)-  , Translator(..)-  , DefaultAcct(..)-  , Currency(..)-  , FitAcct(..)-  , Config(..)-  , FitFileLocation(..)-  , AllowNew(..)-  ) where--import Control.Applicative ((<$>), (<*>))-import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Data.Map as M-import qualified Data.Time as Time-import qualified Penny.Copper.Render as R-import qualified Penny.Lincoln as L-import Data.Text (Text, pack, unpack)-import qualified Data.Text.Encoding as E-import qualified Data.Serialize as S---- | The date reported by the financial institution.-newtype Date = Date { unDate :: Time.Day }-  deriving (Eq, Show, Ord, Read)--instance S.Serialize Date where-  put = S.put . show . unDate-  get = Date <$> (read <$> S.get)---- | Reports changes in account balances. Avoids using /debit/ and--- /credit/ as these terms are used differently by the bank than in--- your ledger (that is, the bank reports it from their perspective,--- not yours) so instead the terms /increase/ and /decrease/ are--- used. IncDec is used to record the bank's transactions so--- /increase/ and /decrease/ are used in the same way you would see--- them on a bank statement, whether it's a credit card, loan,--- checking account, etc.-data IncDec-  = Increase-  -- ^ Increases the account balance. For a checking or savings-  -- account, this is a deposit. For a credit card, this is a purchase.--  | Decrease-  -- ^ Decreases the account balance. On a credit card, this is a-  -- payment. On a checking account, this is a withdrawal.-  deriving (Eq, Show, Read)--instance S.Serialize IncDec where-  put x = case x of-    Increase -> S.putWord8 0-    Decrease -> S.putWord8 1-  get = S.getWord8 >>= f-    where-      f x = case x of-        0 -> return Increase-        1 -> return Decrease-        _ -> fail "read IncDec error"---- | A unique number assigned by Brenner to identify each--- posting. This is unique within a particular financial institution--- account only.-newtype UNumber = UNumber { unUNumber :: Integer }-  deriving (Eq, Show, Ord, Read)--instance S.Serialize UNumber where-  put = S.put . unUNumber-  get = UNumber <$> S.get--putText :: Text -> S.Put-putText = S.put . E.encodeUtf8--getText :: S.Get Text-getText = S.get >>= f-  where-    f bs = case E.decodeUtf8' bs of-      Left _ -> fail "text reading failed"-      Right x -> return x----- | For Brenner to work, the bank has to assign unique identifiers to--- each transaction that it gives you for download. This is the--- easiest reliable way to ensure duplicates are not processed--- multiple times. (There are other ways to accomplish this, but they--- are much harder and less reliable.) If the bank does not do this,--- you can't use Brenner.-newtype FitId = FitId { unFitId :: Text }-  deriving (Eq, Show, Ord, Read)--instance S.Serialize FitId where-  put = putText . unFitId-  get = FitId <$> getText---- | Some financial institutions assign a separate Payee in addition--- to a description. Others just have a single Description field. If--- this institution uses both, put something here. Brenner will prefer--- the Payee if it is not zero length; then it will use the Desc.-newtype Payee = Payee { unPayee :: Text }-  deriving (Eq, Show, Ord, Read)--instance S.Serialize Payee where-  put = putText . unPayee-  get = Payee <$> getText---- | The transaction description. Some institutions assign only a--- description (sometimes muddling a payee with long codes, some--- dates, etc). Brenner prefers the Payee if there is one, and uses a--- Desc otherwise.-newtype Desc =-  Desc { unDesc :: Text }-  deriving (Eq, Show, Ord, Read)--instance S.Serialize Desc where-  put = putText . unDesc-  get = Desc <$> getText---- | The amount of the transaction. Do not include any leading plus or--- minus signs; this should be only digits and a decimal point.-newtype Amount = Amount { unAmount :: Text }-  deriving (Eq, Show, Ord, Read)--instance S.Serialize Amount where-  put = putText . unAmount-  get = getText >>= f-    where-      f x = case mkAmount . unpack $ x of-        Nothing -> fail $ "failed to load amount: " ++ unpack x-        Just a -> return a---- | Ensures that incoming Amounts have only digits and (up to) one--- decimal point.-mkAmount :: String -> Maybe Amount-mkAmount s =-  let isDigit c = c >= '0' && c <= '9'-      (_, rs) = span isDigit s-  in case rs of-      "" -> if not . null $ s-            then return . Amount . pack $ s-            else Nothing-      '.':rest -> if all isDigit rest-                  then return . Amount . pack $ s-                  else Nothing-      _ -> Nothing--translate-  :: IncDec-  -> Translator-  -> L.DrCr-translate Increase IncreaseIsDebit = L.Debit-translate Increase IncreaseIsCredit = L.Credit-translate Decrease IncreaseIsDebit = L.Credit-translate Decrease IncreaseIsCredit = L.Debit--type DbMap = M.Map UNumber Posting-type DbList = [(UNumber, Posting)]--data Posting = Posting-  { date :: Date-  , desc :: Desc-  , incDec :: IncDec-  , amount :: Amount-  , payee :: Payee-  , fitId :: FitId-  } deriving (Read, Show)---instance S.Serialize Posting where-  put x = S.put (date x)-          >> S.put (desc x)-          >> S.put (incDec x)-          >> S.put (amount x)-          >> S.put (payee x)-          >> S.put (fitId x)-  get = Posting-        <$> S.get-        <*> S.get-        <*> S.get-        <*> S.get-        <*> S.get-        <*> S.get---- | Where is the database of postings?-newtype DbLocation = DbLocation { unDbLocation :: Text }-  deriving (Eq, Show)---- | A name used to refer to a batch of settings.-newtype Name = Name { unName :: Text }-  deriving (Eq, Show)---- | The Penny account holding postings for this financial--- institution. For instance it might be @Assets:Checking@ if this is--- your checking account, @Liabilities:Credit Card@, or whatever.-newtype PennyAcct = PennyAcct { unPennyAcct :: L.Account }-  deriving (Eq, Show)---- | What the financial institution shows as an increase or decrease--- has to be recorded as a debit or credit in the PennyAcct.-data Translator-  = IncreaseIsDebit-  -- ^ That is, when the financial institution shows a posting that-  -- increases your account balance, you record a debit. You will-  -- probably use this for deposit accounts, like checking and-  -- savings. These are asset accounts so if the balance goes up you-  -- record a debit in your ledger.--  | IncreaseIsCredit-  -- ^ That is, when the financial institution shows a posting that-  -- increases your account balance, you record a credit. You will-  -- probably use this for liabilities, such as credit cards and other-  -- loans.--  deriving (Eq, Show)---- | The default account to place unclassified postings in. For--- instance @Expenses:Unclassified@.-newtype DefaultAcct = DefaultAcct { unDefaultAcct :: L.Account }-  deriving (Eq, Show)---- | The currency for all transactions, e.g. @$@.-newtype Currency = Currency { unCurrency :: L.Commodity }-  deriving (Eq, Show)---- | A batch of settings representing a single financial institution--- account.-data FitAcct = FitAcct-  { dbLocation :: DbLocation-  , pennyAcct :: PennyAcct-  , defaultAcct :: DefaultAcct-  , currency :: Currency-  , groupSpecs :: R.GroupSpecs-  , translator :: Translator--  , side :: L.Side-  -- ^ When creating new transactions, the commodity will be on this-  -- side--  , spaceBetween :: L.SpaceBetween-  -- ^ When creating new transactions, is there a space between the-  -- commodity and the quantity--  , parser :: ( String-              , FitFileLocation -> IO (Ex.Exceptional String [Posting]))-  -- ^ Parses a file of transactions from the financial-  -- institution. The function must open the file and parse it. This-  -- is in the IO monad not only because the function must open the-  -- file itself, but also so the function can perform arbitrary IO-  -- (run pdftotext, maybe?) If there is failure, the function can-  -- return an Exceptional String, which is the error-  -- message. Alternatively the function can raise an exception in the-  -- IO monad (currently Brenner makes no attempt to catch these) so-  -- if any of the IO functions throw you can simply not handle the-  -- exceptions.-  ---  -- The first element of the pair is a help string which should-  -- indicate how to download the data, as a helpful reminder.--  , toLincolnPayee :: Desc -> Payee -> L.Payee-  -- ^ Sometimes the financial institution provides Payee information,-  -- sometimes it does not. Sometimes the Desc might have additional-  -- information that you might want to remove. This function can be-  -- used to do that. The resulting Lincoln Payee is used for any-  -- transactions that are created by the merge command. The resulting-  -- payee is also used when comparing new financial institution-  -- postings to already existing ledger transactions in order to-  -- guess at which payee and accounts to create in the transactions-  -- created by the merge command.--  }---- | Configuration for the Brenner program. You can optionally have--- a default FitAcct, which is used if you do not specify any FitAcct on the--- command line. You can also name any number of additional FitAccts. If--- you do not specify a default FitAcct, you must specify a FitAcct on the--- command line.--data Config = Config-  { defaultFitAcct :: Maybe FitAcct-  , moreFitAccts :: [(Name, FitAcct)]-  }--newtype FitFileLocation = FitFileLocation { unFitFileLocation :: String }-  deriving (Show, Eq)--newtype AllowNew = AllowNew { unAllowNew :: Bool }-  deriving (Show, Eq)
− Penny/Brenner/Util.hs
@@ -1,87 +0,0 @@-module Penny.Brenner.Util where--import Control.Monad.Exception.Synchronous as Ex-import qualified Penny.Brenner.Types as Y-import qualified Data.ByteString as BS-import qualified System.IO.Error as IOE-import qualified Data.Serialize as S-import qualified Data.Text as X-import qualified Penny.Copper.Parsec as CP-import qualified Text.Parsec as P-import qualified Penny.Lincoln as L---- | Loads the database from disk. If allowNew is True, then does not--- fail if the file was not found.-loadDb-  :: Y.AllowNew-  -- ^ Is a new file allowed?--  -> Y.DbLocation-  -- ^ DB location--  -> IO Y.DbList-loadDb (Y.AllowNew allowNew) (Y.DbLocation dbLoc) = do-  eiStr <- IOE.tryIOError (BS.readFile . X.unpack $ dbLoc)-  case eiStr of-    Left e ->-      if allowNew && IOE.isDoesNotExistError e-      then return []-      else IOE.ioError e-    Right g -> case readDbTuple g of-      Ex.Exception e -> fail e-      Ex.Success good -> return good---- | File version. Increment this when anything in the file format--- changes.-version :: Int-version = 0--brenner :: String-brenner = "penny.brenner"--readDbTuple-  :: BS.ByteString-  -> Ex.Exceptional String Y.DbList-readDbTuple bs = do-  (s, v, ls) <- Ex.fromEither $ S.decode bs-  Ex.assert "database file format not recognized." $ s == brenner-  Ex.assert "wrong database version." $ v == version-  return ls--saveDbTuple :: Y.DbList -> BS.ByteString-saveDbTuple ls = S.encode (brenner, version, ls)---- | Writes a new database to disk.-saveDb :: Y.DbLocation -> Y.DbList -> IO ()-saveDb (Y.DbLocation p) = BS.writeFile (X.unpack p) . saveDbTuple---- | Parses quantities from amounts. All amounts should be verified as--- having only digits, optionally followed by a point and then more--- digits. All these values should parse. So if there is a problem it--- is a programmer error. Apply error.-parseQty :: Y.Amount -> L.Qty-parseQty a = case P.parse CP.quantity "" (Y.unAmount a) of-  Left e -> error $ "could not parse quantity from string: "-            ++ (X.unpack . Y.unAmount $ a) ++ ": " ++ show e-  Right g -> g--label :: String -> X.Text -> String-label s x = s ++ ": " ++ X.unpack x ++ "\n"---- | Shows a Posting in human readable format.-showPosting :: Y.Posting -> String-showPosting (Y.Posting dt dc nc am py fd) =-  label "Date" (X.pack . show . Y.unDate $ dt)-  ++ label "Description" (Y.unDesc dc)-  ++ label "Type" (X.pack $ case nc of-                    Y.Increase -> "increase"-                    Y.Decrease -> "decrease")-  ++ label "Amount" (Y.unAmount am)-  ++ label "Payee" (Y.unPayee py)-  ++ label "Financial institution ID" (Y.unFitId fd)-  ++ "\n"--showDbPair :: (Y.UNumber, Y.Posting) -> String-showDbPair (Y.UNumber u, p) =-  label "U number" (X.pack . show $ u)-  ++ showPosting p
− Penny/Cabin.hs
@@ -1,7 +0,0 @@--- | Cabin - Penny reports------ Cabin contains reports, or functions that take a list of postings--- and return a formatted Text to display data in a human-readable--- format.-module Penny.Cabin where-
− Penny/Cabin/Balance.hs
@@ -1,21 +0,0 @@--- | Penny balance reports. Currently there are two balance reports:--- the MultiCommodity report, which cannot convert commodities and--- which therefore might show more than one commodity in a single--- report, and the Convert report, which uses price data in the Penny--- file to convert all commodities to a single commodity. The Convert--- report always displays only one commodity per account and this one--- commodity for the whole report.-module Penny.Cabin.Balance where--import qualified Penny.Cabin.Balance.MultiCommodity as MC-import qualified Penny.Cabin.Interface as I-import qualified Penny.Cabin.Balance.Convert as C-import qualified Penny.Cabin.Balance.Convert.Options as ConvOpts---- | The default multi-commodity balance report.-multiCommodity :: I.Report-multiCommodity = MC.defaultReport---- | The default converting balance report.-convert :: I.Report-convert = C.cmdLineReport ConvOpts.defaultOptions
− Penny/Cabin/Balance/Convert.hs
@@ -1,334 +0,0 @@--- | The Convert report. This report converts all account balances to--- a single commodity, which must be specified.--module Penny.Cabin.Balance.Convert (-  Opts(..)-  , Sorter-  , report-  , cmdLineReport-  , getSorter-  ) where--import Control.Applicative ((<$>), (<*>))-import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Data.Tree as E-import qualified Data.Traversable as Tvbl-import qualified Penny.Cabin.Options as CO-import qualified Penny.Cabin.Parsers as CP-import qualified Penny.Cabin.Scheme as Scheme-import qualified Penny.Cabin.Balance.Util as U-import qualified Penny.Cabin.Balance.Convert.Chunker as K-import qualified Penny.Cabin.Balance.Convert.Options as O-import qualified Penny.Cabin.Balance.Convert.Parser as P-import qualified Penny.Cabin.Interface as I-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Balance as Bal-import qualified Penny.Liberty as Ly-import qualified Penny.Shield as S-import qualified Data.Either as Ei-import qualified Data.Map as M-import qualified Data.Text as X-import Data.Monoid (mempty, mappend, mconcat)-import qualified System.Console.MultiArg as MA-import qualified System.Console.Rainbow as Rb---- | Options for the Convert report. These are the only options you--- need to use if you are supplying options programatically (as--- opposed to parsing them in from the command line.)-data Opts = Opts-  { balanceFormat :: L.Commodity -> L.Qty -> X.Text-  , showZeroBalances :: CO.ShowZeroBalances-  , sorter :: Sorter-  , target :: L.To-  , dateTime :: L.DateTime-  , textFormats :: Scheme.Changers-  }---- | How to sort each line of the report. Each subaccount has only one--- BottomLine (unlike in the MultiCommodity report, where each--- subaccount may have more than one BottomLine, one for each--- commodity.)-type Sorter =-  (L.SubAccount, L.BottomLine)-  -> (L.SubAccount, L.BottomLine)-  -> Ordering---- | Converts all commodities in a Balance to a single commodity and--- combines all the BottomLines into one. Fails with an error message--- if no conversion data is available.-convertBalance ::-  L.PriceDb-  -> L.DateTime-  -> L.To-  -> L.Balance-  -> Ex.Exceptional X.Text L.BottomLine-convertBalance db dt to bal = fmap mconcat r-  where-    r = mapM (convertOne db dt to) . M.assocs . L.unBalance $ bal---- | Converts a single BottomLine to a new commodity. Fails with an--- error message if no conversion data is available.-convertOne ::-  L.PriceDb-  -> L.DateTime-  -> L.To-  -> (L.Commodity, L.BottomLine)-  -> Ex.Exceptional X.Text L.BottomLine-convertOne db dt to (cty, bl) =-  case bl of-    L.Zero -> return L.Zero-    L.NonZero (L.Column dc qt) -> Ex.mapExceptional e g ex-      where-        ex = L.convert db dt to am-        am = L.Amount qt cty Nothing Nothing-        e = convertError to (L.From cty)-        g r = L.NonZero (L.Column dc r)---- | Creates an error message for conversion errors.-convertError ::-  L.To-  -> L.From-  -> L.PriceDbError-  -> X.Text-convertError (L.To to) (L.From fr) e =-  let fromErr = L.unCommodity fr-      toErr = 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")----- | Create a price database.-buildDb :: [L.PricePoint] -> L.PriceDb-buildDb = foldl f L.emptyDb where-  f db pb = L.addPrice db pb---- | All data for the report after all balances have been converted to--- a single commodity and all the sums of the child accounts have been--- added to the parent accounts.-data ForestAndBL = ForestAndBL {-  _tbForest :: E.Forest (L.SubAccount, L.BottomLine)-  , _tbTotal :: L.BottomLine-  , _tbTo :: L.To-  }---- | Converts the balance data in preparation for screen rendering.-rows :: ForestAndBL -> ([K.Row], L.To)-rows (ForestAndBL f tot to) = (first:second:rest, to)-  where-    first = K.ROneCol $ K.OneColRow 0 desc-    desc = X.pack "All amounts reported in commodity: "-           `X.append` (L.unCommodity-                       . L.unTo-                       $ to)-    second = K.RMain $ K.MainRow 0 (X.pack "Total") tot-    rest = map mainRow-           . concatMap E.flatten-           . map U.labelLevels-           $ f---mainRow :: (Int, (L.SubAccount, L.BottomLine)) -> K.Row-mainRow (l, (a, b)) = K.RMain $ K.MainRow l x b-  where-    x = L.text a---- | The function for the Convert report. Use this function if you are--- setting the options from a program (as opposed to parsing them in--- from the command line.) Will fail if the balance conversions fail.-report-  :: Opts-  -> [L.PricePoint]-  -> [L.Box a]-  -> Ex.Exceptional X.Text [Rb.Chunk]-report os@(Opts getFmt _ _ _ _ txtFormats) ps bs = do-  fstBl <- sumConvertSort os ps bs-  let (rs, L.To cy) = rows fstBl-      fmt = getFmt cy-  return $ K.rowsToChunks txtFormats fmt rs----- | Creates a report respecting the standard interface for reports--- whose options are parsed in from the command line.-cmdLineReport-  :: O.DefaultOpts-  -> I.Report-cmdLineReport o rt = (help o, mkMode)-  where-    mkMode _ _ chgrs _ fsf = MA.Mode-      { MA.mName = "convert"-      , MA.mIntersperse = MA.Intersperse-      , MA.mOpts = map (fmap Right) P.allOptSpecs-      , MA.mPosArgs = Left-      , MA.mProcess = process rt chgrs o fsf-      , MA.mHelp = const (help o)-      }--process-  :: S.Runtime-  -> Scheme.Changers-  -> O.DefaultOpts-  -> ([L.Transaction] -> [L.Box Ly.LibertyMeta])-  -> [Either String (P.Opts -> Ex.Exceptional String P.Opts)]-  -> Ex.Exceptional X.Text I.ArgsAndReport-process rt chgrs defaultOpts fsf ls = do-  let (posArgs, parsed) = Ei.partitionEithers ls-      op' = foldl (>>=) (return (O.toParserOpts defaultOpts rt)) parsed-  case op' of-      Ex.Exception s -> Ex.throw . X.pack $ s-      Ex.Success g -> return $-        let noDefault = X.pack "no default price found"-            f = fromParsedOpts chgrs g-            pr ts pps = do-              rptOpts <- Ex.fromMaybe noDefault $-                f pps (O.format defaultOpts)-              let boxes = fsf ts-              report rptOpts pps boxes-        in (posArgs, pr)----- | Sums the balances from the bottom to the top of the tree (so that--- parent accounts have the sum of the balances of all their--- children.) Then converts the commodities to a single commodity, and--- sorts the accounts as requested. Fails if the conversion fails.-sumConvertSort-  :: Opts-  -> [L.PricePoint]-  -> [L.Box a]-  -> Ex.Exceptional X.Text ForestAndBL-sumConvertSort os ps bs = mkResult <$> convertedFrst <*> convertedTot-  where-    (Opts _ szb str tgt dt _) = os-    bals = U.balances szb bs-    (frst, tot) = U.sumForest mempty mappend bals-    convertBal (a, bal) =-        (\bl -> (a, bl)) <$> convertBalance db dt tgt bal-    db = buildDb ps-    convertedFrst = mapM (Tvbl.mapM convertBal) frst-    convertedTot = convertBalance db dt tgt tot-    mkResult f t = ForestAndBL (U.sortForest str f) t tgt---- | Determine the most frequent To commodity.-mostFrequent :: [L.PricePoint] -> Maybe L.To-mostFrequent = U.lastMode . map (L.to . L.price)---type DoReport = [L.PricePoint]-               -> (L.Commodity -> L.Qty -> X.Text)-               -> (Maybe Opts)---- | Get options for the report, depending on what options were parsed--- from the command line. Fails if the user did not specify a--- commodity and mostFrequent fails.-fromParsedOpts-  :: Scheme.Changers-  -> P.Opts-  -> DoReport-fromParsedOpts chgrs (P.Opts szb tgt dt so sb) =-  \pps fmt -> case tgt of-    P.ManualTarget to ->-      Just $ Opts fmt szb (getSorter so sb) to dt chgrs-    P.AutoTarget ->-      case mostFrequent pps of-        Nothing -> Nothing-        Just to ->-          Just $ Opts fmt szb (getSorter so sb) to dt chgrs---- | Returns a function usable to sort pairs of SubAccount and--- BottomLine depending on how you want them sorted.-getSorter :: CP.SortOrder -> P.SortBy -> Sorter-getSorter o b = flipper f-  where-    flipper = case o of-      CP.Ascending -> id-      CP.Descending ->-        \g p1 p2 -> case g p1 p2 of-            LT -> GT-            GT -> LT-            EQ -> EQ-    f p1@(a1, _) p2@(a2, _) = case b of-      P.SortByName -> compare a1 a2-      P.SortByQty -> cmpBottomLine p1 p2--cmpBottomLine :: Sorter-cmpBottomLine (n1, bl1) (n2, bl2) =-  case (bl1, bl2) of-    (L.Zero, L.Zero) -> EQ-    (L.NonZero _, L.Zero) -> LT-    (L.Zero, L.NonZero _) -> GT-    (L.NonZero c1, L.NonZero c2) ->-      mconcat [dc, qt, na]-      where-        dc = case (Bal.drCr c1, Bal.drCr c2) of-          (L.Debit, L.Debit) -> EQ-          (L.Debit, L.Credit) -> LT-          (L.Credit, L.Debit) -> GT-          (L.Credit, L.Credit) -> EQ-        qt = compare (Bal.qty c1) (Bal.qty c2)-        na = compare n1 n2----------------------------------------------------------------- ## Help--------------------------------------------------------------ifDefault :: Bool -> String-ifDefault b = if b then " (default)" else ""--help :: O.DefaultOpts -> String-help o = unlines $-  [ "convert"-  , "  Show account balances, after converting all amounts"-  , "  to a single commodity. Accepts ONLY the following options:"-  , ""-  , "--show-zero-balances"-  , "  Show balances that are zero"-    ++ ifDefault (CO.unShowZeroBalances . O.showZeroBalances $ o)-  , "--hide-zero-balances"-  , "  Hide balances that are zero"-    ++ ifDefault (not . CO.unShowZeroBalances . O.showZeroBalances $ o)-  , ""-  , "--commodity TARGET-COMMMODITY, -c TARGET-COMMODITY"-  , "  Convert all commodities to TARGET-COMMODITY."-  ] ++ case O.target o of-        P.ManualTarget (L.To cy) ->-          [ "  default: " ++ (X.unpack . L.unCommodity $ cy) ]-        _ -> []-    ++-  [ "--auto-commodity"-  , "  convert all commodities to the commodity that appears most"-  , "  often as the target commodity in your price data. If"-  , "  there is a tie, the price closest to the end of your list"-  , "  of prices is used."-    ++ case O.target o of-        P.AutoTarget -> " (default)"-        _ -> ""-  , ""-  , "--date DATE-TIME, -d DATE-TIME"-  , "  Convert prices as of the date and time given"-  , "  (by default, the current date and time is used.)"-  , ""-  , "--sort qty|name, -s qty|name"-  , "  Sort balances by sub-account name"-    ++ ifDefault (O.sortBy o == P.SortByName)-    ++ " or by quantity"-    ++ ifDefault (O.sortBy o == P.SortByQty)-  , "--ascending"-  , "  Sort in ascending order"-    ++ ifDefault (O.sortOrder o == CP.Ascending)-  , "--descending"-  , "  Sort in descending order"-    ++ ifDefault (O.sortOrder o == CP.Descending)-  , ""-  , "--help, -h"-  , "  Show this help and exit"-  ]-
− Penny/Cabin/Balance/Convert/Chunker.hs
@@ -1,239 +0,0 @@--- | Creates the output Chunks for the Balance report for--- multi-commodity reports only.--module Penny.Cabin.Balance.Convert.Chunker (-  MainRow(..),-  OneColRow(..),-  Row(..),-  rowsToChunks-  ) where---import Control.Applicative-  (Applicative (pure), (<$>), (<*>))-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Cabin.Meta as Meta-import qualified Penny.Cabin.Row as R-import qualified Penny.Lincoln as L-import qualified Data.Foldable as Fdbl-import qualified Data.Text as X-import qualified System.Console.Rainbow as Rb--type IsEven = Bool--data Columns a = Columns {-  acct :: a-  , drCr :: a-  , quantity :: a-  } deriving Show--instance Functor Columns where-  fmap f c = Columns {-    acct = f (acct c)-    , drCr = f (drCr c)-    , quantity = f (quantity c)-    }--instance Applicative Columns where-  pure a = Columns a a a-  fn <*> fa = Columns {-    acct = (acct fn) (acct fa)-    , drCr = (drCr fn) (drCr fa)-    , quantity = (quantity fn) (quantity fa)-     }--data PreSpec = PreSpec {-  _justification :: R.Justification-  , _padSpec :: (E.Label, E.EvenOdd)-  , bits :: Rb.Chunk }---- | 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 (R.Width . X.length . Rb.chunkText . 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---widthSpacerAcct :: Int-widthSpacerAcct = 4--widthSpacerDrCr :: Int-widthSpacerDrCr = 1--colsToBits-  :: E.Changers-  -> IsEven-  -> Columns R.ColumnSpec-  -> [Rb.Chunk]-colsToBits chgrs isEven (Columns a dc q) = let-  fillSpec = if isEven-             then (E.Other, E.Even)-             else (E.Other, E.Odd)-  spacer w = R.ColumnSpec j (R.Width w) fillSpec []-  j = R.LeftJustify-  cs = a-       : spacer widthSpacerAcct-       : dc-       : spacer widthSpacerDrCr-       : q-       : []-  in R.row chgrs cs--colsListToBits-  :: E.Changers-  -> [Columns R.ColumnSpec]-  -> [[Rb.Chunk]]-colsListToBits chgrs = zipWith f bools where-  f b c = colsToBits chgrs b c-  bools = iterate not True--preSpecsToBits-  :: E.Changers-  -> [Columns PreSpec]-  -> [Rb.Chunk]-preSpecsToBits chgrs =-  concat-  . colsListToBits chgrs-  . resizeColumnsInList--data Row = RMain MainRow | ROneCol OneColRow---- | Displays a one-column row.-data OneColRow = OneColRow {-  ocIndentation :: Int-  -- ^ Indent the text by this many levels (not by this many-  -- spaces; this number is multiplied by another number in the-  -- Chunker source to arrive at the final indentation amount)--  , ocText :: X.Text-  -- ^ Text for the left column-  }---- | Displays a single account in a Balance report. In a--- single-commodity report, this account will only be one screen line--- long. In a multi-commodity report, it might be multiple lines long,--- with one screen line for each commodity.-data MainRow = MainRow {-  mrIndentation :: Int-  -- ^ Indent the account name by this many levels (not by this many-  -- spaces; this number is multiplied by another number in the-  -- Chunker source to arrive at the final indentation amount)--  , mrText :: X.Text-  -- ^ Text for the name of the account--  , mrBottomLine :: L.BottomLine-  -- ^ Commodity balances. If this list is empty, dashes are-  -- displayed for the DrCr and Qty.-  }---rowsToChunks-  :: E.Changers-  -> (L.Qty -> X.Text)-  -- ^ How to format a balance to allow for digit grouping-  -> [Row]-  -> [Rb.Chunk]-rowsToChunks chgrs fmt =-  preSpecsToBits chgrs-  . rowsToColumns chgrs fmt--rowsToColumns-  :: E.Changers-  -> (L.Qty -> X.Text)-  -- ^ How to format a balance to allow for digit grouping--  -> [Row]-  -> [Columns PreSpec]-rowsToColumns chgrs fmt rs = map (mkRow chgrs fmt) pairs-  where-    pairs = Meta.visibleNums (,) rs---mkRow-  :: E.Changers-  -> (L.Qty -> X.Text)-  -> (Meta.VisibleNum, Row)-  -> Columns PreSpec-mkRow chgrs fmt (vn, r) = case r of-  RMain m -> mkMainRow chgrs fmt (vn, m)-  ROneCol c -> mkOneColRow chgrs (vn, c)--mkOneColRow-  :: E.Changers-  -> (Meta.VisibleNum, OneColRow)-  -> Columns PreSpec-mkOneColRow chgrs (vn, (OneColRow i t)) = Columns ca cd cq-  where-    txt = X.append indents t-    indents = X.replicate (indentAmount * max 0 i)-              (X.singleton ' ')-    eo = E.fromVisibleNum vn-    lbl = E.Other-    ca = PreSpec R.LeftJustify (lbl, eo)-         (E.getEvenOddLabelValue lbl eo chgrs $ Rb.plain txt)-    cd = PreSpec R.LeftJustify (lbl, eo)-         (E.getEvenOddLabelValue lbl eo chgrs $ Rb.plain X.empty)-    cq = cd--mkMainRow-  :: E.Changers-  -> (L.Qty -> X.Text)-  -> (Meta.VisibleNum, MainRow)-  -> Columns PreSpec-mkMainRow chgrs fmt (vn, (MainRow i acctTxt b)) = Columns ca cd cq-  where-    applyFmt = E.getEvenOddLabelValue lbl eo chgrs-    eo = E.fromVisibleNum vn-    lbl = E.Other-    ca = PreSpec R.LeftJustify (lbl, eo) (applyFmt (Rb.plain txt))-      where-        txt = X.append indents acctTxt-        indents = X.replicate (indentAmount * max 0 i)-                  (X.singleton ' ')-    cd = PreSpec R.LeftJustify (lbl, eo) (applyFmt cksDrCr)-    cq = PreSpec R.LeftJustify (lbl, eo) (applyFmt cksQty)-    (cksDrCr, cksQty) = balanceChunks chgrs fmt vn b---balanceChunks-  :: E.Changers-  -> (L.Qty -> X.Text)-  -> Meta.VisibleNum-  -> L.BottomLine-  -> (Rb.Chunk, Rb.Chunk)-balanceChunks chgrs fmt vn bl = (chkDc, chkQt)-  where-    eo = E.fromVisibleNum vn-    chkDc = E.bottomLineToDrCr bl eo chgrs-    qtFmt = E.getEvenOddLabelValue lbl eo chgrs-    chkQt = qtFmt $ Rb.plain t-    (lbl, t) = case bl of-      L.Zero -> (E.Zero, X.pack "--")-      L.NonZero (L.Column dc qt) -> (E.dcToLbl dc, fmt qt)---indentAmount :: Int-indentAmount = 2-
− Penny/Cabin/Balance/Convert/Options.hs
@@ -1,43 +0,0 @@--- | Default options for the Convert report when used from the command--- line.-module Penny.Cabin.Balance.Convert.Options where--import qualified Penny.Cabin.Balance.Convert.Parser as P-import qualified Penny.Cabin.Parsers as CP-import qualified Penny.Cabin.Options as CO-import qualified Penny.Lincoln as L-import qualified Penny.Shield as S-import qualified Data.Text as X---- | Default options for the Convert report. This record is used as--- the starting point when parsing in options from the command--- line. You don't need to use it if you are setting the options for--- the Convert report directly from your own code.--data DefaultOpts = DefaultOpts-  { showZeroBalances :: CO.ShowZeroBalances-  , target :: P.Target-  , sortOrder :: CP.SortOrder-  , sortBy :: P.SortBy-  , format :: L.Commodity -> L.Qty -> X.Text-  }--toParserOpts :: DefaultOpts -> S.Runtime -> P.Opts-toParserOpts d rt = P.Opts-  { P.showZeroBalances = showZeroBalances d-  , P.target = target d-  , P.dateTime = S.currentTime rt-  , P.sortOrder = sortOrder d-  , P.sortBy = sortBy d-  }--defaultOptions :: DefaultOpts-defaultOptions = DefaultOpts-  { showZeroBalances = CO.ShowZeroBalances False-  , target = P.AutoTarget-  , sortOrder = CP.Ascending-  , sortBy = P.SortByName-  , format = \_ q -> X.pack . show $ q-  }--
− Penny/Cabin/Balance/Convert/Parser.hs
@@ -1,87 +0,0 @@--- | Parsing options for the Convert report from the command line.-module Penny.Cabin.Balance.Convert.Parser (-  Opts(..)-  , Target(..)-  , SortBy(..)-  , allOptSpecs-  ) where---import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Data.Text as X-import qualified Penny.Cabin.Options as CO-import qualified Penny.Cabin.Parsers as P-import qualified Penny.Lincoln as L-import qualified Penny.Copper.Parsec as Pc-import qualified System.Console.MultiArg.Combinator as C-import qualified Text.Parsec as Parsec----- | Is the target commodity determined by the user or automatically?-data Target = AutoTarget | ManualTarget L.To--data SortBy = SortByQty | SortByName deriving (Eq, Show, Ord)---- | Default starting options for the Convert report. After--- considering what is parsed in from the command line and price data,--- a Convert.Opts will be generated.-data Opts = Opts-  { showZeroBalances :: CO.ShowZeroBalances-  , target :: Target-  , dateTime :: L.DateTime-  , sortOrder :: P.SortOrder-  , sortBy :: SortBy-  }---- | Do not be tempted to change the setup in this module so that the--- individual functions such as parseColor and parseBackground return--- parsers rather than OptSpec. Such an arrangement breaks the correct--- parsing of abbreviated long options.-allOptSpecs :: [C.OptSpec (Opts -> Ex.Exceptional String Opts)]-allOptSpecs =-  [ fmap toExc parseZeroBalances-  , parseCommodity-  , fmap toExc parseAuto-  , parseDate-  , fmap toExc parseSort-  , fmap toExc parseOrder ]-  where-    toExc f = return . f--parseZeroBalances :: C.OptSpec (Opts -> Opts)-parseZeroBalances = fmap f P.zeroBalances-  where-    f x o = o { showZeroBalances = x }---parseCommodity :: C.OptSpec (Opts -> Ex.Exceptional String Opts)-parseCommodity = C.OptSpec ["commodity"] "c" (C.OneArg f)-  where-    f a1 os =-      case Parsec.parse Pc.lvl1Cmdty "" (X.pack a1) of-        Left _ -> Ex.throw $ "invalid commodity: " ++ a1-        Right g -> return $ os { target = ManualTarget . L.To $ g }--parseAuto :: C.OptSpec (Opts -> Opts)-parseAuto = C.OptSpec ["auto-commodity"] "" (C.NoArg f)-  where-    f os = os { target = AutoTarget }--parseDate :: C.OptSpec (Opts -> Ex.Exceptional String Opts)-parseDate = C.OptSpec ["date"] "d" (C.OneArg f)-  where-    f a1 os =-      case Parsec.parse Pc.dateTime "" (X.pack a1) of-        Left _ -> Ex.throw $ "invalid date: " ++ a1-        Right g -> return $ os { dateTime = g }--parseSort :: C.OptSpec (Opts -> Opts)-parseSort = C.OptSpec ["sort"] "s" (C.ChoiceArg ls)-  where-    ls = [ ("qty", (\os -> os { sortBy = SortByQty }))-         , ("name", (\os -> os { sortBy = SortByName })) ]--parseOrder :: C.OptSpec (Opts -> Opts)-parseOrder = fmap f P.order-  where-    f x o = o { sortOrder = x }
− Penny/Cabin/Balance/MultiCommodity.hs
@@ -1,177 +0,0 @@--- | The multi-commodity Balance report. This is the simpler balance--- report because it does not allow for commodities to be converted.--module Penny.Cabin.Balance.MultiCommodity (-  Opts(..),-  defaultOpts,-  defaultParseOpts,-  defaultFormat,-  parseReport,-  defaultReport,-  report-  ) where--import Control.Applicative (Applicative, pure)-import qualified Penny.Cabin.Balance.Util as U-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Cabin.Scheme.Schemes as Schemes-import qualified Penny.Lincoln as L-import qualified Penny.Liberty as Ly-import qualified Data.Either as Ei-import qualified Data.Map as M-import qualified Penny.Cabin.Options as CO-import Data.Monoid (mappend, mempty)-import qualified Data.Text as X-import qualified Data.Tree as E-import qualified Penny.Cabin.Balance.MultiCommodity.Chunker as K-import qualified Penny.Cabin.Balance.MultiCommodity.Parser as P-import qualified Penny.Cabin.Interface as I-import qualified Penny.Cabin.Parsers as CP-import qualified System.Console.MultiArg as MA-import qualified System.Console.Rainbow as R---- | Options for making the balance report. These are the only options--- needed to make the report if the options are not being parsed in--- from the command line.-data Opts = Opts-  { balanceFormat :: L.Commodity -> L.Qty -> X.Text-  , showZeroBalances :: CO.ShowZeroBalances-  , order :: L.SubAccount -> L.SubAccount -> Ordering-  , textFormats :: E.Changers-  }--defaultOpts :: Opts-defaultOpts = Opts-  { balanceFormat = defaultFormat-  , showZeroBalances = CO.ShowZeroBalances True-  , order = compare-  , textFormats = Schemes.darkLabels-  }--defaultParseOpts :: P.ParseOpts-defaultParseOpts = P.ParseOpts-  { P.showZeroBalances = CO.ShowZeroBalances False-  , P.order = CP.Ascending-  }--fromParseOpts-  :: E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> P.ParseOpts-  -> Opts-fromParseOpts chgrs fmt (P.ParseOpts szb o) = Opts fmt szb o' chgrs-  where-    o' = case o of-       CP.Ascending -> compare-       CP.Descending -> CO.descending compare--defaultFormat :: a -> L.Qty -> X.Text-defaultFormat _ = X.pack . show--summedSortedBalTree ::-  CO.ShowZeroBalances-  -> (L.SubAccount -> L.SubAccount -> Ordering)-  -> [L.Box a]-  -> (E.Forest (L.SubAccount, L.Balance), L.Balance)-summedSortedBalTree szb o =-  U.sumForest mempty mappend-  . U.sortForest o'-  . U.balances szb-  where-    o' x y = o (fst x) (fst y)--rows ::-  (E.Forest (L.SubAccount, L.Balance), L.Balance)-  -> [K.Row]-rows (o, b) = first:rest-  where-    first = K.Row 0 (X.pack "Total") (M.assocs . L.unBalance $ b)-    rest = map row . concatMap E.flatten . map U.labelLevels $ o-    row (l, (s, ib)) =-      K.Row l (L.text s) (M.assocs . L.unBalance $ ib)---- | This report is what to use if you already have your options (that--- is, you are not parsing them in from the command line.)-report :: Opts -> [L.Box a] -> [R.Chunk]-report (Opts bf szb o chgrs) =-  K.rowsToChunks chgrs bf-  . rows-  . summedSortedBalTree szb o---- | The MultiCommodity report with configurable options that have--- been parsed from the command line.-parseReport ::-  (L.Commodity -> L.Qty -> X.Text)-  -- ^ How to format balances. For instance you can use this to-  -- perform commodity-sensitive digit grouping.--  -> P.ParseOpts-  -- ^ Default options for the report. These can be overriden on the-  -- command line.--  -> I.Report-parseReport fmt o rt = (help o, makeMode)-  where-    makeMode _ _ chgrs _ fsf = MA.Mode-      { MA.mName = "balance"-      , MA.mIntersperse = MA.Intersperse-      , MA.mOpts = map (fmap Right) P.allSpecs-      , MA.mPosArgs = Left-      , MA.mProcess = process chgrs fmt o rt fsf-      , MA.mHelp = const (help o)-      }--process-  :: Applicative f-  => E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> P.ParseOpts-  -> a-  -> ([L.Transaction] -> [L.Box Ly.LibertyMeta])-  -> [Either String (P.ParseOpts -> P.ParseOpts)]-  -> f I.ArgsAndReport-process chgrs fmt o _ fsf ls =-  let (posArgs, fns) = Ei.partitionEithers ls-      mkParsedOpts = foldl (flip (.)) id fns-      os' = mkParsedOpts o-      mcOpts = fromParseOpts chgrs fmt os'-      pr txns _ = return $ report mcOpts (fsf txns)-  in pure (posArgs, pr)----- | The MultiCommodity report, with default options.-defaultReport :: I.Report-defaultReport = parseReport defaultFormat defaultParseOpts----------------------------------------------------------------- ## Help--------------------------------------------------------------ifDefault :: Bool -> String-ifDefault b = if b then " (default)" else ""--help :: P.ParseOpts -> String-help o = unlines-  [ "balance"-  , "  Show account balances. Accepts ONLY the following options:"-  , ""-  , "--show-zero-balances"-  , "  Show balances that are zero"-    ++ ifDefault (CO.unShowZeroBalances . P.showZeroBalances $ o)-  , "--hide-zero-balances"-  , "  Hide balances that are zero"-    ++ ifDefault ( not . CO.unShowZeroBalances-                 . P.showZeroBalances $ o)-  , ""-  , "--ascending"-  , "  Sort in ascending order by account name"-    ++ ifDefault (P.order o == CP.Ascending)--  , "--descending"-  , "  Sort in descending order by account name"-    ++ ifDefault (P.order o == CP.Descending)--  , ""-  , "--help, -h"-  , "  Show this help and exit"-  ]-
− Penny/Cabin/Balance/MultiCommodity/Chunker.hs
@@ -1,222 +0,0 @@--- | Creates the output Chunks for the Balance report for both--- multi-commodity reports.--module Penny.Cabin.Balance.MultiCommodity.Chunker (-  Row(..),-  rowsToChunks-  ) where---import Control.Applicative-  (Applicative (pure), (<$>), (<*>))-import qualified Penny.Cabin.Meta as Meta-import qualified Penny.Cabin.Row as R-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Lincoln as L-import qualified Data.Foldable as Fdbl-import qualified Data.Text as X-import qualified System.Console.Rainbow as Rb--type IsEven = Bool--data Columns a = Columns {-  acct :: a-  , drCr :: a-  , commodity :: a-  , quantity :: a-  } deriving Show--instance Functor Columns where-  fmap f c = Columns {-    acct = f (acct 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 {-    acct = (acct fn) (acct fa)-    , drCr = (drCr fn) (drCr fa)-    , commodity = (commodity fn) (commodity fa)-    , quantity = (quantity fn) (quantity fa)-     }--data PreSpec = PreSpec {-  _justification :: R.Justification-  , _padSpec :: (E.Label, E.EvenOdd)-  , bits :: [Rb.Chunk] }---- | 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 ( safeMaximum (R.Width 0)-                        . map (R.Width . X.length . Rb.chunkText)-                        . bits $ new)-  safeMaximum d ls = if null ls then d else maximum ls---- | 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-  :: E.Changers-  -> IsEven-  -> Columns R.ColumnSpec-  -> [Rb.Chunk]-colsToBits chgrs isEven (Columns a dc c q) = let-  fillSpec = if isEven-             then (E.Other, E.Even)-             else (E.Other, E.Odd)-  spacer w = R.ColumnSpec j (R.Width w) fillSpec []-  j = R.LeftJustify-  cs = a-       : spacer widthSpacerAcct-       : dc-       : spacer widthSpacerDrCr-       : c-       : spacer widthSpacerCommodity-       : q-       : []-  in R.row chgrs cs--colsListToBits-  :: E.Changers-  -> [Columns R.ColumnSpec]-  -> [[Rb.Chunk]]-colsListToBits chgrs = zipWith f bools where-  f b c = colsToBits chgrs b c-  bools = iterate not True--preSpecsToBits-  :: E.Changers-  -> [Columns PreSpec]-  -> [Rb.Chunk]-preSpecsToBits chgrs =-  concat-  . colsListToBits chgrs-  . resizeColumnsInList---- | Displays a single account in a Balance report. In a--- single-commodity report, this account will only be one screen line--- long. In a multi-commodity report, it might be multiple lines long,--- with one screen line for each commodity.-data Row = Row-  { indentation :: Int-  -- ^ Indent the account name by this many levels (not by this many-  -- spaces; this number is multiplied by another number in the-  -- Chunker source to arrive at the final indentation amount)--  , accountTxt :: X.Text-    -- ^ Text for the name of the account--  , balances :: [(L.Commodity, L.BottomLine)]-    -- ^ Commodity balances. If this list is empty, dashes are-    -- displayed for the DrCr, Commodity, and Qty.-  }--rowsToChunks-  :: E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -- ^ How to format a balance to allow for digit grouping-  -> [Row]-  -> [Rb.Chunk]-rowsToChunks chgrs fmt =-  preSpecsToBits chgrs-  . rowsToColumns chgrs fmt--rowsToColumns-  :: E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -- ^ How to format a balance to allow for digit grouping--  -> [Row]-  -> [Columns PreSpec]-rowsToColumns chgrs fmt rs = map (mkColumn chgrs fmt) pairs-  where-    pairs = Meta.visibleNums (,) rs---mkColumn-  :: E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> (Meta.VisibleNum, Row)-  -> Columns PreSpec-mkColumn chgrs fmt (vn, (Row i acctTxt bs)) = Columns ca cd cc cq-  where-    lbl = E.Other-    eo = E.fromVisibleNum vn-    applyFmt = E.getEvenOddLabelValue lbl eo chgrs-    ca = PreSpec R.LeftJustify (lbl, eo) [applyFmt $ Rb.plain txt]-      where-        txt = X.append indents acctTxt-        indents = X.replicate (indentAmount * max 0 i)-                  (X.singleton ' ')-    cd = PreSpec R.LeftJustify (lbl, eo) cksDrCr-    cc = PreSpec R.RightJustify (lbl, eo) cksCmdty-    cq = PreSpec R.LeftJustify (lbl, eo) cksQty-    (cksDrCr, cksCmdty, cksQty) =-      if null bs-      then balanceChunksEmpty chgrs eo-      else-        let balChks = map (balanceChunks chgrs fmt eo) bs-            cDrCr = map (\(a, _, _) -> a) balChks-            cCmdty = map (\(_, a, _) -> a) balChks-            cQty = map (\(_, _, a) -> a) balChks-        in (cDrCr, cCmdty, cQty)---balanceChunksEmpty-  :: E.Changers-  -> E.EvenOdd-  -> ([Rb.Chunk], [Rb.Chunk], [Rb.Chunk])-balanceChunksEmpty chgrs eo = (dash, dash, dash)-  where-    dash = [E.getEvenOddLabelValue E.Other eo chgrs $ Rb.plain (X.pack "--")]--balanceChunks-  :: E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> E.EvenOdd-  -> (L.Commodity, L.BottomLine)-  -> (Rb.Chunk, Rb.Chunk, Rb.Chunk)-balanceChunks chgrs fmt eo (cty, bl) = (chkDc, chkCt, chkQt)-  where-    chkDc = E.bottomLineToDrCr bl eo chgrs-    chkCt = E.bottomLineToCmdty chgrs eo (cty, bl)-    chkQt = E.bottomLineToQty chgrs fmt eo (cty, bl)---indentAmount :: Int-indentAmount = 2-
− Penny/Cabin/Balance/MultiCommodity/Parser.hs
@@ -1,29 +0,0 @@-module Penny.Cabin.Balance.MultiCommodity.Parser (-  ParseOpts(..)-  , allSpecs-  ) where--import qualified Penny.Cabin.Options as CO-import qualified Penny.Cabin.Parsers as P-import qualified System.Console.MultiArg as MA---- | Options for the Balance report that have been parsed from the--- command line.-data ParseOpts = ParseOpts-  { showZeroBalances :: CO.ShowZeroBalances-  , order :: P.SortOrder-  }---zeroBalances :: MA.OptSpec (ParseOpts -> ParseOpts)-zeroBalances = fmap toResult P.zeroBalances-  where-    toResult szb o = o { showZeroBalances = szb }--parseOrder :: MA.OptSpec (ParseOpts -> ParseOpts)-parseOrder = fmap toResult P.order-  where-    toResult x o = o { order = x }--allSpecs :: [MA.OptSpec (ParseOpts -> ParseOpts)]-allSpecs = [zeroBalances, parseOrder]
− Penny/Cabin/Balance/Util.hs
@@ -1,228 +0,0 @@--- | Grab bag of utility functions.--module Penny.Cabin.Balance.Util-  ( tieredForest-  , tieredPostings-  , filterForest-  , balances-  , flatten-  , treeWithParents-  , forestWithParents-  , sumForest-  , sumTree-  , boxesBalance-  , labelLevels-  , sortForest-  , sortTree-  , lastMode-  ) where--import qualified Penny.Cabin.Options as CO-import qualified Penny.Lincoln as L-import qualified Penny.Steel.NestedMap as NM-import qualified Data.Foldable as Fdbl-import qualified Data.Map as M-import Data.Ord (comparing)-import Data.List (sortBy, maximumBy, groupBy)-import Data.Monoid (mconcat, Monoid)-import Data.Maybe (mapMaybe)-import qualified Data.Tree as T-import qualified Penny.Lincoln.Queries as Q---- | Constructs a forest sorted into tiers based on lists of keys that--- are extracted from the elements.-tieredForest ::-  Ord k-  => (a -> [k])-  -- ^ Extracts a key from the elements we are putting in the tree. If-  -- this function returns an empty list for any element, the element-  -- will not appear in the tiered forest.-  -> [a]-  -> T.Forest (k, [a])-tieredForest getKeys ls = fmap (fmap revSnd) . NM.toForest $ nm-  where-    revSnd (a, xs) = (a, reverse xs)-    nm = foldr f NM.empty ls-    f a m = NM.relabel m ps-      where-        ps = case getKeys a of-          [] -> []-          ks ->-            let mkInitPair k = (k, maybe [] id)-                mkLastPair k = (k, maybe [a] (a:))-            in (map mkInitPair . init $ ks)-               ++ [(mkLastPair (last ks))]---- | Takes a list of postings and puts them into a Forest. Each level--- of each of the trees corresponds to a sub account. The label of the--- node tells you the sub account name and gives you a list of the--- postings at that level.-tieredPostings :: [L.Box a] -> T.Forest (L.SubAccount, [L.Box a])-tieredPostings = tieredForest e-  where-    e = Fdbl.toList . L.unAccount . Q.account . L.boxPostFam---- | Keeps only Trees that match a given condition. First examines--- child trees to determine whether they should be retained. If a--- child tree is retained, does not delete the parent tree.-filterForest :: (a -> Bool) -> T.Forest a -> T.Forest a-filterForest f = mapMaybe pruneTree-  where-    pruneTree (T.Node a fs) =-      case filterForest f fs of-        [] -> if not (f a) then Nothing else Just (T.Node a [])-        cs -> Just (T.Node a cs)----- | Puts all Boxes into a Tree and sums the balances. Removes--- accounts that have empty balances if requested. Does NOT sum--- balances from the bottom up.-balances ::-  CO.ShowZeroBalances-  -> [L.Box a]-  -> T.Forest (L.SubAccount, L.Balance)-balances (CO.ShowZeroBalances szb) =-  remover-  . map (fmap (mapSnd boxesBalance))-  . tieredPostings-  where-    remover =-      if szb-      then id-      else filterForest (not . M.null . L.unBalance . snd)-           . map (fmap (mapSnd L.removeZeroCommodities))----- | Takes a tree of Balances (like what is produced by the 'balances'--- function) and produces a flat list of accounts with the balance of--- each account.-flatten-  :: T.Forest (L.SubAccount, L.Balance)-  -> [(L.Account, L.Balance)]-flatten =-  concatMap T.flatten-  . map (fmap toPair) . forestWithParents-  where-    toPair ((s, b), ls) =-      case reverse . map fst $ ls of-        [] -> (L.Account [s], b)-        s1:sr -> (L.Account (s1 : (sr ++ [s])), b)---- | Takes a Tree and returns a Tree where each node has information--- about its parent Nodes. The list of parent nodes has the most--- immediate parent first and the most distant parent last.-treeWithParents :: T.Tree a -> T.Tree (a, [a])-treeWithParents = treeWithParentsR []---- | Given a list of the parents seen so far, return a Tree where each--- node contains information about its parents.-treeWithParentsR :: [a] -> T.Tree a -> T.Tree (a, [a])-treeWithParentsR ls (T.Node n cs) = T.Node (n, ls) cs'-  where-    cs' = map (treeWithParentsR (n:ls)) cs---- | Takes a Forest and returns a Forest where each node has--- information about its parent Nodes.-forestWithParents :: T.Forest a -> T.Forest (a, [a])-forestWithParents = map (treeWithParentsR [])---- | Sums a forest from the bottom up. Returns a pair, where the first--- element is the forest, but with the second element of each node--- replaced with the sum of that node and all its children. The second--- element is the sum of all the second elements in the forest.-sumForest ::-  s-  -- ^ Zero--  -> (s -> s -> s)-  -- ^ Combiner--  -> T.Forest (a, s)-  -> (T.Forest (a, s), s)-sumForest z f ts = (ts', s)-  where-    ts' = map (sumTree z f) ts-    s = foldr f z . map (snd . T.rootLabel) $ ts'---- | Sums a tree from the bottom up.-sumTree ::-  s-  -- ^ Zero--  -> (s -> s -> s)-  -- ^ Combiner--  ->  T.Tree (a, s)-  -> T.Tree (a, s)-sumTree z f (T.Node (a, s) cs) = T.Node (a, f s cSum) cs'-  where-    (cs', cSum) = sumForest z f cs---boxesBalance :: [L.Box a] -> L.Balance-boxesBalance = mconcat . map L.entryToBalance . map Q.entry-               . map L.boxPostFam--mapSnd :: (a -> b) -> (f, a) -> (f, b)-mapSnd f (x, a) = (x, f a)---- | Label each level of a Tree with an integer indicating how deep it--- is. The top node of the tree is level 0.-labelLevels :: T.Tree a -> T.Tree (Int, a)-labelLevels = go 0-  where-    go l (T.Node x xs) = T.Node (l, x) (map (go (l + 1)) xs)---- | Sorts each level of a Forest.-sortForest ::-  (a -> a -> Ordering)-  -> T.Forest a-  -> T.Forest a-sortForest o f = sortBy o' (map (sortTree o) f)-  where-    o' x y = o (T.rootLabel x) (T.rootLabel y)---- | Sorts each level of a Tree.-sortTree ::-  (a -> a -> Ordering)-  -> T.Tree a-  -> T.Tree a-sortTree o (T.Node l f) = T.Node l (sortForest o f)---- | Like lastModeBy but using Ord.-lastMode :: Ord a => [a] -> Maybe a-lastMode = lastModeBy compare---- | Finds the mode of a list. Takes the mode that is located last in--- the list. Returns Nothing if there is no mode (that is, if the list--- is empty).-lastModeBy ::-  (a -> a -> Ordering)-  -> [a]-  -> Maybe a-lastModeBy o ls =-  case modesBy o' ls' of-    [] -> Nothing-    ms -> Just . fst . maximumBy fx $ ms-    where-      fx = comparing snd-      ls' = zip ls ([0..] :: [Int])-      o' x y = o (fst x) (fst y)---- | Finds the modes of a list.-modesBy :: (a -> a -> Ordering) -> [a] -> [a]-modesBy o =-  concat-  . longestLists-  . groupBy (\x y -> o x y == EQ)-  . sortBy o----- | Returns the longest lists. This function is partial. It is bottom--- if the argument list is empty. Therefore, do not export this--- function.-longestLists :: [[a]] -> [[a]]-longestLists as =-  let lengths = map (\ls -> (ls, length ls)) as-      maxLen = maximum . map snd $ lengths-  in map fst . filter (\(_, len) -> len == maxLen) $ lengths
− Penny/Cabin/Interface.hs
@@ -1,77 +0,0 @@--- | An interface for other Penny components to use. A report is--- anything that is a 'Report'.-module Penny.Cabin.Interface where--import qualified Data.Prednote.Expressions as Exp-import qualified Penny.Cabin.Scheme as S-import Control.Monad.Exception.Synchronous (Exceptional)-import qualified Data.Text as X-import Text.Matchers (CaseSensitive)-import qualified Text.Matchers as TM-import qualified System.Console.MultiArg as MA-import qualified System.Console.Rainbow as R--import qualified Penny.Lincoln as L-import qualified Penny.Liberty as Ly-import Penny.Shield (Runtime)---- | The function that will print the report, and the positional--- arguments. If there was a problem parsing the command line options,--- return an Exception with an error message.---- | Parsing the filter options can have one of two results: a help--- string, or a list of positional arguments and a function that--- prints a report. Or, the parse might fail.--type PosArg = String-type HelpStr = String-type ArgsAndReport = ([PosArg], PrintReport)---- | The result of parsing the arguments to a report. Failures are--- indicated with a Text. The name of the executable and the word--- @error@ will be prepended to this Text; otherwise, it is printed--- as-is, so be sure to include any trailing newline if needed.-type ParseResult = Exceptional X.Text ArgsAndReport--type PrintReport-  = [L.Transaction]-  -- ^ All transactions; the report must sort and filter them--  -> [L.PricePoint]-  -- ^ PricePoints to be included in the report---  -> Exceptional X.Text [R.Chunk]-  -- ^ The exception type is a strict Text, containing the error--- message. The success type is a list of either a Chunk or a PreChunk--- containing the resulting report. This allows for errors after the--- list of transactions has been seen. The name of the executable and--- the word @error@ will be prepended to this Text; otherwise, it is--- printed as-is, so be sure to include any trailing newline if--- needed.---type Report = Runtime -> (HelpStr, MkReport)-type MkReport-  = 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 TM.Matcher)-  -- ^ Result from previous parsers indicating the matcher factory the-  -- user wishes to use--  -> S.Changers-  -- ^ Result from previous parsers indicating which color scheme to-  -- use.--  -> Exp.ExprDesc-  -- ^ Result from previous parsers indicating whether the user wants-  -- RPN or infix--  -> ([L.Transaction] -> [L.Box Ly.LibertyMeta])-  -- ^ Result from previous parsers that will sort and filter incoming-  -- transactions--  -> MA.Mode ParseResult
− Penny/Cabin/Meta.hs
@@ -1,38 +0,0 @@--- | Metadata that is specific to Cabin.-module Penny.Cabin.Meta (VisibleNum, unVisibleNum,-                         visibleNumBoxes, visibleNums ) where--import Control.Applicative ((*>))-import qualified Data.Traversable as Tr-import qualified Penny.Lincoln as L---- | Each row that is visible on screen is assigned a VisibleNum. This--- is used to number the rows in the report for the user's benefit. It--- is also used to determine whether the row is even or odd for the--- purpose of assigning the background color (this way the background--- colors can alternate, like a checkbook register.)-newtype VisibleNum = VisibleNum { unVisibleNum :: L.Serial }-                     deriving (Eq, Show)---- | Assigns VisibleNum to a list of boxes.-visibleNumBoxes ::-  (VisibleNum -> a -> b)-  -> [L.Box a]-  -> [L.Box b]-visibleNumBoxes f bs = L.makeSerials k-  where-    k = Tr.sequenceA (replicate (length bs) L.incrementBack)-        *> mapM assign bs-    assign (L.Box m pf) = fmap g L.getSerial-      where-        g ser = L.Box (f (VisibleNum ser) m) pf----- | Assigns VisibleNum to a list.-visibleNums :: (VisibleNum -> a -> b) -> [a] -> [b]-visibleNums f as = L.makeSerials k-  where-    k = Tr.sequenceA (replicate (length as) L.incrementBack)-        *> mapM assign as-    assign a = fmap (\ser -> f (VisibleNum ser) a) L.getSerial-
− Penny/Cabin/Options.hs
@@ -1,16 +0,0 @@--- | Options applicable to multiple Cabin reports.--module Penny.Cabin.Options where---- | Whether to show zero balances in reports.-newtype ShowZeroBalances =-  ShowZeroBalances { unShowZeroBalances :: Bool }-  deriving (Show, Eq)---- | Converts an ordering to a descending order.-descending :: (a -> a -> Ordering)-              -> a -> a -> Ordering-descending f x y = case f x y of-  LT -> GT-  GT -> LT-  EQ -> EQ
− Penny/Cabin/Parsers.hs
@@ -1,24 +0,0 @@--- | Command line parsers that are common to various Cabin reports.--module Penny.Cabin.Parsers where--import qualified Penny.Cabin.Options as CO-import qualified System.Console.MultiArg.Combinator as C---zeroBalances :: C.OptSpec CO.ShowZeroBalances-zeroBalances = C.OptSpec ["zero-balances"] "" (C.ChoiceArg ls)-  where-    ls = [ ("show", CO.ShowZeroBalances True)-         , ("hide", CO.ShowZeroBalances False) ]--data SortOrder = Ascending | Descending deriving (Eq, Ord, Show)--order :: C.OptSpec SortOrder-order = C.OptSpec ["order"] "" (C.ChoiceArg ls)-  where-    ls = [ ("ascending", Ascending)-         , ("descending", Descending) ]--help :: C.OptSpec ()-help = C.OptSpec ["help"] "h" (C.NoArg ())
− Penny/Cabin/Posts.hs
@@ -1,622 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | 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-  ( postsReport-  , zincReport-  , defaultOptions-  , ZincOpts(..)-  , A.Alloc-  , A.SubAccountLength(..)-  , A.alloc-  , yearMonthDay-  , qtyAsIs-  , balanceAsIs-  , defaultWidth-  , columnsVarToWidth-  , widthFromRuntime-  , defaultFields-  , defaultSpacerWidth-  , T.ReportWidth(..)-  ) where--import Control.Applicative ((<$>), (<*>))-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.List.Split (chunksOf)-import qualified Data.Either as Ei-import Data.Monoid ((<>))-import qualified Data.Text as X-import qualified Penny.Cabin.Interface as I-import qualified Penny.Cabin.Options as CO-import qualified Penny.Cabin.Posts.Allocated as A-import qualified Penny.Cabin.Posts.Chunk as C-import qualified Penny.Cabin.Posts.Fields as F-import qualified Penny.Cabin.Posts.Meta as M-import Penny.Cabin.Posts.Meta (Box)-import qualified Penny.Cabin.Posts.Parser as P-import qualified Penny.Cabin.Posts.Spacers as S-import qualified Penny.Cabin.Posts.Types as T-import qualified Penny.Cabin.Scheme as E--import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Queries as Q-import qualified Penny.Liberty as Ly-import qualified Penny.Shield as Sh-import qualified Data.Prednote.Expressions as Exp-import qualified Data.Prednote.Pdct as Pe-import qualified System.Console.Rainbow as Rb--import Data.List (intersperse)-import Data.Maybe (catMaybes)-import qualified Data.Foldable as Fdbl-import Data.Time as Time-import qualified System.Console.MultiArg as MA-import System.Locale (defaultTimeLocale)-import Text.Matchers (CaseSensitive)---- | All information needed to make a Posts report. This function--- never fails.-postsReport-  :: E.Changers-  -> CO.ShowZeroBalances-  -> (Pe.Pdct (L.Box Ly.LibertyMeta))-  -- ^ Removes posts from the report if applying this function to the-  -- post returns False. Posts removed still affect the running-  -- balance.--  -> [Ly.PostFilterFn]-  -- ^ Applies these post-filters to the list of posts that results-  -- from applying the predicate above. Might remove more-  -- postings. Postings removed still affect the running balance.--  -> C.ChunkOpts-  -> [L.Box Ly.LibertyMeta]-  -> [Rb.Chunk]--postsReport ch szb pdct pff co =-  C.makeChunk ch co-  . M.toBoxList szb pdct pff---zincReport :: ZincOpts -> I.Report-zincReport opts rt = (helpStr opts, md)-  where-    md cs fty ch expr fsf = MA.Mode-      { MA.mName = "postings"-      , MA.mIntersperse = MA.Intersperse-      , MA.mOpts = specs rt-      , MA.mPosArgs = Left-      , MA.mProcess = process opts cs fty ch expr fsf-      , MA.mHelp = const (helpStr opts)-      }--specs-  :: Sh.Runtime-  -> [MA.OptSpec (Either String (P.State -> Ex.Exceptional X.Text P.State))]-specs = map (fmap Right) . P.allSpecs---process-  :: ZincOpts-  -> CaseSensitive-  -> L.Factory-  -> E.Changers-  -> Exp.ExprDesc-  -> ([L.Transaction] -> [L.Box Ly.LibertyMeta])-  -> [Either String (P.State -> Ex.Exceptional X.Text P.State)]-  -> Ex.Exceptional X.Text I.ArgsAndReport-process os cs fty ch expr fsf ls =-  let (posArgs, clOpts) = Ei.partitionEithers ls-      pState = newParseState cs fty expr os-      exState' = foldl (>>=) (return pState) clOpts-  in fmap (mkPrintReport posArgs os ch fsf) exState'--mkPrintReport-  :: [String]-  -> ZincOpts-  -> E.Changers-  -> ([L.Transaction] -> [L.Box Ly.LibertyMeta])-  -> P.State-  -> I.ArgsAndReport-mkPrintReport posArgs zo ch fsf st = (posArgs, f)-  where-    f txns _ = do-      pdct <- getPredicate (P.exprDesc st) (P.tokens st)-      let boxes = fsf txns-          rptChks = postsReport ch (P.showZeroBalances st) pdct-                    (P.postFilter st) (chunkOpts st zo) boxes-          expChks = showExpression (P.showExpression st) pdct-          verbChks = showVerboseFilter (P.verboseFilter st) pdct boxes-          chks = expChks-                 ++ verbChks-                 ++ rptChks-      return chks--indentAmt :: Pe.IndentAmt-indentAmt = 4--blankLine :: Rb.Chunk-blankLine = Rb.plain (X.singleton '\n')--showExpression-  :: P.ShowExpression-  -> Pe.Pdct (L.Box Ly.LibertyMeta)-  -> [Rb.Chunk]-showExpression (P.ShowExpression b) pdct =-  if not b then [] else info : blankLine : (chks ++ [blankLine])-  where-    info = Rb.plain (X.pack "Postings filter expression:\n")-    chks = Pe.showPdct indentAmt 0 pdct--showVerboseFilter-  :: P.VerboseFilter-  -> Pe.Pdct (L.Box Ly.LibertyMeta)-  -> [L.Box Ly.LibertyMeta]-  -> [Rb.Chunk]-showVerboseFilter (P.VerboseFilter b) pdct bs =-  if not b then [] else info : blankLine : (chks ++ [blankLine])-  where-    pdcts = map (makeLabeledPdct pdct) bs-    chks = concat . map snd $ zipWith doEval bs pdcts-    doEval subj pd = Pe.evaluate indentAmt False subj 0 pd-    info = Rb.plain (X.pack "Postings report filter:\n")---- | Creates a Pdct and prepends a one-line description of the PostFam--- to the Pdct's label so it can be easily identified in the output.-makeLabeledPdct-  :: Pe.Pdct (L.Box Ly.LibertyMeta)-  -> L.Box Ly.LibertyMeta-  -> Pe.Pdct (L.Box Ly.LibertyMeta)-makeLabeledPdct pd box = Pe.rename f pd-  where-    f old = old <> " - " <> L.display pf-    pf = L.boxPostFam box--defaultOptions-  :: Sh.Runtime-  -> ZincOpts-defaultOptions rt = ZincOpts-  { fields = defaultFields-  , width = widthFromRuntime rt-  , showZeroBalances = CO.ShowZeroBalances False-  , dateFormat = yearMonthDay-  , qtyFormat = qtyAsIs-  , balanceFormat = balanceAsIs-  , subAccountLength = A.SubAccountLength 2-  , payeeAllocation = A.alloc 60-  , accountAllocation = A.alloc 40-  , spacers = defaultSpacerWidth-  }---type Error = X.Text--getPredicate-  :: Exp.ExprDesc-  -> [Exp.Token (L.Box Ly.LibertyMeta)]-  -> Ex.Exceptional Error (Pe.Pdct (L.Box Ly.LibertyMeta))-getPredicate d ts =-  case ts of-    [] -> return $ Pe.always-    _ -> Exp.parseExpression d ts----- | All the information to configure the postings report if the--- options will be parsed in from the command line.-data ZincOpts = ZincOpts-  { fields :: F.Fields Bool-    -- ^ Default fields to show in the report.--  , width :: T.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 current terminal.--  , showZeroBalances :: CO.ShowZeroBalances-    -- ^ Are commodities that have no balance shown in the Total fields-    -- of the report?--  , 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 Box 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 :: L.Commodity -> L.Qty -> X.Text-    -- ^ How to display balance totals. Similar to-    -- balanceFormat.--  , subAccountLength :: A.SubAccountLength-    -- ^ When shortening the names of sub accounts to make-    -- them fit, they will be this long.--  , payeeAllocation :: A.Alloc-    -- ^ 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.Alloc-    -- ^ See payeeAllocation above--  , spacers :: S.Spacers 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.--  }--chunkOpts ::-  P.State-  -> ZincOpts-  -> C.ChunkOpts-chunkOpts s z = C.ChunkOpts-  { C.dateFormat = dateFormat z-  , C.qtyFormat = qtyFormat z-  , C.balanceFormat = balanceFormat z-  , C.fields = P.fields s-  , C.subAccountLength = subAccountLength z-  , C.payeeAllocation = payeeAllocation z-  , C.accountAllocation = accountAllocation z-  , C.spacers = spacers z-  , C.reportWidth = P.width s-  }---newParseState ::-  CaseSensitive-  -> L.Factory-  -> Exp.ExprDesc-  -> ZincOpts-  -> P.State-newParseState cs fty expr o = P.State-  { P.sensitive = cs-  , P.factory = fty-  , P.tokens = []-  , P.postFilter = []-  , P.fields = fields o-  , P.width = width o-  , P.showZeroBalances = showZeroBalances o-  , P.exprDesc = expr-  , P.verboseFilter = P.VerboseFilter False-  , P.showExpression = P.ShowExpression False-  }---- | Shows the date of a posting in YYYY-MM-DD format.-yearMonthDay :: Box -> X.Text-yearMonthDay p = X.pack (Time.formatTime defaultTimeLocale fmt d)-  where-    d = L.day-        . Q.dateTime-        . L.boxPostFam-        $ p-    fmt = "%Y-%m-%d"---- | Shows the quantity of a posting. Does no rounding or--- prettification; simply uses show on the underlying Decimal.-qtyAsIs :: Box -> X.Text-qtyAsIs p = X.pack . show . Q.qty . L.boxPostFam $ p---- | Shows the quantity of a balance. If there is no quantity, shows--- two dashes.-balanceAsIs :: a -> L.Qty -> X.Text-balanceAsIs _ = X.pack . show---- | The default width for the report.-defaultWidth :: T.ReportWidth-defaultWidth = T.ReportWidth 80---- | Applied to the value of the COLUMNS environment variable, returns--- an appropriate ReportWidth.-columnsVarToWidth :: Maybe String -> T.ReportWidth-columnsVarToWidth ms = case ms of-  Nothing -> defaultWidth-  Just str -> case reads str of-    [] -> defaultWidth-    (i, []):[] -> if i > 0 then T.ReportWidth i else defaultWidth-    _ -> defaultWidth---- | Given the Runtime, use the defaultWidth given above to calculate--- the report's width if COLUMNS does not yield a value. Otherwise,--- use what is in COLUMNS.-widthFromRuntime :: Sh.Runtime -> T.ReportWidth-widthFromRuntime rt = case Sh.screenWidth rt of-  Nothing -> defaultWidth-  Just w -> T.ReportWidth . Sh.unScreenWidth $ w---- | Default fields to show in the Postings report.-defaultFields :: F.Fields Bool-defaultFields =-  F.Fields { 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 }---- | Default width of spacers; most are one character wide, but the--- spacer after payee is 4 characters wide.-defaultSpacerWidth :: S.Spacers Int-defaultSpacerWidth =-  S.Spacers { 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 }----------------------------------------------------------------- ## Help---------------------------------------------------------------ifDefault :: Bool -> String-ifDefault b = if b then " (default)" else ""--helpStr :: ZincOpts -> String-helpStr o = unlines $-  [ "postings"-  , "  Show postings in order with a running balance."-  , "  Accepts the following options:"-  , ""-  , "Posting filters"-  , "==============="-  , "These options affect which postings are shown in the report."-  , "Postings not shown still affect the running balance."-  , ""-  , "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)"-  , "--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"-  , ""-  , "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"-  , "--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"-  , ""-  , "Infix or RPN selection"-  , "----------------------"-  , "--infix - use infix notation"-  , "--rpn - use reverse polish notation"-  , "  (default: use what was used in the filtering options)"-  , ""-  , "Infix 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"-  , ""-  , "RPN Operators"-  , "-------------"-  , "expr --not"-  , "  True if expr is false"-  , "expr1 expr2 --and"-  , "  True if expr and expr2 are both true"-  , "expr1 expr2 --or"-  , "  True if either expr1 or expr2 is true"-  , ""-  , "Options affecting patterns"-  , "=========================="-  , ""-  , "-i, --case-insensitive"-  , "  Be case insensitive"-  , "-I, --case-sensitive"-  , "  Be case sensitive"-  , ""-  , "--within"-  , "  Use \"within\" matcher"-  , "--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"-  , ""-  , "Other options"-  , "============="-  , "--width num"-  , "  Hint for roughly how wide the report should be in columns"-  , "  (currently: " ++ (show . T.unReportWidth . width $ o) ++ ")"-  , "--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"-  , ""-  ] ++ showDefaultFields (fields o) ++-  [ ""-  , "--show-zero-balances"-  , "  Show balances that are zero"-    ++ ifDefault (CO.unShowZeroBalances . showZeroBalances $ o)-  , "--hide-zero-balances"-  , "  Hide balances that are zero"-    ++ ifDefault (not . CO.unShowZeroBalances . showZeroBalances $ o)-  , ""-  , "--help, -h"-  , "  Show this help and exit"-  ]---- | Shows which fields are on by default.-showDefaultFields :: F.Fields Bool -> [String]-showDefaultFields i = hdr : rest-  where-    hdr = "Fields shown by default:"-      ++ if null rest then " (none)" else ""-    rest =-      map ("  " ++)-      . map concat-      . map (intersperse ", ")-      . chunksOf 3-      . catMaybes-      . Fdbl.toList-      . toMaybes-      $ i-    toMaybes flds = f <$> flds <*> F.fieldNames-    f b n = if b then Just n else Nothing
− Penny/Cabin/Posts/Allocated.hs
@@ -1,402 +0,0 @@--- | 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. Obtain the width of the Payee and Account spacers. Include each--- spacer if its corresponding field appears in the report.------ 3. Subtract from the total report width the width of the the--- growing cells and the width of the Payee and Account spacers. This--- gives the total width available for the Payee and Account--- fields. If there are not at least two columns available, return--- without including the Payee and Account fields.------ 4. Determine the total width that the Payee and Account fields--- would obtain if they had all the space they could ever need. This--- is the "requested width".------ 5. Split up the available width for the Payee and Account fields--- depending on which fields appear:------ a. If only the one field appears, then it shall be as wide as the--- total available width or the its requested width, whichever is--- smaller.------ b. If both fields appear, then calculate the allocated width for--- each field. If either field's requested width is less than its--- allocated width, then that field is only as wide as its requested--- width. The other field is then as wide as (the sum of its allocated--- width and the leftover width from the other field) or its requested--- width, whichever is smaller. If neither field's requested width is--- less than its allocated width, then each field gets ts allocated--- width.------ 6. Fill cell contents; return filled cells.--module Penny.Cabin.Posts.Allocated (-  payeeAndAcct-  , AllocatedOpts(..)-  , Fields(..)-  , SubAccountLength(..)-  , Alloc-  , alloc-  , unAlloc-  ) where--import Control.Applicative(Applicative((<*>), pure), (<$>))-import Control.Arrow (second)-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 System.Console.Rainbow as Rb-import qualified Penny.Cabin.Row as R-import qualified Penny.Cabin.Posts.Growers as G-import qualified Penny.Cabin.Posts.Meta as M-import Penny.Cabin.Posts.Meta (Box)-import qualified Penny.Cabin.Posts.Spacers as S-import qualified Penny.Cabin.Posts.Types as Ty-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Cabin.TextFormat as TF-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Bits.Qty as Qty-import qualified Penny.Lincoln.Queries as Q-import qualified Penny.Lincoln.HasText as HT--data Fields a = Fields {-  payee :: a-  , account :: a-  } deriving (Eq, Show)--newtype SubAccountLength =-  SubAccountLength { unSubAccountLength :: Int }-  deriving Show--newtype Alloc = Alloc { unAlloc :: Int }-  deriving Show--alloc :: Int -> Alloc-alloc i =-  if i < 1-  then error $ "allocations must be greater than zero."-       ++ " supplied allocation: " ++ show i-  else Alloc i----- | All the information needed for allocated cells.-data AllocatedOpts = AllocatedOpts-  { fields :: Fields Bool-  , subAccountLength :: SubAccountLength-  , allocations :: Fields Alloc-  , spacers :: S.Spacers Int-  , growerWidths :: G.Fields (Maybe Int)-  , reportWidth :: Ty.ReportWidth-  }---- | 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-  :: E.Changers-  -> AllocatedOpts-  -> [Box]-  -> Fields (Maybe ([R.ColumnSpec], Int))-payeeAndAcct ch ao bs =-  let allBuilders =-        T.traverse (builders ch (subAccountLength ao)) bs-      availWidth = availableWidthForAllocs (growerWidths ao)-                   (spacers ao) (fields ao) (reportWidth ao)-      finals = divideAvailableWidth availWidth (fields ao)-               (allocations ao)-               ( fmap (safeMaximum (Request 0))-                 . fmap (fmap fst) $ allBuilders)-  in fmap (fmap (second unFinal))-     . buildSpecs finals-     . fmap (fmap snd)-     $ allBuilders---safeMaximum :: Ord a => a -> [a] -> a-safeMaximum d ls = case ls of-  [] -> d-  xs -> maximum xs--payeeAndAccountSpacerWidth-  :: Fields Bool-  -> S.Spacers Int-  -> Int-payeeAndAccountSpacerWidth flds ss = pye + act-  where-    pye = if payee flds then abs (S.payee ss) else 0-    act = if account flds then abs (S.account ss) else 0--newtype AvailableWidth = AvailableWidth Int-        deriving (Eq, Ord, Show)--availableWidthForAllocs-  :: G.Fields (Maybe Int)-  -> S.Spacers Int-  -> Fields Bool-  -> Ty.ReportWidth-  -> AvailableWidth-availableWidthForAllocs growers ss flds (Ty.ReportWidth w) =-  AvailableWidth $ max 0 diff-  where-    tot = sumGrowersAndSpacers growers ss-          + payeeAndAccountSpacerWidth flds ss-    diff = w - tot---- | 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)-  -> S.Spacers 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-  -> S.Spacers 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 widths of growing cells and their accompanying--- spacers; makes the adjustments described in sumSpacers.-sumGrowersAndSpacers ::-  G.Fields (Maybe Int)-  -> S.Spacers Int-  -> Int-sumGrowersAndSpacers fs ss = spcrs + flds where-  spcrs = sumSpacers fs ss-  flds = Fdbl.foldr f 0 fs where-    f maybeI acc = case maybeI of-      Nothing -> acc-      Just i -> acc + i--newtype Request = Request { unRequest :: Int }-        deriving (Eq, Ord, Show)--newtype Final = Final { unFinal :: Int }-        deriving (Eq, Ord, Show)---buildSpecs-  :: Fields (Maybe Final)-  -> Fields ([Final -> R.ColumnSpec])-  -> Fields (Maybe ([R.ColumnSpec], Final))-buildSpecs finals bs = f <$> finals <*> bs-  where-    f mayFinal gs = case mayFinal of-      Nothing -> Nothing-      Just fin -> Just ((gs <*> pure fin), fin)----- | Divide the total available width between the two fields.-divideAvailableWidth-  :: AvailableWidth-  -> Fields Bool-  -> Fields Alloc-  -> Fields Request-  -> Fields (Maybe Final)-divideAvailableWidth (AvailableWidth aw) appear allocs rws = Fields pye act-  where-    minFinal i1 i2 =-      let m = min i1 i2-      in if m > 0 then Just . Final $ m else Nothing-    pairAtLeast i1 i2 = (atLeast i1, atLeast i2)-      where atLeast i = if i > 0 then Just . Final $ i else Nothing-    reqP = unRequest . payee $ rws-    reqA = unRequest . account $ rws-    (pye, act) = case (payee appear, account appear) of-      (False, False) -> (Nothing, Nothing)-      (True, False) -> (minFinal reqP aw, Nothing)-      (False, True) -> (Nothing, minFinal reqA aw)-      (True, True) ->-        let votes = [unAlloc . payee $ allocs, unAlloc . account $ allocs]-            allocRslt = Qty.largestRemainderMethod (fromIntegral aw)-                        (map fromIntegral votes)-            (allocP, allocA) = case allocRslt of-              x:y:[] -> (fromIntegral x, fromIntegral y)-              _ -> error "divideAvailableWidth error"-        in case (allocP > reqP, allocA > reqA) of-            (True, True) -> pairAtLeast reqP reqA-            (True, False) ->-              pairAtLeast reqP $ (min (allocA + (allocP - reqP))) reqA-            (False, True) ->-              pairAtLeast (min reqP (allocP + (allocA - reqA))) reqA-            (False, False) -> pairAtLeast allocP allocA---builders-  :: E.Changers-  -> SubAccountLength-  -> Box-  -> Fields (Request, Final -> R.ColumnSpec)-builders ch sl b = Fields (buildPayee ch b) (buildAcct ch sl b)--buildPayee-  :: E.Changers-  -> Box-  -> (Request, Final -> R.ColumnSpec)-  -- ^ Returns a tuple. The first element is the maximum width that-  -- this cell needs to display its value perfectly. The second-  -- element is a function that, when applied to an actual width,-  -- returns a ColumnSpec.--buildPayee ch i = (maxW, mkSpec)-  where-    pb = L.boxPostFam i-    eo = E.fromVisibleNum . M.visibleNum . L.boxMeta $ i-    j = R.LeftJustify-    ps = (E.Other, eo)-    md = E.getEvenOddLabelValue E.Other eo ch-    mayPye = Q.payee pb-    maxW = Request $ maybe 0 (X.length . HT.text) mayPye-    mkSpec (Final w) = R.ColumnSpec j (R.Width w) ps sq-      where-        sq = case mayPye of-          Nothing -> []-          Just pye ->-            let wrapped =-                  Fdbl.toList-                  . TF.unLines-                  . TF.wordWrap w-                  . TF.txtWords-                  . HT.text-                  $ pye-                toBit (TF.Words seqTxts) =-                  md-                  . Rb.plain-                  . X.unwords-                  . Fdbl.toList-                  $ seqTxts-            in fmap toBit wrapped---buildAcct-  :: E.Changers-  -> SubAccountLength-  -> Box-  -> (Request, Final -> R.ColumnSpec)-  -- ^ Returns a tuple. The first element is the maximum width that-  -- this cell needs to display its value perfectly. The second-  -- element is a function that, when applied to an actual width,-  -- returns a ColumnSpec.--buildAcct ch sl i = (maxW, mkSpec)-  where-    pb = L.boxPostFam i-    eo = E.fromVisibleNum . M.visibleNum . L.boxMeta $ i-    ps = (E.Other, eo)-    aList = L.unAccount . Q.account $ pb-    maxW = Request-           $ (sum . map (X.length . L.unSubAccount) $ aList)-           + max 0 (length aList - 1)-    md = E.getEvenOddLabelValue E.Other eo ch-    mkSpec (Final aw) = R.ColumnSpec R.LeftJustify (R.Width aw) ps sq-      where-        target = TF.Target aw-        shortest = TF.Shortest . unSubAccountLength $ sl-        ws = TF.Words . Seq.fromList . map L.unSubAccount $ aList-        (TF.Words shortened) = TF.shorten shortest target ws-        sq = [ md-               . Rb.plain-               . 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
@@ -1,649 +0,0 @@--- | 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 (-  BottomOpts(..),-  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 System.Console.Rainbow as Rb-import qualified Penny.Cabin.Scheme as E-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.Posts.Fields as F-import qualified Penny.Cabin.Posts.Growers as G-import qualified Penny.Cabin.Posts.Meta as M-import Penny.Cabin.Posts.Meta (Box)-import qualified Penny.Cabin.Posts.Spacers as S-import qualified Penny.Cabin.Posts.Types as Ty-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.HasText as HT-import qualified Penny.Lincoln.Queries as Q--data BottomOpts = BottomOpts-  { growingWidths :: G.Fields (Maybe Int)-  , allocatedWidths :: A.Fields (Maybe Int)-  , fields :: F.Fields Bool-  , reportWidth :: Ty.ReportWidth-  , spacers :: S.Spacers Int-  }--bottomRows-  :: E.Changers-  -> BottomOpts-  -> [Box]-  -> Fields (Maybe [[Rb.Chunk]])-bottomRows ch os bs = makeRows bs pcs where-  pcs = infoProcessors ch topSpecs (reportWidth os) wanted-  wanted = requestedMakers ch (fields os)-  topSpecs = topCellSpecs (growingWidths os) (allocatedWidths os)-             (spacers 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 :: F.Fields 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-  :: E.Changers-  -> [TopCellSpec]-  -> Maybe ((Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))-            -> Box -> [Rb.Chunk])-hanging ch specs = hangingWidths specs-                >>= return . hangingInfoProcessor ch--hangingInfoProcessor-  :: E.Changers-  -> Hanging Int-  -> (Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))-  -> Box-  -> [Rb.Chunk]-hangingInfoProcessor ch widths mkr info = row where-  row = R.row ch [left, mid, right]-  (ts, mid) = mkr info (mainCell widths)-  mkPad w = R.ColumnSpec R.LeftJustify (R.Width w) ts []-  left = mkPad (leftPad widths)-  right = mkPad (rightPad widths)--widthOfTopColumns-  :: E.Changers-  -> [TopCellSpec]-  -> Maybe ((Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))-            -> Box -> [Rb.Chunk])-widthOfTopColumns ch ts =-  if null ts-  then Nothing-  else Just $ makeSpecificWidth ch w where-    w = Fdbl.foldl' f 0 ts-    f acc (_, maySpcWidth, (ContentWidth cw)) =-      acc + cw + maybe 0 (\(SpacerWidth sw) -> sw) maySpcWidth---widthOfReport-  :: E.Changers-  -> Ty.ReportWidth-  -> (Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))-  -> Box-  -> [Rb.Chunk]-widthOfReport ch (Ty.ReportWidth rw) fn info =-  makeSpecificWidth ch rw fn info--chooseProcessor-  :: E.Changers-  -> [TopCellSpec]-  -> Ty.ReportWidth-  -> (Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))-  -> Box-  -> [Rb.Chunk]-chooseProcessor ch specs rw fn = let-  firstTwo = First (hanging ch specs)-             `mappend` First (widthOfTopColumns ch specs)-  in case getFirst firstTwo of-    Nothing -> widthOfReport ch rw fn-    Just r -> r fn--infoProcessors-  :: E.Changers-  -> [TopCellSpec]-  -> Ty.ReportWidth-  -> Fields (Maybe (Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)))-  -> Fields (Maybe (Box -> [Rb.Chunk]))-infoProcessors ch specs rw flds = let-  chooser = chooseProcessor ch specs rw-  mkProcessor mayFn = case mayFn of-    Nothing -> Nothing-    Just fn -> Just $ chooser fn-  in mkProcessor <$> flds---makeRows ::-  [Box]-  -> Fields (Maybe (Box -> [Rb.Chunk]))-  -> Fields (Maybe [[Rb.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)-                -> S.Spacers 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-  -> S.Spacers 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-  :: E.Changers -> Int -> (Box -> Int -> (a, R.ColumnSpec))-  -> Box -> [Rb.Chunk]-makeSpecificWidth ch w f i = R.row ch [c] where-  (_, c) = f i w---type Maker-  = E.Changers-  -> Box-  -> Int-  -> ((E.Label, E.EvenOdd), 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-  :: E.Changers-  -> F.Fields Bool-  -> Fields (Maybe (Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)))-requestedMakers ch allFlds =-  let flds = bottomRowsFields allFlds-      filler b mkr = if b then Just $ mkr ch else Nothing-  in filler <$> flds <*> makers--tagsCell-  :: E.Changers-  -> Box-  -> Int-  -> ((E.Label, E.EvenOdd), R.ColumnSpec)-tagsCell ch info w = (ts, cell) where-  vn = M.visibleNum . L.boxMeta $ info-  cell = R.ColumnSpec R.LeftJustify (R.Width w) ts cs-  eo = E.fromVisibleNum vn-  ts = (E.Other, eo)-  cs =-    Fdbl.toList-    . fmap toBit-    . TF.unLines-    . TF.wordWrap w-    . TF.Words-    . Seq.fromList-    . map (X.cons '*')-    . HT.textList-    . Q.tags-    . L.boxPostFam-    $ info-  md = E.getEvenOddLabelValue E.Other eo ch-  toBit (TF.Words ws) = md . Rb.plain $ t where-    t = X.concat . intersperse (X.singleton ' ') . Fdbl.toList $ ws---memoBits-  :: E.Changers -> (E.Label, E.EvenOdd) -> L.Memo -> R.Width -> [Rb.Chunk]-memoBits ch (lbl, eo) m (R.Width w) = cs where-  cs = Fdbl.toList-       . fmap toBit-       . TF.unLines-       . TF.wordWrap w-       . TF.Words-       . Seq.fromList-       . X.words-       . X.intercalate (X.singleton ' ')-       . L.unMemo-       $ m-  md = E.getEvenOddLabelValue lbl eo ch-  toBit (TF.Words ws) = md . Rb.plain $ (X.unwords . Fdbl.toList $ ws)---memoCell-  :: E.Changers -> Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)-memoCell ch info width = (ts, cell) where-  w = R.Width width-  vn = M.visibleNum . L.boxMeta $ info-  eo = E.fromVisibleNum vn-  ts = (E.Other, eo)-  cell = R.ColumnSpec R.LeftJustify w ts cs-  mayPm = Q.postingMemo . L.boxPostFam $ info-  mayTm = Q.transactionMemo . L.boxPostFam $ info-  cs = case (mayPm, mayTm) of-    (Nothing, Nothing) -> mempty-    (Nothing, Just tm) -> memoBits ch ts tm w-    (Just pm, Nothing) -> memoBits ch ts pm w-    (Just pm, Just tm) -> memoBits ch ts pm w `mappend` memoBits ch ts tm w---filenameCell-  :: E.Changers -> Box -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)-filenameCell ch info width = (ts, cell) where-  w = R.Width width-  vn = M.visibleNum . L.boxMeta $ info-  eo = E.fromVisibleNum vn-  ts = (E.Other, eo)-  cell = R.ColumnSpec R.LeftJustify w ts cs-  md = E.getEvenOddLabelValue E.Other eo ch-  toBit n = md . Rb.plain-            . X.drop (max 0 (X.length n - width)) $ n-  cs = case Q.filename . L.boxPostFam $ info of-    Nothing -> []-    Just fn -> [toBit . L.unFilename $ fn]---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
@@ -1,152 +0,0 @@-module Penny.Cabin.Posts.Chunk (ChunkOpts(..), makeChunk) where--import qualified Data.Foldable as Fdbl-import Data.List (transpose)-import Data.Maybe (isNothing, catMaybes)-import qualified Penny.Cabin.Posts.Fields as F-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.Spacers as S-import qualified Penny.Cabin.Row as R-import qualified Penny.Cabin.Scheme as E-import qualified System.Console.Rainbow as Rb-import Penny.Cabin.Posts.Meta (Box)-import qualified Penny.Lincoln as L-import qualified Data.Text as X-import qualified Penny.Cabin.Posts.Types as Ty--data ChunkOpts = ChunkOpts-  { dateFormat :: Box -> X.Text-  , qtyFormat :: Box -> X.Text-  , balanceFormat :: L.Commodity -> L.Qty -> X.Text-  , fields :: F.Fields Bool-  , subAccountLength :: A.SubAccountLength-  , payeeAllocation :: A.Alloc-  , accountAllocation :: A.Alloc-  , spacers :: S.Spacers Int-  , reportWidth :: Ty.ReportWidth-  }--growOpts :: ChunkOpts -> G.GrowOpts-growOpts c = G.GrowOpts-  { G.dateFormat = dateFormat c-  , G.qtyFormat = qtyFormat c-  , G.balanceFormat = balanceFormat c-  , G.fields = fields c-  }--allocatedOpts :: ChunkOpts -> G.Fields (Maybe Int) -> A.AllocatedOpts-allocatedOpts c g = A.AllocatedOpts-  { A.fields = let f = fields c-               in A.Fields { A.payee = F.payee f-                           , A.account = F.account f }-  , A.subAccountLength = subAccountLength c-  , A.allocations = A.Fields { A.payee = payeeAllocation c-                             , A.account = accountAllocation c }-  , A.spacers = spacers c-  , A.growerWidths = g-  , A.reportWidth = reportWidth c-  }--bottomOpts ::-  ChunkOpts-  -> G.Fields (Maybe Int)-  -> A.Fields (Maybe Int)-  -> B.BottomOpts-bottomOpts c g a = B.BottomOpts {-  B.growingWidths = g-  , B.allocatedWidths = a-  , B.fields = fields c-  , B.reportWidth = reportWidth c-  , B.spacers = spacers c-  }--makeChunk-  :: E.Changers-  -> ChunkOpts-  -> [Box]-  -> [Rb.Chunk]-makeChunk ch c bs =-  let fmapSnd = fmap (fmap snd)-      fmapFst = fmap (fmap fst)-      gFldW = fmap (fmap snd) gFlds-      aFldW = fmapSnd aFlds-      gFlds = G.growCells ch (growOpts c) bs-      aFlds = A.payeeAndAcct ch (allocatedOpts c gFldW) bs-      bFlds = B.bottomRows ch (bottomOpts c gFldW aFldW) bs-      topCells = B.topRowCells (fmapFst gFlds) (fmap (fmap fst) aFlds)-      withSpacers = B.mergeWithSpacers topCells (spacers c)-      topRows = makeTopRows ch withSpacers-      bottomRows = makeBottomRows bFlds-  in makeAllRows topRows bottomRows---topRowsCells-  :: B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)-  -> [[(R.ColumnSpec, Maybe R.ColumnSpec)]]-topRowsCells t = let-  toWithSpc (mayCs, maySp) = case mayCs of-    Nothing -> Nothing-    Just cs -> Just (makeSpacers cs maySp)-  f mayPairList acc = case mayPairList of-    Nothing -> acc-    (Just pairList) -> pairList : acc-  in transpose $ Fdbl.foldr f [] (fmap toWithSpc t)--makeRow :: E.Changers -> [(R.ColumnSpec, Maybe R.ColumnSpec)] -> [Rb.Chunk]-makeRow ch = R.row ch . foldr f [] where-  f (c, mayC) acc = case mayC of-    Nothing -> c:acc-    Just spcr -> c:spcr:acc---makeSpacers-  :: [R.ColumnSpec]-  -> Maybe Int-  -> [(R.ColumnSpec, Maybe R.ColumnSpec)]-makeSpacers cs mayI = case mayI of-  Nothing -> map (\c -> (c, Nothing)) cs-  Just i -> makeEvenOddSpacers cs i--makeEvenOddSpacers-  :: [R.ColumnSpec]-  -> Int-  -> [(R.ColumnSpec, Maybe R.ColumnSpec)]-makeEvenOddSpacers 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 (R.Width absI) ts []-    evenTs = (E.Other, E.Even)-    oddTs = (E.Other, E.Odd)-    in zip cs spcrs--makeTopRows-  :: E.Changers-  -> B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)-  -> Maybe [[Rb.Chunk]]-makeTopRows ch trc =-  if Fdbl.all (isNothing . fst) trc-  then Nothing-  else Just $ map (makeRow ch) . topRowsCells $ trc---makeBottomRows ::-  B.Fields (Maybe [[Rb.Chunk]])-  -> Maybe [[[Rb.Chunk]]]-makeBottomRows flds =-  if Fdbl.all isNothing flds-  then Nothing-  else Just . transpose . catMaybes . Fdbl.toList $ flds--makeAllRows :: Maybe [[Rb.Chunk]] -> Maybe [[[Rb.Chunk]]] -> [Rb.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
@@ -1,199 +0,0 @@--- | 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 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-  , 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 Fields where-  fmap f fa = Fields {-    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 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-    , payee = a-    , account = a-    , postingDrCr = a-    , postingCmdty = a-    , postingQty = a-    , totalDrCr = a-    , totalCmdty = a-    , totalQty = a-    , tags = a-    , memo = a-    , filename = a }--  ff <*> fa = Fields {-    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 Fields 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))))))))))))))))))))))))))))---fieldNames :: Fields String-fieldNames = Fields-  { globalTransaction    = "globalTransaction"-  , revGlobalTransaction = "revGlobalTransaction"-  , globalPosting        = "globalPosting"-  , revGlobalPosting     = "revGlobalPosting"-  , fileTransaction      = "fileTransaction"-  , revFileTransaction   = "revFileTransaction"-  , filePosting          = "filePosting"-  , revFilePosting       = "revFilePosting"-  , filtered             = "filtered"-  , revFiltered          = "revFiltered"-  , sorted               = "sorted"-  , revSorted            = "revSorted"-  , visible              = "visible"-  , revVisible           = "revVisible"-  , lineNum              = "lineNum"-  , date                 = "date"-  , flag                 = "flag"-  , number               = "number"-  , payee                = "payee"-  , account              = "account"-  , postingDrCr          = "postingDrCr"-  , postingCmdty         = "postingCmdty"-  , postingQty           = "postingQty"-  , totalDrCr            = "totalDrCr"-  , totalCmdty           = "totalCmdty"-  , totalQty             = "totalQty"-  , tags                 = "tags"-  , memo                 = "memo"-  , filename             = "filename"-  }
− Penny/Cabin/Posts/Growers.hs
@@ -1,596 +0,0 @@--- | 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 (-  GrowOpts(..),-  growCells, Fields(..), grownWidth,-  eFields, EFields(..), pairWithSpacer) where--import Control.Applicative((<$>), Applicative(pure, (<*>)))-import qualified Data.Foldable as Fdbl-import Data.Map (elems)-import qualified Data.Map as Map-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.Posts.Fields as F-import qualified Penny.Cabin.Posts.Meta as M-import Penny.Cabin.Posts.Meta (Box)-import qualified Penny.Cabin.Posts.Spacers as S-import qualified Penny.Cabin.Row as R-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Liberty as Ly-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Queries as Q-import qualified System.Console.Rainbow as Rb----- | All the options needed to grow the cells.-data GrowOpts = GrowOpts-  { dateFormat :: Box -> X.Text-  , qtyFormat :: Box -> X.Text-  , balanceFormat :: L.Commodity -> L.Qty -> X.Text-  , fields :: F.Fields Bool-  }---- | 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-  :: E.Changers-  -> GrowOpts-  -> [Box]-  -> Fields (Maybe ([R.ColumnSpec], Int))-growCells ch o infos = toPair <$> wanted <*> growers where-  toPair b gwr-    | b =-      let cs = map (gwr o ch) 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 . fields $ o--widestLine :: PreSpec -> Int-widestLine (PreSpec _ _ bs) =-  case bs of-    [] -> 0-    xs -> maximum . map (X.length . Rb.chunkText) $ xs--data PreSpec = PreSpec {-  _justification :: R.Justification-  , _padSpec :: (E.Label, E.EvenOdd)-  , _bits :: [Rb.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 :: E.Changers -> Text -> E.Label -> Box -> PreSpec-oneLine chgrs t lbl b =-  let eo = E.fromVisibleNum . M.visibleNum . L.boxMeta $ b-      j = R.LeftJustify-      md = E.getEvenOddLabelValue lbl eo chgrs-      ck = [md $ Rb.plain t]-  in PreSpec j (lbl, eo) ck----- | Gets a Fields with each field filled with the function that fills--- the cells for that field.-growers :: Fields (GrowOpts -> E.Changers -> Box -> PreSpec)-growers = Fields-  { globalTransaction    = const getGlobalTransaction-  , revGlobalTransaction = const getRevGlobalTransaction-  , globalPosting        = const getGlobalPosting-  , revGlobalPosting     = const getRevGlobalPosting-  , fileTransaction      = const getFileTransaction-  , revFileTransaction   = const getRevFileTransaction-  , filePosting          = const getFilePosting-  , revFilePosting       = const getRevFilePosting-  , filtered             = const getFiltered-  , revFiltered          = const getRevFiltered-  , sorted               = const getSorted-  , revSorted            = const getRevSorted-  , visible              = const getVisible-  , revVisible           = const getRevVisible-  , lineNum              = const getLineNum-  , date                 = \o ch -> getDate ch (dateFormat o)-  , flag                 = const getFlag-  , number               = const getNumber-  , postingDrCr          = const getPostingDrCr-  , postingCmdty         = const getPostingCmdty-  , postingQty           = \o ch -> getPostingQty ch (qtyFormat o)-  , totalDrCr            = const getTotalDrCr-  , totalCmdty           = const getTotalCmdty-  , totalQty             = \o ch -> getTotalQty ch (balanceFormat o)-  }---- | Make a left justified cell one line long that shows a serial.-serialCellMaybe-  :: E.Changers-  -> (L.PostFam -> Maybe Int)-  -- ^ When applied to a Box, this function returns Just Int if the-  -- box has a serial, or Nothing if not.--  -> Box -> PreSpec-serialCellMaybe chgrs f b = oneLine chgrs t E.Other b-  where-    t = case f (L.boxPostFam b) of-      Nothing -> X.empty-      Just i -> X.pack . show $ i--serialCell-  :: E.Changers-  -> (M.PostMeta -> Int)-  -> Box -> PreSpec-serialCell chgrs f b = oneLine chgrs t E.Other b-  where-    t = pack . show . f . L.boxMeta $ b--getGlobalTransaction :: E.Changers -> Box -> PreSpec-getGlobalTransaction chgrs =-  serialCellMaybe chgrs (fmap (L.forward . L.unGlobalTransaction)-                        . Q.globalTransaction)--getRevGlobalTransaction :: E.Changers -> Box -> PreSpec-getRevGlobalTransaction chgrs =-  serialCellMaybe chgrs (fmap (L.backward . L.unGlobalTransaction)-                        . Q.globalTransaction)--getGlobalPosting :: E.Changers -> Box -> PreSpec-getGlobalPosting chgrs =-  serialCellMaybe chgrs (fmap (L.forward . L.unGlobalPosting)-                        . Q.globalPosting)--getRevGlobalPosting :: E.Changers -> Box -> PreSpec-getRevGlobalPosting chgrs =-  serialCellMaybe chgrs (fmap (L.backward . L.unGlobalPosting)-                   . Q.globalPosting)--getFileTransaction :: E.Changers -> Box -> PreSpec-getFileTransaction chgrs =-  serialCellMaybe chgrs (fmap (L.forward . L.unFileTransaction)-                   . Q.fileTransaction)--getRevFileTransaction :: E.Changers -> Box -> PreSpec-getRevFileTransaction chgrs =-  serialCellMaybe chgrs (fmap (L.backward . L.unFileTransaction)-                   . Q.fileTransaction)--getFilePosting :: E.Changers -> Box -> PreSpec-getFilePosting chgrs =-  serialCellMaybe chgrs (fmap (L.forward . L.unFilePosting)-                   . Q.filePosting)--getRevFilePosting :: E.Changers -> Box -> PreSpec-getRevFilePosting chgrs =-  serialCellMaybe chgrs (fmap (L.backward . L.unFilePosting)-                   . Q.filePosting)--getSorted :: E.Changers -> Box -> PreSpec-getSorted chgrs =-  serialCell chgrs (L.forward . Ly.unSortedNum . M.sortedNum)--getRevSorted :: E.Changers -> Box -> PreSpec-getRevSorted chgrs =-  serialCell chgrs (L.backward . Ly.unSortedNum . M.sortedNum)--getFiltered :: E.Changers -> Box -> PreSpec-getFiltered chgrs =-  serialCell chgrs (L.forward . Ly.unFilteredNum . M.filteredNum)--getRevFiltered :: E.Changers -> Box -> PreSpec-getRevFiltered chgrs =-  serialCell chgrs (L.backward . Ly.unFilteredNum . M.filteredNum)--getVisible :: E.Changers -> Box -> PreSpec-getVisible chgrs =-  serialCell chgrs (L.forward . M.unVisibleNum . M.visibleNum)--getRevVisible :: E.Changers -> Box -> PreSpec-getRevVisible chgrs =-  serialCell chgrs (L.backward . M.unVisibleNum . M.visibleNum)---getLineNum :: E.Changers -> Box -> PreSpec-getLineNum chgrs b = oneLine chgrs t E.Other b where-  lineTxt = pack . show . L.unPostingLine-  t = maybe empty lineTxt (Q.postingLine . L.boxPostFam $ b)--getDate :: E.Changers -> (Box -> X.Text) -> Box -> PreSpec-getDate chgrs gd b = oneLine chgrs (gd b) E.Other b--getFlag :: E.Changers -> Box -> PreSpec-getFlag chgrs i = oneLine chgrs t E.Other i where-  t = maybe empty L.text (Q.flag . L.boxPostFam $ i)--getNumber :: E.Changers -> Box -> PreSpec-getNumber chgrs i = oneLine chgrs t E.Other i where-  t = maybe empty L.text (Q.number . L.boxPostFam $ i)--dcTxt :: L.DrCr -> Text-dcTxt L.Debit = X.singleton '<'-dcTxt L.Credit = X.singleton '>'---- | Gives a one-line cell that is colored according to whether the--- posting is a debit or credit.-coloredPostingCell :: E.Changers -> Text -> Box -> PreSpec-coloredPostingCell chgrs t i = PreSpec j (lbl, eo) [bit] where-  j = R.LeftJustify-  lbl = case Q.drCr . L.boxPostFam $ i of-    L.Debit -> E.Debit-    L.Credit -> E.Credit-  eo = E.fromVisibleNum . M.visibleNum . L.boxMeta $ i-  md = E.getEvenOddLabelValue lbl eo chgrs-  bit = md $ Rb.plain t---getPostingDrCr :: E.Changers -> Box -> PreSpec-getPostingDrCr ch i = coloredPostingCell ch t i where-  t = dcTxt . Q.drCr . L.boxPostFam $ i--getPostingCmdty :: E.Changers -> Box -> PreSpec-getPostingCmdty ch i = coloredPostingCell ch t i where-  t = L.unCommodity . Q.commodity . L.boxPostFam $ i--getPostingQty :: E.Changers -> (Box -> X.Text) -> Box -> PreSpec-getPostingQty ch qf i = coloredPostingCell ch (qf i) i--getTotalDrCr :: E.Changers -> Box -> PreSpec-getTotalDrCr ch i =-  let vn = M.visibleNum . L.boxMeta $ i-      ps = (lbl, eo)-      dc = Q.drCr . L.boxPostFam $ i-      lbl = E.dcToLbl dc-      eo = E.fromVisibleNum vn-      bal = L.unBalance . M.balance . L.boxMeta $ i-      md = E.getEvenOddLabelValue lbl eo ch-      bits =-        if Map.null bal-        then [md . Rb.plain $ pack "--"]-        else let mkChk e = E.bottomLineToDrCr e eo ch-             in fmap mkChk . elems $ bal-      j = R.LeftJustify-  in PreSpec j ps bits--getTotalCmdty :: E.Changers -> Box -> PreSpec-getTotalCmdty ch i =-  let vn = M.visibleNum . L.boxMeta $ i-      j = R.RightJustify-      ps = (lbl, eo)-      dc = Q.drCr . L.boxPostFam $ i-      eo = E.fromVisibleNum vn-      lbl = E.dcToLbl dc-      bal = Map.toList . L.unBalance . M.balance . L.boxMeta $ i-      preChunks = E.balancesToCmdtys ch eo bal-  in PreSpec j ps preChunks--getTotalQty-  :: E.Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> Box-  -> PreSpec-getTotalQty ch balFmt i =-  let vn = M.visibleNum . L.boxMeta $ i-      j = R.LeftJustify-      dc = Q.drCr . L.boxPostFam $ i-      ps = (E.dcToLbl dc, eo)-      eo = E.fromVisibleNum vn-      bal = Map.toList . L.unBalance . M.balance . L.boxMeta $ i-      preChunks = E.balanceToQtys ch balFmt eo bal-  in PreSpec j ps preChunks--growingFields :: F.Fields Bool -> Fields Bool-growingFields f = 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 -> S.Spacers 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)-  -> S.Spacers 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/Meta.hs
@@ -1,79 +0,0 @@-module Penny.Cabin.Posts.Meta (-  M.VisibleNum(M.unVisibleNum)-  , PostMeta(filteredNum, sortedNum, visibleNum, balance)-  , Box-  , toBoxList-  ) 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.Cabin.Meta as M-import qualified Penny.Cabin.Options as CO-import qualified Data.Prednote.Pdct as Pe-import Data.Monoid (mempty, mappend)---- | The Box type that is used throughout the Posts modules.-type Box = L.Box PostMeta--data PostMeta =-  PostMeta { filteredNum :: Ly.FilteredNum-            , sortedNum :: Ly.SortedNum-            , visibleNum :: M.VisibleNum-            , balance :: L.Balance }-  deriving Show---addMetadata ::-  [(L.Box (Ly.LibertyMeta, L.Balance))]-  -> [Box]-addMetadata = M.visibleNumBoxes f where-  f vn (lm, b) =-    PostMeta (Ly.filteredNum lm) (Ly.sortedNum lm) vn b---- | Adds appropriate metadata, including the running balance, to a--- list of Box. Because all posts are incorporated into the running--- balance, first calculates the running balance for all posts. Then,--- removes posts we're not interested in by applying the predicate and--- the post-filter. Finally, adds on the metadata, which will include--- the VisibleNum.-toBoxList ::-  CO.ShowZeroBalances-  -> Pe.Pdct (L.Box Ly.LibertyMeta)-  -- ^ Removes posts from the report if applying this function to the-  -- post returns a value other than Just True. Posts removed still-  -- affect the running balance.--  -> [Ly.PostFilterFn]-  -- ^ Applies these post-filters to the list of posts that results-  -- from applying the predicate above. Might remove more-  -- postings. Postings removed still affect the running balance.--  -> [L.Box Ly.LibertyMeta]-  -> [Box]-toBoxList szb pdct pff =-  addMetadata-  . Ly.processPostFilters pff-  . filter (maybe False id . Pe.eval pdct . fmap fst)-  . addBalances szb--addBalances ::-  CO.ShowZeroBalances-  -> [L.Box Ly.LibertyMeta]-  -> [(L.Box (Ly.LibertyMeta, L.Balance))]-addBalances szb = snd . mapAccumL (balanceAccum szb) mempty--balanceAccum :: -  CO.ShowZeroBalances-  -> L.Balance-  -> L.Box Ly.LibertyMeta-  -> (L.Balance, (L.Box (Ly.LibertyMeta, L.Balance)))-balanceAccum (CO.ShowZeroBalances szb) balOld po =-  let balThis = L.entryToBalance . Q.entry . L.boxPostFam $ po-      balNew = mappend balOld balThis-      balNoZeroes = L.removeZeroCommodities balNew-      bal' = if szb then balNew else balNoZeroes-      po' = L.Box (L.boxMeta po, bal') (L.boxPostFam po)-  in (bal', po')-
− Penny/Cabin/Posts/Parser.hs
@@ -1,285 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Penny.Cabin.Posts.Parser-  ( State(..)-  , allSpecs-  , VerboseFilter(..)-  , ShowExpression(..)-  ) where--import Control.Applicative ((<$>), pure, (<*>),-                            Applicative)-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.Char (toLower)-import qualified Data.Foldable as Fdbl-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as X-import qualified System.Console.MultiArg.Combinator as C-import qualified System.Console.MultiArg as MA--import qualified Penny.Cabin.Parsers as P-import qualified Penny.Cabin.Posts.Fields as F-import qualified Penny.Cabin.Posts.Types as Ty-import qualified Penny.Cabin.Options as CO-import qualified Penny.Liberty as Ly-import qualified Data.Prednote.Expressions as Exp-import qualified Data.Prednote.Pdct as Pt-import qualified Penny.Lincoln as L-import qualified Penny.Shield as S-import qualified Text.Matchers as M--newtype VerboseFilter = VerboseFilter { unVerboseFilter :: Bool }-  deriving (Eq, Show)--newtype ShowExpression = ShowExpression { unShowExpression :: Bool }-  deriving (Eq, Show)--data State = State-  { sensitive :: M.CaseSensitive-  , factory :: L.Factory-  , tokens :: [Exp.Token (L.Box Ly.LibertyMeta)]-  , postFilter :: [Ly.PostFilterFn]-  , fields :: F.Fields Bool-  , width :: Ty.ReportWidth-  , showZeroBalances :: CO.ShowZeroBalances-  , exprDesc :: Exp.ExprDesc-  , verboseFilter :: VerboseFilter-  , showExpression :: ShowExpression-  }--type Error = X.Text--allSpecs-  :: S.Runtime -> [MA.OptSpec (State -> Ex.Exceptional Error State)]-allSpecs rt =-  operand rt-  ++ boxFilters-  ++ parsePostFilter-  ++ (map (fmap (pure .)) matcherSelect)-  ++ (map (fmap (pure .)) caseSelect)-  ++ (map (fmap (pure .)) operator)-  ++ map (fmap (pure .)) parseExprType-  ++ [ parseWidth-     , showField-     , hideField-     , fmap (pure .) showAllFields-     , fmap (pure .) hideAllFields-     , fmap (pure .) parseZeroBalances-     , fmap (pure .) parseShowExpression-     , fmap (pure .) parseVerboseFilter-     ]---operand-  :: S.Runtime-  -> [MA.OptSpec (State -> Ex.Exceptional Error State)]-operand rt = map (fmap f) (Ly.operandSpecs (S.currentTime rt))-  where-    f lyFn st = do-      let cs = sensitive st-          fty = factory st-      g <- lyFn cs fty-      let g' = Pt.boxPdct L.boxPostFam g-          ts' = tokens st ++ [Exp.operand g']-      return $ st { tokens = ts' }----- | Processes a option for box-level serials.-optBoxSerial-  :: String-  -- ^ Serial name--  -> (Ly.LibertyMeta -> Int)-  -- ^ Pulls the serial from the PostMeta--  -> C.OptSpec (State -> Ex.Exceptional Error State)--optBoxSerial nm f = C.OptSpec [nm] "" (C.TwoArg g)-  where-    g a1 a2 st = do-      i <- Ly.parseInt a2-      let getPd = Pt.compareBy (X.pack . show $ i)-                  ("serial " <> X.pack nm) cmp-          cmp l = compare (f . L.boxMeta $ l) i-      pd <- Ly.parseComparer a1 getPd-      let tok = Exp.operand pd-      return $ st { tokens = tokens st ++ [tok] }--optFilteredNum :: C.OptSpec (State -> Ex.Exceptional Error State)-optFilteredNum = optBoxSerial "filtered" f-  where-    f = L.forward . Ly.unFilteredNum . Ly.filteredNum--optRevFilteredNum :: C.OptSpec (State -> Ex.Exceptional Error State)-optRevFilteredNum = optBoxSerial "revFiltered" f-  where-    f = L.backward . Ly.unFilteredNum . Ly.filteredNum--optSortedNum :: C.OptSpec (State -> Ex.Exceptional Error State)-optSortedNum = optBoxSerial "sorted" f-  where-    f = L.forward . Ly.unSortedNum . Ly.sortedNum--optRevSortedNum :: C.OptSpec (State -> Ex.Exceptional Error State)-optRevSortedNum = optBoxSerial "revSorted" f-  where-    f = L.backward . Ly.unSortedNum . Ly.sortedNum--boxFilters :: [C.OptSpec (State -> Ex.Exceptional Error State)]-boxFilters =-  [ optFilteredNum-  , optRevFilteredNum-  , optSortedNum-  , optRevSortedNum-  ]---parsePostFilter :: [C.OptSpec (State -> Ex.Exceptional Error State)]-parsePostFilter = [fmap f optH, fmap f optT]-  where-    (optH, optT) = Ly.postFilterSpecs-    f exc st = fmap g exc-      where-        g pff = st { postFilter = postFilter st ++ [pff] }---matcherSelect :: [C.OptSpec (State -> State)]-matcherSelect = map (fmap f) Ly.matcherSelectSpecs-  where-    f mf st = st { factory = mf }---caseSelect :: [C.OptSpec (State -> State)]-caseSelect = map (fmap f) Ly.caseSelectSpecs-  where-    f cs st = st { sensitive = cs }--operator :: [C.OptSpec (State -> State)]-operator = map (fmap f) Ly.operatorSpecs-  where-    f oo st = st { tokens = tokens st ++ [oo] }--parseWidth :: C.OptSpec (State -> Ex.Exceptional Error State)-parseWidth = C.OptSpec ["width"] "" (C.OneArg f)-  where-    f a1 st = do-      i <- Ly.parseInt a1-      return $ st { width = Ty.ReportWidth i }--parseField :: String -> Ex.Exceptional Error (F.Fields Bool)-parseField str =-  let lower = map toLower str-      checkField s =-        if (map toLower s) == lower-        then (s, True)-        else (s, False)-      flds = checkField <$> F.fieldNames-  in case checkFields flds of-      Ex.Exception e -> case e of-        NoMatchingFields -> Ex.throw-          $ "no field matches the name \"" <> X.pack str <> "\"\n"-        MultipleMatchingFields ts -> Ex.throw-          $ "multiple fields match the name \"" <> X.pack str-            <> "\" matches: " <> mtchs <> "\n"-          where-            mtchs = X.intercalate " "-                    . map (\x -> "\"" <> x <> "\"")-                    $ ts-      Ex.Success g -> return g----- | Turns a field on if it is True.-fieldOn ::-  F.Fields Bool-  -- ^ Fields as seen so far--  -> F.Fields Bool-  -- ^ Record that should have one True element indicating a field-  -- name seen on the command line; other elements should be False--  -> F.Fields Bool-  -- ^ Fields as seen so far, with new field added--fieldOn old new = (||) <$> old <*> new---- | Turns off a field if it is True.-fieldOff ::-  F.Fields Bool-  -- ^ Fields seen so far--  -> F.Fields Bool-  -- ^ Record that should have one True element indicating a field-  -- name seen on the command line; other elements should be False--  -> F.Fields Bool-  -- ^ Fields as seen so far, with new field added--fieldOff old new = f <$> old <*> new-  where-    f o False = o-    f _ True = False--showField :: C.OptSpec (State -> Ex.Exceptional Error State)-showField = C.OptSpec ["show"] "" (C.OneArg f)-  where-    f a1 st = do-      fl <- parseField a1-      let newFl = fieldOn (fields st) fl-      return $ st { fields = newFl }--hideField :: C.OptSpec (State -> Ex.Exceptional Error State)-hideField = C.OptSpec ["hide"] "" (C.OneArg f)-  where-    f a1 st = do-      fl <- parseField a1-      let newFl = fieldOff (fields st) fl-      return $ st { fields = newFl }--showAllFields :: C.OptSpec (State -> State)-showAllFields = C.OptSpec ["show-all"] "" (C.NoArg f)-  where-    f st = st {fields = pure True}--hideAllFields :: C.OptSpec (State -> State)-hideAllFields = C.OptSpec ["hide-all"] "" (C.NoArg f)-  where-    f st = st {fields = pure False}--parseZeroBalances :: C.OptSpec (State -> State)-parseZeroBalances = fmap f P.zeroBalances-  where-    f szb st = st { showZeroBalances = szb }--parseExprType :: [C.OptSpec (State -> State)]-parseExprType = map (fmap f) [Ly.parseInfix, Ly.parseRPN]-  where-    f d st = st { exprDesc = d }--parseShowExpression :: C.OptSpec (State -> State)-parseShowExpression = fmap f Ly.showExpression-  where-    f _ st = st { showExpression = ShowExpression True }--parseVerboseFilter :: C.OptSpec (State -> State)-parseVerboseFilter = fmap f Ly.verboseFilter-  where-    f _ st = st { verboseFilter = VerboseFilter True }--data BadFieldError-  = NoMatchingFields-  | MultipleMatchingFields [Text]-  deriving Show---- | Checks the fields with the True value to ensure there is only one.-checkFields ::-  F.Fields (String, Bool)-  -> Ex.Exceptional BadFieldError (F.Fields Bool)-checkFields fs =-  let f (s, b) ls = if b then s:ls else ls-  in case Fdbl.foldr f [] fs of-    [] -> Ex.throw NoMatchingFields-    _:[] -> return (snd <$> fs)-    ms -> Ex.throw . MultipleMatchingFields . map X.pack $ ms--
− Penny/Cabin/Posts/Spacers.hs
@@ -1,32 +0,0 @@--- | 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 Spacers a = Spacers-  { 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-  } deriving (Show, Eq)
− Penny/Cabin/Posts/Types.hs
@@ -1,4 +0,0 @@-module Penny.Cabin.Posts.Types where--newtype ReportWidth = ReportWidth { unReportWidth :: Int }-                      deriving (Eq, Show, Ord)
− Penny/Cabin/Row.hs
@@ -1,138 +0,0 @@--- | Displays a single on-screen row. A row may contain multiple--- screen lines and multiple columns.------ This module only deals with a single row at a time. Each cell in--- the row can have more than one screen line; this module will make--- sure that the cells have appropriate padding on the bottom so that--- the row appears nicely. This module will also justify each cell so--- that its left side or right side is ragged; however, you first have--- to specify how wide you want the cell to be.------ This module is a little dumber than you might first think it could--- be. For instance it would be possible to write a function that--- takes a number of rows and automatically justifies all the cells by--- finding the widest cell in a column. Indeed I might eventually--- write such a function because it might be useful in, for example,--- the multi-commodity balance report. However, such a function would--- not be useful in all cases; in particular, the Posts report is very--- complicated to lay out, and the automatic function described above--- would not do the right thing.------ So this module offers some useful automation, even if it is at a--- level that is apparently lower that what is possible. Thus the--- present 'row' function likely will not change, even if eventually I--- add a 'table' function that automatically justifies many rows.-module Penny.Cabin.Row (-  Justification(LeftJustify, RightJustify),-  ColumnSpec(ColumnSpec, justification, width, padSpec, bits),-  Width(Width, unWidth),-  row ) where--import Data.List (transpose)-import qualified Data.Text as X-import qualified Penny.Cabin.Scheme as E-import qualified System.Console.Rainbow as R---- | 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 :: Width-             , padSpec :: (E.Label, E.EvenOdd)-             , bits :: [R.Chunk] }--newtype JustifiedCell = JustifiedCell (R.Chunk, R.Chunk)--data JustifiedColumn = JustifiedColumn {-  justifiedCells :: [JustifiedCell]-  , _justifiedWidth :: Width-  , _justifiedPadSpec :: (E.Label, E.EvenOdd) }--newtype PaddedColumns = PaddedColumns [[JustifiedCell]]-newtype CellsByRow = CellsByRow [[JustifiedCell]]-newtype CellRowsWithNewlines = CellRowsWithNewlines [[JustifiedCell]]-newtype Width = Width { unWidth :: Int }-  deriving (Eq, Ord, Show)--justify-  :: Width-  -> Justification-  -> E.Label-  -> E.EvenOdd-  -> E.Changers-  -> R.Chunk-  -> JustifiedCell-justify (Width w) j l eo chgrs pc = JustifiedCell (left, right)-  where-    origWidth = X.length . R.chunkText $ pc-    pad = E.getEvenOddLabelValue l eo chgrs $ R.plain t-    t = X.replicate (max 0 (w - origWidth)) (X.singleton ' ')-    (left, right) = case j of-      LeftJustify -> (pc, pad)-      RightJustify -> (pad, pc)--newtype Height = Height Int-  deriving (Show, Eq, Ord)--height :: [[a]] -> Height-height xs = case xs of-  [] -> Height 0-  ls -> Height . maximum . map length $ ls--row :: E.Changers -> [ColumnSpec] -> [R.Chunk]-row chgrs =-  concat-  . concat-  . toBits-  . toCellRowsWithNewlines-  . toCellsByRow-  . bottomPad chgrs-  . map (justifiedColumn chgrs)--justifiedColumn :: E.Changers -> ColumnSpec -> JustifiedColumn-justifiedColumn chgrs (ColumnSpec j w (l, eo) bs)-  = JustifiedColumn cs w (l, eo)-  where-    cs = map (justify w j l eo chgrs) bs--bottomPad :: E.Changers -> [JustifiedColumn] -> PaddedColumns-bottomPad chgrs jcs = PaddedColumns pcs where-  justCells = map justifiedCells jcs-  (Height h) = height justCells-  pcs = map toPaddedColumn jcs-  toPaddedColumn (JustifiedColumn cs (Width w) (lbl, eo)) =-    let l = length cs-        nPads = max 0 $ h - l-        pad = E.getEvenOddLabelValue lbl eo chgrs $ R.plain t-        t = X.replicate w (X.singleton ' ')-        pads = replicate nPads $ JustifiedCell (R.plain X.empty, pad)-    in cs ++ pads---toCellsByRow :: PaddedColumns -> CellsByRow-toCellsByRow (PaddedColumns cs) = CellsByRow (transpose cs)---toCellRowsWithNewlines :: CellsByRow -> CellRowsWithNewlines-toCellRowsWithNewlines (CellsByRow bs) =-  CellRowsWithNewlines bs' where-    bs' = foldr f [] bs-    newline = JustifiedCell (R.plain X.empty, R.plain (X.singleton '\n'))-    f cells acc = (cells ++ [newline]) : acc---toBits :: CellRowsWithNewlines -> [[[R.Chunk]]]-toBits (CellRowsWithNewlines cs) = map (map toB) cs where-  toB (JustifiedCell (c1, c2)) = [c1, c2]-
− Penny/Cabin/Scheme.hs
@@ -1,140 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Cabin color schemes------ Each element of a Cabin report identifies what it is--a debit on an--- even line, a credit on an odd line, etc. The user can have several--- color schemes; the scheme contains color assignments for 8 and 256--- color terminals. This allows the use of different schemes for light--- and dark terminals or for any other reason.--module Penny.Cabin.Scheme where--import qualified Penny.Cabin.Meta as M-import qualified Penny.Lincoln as L-import qualified Data.Text as X-import qualified System.Console.Rainbow as R--data Label-  = Debit-  | Credit-  | Zero-  | Other-  deriving (Eq, Ord, Show)--data EvenOdd = Even | Odd deriving (Eq, Ord, Show)--data Labels a = Labels-  { debit :: a-  , credit :: a-  , zero :: a-  , other :: a-  } deriving Show--getLabelValue :: Label -> Labels a -> a-getLabelValue l ls = case l of-  Debit -> debit ls-  Credit -> credit ls-  Zero -> zero ls-  Other -> other ls--data EvenAndOdd a = EvenAndOdd-  { eoEven :: a-  , eoOdd :: a-  } deriving Show--type Changers = Labels (EvenAndOdd (R.Chunk -> R.Chunk))--data Scheme = Scheme-  { name :: String-    -- ^ The name of this scheme. How it will be identified on the-    -- command line.--  , description :: String-    -- ^ A brief (one-line) description of what this scheme is, such-    -- as @for dark background terminals@--  , changers :: Changers-  } deriving Show---getEvenOdd :: EvenOdd -> EvenAndOdd a -> a-getEvenOdd eo eao = case eo of-  Even -> eoEven eao-  Odd -> eoOdd eao--getEvenOddLabelValue-  :: Label-  -> EvenOdd-  -> Labels (EvenAndOdd a)-  -> a-getEvenOddLabelValue l eo ls =-  getEvenOdd eo (getLabelValue l ls)--fromVisibleNum :: M.VisibleNum -> EvenOdd-fromVisibleNum vn =-  let s = M.unVisibleNum vn in-  if even . L.forward $ s then Even else Odd--dcToLbl :: L.DrCr -> Label-dcToLbl L.Debit = Debit-dcToLbl L.Credit = Credit--bottomLineToDrCr :: L.BottomLine -> EvenOdd -> Changers -> R.Chunk-bottomLineToDrCr bl eo chgrs = md c-  where-    (c, md) = case bl of-      L.Zero -> (R.plain "--", getEvenOddLabelValue Zero eo chgrs)-      L.NonZero (L.Column clmDrCr _) -> case clmDrCr of-        L.Debit -> (R.plain "<", getEvenOddLabelValue Debit eo chgrs)-        L.Credit -> (R.plain ">", getEvenOddLabelValue Credit eo chgrs)---balancesToCmdtys-  :: Changers-  -> EvenOdd-  -> [(L.Commodity, L.BottomLine)]-  -> [R.Chunk]-balancesToCmdtys chgrs eo ls =-  if null ls-  then [getEvenOddLabelValue Zero eo chgrs $ R.plain "--"]-  else map (bottomLineToCmdty chgrs eo) ls--bottomLineToCmdty-  :: Changers-  -> EvenOdd-  -> (L.Commodity, L.BottomLine)-  -> R.Chunk-bottomLineToCmdty chgrs eo (cy, bl) = md c-  where-    c = R.plain . L.unCommodity $ cy-    lbl = case bl of-      L.Zero -> Zero-      L.NonZero (L.Column clmDrCr _) -> dcToLbl clmDrCr-    md = getEvenOddLabelValue lbl eo chgrs--balanceToQtys-  :: Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> EvenOdd-  -> [(L.Commodity, L.BottomLine)]-  -> [R.Chunk]-balanceToQtys chgrs getTxt eo ls =-  if null ls-  then let md = getEvenOddLabelValue Zero eo chgrs-       in [md (R.plain "--")]-  else map (bottomLineToQty chgrs getTxt eo) ls---bottomLineToQty-  :: Changers-  -> (L.Commodity -> L.Qty -> X.Text)-  -> EvenOdd-  -> (L.Commodity, L.BottomLine)-  -> R.Chunk-bottomLineToQty chgrs getTxt eo (cy, bl) = md (R.plain t)-  where-    (lbl, t) = case bl of-      L.Zero -> (Zero, X.pack "--")-      L.NonZero (L.Column clmDrCr qt) -> (dcToLbl clmDrCr, getTxt cy qt)-    md = getEvenOddLabelValue lbl eo chgrs-
− Penny/Cabin/Scheme/Schemes.hs
@@ -1,87 +0,0 @@--- | Some schemes you can use.--module Penny.Cabin.Scheme.Schemes where--import qualified Penny.Cabin.Scheme as E-import qualified System.Console.Rainbow as R-import System.Console.Rainbow ((.+.))---- | The light color scheme. You can change various values below to--- affect the color scheme.-light :: E.Scheme-light = E.Scheme "light" "for light background terminals"-              lightLabels--lightLabels :: E.Labels (E.EvenAndOdd (R.Chunk -> R.Chunk))-lightLabels = E.Labels-  { E.debit = E.EvenAndOdd { E.eoEven = lightDebit lightEvenTextSpec-                           , E.eoOdd = lightDebit lightOddTextSpec }-  , E.credit = E.EvenAndOdd { E.eoEven = lightCredit lightEvenTextSpec-                            , E.eoOdd = lightCredit lightOddTextSpec }-  , E.zero = E.EvenAndOdd { E.eoEven = lightZero lightEvenTextSpec-                          , E.eoOdd = lightZero lightOddTextSpec }-  , E.other = E.EvenAndOdd { E.eoEven = lightEvenTextSpec-                           , E.eoOdd = lightOddTextSpec }-  }--lightEvenTextSpec :: R.Chunk -> R.Chunk-lightEvenTextSpec = id--lightOddTextSpec :: R.Chunk -> R.Chunk-lightOddTextSpec = id .+. R.color8_b_default .+. R.color256_b_255--lightDebit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk-lightDebit f = f .+. R.color8_f_magenta .+. R.color256_f_52--lightCredit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk-lightCredit f = f .+. R.color8_f_cyan .+. R.color256_f_21--lightZero :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk-lightZero f = f .+. R.color8_f_black .+. R.color256_f_0---- | The dark color scheme. You can change various values below to--- affect the color scheme.-dark :: E.Scheme-dark = E.Scheme "dark" "for dark background terminals"-              darkLabels--darkLabels :: E.Labels (E.EvenAndOdd (R.Chunk -> R.Chunk))-darkLabels = E.Labels-  { E.debit = E.EvenAndOdd { E.eoEven = darkDebit darkEvenTextSpec-                           , E.eoOdd = darkDebit darkOddTextSpec }-  , E.credit = E.EvenAndOdd { E.eoEven = darkCredit darkEvenTextSpec-                            , E.eoOdd = darkCredit darkOddTextSpec }-  , E.zero = E.EvenAndOdd { E.eoEven = darkZero darkEvenTextSpec-                          , E.eoOdd = darkZero darkOddTextSpec }-  , E.other = E.EvenAndOdd { E.eoEven = darkEvenTextSpec-                           , E.eoOdd = darkOddTextSpec }-  }--darkEvenTextSpec :: R.Chunk -> R.Chunk-darkEvenTextSpec = id--darkOddTextSpec :: R.Chunk -> R.Chunk-darkOddTextSpec = id .+. R.color8_b_default .+. R.color256_b_235--darkDebit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk-darkDebit f = f .+. R.color8_f_magenta .+. R.color256_f_208--darkCredit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk-darkCredit f = f .+. R.color8_f_cyan .+. R.color256_f_45--darkZero :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk-darkZero f = f .+. R.color8_f_white .+. R.color256_f_15---- | Plain scheme has no colors at all.-plain :: E.Scheme-plain = E.Scheme "plain" "uses default terminal colors"-              plainLabels--plainLabels :: E.Labels (E.EvenAndOdd (R.Chunk -> R.Chunk))-plainLabels = E.Labels-  { E.debit = E.EvenAndOdd id id-  , E.credit = E.EvenAndOdd id id-  , E.zero = E.EvenAndOdd id id-  , E.other = E.EvenAndOdd id id-  }-
− Penny/Cabin/TextFormat.hs
@@ -1,173 +0,0 @@-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
@@ -1,211 +0,0 @@--- | Copper - the Penny parser.------ The parse functions in this module only accept lists of files--- rather than individual files because in order to correctly assign--- the global serials a single function must be able to see all the--- transactions, not just the transactions in a single file.-module Penny.Copper-  (-  -- * Convenience functions to read and parse files-    parse-  , open--  -- * Types for things found in ledger files-  , Y.Item(BlankLine, IComment, PricePoint, Transaction)-  , Y.mapItem-  , Y.mapItemA-  , Y.Ledger(Ledger, unLedger)-  , Y.mapLedger-  , Y.mapLedgerA-  , Y.Comment(Comment, unComment)-  , FileContents(FileContents, unFileContents)-  , ErrorMsg (unErrorMsg)--  -- * Rendering-  , R.GroupSpec(..)-  , R.GroupSpecs(..)-  , R.ledger--  ) where--import Control.Monad (when, replicateM_)-import Control.Applicative (pure, (*>), (<$>))-import Data.Functor.Compose (Compose(Compose, getCompose))-import Data.Maybe (mapMaybe)-import Data.Monoid (mconcat)-import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Data.Foldable as F-import qualified Data.Text as X-import qualified Data.Text.IO as TIO-import qualified Text.Parsec as Parsec-import qualified Penny.Copper.Parsec as CP--import qualified Penny.Lincoln as L-import qualified Penny.Copper.Render as R-import qualified Penny.Copper.Types as Y-import System.Console.MultiArg.GetArgs (getProgName)-import qualified System.Exit as Exit-import qualified System.IO as IO--newtype FileContents = FileContents { unFileContents :: X.Text }-                       deriving (Eq, Show)--newtype ErrorMsg = ErrorMsg { unErrorMsg :: X.Text }-                   deriving (Eq, Show)--parseFile ::-  (L.Filename, FileContents)-  -> Ex.Exceptional ErrorMsg Y.Ledger-parseFile (fn, (FileContents c)) =-  let p = fmap (addFileMetadata fn) CP.ledger-      fnStr = X.unpack . L.unFilename $ fn-  in case Parsec.parse p fnStr c of-    Left err -> Ex.throw (ErrorMsg . X.pack . show $ err)-    Right g -> return g--addFileTransaction-  :: L.Filename-  -> L.Transaction-  -> L.GenSerial L.Transaction-addFileTransaction fn t = f <$> L.getSerial-  where-    f ser = L.changeTransaction fam t-      where-        fam = L.Family tl e e []-        e = L.emptyPostingChangeData-        tl = L.emptyTopLineChangeData-             { L.tcFileTransaction =-                Just (Just $ L.FileTransaction ser)-             , L.tcFilename =-                Just (Just fn) }--addFilePosting-  :: L.Transaction-  -> L.GenSerial L.Transaction-addFilePosting t = f <$> (L.mapChildrenA g (L.unTransaction t))-  where-    f fam = L.changeTransaction-            (L.mapParent (const L.emptyTopLineChangeData) fam) t-    g = const $ fmap h L.getSerial-      where h ser = L.emptyPostingChangeData-              { L.pcFilePosting = Just (Just (L.FilePosting ser)) }--addFileMetadataTxn-  :: L.Filename-  -> L.Transaction-  -> Compose L.GenSerial L.GenSerial L.Transaction-addFileMetadataTxn fn t = Compose $ do-  t' <- addFileTransaction fn t-  return (addFilePosting t')--toPostings :: L.Transaction -> [L.Posting]-toPostings = F.toList . L.orphans . L.unTransaction--initCntTxn :: [a] -> L.GenSerial ()-initCntTxn ts = replicateM_ (length ts) L.incrementBack--initCntPstg :: [Y.Item] -> L.GenSerial ()-initCntPstg fs = replicateM_ (length ls) L.incrementBack-  where-    ls = concatMap toPostings . mapMaybe toTxn $ fs--toTxn :: Y.Item -> Maybe L.Transaction-toTxn i = case i of-  Y.Transaction t -> Just t-  _ -> Nothing--addFileMetadata :: L.Filename -> Y.Ledger -> Y.Ledger-addFileMetadata fn a@(Y.Ledger ls) =-  (L.makeSerials . (initCntPstg ls *>))-  . (L.makeSerials . (initCntTxn ls *>))-  . getCompose-  . Y.mapLedgerA (Y.mapItemA pure pure (addFileMetadataTxn fn))-  $ a---addGlobalTransaction-  :: L.Transaction-  -> L.GenSerial L.Transaction-addGlobalTransaction t = f <$> L.getSerial-  where-    f ser = L.changeTransaction fam t-      where-        fam = L.Family tl e e []-        e = L.emptyPostingChangeData-        tl = L.emptyTopLineChangeData-             { L.tcGlobalTransaction =-               Just (Just $ L.GlobalTransaction ser) }--addGlobalPosting-  :: L.Transaction-  -> L.GenSerial L.Transaction-addGlobalPosting t = f <$> (L.mapChildrenA g (L.unTransaction t))-  where-    f fam = L.changeTransaction-            (L.mapParent (const L.emptyTopLineChangeData) fam) t-    g = const $ fmap h L.getSerial-      where-        h ser = L.emptyPostingChangeData-          { L.pcGlobalPosting = Just (Just (L.GlobalPosting ser)) }--addGlobalMetadataTxn ::-  L.Transaction-  -> Compose L.GenSerial L.GenSerial L.Transaction-addGlobalMetadataTxn t = Compose $ do-  t' <- addGlobalTransaction t-  return (addGlobalPosting t')--addGlobalMetadata :: [Y.Ledger] -> Y.Ledger-addGlobalMetadata ls =-  (L.makeSerials . (initCntPstg ls' *>))-  . (L.makeSerials . (initCntTxn ls' *>))-  . getCompose-  . Y.mapLedgerA (Y.mapItemA pure pure addGlobalMetadataTxn)-  $ a-  where-    a@(Y.Ledger ls') = mconcat ls--parse ::-  [(L.Filename, FileContents)]-  -> Ex.Exceptional ErrorMsg Y.Ledger-parse ps = fmap addGlobalMetadata $ mapM parseFile ps---parseAndResolve :: (L.Filename, FileContents) -> IO Y.Ledger-parseAndResolve p@(L.Filename fn, _) =-  Ex.switch err return $ parseFile p-  where-    err (ErrorMsg x) = do-      pn <- getProgName-      let msg = pn ++ ": error: could not parse file "-                ++ X.unpack fn ++ "\n"-                ++ X.unpack x-      IO.hPutStr IO.stderr msg-      Exit.exitFailure----- | Reads and parses the given files. If any of the files is @-@,--- reads standard input. If the list of files is empty, reads standard--- input. IO errors are not caught. Parse errors are printed to--- standard error and the program will exit with a failure.-open :: [String] -> IO Y.Ledger-open ss =-  let ls = if null ss-           then fmap (:[]) (getFileContentsStdin "-")-           else mapM getFileContentsStdin ss-  in fmap addGlobalMetadata (ls >>= mapM parseAndResolve)--getFileContentsStdin :: String -> IO (L.Filename, FileContents)-getFileContentsStdin s = do-  pn <- getProgName-  txt <- if s == "-"-          then do-                isTerm <- IO.hIsTerminalDevice IO.stdin-                when isTerm-                  (IO.hPutStrLn IO.stderr $-                     pn ++ ": warning: reading from standard input, which"-                     ++ "is a terminal.")-                TIO.hGetContents IO.stdin-          else TIO.readFile s-  let fn = L.Filename . X.pack $ if s == "-" then "<stdin>" else s-  return (fn, FileContents txt)
− Penny/Copper/Parsec.hs
@@ -1,418 +0,0 @@--- | Parsec parsers for the ledger file format. The format is--- documented in EBNF in the file @doc\/ledger-grammar.org@.-module Penny.Copper.Parsec where--import qualified Penny.Copper.Terminals as T-import qualified Penny.Copper.Types as Y-import Text.Parsec.Text (Parser)-import Text.Parsec (many, many1, satisfy)-import qualified Text.Parsec as P-import qualified Text.Parsec.Pos as Pos-import Control.Arrow (first, second)-import Control.Applicative ((<$>), (<$), (<*>), (*>), (<*),-                            (<|>), optional)-import Control.Monad (replicateM)-import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Transaction.Unverified as U-import Data.Maybe (fromMaybe)-import Data.Text (Text, pack)-import qualified Data.Time as Time--lvl1SubAcct :: Parser L.SubAccount-lvl1SubAcct =-  (L.SubAccount . pack) <$> many1 (satisfy T.lvl1AcctChar)--lvl1FirstSubAcct :: Parser L.SubAccount-lvl1FirstSubAcct = lvl1SubAcct--lvl1OtherSubAcct :: Parser L.SubAccount-lvl1OtherSubAcct = satisfy T.colon *> lvl1SubAcct--lvl1Acct :: Parser L.Account-lvl1Acct = f <$> lvl1FirstSubAcct <*> many lvl1OtherSubAcct-  where-    f a as = L.Account (a:as)--quotedLvl1Acct :: Parser L.Account-quotedLvl1Acct =-  satisfy T.openCurly *> lvl1Acct <* satisfy T.closeCurly--lvl2FirstSubAcct :: Parser L.SubAccount-lvl2FirstSubAcct =-  (\c cs -> L.SubAccount (pack (c:cs)))-  <$> satisfy T.letter-  <*> many (satisfy T.lvl2AcctOtherChar)--lvl2OtherSubAcct :: Parser L.SubAccount-lvl2OtherSubAcct =-  (L.SubAccount . pack)-  <$ satisfy T.colon-  <*> many1 (satisfy T.lvl2AcctOtherChar)--lvl2Acct :: Parser L.Account-lvl2Acct =-  (\a as -> L.Account (a:as))-  <$> lvl2FirstSubAcct-  <*> many lvl2OtherSubAcct--ledgerAcct :: Parser L.Account-ledgerAcct = quotedLvl1Acct <|> lvl2Acct--lvl1Cmdty :: Parser L.Commodity-lvl1Cmdty = (L.Commodity . pack) <$> many1 (satisfy T.lvl1CmdtyChar)--quotedLvl1Cmdty :: Parser L.Commodity-quotedLvl1Cmdty =-  satisfy T.doubleQuote *> lvl1Cmdty <* satisfy (T.doubleQuote)--lvl2Cmdty :: Parser L.Commodity-lvl2Cmdty =-  (\c cs -> L.Commodity (pack (c:cs)))-  <$> satisfy T.lvl2CmdtyFirstChar-  <*> many (satisfy T.lvl2CmdtyOtherChar)--lvl3Cmdty :: Parser L.Commodity-lvl3Cmdty = (L.Commodity . pack) <$> many1 (satisfy T.lvl3CmdtyChar)--digitGroup :: Parser [Char]-digitGroup = satisfy T.thinSpace *> many1 (satisfy T.digit)--digitSequence :: Parser [Char]-digitSequence =-  (++) <$> many1 (satisfy T.digit)-  <*> (concat <$> (many digitGroup))--digitPostSequence :: Parser (Maybe [Char])-digitPostSequence = satisfy T.period *> optional digitSequence--quantity :: Parser L.Qty-quantity = p >>= failOnErr-  where-    p = (L.RadFrac <$> (satisfy T.period *> digitSequence))-        <|> (f <$> digitSequence <*> optional digitPostSequence)-    f digSeq maybePostSeq = case maybePostSeq of-      Nothing -> L.Whole digSeq-      Just ps ->-        maybe (L.WholeRad digSeq) (L.WholeRadFrac digSeq) ps-    failOnErr = maybe (fail msg) return . L.toQty-    msg = "could not read quantity; zero quantities not allowed"--spaceBetween :: Parser L.SpaceBetween-spaceBetween = f <$> optional (many1 (satisfy T.white))-  where-    f = maybe L.NoSpaceBetween (const L.SpaceBetween)--leftCmdtyLvl1Amt :: Parser L.Amount-leftCmdtyLvl1Amt =-  f <$> quotedLvl1Cmdty <*> spaceBetween <*> quantity-  where-    f c s q = L.Amount q c (Just L.CommodityOnLeft) (Just s)--leftCmdtyLvl3Amt :: Parser L.Amount-leftCmdtyLvl3Amt = f <$> lvl3Cmdty <*> spaceBetween <*> quantity-  where-    f c s q = L.Amount q c (Just L.CommodityOnLeft) (Just s)--leftSideCmdtyAmt :: Parser L.Amount-leftSideCmdtyAmt = leftCmdtyLvl1Amt <|> leftCmdtyLvl3Amt--rightSideCmdty :: Parser L.Commodity-rightSideCmdty = quotedLvl1Cmdty <|> lvl2Cmdty--rightSideCmdtyAmt :: Parser L.Amount-rightSideCmdtyAmt =-  f <$> quantity <*> spaceBetween <*> rightSideCmdty-  where-    f q s c = L.Amount q c (Just L.CommodityOnRight) (Just s)---amount :: Parser L.Amount-amount = leftSideCmdtyAmt <|> rightSideCmdtyAmt--comment :: Parser Y.Comment-comment =-  (Y.Comment . pack)-  <$ satisfy T.hash-  <*> many (satisfy T.nonNewline)-  <* satisfy T.newline-  <* many (satisfy T.white)--year :: Parser Integer-year = read <$> replicateM 4 P.digit--month :: Parser Int-month = read <$> replicateM 2 P.digit--day :: Parser Int-day = read <$> replicateM 2 P.digit--date :: Parser Time.Day-date = p >>= failOnErr-  where-    p = Time.fromGregorianValid-        <$> year  <* satisfy T.dateSep-        <*> month <* satisfy T.dateSep-        <*> day-    failOnErr = maybe (fail "could not parse date") return--hours :: Parser L.Hours-hours = p >>= (maybe (fail "could not parse hours") return)-  where-    p = f <$> satisfy T.digit <*> satisfy T.digit-    f d1 d2 = L.intToHours . read $ [d1,d2]---minutes :: Parser L.Minutes-minutes = p >>= maybe (fail "could not parse minutes") return-  where-    p = f <$ satisfy T.colon <*> satisfy T.digit <*> satisfy T.digit-    f d1 d2 = L.intToMinutes . read $ [d1, d2]--seconds :: Parser L.Seconds-seconds = p >>= maybe (fail "could not parse seconds") return-  where-    p = f <$ satisfy T.colon <*> satisfy T.digit <*> satisfy T.digit-    f d1 d2 = L.intToSeconds . read $ [d1, d2]--time :: Parser (L.Hours, L.Minutes, Maybe L.Seconds)-time = (,,) <$> hours <*> minutes <*> optional seconds--tzSign :: Parser (Int -> Int)-tzSign = (id <$ satisfy T.plus) <|> (negate <$ satisfy T.minus)--tzNumber :: Parser Int-tzNumber = read <$> replicateM 4 (satisfy T.digit)--timeZone :: Parser L.TimeZoneOffset-timeZone = p >>= maybe (fail "could not parse time zone") return-  where-    p = f <$> tzSign <*> tzNumber-    f s = L.minsToOffset . s--timeWithZone-  :: Parser (L.Hours, L.Minutes,-             Maybe L.Seconds, Maybe L.TimeZoneOffset)-timeWithZone =-  f <$> time <* many (satisfy T.white) <*> optional timeZone-  where-    f (h, m, s) tz = (h, m, s, tz)--dateTime :: Parser L.DateTime-dateTime =-  f <$> date <* many (satisfy T.white) <*> optional timeWithZone-  where-    f d mayTwithZ = L.DateTime d h m s tz-      where-        ((h, m, s), tz) = case mayTwithZ of-          Nothing -> (L.midnight, L.noOffset)-          Just (hr, mn, mayS, mayTz) ->-            let sec = fromMaybe L.zeroSeconds mayS-                z = fromMaybe L.noOffset mayTz-            in ((hr, mn, sec), z)--debit :: Parser L.DrCr-debit = L.Debit <$ satisfy T.lessThan--credit :: Parser L.DrCr-credit = L.Credit <$ satisfy T.greaterThan--drCr :: Parser L.DrCr-drCr = debit <|> credit--entry :: Parser L.Entry-entry = f <$> drCr <* (many (satisfy T.white)) <*> amount-  where-    f dc am = L.Entry dc am--flag :: Parser L.Flag-flag = (L.Flag . pack) <$ satisfy T.openSquare-  <*> many (satisfy T.flagChar) <* satisfy (T.closeSquare)--postingMemoLine :: Parser Text-postingMemoLine =-  pack-  <$ satisfy T.apostrophe-  <*> many (satisfy T.nonNewline)-  <* satisfy T.newline <* many (satisfy T.white)--postingMemo :: Parser L.Memo-postingMemo = L.Memo <$> many1 postingMemoLine--transactionMemoLine :: Parser Text-transactionMemoLine =-  pack-  <$ satisfy T.semicolon <*> many (satisfy T.nonNewline)-  <* satisfy T.newline <* skipWhite--transactionMemo :: Parser (L.TopMemoLine, L.Memo)-transactionMemo = f <$> lineNum <*> many1 transactionMemoLine-  where-    f tml ls = (L.TopMemoLine tml-               , L.Memo ls)---number :: Parser L.Number-number =-  L.Number . pack <$ satisfy T.openParen-  <*> many (satisfy T.numberChar) <* satisfy T.closeParen--lvl1Payee :: Parser L.Payee-lvl1Payee = L.Payee . pack <$> many (satisfy T.quotedPayeeChar)--quotedLvl1Payee :: Parser L.Payee-quotedLvl1Payee = satisfy T.tilde *> lvl1Payee <* satisfy T.tilde--lvl2Payee :: Parser L.Payee-lvl2Payee = (\c cs -> L.Payee (pack (c:cs))) <$> satisfy T.letter-            <*> many (satisfy T.nonNewline)--fromCmdty :: Parser L.From-fromCmdty = L.From <$> (quotedLvl1Cmdty <|> lvl2Cmdty)--lineNum :: Parser Int-lineNum = Pos.sourceLine <$> P.getPosition--price :: Parser L.PricePoint-price = p >>= maybe (fail msg) return-  where-    f li dt fr (L.Amount qt to sd sb) =-      let cpu = L.CountPerUnit qt-      in case L.newPrice fr (L.To to) cpu of-        Nothing -> Nothing-        Just pr -> Just $ L.PricePoint dt pr-                          sd sb (Just $ L.PriceLine li)-    p = f <$> lineNum <* satisfy T.atSign <* skipWhite-        <*> dateTime <* skipWhite-        <*> fromCmdty <* skipWhite-        <*> amount <* satisfy T.newline <* skipWhite-    msg = "could not parse price, make sure the from and to commodities "-          ++ "are different"--tag :: Parser L.Tag-tag = L.Tag . pack <$ satisfy T.asterisk <*> many (satisfy T.tagChar)-      <* many (satisfy T.white)--tags :: Parser L.Tags-tags = (\t ts -> L.Tags (t:ts)) <$> tag <*> many tag--topLinePayee :: Parser L.Payee-topLinePayee = quotedLvl1Payee <|> lvl2Payee--topLineFlagNum :: Parser (Maybe L.Flag, Maybe L.Number)-topLineFlagNum = p1 <|> p2-  where-    p1 = ( (,) <$> optional flag-               <* many (satisfy T.white) <*> optional number)-    p2 = ( flip (,)-           <$> optional number-           <* many (satisfy T.white) <*> optional flag)--skipWhite :: Parser ()-skipWhite = () <$ many (satisfy T.white)--topLine :: Parser U.TopLine-topLine =-  f <$> optional transactionMemo-    <*> lineNum-    <*> dateTime-    <*  skipWhite-    <*> topLineFlagNum-    <*  skipWhite-    <*> optional topLinePayee-    <*  satisfy T.newline-    <*  skipWhite-  where-    f mayMe lin dt (mayFl, mayNum) mayPy =-      U.TopLine dt mayFl mayNum mayPy me tll tml Nothing-      Nothing Nothing-      where-        (tml, me) = case mayMe of-          Nothing -> (Nothing, Nothing)-          Just (l, m) -> (Just l, Just m)-        tll = Just (L.TopLineLine lin)--pairedMaybes-  :: Parser (a, Maybe b)-  -> Parser (Maybe a, b)-  -> Parser (Maybe a, Maybe b)-pairedMaybes p1 p2 =-  (fmap (first Just) p1) <|> (fmap (second Just) p2)--parsePair-  :: Parser a-  -> Parser b-  -> Parser (Maybe a, Maybe b)-parsePair a b = pairedMaybes aFirst bFirst-  where-    aFirst = (,) <$> a <* skipWhite <*> optional b-    bFirst = flip (,) <$> b <* skipWhite <*> optional a--parseTriple-  :: Parser a-  -> Parser b-  -> Parser c-  -> Parser (a, Maybe b, Maybe c)-parseTriple a b c =-  f-  <$> a-  <* skipWhite-  <*> optional (parsePair b c)-  where-    f ra mayRbc = case mayRbc of-      Nothing -> (ra, Nothing, Nothing)-      Just (rb, rc) -> (ra, rb, rc)---flagFirst :: Parser (L.Flag, Maybe L.Number, Maybe L.Payee)-flagFirst = parseTriple flag number quotedLvl1Payee--numberFirst :: Parser (L.Number, Maybe L.Flag, Maybe L.Payee)-numberFirst = parseTriple number flag quotedLvl1Payee--payeeFirst :: Parser (L.Payee, Maybe L.Flag, Maybe L.Number)-payeeFirst = parseTriple quotedLvl1Payee flag number--flagNumPayee :: Parser (Maybe L.Flag, Maybe L.Number, Maybe L.Payee)-flagNumPayee =-  ((\(f, n, p) -> (Just f, n, p)) <$> flagFirst)-  <|> ((\(n, f, p) -> (f, Just n, p)) <$> numberFirst)-  <|> ((\(p, f, n) -> (f, n, Just p)) <$> payeeFirst)---postingAcct :: Parser L.Account-postingAcct = quotedLvl1Acct <|> lvl2Acct--posting :: Parser U.Posting-posting = f <$> lineNum                <* skipWhite-            <*> optional flagNumPayee  <* skipWhite-            <*> postingAcct            <* skipWhite-            <*> optional tags          <* skipWhite-            <*> optional entry         <* skipWhite-            <*  satisfy T.newline      <* skipWhite-            <*> optional postingMemo   <* skipWhite-  where-    f li mayFnp ac ta mayEn me =-      U.Posting pa nu fl ac tgs mayEn me pl Nothing Nothing-      where-        tgs = fromMaybe (L.Tags []) ta-        pl = Just . L.PostingLine $ li-        (fl, nu, pa) = fromMaybe (Nothing, Nothing, Nothing) mayFnp--transaction :: Parser L.Transaction-transaction = p >>= Ex.switch (fail . show) return-  where-    p = L.transaction <$>-        (L.Family <$> topLine <*> posting-        <*> posting <*> many posting)---blankLine :: Parser Y.Item-blankLine = Y.BlankLine <$ satisfy T.newline <* skipWhite--item :: Parser Y.Item-item = fmap Y.IComment comment <|> fmap Y.PricePoint price-       <|> fmap Y.Transaction transaction <|> blankLine--ledger :: Parser Y.Ledger-ledger = Y.Ledger <$ skipWhite <*> many item <* P.eof
− Penny/Copper/Render.hs
@@ -1,523 +0,0 @@--- | Renders Penny data in a format that can be parsed by--- "Penny.Copper.Parsec". These functions render text that is--- compliant with the EBNF grammar which is at--- @doc\/ledger-grammar.org@.-module Penny.Copper.Render where--import Control.Monad (guard)-import Control.Applicative ((<$>), (<|>), (<*>))-import Data.List (intersperse, intercalate)-import Data.List.Split (chunksOf, splitOn)-import qualified Data.Text as X-import Data.Text (Text, cons, snoc)-import qualified Penny.Copper.Terminals as T-import qualified Penny.Lincoln.Transaction as LT-import qualified Data.Time as Time-import qualified Penny.Copper.Types as Y-import qualified Penny.Lincoln as L---- * Helpers---- | 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---- | 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----- * Accounts---- | Is True if a sub account can be rendered at Level 1;--- False otherwise.-isSubAcctLvl1 :: L.SubAccount -> Bool-isSubAcctLvl1 (L.SubAccount x) =-  X.all T.lvl1AcctChar x && not (X.null x)--isAcctLvl1 :: L.Account -> Bool-isAcctLvl1 (L.Account ls) =-  (not . null $ ls)-  && (all isSubAcctLvl1 ls)--quotedLvl1Acct :: L.Account -> Maybe Text-quotedLvl1Acct a@(L.Account ls) = do-  guard (isAcctLvl1 a)-  let txt = X.concat . intersperse (X.singleton ':')-            . map L.unSubAccount $ ls-  return $ '{' `X.cons` txt `X.snoc` '}'--isFirstSubAcctLvl2 :: L.SubAccount -> Bool-isFirstSubAcctLvl2 (L.SubAccount x) = case X.uncons x of-  Nothing -> False-  Just (c, r) -> T.letter c && (X.all T.lvl2AcctOtherChar r)--isOtherSubAcctLvl2 :: L.SubAccount -> Bool-isOtherSubAcctLvl2 (L.SubAccount x) =-  (not . X.null $ x)-  && (X.all T.lvl2AcctOtherChar x)--isAcctLvl2 :: L.Account -> Bool-isAcctLvl2 (L.Account ls) = case ls of-  [] -> False-  x:xs -> isFirstSubAcctLvl2 x && all isOtherSubAcctLvl2 xs--lvl2Acct :: L.Account -> Maybe Text-lvl2Acct a@(L.Account ls) = do-  guard $ isAcctLvl2 a-  return . X.concat . intersperse (X.singleton ':')-         . map L.unSubAccount $ ls---- | 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.-ledgerAcct :: L.Account -> Maybe Text-ledgerAcct a = lvl2Acct a <|> quotedLvl1Acct a---- * Commodities---- | Render a quoted Level 1 commodity. Fails if any character does--- not satisfy lvl1Char.-quotedLvl1Cmdty :: L.Commodity -> Maybe Text-quotedLvl1Cmdty (L.Commodity c) =-  if X.all T.lvl1CmdtyChar c-  then Just $ '"' `cons` c `snoc` '"'-  else Nothing----- | 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.-lvl2Cmdty :: L.Commodity -> Maybe Text-lvl2Cmdty (L.Commodity c) = do-  (f, rs) <- X.uncons c-  guard $ T.lvl2CmdtyFirstChar f-  guard . X.all T.lvl2CmdtyOtherChar $ rs-  return c----- | Render a Level 3 commodity. Fails if any character is not a--- letter or a symbol.-lvl3Cmdty :: L.Commodity -> Maybe Text-lvl3Cmdty (L.Commodity c) =-  if (not . X.null $ c) && (X.all T.lvl3CmdtyChar c)-  then return c-  else Nothing----- * Quantities---- | Specifies how to perform digit grouping when rendering a--- quantity. All grouping groups into groups of 3 digits.-data GroupSpec =-  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)---data GroupSpecs = GroupSpecs-  { left :: GroupSpec-  , right :: GroupSpec-  } deriving Show---grouper :: String-grouper = "\x2009"--radix :: String-radix = "."---- | Performs grouping for amounts to the left of the radix point.-groupWhole :: GroupSpec -> String -> String-groupWhole gs o = let-  grouped = intercalate grouper-            . reverse-            . map reverse-            . chunksOf 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 :: GroupSpec -> String -> String-groupDecimal gs o = let-  grouped = intercalate grouper-            . chunksOf 3-            $ o-  in case gs of-    NoGrouping -> o-    GroupLarge -> if length o > 4 then grouped else o-    GroupAll -> grouped---- | Renders an unquoted Qty. Performs digit grouping as requested.-quantity-  :: GroupSpecs-  -- ^ Group for the portion to the left and right of the radix point?--  -> L.Qty-  -> X.Text-quantity gs q =-  let qs = show q-  in X.pack $ case splitOn "." qs of-    w:[] -> groupWhole (left gs) w-    w:d:[] ->-      groupWhole (left gs) w ++ radix ++ groupDecimal (right gs) d-    _ -> error "Qty.hs: rendering error"---- * Amounts---- | Render an Amount. The Format is required so that the commodity--- can be displayed in the right place.-amount ::-  GroupSpecs-  -> L.Amount-  -> Maybe X.Text-amount gs (L.Amount qt c maySd maySb) =-  let q = quantity gs qt-  in do-    sd <- maySd-    sb <- maySb-    let ws = case sb of-          L.SpaceBetween -> X.singleton ' '-          L.NoSpaceBetween -> X.empty-    (l, r) <- case sd of-          L.CommodityOnLeft -> do-            cx <- lvl3Cmdty c <|> quotedLvl1Cmdty c-            return (cx, q)-          L.CommodityOnRight -> do-            cx <- lvl2Cmdty c <|> quotedLvl1Cmdty c-            return (q, cx)-    return $ X.concat [l, ws, r]---- * Comments--comment :: Y.Comment -> Maybe X.Text-comment (Y.Comment x) =-  if (not . X.all T.nonNewline $ x)-  then Nothing-  else Just $ '#' `cons` x `snoc` '\n'---- * DateTime---- | Render a DateTime. The day is always printed. If the time zone--- offset is not zero, then the time and time zone offset are both--- printed. If the time zone offset is zero, then the hours and--- minutes are printed, but only if the time is not midnight. If the--- seconds are not zero, they are also printed.--dateTime :: L.DateTime -> X.Text-dateTime (L.DateTime d h m s z) = X.append xd xr-  where-    (iYr, iMo, iDy) = Time.toGregorian d-    xr = hoursMinsSecsZone h m s z-    dash = X.singleton '-'-    xd = X.concat [ showX iYr, dash, pad2 . showX $ iMo, dash,-                    pad2 . showX $ iDy ]--pad2 :: X.Text -> X.Text-pad2 = X.justifyRight 2 '0'--pad4 :: X.Text -> X.Text-pad4 = X.justifyRight 4 '0'--showX :: Show a => a -> X.Text-showX = X.pack . show--hoursMinsSecsZone-  :: L.Hours -> L.Minutes -> L.Seconds -> L.TimeZoneOffset -> X.Text-hoursMinsSecsZone h m s z =-  if z == L.noOffset && (h, m, s) == L.midnight-  then X.empty-  else let xhms = X.concat [xh, colon, xm, xs]-           xh = pad2 . showX . L.unHours $ h-           xm = pad2 . showX . L.unMinutes $ m-           xs = let secs = L.unSeconds s-                in if secs == 0-                   then X.empty-                   else ':' `X.cons` (pad2 . showX $ secs)-           off = L.offsetToMins z-           sign = X.singleton $ if off < 0 then '-' else '+'-           padded = pad4 . showX . abs $ off-           xz = if off == 0-                then X.empty-                else ' ' `X.cons` sign `X.append` padded-           colon = X.singleton ':'-       in ' ' `X.cons` xhms `X.append` xz---- * Entries--entry-  :: GroupSpecs-  -> L.Entry-  -> Maybe X.Text-entry gs (L.Entry dc a) = do-  amt <- amount gs a-  let dcTxt = X.pack $ case dc of-        L.Debit -> "<"-        L.Credit -> ">"-  return $ X.append (X.snoc dcTxt ' ') amt---- * Flags--flag :: L.Flag -> Maybe X.Text-flag (L.Flag fl) =-  if X.all T.flagChar fl-  then Just $ '[' `cons` fl `snoc` ']'-  else Nothing---- * Memos---- | Renders a postingMemoLine, optionally with trailing--- whitespace. The trailing whitespace allows the next line to be--- indented properly if is also a postingMemoLine. This is handled--- using trailing whitespace rather than leading whitespace because--- leading whitespace is inconsistent with the grammar.-postingMemoLine-  :: Int-  -- ^ Pad the end of the output with this many spaces-  -> X.Text-  -> Maybe X.Text-postingMemoLine p x =-  if X.all T.nonNewline x-  then let trailing = X.replicate p (X.singleton ' ')-           ls = [X.singleton '\'', x, X.singleton '\n', trailing]-        in Just $ X.concat ls-  else Nothing---- | Renders a postingMemo. Fails if the postingMemo is empty, as the--- grammar requires that they have at least one line.------ If the boolean is True, inserts padding after the last--- postingMemoLine so that the next line is indented by four--- columns. Use this if the posting memo is followed by another--- posting. If the last boolean if False, there is no indenting after--- the last postingMemoLine.-postingMemo :: Bool -> L.Memo -> Maybe X.Text-postingMemo iLast (L.Memo ls) =-  if null ls-  then Nothing-  else let bs = replicate (length ls - 1) 8 ++ [if iLast then 4 else 0]-       in fmap X.concat . sequence $ zipWith postingMemoLine bs ls---transactionMemoLine :: X.Text -> Maybe X.Text-transactionMemoLine x =-  if X.all T.nonNewline x-  then Just $ ';' `cons` x `snoc` '\n'-  else Nothing--transactionMemo :: L.Memo -> Maybe X.Text-transactionMemo (L.Memo ls) =-  if null ls-  then Nothing-  else fmap X.concat . mapM transactionMemoLine $ ls---- * Numbers--number :: L.Number -> Maybe Text-number (L.Number t) =-  if X.all T.numberChar t-  then Just $ '(' `cons` t `snoc` ')'-  else Nothing---- * Payees--quotedLvl1Payee :: L.Payee -> Maybe Text-quotedLvl1Payee (L.Payee p) = do-  guard (X.all T.quotedPayeeChar p)-  return $ '~' `X.cons` p `X.snoc` '~'--lvl2Payee :: L.Payee -> Maybe Text-lvl2Payee (L.Payee p) = do-  (c1, cs) <- X.uncons p-  guard (T.letter c1)-  guard (X.all T.nonNewline cs)-  return p--payee :: L.Payee -> Maybe Text-payee p = lvl2Payee p <|> quotedLvl1Payee p---- * Prices--price ::-  GroupSpecs-  -> L.PricePoint-  -> Maybe X.Text-price gs pp = let-  dateTxt = dateTime (L.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 = lvl3Cmdty from <|> quotedLvl1Cmdty from-  in do-    amtTxt <- amount gs-              (L.Amount q to (L.ppSide pp) (L.ppSpaceBetween pp))-    fromTxt <- mayFromTxt-    return $-       (X.intercalate (X.singleton ' ')-       [X.singleton '@', dateTxt, fromTxt, amtTxt])-       `snoc` '\n'---- * Tags--tag :: L.Tag -> Maybe X.Text-tag (L.Tag t) =-  if X.all T.tagChar t-  then Just $ X.cons '*' t-  else Nothing--tags :: L.Tags -> Maybe X.Text-tags (L.Tags ts) =-  X.intercalate (X.singleton ' ')-  <$> mapM tag ts---- * TopLine---- | Renders the TopLine. Emits trailing whitespace after the newline--- so that the first posting is properly indented.-topLine :: LT.TopLine -> Maybe X.Text-topLine tl =-  f-  <$> renMaybe (LT.tMemo tl) transactionMemo-  <*> renMaybe (LT.tFlag tl) flag-  <*> renMaybe (LT.tNumber tl) number-  <*> renMaybe (LT.tPayee tl) payee-  where-    f meX flX nuX paX =-      X.concat [ meX, txtWords [dtX, flX, nuX, paX],-                 X.singleton '\n',-                 X.replicate 4 (X.singleton ' ') ]-    dtX = dateTime (LT.tDateTime tl)---- * Posting---- | 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).------ This table is a bit of a lie, because the blank spaces for--- indentation are emitted either by the posting previous to this one--- (either after the posting itself or after its postingMemo) or by--- the TopLine.------ Also emits an additional eight spaces after the trailing newline if--- the posting has a memo. That way the memo will be indented--- properly. (There are trailing spaces here, as opposed to leading--- spaces in the posting memo, because the latter would be--- inconsistent with the grammar.)------ Emits an extra four spaces after the first line if the first--- paramter is True. However, this is overriden if there is a memo, in--- which case eight spaces will be emitted. (This allows the next--- posting to be indented properly.)-posting ::-  GroupSpecs-  -> Bool-  -- ^ If True, emit four spaces after the trailing newline.-  -> L.Posting-  -> Maybe X.Text-posting gs pad p = do-  fl <- renMaybe (LT.pFlag p) flag-  nu <- renMaybe (LT.pNumber p) number-  pa <- renMaybe (LT.pPayee p) quotedLvl1Payee-  ac <- ledgerAcct (LT.pAccount p)-  ta <- tags (LT.pTags p)-  me <- renMaybe (LT.pMemo p) (postingMemo pad)-  mayEn <- case LT.pInferred p of-    LT.Inferred -> return Nothing-    LT.NotInferred -> return (Just . L.pEntry $ p)-  en <- renMaybe mayEn (entry gs)-  return $ formatter pad fl nu pa ac ta en me--formatter ::-  Bool      -- ^ If True, emit four trailing spaces if no memo or-            -- eight trailing spaces if there is a memo.-  -> X.Text -- ^ Flag-  -> X.Text -- ^ Number-  -> X.Text -- ^ Payee-  -> X.Text -- ^ Account-  -> X.Text -- ^ Tags-  -> X.Text -- ^ Entry-  -> X.Text -- ^ Memo-  -> X.Text-formatter pad fl nu pa ac ta en me = let-  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 = '\n' `X.cons` trailingWhite-  trailingWhite = case (X.null me, pad) of-    (True, False) -> X.empty-    (True, True) -> X.replicate 4 (X.singleton ' ')-    (False, _) -> X.replicate 8 (X.singleton ' ')-  in X.concat [colB, colC, colD, rtn, me]----- * Transaction--transaction ::-  GroupSpecs-  -> L.Transaction-  -> Maybe X.Text-transaction gs txn = do-  let (L.Family tl p1 p2 ps) = LT.unTransaction txn-  tlX <- topLine tl-  p1X <- posting gs True p1-  p2X <- posting gs (not . null $ ps) p2-  psX <- if null ps-         then return X.empty-         else let bs = replicate (length ps - 1) True ++ [False]-              in fmap X.concat . sequence-                 $ zipWith (posting gs) bs ps-  return $ X.concat [tlX, p1X, p2X, psX]---- * Item--item :: GroupSpecs -> Y.Item -> Maybe X.Text-item gs i = case i of-  Y.BlankLine -> Just . X.singleton $ '\n'-  Y.IComment x -> comment x-  Y.PricePoint pp -> price gs pp-  Y.Transaction t -> transaction gs t---- * Ledger--ledger :: GroupSpecs -> Y.Ledger -> Maybe X.Text-ledger gs (Y.Ledger is) = fmap X.concat . mapM (item gs) $ is
− Penny/Copper/Terminals.hs
@@ -1,145 +0,0 @@-module Penny.Copper.Terminals where--invalid :: Char -> Bool-invalid c = c >= '\xD800' && c <= '\xDFFF'--unicode :: Char -> Bool-unicode = not . invalid--newline :: Char -> Bool-newline = (== '\x0A')--space :: Char -> Bool-space = (== '\x20')--tab :: Char -> Bool-tab = (== '\x09')--white :: Char -> Bool-white c = space c || tab c--nonNewline :: Char -> Bool-nonNewline c = unicode c && (not . newline $ c)--nonNewlineNonSpace :: Char -> Bool-nonNewlineNonSpace c = nonNewline c && (not . white $ c)--upperCaseAscii :: Char -> Bool-upperCaseAscii c = c >= 'A' && c <= 'Z'--lowerCaseAscii :: Char -> Bool-lowerCaseAscii c = c >= 'a' && c <= 'z'--digit :: Char -> Bool-digit c = c >= '0' && c <= '9'--nonAscii :: Char -> Bool-nonAscii c = nonNewline c && c > '\x7F'--letter :: Char -> Bool-letter c = upperCaseAscii c || lowerCaseAscii c || nonAscii c--dollar :: Char -> Bool-dollar = (== '$')--colon :: Char -> Bool-colon = (== ':')--openCurly :: Char -> Bool-openCurly = (== '{')--closeCurly :: Char -> Bool-closeCurly = (== '}')--openSquare :: Char -> Bool-openSquare = (== '[')--closeSquare :: Char -> Bool-closeSquare = (== ']')--doubleQuote :: Char -> Bool-doubleQuote = (== '"')--period :: Char -> Bool-period = (== '.')--hash :: Char -> Bool-hash = (== '#')--thinSpace :: Char -> Bool-thinSpace = (== '\x2009')--dateSep :: Char -> Bool-dateSep c = c == '/' || c == '-'--plus :: Char -> Bool-plus = (== '+')--minus :: Char -> Bool-minus = (== '-')--lessThan :: Char -> Bool-lessThan = (== '<')--greaterThan :: Char -> Bool-greaterThan = (== '>')--openParen :: Char -> Bool-openParen = (== '(')--closeParen :: Char -> Bool-closeParen = (== ')')--semicolon :: Char -> Bool-semicolon = (== ';')--apostrophe :: Char -> Bool-apostrophe = (== '\x27')--tilde :: Char -> Bool-tilde = (== '~')--underscore :: Char -> Bool-underscore = (== '_')--asterisk :: Char -> Bool-asterisk = (== '*')--lvl1AcctChar :: Char -> Bool-lvl1AcctChar c = nonNewline c && (not . closeCurly $ c)-                 && (not . colon $ c)--lvl2AcctOtherChar :: Char -> Bool-lvl2AcctOtherChar c =-  nonNewline c && (not . white $ c) && (not . colon $ c)-  && (not . asterisk $ c) && (not . greaterThan $ c)-  && (not . lessThan $ c)--lvl1CmdtyChar :: Char -> Bool-lvl1CmdtyChar c =-  nonNewline c && (not . doubleQuote $ c)--lvl2CmdtyFirstChar :: Char -> Bool-lvl2CmdtyFirstChar c = letter c || dollar c--lvl2CmdtyOtherChar :: Char -> Bool-lvl2CmdtyOtherChar c = nonNewline c && (not . white $ c)--lvl3CmdtyChar :: Char -> Bool-lvl3CmdtyChar c = letter c || dollar c--flagChar :: Char -> Bool-flagChar c = nonNewline c && (not . closeSquare $ c)--numberChar :: Char -> Bool-numberChar c = nonNewline c && (not . closeParen $ c)--quotedPayeeChar :: Char -> Bool-quotedPayeeChar c = nonNewline c && (not . tilde $ c)--tagChar :: Char -> Bool-tagChar c = nonNewlineNonSpace c && (not . asterisk $ c)-  && (not . greaterThan $ c) && (not . lessThan $ c)--atSign :: Char -> Bool-atSign = (== '@')
− Penny/Copper/Types.hs
@@ -1,59 +0,0 @@-module Penny.Copper.Types where--import Control.Applicative (Applicative (pure))-import Data.Functor ((<$>))-import qualified Data.Text as X-import qualified Data.Traversable as T-import qualified Penny.Lincoln as L-import qualified Data.Monoid as M--newtype Comment = Comment { unComment :: X.Text }-  deriving (Eq, Show)--data Item = BlankLine-          | IComment Comment-          | PricePoint L.PricePoint-          | Transaction L.Transaction-          deriving Show--mapItem-  :: (Comment -> Comment)-  -> (L.PricePoint -> L.PricePoint)-  -> (L.Transaction -> L.Transaction)-  -> Item-  -> Item-mapItem fc fp ft i = case i of-  BlankLine -> BlankLine-  IComment c -> IComment $ fc c-  PricePoint p -> PricePoint $ fp p-  Transaction t -> Transaction $ ft t--mapItemA-  :: Applicative a-  => (Comment -> a Comment)-  -> (L.PricePoint -> a L.PricePoint)-  -> (L.Transaction -> a L.Transaction)-  -> Item-  -> a Item-mapItemA fc fp ft i = case i of-  BlankLine -> pure BlankLine-  IComment c -> IComment <$> fc c-  PricePoint p -> PricePoint <$> fp p-  Transaction t -> Transaction <$> ft t--newtype Ledger = Ledger { unLedger :: [Item] }-        deriving Show--mapLedger :: (Item -> Item) -> Ledger -> Ledger-mapLedger f (Ledger is) = Ledger $ map f is--mapLedgerA-  :: Applicative a-  => (Item -> a Item)-  -> Ledger-  -> a Ledger-mapLedgerA f (Ledger is) = Ledger <$> T.traverse f is--instance M.Monoid Ledger where-  mempty = Ledger []-  mappend (Ledger x) (Ledger y) = Ledger (x ++ y)
− Penny/Liberty.hs
@@ -1,788 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | 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 (-  MatcherFactory,-  FilteredNum(FilteredNum, unFilteredNum),-  SortedNum(SortedNum, unSortedNum),-  LibertyMeta(filteredNum, sortedNum),-  xactionsToFiltered,-  ListLength(ListLength, unListLength),-  ItemIndex(ItemIndex, unItemIndex),-  PostFilterFn,-  parseComparer,-  processPostFilters,-  parsePredicate,-  parseInt,-  parseInfix,-  parseRPN,-  exprDesc,-  showExpression,-  verboseFilter,--  -- * Parsers-  Operand,-  operandSpecs,-  postFilterSpecs,-  matcherSelectSpecs,-  caseSelectSpecs,-  operatorSpecs,--  -- * Version-  version,--  -- * Errors-  Error--  ) where--import Control.Applicative ((<*>), (<$>), pure, Applicative)-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.Char (toUpper)-import Data.Maybe (mapMaybe)-import Data.Monoid ((<>))-import Data.List (sortBy)-import Data.Text (Text, pack)-import qualified Data.Time as Time-import qualified System.Console.MultiArg as MA-import qualified System.Console.MultiArg.Combinator as C-import System.Console.MultiArg.Combinator (OptSpec)-import Text.Parsec (parse)--import qualified Penny.Copper.Parsec as Pc--import Penny.Lincoln.Family.Child (Child(Child), child, parent)-import qualified Penny.Lincoln.Predicates as P-import qualified Penny.Lincoln.Predicates.Siblings as PS-import qualified Data.Prednote.Pdct as E-import qualified Penny.Lincoln as L-import qualified System.Console.Rainbow as C-import qualified Data.Prednote.Expressions as X--import Text.Matchers (-  CaseSensitive(Sensitive, Insensitive))-import qualified Text.Matchers as TM--import qualified Paths_penny_lib as PPL-import qualified Data.Version as V-import qualified System.Exit as Exit---- | A multiline Text that holds an error message.-type Error = Text---- | 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 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.ExprDesc-  -> [X.Token a]-  -> Ex.Exceptional Error (E.Pdct a)-parsePredicate d ls = case ls of-  [] -> return E.always-  _ -> X.parseExpression d ls---- | 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. Also returns a Text--- containing a description of the evalutation process.-xactionsToFiltered ::--  P.LPdct-  -- ^ 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)--  -> ([C.Chunk], [L.Box LibertyMeta])-  -- ^ Sorted, filtered postings--xactionsToFiltered pdct postFilts s txns =-  let pdcts = map (makeLabeledPdct pdct) pfs-      evaluator subj pd = E.evaluate indentAmt True subj 0 pd-      pairMaybes = zipWith evaluator pfs pdcts-      pairs = mapMaybe rmMaybe pairMaybes-      rmMaybe (mayB, x) = case mayB of-        Nothing -> Nothing-        Just b -> Just (b, x)-      pfs = concatMap L.postFam txns-      txt = concatMap snd pairs-      filtered = map snd . filter fst $ zipWith zipper pairs pfs-      zipper (bool, _) pf = (bool, pf)-      resultLs = addSortedNum-                 . processPostFilters postFilts-                 . sortBy (sorter s)-                 . addFilteredNum-                 . map toBox-                 $ filtered-  in (txt, resultLs)----- | Creates a Pdct and prepends a one-line description of the PostFam--- to the Pdct's label so it can be easily identified in the output.-makeLabeledPdct :: E.Pdct L.PostFam -> L.PostFam -> E.Pdct L.PostFam-makeLabeledPdct pd pf = E.rename f pd-  where-    f old = old <> " - " <> L.display pf--indentAmt :: E.IndentAmt-indentAmt = 4---- | 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 TM.Matcher--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..]------------------------------------------------------------------ Operands----------------------------------------------------------------- | Given a String from the command line which represents a pattern,--- the current case sensitivity, and a MatcherFactory, 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 TM.Matcher--getMatcher s cs f-  = Ex.mapException mkError-  $ f cs (pack s)-  where-    mkError eMsg = "bad pattern: \"" <> pack s <> " - " <> eMsg-      <> "\n"----- | Parses comparers given on command line to a function. Fails if--- the string given is invalid.-parseComparer-  :: String-  -> (Ordering -> E.Pdct a)-  -> Ex.Exceptional Error (E.Pdct a)-parseComparer s f = Ex.fromMaybe ("bad comparer: " <> pack s <> "\n")-                  $ E.parseComparer (pack s) f---- | Parses a date from the command line. On failure, throws back the--- error message from the failed parse.-parseDate :: String -> Ex.Exceptional Error Time.UTCTime-parseDate arg =-  Ex.mapExceptional err L.toUTC-  . Ex.fromEither-  . parse Pc.dateTime ""-  . pack-  $ arg-  where-    err msg = "bad date: \"" <> pack arg <> "\" - " <> (pack . show $ msg)--type Operand = E.Pdct L.PostFam---- | OptSpec for a date.-date :: OptSpec (Ex.Exceptional Error Operand)-date = C.OptSpec ["date"] ['d'] (C.TwoArg f)-  where-    f a1 a2 = do-      utct <- parseDate a2-      parseComparer a1 (flip P.date utct)---current :: L.DateTime -> OptSpec Operand-current dt = C.OptSpec ["current"] [] (C.NoArg f)-  where-    f = E.or [P.date LT (L.toUTC dt), P.date EQ (L.toUTC dt)]---- | Parses exactly one integer; fails if it cannot read exactly one.-parseInt :: String -> Ex.Exceptional Error Int-parseInt t =-  case reads t of-    ((i, ""):[]) -> return i-    _ -> Ex.throw $ "could not parse integer: \"" <> pack t <> "\"\n"----- | 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--  -> (TM.Matcher -> P.LPdct)-  -- ^ When applied to a Matcher, this function returns a predicate.--  -> OptSpec ( CaseSensitive-               -> MatcherFactory-               -> Ex.Exceptional Error Operand )-patternOption str mc f = C.OptSpec [str] so (C.OneArg g)-  where-    so = maybe [] (:[]) mc-    g a1 cs fty = f <$> getMatcher a1 cs fty----- | The account option; matches if the pattern given matches the--- colon-separated account name.-account :: OptSpec ( CaseSensitive-                   -> MatcherFactory-                   -> Ex.Exceptional Error Operand )-account = C.OptSpec ["account"] "a" (C.OneArg f)-  where-    f a1 cs fty-      = fmap P.account-      $ getMatcher a1 cs fty----- | The account-level option; matches if the account at the given--- level matches.-accountLevel :: OptSpec ( CaseSensitive-                        -> MatcherFactory-                        -> Ex.Exceptional Error Operand)-accountLevel = C.OptSpec ["account-level"] "" (C.TwoArg f)-  where-    f a1 a2 cs fty-      = P.accountLevel <$> parseInt a1 <*> getMatcher a2 cs fty----- | The accountAny option; returns True if the matcher given matches--- a single sub-account name at any level.-accountAny :: OptSpec ( CaseSensitive-                        -> MatcherFactory-                        -> Ex.Exceptional Error Operand )-accountAny = patternOption "account-any" Nothing P.accountAny---- | The payee option; returns True if the matcher matches the payee--- name.-payee :: OptSpec ( CaseSensitive-                 -> MatcherFactory-                 -> Ex.Exceptional Error Operand )-payee = patternOption "payee" (Just 'p') P.payee--tag :: OptSpec ( CaseSensitive-                 -> MatcherFactory-                 -> Ex.Exceptional Error Operand)-tag = patternOption "tag" (Just 't') P.tag--number :: OptSpec ( CaseSensitive-                    -> MatcherFactory-                    -> Ex.Exceptional Error Operand )-number = patternOption "number" (Just 'n') P.number--flag :: OptSpec ( CaseSensitive-                  -> MatcherFactory-                  -> Ex.Exceptional Error Operand)-flag = patternOption "flag" (Just 'f') P.flag--commodity :: OptSpec ( CaseSensitive-                       -> MatcherFactory-                       -> Ex.Exceptional Error Operand)-commodity = patternOption "commodity" (Just 'y') P.commodity--filename :: OptSpec ( CaseSensitive-                      -> MatcherFactory-                      -> Ex.Exceptional Error Operand )-filename = patternOption "filename" Nothing P.filename--postingMemo :: OptSpec ( CaseSensitive-                         -> MatcherFactory-                         -> Ex.Exceptional Error Operand)-postingMemo = patternOption "posting-memo" Nothing P.postingMemo--transactionMemo :: OptSpec ( CaseSensitive-                             -> MatcherFactory-                             -> Ex.Exceptional Error Operand)-transactionMemo = patternOption "transaction-memo"-                  Nothing P.transactionMemo--debit :: OptSpec Operand-debit = C.OptSpec ["debit"] [] (C.NoArg P.debit)--credit :: OptSpec Operand-credit = C.OptSpec ["credit"] [] (C.NoArg P.credit)--qtyOption :: OptSpec (Ex.Exceptional Error Operand)-qtyOption = C.OptSpec ["qty"] "q" (C.TwoArg f)-  where-    f a1 a2 = do-      qt <- parseQty a2-      parseComparer a1 (flip P.qty qt)-    parseQty a = case parse Pc.quantity "" (pack a) of-      Left e -> Ex.throw $ "could not parse quantity: "-        <> pack a <> " - "-        <> (pack . show $ e)-      Right g -> pure g----- | 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@--  -> ( OptSpec (Ex.Exceptional Error Operand)-     , OptSpec (Ex.Exceptional Error Operand) )-  -- ^ Parses both descending and ascending serial options.--serialOption getSerial n = (osA, osD)-  where-    osA = C.OptSpec [n] []-          (C.TwoArg (f n L.forward))-    osD = let name = addPrefix "rev" n-          in C.OptSpec [name] []-             (C.TwoArg (f name L.backward))-    f name getInt a1 a2 = do-      num <- parseInt a2-      let getPdct = E.compareByMaybe (pack . show $ num) (pack name) cmp-          cmp l = case getSerial l of-            Nothing -> Nothing-            Just ser -> Just $ compare (getInt ser) num-      parseComparer a1 getPdct----- | Creates two options suitable for comparison of sibling serial--- numbers. Similar to 'serialOption'.-siblingSerialOption--  :: (L.Posting -> Maybe L.Serial)-  -- ^ Function that, when applied to a PostFam, returns the serial-  -- you are interested in.--  -> String-  -- ^ Name of the command line option, such as @global-posting@--  -> ( OptSpec (Ex.Exceptional Error Operand)-     , OptSpec (Ex.Exceptional Error Operand) )-  -- ^ Parses both descending and ascending serial options.--siblingSerialOption getSerial n = (osA, osD)-  where-    osA = C.OptSpec ["s-" ++ n] []-          (C.TwoArg (f n L.forward))-    osD = let name = addPrefix "rev" n-          in C.OptSpec ["s-" ++ name] []-             (C.TwoArg (f name L.backward))-    f name getInt a1 a2 = do-      num <- parseInt a2-      let getPdct ord = E.operand desc fn-            where-              desc = "any sibling serial " <> pack name-                     <> " is " <> descCmp ord-                     <> " " <> (pack . show $ num)-              fn = any doCmp . getSiblings . L.unPostFam-              doCmp p = case getSerial p of-                Nothing -> False-                Just ser -> compare (getInt ser) num == ord-      parseComparer a1 getPdct---getSiblings :: Child p c -> [c]-getSiblings (Child _ s1 ss _) = s1:ss--descCmp :: Ordering -> Text-descCmp o = case o of-  LT -> "less than"-  EQ -> "equal to"-  GT -> "greater than"---- | 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 :: ( OptSpec (Ex.Exceptional Error Operand)-                     , OptSpec (Ex.Exceptional Error Operand) )-globalTransaction =-  let f = fmap L.unGlobalTransaction-          . L.tGlobalTransaction-          . parent-          . L.unPostFam-  in serialOption f "globalTransaction"--globalPosting :: ( OptSpec (Ex.Exceptional Error Operand)-                 , OptSpec (Ex.Exceptional Error Operand) )-globalPosting =-  let f = fmap L.unGlobalPosting-          . L.pGlobalPosting-          . child-          . L.unPostFam-  in serialOption f "globalPosting"--filePosting :: ( OptSpec (Ex.Exceptional Error Operand)-               , OptSpec (Ex.Exceptional Error Operand) )-filePosting =-  let f = fmap L.unFilePosting-          . L.pFilePosting-          . child-          . L.unPostFam-  in serialOption f "filePosting"--fileTransaction :: ( OptSpec (Ex.Exceptional Error Operand)-                   , OptSpec (Ex.Exceptional Error Operand) )-fileTransaction =-  let f = fmap L.unFileTransaction-          . L.tFileTransaction-          . parent-          . L.unPostFam-  in serialOption f "fileTransaction"---- | All operand OptSpec.-operandSpecs-  :: L.DateTime-  -> [OptSpec (CaseSensitive-               -> MatcherFactory-               -> Ex.Exceptional Error Operand)]--operandSpecs dt =-  [ fmap (const . const) date-  , fmap (const . const . pure) (current dt)-  , account-  , accountLevel-  , accountAny-  , payee-  , tag-  , number-  , flag-  , commodity-  , postingMemo-  , transactionMemo-  , filename-  , fmap (const . const . pure) debit-  , fmap (const . const . pure) credit-  , fmap (const . const) qtyOption--  , sAccount-  , sAccountLevel-  , sAccountAny-  , sPayee-  , sTag-  , sNumber-  , sFlag-  , sCommodity-  , sPostingMemo-  , fmap (const . const . pure) sDebit-  , fmap (const . const. pure) sCredit-  , fmap (const . const) sQtyOption-  ]-  ++ serialSpecs--serialSpecs :: [OptSpec (CaseSensitive-                        -> MatcherFactory-                        -> Ex.Exceptional Error Operand)]-serialSpecs-  = concat-  $ [unDouble]-  <*> [ globalTransaction, globalPosting,-        filePosting, fileTransaction,-        sGlobalPosting, sFilePosting ]--unDouble-  :: Functor f-  => (f (Ex.Exceptional Error a),-      f (Ex.Exceptional Error a ))-  -> [ f (x -> y -> Ex.Exceptional Error a) ]-unDouble (o1, o2) = [fmap (const . const) o1, fmap (const . const) o2]------------------------------------------------------------------ Post filters----------------------------------------------------------------- | The user passed a bad number for the head or tail option. The--- argument is the bad number passed.-data BadHeadTailError = BadHeadTailError Text-  deriving Show--optHead :: OptSpec (Ex.Exceptional Error PostFilterFn)-optHead = C.OptSpec ["head"] [] (C.OneArg f)-  where-    f a = do-      num <- parseInt a-      let g _ ii = ii < (ItemIndex num)-      return g--optTail :: OptSpec (Ex.Exceptional Error PostFilterFn)-optTail = C.OptSpec ["tail"] [] (C.OneArg f)-  where-    f a = do-      num <- parseInt a-      let g (ListLength len) (ItemIndex ii) = ii >= len - num-      return g---postFilterSpecs :: ( OptSpec (Ex.Exceptional Error PostFilterFn)-                   , OptSpec (Ex.Exceptional Error PostFilterFn) )-postFilterSpecs = (optHead, optTail)----------------------------------------------------------------- Matcher control---------------------------------------------------------------parseInsensitive :: OptSpec CaseSensitive-parseInsensitive =-  C.OptSpec ["case-insensitive"] ['i'] (C.NoArg Insensitive)---parseSensitive :: OptSpec CaseSensitive-parseSensitive =-  C.OptSpec ["case-sensitive"] ['I'] (C.NoArg Sensitive)---within :: OptSpec MatcherFactory-within =-  C.OptSpec ["within"] "w" . C.NoArg $ \c t ->-    return (TM.within c t)--pcre :: OptSpec MatcherFactory-pcre = C.OptSpec ["pcre"] "r" (C.NoArg TM.pcre)--posix :: OptSpec MatcherFactory-posix = C.OptSpec ["posix"] "" (C.NoArg TM.tdfa)--exact :: OptSpec MatcherFactory-exact = C.OptSpec ["exact"] "x" . C.NoArg $ \c t ->-        return (TM.exact c t)--matcherSelectSpecs :: [OptSpec MatcherFactory]-matcherSelectSpecs = [within, pcre, posix, exact]--caseSelectSpecs :: [OptSpec CaseSensitive]-caseSelectSpecs = [parseInsensitive, parseSensitive]----------------------------------------------------------------- Operators----------------------------------------------------------------- | Open parentheses-open :: OptSpec (X.Token a)-open = C.OptSpec ["open"] "(" (C.NoArg X.openParen)---- | Close parentheses-close :: OptSpec (X.Token a)-close = C.OptSpec ["close"] ")" (C.NoArg X.closeParen)---- | and operator-parseAnd :: OptSpec (X.Token a)-parseAnd = C.OptSpec ["and"] "A" (C.NoArg X.opAnd)---- | or operator-parseOr :: OptSpec (X.Token a)-parseOr = C.OptSpec ["or"] "O" (C.NoArg X.opOr)---- | not operator-parseNot :: OptSpec (X.Token a)-parseNot = C.OptSpec ["not"] "N" (C.NoArg X.opNot)--operatorSpecs :: [OptSpec (X.Token a)]-operatorSpecs =-  [open, close, parseAnd, parseOr, parseNot]---- Infix and RPN expression selectors--parseInfix :: OptSpec X.ExprDesc-parseInfix = C.OptSpec ["infix"] "" (C.NoArg X.Infix)--parseRPN :: OptSpec X.ExprDesc-parseRPN = C.OptSpec ["rpn"] "" (C.NoArg X.RPN)---- | Both Infix and RPN options.-exprDesc :: [OptSpec X.ExprDesc]-exprDesc = [ parseInfix, parseRPN ]--showExpression :: OptSpec ()-showExpression = C.OptSpec ["show-expression"] "" (C.NoArg ())--verboseFilter :: OptSpec ()-verboseFilter = C.OptSpec ["verbose-filter"] "" (C.NoArg ())------- Siblings-----sGlobalPosting :: ( OptSpec (Ex.Exceptional Error Operand)-                  , OptSpec (Ex.Exceptional Error Operand) )-sGlobalPosting =-  siblingSerialOption (fmap (fmap L.unGlobalPosting) L.pGlobalPosting)-                      "globalPosting"--sFilePosting :: ( OptSpec (Ex.Exceptional Error Operand)-                  , OptSpec (Ex.Exceptional Error Operand) )-sFilePosting =-  siblingSerialOption (fmap (fmap L.unFilePosting) L.pFilePosting)-                      "filePosting"--sAccount :: OptSpec ( CaseSensitive-                    -> MatcherFactory-                    -> Ex.Exceptional Error Operand )-sAccount = C.OptSpec ["s-account"] "" (C.OneArg f)-  where-    f a1 cs fty = fmap PS.account-                  $ getMatcher a1 cs fty--sAccountLevel :: OptSpec ( CaseSensitive-                         -> MatcherFactory-                         -> Ex.Exceptional Error Operand )-sAccountLevel = C.OptSpec ["s-account-level"] "" (C.TwoArg f)-  where-    f a1 a2 cs fty-      = PS.accountLevel <$> parseInt a1 <*> getMatcher a2 cs fty--sAccountAny :: OptSpec ( CaseSensitive-                        -> MatcherFactory-                        -> Ex.Exceptional Error Operand )-sAccountAny = patternOption "s-account-any" Nothing PS.accountAny---- | The payee option; returns True if the matcher matches the payee--- name.-sPayee :: OptSpec ( CaseSensitive-                 -> MatcherFactory-                 -> Ex.Exceptional Error Operand )-sPayee = patternOption "s-payee" (Just 'p') PS.payee--sTag :: OptSpec ( CaseSensitive-                 -> MatcherFactory-                 -> Ex.Exceptional Error Operand)-sTag = patternOption "s-tag" (Just 't') PS.tag--sNumber :: OptSpec ( CaseSensitive-                    -> MatcherFactory-                    -> Ex.Exceptional Error Operand )-sNumber = patternOption "s-number" Nothing PS.number--sFlag :: OptSpec ( CaseSensitive-                  -> MatcherFactory-                  -> Ex.Exceptional Error Operand)-sFlag = patternOption "s-flag" Nothing PS.flag--sCommodity :: OptSpec ( CaseSensitive-                       -> MatcherFactory-                       -> Ex.Exceptional Error Operand)-sCommodity = patternOption "s-commodity" Nothing PS.commodity--sPostingMemo :: OptSpec ( CaseSensitive-                         -> MatcherFactory-                         -> Ex.Exceptional Error Operand)-sPostingMemo = patternOption "s-posting-memo" Nothing PS.postingMemo--sDebit :: OptSpec Operand-sDebit = C.OptSpec ["s-debit"] [] (C.NoArg PS.debit)--sCredit :: OptSpec Operand-sCredit = C.OptSpec ["s-credit"] [] (C.NoArg PS.credit)--sQtyOption :: OptSpec (Ex.Exceptional Error Operand)-sQtyOption = C.OptSpec ["s-qty"] [] (C.TwoArg f)-  where-    f a1 a2 = do-      qt <- parseQty a2-      parseComparer a1 (flip PS.qty qt)-    parseQty a = case parse Pc.quantity "" (pack a) of-      Left e -> Ex.throw $ "could not parse quantity: "-        <> pack a <> " - "-        <> (pack . show $ e)-      Right g -> pure g------- Versions------- | Parses the @--version@ option and returns an IO action that--- prints it and exits successfully. You supply the version of the--- executable, as there is no easy way to get that automatically.--version-  :: V.Version-  -- ^ Version of binary-  -> OptSpec (IO a)-version v = C.OptSpec ["version"] [] (C.NoArg f)-  where-    f = do-      pn <- MA.getProgName-      putStrLn $ pn ++ " version " ++ V.showVersion v-      putStrLn $ "using version " ++ V.showVersion PPL.version-                 ++ " of penny-lib"-      Exit.exitSuccess
− Penny/Lincoln.hs
@@ -1,272 +0,0 @@--- | 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.SubAccount (SubAccount, unSubAccount)-  , I.Account(Account, unAccount)--    -- ** Amounts-  , I.Amount (Amount, qty, commodity, side, spaceBetween)--    -- ** Commodities-  , I.Commodity (Commodity, unCommodity)--    -- ** DateTime-  , I.TimeZoneOffset ( offsetToMins )-  , I.minsToOffset-  , I.noOffset-  , I.Hours ( unHours )-  , I.intToHours-  , I.Minutes ( unMinutes )-  , I.intToMinutes-  , I.Seconds ( unSeconds )-  , I.intToSeconds-  , I.zeroSeconds-  , I.midnight-  , I.DateTime ( .. )-  , I.dateTimeMidnightUTC-  , I.toUTC-  , I.toZonedTime-  , I.fromZonedTime-  , I.sameInstant-  , I.showDateTime--    -- ** Debits and credits-  , I.DrCr(Debit, Credit)-  , I.opposite--    -- ** Entries-  , I.Entry (Entry, drCr, amount)--    -- ** Flag-  , I.Flag (Flag, unFlag)--    -- ** Memos-  , 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, dateTime, price, ppSide,-                   ppSpaceBetween, priceLine)--    -- ** Quantities-  , I.Qty-  , I.NumberStr(..)-  , I.toQty-  , I.mantissa-  , I.places-  , I.add-  , I.mult-  , I.difference-  , I.equivalent-  , I.newQty-  , I.Mantissa, I.Places-  , I.Difference(..)-  , I.allocate--    -- ** Tags-  , I.Tag(Tag, unTag)-  , I.Tags(Tags, unTags)---    -- * Builders-  , 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-  , F.filterChildren-  , F.find-  , F.mapChildren-  , F.mapChildrenA-  , F.mapParent-  , F.mapParentA--    -- * HasText-  , HT.HasText(text)-  , HT.HasTextList(textList)--    -- * Transactions-    -- ** Postings and transactions-  , T.Posting-  , T.Transaction-  , T.PostFam--    -- ** Making and deconstructing transactions-  , T.transaction-  , T.RTransaction(..)-  , T.rTransaction-  , T.Error ( UnbalancedError, CouldNotInferError)-  , T.toUnverified--    -- ** Querying postings-  , T.Inferred(Inferred, NotInferred)-  , T.pPayee-  , T.pNumber-  , T.pFlag-  , T.pAccount-  , T.pTags-  , T.pEntry-  , T.pMemo-  , T.pInferred-  , T.pPostingLine-  , T.pGlobalPosting-  , T.pFilePosting--    -- ** Querying transactions-  , T.TopLine-  , T.tDateTime-  , T.tFlag-  , T.tNumber-  , T.tPayee-  , T.tMemo-  , T.tTopLineLine-  , T.tTopMemoLine-  , T.tFilename-  , T.tGlobalTransaction-  , T.tFileTransaction-  , T.postFam--    -- ** Unwrapping Transactions-  , T.unTransaction-  , T.unPostFam--    -- ** Transaction boxes-  , T.Box (Box, boxMeta, boxPostFam)--    -- ** Changing transactions-  , T.TopLineChangeData(..)-  , T.emptyTopLineChangeData-  , T.PostingChangeData(..)-  , T.emptyPostingChangeData-  , T.changeTransaction--  -- * Metadata-  , I.TopLineLine(..)-  , I.TopMemoLine(..)-  , I.Side(CommodityOnLeft, CommodityOnRight)-  , I.SpaceBetween(SpaceBetween, NoSpaceBetween)-  , I.Filename(Filename, unFilename)-  , I.PriceLine(PriceLine, unPriceLine)-  , I.PostingLine(PostingLine, unPostingLine)-  , I.GlobalPosting(GlobalPosting, unGlobalPosting)-  , I.FilePosting(FilePosting, unFilePosting)-  , I.GlobalTransaction(GlobalTransaction, unGlobalTransaction)-  , I.FileTransaction(FileTransaction, unFileTransaction)--    -- * PriceDb-  , DB.PriceDb-  , DB.emptyDb-  , DB.addPrice-  , DB.getPrice-  , DB.PriceDbError(FromNotFound, ToNotFound, CpuNotFound)-  , DB.convert--    -- * Serials-  , S.Serial-  , S.forward-  , S.backward-  , S.GenSerial-  , S.incrementBack-  , S.getSerial-  , S.makeSerials-  , S.serialItems-  , S.nSerials--    -- * Matchers-  , Matchers.Factory--    -- * Showing postFam in one line-  , display--  ) 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.Matchers as Matchers-import qualified Penny.Lincoln.PriceDb as DB-import qualified Penny.Lincoln.Serial as S-import qualified Penny.Lincoln.Transaction as T--import Data.List (intersperse)-import Data.Text (Text)-import qualified Data.Text as X-import qualified Penny.Lincoln.Queries as Q-import qualified Data.Time as Time-import System.Locale (defaultTimeLocale)------- Display------- | Displays a PostFam in a one line format.------ Format:------ File LineNo Date Payee Acct DrCr Cmdty Qty-display :: T.PostFam -> Text-display p = X.pack $ concat (intersperse " " ls)-  where-    ls = [file, lineNo, dt, pye, acct, dc, cmdty, qt]-    file = maybe (labelNo "filename") (X.unpack . I.unFilename)-           (Q.filename p)-    lineNo = maybe (labelNo "line number")-             (show . I.unPostingLine) (Q.postingLine p)-    dateFormat = "%Y-%m-%d %T %z"-    dt = Time.formatTime defaultTimeLocale dateFormat-         . Time.utctDay-         . I.toUTC-         . Q.dateTime-         $ p-    pye = maybe (labelNo "payee")-            (X.unpack . HT.text) (Q.payee p)-    acct = X.unpack . X.intercalate (X.singleton ':')-           . map I.unSubAccount . I.unAccount . Q.account $ p-    dc = case Q.drCr p of-      I.Debit -> "Dr"-      I.Credit -> "Cr"-    cmdty = X.unpack . I.unCommodity . Q.commodity $ p-    qt = show . Q.qty $ p-    labelNo s = "(no " ++ s ++ ")"
− Penny/Lincoln/Balance.hs
@@ -1,109 +0,0 @@-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'.-newtype Balance = Balance (Map B.Commodity BottomLine)-                  deriving (Show, Eq)---- | Returns a map where the keys are the commodities in the balance--- and the values are the balance for each commodity. If there is no--- balance at all, this map can be 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 Nothing Nothing-        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--instance Monoid Balance where-  mempty = Balance M.empty-  mappend = addBalances---- | Removes zero balances from a Balance.-removeZeroCommodities :: Balance -> Balance-removeZeroCommodities (Balance m) =-  let p b = case b of-        Zero -> False-        _ -> True-      m' = M.filter p m-  in Balance m'
− Penny/Lincoln/Bits.hs
@@ -1,97 +0,0 @@--- | Essential data types used to make Transactions and Postings.-module Penny.Lincoln.Bits (-  -- * Accounts-  O.SubAccount(SubAccount, unSubAccount),-  O.Account(Account, unAccount),--  -- * Amounts-  O.Amount(Amount, qty, commodity, side, spaceBetween),--  -- * Commodities-  O.Commodity(Commodity, unCommodity),--  -- * DateTime-  DT.TimeZoneOffset ( offsetToMins ),-  DT.minsToOffset,-  DT.noOffset,-  DT.Hours ( unHours ),-  DT.intToHours,-  DT.zeroHours,-  DT.Minutes ( unMinutes ),-  DT.intToMinutes,-  DT.zeroMinutes,-  DT.midnight,-  DT.Seconds ( unSeconds ),-  DT.intToSeconds,-  DT.zeroSeconds,-  DT.DateTime ( .. ),-  DT.dateTimeMidnightUTC,-  DT.toUTC,-  DT.toZonedTime,-  DT.fromZonedTime,-  DT.sameInstant,-  DT.showDateTime,--  -- * Debits and Credits-  O.DrCr(Debit, Credit),-  O.opposite,--  -- * Entries-  O.Entry(Entry, drCr, amount),--  -- * Flag-  O.Flag(Flag, unFlag),--  -- * Memos-  O.Memo(Memo, unMemo),--  -- * Number-  O.Number(Number, unNumber),--  -- * Payee-  O.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,-  PricePoint ( .. ),--  -- * Quantities-  Q.Qty, Q.NumberStr(..), Q.toQty, Q.mantissa, Q.places,-  Q.add, Q.mult, Q.difference, Q.equivalent, Q.newQty,-  Q.Mantissa, Q.Places,-  Q.Difference(Q.LeftBiggerBy, Q.RightBiggerBy, Q.Equal),-  Q.allocate,--  -- * Tags-  O.Tag(Tag, unTag),-  O.Tags(Tags, unTags),--  -- * Metadata-  O.TopLineLine(..),-  O.TopMemoLine(..),-  O.Side(..),-  O.SpaceBetween(..),-  O.Filename(..),-  O.PriceLine(..),-  O.PostingLine(..),-  O.GlobalPosting(..),-  O.FilePosting(..),-  O.GlobalTransaction(..),-  O.FileTransaction(..)-  ) where---import qualified Penny.Lincoln.Bits.Open as O-import qualified Penny.Lincoln.Bits.DateTime as DT-import qualified Penny.Lincoln.Bits.Price as Pr-import qualified Penny.Lincoln.Bits.Qty as Q--data PricePoint = PricePoint { dateTime :: DT.DateTime-                             , price :: Pr.Price-                             , ppSide :: Maybe O.Side-                             , ppSpaceBetween :: Maybe O.SpaceBetween-                             , priceLine :: Maybe O.PriceLine }-                  deriving (Eq, Show)
− Penny/Lincoln/Bits/DateTime.hs
@@ -1,152 +0,0 @@-module Penny.Lincoln.Bits.DateTime-  ( TimeZoneOffset ( offsetToMins )-  , minsToOffset-  , noOffset-  , Hours ( unHours )-  , intToHours-  , zeroHours-  , Minutes ( unMinutes )-  , intToMinutes-  , zeroMinutes-  , Seconds ( unSeconds )-  , intToSeconds-  , zeroSeconds-  , midnight-  , DateTime ( .. )-  , dateTimeMidnightUTC-  , toUTC-  , toZonedTime-  , fromZonedTime-  , sameInstant-  , showDateTime-  ) 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--newtype Hours = Hours { unHours :: Int }-                deriving (Eq, Ord, Show)--newtype Minutes = Minutes { unMinutes :: Int }-                  deriving (Eq, Ord, Show)--newtype Seconds = Seconds { unSeconds :: Int }-                  deriving (Eq, Ord, Show)---- | succeeds if 0 <= x < 24-intToHours :: Int -> Maybe Hours-intToHours h =-  if h >= 0 && h < 24 then Just . Hours $ h else Nothing--zeroHours :: Hours-zeroHours = Hours 0---- | succeeds if 0 <= x < 60-intToMinutes :: Int -> Maybe Minutes-intToMinutes m =-  if m >= 0 && m < 60 then Just . Minutes $ m else Nothing--zeroMinutes :: Minutes-zeroMinutes = Minutes 0---- | succeeds if 0 <= x < 61 (to allow for leap seconds)-intToSeconds :: Int -> Maybe Seconds-intToSeconds s =-  if s >= 0 && s < 61-  then Just . Seconds $ s-  else Nothing--zeroSeconds :: Seconds-zeroSeconds = Seconds 0--midnight :: (Hours, Minutes, Seconds)-midnight = (zeroHours, zeroMinutes, zeroSeconds)---- | A DateTime is a a local date and time, along with a time zone--- offset.  The Eq and Ord instances are derived; therefore, two--- DateTime instances will not be equivalent if the time zone offsets--- are different, even if they are the same instant. To compare one--- DateTime to another, you probably want to use 'toUTC' and compare--- those. To see if two DateTime are the same instant, use--- 'sameInstant'.-data DateTime = DateTime-  { day :: T.Day-  , hours :: Hours-  , minutes :: Minutes-  , seconds :: Seconds-  , timeZone :: TimeZoneOffset-  } deriving (Eq, Ord, Show)--dateTimeMidnightUTC :: T.Day -> DateTime-dateTimeMidnightUTC d = DateTime d h m s z-  where-    (h, m, s) = midnight-    z = noOffset--toZonedTime :: DateTime -> T.ZonedTime-toZonedTime dt = T.ZonedTime lt tz-  where-    d = day dt-    lt = T.LocalTime d tod-    tod = T.TimeOfDay (unHours . hours $ dt) (unMinutes . minutes $ dt)-          (fromIntegral . unSeconds . seconds $ dt)-    tz = T.TimeZone (offsetToMins . timeZone $ dt) False ""--fromZonedTime :: T.ZonedTime -> Maybe DateTime-fromZonedTime (T.ZonedTime (T.LocalTime d tod) tz) = do-  h <- intToHours . T.todHour $ tod-  m <- intToMinutes . T.todMin $ tod-  let (sWhole, _) = properFraction . T.todSec $ tod-  s <- intToSeconds sWhole-  tzo <- minsToOffset . T.timeZoneMinutes $ tz-  return $ DateTime d h m s tzo--toUTC :: DateTime -> T.UTCTime-toUTC dt = T.localTimeToUTC tz lt-  where-    tz = T.minutesToTimeZone . offsetToMins . timeZone $ dt-    tod = T.TimeOfDay (unHours h) (unMinutes m)-          (fromIntegral . unSeconds $ s)-    DateTime d h m s _ = dt-    lt = T.LocalTime d tod---- | Are these DateTimes the same instant in time, after adjusting for--- local timezones?--sameInstant :: DateTime -> DateTime -> Bool-sameInstant t1 t2 = toUTC t1 == toUTC t2---- | Shows a DateTime in a pretty way.-showDateTime :: DateTime -> String-showDateTime (DateTime d h m s tz) =-  ds ++ " " ++ hmss ++ " " ++ showOffset-  where-    ds = show d-    hmss = hs ++ ":" ++ ms ++ ":" ++ ss-    hs = pad0 . show . unHours $ h-    ms = pad0 . show . unMinutes $ m-    ss = pad0 . show . unSeconds $ s-    pad0 str = if length str < 2 then '0':str else str-    showOffset =-      let (zoneHr, zoneMin) = abs (offsetToMins tz) `divMod` 60-          sign = if offsetToMins tz < 0 then "-" else "+"-      in sign ++ pad0 (show zoneHr) ++ pad0 (show zoneMin)-
− Penny/Lincoln/Bits/Open.hs
@@ -1,124 +0,0 @@--- | These are the bits that are "open"; that is, their constructors--- are exported. This includes most bits. Some bits that have open--- constructors are not in this module because they include other bits--- that do not have exported constructors.--module Penny.Lincoln.Bits.Open where--import Data.Text (Text)-import qualified Data.Text as X-import qualified Penny.Lincoln.Serial as S-import qualified Penny.Lincoln.Bits.Qty as Q--newtype SubAccount =-  SubAccount { unSubAccount :: Text }-  deriving (Eq, Ord, Show)--newtype Account = Account { unAccount :: [SubAccount] }-                  deriving (Eq, Show, Ord)--data Amount = Amount { qty :: Q.Qty-                     , commodity :: Commodity-                     , side :: Maybe Side-                     , spaceBetween :: Maybe SpaceBetween }-              deriving (Eq, Show, Ord)--newtype Commodity =-  Commodity { unCommodity :: Text }-  deriving (Eq, Ord, Show)--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--data Entry = Entry { drCr :: DrCr-                   , amount :: Amount }-             deriving (Eq, Show, Ord)--newtype Flag = Flag { unFlag :: Text }-             deriving (Eq, Show, Ord)---- | There is one item in the list for each line of the memo. Do not--- include newlines in the texts themselves. However there is nothing--- to enforce this convention.-newtype Memo = Memo { unMemo :: [Text] }-             deriving (Eq, Show, Ord)--newtype Number = Number { unNumber :: Text }-                 deriving (Eq, Show, Ord)--newtype Payee = Payee { unPayee :: Text }-              deriving (Eq, Show, Ord)--newtype Tag = Tag { unTag :: Text }-                  deriving (Eq, Show, Ord)--newtype Tags = Tags { unTags :: [Tag] }-               deriving (Eq, Show, Ord)---- Metadata---- | The line number that the TopLine starts on (excluding the memo--- accompanying the TopLine).-newtype TopLineLine = TopLineLine { unTopLineLine :: Int }-                      deriving (Eq, Show)---- | The line number that the memo accompanying the TopLine starts on.-newtype TopMemoLine = TopMemoLine { unTopMemoLine :: Int }-                      deriving (Eq, Show)---- | The commodity and and the quantity may appear with the commodity--- on the left (e.g. USD 2.14) or with the commodity on the right--- (e.g. 2.14 USD).-data Side-  = CommodityOnLeft-  | CommodityOnRight-  deriving (Eq, Show, Ord)---- | There may or may not be a space in between the commodity and the--- quantity.-data SpaceBetween-  = SpaceBetween-  | NoSpaceBetween-  deriving (Eq, Show, Ord)---- | The name of the file in which a transaction appears.-newtype Filename = Filename { unFilename :: X.Text }-                   deriving (Eq, Show)---- | The line number on which a price appears.-newtype PriceLine = PriceLine { unPriceLine :: Int }-                    deriving (Eq, Show)---- | The line number on which a posting appears.-newtype PostingLine = PostingLine { unPostingLine :: Int }-                      deriving (Eq, Show)---- | All postings are numbered in order, beginning with the first--- posting in the first file and ending with the last posting--- in the last file.-newtype GlobalPosting =-  GlobalPosting { unGlobalPosting :: S.Serial }-  deriving (Eq, Show)---- | The postings in each file are numbered in order.-newtype FilePosting =-  FilePosting { unFilePosting :: S.Serial }-  deriving (Eq, Show)---- | All transactions are numbered in order, beginning with the first--- transaction in the first file and ending with the last transaction--- in the last file.-newtype GlobalTransaction =-  GlobalTransaction { unGlobalTransaction :: S.Serial }-  deriving (Eq, Show)---- | The transactions in each file are numbered in order.-newtype FileTransaction =-  FileTransaction { unFileTransaction :: S.Serial }-  deriving (Eq, Show)-
− Penny/Lincoln/Bits/Price.hs
@@ -1,42 +0,0 @@-module Penny.Lincoln.Bits.Price (-  From ( From, unFrom ),-  To ( To, unTo ),-  CountPerUnit ( CountPerUnit, unCountPerUnit ),-  Price ( from, to, countPerUnit ),-  convert,-  newPrice) where--import qualified Penny.Lincoln.Bits.Open as O-import Penny.Lincoln.Bits.Qty (Qty, mult)--newtype From = From { unFrom :: O.Commodity }-               deriving (Eq, Ord, Show)--newtype To = To { unTo :: O.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 -> O.Amount -> Maybe O.Amount-convert p (O.Amount q c sd sb) =-  if (unFrom . from $ p) /= c-  then Nothing-  else let q' = q `mult` (unCountPerUnit . countPerUnit $ p)-       in Just (O.Amount q' (unTo . to $ p) sd sb)---- | 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/Qty.hs
@@ -1,300 +0,0 @@--- | 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, NumberStr(..), toQty, mantissa, places, newQty,-  Mantissa, Places,-  add, mult, Difference(LeftBiggerBy, RightBiggerBy, Equal),-  equivalent, difference, allocate,-  TotSeats, PartyVotes, SeatsWon, largestRemainderMethod) where--import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Data.Foldable as F-import Data.List (genericLength, genericReplicate, genericSplitAt, sortBy)-import Data.List.NonEmpty (NonEmpty((:|)))-import Data.Ord (comparing)--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----- | Converts strings to Qty. Fails if any of the strings have--- non-digits, or if any are negative, or if the result is not greater--- than zero, or if the strings are empty.-toQty :: NumberStr -> Maybe Qty-toQty ns = case ns of-  Whole s -> fmap (\m -> Qty m 0) (readInteger s)-  WholeRad s -> fmap (\m -> Qty m 0) (readInteger s)-  WholeRadFrac w f -> fromWholeRadFrac w f-  RadFrac f -> fromWholeRadFrac "0" f-  where-    fromWholeRadFrac w f =-      fmap (\m -> Qty m (genericLength f)) (readInteger (w ++ f))---- | Reads non-negative integers only.-readInteger :: String -> Maybe Integer-readInteger s = case reads s of-  (i, ""):[] -> if i < 0 then Nothing else Just i-  _ -> Nothing---- | 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.------ The Eq instance is derived. Therefore q1 == q2 only if q1 and q2--- have both the same mantissa and the same exponent. You may instead--- want 'equivalent'.-data Qty = Qty { mantissa :: Integer-               , places :: Integer-               } deriving Eq--type Mantissa = Integer-type Places = Integer--newQty :: Mantissa -> Places -> Maybe Qty-newQty m p-  | m > 0  && p >= 0 = Just $ Qty m p-  | otherwise = Nothing---- | Shows a quantity, nicely formatted after accounting for both the--- mantissa and decimal places, e.g. @0.232@ or @232.12@ or whatever.-instance Show Qty where-  show (Qty m e) =-    let man = show m-        len = genericLength man-        small = "0." ++ ((genericReplicate (e - len) '0') ++ man)-    in case compare e len of-        GT -> small-        EQ -> small-        LT ->-          let (b, end) = genericSplitAt (len - e) man-          in if e == 0-             then man-             else b ++ ['.'] ++ end----- | Compares Qty after equalizing their exponents.------ > compare (newQty 15 1) (newQty 1500 3) == EQ-instance Ord Qty where-  compare q1 q2 = compare (mantissa q1') (mantissa q2')-    where-      (q1', q2') = equalizeExponents q1 q2---- | Adjust the exponents on two Qty so they are equivalent--- before, but now have the same exponent.-equalizeExponents :: Qty -> Qty -> (Qty, Qty)-equalizeExponents x y = (x', y')-  where-    (ex, ey) = (places x, places y)-    (x', y') = case compare ex ey of-      GT -> (x, increaseExponent (ex - ey) y)-      LT -> (increaseExponent (ey - ex) x, y)-      EQ -> (x, y)----- | Increase the exponent by the amount given, so that the new Qty is--- equivalent to the old one. Takes the absolute value of the--- adjustment argument.-increaseExponent :: Integer -> Qty -> Qty-increaseExponent i (Qty m e) = Qty m' e'-  where-    amt = abs i-    m' = m * 10 ^ amt-    e' = e + amt---- | Increases the exponent to the given amount. Does nothing if the--- exponent is already at or higher than this amount.-increaseExponentTo :: Integer -> Qty -> Qty-increaseExponentTo i q@(Qty _ e) =-  let diff = i - e-  in if diff >= 0 then increaseExponent diff q else q---- | Compares Qty after equalizing their exponents.-equivalent :: Qty -> Qty -> Bool-equivalent x y = x' == y'-  where-    (x', y') = equalizeExponents x y--data Difference =-  LeftBiggerBy Qty-  | RightBiggerBy Qty-  | Equal-  deriving (Eq, Show)---- | Subtract the second Qty from the first, after equalizing their--- exponents.-difference :: Qty -> Qty -> Difference-difference x y =-  let (x', y') = equalizeExponents x y-      (mx, my) = (mantissa x', mantissa y')-  in case compare mx my of-    GT -> LeftBiggerBy (Qty (mx - my) (places x'))-    LT -> RightBiggerBy (Qty (my - mx) (places x'))-    EQ -> Equal--add :: Qty -> Qty -> Qty-add x y =-  let ((Qty xm e), (Qty ym _)) = equalizeExponents x y-  in Qty (xm + ym) e--mult :: Qty -> Qty -> Qty-mult (Qty xm xe) (Qty ym ye) = Qty (xm * ym) (xe + ye)----- | Allocate a Qty proportionally so that the sum of the results adds--- up to a given Qty. Fails if the allocation cannot be made (e.g. if--- it is impossible to allocate without overflowing Decimal.) The--- result will always add up to the given sum.-allocate-  :: Qty-  -- ^ The result will add up to this Qty.--  -> NonEmpty Qty-  -- ^ Allocate using this list of Qty.--  -> NonEmpty Qty-  -- ^ The length of this list will be equal to the length of the list-  -- of allocations. Each item will correspond to the original-  -- allocation.--allocate tot ls =-  let (tot', ls', e') = sameExponent tot ls-      (tI, lsI) = (mantissa tot', fmap mantissa ls')-      (seats, (p1 :| ps), moreE) = growTarget tI lsI-      adjSeats = seats - (genericLength ps + 1)-      del = largestRemainderMethod adjSeats (p1 : ps)-      totE = e' + moreE-      r1:rs = fmap (\m -> Qty (m + 1) totE) del-  in r1 :| rs----- | Given a list of Decimals, and a single Decimal, return Decimals--- that are equivalent to the original Decimals, but where all--- Decimals have the same exponent. Also returns new exponent.-sameExponent-  :: Qty-  -> NonEmpty Qty-  -> (Qty, NonEmpty Qty, Integer)-sameExponent dec ls =-  let newExp = max (F.maximum . fmap places $ ls)-                   (places dec)-      dec' = increaseExponentTo newExp dec-      ls' = fmap (increaseExponentTo newExp) ls-  in (dec', ls', newExp)----- | Given an Integer and a list of Integers, multiply all integers by--- ten raised to an exponent, so that the first Integer is larger than--- the count of the number of Integers in the list. Returns--- the new Integer, new list of Integers, and the exponent used.------ Previously this only grew the first Integer so that it was at least--- as large as the count of Integers in the list, but this causes--- problems, as there must be at least one seat for the allocation process.-growTarget-  :: Integer-  -> NonEmpty Integer-  -> (Integer, NonEmpty Integer, Integer)-growTarget target is = go target is 0-  where-    len = genericLength . F.toList $ is-    go t xs c =-      let t' = t * 10 ^ c-          xs' = fmap (\x -> x * 10 ^ c) xs-      in if t' > len-         then (t', xs', c)-         else go t' xs' (c + 1)---- Largest remainder method: votes for one party is divided by--- (total votes / number of seats). Result is an integer and a--- remainder. Each party gets the number of seats indicated by its--- integer. Parties are then ranked on the basis of the remainders, and--- those with the largest remainders get an additional seat until all--- seats have been distributed.-type AutoSeats = Integer-type PartyVotes = Integer-type TotVotes = Integer-type TotSeats = Integer-type Remainder = Rational-type SeatsWon = Integer---- | Allocates integers using the largest remainder method. This is--- the method used to allocate parliamentary seats in many countries,--- so the types are named accordingly.-largestRemainderMethod-  :: TotSeats-  -- ^ Total number of seats in the legislature. This is the integer-  -- that will be allocated. This number must be positive or this-  -- function will fail at runtime.--  -> [PartyVotes]-  -- ^ The total seats will be allocated proportionally depending on-  -- how many votes each party received. The sum of this list must be-  -- positive, and each member of the list must be at least zero;-  -- otherwise a runtime error will occur.--  -> [SeatsWon]-  -- ^ The sum of this list will always be equal to the total number-  -- of seats, and its length will always be equal to length of the-  -- PartyVotes list.--largestRemainderMethod ts pvs =-  let err s = error $ "largestRemainderMethod: error: " ++ s-  in Ex.resolve err $ do-    Ex.assert "TotalSeats not positive" (ts > 0)-    Ex.assert "sum of [PartyVotes] not positive" (sum pvs > 0)-    Ex.assert "negative member of [PartyVotes]" (minimum pvs >= 0)-    return (allocRemainder ts . allocAuto ts $ pvs)--autoAndRemainder-  :: TotSeats -> TotVotes -> PartyVotes -> (AutoSeats, Remainder)-autoAndRemainder ts tv pv =-  let fI = fromIntegral-      quota = if ts == 0-              then error "autoAndRemainder: zero total seats"-              else if tv == 0-                   then error "autoAndRemainder: zero total votes"-                   else fI tv / fI ts-  in properFraction (fI pv / quota)---allocAuto :: TotSeats -> [PartyVotes] -> [(AutoSeats, Remainder)]-allocAuto ts pvs = map (autoAndRemainder ts (sum pvs)) pvs--allocRemainder-  :: TotSeats-  -> [(AutoSeats, Remainder)]-  -> [SeatsWon]-allocRemainder ts ls =-  let totLeft = ts - (sum . map fst $ ls)-      (leftForEach, stillLeft) = totLeft `divMod` genericLength ls-      wIndex = zip ([0..] :: [Integer]) ls-      sorted = sortBy (comparing (snd . snd)) wIndex-      wOrder = zip [0..] sorted-      awarder (ord, (ix, (as, _))) =-        if ord < stillLeft-        then (ix, as + leftForEach + 1)-        else (ix, as + leftForEach)-      awarded = map awarder wOrder-  in map snd . sortBy (comparing fst) $ awarded
− Penny/Lincoln/Builders.hs
@@ -1,28 +0,0 @@--- | 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-  ( account-  ) where--import qualified Penny.Lincoln.Bits as B-import qualified Data.Text as X---- | Create an Account. You supply a single Text, with colons to--- separate the different sub-accounts.-account :: X.Text -> B.Account-account s =-  if X.null s-  then B.Account []-  else B.Account . map B.SubAccount . X.splitOn (X.singleton ':') $ s-
− Penny/Lincoln/Family.hs
@@ -1,104 +0,0 @@--- | 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.mapChildrenA,-  F.mapChildren,-  F.mapParentA,-  F.mapParent,--  -- * Functions to manipulate families-  children,-  orphans,-  adopt,-  marryWith,-  marry,-  divorceWith,-  divorce,-  F.filterChildren,-  F.find,-  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
@@ -1,9 +0,0 @@-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
@@ -1,76 +0,0 @@-module Penny.Lincoln.Family.Family where--import Control.Applicative ((<$>), (<*>), pure, Applicative)-import qualified Data.List as L-import Data.Traversable (traverse)---- | 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 order starting with child--- 1, then child 2, then the children in the list from left to right.-mapChildrenA ::-  Applicative m-  => (a -> m b)-  -> Family p a-  -> m (Family p b)-mapChildrenA f (Family p c1 c2 cs) =-  Family p <$> f c1 <*> f c2 <*> traverse f cs----- | Maps over all children.-mapChildren ::-  (a -> b)-  -> Family p a-  -> Family p b-mapChildren f (Family p c1 c2 cs) =-  Family p (f c1) (f c2) (map f cs)----- | Maps over the parent in an Applicative.-mapParentA ::-  Applicative m-  => (a -> m b)-  -> Family a c-  -> m (Family b c)-mapParentA f (Family p c1 c2 cs) =-  Family <$> f p <*> pure c1 <*> pure c2 <*> pure cs----- | Maps over the parent.-mapParent :: (a -> b) -> Family a c -> Family b c-mapParent f (Family p c1 c2 cs) = Family (f p) c1 c2 cs----- | Finds the first child matching a predicate.-find :: (p -> c -> Bool) -> Family p c -> Maybe c-find f (Family p c1 c2 cs)-  | f p c1 = Just c1-  | f p c2 = Just c2-  | otherwise = L.find (f p) cs---- | Filters the children. Fails if there are not at least two--- children after filtering. Retains the original order of the--- children (after removing the children you don't want.)-filterChildren :: (a -> Bool) -> Family p a -> Maybe (Family p a)-filterChildren f (Family p c1 c2 cs) =-  case (f c1, f c2) of-    (True, True) -> Just (Family p c1 c2 (filter f cs))-    (True, False) ->-      case filter f cs of-        [] -> Nothing-        x:xs -> Just (Family p c1 x xs)-    (False, True) ->-      case filter f cs of-        [] -> Nothing-        x:xs -> Just (Family p c2 x xs)-    (False, False) ->-      case filter f cs of-        x1:x2:xs -> Just (Family p x1 x2 xs)-        _ -> Nothing
− Penny/Lincoln/Family/Siblings.hs
@@ -1,54 +0,0 @@-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
@@ -1,44 +0,0 @@-module Penny.Lincoln.HasText where--import Data.Text (Text)--import qualified Penny.Lincoln.Bits as B--class HasText a where-  text :: a -> Text--instance HasText Text where-  text = id--instance HasText B.SubAccount where-  text = B.unSubAccount--instance HasText B.Flag where-  text = B.unFlag--instance HasText B.Commodity where-  text = B.unCommodity--instance HasText B.Number where-  text = B.unNumber--instance HasText B.Payee where-  text = B.unPayee--instance HasText B.Tag where-  text = B.unTag--instance HasText B.Filename where-  text = B.unFilename--class HasTextList a where-  textList :: a -> [Text]--instance HasTextList B.Account where-  textList = map text . B.unAccount--instance HasTextList B.Tags where-  textList = map text . B.unTags--instance HasTextList B.Memo where-  textList = B.unMemo
− Penny/Lincoln/Matchers.hs
@@ -1,21 +0,0 @@--- | Type synonyms for functions dealing with text matching.--module Penny.Lincoln.Matchers where--import qualified Data.Text as X-import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Text.Matchers as MT---- | A function that makes Matchers.-type Factory-  = MT.CaseSensitive-  -- ^ Will this matcher be case sensitive?--  -> X.Text-  -- ^ The pattern to use when testing for a match. For example, this-  -- might be a regular expression, or simply the text to be matched.--  -> Ex.Exceptional X.Text MT.Matcher-  -- ^ Sometimes producing a matcher might fail; for example, the user-  -- might have supplied a bad pattern. If so, an exception is-  -- returned. On success, a Matcher is returned.
− Penny/Lincoln/Predicates.hs
@@ -1,254 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Functions that return a boolean based upon some criterion that--- matches something, often a PostFam. Useful when filtering--- Postings.-module Penny.Lincoln.Predicates-  ( LPdct-  , MakePdct-  , payee-  , number-  , flag-  , postingMemo-  , transactionMemo-  , date-  , qty-  , drCr-  , debit-  , credit-  , commodity-  , account-  , accountLevel-  , accountAny-  , tag-  , filename-  , reconciled-  , clonedTransactions-  , clonedTopLines-  , clonedPostings-  ) where---import Data.List (intersperse)-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as X-import qualified Data.Time as Time-import qualified Penny.Lincoln.Bits as B-import Penny.Lincoln.HasText (HasText, text, HasTextList, textList)-import qualified Penny.Lincoln.Family.Family as F-import qualified Penny.Lincoln.Queries as Q-import Penny.Lincoln.Transaction (PostFam)-import qualified Penny.Lincoln.Transaction as T-import qualified Text.Matchers as M-import qualified Data.Prednote.Pdct as P--type LPdct = P.Pdct PostFam--type MakePdct = M.Matcher -> LPdct---- * Matching helpers-match-  :: HasText a-  => Text-  -- ^ Description of this field-  -> (PostFam -> a)-  -- ^ Function that returns the field being matched-  -> M.Matcher-  -> LPdct-match t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = M.match m . text . f--matchMaybe-  :: HasText a-  => Text-  -- ^ Description of this field-  -> (PostFam -> Maybe a)-  -> M.Matcher-  -> LPdct-matchMaybe t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = maybe False (M.match m . text) . f--makeDesc :: Text -> M.Matcher -> Text-makeDesc t m-  = "subject: " <> t-  <> " matcher: " <> M.matchDesc m---- | Does the given matcher match any of the elements of the Texts in--- a HasTextList?-matchAny-  :: HasTextList a-  => Text-  -> (PostFam -> a)-  -> M.Matcher-  -> LPdct-matchAny t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = any (M.match m) . textList . f---- | Does the given matcher match the text that is at the given--- element of a HasTextList? If the HasTextList does not have a--- sufficent number of elements to perform this test, returns False.-matchLevel-  :: HasTextList a-  => Int-  -> Text-  -> (PostFam -> a)-  -> M.Matcher-  -> LPdct-matchLevel l d f m = P.operand desc pd-  where-    desc = makeDesc ("level " <> X.pack (show l) <> " of " <> d) m-    pd pf = let ts = textList (f pf)-            in if l < 0 || l >= length ts-               then False-               else M.match m (ts !! l)---- | Does the matcher match the text of the memo? Joins each line of--- the memo with a space.-matchMemo-  :: Text-  -> (PostFam -> Maybe B.Memo)-  -> M.Matcher-  -> LPdct-matchMemo t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = maybe False doMatch . f-    doMatch = M.match m-              . X.intercalate (X.singleton ' ')-              . B.unMemo--matchDelimited-  :: HasTextList a-  => Text-  -- ^ Separator-  -> Text-  -- ^ Label-  -> (PostFam -> a)-  -> M.Matcher-  -> LPdct-matchDelimited sep lbl f m = match lbl f' m-  where-    f' = X.concat . intersperse sep . textList . f---- * Pattern matching fields--payee :: MakePdct-payee = matchMaybe "payee" Q.payee--number :: MakePdct-number = matchMaybe "number" Q.number--flag :: MakePdct-flag = matchMaybe "flag" Q.flag--filename :: MakePdct-filename = matchMaybe "filename" Q.filename--postingMemo :: MakePdct-postingMemo = matchMemo "posting memo" Q.postingMemo--transactionMemo :: MakePdct-transactionMemo = matchMemo "transaction memo" Q.transactionMemo---- * Date--date-  :: Ordering-  -> Time.UTCTime-  -> LPdct-date ord u = P.compareBy (X.pack . show $ u)-             "UTC date and time"-           (\l -> compare (B.toUTC . Q.dateTime $ l) u) ord---qty :: Ordering -> B.Qty -> LPdct-qty o q = P.compareBy (X.pack . show $ q) "quantity"-          (\l -> compare (Q.qty l) q) o---drCr :: B.DrCr -> LPdct-drCr dc = P.operand desc pd-  where-    desc = "entry is a " <> s-    s = case dc of { B.Debit -> "debit"; B.Credit -> "credit" }-    pd pf = Q.drCr pf == dc--debit :: LPdct-debit = drCr B.Debit--credit :: LPdct-credit = drCr B.Credit--commodity :: M.Matcher -> LPdct-commodity = match "commodity" Q.commodity--account :: M.Matcher -> LPdct-account = matchDelimited ":" "account" Q.account--accountLevel :: Int -> M.Matcher -> LPdct-accountLevel i = matchLevel i "account" Q.account--accountAny :: M.Matcher -> LPdct-accountAny = matchAny "any sub-account" Q.account--tag :: M.Matcher -> LPdct-tag = matchAny "any tag" Q.tags---- | True if a posting is reconciled; that is, its flag is exactly--- @R@.-reconciled :: LPdct-reconciled = P.operand d p-  where-    d = "posting flag is exactly \"R\" (is reconciled)"-    p = maybe False ((== X.singleton 'R') . B.unFlag) . Q.flag---- | Returns True if these two transactions are clones; that is, if--- they are identical in all respects except some aspects of their--- metadata. The metadata that is disregarded when testing for clones--- pertains to the location of the transaction. (Resembles cloned--- sheep, which are identical but cannot be in exactly the same--- place.)-clonedTransactions :: T.Transaction -> T.Transaction -> Bool-clonedTransactions a b =-  let (F.Family ta p1a p2a psa) = T.unTransaction a-      (F.Family tb p1b p2b psb) = T.unTransaction b-  in clonedTopLines ta tb-     && clonedPostings p1a p1b-     && clonedPostings p2a p2b-     && (length psa == length psb)-     && (and (zipWith clonedPostings psa psb))---- | Returns True if two TopLines are clones. Considers only the--- non-metadata aspects of the TopLine; the metadata all pertains only--- to the location of the TopLine. The DateTimes are compared based on--- both the local time and the time zone; that is, two times that--- refer to the same instant will not compare as identical if they--- have different time zones.-clonedTopLines :: T.TopLine -> T.TopLine -> Bool-clonedTopLines t1 t2 =-  (T.tDateTime t1 == T.tDateTime t2)-  && (T.tFlag t1 == T.tFlag t2)-  && (T.tNumber t1 == T.tNumber t2)-  && (T.tPayee t2 == T.tPayee t2)-  && (T.tMemo t1 == T.tMemo t2)---- | Returns True if two Postings are clones. Considers only the--- non-location-related aspects of the posting metadata.-clonedPostings :: T.Posting -> T.Posting -> Bool-clonedPostings p1 p2 =-  (T.pPayee p1 == T.pPayee p2)-  && (T.pNumber p1 == T.pNumber p2)-  && (T.pFlag p1 == T.pFlag p2)-  && (T.pAccount p1 == T.pAccount p2)-  && (T.pTags p1 == T.pTags p2)-  && (T.pEntry p1 == T.pEntry p2)-  && (T.pMemo p1 == T.pMemo p2)-  && (T.pInferred p1 == T.pInferred p2)-
− Penny/Lincoln/Predicates/Siblings.hs
@@ -1,210 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Functions that return a boolean based upon some criterion that--- matches something, often a PostFam. Useful when filtering--- Postings.-module Penny.Lincoln.Predicates.Siblings-  ( LPdct-  , MakePdct-  , payee-  , number-  , flag-  , postingMemo-  , qty-  , parseQty-  , drCr-  , debit-  , credit-  , commodity-  , account-  , accountLevel-  , accountAny-  , tag-  , reconciled-  ) where---import Data.List (intersperse)-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as X-import qualified Penny.Lincoln.Bits as B-import Penny.Lincoln.HasText (HasText, text, HasTextList, textList)-import qualified Penny.Lincoln.Queries.Siblings as Q-import Penny.Lincoln.Transaction (PostFam)-import qualified Text.Matchers as M-import qualified Data.Prednote.Pdct as P--type LPdct = P.Pdct PostFam--type MakePdct = M.Matcher -> LPdct---- * Matching helpers-match-  :: HasText a-  => Text-  -- ^ Description of this field-  -> (PostFam -> [a])-  -- ^ Function that returns the field being matched-  -> M.Matcher-  -> LPdct-match t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = any (M.match m) . map text . f--matchMaybe-  :: HasText a-  => Text-  -- ^ Description of this field-  -> (PostFam -> [Maybe a])-  -> M.Matcher-  -> LPdct-matchMaybe t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = any (== (Just True))-         . map (fmap (M.match m . text))-         . f--makeDesc :: Text -> M.Matcher -> Text-makeDesc t m-  = "subject: " <> t <> " (any sibling posting) matcher: "-  <> M.matchDesc m---- | Does the given matcher match any of the elements of the Texts in--- a HasTextList?-matchAny-  :: HasTextList a-  => Text-  -> (PostFam -> [a])-  -> M.Matcher-  -> LPdct-matchAny t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = any (any (M.match m)) . map textList . f---- | Does the given matcher match the text that is at the given--- element of a HasTextList? If the HasTextList does not have a--- sufficent number of elements to perform this test, returns False.-matchLevel-  :: HasTextList a-  => Int-  -> Text-  -> (PostFam -> [a])-  -> M.Matcher-  -> LPdct-matchLevel l d f m = P.operand desc pd-  where-    desc = makeDesc ("level " <> X.pack (show l) <> " of " <> d) m-    pd pf = let doMatch list = if l < 0 || l >= length list-                               then False-                               else M.match m (list !! l)-            in any doMatch . map textList . f $ pf---- | Does the matcher match the text of the memo? Joins each line of--- the memo with a space.-matchMemo-  :: Text-  -> (PostFam -> [Maybe B.Memo])-  -> M.Matcher-  -> LPdct-matchMemo t f m = P.operand desc pd-  where-    desc = makeDesc t m-    pd = any (maybe False doMatch) . f-    doMatch = M.match m-              . X.intercalate (X.singleton ' ')-              . B.unMemo--matchDelimited-  :: HasTextList a-  => Text-  -- ^ Separator-  -> Text-  -- ^ Label-  -> (PostFam -> [a])-  -> M.Matcher-  -> LPdct-matchDelimited sep lbl f m = match lbl f' m-  where-    f' = map (X.concat . intersperse sep . textList) . f---- * Pattern matching fields--payee :: MakePdct-payee = matchMaybe "payee" Q.payee--number :: MakePdct-number = matchMaybe "number" Q.number--flag :: MakePdct-flag = matchMaybe "flag" Q.flag--postingMemo :: MakePdct-postingMemo = matchMemo "posting memo" Q.postingMemo---- | A Pdct that returns True if @compare subject qty@ returns the--- given Ordering.-qty :: Ordering -> B.Qty -> LPdct-qty o q = P.operand desc pd-  where-    desc = "quantity of any sibling is " <> dd <> " " <> X.pack (show q)-    dd = case o of-      LT -> "less than"-      GT -> "greater than"-      EQ -> "equal to"-    pd = any ((== o) . (`compare` q)) . Q.qty--parseQty-  :: X.Text-  -> Maybe (B.Qty -> LPdct)-parseQty x-  | x == "==" = Just (qty EQ)-  | x == "=" = Just (qty EQ)-  | x == ">" = Just (qty GT)-  | x == "<" = Just (qty LT)-  | x == "/=" = Just (\q -> P.not (qty EQ q))-  | x == "!=" = Just (\q -> P.not (qty EQ q))-  | x == ">=" = Just (\q -> P.or [qty GT q, qty EQ q])-  | x == "<=" = Just (\q -> P.or [qty LT q, qty EQ q])-  | otherwise = Nothing--drCr :: B.DrCr -> LPdct-drCr dc = P.operand desc pd-  where-    desc = "entry of any sibling is a " <> s-    s = case dc of { B.Debit -> "debit"; B.Credit -> "credit" }-    pd = any (== dc) . Q.drCr--debit :: LPdct-debit = drCr B.Debit--credit :: LPdct-credit = drCr B.Credit--commodity :: M.Matcher -> LPdct-commodity = match "commodity" Q.commodity--account :: M.Matcher -> LPdct-account = matchDelimited ":" "account" Q.account--accountLevel :: Int -> M.Matcher -> LPdct-accountLevel i = matchLevel i "account" Q.account--accountAny :: M.Matcher -> LPdct-accountAny = matchAny "any sub-account" Q.account--tag :: M.Matcher -> LPdct-tag = matchAny "any tag" Q.tags---- | True if a posting is reconciled; that is, its flag is exactly--- @R@.-reconciled :: LPdct-reconciled = P.operand d p-  where-    d = "posting flag is exactly \"R\" (is reconciled)"-    p = any (maybe False ((== X.singleton 'R') . B.unFlag))-        . Q.flag-
− Penny/Lincoln/PriceDb.hs
@@ -1,98 +0,0 @@--- | 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.Map as M-import qualified Data.Time as T-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 (M.Map B.From ToMap)---- | An empty PriceDb-emptyDb :: PriceDb-emptyDb = PriceDb M.empty---- | Add a single price to the PriceDb.-addPrice :: PriceDb -> B.PricePoint -> PriceDb-addPrice (PriceDb db) (B.PricePoint dt pr _ _ _) = PriceDb m'-  where-    m' = M.alter f (B.from pr) db-    utc = B.toUTC dt-    cpu = B.countPerUnit pr-    f k = case k of-      Nothing -> Just $ M.singleton (B.to pr) cpuMap-        where-          cpuMap = M.singleton utc cpu-      Just tm -> Just tm'-        where-          tm' = M.alter g (B.to pr) tm-          g maybeTo = case maybeTo of-            Nothing -> Just $ M.singleton utc cpu-            Just cpuMap -> Just $ M.insert utc cpu cpuMap------ | 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.------ 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.--getPrice ::-  PriceDb-  -> B.From-  -> B.To-  -> B.DateTime-  -> Ex.Exceptional PriceDbError B.CountPerUnit-getPrice (PriceDb db) fr to dt = do-  let utc = B.toUTC dt-  toMap <- Ex.fromMaybe FromNotFound $ M.lookup fr db-  cpuMap <- Ex.fromMaybe ToNotFound $ M.lookup to toMap-  let (lower, exact, _) = M.splitLookup utc cpuMap-  case exact of-    Just c -> return c-    Nothing ->-      if M.null lower-      then Ex.throw CpuNotFound-      else return . snd . M.findMax $ lower----- | 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.Qty-convert db dt to (B.Amount qt fr _ _)-  | fr == B.unTo to = return qt-  | otherwise = do-    cpu <- fmap B.unCountPerUnit (getPrice db (B.From fr) to dt)-    let qt' = B.mult cpu qt-    return qt'
− Penny/Lincoln/Queries.hs
@@ -1,100 +0,0 @@--- | Examining a PostFam for a particular component of the main--- posting (as opposed to the sibling postings) in the PostFam. For--- some components, such as the payee, the posting might have one--- piece of data while the TopLine has something else. These functions--- will examine the Posting first and, if it has no information, use--- the data from the TopLine if it is there.-module Penny.Lincoln.Queries where--import qualified Penny.Lincoln.Bits as B-import Penny.Lincoln.Family.Child (child, parent)-import qualified Penny.Lincoln.Transaction as T-import Penny.Lincoln.Balance (Balance, entryToBalance)-import qualified Data.Time as Time---- | Uses the data from the Posting if it is set; otherwise, use the--- data from the TopLine.-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 -> Maybe B.Memo-postingMemo = T.pMemo . child . T.unPostFam--transactionMemo :: T.PostFam -> Maybe B.Memo-transactionMemo = T.tMemo . parent . T.unPostFam--dateTime :: T.PostFam -> B.DateTime-dateTime = T.tDateTime . parent . T.unPostFam--localDay :: T.PostFam -> Time.Day-localDay = B.day . dateTime--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--topMemoLine :: T.PostFam -> Maybe B.TopMemoLine-topMemoLine = T.tTopMemoLine . parent . T.unPostFam--topLineLine :: T.PostFam -> Maybe B.TopLineLine-topLineLine = T.tTopLineLine . parent . T.unPostFam--filename :: T.PostFam -> Maybe B.Filename-filename = T.tFilename . parent . T.unPostFam--globalTransaction :: T.PostFam -> Maybe B.GlobalTransaction-globalTransaction = T.tGlobalTransaction . parent . T.unPostFam--fileTransaction :: T.PostFam -> Maybe B.FileTransaction-fileTransaction = T.tFileTransaction . parent . T.unPostFam--postingLine :: T.PostFam -> Maybe B.PostingLine-postingLine = T.pPostingLine . child . T.unPostFam--side :: T.PostFam -> Maybe B.Side-side = B.side . amount--spaceBetween :: T.PostFam -> Maybe B.SpaceBetween-spaceBetween = B.spaceBetween . amount--globalPosting :: T.PostFam -> Maybe B.GlobalPosting-globalPosting = T.pGlobalPosting . child . T.unPostFam--filePosting :: T.PostFam -> Maybe B.FilePosting-filePosting = T.pFilePosting . child . T.unPostFam
− Penny/Lincoln/Queries/Siblings.hs
@@ -1,87 +0,0 @@--- | Like 'Penny.Lincoln.Queries' but instead of querying the main--- posting of the PostFam, queries the siblings. Therefore, these--- functions return a list, with each entry in the list containing the--- best answer for each sibling. There is one item in the list for--- each sibling, even if all these items contain the same data (for--- instance, a posting might have five siblings, but all five siblings--- might have the same payee. Nonetheless the 'payee' function will--- return a list of five items.)-module Penny.Lincoln.Queries.Siblings where--import qualified Penny.Lincoln.Bits as B-import Penny.Lincoln.Family.Child (Child(Child))-import qualified Penny.Lincoln.Transaction as T-import Penny.Lincoln.Balance (Balance, entryToBalance)---- | For all siblings, uses information from the Posting if it is set;--- otherwise, uses data from the TopLine.-bestSibs-  :: (T.Posting -> Maybe a)-  -> (T.TopLine -> Maybe a)-  -> T.PostFam-  -> [Maybe a]-bestSibs fp ft pf =-  let (Child _ s1 ss p) = T.unPostFam pf-      get = maybe (ft p) Just . fp-  in get s1 : map get ss---- | For all siblings, get the information from the Posting if it--- exists; otherwise Nothing.-sibs-  :: (T.Posting -> a)-  -> T.PostFam-  -> [a]-sibs fp pf =-  let (Child _ s1 ss _) = T.unPostFam pf-  in fp s1 : map fp ss--payee :: T.PostFam -> [Maybe B.Payee]-payee = bestSibs T.pPayee T.tPayee--number :: T.PostFam -> [Maybe B.Number]-number = bestSibs T.pNumber T.tNumber--flag :: T.PostFam -> [Maybe B.Flag]-flag = bestSibs T.pFlag T.tFlag--postingMemo :: T.PostFam -> [Maybe B.Memo]-postingMemo = sibs T.pMemo--account :: T.PostFam -> [B.Account]-account = sibs T.pAccount--tags :: T.PostFam -> [B.Tags]-tags = sibs T.pTags--entry :: T.PostFam -> [B.Entry]-entry = sibs T.pEntry--balance :: T.PostFam -> [Balance]-balance = map entryToBalance . entry--drCr :: T.PostFam -> [B.DrCr]-drCr = map B.drCr . entry--amount :: T.PostFam -> [B.Amount]-amount = map B.amount . entry--qty :: T.PostFam -> [B.Qty]-qty = map B.qty . amount--commodity :: T.PostFam -> [B.Commodity]-commodity = map B.commodity . amount--postingLine :: T.PostFam -> [Maybe B.PostingLine]-postingLine = sibs T.pPostingLine--side :: T.PostFam -> [Maybe B.Side]-side = map B.side . amount--spaceBetween :: T.PostFam -> [Maybe B.SpaceBetween]-spaceBetween = map B.spaceBetween . amount--globalPosting :: T.PostFam -> [Maybe B.GlobalPosting]-globalPosting = sibs T.pGlobalPosting--filePosting :: T.PostFam -> [Maybe B.FilePosting]-filePosting = sibs T.pFilePosting
− Penny/Lincoln/Serial.hs
@@ -1,58 +0,0 @@-module Penny.Lincoln.Serial (-  Serial, forward, backward, GenSerial,-  incrementBack, getSerial, makeSerials, serialItems,-  nSerials ) where--import Control.Applicative (Applicative, (<*>), pure, (*>))-import Control.Monad (ap)--data SerialSt = SerialSt-  { nextFwd :: Int-  , nextBack :: Int-  } deriving Show--data Serial = Serial-  { forward :: Int-  , backward :: Int-  } deriving (Eq, Show, Ord)--newtype GenSerial a = GenSerial (SerialSt -> (a, SerialSt))--instance Functor GenSerial where-  fmap f (GenSerial k) = GenSerial $ \s ->-    let (a', st') = k s-    in (f a', st')--instance Applicative GenSerial where-  pure = return-  (<*>) = ap--instance Monad GenSerial where-  return a = GenSerial $ \s -> (a, s)-  (GenSerial k) >>= f = GenSerial $ \s ->-    let (a, s') = k s-        GenSerial g = f a-    in g s'--incrementBack :: GenSerial ()-incrementBack = GenSerial $ \s ->-  let s' = SerialSt (nextFwd s) (nextBack s + 1)-  in ((), s')--getSerial :: GenSerial Serial-getSerial = GenSerial $ \s ->-  let s' = SerialSt (nextFwd s + 1) (nextBack s - 1)-  in (Serial (nextFwd s) (nextBack s), s')--makeSerials :: GenSerial a -> a-makeSerials (GenSerial k) =-  let (r, _) = k (SerialSt 0 0) in r--serialItems :: (Serial -> a -> b) -> [a] -> [b]-serialItems f as = zipWith f (nSerials (length as)) as--nSerials :: Int -> [Serial]-nSerials n =-  makeSerials $-  (sequence . replicate n $ incrementBack)-  *> (sequence . replicate n $ getSerial)
− Penny/Lincoln/Transaction.hs
@@ -1,499 +0,0 @@--- | 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 and deconstructing transactions-  transaction,-  RTransaction(..),-  rTransaction,-  Error ( UnbalancedError, CouldNotInferError),-  toUnverified,--  -- * Querying postings-  Inferred(Inferred, NotInferred),-  pPayee, pNumber, pFlag, pAccount, pTags,-  pEntry, pMemo, pInferred, pPostingLine,-  pGlobalPosting, pFilePosting,--  -- * Querying transactions-  TopLine,-  tDateTime, tFlag, tNumber, tPayee, tMemo, tTopLineLine,-  tTopMemoLine, tFilename, tGlobalTransaction, tFileTransaction,-  unTransaction, postFam,--  -- * Box-  Box ( Box, boxMeta, boxPostFam ),--  -- * Changers--  -- | Functions allowing you to change aspects of an existing-  -- transaction, without having to destroy and completely rebuild the-  -- transaction. You cannot change the Entry or any of its-  -- components, as changing any of these would unbalance the-  -- Transaction.-  TopLineChangeData(..),-  emptyTopLineChangeData,-  PostingChangeData(..),-  emptyPostingChangeData,-  changeTransaction-  ) where--import qualified Penny.Lincoln.Bits as B-import Penny.Lincoln.Family ( children, orphans, adopt )-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 Control.Monad.Exception.Synchronous (-  Exceptional (Exception, Success) , throw )-import qualified Control.Monad.Exception.Synchronous as Ex-import qualified Data.Foldable as Fdbl-import Data.Maybe ( catMaybes, fromMaybe )-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     :: Maybe B.Memo-          , pInferred :: Inferred-          , pPostingLine :: Maybe B.PostingLine-          , pGlobalPosting :: Maybe B.GlobalPosting-          , pFilePosting :: Maybe B.FilePosting-          }-  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     :: Maybe B.Memo-          , tTopLineLine :: Maybe B.TopLineLine-          , tTopMemoLine :: Maybe B.TopMemoLine-          , tFilename :: Maybe B.Filename-          , tGlobalTransaction :: Maybe B.GlobalTransaction-          , tFileTransaction :: Maybe B.FileTransaction }-  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."---}---- | Deconstruct a Transaction to a family of unverified data.-toUnverified :: Transaction -> F.Family U.TopLine U.Posting-toUnverified = F.mapParent fp . F.mapChildren fc . unTransaction-  where-    fp tl = toUTopLine tl-    fc p = toUPosting p---- | 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.pEntry)--infer ::-  U.Posting-  -> Ex.ExceptionalT Error-  (St.State (Maybe B.Entry)) Posting-infer po =-  case U.pEntry 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--toUPosting :: Posting -> U.Posting-toUPosting p = U.Posting-  { U.pPayee = pPayee p-  , U.pNumber = pNumber p-  , U.pFlag = pFlag p-  , U.pAccount = pAccount p-  , U.pTags = pTags p-  , U.pEntry = case pInferred p of-      Inferred -> Nothing-      NotInferred -> Just (pEntry p)-  , U.pMemo = pMemo p-  , U.pPostingLine = pPostingLine p-  , U.pGlobalPosting = pGlobalPosting p-  , U.pFilePosting = pFilePosting p-  }---toPosting :: U.Posting-             -> B.Entry-             -> Inferred-             -> Posting-toPosting u e i =-  Posting-  { pPayee    = U.pPayee u-  , pNumber   = U.pNumber u-  , pFlag     = U.pFlag u-  , pAccount  = U.pAccount u-  , pTags     = U.pTags u-  , pEntry    = e-  , pMemo     = U.pMemo u-  , pInferred = i-  , pPostingLine = U.pPostingLine u-  , pGlobalPosting = U.pGlobalPosting u-  , pFilePosting = U.pFilePosting u-  }---toUTopLine :: TopLine -> U.TopLine-toUTopLine t = U.TopLine-  { U.tDateTime = tDateTime t-  , U.tFlag     = tFlag t-  , U.tNumber   = tNumber t-  , U.tPayee    = tPayee t-  , U.tMemo     = tMemo t-  , U.tTopLineLine = tTopLineLine t-  , U.tTopMemoLine = tTopMemoLine t-  , U.tFilename = tFilename t-  , U.tGlobalTransaction = tGlobalTransaction t-  , U.tFileTransaction = tFileTransaction t-  }--toTopLine :: U.TopLine -> TopLine-toTopLine t = TopLine-  { tDateTime = U.tDateTime t-  , tFlag     = U.tFlag t-  , tNumber   = U.tNumber t-  , tPayee    = U.tPayee t-  , tMemo     = U.tMemo t-  , tTopLineLine = U.tTopLineLine t-  , tTopMemoLine = U.tTopMemoLine t-  , tFilename = U.tFilename t-  , tGlobalTransaction = U.tGlobalTransaction t-  , tFileTransaction = U.tFileTransaction t-  }--fromRPosting :: U.RPosting -> B.Entry -> Inferred -> Posting-fromRPosting u e i = Posting-  { pPayee    = U.rPayee u-  , pNumber   = U.rNumber u-  , pFlag     = U.rFlag u-  , pAccount  = U.rAccount u-  , pTags     = U.rTags u-  , pEntry    = e-  , pMemo     = U.rMemo u-  , pInferred = i-  , pPostingLine = U.rPostingLine u-  , pGlobalPosting = U.rGlobalPosting u-  , pFilePosting = U.rFilePosting u-  }--fromIPosting :: U.IPosting -> B.Entry -> Inferred -> Posting-fromIPosting u e i = Posting-  { pPayee    = U.iPayee u-  , pNumber   = U.iNumber u-  , pFlag     = U.iFlag u-  , pAccount  = U.iAccount u-  , pTags     = U.iTags u-  , pEntry    = e-  , pMemo     = U.iMemo u-  , pInferred = i-  , pPostingLine = U.iPostingLine u-  , pGlobalPosting = U.iGlobalPosting u-  , pFilePosting = U.iFilePosting u-  }--data RTransaction = RTransaction-  { rtCommodity :: B.Commodity-    -- ^ All postings will have this same commodity--  , rtSide :: Maybe B.Side-  -- ^ All commodities will be on this side of the amount--  , rtSpaceBetween :: Maybe B.SpaceBetween-  -- ^ All amounts will have this SpaceBetween--  , rtDrCr :: B.DrCr-  -- ^ All postings except the inferred one will have this DrCr--  , rtTopLine :: U.TopLine--  , rtPosting :: U.RPosting-  -- ^ You must have at least one posting whose quantity you specify--  , rtMorePostings :: [U.RPosting]-  -- ^ Optionally you can have additional restricted postings.--  , rtIPosting :: U.IPosting-  -- ^ And at least one posting whose quantity and DrCr will be inferred--  } deriving Show---- | Creates a @restricted transaction@; that is, one in which all the--- entries will have the same commodity, and in which all but one of--- the postings will all be debits or credits. The last posting will--- have no quantity specified at all and will be inferred. Creating--- these transactions never fails, in contrast to the transactions--- created by 'transaction', which can fail at runtime.-rTransaction :: RTransaction -> Transaction-rTransaction rt = Transaction (F.Family tl p1 p2 ps)-  where-    tl = toTopLine (rtTopLine rt)-    tot = foldl1 B.add $ (U.rQty . rtPosting $ rt)-                         : map U.rQty (rtMorePostings rt)-    sd = rtSide rt-    sb = rtSpaceBetween rt-    inf = fromIPosting (rtIPosting rt)-          ( B.Entry (B.opposite (rtDrCr rt))-            (B.Amount tot (rtCommodity rt) sd sb))-          Inferred-    toPstg p = fromRPosting p-               (B.Entry (rtDrCr rt)-               (B.Amount (U.rQty p) (rtCommodity rt) sd sb)) NotInferred-    p1 = toPstg . rtPosting $ rt-    (p2, ps) = case rtMorePostings rt of-      [] -> (inf, [])-      x:xs -> (toPstg x, (map toPstg xs) ++ [inf])---- | 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------------------------------------------------------------------ Changers------------------------------------------------------------------ | Each field in the record is a Maybe. If Nothing, make no change--- to this part of the TopLine.-data TopLineChangeData = TopLineChangeData-  { tcDateTime :: Maybe B.DateTime-  , tcFlag :: Maybe (Maybe B.Flag)-  , tcNumber :: Maybe (Maybe B.Number)-  , tcPayee :: Maybe (Maybe B.Payee)-  , tcMemo :: Maybe (Maybe B.Memo)-  , tcTopLineLine :: Maybe (Maybe B.TopLineLine)-  , tcTopMemoLine :: Maybe (Maybe B.TopMemoLine)-  , tcFilename :: Maybe (Maybe B.Filename)-  , tcGlobalTransaction :: Maybe (Maybe B.GlobalTransaction)-  , tcFileTransaction :: Maybe (Maybe B.FileTransaction)-  } deriving Show--emptyTopLineChangeData :: TopLineChangeData-emptyTopLineChangeData = TopLineChangeData-  { tcDateTime = Nothing-  , tcFlag = Nothing-  , tcNumber = Nothing-  , tcPayee = Nothing-  , tcMemo = Nothing-  , tcTopLineLine = Nothing-  , tcTopMemoLine = Nothing-  , tcFilename = Nothing-  , tcGlobalTransaction = Nothing-  , tcFileTransaction = Nothing-  }---applyTopLineChange :: TopLineChangeData -> TopLine -> TopLine-applyTopLineChange c t = TopLine-  { tDateTime = fromMaybe (tDateTime t) (tcDateTime c)-  , tFlag = fromMaybe (tFlag t) (tcFlag c)-  , tNumber = fromMaybe (tNumber t) (tcNumber c)-  , tPayee = fromMaybe (tPayee t) (tcPayee c)-  , tMemo = fromMaybe (tMemo t) (tcMemo c)-  , tTopLineLine = fromMaybe (tTopLineLine t) (tcTopLineLine c)-  , tTopMemoLine = fromMaybe (tTopMemoLine t) (tcTopMemoLine c)-  , tFilename = fromMaybe (tFilename t) (tcFilename c)-  , tGlobalTransaction = fromMaybe (tGlobalTransaction t)-                         (tcGlobalTransaction c)-  , tFileTransaction = fromMaybe (tFileTransaction t)-                       (tcFileTransaction c)-  }--data PostingChangeData = PostingChangeData-  { pcPayee :: Maybe (Maybe B.Payee)-  , pcNumber :: Maybe (Maybe B.Number)-  , pcFlag :: Maybe (Maybe B.Flag)-  , pcAccount :: Maybe B.Account-  , pcTags :: Maybe B.Tags-  , pcMemo :: Maybe (Maybe B.Memo)-  , pcSide :: Maybe (Maybe B.Side)-  , pcSpaceBetween :: Maybe (Maybe B.SpaceBetween)-  , pcPostingLine :: Maybe (Maybe B.PostingLine)-  , pcGlobalPosting :: Maybe (Maybe B.GlobalPosting)-  , pcFilePosting :: Maybe (Maybe B.FilePosting)-  } deriving Show--emptyPostingChangeData :: PostingChangeData-emptyPostingChangeData = PostingChangeData-  { pcPayee = Nothing-  , pcNumber = Nothing-  , pcFlag = Nothing-  , pcAccount = Nothing-  , pcTags = Nothing-  , pcMemo = Nothing-  , pcSide = Nothing-  , pcSpaceBetween = Nothing-  , pcPostingLine = Nothing-  , pcGlobalPosting = Nothing-  , pcFilePosting = Nothing-  }---applyPostingChange :: PostingChangeData -> Posting -> Posting-applyPostingChange c p = Posting-  { pPayee = fromMaybe (pPayee p) (pcPayee c)-  , pNumber = fromMaybe (pNumber p) (pcNumber c)-  , pFlag = fromMaybe (pFlag p) (pcFlag c)-  , pAccount = fromMaybe (pAccount p) (pcAccount c)-  , pTags = fromMaybe (pTags p) (pcTags c)-  , pEntry = en-  , pMemo = fromMaybe (pMemo p) (pcMemo c)-  , pInferred = pInferred p-  , pPostingLine = fromMaybe (pPostingLine p) (pcPostingLine c)-  , pGlobalPosting = fromMaybe (pGlobalPosting p) (pcGlobalPosting c)-  , pFilePosting = fromMaybe (pFilePosting p) (pcFilePosting c)-  }-  where-    enOld = pEntry p-    amOld = B.amount enOld-    en = B.Entry (B.drCr enOld) am-    am = B.Amount (B.qty amOld) (B.commodity amOld) sd sb-    sd = fromMaybe (B.side amOld) (pcSide c)-    sb = fromMaybe (B.spaceBetween amOld) (pcSpaceBetween c)---- | Allows you to change the parts of a transaction that can be--- chanaged without unbalancing the transaction. You cannot change the--- DrCr, Qty, or Commodity, as changing these might unbalance the--- transaction. If there are elements you do not want to change at--- all, use an 'emptyTopLineChangeData' or an 'emptyPostingChangeData'--- in the appropriate part of the Family that you pass in. If the--- Family of change data has more children than the transaction, these--- extra children are ignored. If the Family in the Transaction has--- more children than the Family of change data, the extra postings--- are unchanged. That is, 'changeTransaction' will never delete--- postings.-changeTransaction-  :: F.Family TopLineChangeData PostingChangeData-  -> Transaction-  -> Transaction-changeTransaction c (Transaction t) =-  let F.Family ctl cp1 cp2 cps = c-      F.Family tl p1 p2 ps = t-      tl' = applyTopLineChange ctl tl-      p1' = applyPostingChange cp1 p1-      p2' = applyPostingChange cp2 p2-      ps' = zipWith applyPostingChange-            (cps ++ repeat emptyPostingChangeData) ps-  in Transaction (F.Family tl' p1' p2' ps')-
− Penny/Lincoln/Transaction/Unverified.hs
@@ -1,131 +0,0 @@--- | Provides record types to hold the data that are in an unverified--- transaction. Use these records along with the functions in--- 'Penny.Lincoln.Transaction' to create Transactions. You can create--- a Transaction only if the postings are balanced.------ The functions that create transactions will fail at runtime if the--- postings are not balanced (this is impossible to enforce at compile--- time.) However, if you are creating a transaction in which both of--- the amounts have the same commodity, for which all but one of the--- entries will have the same DrCr, and in which one of the postings--- has no entry at all (that is, its entry will be inferred), then it--- is possible to create a transaction that is guaranteed to be--- balanced. Use the RPosting and IPosting types for this purpose. (It--- is necessary for all the postings except for the inferred one to--- have the same DrCr because otherwise it would be possible to create--- a transaction in which the inferred posting would have to have an--- entry with a quantity of zero, which is impossible.-module Penny.Lincoln.Transaction.Unverified where--import qualified Penny.Lincoln.Bits as B--data TopLine = TopLine-  { tDateTime :: B.DateTime-  , tFlag     :: Maybe B.Flag-  , tNumber   :: Maybe B.Number-  , tPayee    :: Maybe B.Payee-  , tMemo     :: Maybe B.Memo-  , tTopLineLine :: Maybe B.TopLineLine-  , tTopMemoLine :: Maybe B.TopMemoLine-  , tFilename :: Maybe B.Filename-  , tGlobalTransaction :: Maybe B.GlobalTransaction-  , tFileTransaction :: Maybe B.FileTransaction-  } deriving (Eq, Show)--emptyTopLine :: B.DateTime -> TopLine-emptyTopLine d = TopLine-  { tDateTime = d-  , tFlag = Nothing-  , tNumber = Nothing-  , tPayee = Nothing-  , tMemo = Nothing-  , tTopLineLine = Nothing-  , tTopMemoLine = Nothing-  , tFilename = Nothing-  , tGlobalTransaction = Nothing-  , tFileTransaction = Nothing-  }--data Posting = Posting-  { pPayee   :: Maybe B.Payee-  , pNumber  :: Maybe B.Number-  , pFlag    :: Maybe B.Flag-  , pAccount :: B.Account-  , pTags    :: B.Tags-  , pEntry   :: Maybe B.Entry-  , pMemo    :: Maybe B.Memo-  , pPostingLine :: Maybe B.PostingLine-  , pGlobalPosting :: Maybe B.GlobalPosting-  , pFilePosting :: Maybe B.FilePosting-  } deriving (Eq, Show)--emptyPosting :: B.Account -> Posting-emptyPosting a = Posting-  { pPayee = Nothing-  , pNumber = Nothing-  , pFlag = Nothing-  , pAccount = a-  , pTags = B.Tags []-  , pEntry = Nothing-  , pMemo = Nothing-  , pPostingLine = Nothing-  , pGlobalPosting = Nothing-  , pFilePosting = Nothing-  }---- | A @restricted posting@ in which only the quantity is specified;--- the commodity and DrCr are specified when the transaction is--- created.-data RPosting = RPosting-  { rPayee   :: Maybe B.Payee-  , rNumber  :: Maybe B.Number-  , rFlag    :: Maybe B.Flag-  , rAccount :: B.Account-  , rTags    :: B.Tags-  , rQty     :: B.Qty-  , rMemo    :: Maybe B.Memo-  , rPostingLine :: Maybe B.PostingLine-  , rGlobalPosting :: Maybe B.GlobalPosting-  , rFilePosting :: Maybe B.FilePosting-  } deriving (Eq, Show)--emptyRPosting :: B.Account -> B.Qty -> RPosting-emptyRPosting a q = RPosting-  { rPayee = Nothing-  , rNumber = Nothing-  , rFlag = Nothing-  , rAccount = a-  , rTags = B.Tags []-  , rQty = q-  , rMemo = Nothing-  , rPostingLine = Nothing-  , rGlobalPosting = Nothing-  , rFilePosting = Nothing-  }---- | An @inferred posting@ in which no quantity is specified.-data IPosting = IPosting-  { iPayee   :: Maybe B.Payee-  , iNumber  :: Maybe B.Number-  , iFlag    :: Maybe B.Flag-  , iAccount :: B.Account-  , iTags    :: B.Tags-  , iMemo    :: Maybe B.Memo-  , iPostingLine :: Maybe B.PostingLine-  , iGlobalPosting :: Maybe B.GlobalPosting-  , iFilePosting :: Maybe B.FilePosting-  } deriving (Eq, Show)--emptyIPosting :: B.Account -> IPosting-emptyIPosting a = IPosting-  { iPayee = Nothing-  , iNumber = Nothing-  , iFlag = Nothing-  , iAccount = a-  , iTags = B.Tags []-  , iMemo = Nothing-  , iPostingLine = Nothing-  , iGlobalPosting = Nothing-  , iFilePosting = Nothing-  }-
− Penny/Shield.hs
@@ -1,105 +0,0 @@--- | 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),-  Runtime,-  environment,-  currentTime,-  output,-  screenLines,-  screenWidth,-  term,-  runtime,-  termFromEnv,-  autoTerm)-  where--import Control.Applicative ((<$>), (<*>))-import qualified Data.Time as T-import System.Environment (getEnvironment)-import System.IO (hIsTerminalDevice, stdout)-import qualified System.Console.Rainbow as C--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 deriving (Eq, Ord, Show)--newtype Term = Term { unTerm :: String } deriving Show---- | Information about the runtime environment.-data Runtime = Runtime { environment :: [(String, String)]-                       , currentTime :: B.DateTime-                       , output :: Output }--runtime :: IO Runtime-runtime = Runtime-          <$> getEnvironment-          <*> (toDT <$> T.getZonedTime)-          <*> findOutput-          where-            toDT t = case B.fromZonedTime t of-              Nothing -> error "time conversion error"-              Just ti -> ti--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---- | Determines which Chunk Term to use based on the TERM environment--- variable, regardless of whether standard output is a terminal. Uses--- Dumb if TERM is not set.-termFromEnv :: Runtime -> C.Term-termFromEnv rt = case term rt of-  Just t -> C.TermName . unTerm $ t-  Nothing -> C.Dumb---- | Determines which Chunk Term to use based on whether standard--- output is a terminal. Uses Dumb if standard output is not a--- terminal; otherwise, uses the TERM environment variable.-autoTerm :: Runtime -> C.Term-autoTerm rt = case output rt of-  IsTTY -> termFromEnv rt-  NotTTY -> C.Dumb-
− Penny/Steel.hs
@@ -1,3 +0,0 @@--- | Steel - independent Penny utilities--module Penny.Steel where
− Penny/Steel/NestedMap.hs
@@ -1,275 +0,0 @@--- | 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.Steel.NestedMap (-  NestedMap ( NestedMap, unNestedMap ),-  empty,-  relabel,-  descend,-  insert,-  cumulativeTotal,-  traverse,-  traverseWithTrail,-  toForest ) where--import Control.Applicative ((<*>), (<$>))-import Data.Map ( Map )-import qualified Data.Foldable as F-import qualified Data.Traversable as T-import qualified Data.Tree as E-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'))---- | Convert a NestedMap to a Forest.-toForest :: Ord k => NestedMap k l -> E.Forest (k, l)-toForest = map toNode . M.assocs . unNestedMap-  where-    toNode (k, (l, m)) = E.Node (k, l) (toForest 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/Wheat.hs
@@ -1,365 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Wheat - Penny ledger tests------ Wheat helps you build tests to check all the postings in your--- ledger. Perhaps you want to make sure all the account names are--- valid, or that your checking account has no unreconciled--- transactions. With Wheat you can easily build a command line--- program that will check all the postings in a ledger for you--- against criteria that you specify.--module Penny.Wheat-  ( -- * Configuration-    WheatConf(..)--    -- * Tests-  , eachPostingMustBeTrue-  , atLeastNPostings--    -- * Convenience functions-  , futureFirstsOfTheMonth--    -- * Running tests-  , main-  ) where--import Control.Monad (when)-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.Either (partitionEithers)-import Data.Maybe (mapMaybe)-import qualified Penny.Copper as Cop-import qualified Penny.Copper.Parsec as CP-import qualified Penny.Lincoln as L-import qualified Penny.Liberty as Ly-import qualified Data.Text as X-import qualified Data.Time as Time-import qualified Text.Matchers as M-import qualified Text.Parsec as Parsec-import qualified System.Exit as Exit-import qualified System.IO as IO-import qualified Penny.Shield as S--import qualified Data.Version as V-import qualified Data.Prednote.TestTree as TT-import qualified Data.Prednote.Pdct as Pe-import qualified System.Console.Rainbow as Rb-import qualified System.Console.MultiArg as MA-import System.Locale (defaultTimeLocale)----------------------------------------------------------------- Other conveniences------------------------------------------------------------------ | A non-terminating list of starting with the first day of the--- first month following the given day, followed by successive first--- days of the month.-futureFirstsOfTheMonth :: Time.Day -> [Time.Day]-futureFirstsOfTheMonth d = iterate (Time.addGregorianMonthsClip 1) d1-  where-    d1 = Time.fromGregorian yr mo 1-    (yr, mo, _) = Time.toGregorian $ Time.addGregorianMonthsClip 1 d----------------------------------------------------------------- CLI----------------------------------------------------------------- | Record holding all data to configure Wheat.-data WheatConf = WheatConf-  { briefDescription :: String-    -- ^ This is displayed at the beginning of the online help. It-    -- should be a one-line description of what this program does--for-    -- example, what it checks for.--  , moreHelp :: [String]-    -- ^ Displayed at the end of the online help. It should be a list-    -- of lines, wich each line not terminated by a newline-    -- character. It is displayed at the end of the online help.--  , tests :: [Time.UTCTime -> TT.TestTree L.PostFam]-    -- ^ The actual tests to run. The UTCTime is the @base time@. Each-    -- test may decide what to do with the base time--for example, the-    -- test might say that all postings have to have a date on or-    -- before that date. Or the test might just ignore the base time.--  , indentAmt :: Pe.IndentAmt-    -- ^ How many spaces to indent each level in a tree of tests.--  , passVerbosity :: TT.Verbosity-    -- ^ Verbosity for tests that pass--  , failVerbosity :: TT.Verbosity-    -- ^ Verbosity for tests that fail--  , groupPred :: TT.Name -> Bool-    -- ^ Group names are filtered with this function; a group is only-    -- run if this function returns True.--  , testPred :: TT.Name -> Bool-    -- ^ Test names are filtered with this function; a test is only-    -- run if this function returns True.--  , showSkippedTests :: Bool-    -- ^ Some tests might be skipped; see 'testPred'. This controls-    -- whether you want to see a notification of tests that were-    -- skipped. (Does not affect skipped groups; see 'groupVerbosity'-    -- for that.)--  , groupVerbosity :: TT.GroupVerbosity-    -- ^ Show group names? Even if you do not show the names of-    -- groups, tests within the group will still be indented.--  , stopOnFail :: Bool-    -- ^ If True, then tests will stop running immediately after a-    -- single test fails. If False, all tests are always run.--  , colorToFile :: Bool-    -- ^ Use colors even if stdout is not a file?--  , baseTime :: Time.UTCTime-    -- ^ Tests may use this date and time as they wish; see-    -- 'tests'. Typically you will set this to the current instant.--  , ledgers :: [String]-    -- ^ Ledger files to read in from disk.-  }--data Parsed = Parsed-  { p_indentAmt :: Pe.IndentAmt-  , p_passVerbosity :: TT.Verbosity-  , p_failVerbosity :: TT.Verbosity-  , p_groupPred :: TT.Name -> Bool-  , p_testPred :: TT.Name -> Bool-  , p_showSkippedTests :: Bool-  , p_groupVerbosity :: TT.GroupVerbosity-  , p_stopOnFail :: Bool-  , p_colorToFile :: Bool-  , p_baseTime :: Time.UTCTime-  , p_help :: Bool-  , p_ledgers :: [String]-  }--parseBaseTime :: String -> Ex.Exceptional MA.OptArgError Time.UTCTime-parseBaseTime s = case Parsec.parse CP.dateTime  "" (X.pack s) of-  Left e -> Ex.throw (MA.ErrorMsg $ "could not parse date: " ++ show e)-  Right g -> return . L.toUTC $ g--parseRegexp :: String -> Ex.Exceptional MA.OptArgError (TT.Name -> Bool)-parseRegexp s = case M.pcre M.Sensitive (X.pack s) of-  Ex.Exception e -> Ex.throw . MA.ErrorMsg $-    "could not parse regular expression: " ++ X.unpack e-  Ex.Success m -> return . M.match $ m--parseArg :: String -> Parsed -> Parsed-parseArg s p = p { p_ledgers = p_ledgers p ++ [s] }--allOpts :: [MA.OptSpec (Parsed -> Parsed)]-allOpts =-  let allChoices =-        [ ("silent", \p -> p { p_failVerbosity = TT.Silent })-        , ("minimal", \p -> p { p_failVerbosity = TT.PassFail })-        , ("false", \p -> p { p_failVerbosity = TT.FalseSubjects })-        , ("true", \p -> p { p_failVerbosity = TT.TrueSubjects })-        , ("all", \p -> p { p_failVerbosity = TT.Discards })-        ] in-  [ MA.OptSpec ["indentation"] "i"-    (fmap (\i p -> p { p_indentAmt = i }) (MA.OneArgE MA.reader))--  , MA.OptSpec ["pass-verbosity"] "p" $ MA.ChoiceArg allChoices--  , MA.OptSpec ["fail-verbosity"] "f" $ MA.ChoiceArg allChoices--  , MA.OptSpec ["group-regexp"] "g"-    (fmap (\f p -> p { p_groupPred = f }) (MA.OneArgE parseRegexp))--  , MA.OptSpec ["test-regexp"] "t"-    (fmap (\f p -> p { p_testPred = f }) (MA.OneArgE parseRegexp))--  , MA.OptSpec ["show-skipped-tests"] ""-    ( MA.NoArg (\p -> p { p_showSkippedTests-                          = not (p_showSkippedTests p) }))--  , MA.OptSpec ["group-verbosity"] "G" $ MA.ChoiceArg-    [ ("silent", \p -> p { p_groupVerbosity = TT.NoGroups })-    , ("active", \p -> p { p_groupVerbosity = TT.ActiveGroups })-    , ("all", \p -> p { p_groupVerbosity = TT.AllGroups })-    ]--  , MA.OptSpec ["stop-on-failure"] ""-    ( MA.NoArg (\p -> p { p_stopOnFail-                          = not (p_stopOnFail p) }))--  , MA.OptSpec ["color-to-file"] ""-    ( MA.NoArg (\p -> p { p_colorToFile-                          = not (p_colorToFile p) }))--  , MA.OptSpec ["base-date"] ""-    (fmap (\d p -> p { p_baseTime = d }) (MA.OneArgE parseBaseTime))-  ]--getTTOpts :: [a] -> Parsed -> TT.TestOpts a-getTTOpts as o = TT.TestOpts-  { TT.tIndentAmt = p_indentAmt o-  , TT.tPassVerbosity = p_passVerbosity o-  , TT.tFailVerbosity = p_failVerbosity o-  , TT.tGroupPred = p_groupPred o-  , TT.tTestPred = p_testPred o-  , TT.tShowSkippedTests = p_showSkippedTests o-  , TT.tGroupVerbosity = p_groupVerbosity o-  , TT.tSubjects = as-  , TT.tStopOnFail = p_stopOnFail o-  }---- | Runs Wheat tests. Prints the result to standard output. Exits--- unsuccessfully if the user gave bad command line options or if at--- least a single test failed; exits successfully if all tests--- succeeded. Shows the version number and exits successfully if that--- was requested.-main-  :: V.Version-  -- ^ Version of the binary-  -> (S.Runtime -> WheatConf) -> IO ()-main ver getWc = do-  rt <- S.runtime-  let wc = getWc rt-  parsed <- MA.simpleWithHelp (help wc) MA.Intersperse-         (fmap Left (Ly.version ver) : (map (fmap Right) allOpts))-         (fmap Right parseArg)-  let (showVers, fns) = partitionEithers parsed-  case showVers of-    [] -> return ()-    x:_ -> x-  let fn = foldl (flip (.)) id fns-      psd = fn (getParsedFromWheatConf wc)-  term <- Rb.smartTermFromEnv (p_colorToFile psd) IO.stdout-  pfs <- getItems (p_ledgers psd)-  let ttOpts = getTTOpts pfs psd-      tts = zipWith ($) (tests wc) (repeat (p_baseTime psd))-      (cks, _, nFail) = TT.runTests ttOpts 0 tts-  Rb.printChunks term cks-  when (nFail > 0) Exit.exitFailure--getParsedFromWheatConf :: WheatConf -> Parsed-getParsedFromWheatConf w = Parsed-  { p_indentAmt = indentAmt w-  , p_passVerbosity = passVerbosity w-  , p_failVerbosity = failVerbosity w-  , p_groupPred = groupPred w-  , p_testPred = testPred w-  , p_showSkippedTests = showSkippedTests w-  , p_groupVerbosity = groupVerbosity w-  , p_stopOnFail = stopOnFail w-  , p_colorToFile = colorToFile w-  , p_baseTime = baseTime w-  , p_help = False-  , p_ledgers = ledgers w-  }--getItems :: [String] -> IO [L.PostFam]-getItems ss = fmap f $ Cop.open ss-  where-    f = concatMap L.postFam . mapMaybe toTxn . Cop.unLedger-    toTxn i = case i of { Cop.Transaction x -> Just x; _ -> Nothing }------- Tests------- | Passes only if each posting is True.-eachPostingMustBeTrue-  :: TT.Name-  -> Pe.Pdct L.PostFam-  -> TT.TestTree L.PostFam-eachPostingMustBeTrue n = TT.eachSubjectMustBeTrue n L.display---- | Passes if at least a particular number of postings is True.-atLeastNPostings-  :: Int-  -- ^ The number of postings that must be true for the test to pass-  -> TT.Name-  -> Pe.Pdct L.PostFam-  -> TT.TestTree L.PostFam-atLeastNPostings i n = TT.nSubjectsMustBeTrue n L.display i------- Help-----help-  :: WheatConf-  -> String-  -- ^ Program name-  -> String-help wc pn = unlines-  [ "usage: " ++ pn ++ " [options] [FILE...]"-  , ""-  , briefDescription wc-  , ""-  , "Options:"-  , "  -i, --indentation AMT"-  , "    Indent each level by this many spaces"-  , "    " ++ dflt (show . indentAmt $ wc)-  , "  -p, --pass-verbosity VERBOSITY"-  , "    Verbosity for tests that pass. Argument may be:"-  , "      silent - show nothing at all"-  , "      minimal - show whether the test passed or failed"-  , "      false - show subjects that are false"-  , "      true - show subjects that are true or false"-  , "      all - show all subjects"-  , "      " ++ dflt (showVerbosity . passVerbosity $ wc)-  , "  -f, --fail-verbosity VERBOSITY"-  , "    Verbosity for tests that fail."-  , "    (uses same VERBOSITY options as --pass-verbosity)"-  , "    " ++ dflt (showVerbosity . failVerbosity $ wc)-  , "  -g, --group-regexp REGEXP"-  , "    Run only groups whose name matches the given"-  , "    Perl-compatible regular expression"-  , "    (overrides the compiled-in default)"-  , "  -t, --test-regexp REGEXP"-  , "    Run only tests whose name matches the given"-  , "    Perl-compatible regular expression"-  , "    (overrides the compiled-in default)"-  , "  --show-skipped-tests"-  , "    Toggle whether to show tests that are skipped"-  , "    using the --test-regexp option"-  , "    (does not affect groups that are skipped; see next option)"-  , "    " ++ dflt (show . showSkippedTests $ wc)-  , "  --G, group-verbosity ARG"-  , "    Control which group names are shown. Argument may be:"-  , "      silent - do not show any group names"-  , "      active - show group names that were not skipped"-  , "      all - show all group names, including skipped ones"-  , "      " ++ dflt (showGroupVerbosity . groupVerbosity $ wc)-  , "  --stop-on-failure"-  , "    Stop running tests after a single test fails"-  , "    " ++ dflt (show . stopOnFail $ wc)-  , "  --color-to-file"-  , "    Use color even when standard output is not a terminal"-  , "    " ++ dflt (show . colorToFile $ wc)-  , "  --base-date DATE"-  , "    Use this date as a basis for checks"-  , "    " ++ dflt ( Time.formatTime defaultTimeLocale "%c"-                     . baseTime $ wc)-  , ""-  ]-  ++ unlines (moreHelp wc)--dflt :: String -> String-dflt s = "(default: " ++ s ++ ")"--showVerbosity :: TT.Verbosity -> String-showVerbosity v = case v of-  TT.Silent -> "silent"-  TT.PassFail -> "minimal"-  TT.FalseSubjects -> "false"-  TT.TrueSubjects -> "true"-  TT.Discards -> "all"--showGroupVerbosity :: TT.GroupVerbosity -> String-showGroupVerbosity v = case v of-  TT.NoGroups -> "silent"-  TT.ActiveGroups -> "active"-  TT.AllGroups -> "all"--
− Penny/Zinc.hs
@@ -1,747 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Zinc - the Penny command-line interface-module Penny.Zinc-  ( Defaults(..)-  , ColorToFile(..)-  , Matcher(..)-  , SortField(..)-  , runZinc-  ) where--import qualified Penny.Cabin.Interface as I-import qualified Penny.Cabin.Parsers as P-import qualified Penny.Cabin.Scheme as E-import qualified Penny.Cabin.Scheme.Schemes as Schemes-import qualified Penny.Copper as C-import qualified Penny.Liberty as Ly-import qualified Data.Prednote.Expressions as X-import qualified Data.Prednote.Pdct as Pe-import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Queries as Q-import qualified Penny.Shield as S--import Control.Applicative ((<*>), pure, (<$))-import Control.Monad (join)-import qualified Control.Monad.Trans.State as St-import qualified Control.Monad.Exception.Synchronous as Ex-import Data.Char (toUpper, toLower)-import Data.Either (partitionEithers)-import Data.List (isPrefixOf)-import Data.Maybe (mapMaybe, catMaybes, fromMaybe)-import Data.Monoid (mappend, mconcat, (<>))-import Data.Ord (comparing)-import Data.Text (Text, pack)-import Data.Version (Version)-import qualified Data.Text.IO as TIO-import qualified System.Console.MultiArg as MA-import qualified System.Exit as Exit-import qualified System.IO as IO-import qualified Text.Matchers as M-import qualified System.Console.Rainbow as R--runZinc-  :: Version-  -- ^ Version of the executable-  -> Defaults-  -> S.Runtime-  -> [I.Report]-  -> IO ()-runZinc ver df rt rs = do-  let ord = sortPairsToFn . sorter $ df-      hlp = helpText df rt rs-  join $ MA.modesWithHelp hlp (allOpts ver (S.currentTime rt) df)-    (processGlobal rt ord df rs)----- | Whether to use color when standard output is not a terminal.-newtype ColorToFile = ColorToFile { unColorToFile :: Bool }-  deriving (Eq, Show)--data Matcher-  = Within-  | Exact-  | TDFA-  | PCRE-  deriving (Eq, Show)--data SortField-  = Payee-  | Date-  | Flag-  | Number-  | Account-  | DrCr-  | Qty-  | Commodity-  | PostingMemo-  | TransactionMemo-  deriving (Eq, Show, Ord)--data Defaults = Defaults-  { sensitive :: M.CaseSensitive-  , matcher :: Matcher-  , colorToFile :: ColorToFile-  , defaultScheme :: Maybe E.Scheme-    -- ^ If Nothing, no default scheme. If the user does not pick a-    -- scheme, no colors are used.-  , moreSchemes :: [E.Scheme]-  , sorter :: [(SortField, P.SortOrder)]-    -- ^ For example, to sort by date and then by payee if the dates-    -- are equal, use-    ---    -- > [(Date, Ascending), (Payee, Ascending)]--  , exprDesc :: X.ExprDesc-  }--sortPairToFn :: (SortField, P.SortOrder) -> Orderer-sortPairToFn (s, d) = if d == P.Descending then flipOrder r else r-  where-    r = case s of-      Payee -> comparing Q.payee-      Date -> comparing Q.dateTime-      Flag -> comparing Q.flag-      Number -> comparing Q.number-      Account -> comparing Q.account-      DrCr -> comparing Q.drCr-      Qty -> comparing Q.qty-      Commodity -> comparing Q.commodity-      PostingMemo -> comparing Q.postingMemo-      TransactionMemo -> comparing Q.transactionMemo--descPair :: (SortField, P.SortOrder) -> String-descPair (i, d) = desc ++ ", " ++ dir-  where-    dir = case d of-      P.Ascending -> "ascending"-      P.Descending -> "descending"-    desc = case show i of-      [] -> []-      x:xs -> toLower x : xs--descSortList :: [(SortField, P.SortOrder)] -> [String]-descSortList ls = case ls of-  [] -> ["    No sorting performed by default"]-  x:xs -> descFirst x : map descRest xs--descFirst :: (SortField, P.SortOrder) -> String-descFirst p = "  Default sort order: " ++ descPair p--descRest :: (SortField, P.SortOrder) -> String-descRest p = "    then: " ++ descPair p--sortPairsToFn :: [(SortField, P.SortOrder)] -> Orderer-sortPairsToFn = mconcat . map sortPairToFn------- ## Option parsing---------- ## OptResult, and functions dealing with it----newtype ShowExpression = ShowExpression Bool-  deriving (Show, Eq)--newtype VerboseFilter = VerboseFilter Bool-  deriving (Show, Eq)--type Error = Text--data OptResult-  = ROperand (M.CaseSensitive-             -> Ly.MatcherFactory-             -> Ex.Exceptional Ly.Error Ly.Operand)-  | RPostFilter (Ex.Exceptional Ly.Error Ly.PostFilterFn)-  | RMatcherSelect Ly.MatcherFactory-  | RCaseSelect M.CaseSensitive-  | ROperator (X.Token L.PostFam)-  | RSortSpec (Ex.Exceptional Error Orderer)-  | RColorToFile ColorToFile-  | RScheme E.Changers-  | RExprDesc X.ExprDesc-  | RShowExpression-  | RVerboseFilter-  | RShowVersion (IO ())--getPostFilters-  :: [OptResult]-  -> Ex.Exceptional Ly.Error [Ly.PostFilterFn]-getPostFilters =-  sequence-  . mapMaybe f-  where-    f o = case o of-      RPostFilter pf -> Just pf-      _ -> Nothing--getExprDesc-  :: Defaults-  -> [OptResult]-  -> X.ExprDesc-getExprDesc df os = case mapMaybe f os of-  [] -> exprDesc df-  xs -> last xs-  where-    f (RExprDesc d) = Just d-    f _ = Nothing--getSortSpec-  :: Orderer-  -> [OptResult]-  -> Ex.Exceptional Error Orderer-getSortSpec i ls =-  let getSpec o = case o of-        RSortSpec x -> Just x-        _ -> Nothing-      exSpecs = mapMaybe getSpec ls-  in if null exSpecs-     then return i-     else fmap mconcat . sequence $ exSpecs--getShowVersion :: [OptResult] -> Maybe (IO ())-getShowVersion ls = case mapMaybe f ls of-  [] -> Nothing-  xs -> Just $ last xs-  where-    f o = case o of { RShowVersion i -> Just i; _ -> Nothing }--type Factory = M.CaseSensitive-             -> Text -> Ex.Exceptional Text M.Matcher--makeToken-  :: OptResult-  -> St.State (M.CaseSensitive, Factory)-              (Maybe (Ex.Exceptional Ly.Error (X.Token L.PostFam)))-makeToken o = case o of-  ROperand f -> do-    (s, fty) <- St.get-    let g = fmap X.operand (f s fty)-    return (Just g)-  RMatcherSelect f -> do-    (c, _) <- St.get-    St.put (c, f)-    return Nothing-  RCaseSelect c -> do-    (_, f) <- St.get-    St.put (c, f)-    return Nothing-  ROperator t -> return . Just . return $ t-  _ -> return Nothing---makeTokens-  :: Defaults-  -> [OptResult]-  -> Ex.Exceptional Ly.Error ( [X.Token L.PostFam]-                             , (M.CaseSensitive, Factory) )-makeTokens df os =-  let initSt = (sensitive df, fty)-      fty = case matcher df of-        Within -> \c t -> return (M.within c t)-        Exact -> \c t -> return (M.exact c t)-        TDFA -> M.tdfa-        PCRE -> M.pcre-      lsSt = mapM makeToken os-      (ls, st') = St.runState lsSt initSt-  in fmap (\xs -> (xs, st')) . sequence . catMaybes $ ls---allOpts :: Version -> L.DateTime -> Defaults -> [MA.OptSpec OptResult]-allOpts ver dt df =-  map (fmap ROperand) (Ly.operandSpecs dt)-  ++ [fmap RPostFilter . fst $ Ly.postFilterSpecs]-  ++ [fmap RPostFilter . snd $ Ly.postFilterSpecs]-  ++ map (fmap RMatcherSelect) Ly.matcherSelectSpecs-  ++ map (fmap RCaseSelect) Ly.caseSelectSpecs-  ++ map (fmap ROperator) Ly.operatorSpecs-  ++ [fmap RSortSpec sortSpecs]-  ++ [ optColorToFile ]-  ++ let ss = moreSchemes df-     in (if not . null $ ss then [optScheme ss] else [])-  ++ map (fmap RExprDesc) Ly.exprDesc-  ++ [ RShowExpression <$ Ly.showExpression-     , RVerboseFilter <$ Ly.verboseFilter-     , fmap RShowVersion (Ly.version ver)-     ]--optColorToFile :: MA.OptSpec OptResult-optColorToFile = MA.OptSpec ["color-to-file"] "" (MA.ChoiceArg ls)-  where-    ls = [ ("yes", RColorToFile $ ColorToFile True)-         , ("no", RColorToFile $ ColorToFile False) ]--getColorToFile :: Defaults -> [OptResult] -> ColorToFile-getColorToFile d ls =-  case mapMaybe getOpt ls of-    [] -> colorToFile d-    xs -> last xs-  where-    getOpt o = case o of-      RColorToFile c -> Just c-      _ -> Nothing--optScheme :: [E.Scheme] -> MA.OptSpec OptResult-optScheme ss = MA.OptSpec ["scheme"] "" (MA.ChoiceArg ls)-  where-    ls = map f ss-    f (E.Scheme n _ s) = (n, RScheme s)--getScheme :: Defaults -> [OptResult] -> Maybe E.Changers-getScheme d ls =-  case mapMaybe getOpt ls of-    [] -> fmap E.changers $ defaultScheme d-    xs -> Just $ last xs-  where-    getOpt o = case o of-      RScheme s -> Just s-      _ -> Nothing--getShowExpression :: [OptResult] -> ShowExpression-getShowExpression ls = case mapMaybe f ls of-  [] -> ShowExpression False-  _ -> ShowExpression True-  where-    f o = case o of { RShowExpression -> Just (); _ -> Nothing }--getVerboseFilter :: [OptResult] -> VerboseFilter-getVerboseFilter ls = case mapMaybe f ls of-  [] -> VerboseFilter False-  _ -> VerboseFilter True-  where-    f o = case o of { RVerboseFilter -> Just (); _ -> Nothing }---- | Indicates the result of a successful parse of filtering options.-data FilterOpts = FilterOpts-  { foResultFactory :: Factory-    -- ^ The factory indicated, so that it can be used in-    -- subsequent parses of the same command line.--  , foResultSensitive :: M.CaseSensitive-    -- ^ Indicated case sensitivity, so that it can be used in-    -- subsequent parses of the command line.--  , foSorterFilterer :: [L.Transaction]-                    -> ([R.Chunk], [L.Box Ly.LibertyMeta])-    -- ^ Applied to a list of Transaction, will sort and filter-    -- the transactions and assign them LibertyMeta.--  , foTextSpecs :: Maybe E.Changers--  , foColorToFile :: ColorToFile-  , foExprDesc :: X.ExprDesc-  , foPredicate :: Pe.Pdct L.PostFam-  , foShowExpression :: ShowExpression-  , foVerboseFilter :: VerboseFilter-  }--processGlobal-  :: S.Runtime-  -> Orderer-  -> Defaults-  -> [I.Report]-  -> [OptResult]-  -> Either (a -> IO ()) [MA.Mode (IO ())]-processGlobal rt srt df rpts os-  = case processFiltOpts srt df os of-      Ex.Exception s -> Left $ (const $ handleTextError s)-      Ex.Success mayFo -> case mayFo of-        Left i -> Left . const $ i-        Right fo -> Right $ map (makeMode rt fo) rpts--processFiltOpts-  :: Orderer-  -> Defaults-  -> [OptResult]-  -> Ex.Exceptional Error (Either (IO ()) FilterOpts)-  -- ^ Left if the user asked to see the version; Right with the-  -- FilterOpts otherwise.-processFiltOpts ord df os = case getShowVersion os of-  Just i -> return $ Left i-  Nothing -> do-    postFilts <- getPostFilters os-    sortSpec <- getSortSpec ord os-    (toks, (rs, rf)) <- makeTokens df os-    let ctf = getColorToFile df os-        sch = getScheme df os-        expDsc = getExprDesc df os-        showExpr = getShowExpression os-        verbFilt = getVerboseFilter os-    pdct <- Ly.parsePredicate expDsc toks-    let sf = Ly.xactionsToFiltered pdct postFilts sortSpec-    return . Right $ FilterOpts rf rs sf sch-                                ctf expDsc pdct showExpr verbFilt--makeMode-  :: S.Runtime-  -> FilterOpts-  -> I.Report-  -> MA.Mode (IO ())-makeMode rt fo r = fmap makeIO mode-  where-    mode = snd (r rt) (foResultSensitive fo) (foResultFactory fo)-           (fromMaybe Schemes.plainLabels . foTextSpecs $ fo)-           (foExprDesc fo) (fmap snd (foSorterFilterer fo))-    makeIO parseResult = do-      (posArgs, printRpt) <--        Ex.switch handleTextError return parseResult-      (txns, pps) <- fmap splitLedger $ C.open posArgs-      let term = if unColorToFile (foColorToFile fo)-                 then S.termFromEnv rt-                 else S.autoTerm rt-          printer = R.printChunks term-          verbFiltChunks = fst . foSorterFilterer fo $ txns-      showFilterExpression printer (foShowExpression fo) (foPredicate fo)-      showVerboseFilter printer (foVerboseFilter fo) verbFiltChunks-      Ex.switch handleTextError (R.printChunks term)-        $ printRpt txns pps---handleTextError :: Text -> IO a-handleTextError x = do-  pn <- MA.getProgName-  TIO.hPutStr IO.stderr $ (pack pn) <> ": error: " <> x-  Exit.exitFailure--indentAmt :: Pe.IndentAmt-indentAmt = 4--blankLine :: R.Chunk-blankLine = R.plain "\n"--showFilterExpression-  :: ([R.Chunk] -> IO ())-  -> ShowExpression-  -> Pe.Pdct L.PostFam-  -> IO ()-showFilterExpression ptr (ShowExpression se) pdct =-  if not se-  then return ()-  else ptr $ info : blankLine :-             (Pe.showPdct indentAmt 0 pdct ++ [blankLine])-  where-    info = R.plain "Posting filter expression:\n"--showVerboseFilter-  :: ([R.Chunk] -> IO ())-  -> VerboseFilter-  -> [R.Chunk]-  -> IO ()-showVerboseFilter ptr (VerboseFilter vb) cks =-  if not vb-  then return ()-  else ptr $ info : blankLine : (cks ++ [blankLine])-  where-    info = R.plain "Filtering information:\n"---- | Splits a Ledger into its Transactions and PricePoints.-splitLedger :: C.Ledger -> ([L.Transaction], [L.PricePoint])-splitLedger = partitionEithers . mapMaybe toEither . C.unLedger-  where-    toEither i = case i of-      C.Transaction t -> Just $ Left t-      C.PricePoint p -> Just $ Right p-      _ -> Nothing--helpText-  :: Defaults-  -> S.Runtime-  -> [I.Report]-  -> String-  -> String-helpText df rt pairMakers pn =-  mappend (help df pn) . mconcat . map addHdr . fmap fst $ pairs-  where-    pairs = pairMakers <*> pure rt-    addHdr s = hdr ++ s-    hdr = unlines [ "", replicate 50 '=' ]------------------------------------------------------------------ ## Sorting----------------------------------------------------------------- The monoid instance of Ordering takes the first non-EQ item. For--- example:------ mconcat [EQ, LT, GT] == LT.------ If b is a monoid, then (a -> b) is also a monoid. Therefore (a -> a--- -> Ordering) is also a monoid. So for example to compare the first--- element of a pair and then by the second element only if the first--- element is equal:------ mconcat [comparing fst, comparing snd]--type Orderer = L.PostFam -> L.PostFam -> Ordering--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", comparing Q.payee)-  , ("date", comparing Q.dateTime)-  , ("flag", comparing Q.flag)-  , ("number", comparing Q.number)-  , ("account", comparing Q.account)-  , ("drCr", comparing Q.drCr)-  , ("qty", comparing Q.qty)-  , ("commodity", comparing Q.commodity)-  , ("postingMemo", comparing Q.postingMemo)-  , ("transactionMemo", comparing Q.transactionMemo) ]--ords :: [(String, Orderer)]-ords = ordPairs ++ uppers ++ [none] where-  uppers = map toReversed ordPairs-  toReversed (s, f) =-    (capitalizeFirstLetter s, flipOrder f)-  none = ("none", const . const $ EQ)----- | True if the first argument matches the second argument. The match--- on the first letter is case sensitive; the match on the other--- letters is not case sensitive. True if both strings are empty.-argMatch :: String -> String -> Bool-argMatch s1 s2 = case (s1, s2) of-  (x:xs, y:ys) ->-    (x == y) && ((map toUpper xs) `isPrefixOf` (map toUpper ys))-  _ -> True--sortSpecs :: MA.OptSpec (Ex.Exceptional Error Orderer)-sortSpecs = MA.OptSpec ["sort"] ['s'] (MA.OneArg f)-  where-    f a =-      let matches = filter (\p -> a `argMatch` (fst p)) ords-      in case matches of-        x:[] -> return $ snd x-        _ -> Ex.throw $ "bad sort specification: " <> pack a <> "\n"------------------------------------------------------------------- ## Help---------------------------------------------------------------help :: Defaults -> String -> String-help d pn = unlines $-  [ "usage: " ++ pn ++ " [posting filters] report [report options] file . . ."-  , ""-  , "Posting filters"-  , "------------------------------------------"-  , ""-  , "Dates"-  , "-----"-  , ""-  , "-d, --date 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"-  , "-n, --number pattern"-  , "  Number must match pattern"-  , "-f, --flag pattern"-  , "  Flag must match pattern"-  , "-y, --commodity pattern"-  , "  Pattern must match commodity name"-  , "--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"-  , "-q, --qty cmp number"-  , "  Entry quantity must fall within given range"-  , "--filename pattern"-  , "  Filename of posting must match pattern"-  , ""-  , "Filtering based upon sibling postings"-  , "-------------------------------------"-  , "--s-globalPosting"-  , "--s-revGlobalPosting"-  , "--s-filePosting"-  , "--s-revFilePosting"-  , "--s-account"-  , "--s-account-level"-  , "--s-account-any"-  , "--s-payee"-  , "--s-tag"-  , "--s-number"-  , "--s-flag"-  , "--s-commodity"-  , "--s-posting-memo"-  , "--s-debit"-  , "--s-credit"-  , "--s-qty"-  , ""-  , "Options affecting patterns"-  , "--------------------------"-  , ""--  , "-i, --case-insensitive"-  , "  Be case insensitive"-    ++ ifDefault (sensitive d == M.Insensitive)--  , "-I, --case-sensitive"-  , "  Be case sensitive"-    ++ ifDefault (sensitive d == M.Sensitive)--  , ""--  , "-w, --within"-  , "  Use \"within\" matcher"-    ++ ifDefault (matcher d == Within)--  , "-r, --pcre"-  , "  Use \"pcre\" matcher"-    ++ ifDefault (matcher d == PCRE)--  , "--posix"-  , "  Use \"posix\" matcher"-    ++ ifDefault (matcher d == TDFA)--  , "-x, --exact"-  , "  Use \"exact\" matcher"-    ++ ifDefault (matcher d == Exact)-  , ""-  , "Infix or RPN selection"-  , "----------------------"-  , "--infix - use infix notation"-    ++ ifDefault (exprDesc d == X.Infix)-  , "--rpn - use reverse polish notation"-    ++ ifDefault (exprDesc d == X.RPN)-  , ""-  , "Infix Operators - from highest to lowest precedence"-  , "(all are left associative)"-  , "--------------------------"-  , "--open expr --close"-  , "-( expr -)"-  , "  Force precedence (as in \"open\" and \"close\" parentheses)"-  , "--not, -N expr"-  , "  True if expr is false"-  , "expr1 --and expr2"-  , "expr -A expr2"-  , "  True if expr and expr2 are both true"-  , "expr1 --or expr2"-  , "expr1 -O expr2"-  , "  True if either expr1 or expr2 is true"-  , ""-  , "RPN Operators"-  , "-------------"-  , "--not, -N"-  , "--and, -A"-  , "--or, -O"-  , "  RPN counterparts to the infix operators"-  , "  are postfix and manipulate the RPN stack accordingly"-  , ""-  , "Showing expressions"-  , "-------------------"-  , "--show-expression"-  , "  Show the parsed filter expression"-  , "--verbose-filter"-  , "  Verbosely show filtering results"-  , ""-  , "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."-  , "  (use \"none\" to leave postings in ledger file order)"-  , ""-  ] ++ descSortList (sorter d) ++-  [ ""-  , "Colors"-  , "------"-  , "default scheme:"-  ,  maybe "    (none)" descScheme (defaultScheme d)-  , ""-  ]-  ++ let schs = moreSchemes d-     in (if not . null $ schs-        then-          [ "--scheme SCHEME_NAME"-          , "  use color scheme for report. Available schemes:"-          ] ++ map descScheme schs-        else [])-  ++-  [ ""-  , "--color-to-file no|yes"-  , "  Whether to use color when standard output is not a"-  , "  terminal (default: " ++-    if unColorToFile . colorToFile $ d then "yes)" else "no)"-  , ""-  , "Meta"-  , "----"-  , "--help, -h - show this help and exit"-  , "--version - show version and exit"-  ]---descScheme :: E.Scheme -> String-descScheme (E.Scheme n d _) = "    " ++ n ++ " - " ++ d---- | The string @ (default)@ if the condition is True; otherwise,--- nothing.-ifDefault :: Bool -> String-ifDefault b = if b then " (default)" else ""
+ lib/Penny.hs view
@@ -0,0 +1,455 @@+-- | Penny - extensible double-entry accounting system++module Penny+  ( -- * Building a custom Penny binary++    -- | Everything you need to create a custom Penny program is+    -- available by importing only this module.+    Version(..)+  , Defaults(..)+  , Z.Matcher(..)++  -- ** Color schemes+  , E.Scheme(..)+  , E.Changers+  , E.Labels(..)+  , E.EvenAndOdd(..)+  , module System.Console.Rainbow++  -- ** Sorting+  , Z.SortField(..)+  , CabP.SortOrder(..)++  -- ** Expression type+  , Exp.ExprDesc(..)++  -- ** Formatting quantities+  , defaultQtyFormat++  -- ** Convert report options+  , Target(..)+  , CP.SortBy(..)++  -- ** Postings report options+  , Fields(..)+  , Spacers(..)+  , widthFromRuntime+  , Ps.yearMonthDay+  , Ps.qtyAsIs+  , Ps.balanceAsIs++  -- ** Runtime+  , S.Runtime+  , S.environment++  -- ** Text+  , X.Text+  , X.pack++  -- ** Main function+  , runPenny++    -- * Developer overview++    -- | Penny is organized into a tree of modules, each with a+    -- name. Check out the links for details on each component of+    -- Penny.+    --+    -- "Penny.Brenner" - Penny financial institution transaction+    -- handling. Depends on Lincoln and Copper.+    --+    -- "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. Depends on+    -- Lincoln.+    --+    -- "Penny.Steel" - independent utilities. Depends on no other+    -- Penny components.+    --+    -- "Penny.Wheat" - tools to use with+    -- "Penny.Steel.Prednote". Depends on Steel, Lincoln, and Copper.+    --+    -- "Penny.Zinc" - the Penny command-line interface. Depends on+    -- Cabin, Copper, Liberty, and Lincoln.+    --+    -- The dependencies are represented as a dot file in+    -- bin/doc/dependencies.dot in the Penny git repository.+  ) where++import qualified Data.Text as X+import Data.Version (Version(..))+import qualified Penny.Cabin.Balance.Convert as Conv+import qualified Penny.Cabin.Balance.Convert.Parser as CP+import qualified Penny.Cabin.Balance.Convert.Options as ConvOpts+import qualified Penny.Cabin.Balance.MultiCommodity as MC+import qualified Penny.Cabin.Balance.MultiCommodity.Parser as MP+import System.Console.Rainbow+import qualified Penny.Cabin.Interface as I+import qualified Penny.Cabin.Options as CO+import qualified Penny.Cabin.Parsers as CabP+import qualified Penny.Cabin.Posts as Ps+import qualified Penny.Cabin.Posts.Fields as PF+import qualified Penny.Cabin.Posts.Spacers as PS+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Lincoln as L+import qualified Data.Prednote.Expressions as Exp+import qualified Penny.Zinc as Z+import qualified Penny.Shield as S+import qualified Text.Matchers as Mr++-- | This type contains settings for all the reports, as well as+-- default settings for the global options. Some of these can be+-- overridden on the command line.+data Defaults = Defaults+  { caseSensitive :: Bool+    -- ^ Whether the matcher is case sensitive by default++  , matcher :: Z.Matcher+    -- ^ Which matcher to use++  , colorToFile :: Bool+    -- ^ Use colors when standard output is not a terminal?++  , expressionType :: Exp.ExprDesc+    -- ^ Use RPN or infix expressions? This affects both the posting+    -- filter and the filter for the Postings report.++  , defaultScheme :: Maybe E.Scheme+    -- ^ Default color scheme. If Nothing, there is no default color+    -- scheme. If there is no default color scheme and the user does+    -- not pick one on the command line, no colors will be used.++  , additionalSchemes :: [E.Scheme]+    -- ^ Additional color schemes the user can pick from on the+    -- command line.++  , sorter :: [(Z.SortField, CabP.SortOrder)]+    -- ^ Postings are sorted in this order by default. For example, if+    -- the first pair is (Date, Ascending), then postings are first+    -- sorted by date in ascending order. If the second pair is+    -- (Payee, Ascending), then postings with the same date are then+    -- sorted by payee.+    --+    -- If this list is empty, then by default postings are left in the+    -- same order as they appear in the ledger files.++  , balanceFormat :: L.Commodity -> L.Qty -> X.Text+    -- ^ How to format balances in the balance report. Change this+    -- function if, for example, you want to allow for digit grouping.++  , balanceShowZeroBalances :: Bool+    -- ^ Show zero balances in the balance report? If True, show them;+    -- if False, hide them.++  , balanceOrder :: CabP.SortOrder+    -- ^ Whether to sort the accounts in ascending or descending order+    -- by account name in the balance report.++  , convertShowZeroBalances :: Bool+    -- ^ Show zero balances in the convert report? If True, show them;+    -- if False, hide them.++  , convertTarget :: Target+    -- ^ The commodity to which to convert the commodities in the+    -- convert report.++  , convertOrder :: CabP.SortOrder+    -- ^ Sort the convert report in ascending or descending order.++  , convertSortBy :: CP.SortBy+    -- ^ Sort by account or by quantity in the convert report.++  , convertFormat :: L.Commodity -> L.Qty -> X.Text+    -- ^ How to format balances in the convert report. For instance,+    -- this function might perform digit grouping.++  , postingsFields :: Fields Bool+    -- ^ Fields to show by default in the postings report.++  , postingsWidth :: Int+    -- ^ The postings report is roughly this wide by+    -- default. Typically this will be as wide as your terminal.++  , postingsShowZeroBalances :: Bool+    -- ^ Show zero balances in the postings report? If True, show+    -- them; if False, hide them.++  , postingsDateFormat :: (M.PostMeta, L.Posting) -> X.Text+    -- ^ How to format dates in the postings report.++  , postingsQtyFormat :: (M.PostMeta, L.Posting) -> X.Text+    -- ^ How to format quantities in the balance report. This function+    -- is used when showing the quantity for the posting itself, and+    -- not the quantity for the totals columns (for that, see+    -- postingsBalanceFormat.) For example this function might perform+    -- digit grouping.++  , postingsBalanceFormat :: L.Commodity -> L.Qty -> X.Text+    -- ^ How to format balance totals in the postings report.++  , postingsSubAccountLength :: Int+    -- ^ Account names in the postings report are shortened if+    -- necessary in order to help the report fit within the allotted+    -- width (see postingsWidth). Account names are only shortened as+    -- much as is necessary for them to fit; however, each sub-account+    -- name will not be shortened any more than the amount given here.++  , postingsPayeeAllocation :: Int+    -- ^ postingsPayeeAllocation and postingsAccountAllocation+    -- determine how much space is allotted to the payee and account+    -- fields in the postings report. These fields are variable+    -- width. After space for most other fields is allotted, space is+    -- allotted for these two fields. The two fields divide the space+    -- proportionally depending on postingsPayeeAllocation and+    -- postingsAccountAllocation. For example, if+    -- postingsPayeeAllocation is 60 and postingsAccountAllocation is+    -- 40, then the payee field gets 60 percent of the leftover space+    -- and the account field gets 40 percent of the leftover space.+    --+    -- Both postingsPayeeAllocation and postingsAccountAllocation+    -- must be positive integers; if either one is less than 1, your+    -- program will crash at runtime.++  , postingsAccountAllocation :: Int+    -- ^ See postingsPayeeAllocation above for an explanation++  , postingsSpacers :: Spacers Int+    -- ^ Determines the number of spaces that appears to the right of+    -- each named field; for example, sPayee indicates how many spaces+    -- will appear to the right of the payee field. Each field of the+    -- Spacers should be a non-negative integer (although currently+    -- the absolute value of the field is taken.)+  }++-- | Creates an IO action that you can use for the main function.+runPenny+  :: Version+  -- ^ Version of the executable+  -> (S.Runtime -> Defaults)+     -- ^ runPenny will apply this function to the Runtime. This way+     -- the defaults you use can vary depending on environment+     -- variables, the terminal type, the date, etc.+  -> IO ()+runPenny ver getDefaults = do+  rt <- S.runtime+  let df = getDefaults rt+      rs = allReports df+  Z.runZinc ver (toZincDefaults df) rt rs++-- | The commodity to which to convert the commodities in the convert+-- report.+data Target+  = AutoTarget+    -- ^ Selects a target commodity automatically, based on which+    -- commodity is the most common target commodity in the prices in+    -- your ledger files. If there is a tie for most common target+    -- commodity, the target that appears later in your ledger files+    -- is used.+  | ManualTarget String+    -- ^ Always uses the commodity named by the string given.+  deriving Show++-- | Gets the current screen width from the runtime. If the COLUMNS+-- environment variable is not set, uses 80.+widthFromRuntime :: S.Runtime -> Int+widthFromRuntime rt = case S.screenWidth rt of+  Nothing -> 80+  Just sw -> S.unScreenWidth sw++convTarget :: Target -> CP.Target+convTarget t = case t of+  AutoTarget -> CP.AutoTarget+  ManualTarget s -> CP.ManualTarget . L.To . L.Commodity . X.pack $ s++allReports+  :: Defaults+  -> [I.Report]+allReports df =+  let bd = toBalanceDefaults df+      cd = toConvertDefaults df+      pd = toPostingsDefaults df+  in [ Ps.zincReport pd+     , MC.parseReport (balanceFormat df) bd+     , Conv.cmdLineReport cd+     ]++toZincDefaults :: Defaults -> Z.Defaults+toZincDefaults d = Z.Defaults+  { Z.sensitive =+      if caseSensitive d then Mr.Sensitive else Mr.Insensitive+  , Z.matcher = matcher d+  , Z.colorToFile = Z.ColorToFile . colorToFile $ d+  , Z.defaultScheme = defaultScheme d+  , Z.moreSchemes = additionalSchemes d+  , Z.sorter = sorter d+  , Z.exprDesc = expressionType d+  }++toBalanceDefaults :: Defaults -> MP.ParseOpts+toBalanceDefaults d = MP.ParseOpts+  { MP.showZeroBalances =+      CO.ShowZeroBalances . balanceShowZeroBalances $ d+  , MP.order = balanceOrder d+  }++toConvertDefaults :: Defaults -> ConvOpts.DefaultOpts+toConvertDefaults d = ConvOpts.DefaultOpts+  { ConvOpts.showZeroBalances =+      CO.ShowZeroBalances . convertShowZeroBalances $ d+  , ConvOpts.target = convTarget . convertTarget $ d+  , ConvOpts.sortOrder = convertOrder d+  , ConvOpts.sortBy = convertSortBy d+  , ConvOpts.format = convertFormat d+  }++toPostingsDefaults :: Defaults -> Ps.ZincOpts+toPostingsDefaults d = Ps.ZincOpts+  { Ps.fields = convFields . postingsFields $ d+  , Ps.width = Ps.ReportWidth . postingsWidth $ d+  , Ps.showZeroBalances =+      CO.ShowZeroBalances . postingsShowZeroBalances $ d+  , Ps.dateFormat = postingsDateFormat d+  , Ps.qtyFormat = postingsQtyFormat d+  , Ps.balanceFormat = postingsBalanceFormat d+  , Ps.subAccountLength =+      Ps.SubAccountLength . postingsSubAccountLength $ d+  , Ps.payeeAllocation =+      Ps.alloc . postingsPayeeAllocation $ d+  , Ps.accountAllocation =+      Ps.alloc . postingsAccountAllocation $ d+  , Ps.spacers = convSpacers . postingsSpacers $ d+  }++defaultQtyFormat :: L.Qty -> X.Text+defaultQtyFormat = X.pack . L.prettyShowQty++data Spacers a = Spacers+  { sGlobalTransaction :: a+  , sRevGlobalTransaction :: a+  , sGlobalPosting :: a+  , sRevGlobalPosting :: a+  , sFileTransaction :: a+  , sRevFileTransaction :: a+  , sFilePosting :: a+  , sRevFilePosting :: a+  , sFiltered :: a+  , sRevFiltered :: a+  , sSorted :: a+  , sRevSorted :: a+  , sVisible :: a+  , sRevVisible :: a+  , sLineNum :: a+  , sDate :: a+  , sFlag :: a+  , sNumber :: a+  , sPayee :: a+  , sAccount :: a+  , sPostingDrCr :: a+  , sPostingCmdty :: a+  , sPostingQty :: a+  , sTotalDrCr :: a+  , sTotalCmdty :: a+  } deriving (Show, Eq)++data Fields a = Fields+  { fGlobalTransaction :: a+  , fRevGlobalTransaction :: a+  , fGlobalPosting :: a+  , fRevGlobalPosting :: a+  , fFileTransaction :: a+  , fRevFileTransaction :: a+  , fFilePosting :: a+  , fRevFilePosting :: a+  , fFiltered :: a+  , fRevFiltered :: a+  , fSorted :: a+  , fRevSorted :: a+  , fVisible :: a+  , fRevVisible :: a+  , fLineNum :: a+  , fDate :: a+  , fFlag :: a+  , fNumber :: a+  , fPayee :: a+  , fAccount :: a+  , fPostingDrCr :: a+  , fPostingCmdty :: a+  , fPostingQty :: a+  , fTotalDrCr :: a+  , fTotalCmdty :: a+  , fTotalQty :: a+  , fTags :: a+  , fMemo :: a+  , fFilename :: a+  } deriving (Show, Eq)++convSpacers :: Spacers a -> PS.Spacers a+convSpacers s = PS.Spacers+  { PS.globalTransaction = sGlobalTransaction s+  , PS.revGlobalTransaction = sRevGlobalTransaction s+  , PS.globalPosting = sGlobalPosting s+  , PS.revGlobalPosting = sRevGlobalPosting s+  , PS.fileTransaction = sFileTransaction s+  , PS.revFileTransaction = sRevFileTransaction s+  , PS.filePosting = sFilePosting s+  , PS.revFilePosting = sRevFilePosting s+  , PS.filtered = sFiltered s+  , PS.revFiltered = sRevFiltered s+  , PS.sorted = sSorted s+  , PS.revSorted = sRevSorted s+  , PS.visible = sVisible s+  , PS.revVisible = sRevVisible s+  , PS.lineNum = sLineNum s+  , PS.date = sDate s+  , PS.flag = sFlag s+  , PS.number = sNumber s+  , PS.payee = sPayee s+  , PS.account = sAccount s+  , PS.postingDrCr = sPostingDrCr s+  , PS.postingCmdty = sPostingCmdty s+  , PS.postingQty = sPostingQty s+  , PS.totalDrCr = sTotalDrCr s+  , PS.totalCmdty = sTotalCmdty s+  }++convFields :: Fields a -> PF.Fields a+convFields f = PF.Fields+  { PF.globalTransaction = fGlobalTransaction f+  , PF.revGlobalTransaction = fRevGlobalTransaction f+  , PF.globalPosting = fGlobalPosting f+  , PF.revGlobalPosting = fRevGlobalPosting f+  , PF.fileTransaction = fFileTransaction f+  , PF.revFileTransaction = fRevFileTransaction f+  , PF.filePosting = fFilePosting f+  , PF.revFilePosting = fRevFilePosting f+  , PF.filtered = fFiltered f+  , PF.revFiltered = fRevFiltered f+  , PF.sorted = fSorted f+  , PF.revSorted = fRevSorted f+  , PF.visible = fVisible f+  , PF.revVisible = fRevVisible f+  , PF.lineNum = fLineNum f+  , PF.date = fDate f+  , PF.flag = fFlag f+  , PF.number = fNumber f+  , PF.payee = fPayee f+  , PF.account = fAccount f+  , PF.postingDrCr = fPostingDrCr f+  , PF.postingCmdty = fPostingCmdty f+  , PF.postingQty = fPostingQty f+  , PF.totalDrCr = fTotalDrCr f+  , PF.totalCmdty = fTotalCmdty f+  , PF.totalQty = fTotalQty f+  , PF.tags = fTags f+  , PF.memo = fMemo f+  , PF.filename = fFilename f+  }
+ lib/Penny/Brenner.hs view
@@ -0,0 +1,263 @@+-- | Brenner - Penny financial institution interfaces+--+-- Brenner provides a uniform way to interact with downloaded data+-- from financial Given a parser, Brenner will import the transactions+-- and store them in a database. From there it is easy to merge the+-- transactions (without duplicates) into a ledger file, and then to+-- clear transactions from statements in an automated fashion.+module Penny.Brenner+  ( FitAcct(..)+  , Config(..)+  , R.GroupSpecs(..)+  , R.GroupSpec(..)+  , Y.Translator(..)+  , L.Side(..)+  , L.SpaceBetween(..)+  , usePayeeOrDesc+  , brennerMain+  , ofxParser+  ) where++import qualified Penny.Brenner.Types as Y+import Control.Monad (join)+import Data.Either (partitionEithers)+import qualified Data.Text as X+import qualified Data.Version as V+import qualified Penny.Liberty as Ly+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Builders as Bd+import qualified Penny.Copper.Render as R+import qualified Penny.Brenner.Clear as C+import qualified Penny.Brenner.Database as D+import qualified Penny.Brenner.Import as I+import qualified Penny.Brenner.Info as Info+import qualified Penny.Brenner.Merge as M+import qualified Penny.Brenner.OFX as O+import qualified Penny.Brenner.Print as P+import qualified Penny.Brenner.Util as U+import qualified System.Console.MultiArg as MA+import qualified Control.Monad.Exception.Synchronous as Ex++-- | Brenner, with a pre-compiled configuration.+brennerMain+  :: V.Version+  -- ^ Binary version+  -> Config+  -> IO ()+brennerMain v cf = do+  let cf' = convertConfig cf+  join $ MA.modesWithHelp (help False) (globalOpts v)+                          (preProcessor cf')++-- | Parses global options for a pre-compiled configuration.+globalOpts+  :: V.Version+  -- ^ Binary version+  -> [MA.OptSpec (Either (IO ()) Y.FitAcctName)]+globalOpts v =+  [ MA.OptSpec ["fit-account"] "f"+  (MA.OneArg (Right . Y.FitAcctName . X.pack))+  , fmap Left (Ly.version v)+  ]++-- | Pre-processes global options for a pre-compiled configuration.+preProcessor+  :: Y.Config+  -> [Either (IO ()) Y.FitAcctName]+  -> Either (a -> IO ()) [MA.Mode (IO ())]+preProcessor cf args =+  let (vers, as) = partitionEithers args+  in case vers of+      [] -> makeModes Nothing cf as+      x:_ -> Left (const x)++-- | Makes modes for a pre-compiled configuration.+makeModes+  :: Maybe Y.ConfigLocation+  -> Y.Config+  -> [Y.FitAcctName]+  -- ^ Names of financial institutions given on command line+  -> Either (a -> IO ()) [MA.Mode (IO ())]+makeModes cl cf as = Ex.toEither . Ex.mapException (const . U.errExit) $ do+  mayFi <- case as of+    [] -> return $ Y.defaultFitAcct cf+    _ ->+      let pdct a = Y.fitAcctName a == s+          s = last as+          toFilter = case Y.defaultFitAcct cf of+            Nothing -> Y.moreFitAccts cf+            Just d -> d : Y.moreFitAccts cf+      in case filter pdct toFilter of+           [] -> Ex.throw $+              "financial institution account "+              ++ (X.unpack . Y.unFitAcctName $ s) ++ " not configured."+           c:[] -> return $ Just c+           _ -> Ex.throw $+              "more than one financial institution account "+              ++ "named " ++ (X.unpack . Y.unFitAcctName $ s)+              ++ " configured."+  return . map (fmap (\f -> f cl cf mayFi)) $ allModes++-- | Each mode takes a Maybe FitAcct. Even if every mode needs a+-- FitAcct to function, they take a Maybe FitAcct because otherwise+-- the user will not even get online help if a FitAcct is not+-- supplied. Each mode must fail on its own if it actually needs a+-- FitAcct.+type ModeFunc+  = Maybe Y.ConfigLocation+  -> Y.Config+  -> Maybe Y.FitAcct+  -> IO ()++allModes :: [MA.Mode ModeFunc]+allModes =+  fmap (\f cl cf _ -> f cl cf) Info.mode+  : map (fmap (const . const))+        [C.mode, I.mode, M.mode, P.mode, D.mode]++-- | Help for a pre-compiled configuration.+help+  :: Bool+  -- ^ True if running under a dynamic configuration+  -> String+  -- ^ Program name++  -> String+help dyn n = unlines ls+  where+    ls = [ "usage: " ++ n ++ " [global-options]"+            ++ " COMMAND [local-options]"+            ++ " ARGS..."+         , ""+         , "where COMMAND is one of:"+         , "import, merge, clear, database, print, info"+         , ""+         , "For help on an individual command and its"+           ++ " local options, use "+         , n ++ " COMMAND --help"+         , ""+         , "Global Options:"+         , "-f, --fit-account ACCOUNT"+         , "  use the given financial institution account"+         , "  (use the \"info\" command to see which are available)."+         , "  If this option does not appear,"+         , "  the default account is used if there is one."+         ] ++ if not dyn then [] else+                  [ ""+                  , "-c, --config-file FILENAME"+                  , "  Specify configuration file location"+                  ]++-- | Information to configure a single financial institution account.+data FitAcct = FitAcct+  { fitAcctName :: String+    -- ^ Name for this financial institution account, e.g. @House+    -- Checking@ or @Megabank@.++  , fitAcctDesc :: String+    -- ^ Additional information about this financial institution+    -- account. Here I put information on where to find the statments+    -- for download on the website.++  , dbLocation :: String+    -- ^ Path and filename to where the database is kept. You can use+    -- an absolute or relative path (if it is relative, it will be+    -- resolved relative to the current directory at runtime.)++  , pennyAcct :: String+    -- ^ The account that you use in your Penny file to hold+    -- transactions for this card. Separate each sub-account with+    -- colons (as you do in the Penny file.)++  , defaultAcct :: String+    -- ^ When new transactions are created, one of the postings will+    -- be in the amexAcct given above. The other posting will be in+    -- this account.++  , currency :: String+    -- ^ The commodity for the currency of your card (e.g. @$@).++  , groupSpecs :: R.GroupSpecs+    -- ^ How to group digits when printing the resulting ledger. All+    -- quantities (not just those affected by this program) will be+    -- formatted using this specification.++  , translator :: Y.Translator+    -- ^ See the documentation under the 'Translator' type for+    -- details.++  , side :: L.Side+  -- ^ When creating new transactions, the commodity will be on this+  -- side++  , spaceBetween :: L.SpaceBetween+  -- ^ When creating new transactions, is there a space between the+  -- commodity and the quantity++  , parser :: ( Y.ParserDesc+              , Y.FitFileLocation -> IO (Ex.Exceptional String [Y.Posting]))+  -- ^ Parses a file of transactions from the financial+  -- institution. The function must open the file and parse it. This+  -- is in the IO monad not only because the function must open the+  -- file itself, but also so the function can perform arbitrary IO+  -- (run pdftotext, maybe?) If there is failure, the function can+  -- return an Exceptional String, which is the error+  -- message. Alternatively the function can raise an exception in the+  -- IO monad (currently Brenner makes no attempt to catch these) so+  -- if any of the IO functions throw you can simply not handle the+  -- exceptions.+  --+  -- The first element of the pair is a help string which should+  -- indicate how to download the data, as a helpful reminder.++  , toLincolnPayee :: Y.Desc -> Y.Payee -> L.Payee+  -- ^ Sometimes the financial institution provides Payee information,+  -- sometimes it does not. Sometimes the Desc might have additional+  -- information that you might want to remove. This function can be+  -- used to do that. The resulting Lincoln Payee is used for any+  -- transactions that are created by the merge command. The resulting+  -- payee is also used when comparing new financial institution+  -- postings to already existing ledger transactions in order to+  -- guess at which payee and accounts to create in the transactions+  -- created by the merge command.+++  } deriving Show++convertFitAcct :: FitAcct -> Y.FitAcct+convertFitAcct (FitAcct fn fd db ax df cy gs tl sd sb ps tlp) = Y.FitAcct+  { Y.fitAcctName = Y.FitAcctName . X.pack $ fn+  , Y.fitAcctDesc = Y.FitAcctDesc . X.pack $ fd+  , Y.dbLocation = Y.DbLocation . X.pack $ db+  , Y.pennyAcct = Y.PennyAcct . Bd.account . X.pack $ ax+  , Y.defaultAcct = Y.DefaultAcct . Bd.account . X.pack $ df+  , Y.currency = Y.Currency . L.Commodity . X.pack $ cy+  , Y.groupSpecs = gs+  , Y.translator = tl+  , Y.side = sd+  , Y.spaceBetween = sb+  , Y.parser = ps+  , Y.toLincolnPayee = tlp+  }++data Config = Config+  { defaultFitAcct :: Maybe FitAcct+  , moreFitAccts :: [FitAcct]+  } deriving Show++convertConfig :: Config -> Y.Config+convertConfig (Config d m) = Y.Config+  { Y.defaultFitAcct = fmap convertFitAcct d+  , Y.moreFitAccts = map convertFitAcct m+  }++-- | A simple function to use for 'toLincolnPayee'. Uses the financial+-- institution payee if it is available; otherwise, uses the financial+-- institution description.+usePayeeOrDesc :: Y.Desc -> Y.Payee -> L.Payee+usePayeeOrDesc (Y.Desc d) (Y.Payee p) = L.Payee $+  if X.null p then d else p++-- | Parser for OFX data.+ofxParser :: (Y.ParserDesc, Y.ParserFn)+ofxParser = O.parser
+ lib/Penny/Brenner/Clear.hs view
@@ -0,0 +1,185 @@+module Penny.Brenner.Clear (mode) where++import Control.Applicative+import qualified Control.Monad.Exception.Synchronous as Ex+import Control.Monad (guard, mzero, when)+import Data.Maybe (mapMaybe, fromMaybe)+import Data.Monoid (mconcat, First(..))+import qualified Data.Set as Set+import qualified Data.Map as M+import qualified Data.Text as X+import qualified Data.Traversable as Tr+import qualified System.Console.MultiArg as MA+import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import qualified Penny.Steel.Sums as S+import qualified Control.Monad.Trans.State as St+import qualified Control.Monad.Trans.Maybe as MT+import Control.Monad.Trans.Class (lift)+import qualified Penny.Copper as C+import qualified Penny.Copper.Render as R+import Text.Show.Pretty (ppShow)+import qualified Penny.Brenner.Types as Y+import qualified Penny.Brenner.Util as U+++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ " clear [options] FIT_FILE LEDGER_FILE..."+  , "Parses all postings that are in FIT_FILE. Then marks all"+  , "postings that are in the FILEs given that correspond to one"+  , "of the postings in the FIT_FILE as being cleared."+  , "Quits if one of the postings found in FIT_FILE is not found"+  , "in the database, if one of the postings in the database"+  , "is not found in one of the FILEs, or if any of the postings found"+  , "in one of the FILEs already has a flag."+  , ""+  , "Results are printed to standard output. If no FILE, or FILE is \"-\","+  , "read standard input."+  , ""+  , "Options:"+  , "  -o, --output FILENAME - send output to FILENAME"+  , "     (default: send to standard output)"+  , "  -h, --help - show help and exit"+  ]++data Arg+  = APosArg String+  | AOutput (X.Text -> IO ())++toPosArg :: Arg -> Maybe String+toPosArg a = case a of { APosArg s -> Just s; _ -> Nothing }++toOutput :: Arg -> Maybe (X.Text -> IO ())+toOutput a = case a of { AOutput x -> Just x; _ -> Nothing }++data Opts = Opts+  { csvLocation :: Y.FitFileLocation+  , ledgerLocations :: [String]+  , printer :: X.Text -> IO ()+  } deriving Show+++mode :: MA.Mode (Maybe Y.FitAcct -> IO ())+mode = MA.Mode+  { MA.mName = "clear"+  , MA.mIntersperse = MA.Intersperse+  , MA.mOpts = [fmap AOutput Ly.output]+  , MA.mPosArgs = return . APosArg+  , MA.mProcess = process+  , MA.mHelp = help+  }++process :: [Arg] -> Maybe Y.FitAcct -> IO ()+process as mayFa = do+  fa <- U.getFitAcct mayFa+  (csv, ls) <- case mapMaybe toPosArg as of+    [] -> fail "clear: you must provide a postings file."+    x:xs -> return (Y.FitFileLocation x, xs)+  let os = Opts csv ls (Ly.processOutput . mapMaybe toOutput $ as)+  runClear fa os++runClear :: Y.FitAcct -> Opts -> IO ()+runClear c os = do+  dbList <- U.loadDb (Y.AllowNew False) (Y.dbLocation c)+  let db = M.fromList dbList+      (_, prsr) = Y.parser c+  txns <- fmap (Ex.switch fail return) $ prsr (csvLocation os)+  leds <- C.open (ledgerLocations os)+  toClear <- case mapM (findUNumber db) (concat txns) of+    Nothing -> fail $ "at least one posting was not found in the"+                       ++ " database. Ensure all postings have "+                       ++ "been imported and merged."+    Just ls -> return $ Set.fromList ls+  let (led', left) = changeLedger (Y.pennyAcct c) toClear leds+      led'' = map C.stripMeta led'+  when (not (Set.null left))+    (fail $ "some postings were not cleared. "+      ++ "Those not cleared:\n" ++ ppShow left)+  case mapM (R.item (Y.groupSpecs c)) led'' of+    Nothing ->+      fail "could not render resulting ledger."+    Just txts ->+      let glued = X.concat txts+      in glued `seq` printer os glued+++-- | Examines an financial institution transaction and the DbMap to+-- find a matching UNumber. Fails if the financial institution+-- transaction is not in the Db.+findUNumber :: Y.DbMap -> Y.Posting -> Maybe Y.UNumber+findUNumber m pstg =+  let atn = Y.fitId pstg+      p ap = Y.fitId ap == atn+      filteredMap = M.filter p m+      ls = M.toList filteredMap+  in case ls of+      (n, _):[] -> Just n+      _ -> Nothing+++clearedFlag :: L.Flag+clearedFlag = L.Flag . X.singleton $ 'C'++-- | Changes a ledger to clear postings. Returns postings still not+-- cleared.+changeLedger+  :: Y.PennyAcct+  -> Set.Set Y.UNumber+  -> [C.LedgerItem]+  -> ([C.LedgerItem], Set.Set Y.UNumber)+changeLedger ax s l = St.runState k s+  where+    k = mapM f l+    f x = case x of+      S.S4a t -> fmap S.S4a $ changeTxn ax t+      S.S4b z -> fmap S.S4b $ return z+      S.S4c z -> fmap S.S4c $ return z+      S.S4d z -> fmap S.S4d $ return z++changeTxn+  :: Y.PennyAcct+  -> L.Transaction+  -> St.State (Set.Set Y.UNumber) L.Transaction+changeTxn ax (L.Transaction (tld, d)) =+  (\tl es -> L.Transaction (tl, es))+  <$> pure tld+  <*> Tr.mapM (changePstg ax) d+++-- | Sees if this posting is a posting in the right account and has a+-- UNumber that needs to be cleared. If so, clears it. If this posting+-- already has a flag, skips it.+changePstg+  :: Y.PennyAcct+  -> L.PostingData+  -> St.State (Set.Set Y.UNumber) L.PostingData+changePstg ax p =+  fmap (fromMaybe p) . MT.runMaybeT $ do+    let c = L.pdCore p+    guard (L.pAccount c == (Y.unPennyAcct ax))+    let tags = L.pTags c+    un <- maybe mzero return $ parseUNumberFromTags tags+    guard (L.pFlag c == Nothing)+    set <- lift St.get+    guard (Set.member un set)+    lift $ St.put (Set.delete un set)+    let c' = c { L.pFlag = Just clearedFlag }+    return $ p { L.pdCore = c' }++parseUNumberFromTags :: L.Tags -> Maybe Y.UNumber+parseUNumberFromTags =+  getFirst+  . mconcat+  . map First+  . map parseUNumberFromTag+  . L.unTags++parseUNumberFromTag :: L.Tag -> Maybe Y.UNumber+parseUNumberFromTag (L.Tag x) = do+  (f, xs) <- X.uncons x+  guard (f == 'U')+  case reads . X.unpack $ xs of+    (u, ""):[] -> Just (Y.UNumber u)+    _ -> Nothing+
+ lib/Penny/Brenner/Database.hs view
@@ -0,0 +1,41 @@+module Penny.Brenner.Database (mode) where++import qualified Penny.Brenner.Types as Y+import qualified Penny.Brenner.Util as U+import qualified System.Console.MultiArg as MA++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ " [global-options] database [local-options]"+  , "Shows the database of financial institution transactions."+  , "Does not accept any non-option arguments."+  , ""+  , "Local options:"+  , "  --help, -h Show this help and exit."+  ]++data Arg = ArgPos String++mode :: MA.Mode (Maybe Y.FitAcct -> IO ())+mode = MA.Mode+  { MA.mName = "database"+  , MA.mIntersperse = MA.Intersperse+  , MA.mOpts = []+  , MA.mPosArgs = return . ArgPos+  , MA.mProcess = processor+  , MA.mHelp = help+  }++processor+  :: [Arg]+  -> Maybe Y.FitAcct+  -> IO ()+processor ls mayFa+  | not . null $ ls = fail $+        "penny-fit database: error: this command does"+        ++ " not accept non-option arguments."+  | otherwise = do+        fa <- U.getFitAcct mayFa+        let dbLoc = Y.dbLocation fa+        db <- U.loadDb (Y.AllowNew False) dbLoc+        mapM_ putStr . map U.showDbPair $ db
+ lib/Penny/Brenner/Import.hs view
@@ -0,0 +1,140 @@+module Penny.Brenner.Import (mode) where++import Control.Applicative ((<|>))+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Maybe (mapMaybe)+import qualified System.Console.MultiArg as MA+import qualified Penny.Brenner.Types as Y+import qualified Penny.Brenner.Util as U++data Arg+  = AFitFile String+  | AAllowNew+  | AUNumber Integer++toFitFile :: Arg -> Maybe String+toFitFile a = case a of+  AFitFile s -> Just s+  _ -> Nothing++toNewUNumber :: [Arg] -> Maybe Integer+toNewUNumber as =+  let f i = case i of { AUNumber x -> Just x; _ -> Nothing }+  in case mapMaybe f as of+      [] -> Nothing+      xs -> Just $ last xs++data ImportOpts = ImportOpts+  { fitFile :: Y.FitFileLocation+  , allowNew :: Y.AllowNew+  , parser :: Y.FitFileLocation+              -> IO (Ex.Exceptional String [Y.Posting])+  , newUNumber :: Maybe Integer+  }++mode+  :: MA.Mode (Maybe Y.FitAcct -> IO ())+mode = MA.Mode+  { MA.mName = "import"+  , MA.mIntersperse = MA.Intersperse+  , MA.mOpts =+      [ MA.OptSpec ["new"] "n" (MA.NoArg AAllowNew)+      , MA.OptSpec ["unumber"] "u" . MA.OneArgE $ \s -> do+          i <- MA.reader s+          return $ AUNumber i+      ]+  , MA.mPosArgs = return . AFitFile+  , MA.mProcess = processor+  , MA.mHelp = help+  }++processor+  :: [Arg]+  -> Maybe Y.FitAcct+  -> IO ()+processor as mayFa = do+  fa <- U.getFitAcct mayFa+  let (dbLoc, prsr) = (Y.dbLocation fa, snd . Y.parser $ fa)+  loc <- case mapMaybe toFitFile as of+    [] -> fail "you must provide a postings file to read"+    x:[] -> return (Y.FitFileLocation x)+    _ -> fail "you cannot provide more than one postings file to read"+  let aNew = Y.AllowNew+        $ any (\a -> case a of { AAllowNew -> True; _ -> False }) as+      maybeNewU = toNewUNumber as+  doImport dbLoc (ImportOpts loc aNew prsr maybeNewU)+++-- | Appends new Amex transactions to the existing list.+appendNew+  :: Maybe Integer+  -- ^ If Just, this is the new U-number for the first+  -- transaction. Otherwise, the next U number will be the one that is+  -- one larger than the current maximum in the database.++  -> [(Y.UNumber, Y.Posting)]+  -- ^ Existing transactions++  -> [Y.Posting]+  -- ^ New transactions++  -> Maybe ([(Y.UNumber, Y.Posting)], Int)+  -- ^ New list, and number of transactions added. Fails if the new U+  -- number was passed in the first argument and this number is not+  -- valid.++appendNew mu db new =+  let currFitIds = map (Y.fitId . snd) db+      isNew p = not (any (== Y.fitId p) currFitIds)+      newPstgs = filter isNew new+      mkPair i p = (Y.UNumber i, p)+      maybeU = nextUNum mu db+  in fmap (\u -> let newWithU = (zipWith mkPair [u..] newPstgs)+                 in (db ++ newWithU, length newWithU)) maybeU++nextUNum+  :: Maybe Integer+  -> [(Y.UNumber, Y.Posting)]+  -> Maybe Integer+nextUNum mu db =+  let defaultU = if null db then Nothing+                 else Just $ ( Y.unUNumber . maximum+                               . map fst $ db) + 1+  in case mu of+      Nothing -> defaultU <|> Just 0+      Just u -> case defaultU of+        Nothing -> if u >= 0 then Just u else Nothing+        Just du -> if u >= du then Just u else Nothing++doImport :: Y.DbLocation -> ImportOpts -> IO ()+doImport dbLoc os = do+  txnsOld <- U.loadDb (allowNew os) dbLoc+  parseResult <- parser os (fitFile os)+  ins <- case parseResult of+    Ex.Exception e -> fail e+    Ex.Success g -> return g+  (new, len) <- case appendNew (newUNumber os) txnsOld ins of+    Just r -> return r+    Nothing -> fail "invalid new U number given."+  U.saveDb dbLoc new+  putStrLn $ "imported " ++ show len ++ " new transactions."++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ "  [global-options] import [local-options] FIT_FILE"+  , "where FIT_FILE is the file downloaded from the financial"+  , "institution."+  , ""+  , "Local Options:"+  , ""+  , "-n, --new - Allows creation of new database. Without this option,"+  , "if the database file is not found, quits with an error."+  , ""+  , "-u, --unumber - The first U number assigned will be this number."+  , "Fails if the number you give is not greater than the largest"+  , "U number already in the database."+  , ""+  , "-h, --help - Show this help."+  , ""+  ]+
+ lib/Penny/Brenner/Info.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE OverloadedStrings #-}+module Penny.Brenner.Info (mode) where++import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Penny.Brenner.Types as Y+import qualified Data.Text as X+import qualified Data.Text.IO as TIO+import Data.Monoid ((<>))+import qualified Penny.Lincoln as L+import qualified Penny.Copper.Render as R+import qualified System.Console.MultiArg as MA++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ " info [options]"+  , "Shows further information about the configuration of your"+  , "financial institution accounts."+  , ""+  , "Options:"+  , "  -h, --help - show help and exit"+  ]++mode :: MA.Mode (Maybe Y.ConfigLocation -> Y.Config -> IO ())+mode = MA.Mode+  { MA.mName = "info"+  , MA.mIntersperse = MA.Intersperse+  , MA.mOpts = []+  , MA.mPosArgs = const . Ex.throw . MA.ErrorMsg+    $ "this mode does not accept positional arguments"+  , MA.mProcess = const process+  , MA.mHelp = help+  }++process :: Maybe Y.ConfigLocation -> Y.Config -> IO ()+process cf cn = TIO.putStr $ showInfo cf cn++showInfo :: Maybe Y.ConfigLocation -> Y.Config -> X.Text+showInfo cf cn =+  maybe "These settings are compiled into your program.\n\n"+        (\l -> label "From configuration file at" (L.text l) <> "\n\n")+        cf+  <> showConfig cn++showConfig :: Y.Config -> X.Text+showConfig (Y.Config dflt more) =+  "Default financial institution account:"+  <> case dflt of+      Nothing -> " (no default)\n\n"+      Just d -> "\n\n" <> showFitAcct d <> "\n"+  <> "Additional financial institution accounts:"+  <> case more of+      [] -> " no additional accounts\n"+      ls -> "\n\n" <> showFitAccts ls++sepBar :: X.Text+sepBar = X.replicate 40 "=" <> "\n"++sepWithSpace :: X.Text+sepWithSpace = "\n" <> sepBar <> "\n"++showFitAccts :: [Y.FitAcct] -> X.Text+showFitAccts = X.intercalate sepWithSpace . map showFitAcct++label :: X.Text -> X.Text -> X.Text+label l t = l <> ": " <> t++showFitAcct :: Y.FitAcct -> X.Text+showFitAcct c =+  (L.text . Y.fitAcctName $ c) <> "\n\n"+  <> (L.text . Y.fitAcctDesc $ c) <> "\n"+  <> X.unlines+  [ label "Database location" (L.text . Y.dbLocation $ c)+  , label "Penny account" (L.text . L.Delimited ":" . Y.pennyAcct $ c)+  , label "Default account" (L.text . L.Delimited ":" . Y.defaultAcct $ c)+  , label "Currency" (L.text . Y.currency $ c)+  , label "Group amounts to left of decimal point"+    (showGroupLeft . R.left . Y.groupSpecs $ c)++  , label "Group amounts to right of decimal point"+    (showGroupRight . R.right . Y.groupSpecs $ c)++  , label "Financial institution increases are"+    (showTranslator . Y.translator $ c)++  , label "In new postings, commodity is on the"+    (showSide . Y.side $ c)++  , label "Space between commodity and quantity in new postings"+    (showSpaceBetween . Y.spaceBetween $ c)+  ]+  <> "Parser description:\n"+  <> (L.text . fst . Y.parser $ c)++showGroupLeft :: R.GroupSpec -> X.Text+showGroupLeft s = case s of+  R.NoGrouping -> "never"+  R.GroupLarge -> "when greater than 9,999"+  R.GroupAll -> "always"++showGroupRight :: R.GroupSpec -> X.Text+showGroupRight s = case s of+  R.NoGrouping -> "never"+  R.GroupLarge -> "when mor than four decimal places"+  R.GroupAll -> "always"++showTranslator :: Y.Translator -> X.Text+showTranslator y = case y of+  Y.IncreaseIsDebit -> "debits"+  Y.IncreaseIsCredit -> "credits"++showSide :: L.Side -> X.Text+showSide L.CommodityOnLeft = "left"+showSide L.CommodityOnRight = "right"++showSpaceBetween :: L.SpaceBetween -> X.Text+showSpaceBetween L.SpaceBetween = "yes"+showSpaceBetween L.NoSpaceBetween = "no"++{-+  label "Database location"+    (X.unpack . Y.unDbLocation . Y.dbLocation $ c)++  ++ label "Penny account"+     (showAccount . Y.unPennyAcct . Y.pennyAcct $ c)++  ++ label "Account for new offsetting postings"+     (showAccount . Y.unDefaultAcct . Y.defaultAcct $ c)++  ++ label "Currency"+     (X.unpack . L.unCommodity . Y.unCurrency . Y.currency $ c)++  ++ "\n"++  ++ "More information about the parser:\n"+  ++ (Y.unParserDesc . fst . Y.parser $ c)+  ++ "\n\n"+++-}
+ lib/Penny/Brenner/Merge.hs view
@@ -0,0 +1,360 @@+module Penny.Brenner.Merge (mode) where++import Control.Applicative+import Control.Monad (guard)+import qualified Control.Monad.Trans.State as St+import Data.List (find, sortBy, foldl')+import qualified Data.Map as M+import Data.Maybe (mapMaybe, isNothing, fromMaybe)+import Data.Monoid (First(..), mconcat)+import qualified Data.Text as X+import qualified System.Console.MultiArg as MA+import qualified Penny.Copper as C+import qualified Penny.Copper.Render as R+import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Brenner.Types as Y+import qualified Penny.Brenner.Util as U+import qualified Penny.Steel.Sums as S++type NoAuto = Bool++data Arg+  = APos String+  | ANoAuto+  | AOutput (X.Text -> IO ())++instance Eq Arg where+  APos l == APos r = l == r+  ANoAuto == ANoAuto = True+  _ == _ = False++toPosArg :: Arg -> Maybe String+toPosArg a = case a of { APos s -> Just s; _ -> Nothing }++toOutput :: Arg -> Maybe (X.Text -> IO ())+toOutput a = case a of { AOutput x -> Just x; _ -> Nothing }++mode :: MA.Mode (Maybe Y.FitAcct -> IO ())+mode = MA.Mode+  { MA.mName = "merge"+  , MA.mIntersperse = MA.Intersperse+  , MA.mOpts = [ MA.OptSpec ["no-auto"] "n" (MA.NoArg ANoAuto)+               , fmap AOutput Ly.output+               ]+  , MA.mPosArgs = return . APos+  , MA.mProcess = processor+  , MA.mHelp = help+  }++processor :: [Arg] -> Maybe Y.FitAcct -> IO ()+processor as mayFa = do+  fa <- U.getFitAcct mayFa+  doMerge fa+          (ANoAuto `elem` as)+          (Ly.processOutput . mapMaybe toOutput $ as)+          (mapMaybe toPosArg as)++doMerge+  :: Y.FitAcct+  -> NoAuto+  -> (X.Text -> IO ())+  -- ^ Function to handle the output+  -> [String]+  -- ^ Ledger filenames to open+  -> IO ()+doMerge acct noAuto printer ss = do+  dbLs <- U.loadDb (Y.AllowNew False) (Y.dbLocation acct)+  l <- C.open ss+  let dbWithEntry = fmap (pairWithEntry acct) . M.fromList $ dbLs+      (l', db') = changeItems acct+                  l (filterDb (Y.pennyAcct acct) dbWithEntry l)+      newTxns = createTransactions noAuto acct l dbLs db'+      final = l' ++ newTxns+  case mapM (R.item (Y.groupSpecs acct)) (map C.stripMeta final) of+    Nothing -> fail "Could not render final ledger."+    Just txts ->+      let txt = X.concat txts+      in txt `seq` printer txt+++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ " merge: merges new transactions from database"+  , "to ledger file."+  , "usage: penny-fit merge [options] FILE..."+  , "Results are printed to standard output. If no FILE, or if FILE is -,"+  , "read standard input."+  , ""+  , "Options:"+  , "  -n, --no-auto - do not automatically assign payees and accounts"+  , "  -o, --output FILENAME - send output to FILENAME"+  , "     (default: send to standard output)"+  , "  -h, --help - show help and exit"+  ]++-- | Removes all Brenner postings that already have a Penny posting+-- with the correct uNumber.+filterDb :: Y.PennyAcct -> DbWithEntry -> [C.LedgerItem] -> DbWithEntry+filterDb ax m l = M.difference m ml+  where+    ml = M.fromList+       . flip zip (repeat ())+       . mapMaybe toUNum+       . filter inPennyAcct+       . concatMap L.transactionToPostings+       . ( let cn = const Nothing+           in mapMaybe (S.caseS4 Just cn cn cn))+       $ l+    inPennyAcct p = Q.account p == (Y.unPennyAcct ax)+    toUNum p = getUNumberFromTags . Q.tags $ p++-- | Gets the first UNumber from a list of Tags.+getUNumberFromTags :: L.Tags -> Maybe Y.UNumber+getUNumberFromTags =+  getFirst+  . mconcat+  . map First+  . map getUNumberFromTag+  . L.unTags++-- | Examines a tag to see if it is a uNumber. If so, returns the+-- UNumber. Otherwise, returns Nothing.+getUNumberFromTag :: L.Tag -> Maybe Y.UNumber+getUNumberFromTag (L.Tag x) = do+  (f, r) <- X.uncons x+  guard (f == 'U')+  case reads . X.unpack $ r of+    (y, ""):[] -> return $ Y.UNumber y+    _ -> Nothing+++-- | Changes a single Item.+changeItem+  :: Y.FitAcct+  -> C.LedgerItem+  -> St.State DbWithEntry C.LedgerItem+changeItem acct =+  S.caseS4 (fmap S.S4a . changeTransaction acct)+           (return . S.S4b) (return . S.S4c) (return . S.S4d)+++-- | Changes all postings that match an AmexTxn to assign them the+-- proper UNumber. Returns a list of changed items, and the DbMap of+-- still-unassigned AmexTxns.+changeItems+  :: Y.FitAcct+  -> [C.LedgerItem]+  -> DbWithEntry+  -> ([C.LedgerItem], DbWithEntry)+changeItems acct l = St.runState (mapM (changeItem acct) l)+++changeTransaction+  :: Y.FitAcct+  -> L.Transaction+  -> St.State DbWithEntry L.Transaction+changeTransaction acct txn =+  (\tl es -> L.Transaction (tl, es))+  <$> pure (fst . L.unTransaction $ txn)+  <*> L.traverseEnts (inspectAndChange acct+                      (fst . L.unTransaction $ txn))+                      (snd . L.unTransaction $ txn)++-- | Inspects a posting to see if it is an Amex posting and, if so,+-- whether it matches one of the remaining AmexTxns. If so, then+-- changes the transaction's UNumber, and remove that UNumber from the+-- DbMap. If the posting alreay has a Number (UNumber or otherwise)+-- skips it.+inspectAndChange+  :: Y.FitAcct+  -> L.TopLineData+  -> L.Ent L.PostingData+  -> St.State DbWithEntry L.PostingData+inspectAndChange acct tld p = do+  m <- St.get+  case findMatch acct tld p m of+    Nothing -> return (L.meta p)+    Just (n, m') ->+      let c = L.pdCore . L.meta $ p+          L.Tags oldTags = L.pTags c+          tags' = L.Tags (oldTags ++ [newLincolnUNumber n])+          c' = c { L.pTags = tags' }+          p' = (L.meta p) { L.pdCore = c' }+      in St.put m' >> return p'++newLincolnUNumber :: Y.UNumber -> L.Tag+newLincolnUNumber a =+  L.Tag ('U' `X.cons` (X.pack . show . Y.unUNumber $ a))+++-- | Searches a DbMap for an AmexTxn that matches a given posting. If+-- a match is found, returns the matching UNumber and a new DbMap that+-- has the match removed.+findMatch+  :: Y.FitAcct+  -> L.TopLineData+  -> L.Ent L.PostingData+  -> DbWithEntry+  -> Maybe (Y.UNumber, DbWithEntry)+findMatch acct tl p m = fmap toResult findResult+  where+    findResult = find (pennyTxnMatches acct tl p)+                 . M.toList $ m+    toResult (u, (_, _)) = (u, M.delete u m)++-- | Pairs each association in a DbMap with an Entry representing the+-- transaction's entry in the ledger.+pairWithEntry :: Y.FitAcct -> Y.Posting -> (Y.Posting, L.Entry)+pairWithEntry acct p = (p, en)+  where+    en = L.Entry dc (L.Amount qty cty)+    dc = Y.translate (Y.incDec p) (Y.translator acct)+    qty = U.parseQty (Y.amount p)+    cty = Y.unCurrency . Y.currency $ acct++type DbWithEntry = M.Map Y.UNumber (Y.Posting, L.Entry)++-- | Does the given Penny transaction match this posting? Makes sure+-- that the account, quantity, date, commodity, and DrCr match, and+-- that the posting does not have a number (it's OK if the transaction+-- has a number.)+pennyTxnMatches+  :: Y.FitAcct+  -> L.TopLineData+  -> L.Ent L.PostingData+  -> (a, (Y.Posting, L.Entry))+  -> Bool+pennyTxnMatches acct tl pstg (_, (a, e)) =+  mA && noFlag && mQ && mDC && mDate && mCmdty+  where+    p = L.pdCore . L.meta $ pstg+    mA = L.pAccount p == (Y.unPennyAcct . Y.pennyAcct $ acct)+    mQ = L.equivalent (L.qty . L.amount . L.entry $ pstg)+                      (L.qty . L.amount $ e)+    mDC = (L.drCr e) == (L.drCr . L.entry $ pstg)+    mDate = (L.day . L.tDateTime . L.tlCore $ tl) == (Y.unDate . Y.date $ a)+    noFlag = isNothing . L.pNumber $ p+    mCmdty = (L.commodity . L.amount . L.entry $ pstg)+             == (Y.unCurrency . Y.currency $ acct)+++-- | Creates a new transaction corresponding to a given AmexTxn. Uses+-- the Amex payee if that string is non empty; otherwise, uses the+-- Amex description for the payee.+newTransaction+  :: NoAuto+  -> Y.FitAcct+  -> UNumberLookupMap+  -> PyeLookupMap+  -> (Y.UNumber, (Y.Posting, L.Entry))+  -> L.Transaction+newTransaction noAuto acct mu mp (u, (a, e)) = L.Transaction (tld, ents) where+  tld = L.TopLineData tlc Nothing Nothing+  tlc = (L.emptyTopLineCore (L.dateTimeMidnightUTC . Y.unDate . Y.date $ a))+        { L.tPayee = Just pa }+  (pa, ac) = if noAuto then (dfltPye, dfltAcct)+    else ( fromMaybe dfltPye guessedPye,+           fromMaybe dfltAcct guessedAcct)+  (guessedPye, guessedAcct) = guessInfo (Y.toLincolnPayee acct) mu mp a+  dfltPye = getPye (Y.desc a) (Y.payee a)+  dfltAcct = Y.unDefaultAcct . Y.defaultAcct $ acct+  getPye = Y.toLincolnPayee acct+  pennyAcct = Y.unPennyAcct . Y.pennyAcct $ acct+  p1data = L.PostingData p1core Nothing Nothing+  p2data = L.PostingData p2core Nothing Nothing+  p1core = (L.emptyPostingCore pennyAcct)+           { L.pTags = L.Tags [newLincolnUNumber u]+           , L.pSide = Just $ Y.side acct+           , L.pSpaceBetween = Just $ Y.spaceBetween acct+           }+  p2core = L.emptyPostingCore ac+  ents = L.rEnts (Y.unCurrency . Y.currency $ acct) (L.drCr e)+                 (L.qty . L.amount $ e, p1data)+                 [] p2data++-- | Creates new transactions for all the items remaining in the+-- DbMap. Appends a blank line after each one.+createTransactions+  :: NoAuto+  -> Y.FitAcct+  -> [C.LedgerItem]+  -> Y.DbList+  -> DbWithEntry+  -> [C.LedgerItem]+createTransactions noAuto acct led dbLs db =+  concatMap (\i -> [i, S.S4d C.BlankLine])+  . map S.S4a+  . map (newTransaction noAuto acct mu mp)+  . M.assocs+  $ db+  where+    mu = makeUNumberLookup (Y.toLincolnPayee acct) dbLs+    mp = makePyeLookupMap (Y.pennyAcct acct) led++-- | Maps financial institution postings to UNumbers. The key is the+-- Lincoln Payee of the financial institution posting, which is+-- computed using the toLincolnPayee function in the FitAcct.  The+-- UNumbers are in a list, with UNumbers from most recent financial+-- institution postings first.+type UNumberLookupMap = M.Map L.Payee [Y.UNumber]++-- | Create a UNumberLookupMap from a DbWithEntry. Financial+-- institution postings with higher U-numbers will come first.+makeUNumberLookup+  :: (Y.Desc -> Y.Payee -> L.Payee)+  -> Y.DbList+  -> UNumberLookupMap+makeUNumberLookup toPye = foldl' ins M.empty . map f . sortBy g+  where+    ins m (k, v) = M.alter alterer k m+      where alterer Nothing = Just [v]+            alterer (Just ls) = Just $ v:ls+    f (u, p) = (toPye (Y.desc p) (Y.payee p), u)+    g (_, p1) (_, p2) = compare (Y.date p1) (Y.date p2)++-- | Given a list of keys, find the first key that is in the+-- map. Returns Nothing if no key is in the map.+findFirstKey :: Ord k => M.Map k v -> [k] -> Maybe v+findFirstKey _ [] = Nothing+findFirstKey m (k:ks) = case M.lookup k m of+  Nothing -> findFirstKey m ks+  Just v -> Just v++-- | Maps UNumbers to payees and accounts from the ledger.+type PyeLookupMap = M.Map Y.UNumber (Maybe L.Payee, Maybe L.Account)++-- | Makes a payee lookup map. Puts those postings which match the+-- PennyAcct and have a UNumber into the map. (If two postings match+-- the PennyAcct and have the same UNumber, the one that appears later+-- in the ledger file will be in the map.)+makePyeLookupMap :: Y.PennyAcct -> [C.LedgerItem] -> PyeLookupMap+makePyeLookupMap a l+  = M.fromList . mapMaybe f . concatMap L.transactionToPostings+    . mapMaybe toPstg+    $ l+  where+    f pstg = do+      guard $ (Q.account pstg) == Y.unPennyAcct a+      u <- getUNumberFromTags . Q.tags $ pstg+      let tailents = L.tailEnts . snd . L.unPosting $ pstg+          ac = case tailents of+            (x, []) -> Just (L.pAccount . L.pdCore . L.meta $ x)+            _ -> Nothing+      return (u, (Q.payee pstg, ac))+    toPstg = let cn = const Nothing in S.caseS4 Just cn cn cn++-- | Given a UNumber and the maps, looks up the payee and account+-- information from previous transactions if this information is+-- available.+guessInfo+  :: (Y.Desc -> Y.Payee -> L.Payee)+  -> UNumberLookupMap+  -> PyeLookupMap+  -> Y.Posting+  -> (Maybe L.Payee, Maybe L.Account)+guessInfo getPye mu mp p = fromMaybe (Nothing, Nothing) $ do+  let pstgPayee = getPye (Y.desc p) (Y.payee p)+  unums <- M.lookup pstgPayee mu+  findFirstKey mp unums
+ lib/Penny/Brenner/OFX.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Parses any OFX 1.0-series file. Uses the parser from the ofx+-- package.++module Penny.Brenner.OFX (parser) where++import Control.Applicative+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.List (isPrefixOf)+import qualified Data.OFX as O+import qualified Data.Text as X+import qualified Data.Time as T+import qualified Penny.Brenner.Types as Y+import qualified Text.Parsec as P++-- | Parser for OFX files.+parser :: ( Y.ParserDesc, Y.ParserFn )+parser = (Y.ParserDesc d, loadIncoming)+  where+    d = X.unlines+      [ "Parses OFX 1.0-series files."+      , "Open Financial Exchange (OFX) is a standard format"+      , "for providing financial information. It is documented"+      , "at http://www.ofx.net"+      , "This parser also handles QFX files, which are OFX"+      , "files with minor additions by the makers of Quicken."+      , "Many banks make this format available with the label"+      , "\"Download to Quicken\" or similar."+      ]++loadIncoming+  :: Y.FitFileLocation+  -> IO (Ex.Exceptional String [Y.Posting])+loadIncoming (Y.FitFileLocation fn) = do+  contents <- readFile fn+  return $+    ( Ex.mapException show+      . Ex.fromEither+      $ P.parse O.ofxFile fn contents )+    >>= O.transactions+    >>= mapM txnToPosting+++txnToPosting+  :: O.Transaction+  -> Ex.Exceptional String Y.Posting+txnToPosting t = Y.Posting+  <$> pure (Y.Date ( T.utctDay . T.zonedTimeToUTC+                   . O.txDTPOSTED $ t))+  <*> pure (Y.Desc X.empty)+  <*> pure incDec+  <*> amt+  <*> pure ( Y.Payee $ case O.txPayeeInfo t of+              Nothing -> X.empty+              Just ei -> case ei of+                Left x -> X.pack x+                Right p -> X.pack . O.peNAME $ p )+  <*> pure (Y.FitId . X.pack . O.txFITID $ t)+  where+    amtStr = O.txTRNAMT t+    incDec =+      if "-" `isPrefixOf` amtStr then Y.Decrease else Y.Increase+    amt = case amtStr of+      [] -> Ex.throw "empty amount"+      x:xs -> let str = if x == '-' || x == '+' then xs else amtStr+              in Ex.fromMaybe ("could not parse amount: " ++ amtStr)+                 $ Y.mkAmount str+
+ lib/Penny/Brenner/Print.hs view
@@ -0,0 +1,59 @@+-- | Prints parsed transactions.+--+-- TODO add support to this and other Brenner components for reading+-- from stdin.+module Penny.Brenner.Print (mode) where++import qualified Penny.Brenner.Types as Y+import qualified Penny.Brenner.Util as U+import qualified System.Console.MultiArg as MA+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Maybe (mapMaybe)++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ "  [global-options] print [local-options] FILE..."+  , "Parses the transactions in each FILE using the appropriate parser"+  , "and prints the parse result to standard output."+  , ""+  , "Local options:"+  , "  --help, -h Show this help and exit."+  ]++data Arg+  = ArgFile String++mode+  :: MA.Mode (Maybe Y.FitAcct -> IO ())+mode = MA.Mode+  { MA.mName = "print"+  , MA.mIntersperse = MA.Intersperse+  , MA.mOpts = []+  , MA.mPosArgs = return . ArgFile+  , MA.mProcess = processor+  , MA.mHelp = help+  }++processor+  :: [Arg]+  -> Maybe Y.FitAcct+  -> IO ()+processor ls mayFa = do+  fa <- U.getFitAcct mayFa+  doPrint (snd . Y.parser $ fa) ls++doPrint+  :: (Y.FitFileLocation -> IO (Ex.Exceptional String [Y.Posting]))+  -> [Arg]+  -> IO ()+doPrint prsr ls = mapM_ f . mapMaybe toFile $ ls+  where+    f file = do+      r <- prsr file+      case r of+        Ex.Exception s -> do+          fail $ "penny-fit print: error: " ++ s+        Ex.Success ps -> mapM putStr . map U.showPosting $ ps+    toFile a = case a of+      ArgFile s -> Just (Y.FitFileLocation s)+
+ lib/Penny/Brenner/Types.hs view
@@ -0,0 +1,340 @@+module Penny.Brenner.Types+  ( Date(..)+  , IncDec(..)+  , UNumber(..)+  , FitId(..)+  , Payee(..)+  , Desc(..)+  , Amount(unAmount)+  , mkAmount+  , translate+  , DbMap+  , DbList+  , Posting(..)+  , ConfigLocation(..)+  , DbLocation(..)+  , FitAcctName(..)+  , FitAcctDesc(..)+  , ParserDesc(..)+  , PennyAcct(..)+  , Translator(..)+  , DefaultAcct(..)+  , Currency(..)+  , FitAcct(..)+  , Config(..)+  , FitFileLocation(..)+  , AllowNew(..)+  , ParserFn+  ) where++import Control.Applicative ((<$>), (<*>))+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Map as M+import qualified Data.Time as Time+import qualified Penny.Copper.Render as R+import qualified Penny.Lincoln as L+import Data.Text (Text, pack, unpack)+import qualified Data.Text.Encoding as E+import qualified Data.Serialize as S++-- | The date reported by the financial institution.+newtype Date = Date { unDate :: Time.Day }+  deriving (Eq, Show, Ord, Read)++instance S.Serialize Date where+  put = S.put . show . unDate+  get = Date <$> (read <$> S.get)++-- | Reports changes in account balances. Avoids using /debit/ and+-- /credit/ as these terms are used differently by the bank than in+-- your ledger (that is, the bank reports it from their perspective,+-- not yours) so instead the terms /increase/ and /decrease/ are+-- used. IncDec is used to record the bank's transactions so+-- /increase/ and /decrease/ are used in the same way you would see+-- them on a bank statement, whether it's a credit card, loan,+-- checking account, etc.+data IncDec+  = Increase+  -- ^ Increases the account balance. For a checking or savings+  -- account, this is a deposit. For a credit card, this is a purchase.++  | Decrease+  -- ^ Decreases the account balance. On a credit card, this is a+  -- payment. On a checking account, this is a withdrawal.+  deriving (Eq, Show, Read)++instance S.Serialize IncDec where+  put x = case x of+    Increase -> S.putWord8 0+    Decrease -> S.putWord8 1+  get = S.getWord8 >>= f+    where+      f x = case x of+        0 -> return Increase+        1 -> return Decrease+        _ -> fail "read IncDec error"++-- | A unique number assigned by Brenner to identify each+-- posting. This is unique within a particular financial institution+-- account only.+newtype UNumber = UNumber { unUNumber :: Integer }+  deriving (Eq, Show, Ord, Read)++instance S.Serialize UNumber where+  put = S.put . unUNumber+  get = UNumber <$> S.get++putText :: Text -> S.Put+putText = S.put . E.encodeUtf8++getText :: S.Get Text+getText = S.get >>= f+  where+    f bs = case E.decodeUtf8' bs of+      Left _ -> fail "text reading failed"+      Right x -> return x+++-- | For Brenner to work, the bank has to assign unique identifiers to+-- each transaction that it gives you for download. This is the+-- easiest reliable way to ensure duplicates are not processed+-- multiple times. (There are other ways to accomplish this, but they+-- are much harder and less reliable.) If the bank does not do this,+-- you can't use Brenner.+newtype FitId = FitId { unFitId :: Text }+  deriving (Eq, Show, Ord, Read)++instance S.Serialize FitId where+  put = putText . unFitId+  get = FitId <$> getText++-- | Some financial institutions assign a separate Payee in addition+-- to a description. Others just have a single Description field. If+-- this institution uses both, put something here. Brenner will prefer+-- the Payee if it is not zero length; then it will use the Desc.+newtype Payee = Payee { unPayee :: Text }+  deriving (Eq, Show, Ord, Read)++instance S.Serialize Payee where+  put = putText . unPayee+  get = Payee <$> getText++-- | The transaction description. Some institutions assign only a+-- description (sometimes muddling a payee with long codes, some+-- dates, etc). Brenner prefers the Payee if there is one, and uses a+-- Desc otherwise.+newtype Desc =+  Desc { unDesc :: Text }+  deriving (Eq, Show, Ord, Read)++instance S.Serialize Desc where+  put = putText . unDesc+  get = Desc <$> getText++-- | The amount of the transaction. Do not include any leading plus or+-- minus signs; this should be only digits and a decimal point.+newtype Amount = Amount { unAmount :: Text }+  deriving (Eq, Show, Ord, Read)++instance S.Serialize Amount where+  put = putText . unAmount+  get = getText >>= f+    where+      f x = case mkAmount . unpack $ x of+        Nothing -> fail $ "failed to load amount: " ++ unpack x+        Just a -> return a++-- | Ensures that incoming Amounts have only digits and (up to) one+-- decimal point.+mkAmount :: String -> Maybe Amount+mkAmount s =+  let isDigit c = c >= '0' && c <= '9'+      (_, rs) = span isDigit s+  in case rs of+      "" -> if not . null $ s+            then return . Amount . pack $ s+            else Nothing+      '.':rest -> if all isDigit rest+                  then return . Amount . pack $ s+                  else Nothing+      _ -> Nothing++translate+  :: IncDec+  -> Translator+  -> L.DrCr+translate Increase IncreaseIsDebit = L.Debit+translate Increase IncreaseIsCredit = L.Credit+translate Decrease IncreaseIsDebit = L.Credit+translate Decrease IncreaseIsCredit = L.Debit++type DbMap = M.Map UNumber Posting+type DbList = [(UNumber, Posting)]++data Posting = Posting+  { date :: Date+  , desc :: Desc+  , incDec :: IncDec+  , amount :: Amount+  , payee :: Payee+  , fitId :: FitId+  } deriving (Read, Show)+++instance S.Serialize Posting where+  put x = S.put (date x)+          >> S.put (desc x)+          >> S.put (incDec x)+          >> S.put (amount x)+          >> S.put (payee x)+          >> S.put (fitId x)+  get = Posting+        <$> S.get+        <*> S.get+        <*> S.get+        <*> S.get+        <*> S.get+        <*> S.get++-- | Where is a configuration file+newtype ConfigLocation = ConfigLocation+  { unConfigLocation :: Text }+  deriving (Eq, Show)++instance L.HasText ConfigLocation where text = unConfigLocation++-- | Where is the database of postings?+newtype DbLocation = DbLocation { unDbLocation :: Text }+  deriving (Eq, Show)++instance L.HasText DbLocation where text = unDbLocation++-- | Text description of the financial institution account.+newtype FitAcctDesc = FitAcctDesc { unFitAcctDesc :: Text }+  deriving (Eq, Show)++instance L.HasText FitAcctDesc where text = unFitAcctDesc++-- | Text description of the parser itself.+newtype ParserDesc = ParserDesc { unParserDesc :: Text }+  deriving (Eq, Show)++instance L.HasText ParserDesc where text = unParserDesc++-- | A name used to refer to a batch of settings.+newtype FitAcctName = FitAcctName { unFitAcctName :: Text }+  deriving (Eq, Show)++instance L.HasText FitAcctName where text = unFitAcctName++-- | The Penny account holding postings for this financial+-- institution. For instance it might be @Assets:Checking@ if this is+-- your checking account, @Liabilities:Credit Card@, or whatever.+newtype PennyAcct = PennyAcct { unPennyAcct :: L.Account }+  deriving (Eq, Show)++instance L.HasTextList PennyAcct where+  textList = L.textList . unPennyAcct++-- | What the financial institution shows as an increase or decrease+-- has to be recorded as a debit or credit in the PennyAcct.+data Translator+  = IncreaseIsDebit+  -- ^ That is, when the financial institution shows a posting that+  -- increases your account balance, you record a debit. You will+  -- probably use this for deposit accounts, like checking and+  -- savings. These are asset accounts so if the balance goes up you+  -- record a debit in your ledger.++  | IncreaseIsCredit+  -- ^ That is, when the financial institution shows a posting that+  -- increases your account balance, you record a credit. You will+  -- probably use this for liabilities, such as credit cards and other+  -- loans.++  deriving (Eq, Show)++-- | The default account to place unclassified postings in. For+-- instance @Expenses:Unclassified@.+newtype DefaultAcct = DefaultAcct { unDefaultAcct :: L.Account }+  deriving (Eq, Show)++instance L.HasTextList DefaultAcct where+  textList = L.textList . unDefaultAcct++-- | The currency for all transactions, e.g. @$@.+newtype Currency = Currency { unCurrency :: L.Commodity }+  deriving (Eq, Show)++instance L.HasText Currency where text = L.text . unCurrency++-- | A batch of settings representing a single financial institution+-- account.+data FitAcct = FitAcct+  { fitAcctName :: FitAcctName+  , fitAcctDesc :: FitAcctDesc+  , dbLocation :: DbLocation+  , pennyAcct :: PennyAcct+  , defaultAcct :: DefaultAcct+  , currency :: Currency+  , groupSpecs :: R.GroupSpecs+  , translator :: Translator++  , side :: L.Side+  -- ^ When creating new transactions, the commodity will be on this+  -- side++  , spaceBetween :: L.SpaceBetween+  -- ^ When creating new transactions, is there a space between the+  -- commodity and the quantity++  , parser :: ( ParserDesc+              , FitFileLocation -> IO (Ex.Exceptional String [Posting]))+  -- ^ Parses a file of transactions from the financial+  -- institution. The function must open the file and parse it. This+  -- is in the IO monad not only because the function must open the+  -- file itself, but also so the function can perform arbitrary IO+  -- (run pdftotext, maybe?) If there is failure, the function can+  -- return an Exceptional String, which is the error+  -- message. Alternatively the function can raise an exception in the+  -- IO monad (currently Brenner makes no attempt to catch these) so+  -- if any of the IO functions throw you can simply not handle the+  -- exceptions.+  --+  -- The first element of the pair gives information about the parser.++  , toLincolnPayee :: Desc -> Payee -> L.Payee+  -- ^ Sometimes the financial institution provides Payee information,+  -- sometimes it does not. Sometimes the Desc might have additional+  -- information that you might want to remove. This function can be+  -- used to do that. The resulting Lincoln Payee is used for any+  -- transactions that are created by the merge command. The resulting+  -- payee is also used when comparing new financial institution+  -- postings to already existing ledger transactions in order to+  -- guess at which payee and accounts to create in the transactions+  -- created by the merge command.++  }++-- | Configuration for the Brenner program. You can optionally have+-- a default FitAcct, which is used if you do not specify any FitAcct on the+-- command line. You can also name any number of additional FitAccts. If+-- you do not specify a default FitAcct, you must specify a FitAcct on the+-- command line.++data Config = Config+  { defaultFitAcct :: Maybe FitAcct+  , moreFitAccts :: [FitAcct]+  }++newtype FitFileLocation = FitFileLocation { unFitFileLocation :: String }+  deriving (Show, Eq)++newtype AllowNew = AllowNew { unAllowNew :: Bool }+  deriving (Show, Eq)++-- | All parsers must be of this type.+type ParserFn+  = FitFileLocation+  -> IO (Ex.Exceptional String [Posting])+
+ lib/Penny/Brenner/Util.hs view
@@ -0,0 +1,106 @@+module Penny.Brenner.Util where++import Control.Monad.Exception.Synchronous as Ex+import qualified Penny.Brenner.Types as Y+import qualified Data.ByteString as BS+import qualified System.IO.Error as IOE+import qualified Data.Serialize as S+import qualified Data.Text as X+import qualified Penny.Copper.Parsec as CP+import qualified Text.Parsec as P+import qualified Penny.Lincoln as L+import qualified System.Exit as Exit+import qualified System.Console.MultiArg as MA+import qualified System.IO as IO++-- | Print an error message and exit.+errExit :: String -> IO a+errExit s = do+  pn <- MA.getProgName+  IO.hPutStrLn IO.stderr $ pn ++ ": error: " ++ s+  Exit.exitFailure++-- | Gets the FitAcct, if it was provided. If it was not provided,+-- exit with an error message.+getFitAcct :: Maybe Y.FitAcct -> IO Y.FitAcct+getFitAcct ma = case ma of+  Nothing -> errExit $ "no default financial institution account, "+             ++ "and no financial institution account provided"+             ++ " on command line."+  Just a -> return a++-- | Loads the database from disk. If allowNew is True, then does not+-- fail if the file was not found.+loadDb+  :: Y.AllowNew+  -- ^ Is a new file allowed?++  -> Y.DbLocation+  -- ^ DB location++  -> IO Y.DbList+loadDb (Y.AllowNew allowNew) (Y.DbLocation dbLoc) = do+  eiStr <- IOE.tryIOError (BS.readFile . X.unpack $ dbLoc)+  case eiStr of+    Left e ->+      if allowNew && IOE.isDoesNotExistError e+      then return []+      else IOE.ioError e+    Right g -> case readDbTuple g of+      Ex.Exception e -> fail e+      Ex.Success good -> return good++-- | File version. Increment this when anything in the file format+-- changes.+version :: Int+version = 0++brenner :: String+brenner = "penny.brenner"++readDbTuple+  :: BS.ByteString+  -> Ex.Exceptional String Y.DbList+readDbTuple bs = do+  (s, v, ls) <- Ex.fromEither $ S.decode bs+  Ex.assert "database file format not recognized." $ s == brenner+  Ex.assert "wrong database version." $ v == version+  return ls++saveDbTuple :: Y.DbList -> BS.ByteString+saveDbTuple ls = S.encode (brenner, version, ls)++-- | Writes a new database to disk.+saveDb :: Y.DbLocation -> Y.DbList -> IO ()+saveDb (Y.DbLocation p) = BS.writeFile (X.unpack p) . saveDbTuple++-- | Parses quantities from amounts. All amounts should be verified as+-- having only digits, optionally followed by a point and then more+-- digits. All these values should parse. So if there is a problem it+-- is a programmer error. Apply error.+parseQty :: Y.Amount -> L.Qty+parseQty a = case P.parse CP.quantity "" (Y.unAmount a) of+  Left e -> error $ "could not parse quantity from string: "+            ++ (X.unpack . Y.unAmount $ a) ++ ": " ++ show e+  Right g -> g++label :: String -> X.Text -> String+label s x = s ++ ": " ++ X.unpack x ++ "\n"++-- | Shows a Posting in human readable format.+showPosting :: Y.Posting -> String+showPosting (Y.Posting dt dc nc am py fd) =+  label "Date" (X.pack . show . Y.unDate $ dt)+  ++ label "Description" (Y.unDesc dc)+  ++ label "Type" (X.pack $ case nc of+                    Y.Increase -> "increase"+                    Y.Decrease -> "decrease")+  ++ label "Amount" (Y.unAmount am)+  ++ label "Payee" (Y.unPayee py)+  ++ label "Financial institution ID" (Y.unFitId fd)+  ++ "\n"++showDbPair :: (Y.UNumber, Y.Posting) -> String+showDbPair (Y.UNumber u, p) =+  label "U number" (X.pack . show $ u)+  ++ showPosting p
+ lib/Penny/Cabin.hs view
@@ -0,0 +1,7 @@+-- | Cabin - Penny reports+--+-- Cabin contains reports, or functions that take a list of postings+-- and return a formatted Text to display data in a human-readable+-- format.+module Penny.Cabin where+
+ lib/Penny/Cabin/Balance.hs view
@@ -0,0 +1,21 @@+-- | Penny balance reports. Currently there are two balance reports:+-- the MultiCommodity report, which cannot convert commodities and+-- which therefore might show more than one commodity in a single+-- report, and the Convert report, which uses price data in the Penny+-- file to convert all commodities to a single commodity. The Convert+-- report always displays only one commodity per account and this one+-- commodity for the whole report.+module Penny.Cabin.Balance where++import qualified Penny.Cabin.Balance.MultiCommodity as MC+import qualified Penny.Cabin.Interface as I+import qualified Penny.Cabin.Balance.Convert as C+import qualified Penny.Cabin.Balance.Convert.Options as ConvOpts++-- | The default multi-commodity balance report.+multiCommodity :: I.Report+multiCommodity = MC.defaultReport++-- | The default converting balance report.+convert :: I.Report+convert = C.cmdLineReport ConvOpts.defaultOptions
+ lib/Penny/Cabin/Balance/Convert.hs view
@@ -0,0 +1,334 @@+-- | The Convert report. This report converts all account balances to+-- a single commodity, which must be specified.++module Penny.Cabin.Balance.Convert (+  Opts(..)+  , Sorter+  , report+  , cmdLineReport+  , getSorter+  ) where++import Control.Applicative ((<$>), (<*>))+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Tree as E+import qualified Data.Traversable as Tvbl+import qualified Penny.Cabin.Options as CO+import qualified Penny.Cabin.Parsers as CP+import qualified Penny.Cabin.Scheme as Scheme+import qualified Penny.Cabin.Balance.Util as U+import qualified Penny.Cabin.Balance.Convert.Chunker as K+import qualified Penny.Cabin.Balance.Convert.Options as O+import qualified Penny.Cabin.Balance.Convert.Parser as P+import qualified Penny.Cabin.Interface as I+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Balance as Bal+import qualified Penny.Liberty as Ly+import qualified Penny.Shield as S+import qualified Data.Either as Ei+import qualified Data.Map as M+import qualified Data.Text as X+import Data.Monoid (mempty, mappend, mconcat)+import qualified System.Console.MultiArg as MA+import qualified System.Console.Rainbow as Rb++-- | Options for the Convert report. These are the only options you+-- need to use if you are supplying options programatically (as+-- opposed to parsing them in from the command line.)+data Opts = Opts+  { balanceFormat :: L.Commodity -> L.Qty -> X.Text+  , showZeroBalances :: CO.ShowZeroBalances+  , sorter :: Sorter+  , target :: L.To+  , dateTime :: L.DateTime+  , textFormats :: Scheme.Changers+  }++-- | How to sort each line of the report. Each subaccount has only one+-- BottomLine (unlike in the MultiCommodity report, where each+-- subaccount may have more than one BottomLine, one for each+-- commodity.)+type Sorter =+  (L.SubAccount, L.BottomLine)+  -> (L.SubAccount, L.BottomLine)+  -> Ordering++-- | Converts all commodities in a Balance to a single commodity and+-- combines all the BottomLines into one. Fails with an error message+-- if no conversion data is available.+convertBalance ::+  L.PriceDb+  -> L.DateTime+  -> L.To+  -> L.Balance+  -> Ex.Exceptional X.Text L.BottomLine+convertBalance db dt to bal = fmap mconcat r+  where+    r = mapM (convertOne db dt to) . M.assocs . L.unBalance $ bal++-- | Converts a single BottomLine to a new commodity. Fails with an+-- error message if no conversion data is available.+convertOne ::+  L.PriceDb+  -> L.DateTime+  -> L.To+  -> (L.Commodity, L.BottomLine)+  -> Ex.Exceptional X.Text L.BottomLine+convertOne db dt to (cty, bl) =+  case bl of+    L.Zero -> return L.Zero+    L.NonZero (L.Column dc qt) -> Ex.mapExceptional e g ex+      where+        ex = L.convertAsOf db dt to am+        am = L.Amount qt cty+        e = convertError to (L.From cty)+        g r = L.NonZero (L.Column dc r)++-- | Creates an error message for conversion errors.+convertError ::+  L.To+  -> L.From+  -> L.PriceDbError+  -> X.Text+convertError (L.To to) (L.From fr) e =+  let fromErr = L.unCommodity fr+      toErr = 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")+++-- | Create a price database.+buildDb :: [L.PricePoint] -> L.PriceDb+buildDb = foldl f L.emptyDb where+  f db pb = L.addPrice db pb++-- | All data for the report after all balances have been converted to+-- a single commodity and all the sums of the child accounts have been+-- added to the parent accounts.+data ForestAndBL = ForestAndBL {+  _tbForest :: E.Forest (L.SubAccount, L.BottomLine)+  , _tbTotal :: L.BottomLine+  , _tbTo :: L.To+  }++-- | Converts the balance data in preparation for screen rendering.+rows :: ForestAndBL -> ([K.Row], L.To)+rows (ForestAndBL f tot to) = (first:second:rest, to)+  where+    first = K.ROneCol $ K.OneColRow 0 desc+    desc = X.pack "All amounts reported in commodity: "+           `X.append` (L.unCommodity+                       . L.unTo+                       $ to)+    second = K.RMain $ K.MainRow 0 (X.pack "Total") tot+    rest = map mainRow+           . concatMap E.flatten+           . map U.labelLevels+           $ f+++mainRow :: (Int, (L.SubAccount, L.BottomLine)) -> K.Row+mainRow (l, (a, b)) = K.RMain $ K.MainRow l x b+  where+    x = L.text a++-- | The function for the Convert report. Use this function if you are+-- setting the options from a program (as opposed to parsing them in+-- from the command line.) Will fail if the balance conversions fail.+report+  :: Opts+  -> [L.PricePoint]+  -> [(a, L.Posting)]+  -> Ex.Exceptional X.Text [Rb.Chunk]+report os@(Opts getFmt _ _ _ _ txtFormats) ps bs = do+  fstBl <- sumConvertSort os ps bs+  let (rs, L.To cy) = rows fstBl+      fmt = getFmt cy+  return $ K.rowsToChunks txtFormats fmt rs+++-- | Creates a report respecting the standard interface for reports+-- whose options are parsed in from the command line.+cmdLineReport+  :: O.DefaultOpts+  -> I.Report+cmdLineReport o rt = (help o, mkMode)+  where+    mkMode _ _ chgrs _ fsf = MA.Mode+      { MA.mName = "convert"+      , MA.mIntersperse = MA.Intersperse+      , MA.mOpts = map (fmap Right) P.allOptSpecs+      , MA.mPosArgs = return . Left+      , MA.mProcess = process rt chgrs o fsf+      , MA.mHelp = const (help o)+      }++process+  :: S.Runtime+  -> Scheme.Changers+  -> O.DefaultOpts+  -> ([L.Transaction] -> [(Ly.LibertyMeta, L.Posting)])+  -> [Either String (P.Opts -> Ex.Exceptional String P.Opts)]+  -> Ex.Exceptional X.Text I.ArgsAndReport+process rt chgrs defaultOpts fsf ls = do+  let (posArgs, parsed) = Ei.partitionEithers ls+      op' = foldl (>>=) (return (O.toParserOpts defaultOpts rt)) parsed+  case op' of+      Ex.Exception s -> Ex.throw . X.pack $ s+      Ex.Success g -> return $+        let noDefault = X.pack "no default price found"+            f = fromParsedOpts chgrs g+            pr ts pps = do+              rptOpts <- Ex.fromMaybe noDefault $+                f pps (O.format defaultOpts)+              let boxes = fsf ts+              report rptOpts pps boxes+        in (posArgs, pr)+++-- | Sums the balances from the bottom to the top of the tree (so that+-- parent accounts have the sum of the balances of all their+-- children.) Then converts the commodities to a single commodity, and+-- sorts the accounts as requested. Fails if the conversion fails.+sumConvertSort+  :: Opts+  -> [L.PricePoint]+  -> [(a, L.Posting)]+  -> Ex.Exceptional X.Text ForestAndBL+sumConvertSort os ps bs = mkResult <$> convertedFrst <*> convertedTot+  where+    (Opts _ szb str tgt dt _) = os+    bals = U.balances szb bs+    (frst, tot) = U.sumForest mempty mappend bals+    convertBal (a, bal) =+        (\bl -> (a, bl)) <$> convertBalance db dt tgt bal+    db = buildDb ps+    convertedFrst = mapM (Tvbl.mapM convertBal) frst+    convertedTot = convertBalance db dt tgt tot+    mkResult f t = ForestAndBL (U.sortForest str f) t tgt++-- | Determine the most frequent To commodity.+mostFrequent :: [L.PricePoint] -> Maybe L.To+mostFrequent = U.lastMode . map (L.to . L.price)+++type DoReport = [L.PricePoint]+               -> (L.Commodity -> L.Qty -> X.Text)+               -> (Maybe Opts)++-- | Get options for the report, depending on what options were parsed+-- from the command line. Fails if the user did not specify a+-- commodity and mostFrequent fails.+fromParsedOpts+  :: Scheme.Changers+  -> P.Opts+  -> DoReport+fromParsedOpts chgrs (P.Opts szb tgt dt so sb) =+  \pps fmt -> case tgt of+    P.ManualTarget to ->+      Just $ Opts fmt szb (getSorter so sb) to dt chgrs+    P.AutoTarget ->+      case mostFrequent pps of+        Nothing -> Nothing+        Just to ->+          Just $ Opts fmt szb (getSorter so sb) to dt chgrs++-- | Returns a function usable to sort pairs of SubAccount and+-- BottomLine depending on how you want them sorted.+getSorter :: CP.SortOrder -> P.SortBy -> Sorter+getSorter o b = flipper f+  where+    flipper = case o of+      CP.Ascending -> id+      CP.Descending ->+        \g p1 p2 -> case g p1 p2 of+            LT -> GT+            GT -> LT+            EQ -> EQ+    f p1@(a1, _) p2@(a2, _) = case b of+      P.SortByName -> compare a1 a2+      P.SortByQty -> cmpBottomLine p1 p2++cmpBottomLine :: Sorter+cmpBottomLine (n1, bl1) (n2, bl2) =+  case (bl1, bl2) of+    (L.Zero, L.Zero) -> EQ+    (L.NonZero _, L.Zero) -> LT+    (L.Zero, L.NonZero _) -> GT+    (L.NonZero c1, L.NonZero c2) ->+      mconcat [dc, qt, na]+      where+        dc = case (Bal.colDrCr c1, Bal.colDrCr c2) of+          (L.Debit, L.Debit) -> EQ+          (L.Debit, L.Credit) -> LT+          (L.Credit, L.Debit) -> GT+          (L.Credit, L.Credit) -> EQ+        qt = compare (Bal.colQty c1) (Bal.colQty c2)+        na = compare n1 n2++------------------------------------------------------------+-- ## Help+------------------------------------------------------------+ifDefault :: Bool -> String+ifDefault b = if b then " (default)" else ""++help :: O.DefaultOpts -> String+help o = unlines $+  [ "convert"+  , "  Show account balances, after converting all amounts"+  , "  to a single commodity. Accepts ONLY the following options:"+  , ""+  , "--show-zero-balances"+  , "  Show balances that are zero"+    ++ ifDefault (CO.unShowZeroBalances . O.showZeroBalances $ o)+  , "--hide-zero-balances"+  , "  Hide balances that are zero"+    ++ ifDefault (not . CO.unShowZeroBalances . O.showZeroBalances $ o)+  , ""+  , "--commodity TARGET-COMMMODITY, -c TARGET-COMMODITY"+  , "  Convert all commodities to TARGET-COMMODITY."+  ] ++ case O.target o of+        P.ManualTarget (L.To cy) ->+          [ "  default: " ++ (X.unpack . L.unCommodity $ cy) ]+        _ -> []+    +++  [ "--auto-commodity"+  , "  convert all commodities to the commodity that appears most"+  , "  often as the target commodity in your price data. If"+  , "  there is a tie, the price closest to the end of your list"+  , "  of prices is used."+    ++ case O.target o of+        P.AutoTarget -> " (default)"+        _ -> ""+  , ""+  , "--date DATE-TIME, -d DATE-TIME"+  , "  Convert prices as of the date and time given"+  , "  (by default, the current date and time is used.)"+  , ""+  , "--sort qty|name, -s qty|name"+  , "  Sort balances by sub-account name"+    ++ ifDefault (O.sortBy o == P.SortByName)+    ++ " or by quantity"+    ++ ifDefault (O.sortBy o == P.SortByQty)+  , "--ascending"+  , "  Sort in ascending order"+    ++ ifDefault (O.sortOrder o == CP.Ascending)+  , "--descending"+  , "  Sort in descending order"+    ++ ifDefault (O.sortOrder o == CP.Descending)+  , ""+  , "--help, -h"+  , "  Show this help and exit"+  ]+
+ lib/Penny/Cabin/Balance/Convert/Chunker.hs view
@@ -0,0 +1,239 @@+-- | Creates the output Chunks for the Balance report for+-- multi-commodity reports only.++module Penny.Cabin.Balance.Convert.Chunker (+  MainRow(..),+  OneColRow(..),+  Row(..),+  rowsToChunks+  ) where+++import Control.Applicative+  (Applicative (pure), (<$>), (<*>))+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Cabin.Meta as Meta+import qualified Penny.Cabin.Row as R+import qualified Penny.Lincoln as L+import qualified Data.Foldable as Fdbl+import qualified Data.Text as X+import qualified System.Console.Rainbow as Rb++type IsEven = Bool++data Columns a = Columns {+  acct :: a+  , drCr :: a+  , quantity :: a+  } deriving Show++instance Functor Columns where+  fmap f c = Columns {+    acct = f (acct c)+    , drCr = f (drCr c)+    , quantity = f (quantity c)+    }++instance Applicative Columns where+  pure a = Columns a a a+  fn <*> fa = Columns {+    acct = (acct fn) (acct fa)+    , drCr = (drCr fn) (drCr fa)+    , quantity = (quantity fn) (quantity fa)+     }++data PreSpec = PreSpec {+  _justification :: R.Justification+  , _padSpec :: (E.Label, E.EvenOdd)+  , bits :: Rb.Chunk }++-- | 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 (R.Width . X.length . Rb.chunkText . 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+++widthSpacerAcct :: Int+widthSpacerAcct = 4++widthSpacerDrCr :: Int+widthSpacerDrCr = 1++colsToBits+  :: E.Changers+  -> IsEven+  -> Columns R.ColumnSpec+  -> [Rb.Chunk]+colsToBits chgrs isEven (Columns a dc q) = let+  fillSpec = if isEven+             then (E.Other, E.Even)+             else (E.Other, E.Odd)+  spacer w = R.ColumnSpec j (R.Width w) fillSpec []+  j = R.LeftJustify+  cs = a+       : spacer widthSpacerAcct+       : dc+       : spacer widthSpacerDrCr+       : q+       : []+  in R.row chgrs cs++colsListToBits+  :: E.Changers+  -> [Columns R.ColumnSpec]+  -> [[Rb.Chunk]]+colsListToBits chgrs = zipWith f bools where+  f b c = colsToBits chgrs b c+  bools = iterate not True++preSpecsToBits+  :: E.Changers+  -> [Columns PreSpec]+  -> [Rb.Chunk]+preSpecsToBits chgrs =+  concat+  . colsListToBits chgrs+  . resizeColumnsInList++data Row = RMain MainRow | ROneCol OneColRow++-- | Displays a one-column row.+data OneColRow = OneColRow {+  ocIndentation :: Int+  -- ^ Indent the text by this many levels (not by this many+  -- spaces; this number is multiplied by another number in the+  -- Chunker source to arrive at the final indentation amount)++  , ocText :: X.Text+  -- ^ Text for the left column+  }++-- | Displays a single account in a Balance report. In a+-- single-commodity report, this account will only be one screen line+-- long. In a multi-commodity report, it might be multiple lines long,+-- with one screen line for each commodity.+data MainRow = MainRow {+  mrIndentation :: Int+  -- ^ Indent the account name by this many levels (not by this many+  -- spaces; this number is multiplied by another number in the+  -- Chunker source to arrive at the final indentation amount)++  , mrText :: X.Text+  -- ^ Text for the name of the account++  , mrBottomLine :: L.BottomLine+  -- ^ Commodity balances. If this list is empty, dashes are+  -- displayed for the DrCr and Qty.+  }+++rowsToChunks+  :: E.Changers+  -> (L.Qty -> X.Text)+  -- ^ How to format a balance to allow for digit grouping+  -> [Row]+  -> [Rb.Chunk]+rowsToChunks chgrs fmt =+  preSpecsToBits chgrs+  . rowsToColumns chgrs fmt++rowsToColumns+  :: E.Changers+  -> (L.Qty -> X.Text)+  -- ^ How to format a balance to allow for digit grouping++  -> [Row]+  -> [Columns PreSpec]+rowsToColumns chgrs fmt+  = map (mkRow chgrs fmt)+  . L.serialItems (\ser r -> (Meta.VisibleNum ser, r))+++mkRow+  :: E.Changers+  -> (L.Qty -> X.Text)+  -> (Meta.VisibleNum, Row)+  -> Columns PreSpec+mkRow chgrs fmt (vn, r) = case r of+  RMain m -> mkMainRow chgrs fmt (vn, m)+  ROneCol c -> mkOneColRow chgrs (vn, c)++mkOneColRow+  :: E.Changers+  -> (Meta.VisibleNum, OneColRow)+  -> Columns PreSpec+mkOneColRow chgrs (vn, (OneColRow i t)) = Columns ca cd cq+  where+    txt = X.append indents t+    indents = X.replicate (indentAmount * max 0 i)+              (X.singleton ' ')+    eo = E.fromVisibleNum vn+    lbl = E.Other+    ca = PreSpec R.LeftJustify (lbl, eo)+         (E.getEvenOddLabelValue lbl eo chgrs $ Rb.plain txt)+    cd = PreSpec R.LeftJustify (lbl, eo)+         (E.getEvenOddLabelValue lbl eo chgrs $ Rb.plain X.empty)+    cq = cd++mkMainRow+  :: E.Changers+  -> (L.Qty -> X.Text)+  -> (Meta.VisibleNum, MainRow)+  -> Columns PreSpec+mkMainRow chgrs fmt (vn, (MainRow i acctTxt b)) = Columns ca cd cq+  where+    applyFmt = E.getEvenOddLabelValue lbl eo chgrs+    eo = E.fromVisibleNum vn+    lbl = E.Other+    ca = PreSpec R.LeftJustify (lbl, eo) (applyFmt (Rb.plain txt))+      where+        txt = X.append indents acctTxt+        indents = X.replicate (indentAmount * max 0 i)+                  (X.singleton ' ')+    cd = PreSpec R.LeftJustify (lbl, eo) (applyFmt cksDrCr)+    cq = PreSpec R.LeftJustify (lbl, eo) (applyFmt cksQty)+    (cksDrCr, cksQty) = balanceChunks chgrs fmt vn b+++balanceChunks+  :: E.Changers+  -> (L.Qty -> X.Text)+  -> Meta.VisibleNum+  -> L.BottomLine+  -> (Rb.Chunk, Rb.Chunk)+balanceChunks chgrs fmt vn bl = (chkDc, chkQt)+  where+    eo = E.fromVisibleNum vn+    chkDc = E.bottomLineToDrCr bl eo chgrs+    qtFmt = E.getEvenOddLabelValue lbl eo chgrs+    chkQt = qtFmt $ Rb.plain t+    (lbl, t) = case bl of+      L.Zero -> (E.Zero, X.pack "--")+      L.NonZero (L.Column dc qt) -> (E.dcToLbl dc, fmt qt)+++indentAmount :: Int+indentAmount = 2+
+ lib/Penny/Cabin/Balance/Convert/Options.hs view
@@ -0,0 +1,43 @@+-- | Default options for the Convert report when used from the command+-- line.+module Penny.Cabin.Balance.Convert.Options where++import qualified Penny.Cabin.Balance.Convert.Parser as P+import qualified Penny.Cabin.Parsers as CP+import qualified Penny.Cabin.Options as CO+import qualified Penny.Lincoln as L+import qualified Penny.Shield as S+import qualified Data.Text as X++-- | Default options for the Convert report. This record is used as+-- the starting point when parsing in options from the command+-- line. You don't need to use it if you are setting the options for+-- the Convert report directly from your own code.++data DefaultOpts = DefaultOpts+  { showZeroBalances :: CO.ShowZeroBalances+  , target :: P.Target+  , sortOrder :: CP.SortOrder+  , sortBy :: P.SortBy+  , format :: L.Commodity -> L.Qty -> X.Text+  }++toParserOpts :: DefaultOpts -> S.Runtime -> P.Opts+toParserOpts d rt = P.Opts+  { P.showZeroBalances = showZeroBalances d+  , P.target = target d+  , P.dateTime = S.currentTime rt+  , P.sortOrder = sortOrder d+  , P.sortBy = sortBy d+  }++defaultOptions :: DefaultOpts+defaultOptions = DefaultOpts+  { showZeroBalances = CO.ShowZeroBalances False+  , target = P.AutoTarget+  , sortOrder = CP.Ascending+  , sortBy = P.SortByName+  , format = \_ q -> X.pack . L.prettyShowQty $ q+  }++
+ lib/Penny/Cabin/Balance/Convert/Parser.hs view
@@ -0,0 +1,87 @@+-- | Parsing options for the Convert report from the command line.+module Penny.Cabin.Balance.Convert.Parser (+  Opts(..)+  , Target(..)+  , SortBy(..)+  , allOptSpecs+  ) where+++import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Text as X+import qualified Penny.Cabin.Options as CO+import qualified Penny.Cabin.Parsers as P+import qualified Penny.Lincoln as L+import qualified Penny.Copper.Parsec as Pc+import qualified System.Console.MultiArg.Combinator as C+import qualified Text.Parsec as Parsec+++-- | Is the target commodity determined by the user or automatically?+data Target = AutoTarget | ManualTarget L.To++data SortBy = SortByQty | SortByName deriving (Eq, Show, Ord)++-- | Default starting options for the Convert report. After+-- considering what is parsed in from the command line and price data,+-- a Convert.Opts will be generated.+data Opts = Opts+  { showZeroBalances :: CO.ShowZeroBalances+  , target :: Target+  , dateTime :: L.DateTime+  , sortOrder :: P.SortOrder+  , sortBy :: SortBy+  }++-- | Do not be tempted to change the setup in this module so that the+-- individual functions such as parseColor and parseBackground return+-- parsers rather than OptSpec. Such an arrangement breaks the correct+-- parsing of abbreviated long options.+allOptSpecs :: [C.OptSpec (Opts -> Ex.Exceptional String Opts)]+allOptSpecs =+  [ fmap toExc parseZeroBalances+  , parseCommodity+  , fmap toExc parseAuto+  , parseDate+  , fmap toExc parseSort+  , fmap toExc parseOrder ]+  where+    toExc f = return . f++parseZeroBalances :: C.OptSpec (Opts -> Opts)+parseZeroBalances = fmap f P.zeroBalances+  where+    f x o = o { showZeroBalances = x }+++parseCommodity :: C.OptSpec (Opts -> Ex.Exceptional String Opts)+parseCommodity = C.OptSpec ["commodity"] "c" (C.OneArg f)+  where+    f a1 os =+      case Parsec.parse Pc.lvl1Cmdty "" (X.pack a1) of+        Left _ -> Ex.throw $ "invalid commodity: " ++ a1+        Right g -> return $ os { target = ManualTarget . L.To $ g }++parseAuto :: C.OptSpec (Opts -> Opts)+parseAuto = C.OptSpec ["auto-commodity"] "" (C.NoArg f)+  where+    f os = os { target = AutoTarget }++parseDate :: C.OptSpec (Opts -> Ex.Exceptional String Opts)+parseDate = C.OptSpec ["date"] "d" (C.OneArg f)+  where+    f a1 os =+      case Parsec.parse Pc.dateTime "" (X.pack a1) of+        Left _ -> Ex.throw $ "invalid date: " ++ a1+        Right g -> return $ os { dateTime = g }++parseSort :: C.OptSpec (Opts -> Opts)+parseSort = C.OptSpec ["sort"] "s" (C.ChoiceArg ls)+  where+    ls = [ ("qty", (\os -> os { sortBy = SortByQty }))+         , ("name", (\os -> os { sortBy = SortByName })) ]++parseOrder :: C.OptSpec (Opts -> Opts)+parseOrder = fmap f P.order+  where+    f x o = o { sortOrder = x }
+ lib/Penny/Cabin/Balance/MultiCommodity.hs view
@@ -0,0 +1,177 @@+-- | The multi-commodity Balance report. This is the simpler balance+-- report because it does not allow for commodities to be converted.++module Penny.Cabin.Balance.MultiCommodity (+  Opts(..),+  defaultOpts,+  defaultParseOpts,+  defaultFormat,+  parseReport,+  defaultReport,+  report+  ) where++import Control.Applicative (Applicative, pure)+import qualified Penny.Cabin.Balance.Util as U+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Cabin.Scheme.Schemes as Schemes+import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import qualified Data.Either as Ei+import qualified Data.Map as M+import qualified Penny.Cabin.Options as CO+import Data.Monoid (mappend, mempty)+import qualified Data.Text as X+import qualified Data.Tree as E+import qualified Penny.Cabin.Balance.MultiCommodity.Chunker as K+import qualified Penny.Cabin.Balance.MultiCommodity.Parser as P+import qualified Penny.Cabin.Interface as I+import qualified Penny.Cabin.Parsers as CP+import qualified System.Console.MultiArg as MA+import qualified System.Console.Rainbow as R++-- | Options for making the balance report. These are the only options+-- needed to make the report if the options are not being parsed in+-- from the command line.+data Opts = Opts+  { balanceFormat :: L.Commodity -> L.Qty -> X.Text+  , showZeroBalances :: CO.ShowZeroBalances+  , order :: L.SubAccount -> L.SubAccount -> Ordering+  , textFormats :: E.Changers+  }++defaultOpts :: Opts+defaultOpts = Opts+  { balanceFormat = defaultFormat+  , showZeroBalances = CO.ShowZeroBalances True+  , order = compare+  , textFormats = Schemes.darkLabels+  }++defaultParseOpts :: P.ParseOpts+defaultParseOpts = P.ParseOpts+  { P.showZeroBalances = CO.ShowZeroBalances False+  , P.order = CP.Ascending+  }++fromParseOpts+  :: E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> P.ParseOpts+  -> Opts+fromParseOpts chgrs fmt (P.ParseOpts szb o) = Opts fmt szb o' chgrs+  where+    o' = case o of+       CP.Ascending -> compare+       CP.Descending -> CO.descending compare++defaultFormat :: a -> L.Qty -> X.Text+defaultFormat _ = X.pack . L.prettyShowQty++summedSortedBalTree ::+  CO.ShowZeroBalances+  -> (L.SubAccount -> L.SubAccount -> Ordering)+  -> [(a, L.Posting)]+  -> (E.Forest (L.SubAccount, L.Balance), L.Balance)+summedSortedBalTree szb o =+  U.sumForest mempty mappend+  . U.sortForest o'+  . U.balances szb+  where+    o' x y = o (fst x) (fst y)++rows ::+  (E.Forest (L.SubAccount, L.Balance), L.Balance)+  -> [K.Row]+rows (o, b) = first:rest+  where+    first = K.Row 0 (X.pack "Total") (M.assocs . L.unBalance $ b)+    rest = map row . concatMap E.flatten . map U.labelLevels $ o+    row (l, (s, ib)) =+      K.Row l (L.text s) (M.assocs . L.unBalance $ ib)++-- | This report is what to use if you already have your options (that+-- is, you are not parsing them in from the command line.)+report :: Opts -> [(a, L.Posting)] -> [R.Chunk]+report (Opts bf szb o chgrs) =+  K.rowsToChunks chgrs bf+  . rows+  . summedSortedBalTree szb o++-- | The MultiCommodity report with configurable options that have+-- been parsed from the command line.+parseReport ::+  (L.Commodity -> L.Qty -> X.Text)+  -- ^ How to format balances. For instance you can use this to+  -- perform commodity-sensitive digit grouping.++  -> P.ParseOpts+  -- ^ Default options for the report. These can be overriden on the+  -- command line.++  -> I.Report+parseReport fmt o rt = (help o, makeMode)+  where+    makeMode _ _ chgrs _ fsf = MA.Mode+      { MA.mName = "balance"+      , MA.mIntersperse = MA.Intersperse+      , MA.mOpts = map (fmap Right) P.allSpecs+      , MA.mPosArgs = return . Left+      , MA.mProcess = process chgrs fmt o rt fsf+      , MA.mHelp = const (help o)+      }++process+  :: Applicative f+  => E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> P.ParseOpts+  -> a+  -> ([L.Transaction] -> [(Ly.LibertyMeta, L.Posting)])+  -> [Either String (P.ParseOpts -> P.ParseOpts)]+  -> f I.ArgsAndReport+process chgrs fmt o _ fsf ls =+  let (posArgs, fns) = Ei.partitionEithers ls+      mkParsedOpts = foldl (flip (.)) id fns+      os' = mkParsedOpts o+      mcOpts = fromParseOpts chgrs fmt os'+      pr txns _ = return $ report mcOpts (fsf txns)+  in pure (posArgs, pr)+++-- | The MultiCommodity report, with default options.+defaultReport :: I.Report+defaultReport = parseReport defaultFormat defaultParseOpts++------------------------------------------------------------+-- ## Help+------------------------------------------------------------+ifDefault :: Bool -> String+ifDefault b = if b then " (default)" else ""++help :: P.ParseOpts -> String+help o = unlines+  [ "balance"+  , "  Show account balances. Accepts ONLY the following options:"+  , ""+  , "--show-zero-balances"+  , "  Show balances that are zero"+    ++ ifDefault (CO.unShowZeroBalances . P.showZeroBalances $ o)+  , "--hide-zero-balances"+  , "  Hide balances that are zero"+    ++ ifDefault ( not . CO.unShowZeroBalances+                 . P.showZeroBalances $ o)+  , ""+  , "--ascending"+  , "  Sort in ascending order by account name"+    ++ ifDefault (P.order o == CP.Ascending)++  , "--descending"+  , "  Sort in descending order by account name"+    ++ ifDefault (P.order o == CP.Descending)++  , ""+  , "--help, -h"+  , "  Show this help and exit"+  ]+
+ lib/Penny/Cabin/Balance/MultiCommodity/Chunker.hs view
@@ -0,0 +1,222 @@+-- | Creates the output Chunks for the Balance report for both+-- multi-commodity reports.++module Penny.Cabin.Balance.MultiCommodity.Chunker (+  Row(..),+  rowsToChunks+  ) where+++import Control.Applicative+  (Applicative (pure), (<$>), (<*>))+import qualified Penny.Cabin.Meta as Meta+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Lincoln as L+import qualified Data.Foldable as Fdbl+import qualified Data.Text as X+import qualified System.Console.Rainbow as Rb++type IsEven = Bool++data Columns a = Columns {+  acct :: a+  , drCr :: a+  , commodity :: a+  , quantity :: a+  } deriving Show++instance Functor Columns where+  fmap f c = Columns {+    acct = f (acct 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 {+    acct = (acct fn) (acct fa)+    , drCr = (drCr fn) (drCr fa)+    , commodity = (commodity fn) (commodity fa)+    , quantity = (quantity fn) (quantity fa)+     }++data PreSpec = PreSpec {+  _justification :: R.Justification+  , _padSpec :: (E.Label, E.EvenOdd)+  , bits :: [Rb.Chunk] }++-- | 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 ( safeMaximum (R.Width 0)+                        . map (R.Width . X.length . Rb.chunkText)+                        . bits $ new)+  safeMaximum d ls = if null ls then d else maximum ls++-- | 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+  :: E.Changers+  -> IsEven+  -> Columns R.ColumnSpec+  -> [Rb.Chunk]+colsToBits chgrs isEven (Columns a dc c q) = let+  fillSpec = if isEven+             then (E.Other, E.Even)+             else (E.Other, E.Odd)+  spacer w = R.ColumnSpec j (R.Width w) fillSpec []+  j = R.LeftJustify+  cs = a+       : spacer widthSpacerAcct+       : dc+       : spacer widthSpacerDrCr+       : c+       : spacer widthSpacerCommodity+       : q+       : []+  in R.row chgrs cs++colsListToBits+  :: E.Changers+  -> [Columns R.ColumnSpec]+  -> [[Rb.Chunk]]+colsListToBits chgrs = zipWith f bools where+  f b c = colsToBits chgrs b c+  bools = iterate not True++preSpecsToBits+  :: E.Changers+  -> [Columns PreSpec]+  -> [Rb.Chunk]+preSpecsToBits chgrs =+  concat+  . colsListToBits chgrs+  . resizeColumnsInList++-- | Displays a single account in a Balance report. In a+-- single-commodity report, this account will only be one screen line+-- long. In a multi-commodity report, it might be multiple lines long,+-- with one screen line for each commodity.+data Row = Row+  { indentation :: Int+  -- ^ Indent the account name by this many levels (not by this many+  -- spaces; this number is multiplied by another number in the+  -- Chunker source to arrive at the final indentation amount)++  , accountTxt :: X.Text+    -- ^ Text for the name of the account++  , balances :: [(L.Commodity, L.BottomLine)]+    -- ^ Commodity balances. If this list is empty, dashes are+    -- displayed for the DrCr, Commodity, and Qty.+  }++rowsToChunks+  :: E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -- ^ How to format a balance to allow for digit grouping+  -> [Row]+  -> [Rb.Chunk]+rowsToChunks chgrs fmt =+  preSpecsToBits chgrs+  . rowsToColumns chgrs fmt++rowsToColumns+  :: E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -- ^ How to format a balance to allow for digit grouping++  -> [Row]+  -> [Columns PreSpec]+rowsToColumns chgrs fmt+  = map (mkColumn chgrs fmt)+  . L.serialItems (\ser a -> (Meta.VisibleNum ser, a))+++mkColumn+  :: E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> (Meta.VisibleNum, Row)+  -> Columns PreSpec+mkColumn chgrs fmt (vn, (Row i acctTxt bs)) = Columns ca cd cc cq+  where+    lbl = E.Other+    eo = E.fromVisibleNum vn+    applyFmt = E.getEvenOddLabelValue lbl eo chgrs+    ca = PreSpec R.LeftJustify (lbl, eo) [applyFmt $ Rb.plain txt]+      where+        txt = X.append indents acctTxt+        indents = X.replicate (indentAmount * max 0 i)+                  (X.singleton ' ')+    cd = PreSpec R.LeftJustify (lbl, eo) cksDrCr+    cc = PreSpec R.RightJustify (lbl, eo) cksCmdty+    cq = PreSpec R.LeftJustify (lbl, eo) cksQty+    (cksDrCr, cksCmdty, cksQty) =+      if null bs+      then balanceChunksEmpty chgrs eo+      else+        let balChks = map (balanceChunks chgrs fmt eo) bs+            cDrCr = map (\(a, _, _) -> a) balChks+            cCmdty = map (\(_, a, _) -> a) balChks+            cQty = map (\(_, _, a) -> a) balChks+        in (cDrCr, cCmdty, cQty)+++balanceChunksEmpty+  :: E.Changers+  -> E.EvenOdd+  -> ([Rb.Chunk], [Rb.Chunk], [Rb.Chunk])+balanceChunksEmpty chgrs eo = (dash, dash, dash)+  where+    dash = [E.getEvenOddLabelValue E.Other eo chgrs $ Rb.plain (X.pack "--")]++balanceChunks+  :: E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> E.EvenOdd+  -> (L.Commodity, L.BottomLine)+  -> (Rb.Chunk, Rb.Chunk, Rb.Chunk)+balanceChunks chgrs fmt eo (cty, bl) = (chkDc, chkCt, chkQt)+  where+    chkDc = E.bottomLineToDrCr bl eo chgrs+    chkCt = E.bottomLineToCmdty chgrs eo (cty, bl)+    chkQt = E.bottomLineToQty chgrs fmt eo (cty, bl)+++indentAmount :: Int+indentAmount = 2+
+ lib/Penny/Cabin/Balance/MultiCommodity/Parser.hs view
@@ -0,0 +1,29 @@+module Penny.Cabin.Balance.MultiCommodity.Parser (+  ParseOpts(..)+  , allSpecs+  ) where++import qualified Penny.Cabin.Options as CO+import qualified Penny.Cabin.Parsers as P+import qualified System.Console.MultiArg as MA++-- | Options for the Balance report that have been parsed from the+-- command line.+data ParseOpts = ParseOpts+  { showZeroBalances :: CO.ShowZeroBalances+  , order :: P.SortOrder+  }+++zeroBalances :: MA.OptSpec (ParseOpts -> ParseOpts)+zeroBalances = fmap toResult P.zeroBalances+  where+    toResult szb o = o { showZeroBalances = szb }++parseOrder :: MA.OptSpec (ParseOpts -> ParseOpts)+parseOrder = fmap toResult P.order+  where+    toResult x o = o { order = x }++allSpecs :: [MA.OptSpec (ParseOpts -> ParseOpts)]+allSpecs = [zeroBalances, parseOrder]
+ lib/Penny/Cabin/Balance/Util.hs view
@@ -0,0 +1,230 @@+-- | Grab bag of utility functions.++module Penny.Cabin.Balance.Util+  ( tieredForest+  , tieredPostings+  , filterForest+  , balances+  , flatten+  , treeWithParents+  , forestWithParents+  , sumForest+  , sumTree+  , boxesBalance+  , labelLevels+  , sortForest+  , sortTree+  , lastMode+  ) where++import qualified Penny.Cabin.Options as CO+import qualified Penny.Lincoln as L+import qualified Penny.Steel.NestedMap as NM+import qualified Data.Foldable as Fdbl+import qualified Data.Map as M+import Data.Ord (comparing)+import Data.List (sortBy, maximumBy, groupBy)+import Data.Monoid (mconcat, Monoid)+import Data.Maybe (mapMaybe)+import qualified Data.Tree as T+import qualified Penny.Lincoln.Queries as Q++-- | Constructs a forest sorted into tiers based on lists of keys that+-- are extracted from the elements.+tieredForest ::+  Ord k+  => (a -> [k])+  -- ^ Extracts a key from the elements we are putting in the tree. If+  -- this function returns an empty list for any element, the element+  -- will not appear in the tiered forest.+  -> [a]+  -> T.Forest (k, [a])+tieredForest getKeys ls = fmap (fmap revSnd) . NM.toForest $ nm+  where+    revSnd (a, xs) = (a, reverse xs)+    nm = foldr f NM.empty ls+    f a m = NM.relabel m ps+      where+        ps = case getKeys a of+          [] -> []+          ks ->+            let mkInitPair k = (k, maybe [] id)+                mkLastPair k = (k, maybe [a] (a:))+            in (map mkInitPair . init $ ks)+               ++ [(mkLastPair (last ks))]++-- | Takes a list of postings and puts them into a Forest. Each level+-- of each of the trees corresponds to a sub account. The label of the+-- node tells you the sub account name and gives you a list of the+-- postings at that level.+tieredPostings+  :: [(a, L.Posting)]+  -> T.Forest (L.SubAccount, [(a, L.Posting)])+tieredPostings = tieredForest e+  where+    e = Fdbl.toList . L.unAccount . Q.account . snd++-- | Keeps only Trees that match a given condition. First examines+-- child trees to determine whether they should be retained. If a+-- child tree is retained, does not delete the parent tree.+filterForest :: (a -> Bool) -> T.Forest a -> T.Forest a+filterForest f = mapMaybe pruneTree+  where+    pruneTree (T.Node a fs) =+      case filterForest f fs of+        [] -> if not (f a) then Nothing else Just (T.Node a [])+        cs -> Just (T.Node a cs)+++-- | Puts all Boxes into a Tree and sums the balances. Removes+-- accounts that have empty balances if requested. Does NOT sum+-- balances from the bottom up.+balances ::+  CO.ShowZeroBalances+  -> [(a, L.Posting)]+  -> T.Forest (L.SubAccount, L.Balance)+balances (CO.ShowZeroBalances szb) =+  remover+  . map (fmap (mapSnd boxesBalance))+  . tieredPostings+  where+    remover =+      if szb+      then id+      else filterForest (not . M.null . L.unBalance . snd)+           . map (fmap (mapSnd L.removeZeroCommodities))+++-- | Takes a tree of Balances (like what is produced by the 'balances'+-- function) and produces a flat list of accounts with the balance of+-- each account.+flatten+  :: T.Forest (L.SubAccount, L.Balance)+  -> [(L.Account, L.Balance)]+flatten =+  concatMap T.flatten+  . map (fmap toPair) . forestWithParents+  where+    toPair ((s, b), ls) =+      case reverse . map fst $ ls of+        [] -> (L.Account [s], b)+        s1:sr -> (L.Account (s1 : (sr ++ [s])), b)++-- | Takes a Tree and returns a Tree where each node has information+-- about its parent Nodes. The list of parent nodes has the most+-- immediate parent first and the most distant parent last.+treeWithParents :: T.Tree a -> T.Tree (a, [a])+treeWithParents = treeWithParentsR []++-- | Given a list of the parents seen so far, return a Tree where each+-- node contains information about its parents.+treeWithParentsR :: [a] -> T.Tree a -> T.Tree (a, [a])+treeWithParentsR ls (T.Node n cs) = T.Node (n, ls) cs'+  where+    cs' = map (treeWithParentsR (n:ls)) cs++-- | Takes a Forest and returns a Forest where each node has+-- information about its parent Nodes.+forestWithParents :: T.Forest a -> T.Forest (a, [a])+forestWithParents = map (treeWithParentsR [])++-- | Sums a forest from the bottom up. Returns a pair, where the first+-- element is the forest, but with the second element of each node+-- replaced with the sum of that node and all its children. The second+-- element is the sum of all the second elements in the forest.+sumForest ::+  s+  -- ^ Zero++  -> (s -> s -> s)+  -- ^ Combiner++  -> T.Forest (a, s)+  -> (T.Forest (a, s), s)+sumForest z f ts = (ts', s)+  where+    ts' = map (sumTree z f) ts+    s = foldr f z . map (snd . T.rootLabel) $ ts'++-- | Sums a tree from the bottom up.+sumTree ::+  s+  -- ^ Zero++  -> (s -> s -> s)+  -- ^ Combiner++  ->  T.Tree (a, s)+  -> T.Tree (a, s)+sumTree z f (T.Node (a, s) cs) = T.Node (a, f s cSum) cs'+  where+    (cs', cSum) = sumForest z f cs+++boxesBalance :: [(a, L.Posting)] -> L.Balance+boxesBalance = mconcat . map L.entryToBalance . map Q.entry+               . map snd++mapSnd :: (a -> b) -> (f, a) -> (f, b)+mapSnd f (x, a) = (x, f a)++-- | Label each level of a Tree with an integer indicating how deep it+-- is. The top node of the tree is level 0.+labelLevels :: T.Tree a -> T.Tree (Int, a)+labelLevels = go 0+  where+    go l (T.Node x xs) = T.Node (l, x) (map (go (l + 1)) xs)++-- | Sorts each level of a Forest.+sortForest ::+  (a -> a -> Ordering)+  -> T.Forest a+  -> T.Forest a+sortForest o f = sortBy o' (map (sortTree o) f)+  where+    o' x y = o (T.rootLabel x) (T.rootLabel y)++-- | Sorts each level of a Tree.+sortTree ::+  (a -> a -> Ordering)+  -> T.Tree a+  -> T.Tree a+sortTree o (T.Node l f) = T.Node l (sortForest o f)++-- | Like lastModeBy but using Ord.+lastMode :: Ord a => [a] -> Maybe a+lastMode = lastModeBy compare++-- | Finds the mode of a list. Takes the mode that is located last in+-- the list. Returns Nothing if there is no mode (that is, if the list+-- is empty).+lastModeBy ::+  (a -> a -> Ordering)+  -> [a]+  -> Maybe a+lastModeBy o ls =+  case modesBy o' ls' of+    [] -> Nothing+    ms -> Just . fst . maximumBy fx $ ms+    where+      fx = comparing snd+      ls' = zip ls ([0..] :: [Int])+      o' x y = o (fst x) (fst y)++-- | Finds the modes of a list.+modesBy :: (a -> a -> Ordering) -> [a] -> [a]+modesBy o =+  concat+  . longestLists+  . groupBy (\x y -> o x y == EQ)+  . sortBy o+++-- | Returns the longest lists. This function is partial. It is bottom+-- if the argument list is empty. Therefore, do not export this+-- function.+longestLists :: [[a]] -> [[a]]+longestLists as =+  let lengths = map (\ls -> (ls, length ls)) as+      maxLen = maximum . map snd $ lengths+  in map fst . filter (\(_, len) -> len == maxLen) $ lengths
+ lib/Penny/Cabin/Interface.hs view
@@ -0,0 +1,77 @@+-- | An interface for other Penny components to use. A report is+-- anything that is a 'Report'.+module Penny.Cabin.Interface where++import qualified Data.Prednote.Expressions as Exp+import qualified Penny.Cabin.Scheme as S+import Control.Monad.Exception.Synchronous (Exceptional)+import qualified Data.Text as X+import Text.Matchers (CaseSensitive)+import qualified Text.Matchers as TM+import qualified System.Console.MultiArg as MA+import qualified System.Console.Rainbow as R++import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import Penny.Shield (Runtime)++-- | The function that will print the report, and the positional+-- arguments. If there was a problem parsing the command line options,+-- return an Exception with an error message.++-- | Parsing the filter options can have one of two results: a help+-- string, or a list of positional arguments and a function that+-- prints a report. Or, the parse might fail.++type PosArg = String+type HelpStr = String+type ArgsAndReport = ([PosArg], PrintReport)++-- | The result of parsing the arguments to a report. Failures are+-- indicated with a Text. The name of the executable and the word+-- @error@ will be prepended to this Text; otherwise, it is printed+-- as-is, so be sure to include any trailing newline if needed.+type ParseResult = Exceptional X.Text ArgsAndReport++type PrintReport+  = [L.Transaction]+  -- ^ All transactions; the report must sort and filter them++  -> [L.PricePoint]+  -- ^ PricePoints to be included in the report+++  -> Exceptional X.Text [R.Chunk]+  -- ^ The exception type is a strict Text, containing the error+-- message. The success type is a list of either a Chunk or a PreChunk+-- containing the resulting report. This allows for errors after the+-- list of transactions has been seen. The name of the executable and+-- the word @error@ will be prepended to this Text; otherwise, it is+-- printed as-is, so be sure to include any trailing newline if+-- needed.+++type Report = Runtime -> (HelpStr, MkReport)+type MkReport+  = 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 TM.Matcher)+  -- ^ Result from previous parsers indicating the matcher factory the+  -- user wishes to use++  -> S.Changers+  -- ^ Result from previous parsers indicating which color scheme to+  -- use.++  -> Exp.ExprDesc+  -- ^ Result from previous parsers indicating whether the user wants+  -- RPN or infix++  -> ([L.Transaction] -> [(Ly.LibertyMeta, L.Posting)])+  -- ^ Result from previous parsers that will sort and filter incoming+  -- transactions++  -> MA.Mode ParseResult
+ lib/Penny/Cabin/Meta.hs view
@@ -0,0 +1,13 @@+-- | Metadata that is specific to Cabin.+module Penny.Cabin.Meta where++import qualified Penny.Lincoln as L++-- | Each row that is visible on screen is assigned a VisibleNum. This+-- is used to number the rows in the report for the user's benefit. It+-- is also used to determine whether the row is even or odd for the+-- purpose of assigning the background color (this way the background+-- colors can alternate, like a checkbook register.)+newtype VisibleNum = VisibleNum { unVisibleNum :: L.Serial }+                     deriving (Eq, Show)+
+ lib/Penny/Cabin/Options.hs view
@@ -0,0 +1,16 @@+-- | Options applicable to multiple Cabin reports.++module Penny.Cabin.Options where++-- | Whether to show zero balances in reports.+newtype ShowZeroBalances =+  ShowZeroBalances { unShowZeroBalances :: Bool }+  deriving (Show, Eq)++-- | Converts an ordering to a descending order.+descending :: (a -> a -> Ordering)+              -> a -> a -> Ordering+descending f x y = case f x y of+  LT -> GT+  GT -> LT+  EQ -> EQ
+ lib/Penny/Cabin/Parsers.hs view
@@ -0,0 +1,24 @@+-- | Command line parsers that are common to various Cabin reports.++module Penny.Cabin.Parsers where++import qualified Penny.Cabin.Options as CO+import qualified System.Console.MultiArg.Combinator as C+++zeroBalances :: C.OptSpec CO.ShowZeroBalances+zeroBalances = C.OptSpec ["zero-balances"] "" (C.ChoiceArg ls)+  where+    ls = [ ("show", CO.ShowZeroBalances True)+         , ("hide", CO.ShowZeroBalances False) ]++data SortOrder = Ascending | Descending deriving (Eq, Ord, Show)++order :: C.OptSpec SortOrder+order = C.OptSpec ["order"] "" (C.ChoiceArg ls)+  where+    ls = [ ("ascending", Ascending)+         , ("descending", Descending) ]++help :: C.OptSpec ()+help = C.OptSpec ["help"] "h" (C.NoArg ())
+ lib/Penny/Cabin/Posts.hs view
@@ -0,0 +1,621 @@+{-# LANGUAGE OverloadedStrings #-}++-- | 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+  ( postsReport+  , zincReport+  , defaultOptions+  , ZincOpts(..)+  , A.Alloc+  , A.SubAccountLength(..)+  , A.alloc+  , yearMonthDay+  , qtyAsIs+  , balanceAsIs+  , defaultWidth+  , columnsVarToWidth+  , widthFromRuntime+  , defaultFields+  , defaultSpacerWidth+  , T.ReportWidth(..)+  ) where++import Control.Applicative ((<$>), (<*>))+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.List.Split (chunksOf)+import qualified Data.Either as Ei+import Data.Monoid ((<>))+import qualified Data.Text as X+import qualified Penny.Cabin.Interface as I+import qualified Penny.Cabin.Options as CO+import qualified Penny.Cabin.Posts.Allocated as A+import qualified Penny.Cabin.Posts.Chunk as C+import qualified Penny.Cabin.Posts.Fields as F+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Posts.Parser as P+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Cabin.Posts.Types as T+import qualified Penny.Cabin.Scheme as E++import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Liberty as Ly+import qualified Penny.Shield as Sh+import qualified Data.Prednote.Expressions as Exp+import qualified Data.Prednote.Pdct as Pe+import qualified System.Console.Rainbow as Rb++import Data.List (intersperse)+import Data.Maybe (catMaybes)+import qualified Data.Foldable as Fdbl+import Data.Time as Time+import qualified System.Console.MultiArg as MA+import System.Locale (defaultTimeLocale)+import Text.Matchers (CaseSensitive)++-- | All information needed to make a Posts report. This function+-- never fails.+postsReport+  :: E.Changers+  -> CO.ShowZeroBalances+  -> (Pe.Pdct (Ly.LibertyMeta, L.Posting))+  -- ^ Removes posts from the report if applying this function to the+  -- post returns False. Posts removed still affect the running+  -- balance.++  -> [Ly.PostFilterFn]+  -- ^ Applies these post-filters to the list of posts that results+  -- from applying the predicate above. Might remove more+  -- postings. Postings removed still affect the running balance.++  -> C.ChunkOpts+  -> [(Ly.LibertyMeta, L.Posting)]+  -> [Rb.Chunk]++postsReport ch szb pdct pff co =+  C.makeChunk ch co+  . M.toBoxList szb pdct pff+++zincReport :: ZincOpts -> I.Report+zincReport opts rt = (helpStr opts, md)+  where+    md cs fty ch expr fsf = MA.Mode+      { MA.mName = "postings"+      , MA.mIntersperse = MA.Intersperse+      , MA.mOpts = specs rt+      , MA.mPosArgs = return . Left+      , MA.mProcess = process opts cs fty ch expr fsf+      , MA.mHelp = const (helpStr opts)+      }++specs+  :: Sh.Runtime+  -> [MA.OptSpec (Either String (P.State -> Ex.Exceptional X.Text P.State))]+specs = map (fmap Right) . P.allSpecs+++process+  :: ZincOpts+  -> CaseSensitive+  -> L.Factory+  -> E.Changers+  -> Exp.ExprDesc+  -> ([L.Transaction] -> [(Ly.LibertyMeta, L.Posting)])+  -> [Either String (P.State -> Ex.Exceptional X.Text P.State)]+  -> Ex.Exceptional X.Text I.ArgsAndReport+process os cs fty ch expr fsf ls =+  let (posArgs, clOpts) = Ei.partitionEithers ls+      pState = newParseState cs fty expr os+      exState' = foldl (>>=) (return pState) clOpts+  in fmap (mkPrintReport posArgs os ch fsf) exState'++mkPrintReport+  :: [String]+  -> ZincOpts+  -> E.Changers+  -> ([L.Transaction] -> [(Ly.LibertyMeta, L.Posting)])+  -> P.State+  -> I.ArgsAndReport+mkPrintReport posArgs zo ch fsf st = (posArgs, f)+  where+    f txns _ = do+      pdct <- getPredicate (P.exprDesc st) (P.tokens st)+      let boxes = fsf txns+          rptChks = postsReport ch (P.showZeroBalances st) pdct+                    (P.postFilter st) (chunkOpts st zo) boxes+          expChks = showExpression (P.showExpression st) pdct+          verbChks = showVerboseFilter (P.verboseFilter st) pdct boxes+          chks = expChks+                 ++ verbChks+                 ++ rptChks+      return chks++indentAmt :: Pe.IndentAmt+indentAmt = 4++blankLine :: Rb.Chunk+blankLine = Rb.plain (X.singleton '\n')++showExpression+  :: P.ShowExpression+  -> Pe.Pdct ((Ly.LibertyMeta, L.Posting))+  -> [Rb.Chunk]+showExpression (P.ShowExpression b) pdct =+  if not b then [] else info : blankLine : (chks ++ [blankLine])+  where+    info = Rb.plain (X.pack "Postings filter expression:\n")+    chks = Pe.showPdct indentAmt 0 pdct++showVerboseFilter+  :: P.VerboseFilter+  -> Pe.Pdct ((Ly.LibertyMeta, L.Posting))+  -> [(Ly.LibertyMeta, L.Posting)]+  -> [Rb.Chunk]+showVerboseFilter (P.VerboseFilter b) pdct bs =+  if not b then [] else info : blankLine : (chks ++ [blankLine])+  where+    pdcts = map (makeLabeledPdct pdct) bs+    chks = concat . map snd $ zipWith doEval bs pdcts+    doEval subj pd = Pe.evaluate indentAmt False subj 0 pd+    info = Rb.plain (X.pack "Postings report filter:\n")++-- | Creates a Pdct and prepends a one-line description of the PostFam+-- to the Pdct's label so it can be easily identified in the output.+makeLabeledPdct+  :: Pe.Pdct ((Ly.LibertyMeta, L.Posting))+  -> (Ly.LibertyMeta, L.Posting)+  -> Pe.Pdct ((Ly.LibertyMeta, L.Posting))+makeLabeledPdct pd box = Pe.rename f pd+  where+    f old = old <> " - " <> L.display pf+    pf = snd box++defaultOptions+  :: Sh.Runtime+  -> ZincOpts+defaultOptions rt = ZincOpts+  { fields = defaultFields+  , width = widthFromRuntime rt+  , showZeroBalances = CO.ShowZeroBalances False+  , dateFormat = yearMonthDay+  , qtyFormat = qtyAsIs+  , balanceFormat = balanceAsIs+  , subAccountLength = A.SubAccountLength 2+  , payeeAllocation = A.alloc 60+  , accountAllocation = A.alloc 40+  , spacers = defaultSpacerWidth+  }+++type Error = X.Text++getPredicate+  :: Exp.ExprDesc+  -> [Exp.Token ((Ly.LibertyMeta, L.Posting))]+  -> Ex.Exceptional Error (Pe.Pdct ((Ly.LibertyMeta, L.Posting)))+getPredicate d ts =+  case ts of+    [] -> return $ Pe.always+    _ -> Exp.parseExpression d ts+++-- | All the information to configure the postings report if the+-- options will be parsed in from the command line.+data ZincOpts = ZincOpts+  { fields :: F.Fields Bool+    -- ^ Default fields to show in the report.++  , width :: T.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 current terminal.++  , showZeroBalances :: CO.ShowZeroBalances+    -- ^ Are commodities that have no balance shown in the Total fields+    -- of the report?++  , dateFormat :: (M.PostMeta, L.Posting) -> 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 :: (M.PostMeta, L.Posting) -> X.Text+    -- ^ How to display the quantity of the posting. This+    -- function is applied to a Box 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 :: L.Commodity -> L.Qty -> X.Text+    -- ^ How to display balance totals. Similar to+    -- balanceFormat.++  , subAccountLength :: A.SubAccountLength+    -- ^ When shortening the names of sub accounts to make+    -- them fit, they will be this long.++  , payeeAllocation :: A.Alloc+    -- ^ 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.Alloc+    -- ^ See payeeAllocation above++  , spacers :: S.Spacers 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.++  }++chunkOpts ::+  P.State+  -> ZincOpts+  -> C.ChunkOpts+chunkOpts s z = C.ChunkOpts+  { C.dateFormat = dateFormat z+  , C.qtyFormat = qtyFormat z+  , C.balanceFormat = balanceFormat z+  , C.fields = P.fields s+  , C.subAccountLength = subAccountLength z+  , C.payeeAllocation = payeeAllocation z+  , C.accountAllocation = accountAllocation z+  , C.spacers = spacers z+  , C.reportWidth = P.width s+  }+++newParseState ::+  CaseSensitive+  -> L.Factory+  -> Exp.ExprDesc+  -> ZincOpts+  -> P.State+newParseState cs fty expr o = P.State+  { P.sensitive = cs+  , P.factory = fty+  , P.tokens = []+  , P.postFilter = []+  , P.fields = fields o+  , P.width = width o+  , P.showZeroBalances = showZeroBalances o+  , P.exprDesc = expr+  , P.verboseFilter = P.VerboseFilter False+  , P.showExpression = P.ShowExpression False+  }++-- | Shows the date of a posting in YYYY-MM-DD format.+yearMonthDay :: (M.PostMeta, L.Posting) -> X.Text+yearMonthDay p = X.pack (Time.formatTime defaultTimeLocale fmt d)+  where+    d = L.day+        . Q.dateTime+        . snd+        $ p+    fmt = "%Y-%m-%d"++-- | Shows the quantity of a posting. Does no rounding or+-- prettification; simply uses show on the underlying Decimal.+qtyAsIs :: (M.PostMeta, L.Posting) -> X.Text+qtyAsIs p = X.pack . L.prettyShowQty . Q.qty . snd $ p++-- | Shows the quantity of a balance. If there is no quantity, shows+-- two dashes.+balanceAsIs :: a -> L.Qty -> X.Text+balanceAsIs _ = X.pack . L.prettyShowQty++-- | The default width for the report.+defaultWidth :: T.ReportWidth+defaultWidth = T.ReportWidth 80++-- | Applied to the value of the COLUMNS environment variable, returns+-- an appropriate ReportWidth.+columnsVarToWidth :: Maybe String -> T.ReportWidth+columnsVarToWidth ms = case ms of+  Nothing -> defaultWidth+  Just str -> case reads str of+    [] -> defaultWidth+    (i, []):[] -> if i > 0 then T.ReportWidth i else defaultWidth+    _ -> defaultWidth++-- | Given the Runtime, use the defaultWidth given above to calculate+-- the report's width if COLUMNS does not yield a value. Otherwise,+-- use what is in COLUMNS.+widthFromRuntime :: Sh.Runtime -> T.ReportWidth+widthFromRuntime rt = case Sh.screenWidth rt of+  Nothing -> defaultWidth+  Just w -> T.ReportWidth . Sh.unScreenWidth $ w++-- | Default fields to show in the Postings report.+defaultFields :: F.Fields Bool+defaultFields =+  F.Fields { 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 }++-- | Default width of spacers; most are one character wide, but the+-- spacer after payee is 4 characters wide.+defaultSpacerWidth :: S.Spacers Int+defaultSpacerWidth =+  S.Spacers { 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 }++------------------------------------------------------------+-- ## Help+------------------------------------------------------------++ifDefault :: Bool -> String+ifDefault b = if b then " (default)" else ""++helpStr :: ZincOpts -> String+helpStr o = unlines $+  [ "postings"+  , "  Show postings in order with a running balance."+  , "  Accepts the following options:"+  , ""+  , "Posting filters"+  , "==============="+  , "These options affect which postings are shown in the report."+  , "Postings not shown still affect the running balance."+  , ""+  , "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)"+  , "--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"+  , ""+  , "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"+  , "--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"+  , ""+  , "Infix or RPN selection"+  , "----------------------"+  , "--infix - use infix notation"+  , "--rpn - use reverse polish notation"+  , "  (default: use what was used in the filtering options)"+  , ""+  , "Infix 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"+  , ""+  , "RPN Operators"+  , "-------------"+  , "expr --not"+  , "  True if expr is false"+  , "expr1 expr2 --and"+  , "  True if expr and expr2 are both true"+  , "expr1 expr2 --or"+  , "  True if either expr1 or expr2 is true"+  , ""+  , "Options affecting patterns"+  , "=========================="+  , ""+  , "-i, --case-insensitive"+  , "  Be case insensitive"+  , "-I, --case-sensitive"+  , "  Be case sensitive"+  , ""+  , "--within"+  , "  Use \"within\" matcher"+  , "--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"+  , ""+  , "Other options"+  , "============="+  , "--width num"+  , "  Hint for roughly how wide the report should be in columns"+  , "  (currently: " ++ (show . T.unReportWidth . width $ o) ++ ")"+  , "--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"+  , ""+  ] ++ showDefaultFields (fields o) +++  [ ""+  , "--show-zero-balances"+  , "  Show balances that are zero"+    ++ ifDefault (CO.unShowZeroBalances . showZeroBalances $ o)+  , "--hide-zero-balances"+  , "  Hide balances that are zero"+    ++ ifDefault (not . CO.unShowZeroBalances . showZeroBalances $ o)+  , ""+  , "--help, -h"+  , "  Show this help and exit"+  ]++-- | Shows which fields are on by default.+showDefaultFields :: F.Fields Bool -> [String]+showDefaultFields i = hdr : rest+  where+    hdr = "Fields shown by default:"+      ++ if null rest then " (none)" else ""+    rest =+      map ("  " ++)+      . map concat+      . map (intersperse ", ")+      . chunksOf 3+      . catMaybes+      . Fdbl.toList+      . toMaybes+      $ i+    toMaybes flds = f <$> flds <*> F.fieldNames+    f b n = if b then Just n else Nothing
+ lib/Penny/Cabin/Posts/Allocated.hs view
@@ -0,0 +1,401 @@+-- | 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. Obtain the width of the Payee and Account spacers. Include each+-- spacer if its corresponding field appears in the report.+--+-- 3. Subtract from the total report width the width of the the+-- growing cells and the width of the Payee and Account spacers. This+-- gives the total width available for the Payee and Account+-- fields. If there are not at least two columns available, return+-- without including the Payee and Account fields.+--+-- 4. Determine the total width that the Payee and Account fields+-- would obtain if they had all the space they could ever need. This+-- is the "requested width".+--+-- 5. Split up the available width for the Payee and Account fields+-- depending on which fields appear:+--+-- a. If only the one field appears, then it shall be as wide as the+-- total available width or the its requested width, whichever is+-- smaller.+--+-- b. If both fields appear, then calculate the allocated width for+-- each field. If either field's requested width is less than its+-- allocated width, then that field is only as wide as its requested+-- width. The other field is then as wide as (the sum of its allocated+-- width and the leftover width from the other field) or its requested+-- width, whichever is smaller. If neither field's requested width is+-- less than its allocated width, then each field gets ts allocated+-- width.+--+-- 6. Fill cell contents; return filled cells.++module Penny.Cabin.Posts.Allocated (+  payeeAndAcct+  , AllocatedOpts(..)+  , Fields(..)+  , SubAccountLength(..)+  , Alloc+  , alloc+  , unAlloc+  ) where++import Control.Applicative(Applicative((<*>), pure), (<$>))+import Control.Arrow (second)+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 System.Console.Rainbow as Rb+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.Posts.Growers as G+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Cabin.Posts.Types as Ty+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Cabin.TextFormat as TF+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Bits.Qty as Qty+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Lincoln.HasText as HT++data Fields a = Fields {+  payee :: a+  , account :: a+  } deriving (Eq, Show)++newtype SubAccountLength =+  SubAccountLength { unSubAccountLength :: Int }+  deriving Show++newtype Alloc = Alloc { unAlloc :: Int }+  deriving Show++alloc :: Int -> Alloc+alloc i =+  if i < 1+  then error $ "allocations must be greater than zero."+       ++ " supplied allocation: " ++ show i+  else Alloc i+++-- | All the information needed for allocated cells.+data AllocatedOpts = AllocatedOpts+  { fields :: Fields Bool+  , subAccountLength :: SubAccountLength+  , allocations :: Fields Alloc+  , spacers :: S.Spacers Int+  , growerWidths :: G.Fields (Maybe Int)+  , reportWidth :: Ty.ReportWidth+  }++-- | 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+  :: E.Changers+  -> AllocatedOpts+  -> [(M.PostMeta, L.Posting)]+  -> Fields (Maybe ([R.ColumnSpec], Int))+payeeAndAcct ch ao bs =+  let allBuilders =+        T.traverse (builders ch (subAccountLength ao)) bs+      availWidth = availableWidthForAllocs (growerWidths ao)+                   (spacers ao) (fields ao) (reportWidth ao)+      finals = divideAvailableWidth availWidth (fields ao)+               (allocations ao)+               ( fmap (safeMaximum (Request 0))+                 . fmap (fmap fst) $ allBuilders)+  in fmap (fmap (second unFinal))+     . buildSpecs finals+     . fmap (fmap snd)+     $ allBuilders+++safeMaximum :: Ord a => a -> [a] -> a+safeMaximum d ls = case ls of+  [] -> d+  xs -> maximum xs++payeeAndAccountSpacerWidth+  :: Fields Bool+  -> S.Spacers Int+  -> Int+payeeAndAccountSpacerWidth flds ss = pye + act+  where+    pye = if payee flds then abs (S.payee ss) else 0+    act = if account flds then abs (S.account ss) else 0++newtype AvailableWidth = AvailableWidth Int+        deriving (Eq, Ord, Show)++availableWidthForAllocs+  :: G.Fields (Maybe Int)+  -> S.Spacers Int+  -> Fields Bool+  -> Ty.ReportWidth+  -> AvailableWidth+availableWidthForAllocs growers ss flds (Ty.ReportWidth w) =+  AvailableWidth $ max 0 diff+  where+    tot = sumGrowersAndSpacers growers ss+          + payeeAndAccountSpacerWidth flds ss+    diff = w - tot++-- | 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)+  -> S.Spacers 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+  -> S.Spacers 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 widths of growing cells and their accompanying+-- spacers; makes the adjustments described in sumSpacers.+sumGrowersAndSpacers ::+  G.Fields (Maybe Int)+  -> S.Spacers Int+  -> Int+sumGrowersAndSpacers fs ss = spcrs + flds where+  spcrs = sumSpacers fs ss+  flds = Fdbl.foldr f 0 fs where+    f maybeI acc = case maybeI of+      Nothing -> acc+      Just i -> acc + i++newtype Request = Request { unRequest :: Int }+        deriving (Eq, Ord, Show)++newtype Final = Final { unFinal :: Int }+        deriving (Eq, Ord, Show)+++buildSpecs+  :: Fields (Maybe Final)+  -> Fields ([Final -> R.ColumnSpec])+  -> Fields (Maybe ([R.ColumnSpec], Final))+buildSpecs finals bs = f <$> finals <*> bs+  where+    f mayFinal gs = case mayFinal of+      Nothing -> Nothing+      Just fin -> Just ((gs <*> pure fin), fin)+++-- | Divide the total available width between the two fields.+divideAvailableWidth+  :: AvailableWidth+  -> Fields Bool+  -> Fields Alloc+  -> Fields Request+  -> Fields (Maybe Final)+divideAvailableWidth (AvailableWidth aw) appear allocs rws = Fields pye act+  where+    minFinal i1 i2 =+      let m = min i1 i2+      in if m > 0 then Just . Final $ m else Nothing+    pairAtLeast i1 i2 = (atLeast i1, atLeast i2)+      where atLeast i = if i > 0 then Just . Final $ i else Nothing+    reqP = unRequest . payee $ rws+    reqA = unRequest . account $ rws+    (pye, act) = case (payee appear, account appear) of+      (False, False) -> (Nothing, Nothing)+      (True, False) -> (minFinal reqP aw, Nothing)+      (False, True) -> (Nothing, minFinal reqA aw)+      (True, True) ->+        let votes = [unAlloc . payee $ allocs, unAlloc . account $ allocs]+            allocRslt = Qty.largestRemainderMethod (fromIntegral aw)+                        (map fromIntegral votes)+            (allocP, allocA) = case allocRslt of+              x:y:[] -> (fromIntegral x, fromIntegral y)+              _ -> error "divideAvailableWidth error"+        in case (allocP > reqP, allocA > reqA) of+            (True, True) -> pairAtLeast reqP reqA+            (True, False) ->+              pairAtLeast reqP $ (min (allocA + (allocP - reqP))) reqA+            (False, True) ->+              pairAtLeast (min reqP (allocP + (allocA - reqA))) reqA+            (False, False) -> pairAtLeast allocP allocA+++builders+  :: E.Changers+  -> SubAccountLength+  -> (M.PostMeta, L.Posting)+  -> Fields (Request, Final -> R.ColumnSpec)+builders ch sl b = Fields (buildPayee ch b) (buildAcct ch sl b)++buildPayee+  :: E.Changers+  -> (M.PostMeta, L.Posting)+  -> (Request, Final -> R.ColumnSpec)+  -- ^ Returns a tuple. The first element is the maximum width that+  -- this cell needs to display its value perfectly. The second+  -- element is a function that, when applied to an actual width,+  -- returns a ColumnSpec.++buildPayee ch i = (maxW, mkSpec)+  where+    pb = snd i+    eo = E.fromVisibleNum . M.visibleNum . fst $ i+    j = R.LeftJustify+    ps = (E.Other, eo)+    md = E.getEvenOddLabelValue E.Other eo ch+    mayPye = Q.payee pb+    maxW = Request $ maybe 0 (X.length . HT.text) mayPye+    mkSpec (Final w) = R.ColumnSpec j (R.Width w) ps sq+      where+        sq = case mayPye of+          Nothing -> []+          Just pye ->+            let wrapped =+                  Fdbl.toList+                  . TF.unLines+                  . TF.wordWrap w+                  . TF.txtWords+                  . HT.text+                  $ pye+                toBit (TF.Words seqTxts) =+                  md+                  . Rb.plain+                  . X.unwords+                  . Fdbl.toList+                  $ seqTxts+            in fmap toBit wrapped+++buildAcct+  :: E.Changers+  -> SubAccountLength+  -> (M.PostMeta, L.Posting)+  -> (Request, Final -> R.ColumnSpec)+  -- ^ Returns a tuple. The first element is the maximum width that+  -- this cell needs to display its value perfectly. The second+  -- element is a function that, when applied to an actual width,+  -- returns a ColumnSpec.++buildAcct ch sl i = (maxW, mkSpec)+  where+    pb = snd i+    eo = E.fromVisibleNum . M.visibleNum . fst $ i+    ps = (E.Other, eo)+    aList = L.unAccount . Q.account $ pb+    maxW = Request+           $ (sum . map (X.length . L.unSubAccount) $ aList)+           + max 0 (length aList - 1)+    md = E.getEvenOddLabelValue E.Other eo ch+    mkSpec (Final aw) = R.ColumnSpec R.LeftJustify (R.Width aw) ps sq+      where+        target = TF.Target aw+        shortest = TF.Shortest . unSubAccountLength $ sl+        ws = TF.Words . Seq.fromList . map L.unSubAccount $ aList+        (TF.Words shortened) = TF.shorten shortest target ws+        sq = [ md+               . Rb.plain+               . 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)+
+ lib/Penny/Cabin/Posts/BottomRows.hs view
@@ -0,0 +1,648 @@+-- | 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 (+  BottomOpts(..),+  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 System.Console.Rainbow as Rb+import qualified Penny.Cabin.Scheme as E+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.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.Spacers as S+import qualified Penny.Cabin.Posts.Types as Ty+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.HasText as HT+import qualified Penny.Lincoln.Queries as Q++data BottomOpts = BottomOpts+  { growingWidths :: G.Fields (Maybe Int)+  , allocatedWidths :: A.Fields (Maybe Int)+  , fields :: F.Fields Bool+  , reportWidth :: Ty.ReportWidth+  , spacers :: S.Spacers Int+  }++bottomRows+  :: E.Changers+  -> BottomOpts+  -> [(M.PostMeta, L.Posting)]+  -> Fields (Maybe [[Rb.Chunk]])+bottomRows ch os bs = makeRows bs pcs where+  pcs = infoProcessors ch topSpecs (reportWidth os) wanted+  wanted = requestedMakers ch (fields os)+  topSpecs = topCellSpecs (growingWidths os) (allocatedWidths os)+             (spacers 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 :: F.Fields 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+  :: E.Changers+  -> [TopCellSpec]+  -> Maybe (((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))+            -> (M.PostMeta, L.Posting) -> [Rb.Chunk])+hanging ch specs = hangingWidths specs+                >>= return . hangingInfoProcessor ch++hangingInfoProcessor+  :: E.Changers+  -> Hanging Int+  -> ((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))+  -> (M.PostMeta, L.Posting)+  -> [Rb.Chunk]+hangingInfoProcessor ch widths mkr info = row where+  row = R.row ch [left, mid, right]+  (ts, mid) = mkr info (mainCell widths)+  mkPad w = R.ColumnSpec R.LeftJustify (R.Width w) ts []+  left = mkPad (leftPad widths)+  right = mkPad (rightPad widths)++widthOfTopColumns+  :: E.Changers+  -> [TopCellSpec]+  -> Maybe (((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))+            -> (M.PostMeta, L.Posting) -> [Rb.Chunk])+widthOfTopColumns ch ts =+  if null ts+  then Nothing+  else Just $ makeSpecificWidth ch w where+    w = Fdbl.foldl' f 0 ts+    f acc (_, maySpcWidth, (ContentWidth cw)) =+      acc + cw + maybe 0 (\(SpacerWidth sw) -> sw) maySpcWidth+++widthOfReport+  :: E.Changers+  -> Ty.ReportWidth+  -> ((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))+  -> (M.PostMeta, L.Posting)+  -> [Rb.Chunk]+widthOfReport ch (Ty.ReportWidth rw) fn info =+  makeSpecificWidth ch rw fn info++chooseProcessor+  :: E.Changers+  -> [TopCellSpec]+  -> Ty.ReportWidth+  -> ((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec))+  -> (M.PostMeta, L.Posting)+  -> [Rb.Chunk]+chooseProcessor ch specs rw fn = let+  firstTwo = First (hanging ch specs)+             `mappend` First (widthOfTopColumns ch specs)+  in case getFirst firstTwo of+    Nothing -> widthOfReport ch rw fn+    Just r -> r fn++infoProcessors+  :: E.Changers+  -> [TopCellSpec]+  -> Ty.ReportWidth+  -> Fields (Maybe ((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)))+  -> Fields (Maybe ((M.PostMeta, L.Posting) -> [Rb.Chunk]))+infoProcessors ch specs rw flds = let+  chooser = chooseProcessor ch specs rw+  mkProcessor mayFn = case mayFn of+    Nothing -> Nothing+    Just fn -> Just $ chooser fn+  in mkProcessor <$> flds+++makeRows ::+  [(M.PostMeta, L.Posting)]+  -> Fields (Maybe ((M.PostMeta, L.Posting) -> [Rb.Chunk]))+  -> Fields (Maybe [[Rb.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)+                -> S.Spacers 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+  -> S.Spacers 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+  :: E.Changers -> Int -> ((M.PostMeta, L.Posting) -> Int -> (a, R.ColumnSpec))+  -> (M.PostMeta, L.Posting) -> [Rb.Chunk]+makeSpecificWidth ch w f i = R.row ch [c] where+  (_, c) = f i w+++type Maker+  = E.Changers+  -> (M.PostMeta, L.Posting)+  -> Int+  -> ((E.Label, E.EvenOdd), 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+  :: E.Changers+  -> F.Fields Bool+  -> Fields (Maybe ((M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)))+requestedMakers ch allFlds =+  let flds = bottomRowsFields allFlds+      filler b mkr = if b then Just $ mkr ch else Nothing+  in filler <$> flds <*> makers++tagsCell+  :: E.Changers+  -> (M.PostMeta, L.Posting)+  -> Int+  -> ((E.Label, E.EvenOdd), R.ColumnSpec)+tagsCell ch info w = (ts, cell) where+  vn = M.visibleNum . fst $ info+  cell = R.ColumnSpec R.LeftJustify (R.Width w) ts cs+  eo = E.fromVisibleNum vn+  ts = (E.Other, eo)+  cs =+    Fdbl.toList+    . fmap toBit+    . TF.unLines+    . TF.wordWrap w+    . TF.Words+    . Seq.fromList+    . map (X.cons '*')+    . HT.textList+    . Q.tags+    . snd+    $ info+  md = E.getEvenOddLabelValue E.Other eo ch+  toBit (TF.Words ws) = md . Rb.plain $ t where+    t = X.concat . intersperse (X.singleton ' ') . Fdbl.toList $ ws+++memoBits+  :: E.Changers -> (E.Label, E.EvenOdd) -> L.Memo -> R.Width -> [Rb.Chunk]+memoBits ch (lbl, eo) m (R.Width w) = cs where+  cs = Fdbl.toList+       . fmap toBit+       . TF.unLines+       . TF.wordWrap w+       . TF.Words+       . Seq.fromList+       . X.words+       . X.intercalate (X.singleton ' ')+       . L.unMemo+       $ m+  md = E.getEvenOddLabelValue lbl eo ch+  toBit (TF.Words ws) = md . Rb.plain $ (X.unwords . Fdbl.toList $ ws)+++memoCell+  :: E.Changers -> (M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)+memoCell ch info width = (ts, cell) where+  w = R.Width width+  vn = M.visibleNum . fst $ info+  eo = E.fromVisibleNum vn+  ts = (E.Other, eo)+  cell = R.ColumnSpec R.LeftJustify w ts cs+  mayPm = Q.postingMemo . snd $ info+  mayTm = Q.transactionMemo . snd $ info+  cs = case (mayPm, mayTm) of+    (Nothing, Nothing) -> mempty+    (Nothing, Just tm) -> memoBits ch ts tm w+    (Just pm, Nothing) -> memoBits ch ts pm w+    (Just pm, Just tm) -> memoBits ch ts pm w `mappend` memoBits ch ts tm w+++filenameCell+  :: E.Changers -> (M.PostMeta, L.Posting) -> Int -> ((E.Label, E.EvenOdd), R.ColumnSpec)+filenameCell ch info width = (ts, cell) where+  w = R.Width width+  vn = M.visibleNum . fst $ info+  eo = E.fromVisibleNum vn+  ts = (E.Other, eo)+  cell = R.ColumnSpec R.LeftJustify w ts cs+  md = E.getEvenOddLabelValue E.Other eo ch+  toBit n = md . Rb.plain+            . X.drop (max 0 (X.length n - width)) $ n+  cs = case Q.filename . snd $ info of+    Nothing -> []+    Just fn -> [toBit . L.unFilename $ fn]+++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)+
+ lib/Penny/Cabin/Posts/Chunk.hs view
@@ -0,0 +1,152 @@+module Penny.Cabin.Posts.Chunk (ChunkOpts(..), makeChunk) where++import qualified Data.Foldable as Fdbl+import Data.List (transpose)+import Data.Maybe (isNothing, catMaybes)+import qualified Penny.Cabin.Posts.Fields as F+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.Spacers as S+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.Scheme as E+import qualified System.Console.Rainbow as Rb+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Lincoln as L+import qualified Data.Text as X+import qualified Penny.Cabin.Posts.Types as Ty++data ChunkOpts = ChunkOpts+  { dateFormat :: (M.PostMeta, L.Posting) -> X.Text+  , qtyFormat :: (M.PostMeta, L.Posting) -> X.Text+  , balanceFormat :: L.Commodity -> L.Qty -> X.Text+  , fields :: F.Fields Bool+  , subAccountLength :: A.SubAccountLength+  , payeeAllocation :: A.Alloc+  , accountAllocation :: A.Alloc+  , spacers :: S.Spacers Int+  , reportWidth :: Ty.ReportWidth+  }++growOpts :: ChunkOpts -> G.GrowOpts+growOpts c = G.GrowOpts+  { G.dateFormat = dateFormat c+  , G.qtyFormat = qtyFormat c+  , G.balanceFormat = balanceFormat c+  , G.fields = fields c+  }++allocatedOpts :: ChunkOpts -> G.Fields (Maybe Int) -> A.AllocatedOpts+allocatedOpts c g = A.AllocatedOpts+  { A.fields = let f = fields c+               in A.Fields { A.payee = F.payee f+                           , A.account = F.account f }+  , A.subAccountLength = subAccountLength c+  , A.allocations = A.Fields { A.payee = payeeAllocation c+                             , A.account = accountAllocation c }+  , A.spacers = spacers c+  , A.growerWidths = g+  , A.reportWidth = reportWidth c+  }++bottomOpts ::+  ChunkOpts+  -> G.Fields (Maybe Int)+  -> A.Fields (Maybe Int)+  -> B.BottomOpts+bottomOpts c g a = B.BottomOpts {+  B.growingWidths = g+  , B.allocatedWidths = a+  , B.fields = fields c+  , B.reportWidth = reportWidth c+  , B.spacers = spacers c+  }++makeChunk+  :: E.Changers+  -> ChunkOpts+  -> [(M.PostMeta, L.Posting)]+  -> [Rb.Chunk]+makeChunk ch c bs =+  let fmapSnd = fmap (fmap snd)+      fmapFst = fmap (fmap fst)+      gFldW = fmap (fmap snd) gFlds+      aFldW = fmapSnd aFlds+      gFlds = G.growCells ch (growOpts c) bs+      aFlds = A.payeeAndAcct ch (allocatedOpts c gFldW) bs+      bFlds = B.bottomRows ch (bottomOpts c gFldW aFldW) bs+      topCells = B.topRowCells (fmapFst gFlds) (fmap (fmap fst) aFlds)+      withSpacers = B.mergeWithSpacers topCells (spacers c)+      topRows = makeTopRows ch withSpacers+      bottomRows = makeBottomRows bFlds+  in makeAllRows topRows bottomRows+++topRowsCells+  :: B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)+  -> [[(R.ColumnSpec, Maybe R.ColumnSpec)]]+topRowsCells t = let+  toWithSpc (mayCs, maySp) = case mayCs of+    Nothing -> Nothing+    Just cs -> Just (makeSpacers cs maySp)+  f mayPairList acc = case mayPairList of+    Nothing -> acc+    (Just pairList) -> pairList : acc+  in transpose $ Fdbl.foldr f [] (fmap toWithSpc t)++makeRow :: E.Changers -> [(R.ColumnSpec, Maybe R.ColumnSpec)] -> [Rb.Chunk]+makeRow ch = R.row ch . foldr f [] where+  f (c, mayC) acc = case mayC of+    Nothing -> c:acc+    Just spcr -> c:spcr:acc+++makeSpacers+  :: [R.ColumnSpec]+  -> Maybe Int+  -> [(R.ColumnSpec, Maybe R.ColumnSpec)]+makeSpacers cs mayI = case mayI of+  Nothing -> map (\c -> (c, Nothing)) cs+  Just i -> makeEvenOddSpacers cs i++makeEvenOddSpacers+  :: [R.ColumnSpec]+  -> Int+  -> [(R.ColumnSpec, Maybe R.ColumnSpec)]+makeEvenOddSpacers 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 (R.Width absI) ts []+    evenTs = (E.Other, E.Even)+    oddTs = (E.Other, E.Odd)+    in zip cs spcrs++makeTopRows+  :: E.Changers+  -> B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)+  -> Maybe [[Rb.Chunk]]+makeTopRows ch trc =+  if Fdbl.all (isNothing . fst) trc+  then Nothing+  else Just $ map (makeRow ch) . topRowsCells $ trc+++makeBottomRows ::+  B.Fields (Maybe [[Rb.Chunk]])+  -> Maybe [[[Rb.Chunk]]]+makeBottomRows flds =+  if Fdbl.all isNothing flds+  then Nothing+  else Just . transpose . catMaybes . Fdbl.toList $ flds++makeAllRows :: Maybe [[Rb.Chunk]] -> Maybe [[[Rb.Chunk]]] -> [Rb.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]++
+ lib/Penny/Cabin/Posts/Fields.hs view
@@ -0,0 +1,199 @@+-- | 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 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+  , 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 Fields where+  fmap f fa = Fields {+    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 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+    , payee = a+    , account = a+    , postingDrCr = a+    , postingCmdty = a+    , postingQty = a+    , totalDrCr = a+    , totalCmdty = a+    , totalQty = a+    , tags = a+    , memo = a+    , filename = a }++  ff <*> fa = Fields {+    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 Fields 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))))))))))))))))))))))))))))+++fieldNames :: Fields String+fieldNames = Fields+  { globalTransaction    = "globalTransaction"+  , revGlobalTransaction = "revGlobalTransaction"+  , globalPosting        = "globalPosting"+  , revGlobalPosting     = "revGlobalPosting"+  , fileTransaction      = "fileTransaction"+  , revFileTransaction   = "revFileTransaction"+  , filePosting          = "filePosting"+  , revFilePosting       = "revFilePosting"+  , filtered             = "filtered"+  , revFiltered          = "revFiltered"+  , sorted               = "sorted"+  , revSorted            = "revSorted"+  , visible              = "visible"+  , revVisible           = "revVisible"+  , lineNum              = "lineNum"+  , date                 = "date"+  , flag                 = "flag"+  , number               = "number"+  , payee                = "payee"+  , account              = "account"+  , postingDrCr          = "postingDrCr"+  , postingCmdty         = "postingCmdty"+  , postingQty           = "postingQty"+  , totalDrCr            = "totalDrCr"+  , totalCmdty           = "totalCmdty"+  , totalQty             = "totalQty"+  , tags                 = "tags"+  , memo                 = "memo"+  , filename             = "filename"+  }
+ lib/Penny/Cabin/Posts/Growers.hs view
@@ -0,0 +1,595 @@+-- | 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 (+  GrowOpts(..),+  growCells, Fields(..), grownWidth,+  eFields, EFields(..), pairWithSpacer) where++import Control.Applicative((<$>), Applicative(pure, (<*>)))+import qualified Data.Foldable as Fdbl+import Data.Map (elems)+import qualified Data.Map as Map+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.Posts.Fields as F+import qualified Penny.Cabin.Posts.Meta as M+import qualified Penny.Cabin.Posts.Spacers as S+import qualified Penny.Cabin.Row as R+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Liberty as Ly+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified System.Console.Rainbow as Rb+++-- | All the options needed to grow the cells.+data GrowOpts = GrowOpts+  { dateFormat :: (M.PostMeta, L.Posting) -> X.Text+  , qtyFormat :: (M.PostMeta, L.Posting) -> X.Text+  , balanceFormat :: L.Commodity -> L.Qty -> X.Text+  , fields :: F.Fields Bool+  }++-- | 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+  :: E.Changers+  -> GrowOpts+  -> [(M.PostMeta, L.Posting)]+  -> Fields (Maybe ([R.ColumnSpec], Int))+growCells ch o infos = toPair <$> wanted <*> growers where+  toPair b gwr+    | b =+      let cs = map (gwr o ch) 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 . fields $ o++widestLine :: PreSpec -> Int+widestLine (PreSpec _ _ bs) =+  case bs of+    [] -> 0+    xs -> maximum . map (X.length . Rb.chunkText) $ xs++data PreSpec = PreSpec {+  _justification :: R.Justification+  , _padSpec :: (E.Label, E.EvenOdd)+  , _bits :: [Rb.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 :: E.Changers -> Text -> E.Label -> (M.PostMeta, L.Posting) -> PreSpec+oneLine chgrs t lbl b =+  let eo = E.fromVisibleNum . M.visibleNum . fst $ b+      j = R.LeftJustify+      md = E.getEvenOddLabelValue lbl eo chgrs+      ck = [md $ Rb.plain t]+  in PreSpec j (lbl, eo) ck+++-- | Gets a Fields with each field filled with the function that fills+-- the cells for that field.+growers :: Fields (GrowOpts -> E.Changers -> (M.PostMeta, L.Posting) -> PreSpec)+growers = Fields+  { globalTransaction    = const getGlobalTransaction+  , revGlobalTransaction = const getRevGlobalTransaction+  , globalPosting        = const getGlobalPosting+  , revGlobalPosting     = const getRevGlobalPosting+  , fileTransaction      = const getFileTransaction+  , revFileTransaction   = const getRevFileTransaction+  , filePosting          = const getFilePosting+  , revFilePosting       = const getRevFilePosting+  , filtered             = const getFiltered+  , revFiltered          = const getRevFiltered+  , sorted               = const getSorted+  , revSorted            = const getRevSorted+  , visible              = const getVisible+  , revVisible           = const getRevVisible+  , lineNum              = const getLineNum+  , date                 = \o ch -> getDate ch (dateFormat o)+  , flag                 = const getFlag+  , number               = const getNumber+  , postingDrCr          = const getPostingDrCr+  , postingCmdty         = const getPostingCmdty+  , postingQty           = \o ch -> getPostingQty ch (qtyFormat o)+  , totalDrCr            = const getTotalDrCr+  , totalCmdty           = const getTotalCmdty+  , totalQty             = \o ch -> getTotalQty ch (balanceFormat o)+  }++-- | Make a left justified cell one line long that shows a serial.+serialCellMaybe+  :: E.Changers+  -> (L.Posting -> Maybe Int)+  -- ^ When applied to a Box, this function returns Just Int if the+  -- box has a serial, or Nothing if not.++  -> (M.PostMeta, L.Posting) -> PreSpec+serialCellMaybe chgrs f b = oneLine chgrs t E.Other b+  where+    t = case f (snd b) of+      Nothing -> X.empty+      Just i -> X.pack . show $ i++serialCell+  :: E.Changers+  -> (M.PostMeta -> Int)+  -> (M.PostMeta, L.Posting) -> PreSpec+serialCell chgrs f b = oneLine chgrs t E.Other b+  where+    t = pack . show . f . fst $ b++getGlobalTransaction :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getGlobalTransaction chgrs =+  serialCellMaybe chgrs (fmap (L.forward . L.unGlobalTransaction)+                        . Q.globalTransaction)++getRevGlobalTransaction :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevGlobalTransaction chgrs =+  serialCellMaybe chgrs (fmap (L.backward . L.unGlobalTransaction)+                        . Q.globalTransaction)++getGlobalPosting :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getGlobalPosting chgrs =+  serialCellMaybe chgrs (fmap (L.forward . L.unGlobalPosting)+                        . Q.globalPosting)++getRevGlobalPosting :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevGlobalPosting chgrs =+  serialCellMaybe chgrs (fmap (L.backward . L.unGlobalPosting)+                   . Q.globalPosting)++getFileTransaction :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getFileTransaction chgrs =+  serialCellMaybe chgrs (fmap (L.forward . L.unFileTransaction)+                   . Q.fileTransaction)++getRevFileTransaction :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevFileTransaction chgrs =+  serialCellMaybe chgrs (fmap (L.backward . L.unFileTransaction)+                   . Q.fileTransaction)++getFilePosting :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getFilePosting chgrs =+  serialCellMaybe chgrs (fmap (L.forward . L.unFilePosting)+                   . Q.filePosting)++getRevFilePosting :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevFilePosting chgrs =+  serialCellMaybe chgrs (fmap (L.backward . L.unFilePosting)+                   . Q.filePosting)++getSorted :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getSorted chgrs =+  serialCell chgrs (L.forward . Ly.unSortedNum . M.sortedNum)++getRevSorted :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevSorted chgrs =+  serialCell chgrs (L.backward . Ly.unSortedNum . M.sortedNum)++getFiltered :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getFiltered chgrs =+  serialCell chgrs (L.forward . Ly.unFilteredNum . M.filteredNum)++getRevFiltered :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevFiltered chgrs =+  serialCell chgrs (L.backward . Ly.unFilteredNum . M.filteredNum)++getVisible :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getVisible chgrs =+  serialCell chgrs (L.forward . M.unVisibleNum . M.visibleNum)++getRevVisible :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getRevVisible chgrs =+  serialCell chgrs (L.backward . M.unVisibleNum . M.visibleNum)+++getLineNum :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getLineNum chgrs b = oneLine chgrs t E.Other b where+  lineTxt = pack . show . L.unPostingLine+  t = maybe empty lineTxt (Q.postingLine . snd $ b)++getDate :: E.Changers -> ((M.PostMeta, L.Posting) -> X.Text) -> (M.PostMeta, L.Posting) -> PreSpec+getDate chgrs gd b = oneLine chgrs (gd b) E.Other b++getFlag :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getFlag chgrs i = oneLine chgrs t E.Other i where+  t = maybe empty L.text (Q.flag . snd $ i)++getNumber :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getNumber chgrs i = oneLine chgrs t E.Other i where+  t = maybe empty L.text (Q.number . snd $ i)++dcTxt :: L.DrCr -> Text+dcTxt L.Debit = X.singleton '<'+dcTxt L.Credit = X.singleton '>'++-- | Gives a one-line cell that is colored according to whether the+-- posting is a debit or credit.+coloredPostingCell :: E.Changers -> Text -> (M.PostMeta, L.Posting) -> PreSpec+coloredPostingCell chgrs t i = PreSpec j (lbl, eo) [bit] where+  j = R.LeftJustify+  lbl = case Q.drCr . snd $ i of+    L.Debit -> E.Debit+    L.Credit -> E.Credit+  eo = E.fromVisibleNum . M.visibleNum . fst $ i+  md = E.getEvenOddLabelValue lbl eo chgrs+  bit = md $ Rb.plain t+++getPostingDrCr :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getPostingDrCr ch i = coloredPostingCell ch t i where+  t = dcTxt . Q.drCr . snd $ i++getPostingCmdty :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getPostingCmdty ch i = coloredPostingCell ch t i where+  t = L.unCommodity . Q.commodity . snd $ i++getPostingQty :: E.Changers -> ((M.PostMeta, L.Posting) -> X.Text) -> (M.PostMeta, L.Posting) -> PreSpec+getPostingQty ch qf i = coloredPostingCell ch (qf i) i++getTotalDrCr :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getTotalDrCr ch i =+  let vn = M.visibleNum . fst $ i+      ps = (lbl, eo)+      dc = Q.drCr . snd $ i+      lbl = E.dcToLbl dc+      eo = E.fromVisibleNum vn+      bal = L.unBalance . M.balance . fst $ i+      md = E.getEvenOddLabelValue lbl eo ch+      bits =+        if Map.null bal+        then [md . Rb.plain $ pack "--"]+        else let mkChk e = E.bottomLineToDrCr e eo ch+             in fmap mkChk . elems $ bal+      j = R.LeftJustify+  in PreSpec j ps bits++getTotalCmdty :: E.Changers -> (M.PostMeta, L.Posting) -> PreSpec+getTotalCmdty ch i =+  let vn = M.visibleNum . fst $ i+      j = R.RightJustify+      ps = (lbl, eo)+      dc = Q.drCr . snd $ i+      eo = E.fromVisibleNum vn+      lbl = E.dcToLbl dc+      bal = Map.toList . L.unBalance . M.balance . fst $ i+      preChunks = E.balancesToCmdtys ch eo bal+  in PreSpec j ps preChunks++getTotalQty+  :: E.Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> (M.PostMeta, L.Posting)+  -> PreSpec+getTotalQty ch balFmt i =+  let vn = M.visibleNum . fst $ i+      j = R.LeftJustify+      dc = Q.drCr . snd $ i+      ps = (E.dcToLbl dc, eo)+      eo = E.fromVisibleNum vn+      bal = Map.toList . L.unBalance . M.balance . fst $ i+      preChunks = E.balanceToQtys ch balFmt eo bal+  in PreSpec j ps preChunks++growingFields :: F.Fields Bool -> Fields Bool+growingFields f = 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 -> S.Spacers 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)+  -> S.Spacers 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+
+ lib/Penny/Cabin/Posts/Meta.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE RankNTypes #-}+module Penny.Cabin.Posts.Meta+  ( M.VisibleNum(M.unVisibleNum)+  , PostMeta(filteredNum, sortedNum, visibleNum, balance)+  , toBoxList+  ) 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.Cabin.Meta as M+import qualified Penny.Cabin.Options as CO+import qualified Data.Prednote.Pdct as Pe+import Data.Monoid (mempty, mappend)++data PostMeta = PostMeta+  { filteredNum :: Ly.FilteredNum+  , sortedNum :: Ly.SortedNum+  , visibleNum :: M.VisibleNum+  , balance :: L.Balance }+  deriving Show+++addMetadata+  :: [(L.Balance, (Ly.LibertyMeta, L.Posting))]+  -> [(PostMeta, L.Posting)]+addMetadata = L.serialItems f where+  f ser (bal, (lm, p)) = (pm, p)+    where+      pm = PostMeta+        { filteredNum = Ly.filteredNum lm+        , sortedNum = Ly.sortedNum lm+        , visibleNum = M.VisibleNum ser+        , balance = bal+        }++-- | Adds appropriate metadata, including the running balance, to a+-- list of Box. Because all posts are incorporated into the running+-- balance, first calculates the running balance for all posts. Then,+-- removes posts we're not interested in by applying the predicate and+-- the post-filter. Finally, adds on the metadata, which will include+-- the VisibleNum.+toBoxList+  :: CO.ShowZeroBalances+  -> Pe.Pdct (Ly.LibertyMeta, L.Posting)+  -- ^ Removes posts from the report if applying this function to the+  -- post returns a value other than Just True. Posts removed still+  -- affect the running balance.++  -> [Ly.PostFilterFn]+  -- ^ Applies these post-filters to the list of posts that results+  -- from applying the predicate above. Might remove more+  -- postings. Postings removed still affect the running balance.++  -> [(Ly.LibertyMeta, L.Posting)]+  -> [(PostMeta, L.Posting)]+toBoxList szb pdct pff+  = addMetadata+  . Ly.processPostFilters pff+  . filter (maybe False id . Pe.eval pdct . snd)+  . addBalances szb++addBalances+  :: CO.ShowZeroBalances+  -> [(a, L.Posting)]+  -> [(L.Balance, (a, L.Posting))]+addBalances szb = snd . mapAccumL (balanceAccum szb) mempty++balanceAccum+  :: CO.ShowZeroBalances+  -> L.Balance+  -> (a, L.Posting)+  -> (L.Balance, (L.Balance, (a, L.Posting)))+balanceAccum (CO.ShowZeroBalances szb) balOld (x, po) =+  let balThis = L.entryToBalance . Q.entry $ po+      balNew = mappend balOld balThis+      balNoZeroes = L.removeZeroCommodities balNew+      bal' = if szb then balNew else balNoZeroes+      po' = (bal', (x, po))+  in (bal', po')+
+ lib/Penny/Cabin/Posts/Parser.hs view
@@ -0,0 +1,286 @@+{-# LANGUAGE OverloadedStrings #-}++module Penny.Cabin.Posts.Parser+  ( State(..)+  , allSpecs+  , Error+  , VerboseFilter(..)+  , ShowExpression(..)+  ) where++import Control.Applicative ((<$>), pure, (<*>),+                            Applicative)+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Char (toLower)+import qualified Data.Foldable as Fdbl+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as X+import qualified System.Console.MultiArg.Combinator as C+import qualified System.Console.MultiArg as MA++import qualified Penny.Cabin.Parsers as P+import qualified Penny.Cabin.Posts.Fields as F+import qualified Penny.Cabin.Posts.Types as Ty+import qualified Penny.Cabin.Options as CO+import qualified Penny.Liberty as Ly+import qualified Data.Prednote.Expressions as Exp+import qualified Data.Prednote.Pdct as Pt+import qualified Penny.Lincoln as L+import qualified Penny.Shield as S+import qualified Text.Matchers as M++newtype VerboseFilter = VerboseFilter { unVerboseFilter :: Bool }+  deriving (Eq, Show)++newtype ShowExpression = ShowExpression { unShowExpression :: Bool }+  deriving (Eq, Show)++data State = State+  { sensitive :: M.CaseSensitive+  , factory :: L.Factory+  , tokens :: [Exp.Token (Ly.LibertyMeta, L.Posting)]+  , postFilter :: [Ly.PostFilterFn]+  , fields :: F.Fields Bool+  , width :: Ty.ReportWidth+  , showZeroBalances :: CO.ShowZeroBalances+  , exprDesc :: Exp.ExprDesc+  , verboseFilter :: VerboseFilter+  , showExpression :: ShowExpression+  }++type Error = X.Text++allSpecs+  :: S.Runtime -> [MA.OptSpec (State -> Ex.Exceptional Error State)]+allSpecs rt =+  operand rt+  ++ boxFilters+  ++ parsePostFilter+  ++ (map (fmap (pure .)) matcherSelect)+  ++ (map (fmap (pure .)) caseSelect)+  ++ (map (fmap (pure .)) operator)+  ++ map (fmap (pure .)) parseExprType+  ++ [ parseWidth+     , showField+     , hideField+     , fmap (pure .) showAllFields+     , fmap (pure .) hideAllFields+     , fmap (pure .) parseZeroBalances+     , fmap (pure .) parseShowExpression+     , fmap (pure .) parseVerboseFilter+     ]+++operand+  :: S.Runtime+  -> [MA.OptSpec (State -> Ex.Exceptional Error State)]+operand rt = map (fmap f) (Ly.operandSpecs (S.currentTime rt))+  where+    f lyFn st = do+      let cs = sensitive st+          fty = factory st+      g <- lyFn cs fty+      let g' = Pt.boxPdct snd g+          ts' = tokens st ++ [Exp.operand g']+      return $ st { tokens = ts' }+++-- | Processes a option for box-level serials.+optBoxSerial+  :: String+  -- ^ Serial name++  -> (Ly.LibertyMeta -> Int)+  -- ^ Pulls the serial from the PostMeta++  -> C.OptSpec (State -> Ex.Exceptional Error State)++optBoxSerial nm f = C.OptSpec [nm] "" (C.TwoArg g)+  where+    g a1 a2 st = do+      i <- Ly.parseInt a2+      let getPd = Pt.compareBy (X.pack . show $ i)+                  ("serial " <> X.pack nm) cmp+          cmp l = compare (f . fst $ l) i+      pd <- Ly.parseComparer a1 getPd+      let tok = Exp.operand pd+      return $ st { tokens = tokens st ++ [tok] }++optFilteredNum :: C.OptSpec (State -> Ex.Exceptional Error State)+optFilteredNum = optBoxSerial "filtered" f+  where+    f = L.forward . Ly.unFilteredNum . Ly.filteredNum++optRevFilteredNum :: C.OptSpec (State -> Ex.Exceptional Error State)+optRevFilteredNum = optBoxSerial "revFiltered" f+  where+    f = L.backward . Ly.unFilteredNum . Ly.filteredNum++optSortedNum :: C.OptSpec (State -> Ex.Exceptional Error State)+optSortedNum = optBoxSerial "sorted" f+  where+    f = L.forward . Ly.unSortedNum . Ly.sortedNum++optRevSortedNum :: C.OptSpec (State -> Ex.Exceptional Error State)+optRevSortedNum = optBoxSerial "revSorted" f+  where+    f = L.backward . Ly.unSortedNum . Ly.sortedNum++boxFilters :: [C.OptSpec (State -> Ex.Exceptional Error State)]+boxFilters =+  [ optFilteredNum+  , optRevFilteredNum+  , optSortedNum+  , optRevSortedNum+  ]+++parsePostFilter :: [C.OptSpec (State -> Ex.Exceptional Error State)]+parsePostFilter = [fmap f optH, fmap f optT]+  where+    (optH, optT) = Ly.postFilterSpecs+    f exc st = fmap g exc+      where+        g pff = st { postFilter = postFilter st ++ [pff] }+++matcherSelect :: [C.OptSpec (State -> State)]+matcherSelect = map (fmap f) Ly.matcherSelectSpecs+  where+    f mf st = st { factory = mf }+++caseSelect :: [C.OptSpec (State -> State)]+caseSelect = map (fmap f) Ly.caseSelectSpecs+  where+    f cs st = st { sensitive = cs }++operator :: [C.OptSpec (State -> State)]+operator = map (fmap f) Ly.operatorSpecs+  where+    f oo st = st { tokens = tokens st ++ [oo] }++parseWidth :: C.OptSpec (State -> Ex.Exceptional Error State)+parseWidth = C.OptSpec ["width"] "" (C.OneArg f)+  where+    f a1 st = do+      i <- Ly.parseInt a1+      return $ st { width = Ty.ReportWidth i }++parseField :: String -> Ex.Exceptional Error (F.Fields Bool)+parseField str =+  let lower = map toLower str+      checkField s =+        if (map toLower s) == lower+        then (s, True)+        else (s, False)+      flds = checkField <$> F.fieldNames+  in case checkFields flds of+      Ex.Exception e -> case e of+        NoMatchingFields -> Ex.throw+          $ "no field matches the name \"" <> X.pack str <> "\"\n"+        MultipleMatchingFields ts -> Ex.throw+          $ "multiple fields match the name \"" <> X.pack str+            <> "\" matches: " <> mtchs <> "\n"+          where+            mtchs = X.intercalate " "+                    . map (\x -> "\"" <> x <> "\"")+                    $ ts+      Ex.Success g -> return g+++-- | Turns a field on if it is True.+fieldOn ::+  F.Fields Bool+  -- ^ Fields as seen so far++  -> F.Fields Bool+  -- ^ Record that should have one True element indicating a field+  -- name seen on the command line; other elements should be False++  -> F.Fields Bool+  -- ^ Fields as seen so far, with new field added++fieldOn old new = (||) <$> old <*> new++-- | Turns off a field if it is True.+fieldOff ::+  F.Fields Bool+  -- ^ Fields seen so far++  -> F.Fields Bool+  -- ^ Record that should have one True element indicating a field+  -- name seen on the command line; other elements should be False++  -> F.Fields Bool+  -- ^ Fields as seen so far, with new field added++fieldOff old new = f <$> old <*> new+  where+    f o False = o+    f _ True = False++showField :: C.OptSpec (State -> Ex.Exceptional Error State)+showField = C.OptSpec ["show"] "" (C.OneArg f)+  where+    f a1 st = do+      fl <- parseField a1+      let newFl = fieldOn (fields st) fl+      return $ st { fields = newFl }++hideField :: C.OptSpec (State -> Ex.Exceptional Error State)+hideField = C.OptSpec ["hide"] "" (C.OneArg f)+  where+    f a1 st = do+      fl <- parseField a1+      let newFl = fieldOff (fields st) fl+      return $ st { fields = newFl }++showAllFields :: C.OptSpec (State -> State)+showAllFields = C.OptSpec ["show-all"] "" (C.NoArg f)+  where+    f st = st {fields = pure True}++hideAllFields :: C.OptSpec (State -> State)+hideAllFields = C.OptSpec ["hide-all"] "" (C.NoArg f)+  where+    f st = st {fields = pure False}++parseZeroBalances :: C.OptSpec (State -> State)+parseZeroBalances = fmap f P.zeroBalances+  where+    f szb st = st { showZeroBalances = szb }++parseExprType :: [C.OptSpec (State -> State)]+parseExprType = map (fmap f) [Ly.parseInfix, Ly.parseRPN]+  where+    f d st = st { exprDesc = d }++parseShowExpression :: C.OptSpec (State -> State)+parseShowExpression = fmap f Ly.showExpression+  where+    f _ st = st { showExpression = ShowExpression True }++parseVerboseFilter :: C.OptSpec (State -> State)+parseVerboseFilter = fmap f Ly.verboseFilter+  where+    f _ st = st { verboseFilter = VerboseFilter True }++data BadFieldError+  = NoMatchingFields+  | MultipleMatchingFields [Text]+  deriving Show++-- | Checks the fields with the True value to ensure there is only one.+checkFields ::+  F.Fields (String, Bool)+  -> Ex.Exceptional BadFieldError (F.Fields Bool)+checkFields fs =+  let f (s, b) ls = if b then s:ls else ls+  in case Fdbl.foldr f [] fs of+    [] -> Ex.throw NoMatchingFields+    _:[] -> return (snd <$> fs)+    ms -> Ex.throw . MultipleMatchingFields . map X.pack $ ms++
+ lib/Penny/Cabin/Posts/Spacers.hs view
@@ -0,0 +1,32 @@+-- | 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 Spacers a = Spacers+  { 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+  } deriving (Show, Eq)
+ lib/Penny/Cabin/Posts/Types.hs view
@@ -0,0 +1,4 @@+module Penny.Cabin.Posts.Types where++newtype ReportWidth = ReportWidth { unReportWidth :: Int }+                      deriving (Eq, Show, Ord)
+ lib/Penny/Cabin/Row.hs view
@@ -0,0 +1,138 @@+-- | Displays a single on-screen row. A row may contain multiple+-- screen lines and multiple columns.+--+-- This module only deals with a single row at a time. Each cell in+-- the row can have more than one screen line; this module will make+-- sure that the cells have appropriate padding on the bottom so that+-- the row appears nicely. This module will also justify each cell so+-- that its left side or right side is ragged; however, you first have+-- to specify how wide you want the cell to be.+--+-- This module is a little dumber than you might first think it could+-- be. For instance it would be possible to write a function that+-- takes a number of rows and automatically justifies all the cells by+-- finding the widest cell in a column. Indeed I might eventually+-- write such a function because it might be useful in, for example,+-- the multi-commodity balance report. However, such a function would+-- not be useful in all cases; in particular, the Posts report is very+-- complicated to lay out, and the automatic function described above+-- would not do the right thing.+--+-- So this module offers some useful automation, even if it is at a+-- level that is apparently lower that what is possible. Thus the+-- present 'row' function likely will not change, even if eventually I+-- add a 'table' function that automatically justifies many rows.+module Penny.Cabin.Row (+  Justification(LeftJustify, RightJustify),+  ColumnSpec(ColumnSpec, justification, width, padSpec, bits),+  Width(Width, unWidth),+  row ) where++import Data.List (transpose)+import qualified Data.Text as X+import qualified Penny.Cabin.Scheme as E+import qualified System.Console.Rainbow as R++-- | 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 :: Width+             , padSpec :: (E.Label, E.EvenOdd)+             , bits :: [R.Chunk] }++newtype JustifiedCell = JustifiedCell (R.Chunk, R.Chunk)++data JustifiedColumn = JustifiedColumn {+  justifiedCells :: [JustifiedCell]+  , _justifiedWidth :: Width+  , _justifiedPadSpec :: (E.Label, E.EvenOdd) }++newtype PaddedColumns = PaddedColumns [[JustifiedCell]]+newtype CellsByRow = CellsByRow [[JustifiedCell]]+newtype CellRowsWithNewlines = CellRowsWithNewlines [[JustifiedCell]]+newtype Width = Width { unWidth :: Int }+  deriving (Eq, Ord, Show)++justify+  :: Width+  -> Justification+  -> E.Label+  -> E.EvenOdd+  -> E.Changers+  -> R.Chunk+  -> JustifiedCell+justify (Width w) j l eo chgrs pc = JustifiedCell (left, right)+  where+    origWidth = X.length . R.chunkText $ pc+    pad = E.getEvenOddLabelValue l eo chgrs $ R.plain t+    t = X.replicate (max 0 (w - origWidth)) (X.singleton ' ')+    (left, right) = case j of+      LeftJustify -> (pc, pad)+      RightJustify -> (pad, pc)++newtype Height = Height Int+  deriving (Show, Eq, Ord)++height :: [[a]] -> Height+height xs = case xs of+  [] -> Height 0+  ls -> Height . maximum . map length $ ls++row :: E.Changers -> [ColumnSpec] -> [R.Chunk]+row chgrs =+  concat+  . concat+  . toBits+  . toCellRowsWithNewlines+  . toCellsByRow+  . bottomPad chgrs+  . map (justifiedColumn chgrs)++justifiedColumn :: E.Changers -> ColumnSpec -> JustifiedColumn+justifiedColumn chgrs (ColumnSpec j w (l, eo) bs)+  = JustifiedColumn cs w (l, eo)+  where+    cs = map (justify w j l eo chgrs) bs++bottomPad :: E.Changers -> [JustifiedColumn] -> PaddedColumns+bottomPad chgrs jcs = PaddedColumns pcs where+  justCells = map justifiedCells jcs+  (Height h) = height justCells+  pcs = map toPaddedColumn jcs+  toPaddedColumn (JustifiedColumn cs (Width w) (lbl, eo)) =+    let l = length cs+        nPads = max 0 $ h - l+        pad = E.getEvenOddLabelValue lbl eo chgrs $ R.plain t+        t = X.replicate w (X.singleton ' ')+        pads = replicate nPads $ JustifiedCell (R.plain X.empty, pad)+    in cs ++ pads+++toCellsByRow :: PaddedColumns -> CellsByRow+toCellsByRow (PaddedColumns cs) = CellsByRow (transpose cs)+++toCellRowsWithNewlines :: CellsByRow -> CellRowsWithNewlines+toCellRowsWithNewlines (CellsByRow bs) =+  CellRowsWithNewlines bs' where+    bs' = foldr f [] bs+    newline = JustifiedCell (R.plain X.empty, R.plain (X.singleton '\n'))+    f cells acc = (cells ++ [newline]) : acc+++toBits :: CellRowsWithNewlines -> [[[R.Chunk]]]+toBits (CellRowsWithNewlines cs) = map (map toB) cs where+  toB (JustifiedCell (c1, c2)) = [c1, c2]+
+ lib/Penny/Cabin/Scheme.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Cabin color schemes+--+-- Each element of a Cabin report identifies what it is--a debit on an+-- even line, a credit on an odd line, etc. The user can have several+-- color schemes; the scheme contains color assignments for 8 and 256+-- color terminals. This allows the use of different schemes for light+-- and dark terminals or for any other reason.++module Penny.Cabin.Scheme where++import qualified Penny.Cabin.Meta as M+import qualified Penny.Lincoln as L+import qualified Data.Text as X+import qualified System.Console.Rainbow as R++data Label+  = Debit+  | Credit+  | Zero+  | Other+  deriving (Eq, Ord, Show)++data EvenOdd = Even | Odd deriving (Eq, Ord, Show)++data Labels a = Labels+  { debit :: a+  , credit :: a+  , zero :: a+  , other :: a+  } deriving Show++getLabelValue :: Label -> Labels a -> a+getLabelValue l ls = case l of+  Debit -> debit ls+  Credit -> credit ls+  Zero -> zero ls+  Other -> other ls++data EvenAndOdd a = EvenAndOdd+  { eoEven :: a+  , eoOdd :: a+  } deriving Show++type Changers = Labels (EvenAndOdd (R.Chunk -> R.Chunk))++data Scheme = Scheme+  { name :: String+    -- ^ The name of this scheme. How it will be identified on the+    -- command line.++  , description :: String+    -- ^ A brief (one-line) description of what this scheme is, such+    -- as @for dark background terminals@++  , changers :: Changers+  } deriving Show+++getEvenOdd :: EvenOdd -> EvenAndOdd a -> a+getEvenOdd eo eao = case eo of+  Even -> eoEven eao+  Odd -> eoOdd eao++getEvenOddLabelValue+  :: Label+  -> EvenOdd+  -> Labels (EvenAndOdd a)+  -> a+getEvenOddLabelValue l eo ls =+  getEvenOdd eo (getLabelValue l ls)++fromVisibleNum :: M.VisibleNum -> EvenOdd+fromVisibleNum vn =+  let s = M.unVisibleNum vn in+  if even . L.forward $ s then Even else Odd++dcToLbl :: L.DrCr -> Label+dcToLbl L.Debit = Debit+dcToLbl L.Credit = Credit++bottomLineToDrCr :: L.BottomLine -> EvenOdd -> Changers -> R.Chunk+bottomLineToDrCr bl eo chgrs = md c+  where+    (c, md) = case bl of+      L.Zero -> (R.plain "--", getEvenOddLabelValue Zero eo chgrs)+      L.NonZero (L.Column clmDrCr _) -> case clmDrCr of+        L.Debit -> (R.plain "<", getEvenOddLabelValue Debit eo chgrs)+        L.Credit -> (R.plain ">", getEvenOddLabelValue Credit eo chgrs)+++balancesToCmdtys+  :: Changers+  -> EvenOdd+  -> [(L.Commodity, L.BottomLine)]+  -> [R.Chunk]+balancesToCmdtys chgrs eo ls =+  if null ls+  then [getEvenOddLabelValue Zero eo chgrs $ R.plain "--"]+  else map (bottomLineToCmdty chgrs eo) ls++bottomLineToCmdty+  :: Changers+  -> EvenOdd+  -> (L.Commodity, L.BottomLine)+  -> R.Chunk+bottomLineToCmdty chgrs eo (cy, bl) = md c+  where+    c = R.plain . L.unCommodity $ cy+    lbl = case bl of+      L.Zero -> Zero+      L.NonZero (L.Column clmDrCr _) -> dcToLbl clmDrCr+    md = getEvenOddLabelValue lbl eo chgrs++balanceToQtys+  :: Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> EvenOdd+  -> [(L.Commodity, L.BottomLine)]+  -> [R.Chunk]+balanceToQtys chgrs getTxt eo ls =+  if null ls+  then let md = getEvenOddLabelValue Zero eo chgrs+       in [md (R.plain "--")]+  else map (bottomLineToQty chgrs getTxt eo) ls+++bottomLineToQty+  :: Changers+  -> (L.Commodity -> L.Qty -> X.Text)+  -> EvenOdd+  -> (L.Commodity, L.BottomLine)+  -> R.Chunk+bottomLineToQty chgrs getTxt eo (cy, bl) = md (R.plain t)+  where+    (lbl, t) = case bl of+      L.Zero -> (Zero, X.pack "--")+      L.NonZero (L.Column clmDrCr qt) -> (dcToLbl clmDrCr, getTxt cy qt)+    md = getEvenOddLabelValue lbl eo chgrs+
+ lib/Penny/Cabin/Scheme/Schemes.hs view
@@ -0,0 +1,87 @@+-- | Some schemes you can use.++module Penny.Cabin.Scheme.Schemes where++import qualified Penny.Cabin.Scheme as E+import qualified System.Console.Rainbow as R+import System.Console.Rainbow ((.+.))++-- | The light color scheme. You can change various values below to+-- affect the color scheme.+light :: E.Scheme+light = E.Scheme "light" "for light background terminals"+              lightLabels++lightLabels :: E.Labels (E.EvenAndOdd (R.Chunk -> R.Chunk))+lightLabels = E.Labels+  { E.debit = E.EvenAndOdd { E.eoEven = lightDebit lightEvenTextSpec+                           , E.eoOdd = lightDebit lightOddTextSpec }+  , E.credit = E.EvenAndOdd { E.eoEven = lightCredit lightEvenTextSpec+                            , E.eoOdd = lightCredit lightOddTextSpec }+  , E.zero = E.EvenAndOdd { E.eoEven = lightZero lightEvenTextSpec+                          , E.eoOdd = lightZero lightOddTextSpec }+  , E.other = E.EvenAndOdd { E.eoEven = lightEvenTextSpec+                           , E.eoOdd = lightOddTextSpec }+  }++lightEvenTextSpec :: R.Chunk -> R.Chunk+lightEvenTextSpec = id++lightOddTextSpec :: R.Chunk -> R.Chunk+lightOddTextSpec = id .+. R.color8_b_default .+. R.color256_b_255++lightDebit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk+lightDebit f = f .+. R.color8_f_magenta .+. R.color256_f_52++lightCredit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk+lightCredit f = f .+. R.color8_f_cyan .+. R.color256_f_21++lightZero :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk+lightZero f = f .+. R.color8_f_black .+. R.color256_f_0++-- | The dark color scheme. You can change various values below to+-- affect the color scheme.+dark :: E.Scheme+dark = E.Scheme "dark" "for dark background terminals"+              darkLabels++darkLabels :: E.Labels (E.EvenAndOdd (R.Chunk -> R.Chunk))+darkLabels = E.Labels+  { E.debit = E.EvenAndOdd { E.eoEven = darkDebit darkEvenTextSpec+                           , E.eoOdd = darkDebit darkOddTextSpec }+  , E.credit = E.EvenAndOdd { E.eoEven = darkCredit darkEvenTextSpec+                            , E.eoOdd = darkCredit darkOddTextSpec }+  , E.zero = E.EvenAndOdd { E.eoEven = darkZero darkEvenTextSpec+                          , E.eoOdd = darkZero darkOddTextSpec }+  , E.other = E.EvenAndOdd { E.eoEven = darkEvenTextSpec+                           , E.eoOdd = darkOddTextSpec }+  }++darkEvenTextSpec :: R.Chunk -> R.Chunk+darkEvenTextSpec = id++darkOddTextSpec :: R.Chunk -> R.Chunk+darkOddTextSpec = id .+. R.color8_b_default .+. R.color256_b_235++darkDebit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk+darkDebit f = f .+. R.color8_f_magenta .+. R.color256_f_208++darkCredit :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk+darkCredit f = f .+. R.color8_f_cyan .+. R.color256_f_45++darkZero :: (R.Chunk -> R.Chunk) -> R.Chunk -> R.Chunk+darkZero f = f .+. R.color8_f_white .+. R.color256_f_15++-- | Plain scheme has no colors at all.+plain :: E.Scheme+plain = E.Scheme "plain" "uses default terminal colors"+              plainLabels++plainLabels :: E.Labels (E.EvenAndOdd (R.Chunk -> R.Chunk))+plainLabels = E.Labels+  { E.debit = E.EvenAndOdd id id+  , E.credit = E.EvenAndOdd id id+  , E.zero = E.EvenAndOdd id id+  , E.other = E.EvenAndOdd id id+  }+
+ lib/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" ]
+ lib/Penny/Copper.hs view
@@ -0,0 +1,163 @@+-- | Copper - the Penny parser.+--+-- The parse functions in this module only accept lists of files+-- rather than individual files because in order to correctly assign+-- the global serials a single function must be able to see all the+-- transactions, not just the transactions in a single file.+module Penny.Copper+  (+  -- * Convenience functions to read and parse files+  open++  -- * Types for things found in ledger files+  , module Penny.Copper.Interface++  -- * Rendering+  , R.GroupSpec(..)+  , R.GroupSpecs(..)+  , R.item+++  ) where++import Control.Arrow (second)+import qualified Data.Traversable as Tr+import qualified Penny.Copper.Parsec as CP+import qualified Penny.Steel.Sums as S+import Penny.Copper.Interface+import qualified Penny.Copper.Interface as I++import qualified Penny.Lincoln as L+import qualified Penny.Copper.Render as R++-- | Reads and parses the given files. If any of the files is @-@,+-- reads standard input. If the list of files is empty, reads standard+-- input. IO errors are not caught. Parse errors are printed to+-- standard error and the program will exit with a failure.+open :: [String] -> IO [I.LedgerItem]+open ss = fmap parsedToWrapped $ mapM CP.parse ss++addFilePosting+  :: Tr.Traversable f+  => [S.S4 (a, f b) x y z]+  -> [S.S4 (a, f (L.FilePosting, b)) x y z]+addFilePosting = L.serialNestedItems f where+  f i = case i of+    S.S4a (a, ctnr) ->+      Right ( ctnr+            , (\ser ii -> (L.FilePosting ser, ii))+            , (\res -> S.S4a (a, res))+            )+    S.S4b x -> Left (S.S4b x)+    S.S4c x -> Left (S.S4c x)+    S.S4d x -> Left (S.S4d x)++addFileTransaction+  :: [S.S4 (a, b) x y z]+  -> [S.S4 ((L.FileTransaction, a), b) x y z]+addFileTransaction = L.serialSomeItems f where+  f i = case i of+    S.S4a (a, b) -> Right (\ser -> S.S4a ((L.FileTransaction ser, a), b))+    S.S4b x -> Left (S.S4b x)+    S.S4c x -> Left (S.S4c x)+    S.S4d x -> Left (S.S4d x)++addGlobalTransaction+  :: [S.S4 (a, b) x y z]+  -> [S.S4 ((L.GlobalTransaction, a), b) x y z]+addGlobalTransaction = L.serialSomeItems f where+  f i = case i of+    S.S4a (a, b) -> Right (\ser -> S.S4a ((L.GlobalTransaction ser, a), b))+    S.S4b x -> Left (S.S4b x)+    S.S4c x -> Left (S.S4c x)+    S.S4d x -> Left (S.S4d x)++addGlobalPosting+  :: Tr.Traversable f+  => [S.S4 (a, f b) x y z]+  -> [S.S4 (a, f (L.GlobalPosting, b)) x y z]+addGlobalPosting = L.serialNestedItems f where+  f i = case i of+    S.S4a (a, ctnr) ->+      Right ( ctnr+            , (\ser ii -> (L.GlobalPosting ser, ii))+            , (\res -> S.S4a (a, res))+            )+    S.S4b x -> Left (S.S4b x)+    S.S4c x -> Left (S.S4c x)+    S.S4d x -> Left (S.S4d x)++addFilename+  :: L.Filename+  -> [S.S4 (a, b) x y z]+  -> [S.S4 ((L.Filename, a), b) x y z]+addFilename fn = map f where+  f i = case i of+    S.S4a (a, b) -> S.S4a ((fn, a), b)+    S.S4b x -> S.S4b x+    S.S4c x -> S.S4c x+    S.S4d x -> S.S4d x++addFileSerials+  :: Tr.Traversable f+  => [S.S4 (a, f b) x y z]+  -> [S.S4 ((L.FileTransaction, a), f (L.FilePosting, b)) x y z]+addFileSerials+  = addFilePosting+  . addFileTransaction++addFileData+  :: Tr.Traversable f+  => (L.Filename, [S.S4 (a, f b) x y z])+  -> [S.S4 ((L.Filename, (L.FileTransaction, a)), f (L.FilePosting, b)) x y z]+addFileData = uncurry addFilename . second addFileSerials++addGlobalSerials+  :: Tr.Traversable f+  => [S.S4 (a, f b) x y z]+  -> [S.S4 ((L.GlobalTransaction, a), f (L.GlobalPosting, b)) x y z]+addGlobalSerials+  = addGlobalTransaction+  . addGlobalPosting++addAllMetadata+  :: Tr.Traversable f+  => [(L.Filename, [S.S4 (a, f b) x y z])]+  -> [S.S4 ((L.GlobalTransaction, (L.Filename, (L.FileTransaction, a))),+              f (L.GlobalPosting, (L.FilePosting, b))) x y z]+addAllMetadata+  = addGlobalSerials+  . concat+  . map addFileData++rewrapMetadata+  :: Functor f+  => ( (L.GlobalTransaction, (L.Filename, (L.FileTransaction, I.ParsedTopLine)))+     , f (L.GlobalPosting, (L.FilePosting, (L.PostingCore, L.PostingLine))))+  -> (L.TopLineData, f (L.PostingData))+rewrapMetadata ((gt, (fn, (ft, ptl))), ctr) = (tld, fmap f ctr)+  where+    tld = L.TopLineData+      tlc+      (Just (L.TopLineFileMeta fn (I.ptlTopLineLine ptl)+                                  (fmap snd $ I.ptlMemo ptl)+                             ft))+      (Just gt)+    tlc = L.TopLineCore (I.ptlDateTime ptl) (I.ptlNumber ptl)+                        (I.ptlFlag ptl) (I.ptlPayee ptl)+                        (fmap fst $ I.ptlMemo ptl)+    f (gp, (fp, (pc, pl))) = L.PostingData+      pc+      (Just (L.PostingFileMeta pl fp))+      (Just gp)++parsedToWrapped+  :: [(L.Filename, [I.ParsedItem])]+  -> [I.LedgerItem]+parsedToWrapped = map rewrap . addAllMetadata where+  rewrap i = case i of+    S.S4a x -> S.S4a (L.Transaction . rewrapMetadata $ x)+    S.S4b x -> S.S4b x+    S.S4c x -> S.S4c x+    S.S4d x -> S.S4d x+
+ lib/Penny/Copper/Interface.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DeriveGeneric #-}++module Penny.Copper.Interface where++import qualified Penny.Lincoln as L+import qualified Data.Text as X+import qualified Data.Text.Encoding as XE+import GHC.Generics (Generic)+import qualified Data.Binary as B+import qualified Penny.Steel.Sums as S++data ParsedTopLine = ParsedTopLine+  { ptlDateTime :: L.DateTime+  , ptlNumber :: Maybe L.Number+  , ptlFlag :: Maybe L.Flag+  , ptlPayee :: Maybe L.Payee+  , ptlMemo :: Maybe (L.Memo, L.TopMemoLine)+  , ptlTopLineLine :: L.TopLineLine+  } deriving (Show, Generic)++toTopLineCore :: ParsedTopLine -> L.TopLineCore+toTopLineCore (ParsedTopLine dt nu fl pa me _)+  = L.TopLineCore dt nu fl pa (fmap fst me)++instance B.Binary ParsedTopLine++type ParsedTxn = (ParsedTopLine , L.Ents (L.PostingCore, L.PostingLine))++data BlankLine = BlankLine+  deriving (Eq, Show, Generic)++instance B.Binary BlankLine++newtype Comment = Comment { unComment :: X.Text }+  deriving (Eq, Show)++instance B.Binary Comment where+  get = fmap (Comment . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unComment++type ParsedItem =+  S.S4 ParsedTxn L.PricePoint Comment BlankLine++type LedgerItem =+  S.S4 L.Transaction L.PricePoint Comment BlankLine++type Parser+  = String+  -- ^ Filename of the file to be parsed+  -> IO (L.Filename, [ParsedItem])++-- | Changes a ledger item to remove metadata.+stripMeta+  :: LedgerItem+  -> S.S4 (L.TopLineCore, L.Ents L.PostingCore)+          L.PricePoint+          Comment+          BlankLine+stripMeta = S.mapS4 f id id id where+  f t = let (tl, es) = L.unTransaction t+        in (L.tlCore tl, fmap L.pdCore es)+
+ lib/Penny/Copper/Parsec.hs view
@@ -0,0 +1,415 @@+-- | Parsec parsers for the ledger file format. The format is+-- documented in EBNF in the file @doc\/ledger-grammar.org@.+module Penny.Copper.Parsec where++import qualified Penny.Copper.Interface as I+import qualified Penny.Copper.Terminals as T+import Text.Parsec.Text (Parser)+import Text.Parsec (many, many1, satisfy)+import qualified Text.Parsec as P+import qualified Text.Parsec.Pos as Pos+import Control.Applicative.Permutation (runPerms, maybeAtom)+import Control.Applicative ((<$>), (<$), (<*>), (*>), (<*),+                            (<|>), optional)+import Control.Monad (replicateM, when)+import qualified Penny.Lincoln as L+import qualified Penny.Steel.Sums as S+import Data.Maybe (fromMaybe)+import Data.Text (Text, pack)+import qualified Data.Text as X+import qualified Data.Time as Time+import qualified System.Exit as Exit+import System.Environment (getProgName)+import qualified System.IO as IO+import qualified Data.Text.IO as TIO++lvl1SubAcct :: Parser L.SubAccount+lvl1SubAcct =+  (L.SubAccount . pack) <$> many1 (satisfy T.lvl1AcctChar)++lvl1FirstSubAcct :: Parser L.SubAccount+lvl1FirstSubAcct = lvl1SubAcct++lvl1OtherSubAcct :: Parser L.SubAccount+lvl1OtherSubAcct = satisfy T.colon *> lvl1SubAcct++lvl1Acct :: Parser L.Account+lvl1Acct = f <$> lvl1FirstSubAcct <*> many lvl1OtherSubAcct+  where+    f a as = L.Account (a:as)++quotedLvl1Acct :: Parser L.Account+quotedLvl1Acct =+  satisfy T.openCurly *> lvl1Acct <* satisfy T.closeCurly++lvl2FirstSubAcct :: Parser L.SubAccount+lvl2FirstSubAcct =+  (\c cs -> L.SubAccount (pack (c:cs)))+  <$> satisfy T.letter+  <*> many (satisfy T.lvl2AcctOtherChar)++lvl2OtherSubAcct :: Parser L.SubAccount+lvl2OtherSubAcct =+  (L.SubAccount . pack)+  <$ satisfy T.colon+  <*> many1 (satisfy T.lvl2AcctOtherChar)++lvl2Acct :: Parser L.Account+lvl2Acct =+  (\a as -> L.Account (a:as))+  <$> lvl2FirstSubAcct+  <*> many lvl2OtherSubAcct++ledgerAcct :: Parser L.Account+ledgerAcct = quotedLvl1Acct <|> lvl2Acct++lvl1Cmdty :: Parser L.Commodity+lvl1Cmdty = (L.Commodity . pack) <$> many1 (satisfy T.lvl1CmdtyChar)++quotedLvl1Cmdty :: Parser L.Commodity+quotedLvl1Cmdty =+  satisfy T.doubleQuote *> lvl1Cmdty <* satisfy (T.doubleQuote)++lvl2Cmdty :: Parser L.Commodity+lvl2Cmdty =+  (\c cs -> L.Commodity (pack (c:cs)))+  <$> satisfy T.lvl2CmdtyFirstChar+  <*> many (satisfy T.lvl2CmdtyOtherChar)++lvl3Cmdty :: Parser L.Commodity+lvl3Cmdty = (L.Commodity . pack) <$> many1 (satisfy T.lvl3CmdtyChar)++digitGroup :: Parser [Char]+digitGroup = satisfy T.thinSpace *> many1 (satisfy T.digit)++digitSequence :: Parser [Char]+digitSequence =+  (++) <$> many1 (satisfy T.digit)+  <*> (concat <$> (many digitGroup))++digitPostSequence :: Parser (Maybe [Char])+digitPostSequence = satisfy T.period *> optional digitSequence++quantity :: Parser L.Qty+quantity = p >>= failOnErr+  where+    p = (L.RadFrac <$> (satisfy T.period *> digitSequence))+        <|> (f <$> digitSequence <*> optional digitPostSequence)+    f digSeq maybePostSeq = case maybePostSeq of+      Nothing -> L.Whole digSeq+      Just ps ->+        maybe (L.WholeRad digSeq) (L.WholeRadFrac digSeq) ps+    failOnErr = maybe (fail msg) return . L.toQty+    msg = "could not read quantity; zero quantities not allowed"++spaceBetween :: Parser L.SpaceBetween+spaceBetween = f <$> optional (many1 (satisfy T.white))+  where+    f = maybe L.NoSpaceBetween (const L.SpaceBetween)++leftCmdtyLvl1Amt :: Parser (L.Amount, L.Side, L.SpaceBetween)+leftCmdtyLvl1Amt =+  f <$> quotedLvl1Cmdty <*> spaceBetween <*> quantity+  where+    f c s q = (L.Amount q c , L.CommodityOnLeft, s)++leftCmdtyLvl3Amt :: Parser (L.Amount, L.Side, L.SpaceBetween)+leftCmdtyLvl3Amt = f <$> lvl3Cmdty <*> spaceBetween <*> quantity+  where+    f c s q = (L.Amount q c, L.CommodityOnLeft, s)++leftSideCmdtyAmt :: Parser (L.Amount, L.Side, L.SpaceBetween)+leftSideCmdtyAmt = leftCmdtyLvl1Amt <|> leftCmdtyLvl3Amt++rightSideCmdty :: Parser L.Commodity+rightSideCmdty = quotedLvl1Cmdty <|> lvl2Cmdty++rightSideCmdtyAmt :: Parser (L.Amount, L.Side, L.SpaceBetween)+rightSideCmdtyAmt =+  f <$> quantity <*> spaceBetween <*> rightSideCmdty+  where+    f q s c = (L.Amount q c ,L.CommodityOnRight, s)+++amount :: Parser (L.Amount, L.Side, L.SpaceBetween)+amount = leftSideCmdtyAmt <|> rightSideCmdtyAmt++comment :: Parser I.Comment+comment =+  (I.Comment . pack)+  <$ satisfy T.hash+  <*> many (satisfy T.nonNewline)+  <* satisfy T.newline+  <* many (satisfy T.white)++year :: Parser Integer+year = read <$> replicateM 4 P.digit++month :: Parser Int+month = read <$> replicateM 2 P.digit++day :: Parser Int+day = read <$> replicateM 2 P.digit++date :: Parser Time.Day+date = p >>= failOnErr+  where+    p = Time.fromGregorianValid+        <$> year  <* satisfy T.dateSep+        <*> month <* satisfy T.dateSep+        <*> day+    failOnErr = maybe (fail "could not parse date") return++hours :: Parser L.Hours+hours = p >>= (maybe (fail "could not parse hours") return)+  where+    p = f <$> satisfy T.digit <*> satisfy T.digit+    f d1 d2 = L.intToHours . read $ [d1,d2]+++minutes :: Parser L.Minutes+minutes = p >>= maybe (fail "could not parse minutes") return+  where+    p = f <$ satisfy T.colon <*> satisfy T.digit <*> satisfy T.digit+    f d1 d2 = L.intToMinutes . read $ [d1, d2]++seconds :: Parser L.Seconds+seconds = p >>= maybe (fail "could not parse seconds") return+  where+    p = f <$ satisfy T.colon <*> satisfy T.digit <*> satisfy T.digit+    f d1 d2 = L.intToSeconds . read $ [d1, d2]++time :: Parser (L.Hours, L.Minutes, Maybe L.Seconds)+time = (,,) <$> hours <*> minutes <*> optional seconds++tzSign :: Parser (Int -> Int)+tzSign = (id <$ satisfy T.plus) <|> (negate <$ satisfy T.minus)++tzNumber :: Parser Int+tzNumber = read <$> replicateM 4 (satisfy T.digit)++timeZone :: Parser L.TimeZoneOffset+timeZone = p >>= maybe (fail "could not parse time zone") return+  where+    p = f <$> tzSign <*> tzNumber+    f s = L.minsToOffset . s++timeWithZone+  :: Parser (L.Hours, L.Minutes,+             Maybe L.Seconds, Maybe L.TimeZoneOffset)+timeWithZone =+  f <$> time <* many (satisfy T.white) <*> optional timeZone+  where+    f (h, m, s) tz = (h, m, s, tz)++dateTime :: Parser L.DateTime+dateTime =+  f <$> date <* many (satisfy T.white) <*> optional timeWithZone+  where+    f d mayTwithZ = L.DateTime d h m s tz+      where+        ((h, m, s), tz) = case mayTwithZ of+          Nothing -> (L.midnight, L.noOffset)+          Just (hr, mn, mayS, mayTz) ->+            let sec = fromMaybe L.zeroSeconds mayS+                z = fromMaybe L.noOffset mayTz+            in ((hr, mn, sec), z)++debit :: Parser L.DrCr+debit = L.Debit <$ satisfy T.lessThan++credit :: Parser L.DrCr+credit = L.Credit <$ satisfy T.greaterThan++drCr :: Parser L.DrCr+drCr = debit <|> credit++entry :: Parser (L.Entry, L.Side, L.SpaceBetween)+entry = f <$> drCr <* (many (satisfy T.white)) <*> amount+  where+    f dc (am, sd, sb) = (L.Entry dc am, sd, sb)++flag :: Parser L.Flag+flag = (L.Flag . pack) <$ satisfy T.openSquare+  <*> many (satisfy T.flagChar) <* satisfy (T.closeSquare)++postingMemoLine :: Parser Text+postingMemoLine =+  pack+  <$ satisfy T.apostrophe+  <*> many (satisfy T.nonNewline)+  <* satisfy T.newline <* many (satisfy T.white)++postingMemo :: Parser L.Memo+postingMemo = L.Memo <$> many1 postingMemoLine++transactionMemoLine :: Parser Text+transactionMemoLine =+  pack+  <$ satisfy T.semicolon <*> many (satisfy T.nonNewline)+  <* satisfy T.newline <* skipWhite++transactionMemo :: Parser (L.TopMemoLine, L.Memo)+transactionMemo = f <$> lineNum <*> many1 transactionMemoLine+  where+    f tml ls = (L.TopMemoLine tml+               , L.Memo ls)+++number :: Parser L.Number+number =+  L.Number . pack <$ satisfy T.openParen+  <*> many (satisfy T.numberChar) <* satisfy T.closeParen++lvl1Payee :: Parser L.Payee+lvl1Payee = L.Payee . pack <$> many (satisfy T.quotedPayeeChar)++quotedLvl1Payee :: Parser L.Payee+quotedLvl1Payee = satisfy T.tilde *> lvl1Payee <* satisfy T.tilde++lvl2Payee :: Parser L.Payee+lvl2Payee = (\c cs -> L.Payee (pack (c:cs))) <$> satisfy T.letter+            <*> many (satisfy T.nonNewline)++fromCmdty :: Parser L.From+fromCmdty = L.From <$> (quotedLvl1Cmdty <|> lvl2Cmdty)++lineNum :: Parser Int+lineNum = Pos.sourceLine <$> P.getPosition++price :: Parser L.PricePoint+price = p >>= maybe (fail msg) return+  where+    f li dt fr (L.Amount qt to, sd, sb) =+      let cpu = L.CountPerUnit qt+      in case L.newPrice fr (L.To to) cpu of+        Nothing -> Nothing+        Just pr -> Just $ L.PricePoint dt pr+                          (Just sd) (Just sb) (Just $ L.PriceLine li)+    p = f <$> lineNum <* satisfy T.atSign <* skipWhite+        <*> dateTime <* skipWhite+        <*> fromCmdty <* skipWhite+        <*> amount <* satisfy T.newline <* skipWhite+    msg = "could not parse price, make sure the from and to commodities "+          ++ "are different"++tag :: Parser L.Tag+tag = L.Tag . pack <$ satisfy T.asterisk <*> many (satisfy T.tagChar)+      <* many (satisfy T.white)++tags :: Parser L.Tags+tags = (\t ts -> L.Tags (t:ts)) <$> tag <*> many tag++topLinePayee :: Parser L.Payee+topLinePayee = quotedLvl1Payee <|> lvl2Payee++topLineFlagNum :: Parser (Maybe L.Flag, Maybe L.Number)+topLineFlagNum = p1 <|> p2+  where+    p1 = ( (,) <$> optional flag+               <* many (satisfy T.white) <*> optional number)+    p2 = ( flip (,)+           <$> optional number+           <* many (satisfy T.white) <*> optional flag)++skipWhite :: Parser ()+skipWhite = () <$ many (satisfy T.white)++topLine :: Parser I.ParsedTopLine+topLine =+  f <$> optional transactionMemo+    <*> lineNum+    <*> dateTime+    <*  skipWhite+    <*> topLineFlagNum+    <*  skipWhite+    <*> optional topLinePayee+    <*  satisfy T.newline+    <*  skipWhite+  where+    f mayMe lin dt (mayFl, mayNum) mayPy =+      I.ParsedTopLine dt mayNum mayFl mayPy me (L.TopLineLine lin)+      where+        me = fmap (\(a, b) -> (b, a)) mayMe++flagNumPayee :: Parser (Maybe L.Flag, Maybe L.Number, Maybe L.Payee)+flagNumPayee = runPerms+  ( (,,) <$> maybeAtom (flag <* skipWhite)+         <*> maybeAtom (number <* skipWhite)+         <*> maybeAtom (quotedLvl1Payee <* skipWhite) )++postingAcct :: Parser L.Account+postingAcct = quotedLvl1Acct <|> lvl2Acct++posting :: Parser (L.PostingCore, L.PostingLine, Maybe L.Entry)+posting = f <$> lineNum                <* skipWhite+            <*> optional flagNumPayee  <* skipWhite+            <*> postingAcct            <* skipWhite+            <*> optional tags          <* skipWhite+            <*> optional entry         <* skipWhite+            <*  satisfy T.newline      <* skipWhite+            <*> optional postingMemo   <* skipWhite+  where+    f li mayFnp ac ta mayEn me =+      (L.PostingCore pa nu fl ac tgs me sd sb, pl, en)+      where+        tgs = fromMaybe (L.Tags []) ta+        pl = L.PostingLine li+        (fl, nu, pa) = fromMaybe (Nothing, Nothing, Nothing) mayFnp+        (en, sd, sb) = maybe (Nothing, Nothing, Nothing)+          (\(a, b, c) -> (Just a, Just b, Just c)) mayEn++transaction :: Parser I.ParsedTxn+transaction = do+  ptl <- topLine+  let getEntPair (core, lin, mayEn) = (mayEn, (core, lin))+  ts <- fmap (map getEntPair) $ many posting+  ents <- maybe (fail "unbalanced transaction") return $ L.ents ts+  return (ptl, ents)+++blankLine :: Parser ()+blankLine = () <$ satisfy T.newline <* skipWhite++item :: Parser I.ParsedItem+item+  = fmap S.S4a transaction+  <|> fmap S.S4b price+  <|> fmap S.S4c comment+  <|> (S.S4d I.BlankLine) <$ blankLine++parse+  :: String+  -- ^ Name of the file to be parsed+  -> IO (L.Filename, [I.ParsedItem])+  -- ^ Returns items if successfully parsed. Quits and exits if the+  -- parse fails.++parse s = do+  (fn, txt) <- getFileContentsStdin s+  let parser = P.spaces *> P.many item <* P.spaces <* P.eof+      filename = X.unpack . L.unFilename $ fn+  case P.parse parser filename txt of+    Left err -> do+      pn <- getProgName+      let msg = pn ++ ": error: could not parse file "+                ++ filename ++ "\n" ++ show err+      IO.hPutStr IO.stderr msg+      Exit.exitFailure+    Right g -> return (fn, g)+++getFileContentsStdin :: String -> IO (L.Filename, Text)+getFileContentsStdin s = do+  pn <- getProgName+  txt <- if s == "-"+    then do+          isTerm <- IO.hIsTerminalDevice IO.stdin+          when isTerm+            (IO.hPutStrLn IO.stderr $+               pn ++ ": warning: reading from standard input, which"+               ++ "is a terminal.")+          TIO.hGetContents IO.stdin+    else TIO.readFile s+  let fn = L.Filename . X.pack $ if s == "-" then "<stdin>" else s+  return (fn, txt)
+ lib/Penny/Copper/Render.hs view
@@ -0,0 +1,530 @@+-- | Renders Penny data in a format that can be parsed by+-- "Penny.Copper.Parsec". These functions render text that is+-- compliant with the EBNF grammar which is at+-- @doc\/ledger-grammar.org@.+module Penny.Copper.Render where++import Control.Monad (guard)+import Control.Applicative ((<$>), (<|>), (<*>), pure)+import Data.List (intersperse, intercalate)+import Data.List.Split (chunksOf, splitOn)+import qualified Data.Text as X+import Data.Text (Text, cons, snoc)+import qualified Penny.Copper.Terminals as T+import qualified Data.Time as Time+import qualified Penny.Copper.Interface as I+import qualified Penny.Lincoln as L+import qualified Penny.Steel.Sums as S++-- * Helpers++-- | 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++-- | 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+++-- * Accounts++-- | Is True if a sub account can be rendered at Level 1;+-- False otherwise.+isSubAcctLvl1 :: L.SubAccount -> Bool+isSubAcctLvl1 (L.SubAccount x) =+  X.all T.lvl1AcctChar x && not (X.null x)++isAcctLvl1 :: L.Account -> Bool+isAcctLvl1 (L.Account ls) =+  (not . null $ ls)+  && (all isSubAcctLvl1 ls)++quotedLvl1Acct :: L.Account -> Maybe Text+quotedLvl1Acct a@(L.Account ls) = do+  guard (isAcctLvl1 a)+  let txt = X.concat . intersperse (X.singleton ':')+            . map L.unSubAccount $ ls+  return $ '{' `X.cons` txt `X.snoc` '}'++isFirstSubAcctLvl2 :: L.SubAccount -> Bool+isFirstSubAcctLvl2 (L.SubAccount x) = case X.uncons x of+  Nothing -> False+  Just (c, r) -> T.letter c && (X.all T.lvl2AcctOtherChar r)++isOtherSubAcctLvl2 :: L.SubAccount -> Bool+isOtherSubAcctLvl2 (L.SubAccount x) =+  (not . X.null $ x)+  && (X.all T.lvl2AcctOtherChar x)++isAcctLvl2 :: L.Account -> Bool+isAcctLvl2 (L.Account ls) = case ls of+  [] -> False+  x:xs -> isFirstSubAcctLvl2 x && all isOtherSubAcctLvl2 xs++lvl2Acct :: L.Account -> Maybe Text+lvl2Acct a@(L.Account ls) = do+  guard $ isAcctLvl2 a+  return . X.concat . intersperse (X.singleton ':')+         . map L.unSubAccount $ ls++-- | 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.+ledgerAcct :: L.Account -> Maybe Text+ledgerAcct a = lvl2Acct a <|> quotedLvl1Acct a++-- * Commodities++-- | Render a quoted Level 1 commodity. Fails if any character does+-- not satisfy lvl1Char.+quotedLvl1Cmdty :: L.Commodity -> Maybe Text+quotedLvl1Cmdty (L.Commodity c) =+  if X.all T.lvl1CmdtyChar c+  then Just $ '"' `cons` c `snoc` '"'+  else Nothing+++-- | 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.+lvl2Cmdty :: L.Commodity -> Maybe Text+lvl2Cmdty (L.Commodity c) = do+  (f, rs) <- X.uncons c+  guard $ T.lvl2CmdtyFirstChar f+  guard . X.all T.lvl2CmdtyOtherChar $ rs+  return c+++-- | Render a Level 3 commodity. Fails if any character is not a+-- letter or a symbol.+lvl3Cmdty :: L.Commodity -> Maybe Text+lvl3Cmdty (L.Commodity c) =+  if (not . X.null $ c) && (X.all T.lvl3CmdtyChar c)+  then return c+  else Nothing+++-- * Quantities++-- | Specifies how to perform digit grouping when rendering a+-- quantity. All grouping groups into groups of 3 digits.+data GroupSpec =+  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)+++data GroupSpecs = GroupSpecs+  { left :: GroupSpec+  , right :: GroupSpec+  } deriving Show+++grouper :: String+grouper = "\x2009"++radix :: String+radix = "."++-- | Performs grouping for amounts to the left of the radix point.+groupWhole :: GroupSpec -> String -> String+groupWhole gs o = let+  grouped = intercalate grouper+            . reverse+            . map reverse+            . chunksOf 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 :: GroupSpec -> String -> String+groupDecimal gs o = let+  grouped = intercalate grouper+            . chunksOf 3+            $ o+  in case gs of+    NoGrouping -> o+    GroupLarge -> if length o > 4 then grouped else o+    GroupAll -> grouped++-- | Renders an unquoted Qty. Performs digit grouping as requested.+quantity+  :: GroupSpecs+  -- ^ Group for the portion to the left and right of the radix point?++  -> L.Qty+  -> X.Text+quantity gs q =+  let qs = L.prettyShowQty q+  in X.pack $ case splitOn "." qs of+    w:[] -> groupWhole (left gs) w+    w:d:[] ->+      groupWhole (left gs) w ++ radix ++ groupDecimal (right gs) d+    _ -> error "Qty.hs: rendering error"++-- * Amounts++-- | Render an Amount. The Format is required so that the commodity+-- can be displayed in the right place.+amount+  :: GroupSpecs+  -> Maybe L.Side+  -> Maybe L.SpaceBetween+  -> L.Amount+  -> Maybe X.Text+amount gs maySd maySb (L.Amount qt c) =+  let q = quantity gs qt+  in do+    sd <- maySd+    sb <- maySb+    let ws = case sb of+          L.SpaceBetween -> X.singleton ' '+          L.NoSpaceBetween -> X.empty+    (l, r) <- case sd of+          L.CommodityOnLeft -> do+            cx <- lvl3Cmdty c <|> quotedLvl1Cmdty c+            return (cx, q)+          L.CommodityOnRight -> do+            cx <- lvl2Cmdty c <|> quotedLvl1Cmdty c+            return (q, cx)+    return $ X.concat [l, ws, r]++-- * Comments++comment :: I.Comment -> Maybe X.Text+comment (I.Comment x) =+  if (not . X.all T.nonNewline $ x)+  then Nothing+  else Just $ '#' `cons` x `snoc` '\n'++-- * DateTime++-- | Render a DateTime. The day is always printed. If the time zone+-- offset is not zero, then the time and time zone offset are both+-- printed. If the time zone offset is zero, then the hours and+-- minutes are printed, but only if the time is not midnight. If the+-- seconds are not zero, they are also printed.++dateTime :: L.DateTime -> X.Text+dateTime (L.DateTime d h m s z) = X.append xd xr+  where+    (iYr, iMo, iDy) = Time.toGregorian d+    xr = hoursMinsSecsZone h m s z+    dash = X.singleton '-'+    xd = X.concat [ showX iYr, dash, pad2 . showX $ iMo, dash,+                    pad2 . showX $ iDy ]++pad2 :: X.Text -> X.Text+pad2 = X.justifyRight 2 '0'++pad4 :: X.Text -> X.Text+pad4 = X.justifyRight 4 '0'++showX :: Show a => a -> X.Text+showX = X.pack . show++hoursMinsSecsZone+  :: L.Hours -> L.Minutes -> L.Seconds -> L.TimeZoneOffset -> X.Text+hoursMinsSecsZone h m s z =+  if z == L.noOffset && (h, m, s) == L.midnight+  then X.empty+  else let xhms = X.concat [xh, colon, xm, xs]+           xh = pad2 . showX . L.unHours $ h+           xm = pad2 . showX . L.unMinutes $ m+           xs = let secs = L.unSeconds s+                in if secs == 0+                   then X.empty+                   else ':' `X.cons` (pad2 . showX $ secs)+           off = L.offsetToMins z+           sign = X.singleton $ if off < 0 then '-' else '+'+           padded = pad4 . showX . abs $ off+           xz = if off == 0+                then X.empty+                else ' ' `X.cons` sign `X.append` padded+           colon = X.singleton ':'+       in ' ' `X.cons` xhms `X.append` xz++-- * Entries++entry+  :: GroupSpecs+  -> Maybe L.Side+  -> Maybe L.SpaceBetween+  -> L.Entry+  -> Maybe X.Text+entry gs sd sb (L.Entry dc a) = do+  amt <- amount gs sd sb a+  let dcTxt = X.pack $ case dc of+        L.Debit -> "<"+        L.Credit -> ">"+  return $ X.append (X.snoc dcTxt ' ') amt++-- * Flags++flag :: L.Flag -> Maybe X.Text+flag (L.Flag fl) =+  if X.all T.flagChar fl+  then Just $ '[' `cons` fl `snoc` ']'+  else Nothing++-- * Memos++-- | Renders a postingMemoLine, optionally with trailing+-- whitespace. The trailing whitespace allows the next line to be+-- indented properly if is also a postingMemoLine. This is handled+-- using trailing whitespace rather than leading whitespace because+-- leading whitespace is inconsistent with the grammar.+postingMemoLine+  :: Int+  -- ^ Pad the end of the output with this many spaces+  -> X.Text+  -> Maybe X.Text+postingMemoLine p x =+  if X.all T.nonNewline x+  then let trailing = X.replicate p (X.singleton ' ')+           ls = [X.singleton '\'', x, X.singleton '\n', trailing]+        in Just $ X.concat ls+  else Nothing++-- | Renders a postingMemo. Fails if the postingMemo is empty, as the+-- grammar requires that they have at least one line.+--+-- If the boolean is True, inserts padding after the last+-- postingMemoLine so that the next line is indented by four+-- columns. Use this if the posting memo is followed by another+-- posting. If the last boolean if False, there is no indenting after+-- the last postingMemoLine.+postingMemo :: Bool -> L.Memo -> Maybe X.Text+postingMemo iLast (L.Memo ls) =+  if null ls+  then Nothing+  else let bs = replicate (length ls - 1) 8 ++ [if iLast then 4 else 0]+       in fmap X.concat . sequence $ zipWith postingMemoLine bs ls+++transactionMemoLine :: X.Text -> Maybe X.Text+transactionMemoLine x =+  if X.all T.nonNewline x+  then Just $ ';' `cons` x `snoc` '\n'+  else Nothing++transactionMemo :: L.Memo -> Maybe X.Text+transactionMemo (L.Memo ls) =+  if null ls+  then Nothing+  else fmap X.concat . mapM transactionMemoLine $ ls++-- * Numbers++number :: L.Number -> Maybe Text+number (L.Number t) =+  if X.all T.numberChar t+  then Just $ '(' `cons` t `snoc` ')'+  else Nothing++-- * Payees++quotedLvl1Payee :: L.Payee -> Maybe Text+quotedLvl1Payee (L.Payee p) = do+  guard (X.all T.quotedPayeeChar p)+  return $ '~' `X.cons` p `X.snoc` '~'++lvl2Payee :: L.Payee -> Maybe Text+lvl2Payee (L.Payee p) = do+  (c1, cs) <- X.uncons p+  guard (T.letter c1)+  guard (X.all T.nonNewline cs)+  return p++payee :: L.Payee -> Maybe Text+payee p = lvl2Payee p <|> quotedLvl1Payee p++-- * Prices++price ::+  GroupSpecs+  -> L.PricePoint+  -> Maybe X.Text+price gs pp = let+  dateTxt = dateTime (L.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 = lvl3Cmdty from <|> quotedLvl1Cmdty from+  in do+    amtTxt <- amount gs (L.ppSide pp) (L.ppSpaceBetween pp)+              (L.Amount q to)+    fromTxt <- mayFromTxt+    return $+       (X.intercalate (X.singleton ' ')+       [X.singleton '@', dateTxt, fromTxt, amtTxt])+       `snoc` '\n'++-- * Tags++tag :: L.Tag -> Maybe X.Text+tag (L.Tag t) =+  if X.all T.tagChar t+  then Just $ X.cons '*' t+  else Nothing++tags :: L.Tags -> Maybe X.Text+tags (L.Tags ts) =+  X.intercalate (X.singleton ' ')+  <$> mapM tag ts++-- * TopLine++-- | Renders the TopLine. Emits trailing whitespace after the newline+-- so that the first posting is properly indented.+topLine :: L.TopLineCore -> Maybe X.Text+topLine tl =+  f+  <$> pure (dateTime (L.tDateTime tl))+  <*> renMaybe (L.tMemo tl) transactionMemo+  <*> renMaybe (L.tFlag tl) flag+  <*> renMaybe (L.tNumber tl) number+  <*> renMaybe (L.tPayee tl) payee+  where+    f dtX meX flX nuX paX =+      X.concat [ meX, txtWords [dtX, flX, nuX, paX],+                 X.singleton '\n',+                 X.replicate 4 (X.singleton ' ') ]++-- * Posting++-- | 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).+--+-- This table is a bit of a lie, because the blank spaces for+-- indentation are emitted either by the posting previous to this one+-- (either after the posting itself or after its postingMemo) or by+-- the TopLine.+--+-- Also emits an additional eight spaces after the trailing newline if+-- the posting has a memo. That way the memo will be indented+-- properly. (There are trailing spaces here, as opposed to leading+-- spaces in the posting memo, because the latter would be+-- inconsistent with the grammar.)+--+-- Emits an extra four spaces after the first line if the first+-- paramter is True. However, this is overriden if there is a memo, in+-- which case eight spaces will be emitted. (This allows the next+-- posting to be indented properly.)+posting+  :: GroupSpecs+  -> Bool+  -- ^ If True, emit four spaces after the trailing newline.+  -> L.Ent L.PostingCore+  -> Maybe X.Text+posting gs pad ent = do+  let p = L.meta ent+  fl <- renMaybe (L.pFlag p) flag+  nu <- renMaybe (L.pNumber p) number+  pa <- renMaybe (L.pPayee p) quotedLvl1Payee+  ac <- ledgerAcct (L.pAccount p)+  ta <- tags (L.pTags p)+  me <- renMaybe (L.pMemo p) (postingMemo pad)+  let mayEn = case L.inferred ent of+        L.Inferred -> Nothing+        L.NotInferred -> (Just . L.entry $ ent)+  en <- renMaybe mayEn (entry gs (L.pSide p) (L.pSpaceBetween p))+  return $ formatter pad fl nu pa ac ta en me++formatter ::+  Bool      -- ^ If True, emit four trailing spaces if no memo or+            -- eight trailing spaces if there is a memo.+  -> X.Text -- ^ Flag+  -> X.Text -- ^ Number+  -> X.Text -- ^ Payee+  -> X.Text -- ^ Account+  -> X.Text -- ^ Tags+  -> X.Text -- ^ Entry+  -> X.Text -- ^ Memo+  -> X.Text+formatter pad fl nu pa ac ta en me = let+  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 = '\n' `X.cons` trailingWhite+  trailingWhite = case (X.null me, pad) of+    (True, False) -> X.empty+    (True, True) -> X.replicate 4 (X.singleton ' ')+    (False, _) -> X.replicate 8 (X.singleton ' ')+  in X.concat [colB, colC, colD, rtn, me]+++-- * Transaction++transaction+  :: GroupSpecs+  -> (L.TopLineCore, L.Ents L.PostingCore)+  -> Maybe X.Text+transaction gs txn = do+  tlX <- topLine . fst $ txn+  let (p1, p2, ps) = L.tupleEnts . snd $ txn+  p1X <- posting gs True p1+  p2X <- posting gs (not . null $ ps) p2+  psX <- if null ps+         then return X.empty+         else let bs = replicate (length ps - 1) True ++ [False]+              in fmap X.concat . sequence+                 $ zipWith (posting gs) bs ps+  return $ X.concat [tlX, p1X, p2X, psX]++-- * Item++item+  :: GroupSpecs+  -> S.S4 (L.TopLineCore, L.Ents L.PostingCore)+          L.PricePoint+          I.Comment+          I.BlankLine+  -> Maybe X.Text+item gs =+  S.caseS4 (transaction gs)+           (price gs)+           comment+           (const (Just (X.pack "\n")))+
+ lib/Penny/Copper/Terminals.hs view
@@ -0,0 +1,145 @@+module Penny.Copper.Terminals where++invalid :: Char -> Bool+invalid c = c >= '\xD800' && c <= '\xDFFF'++unicode :: Char -> Bool+unicode = not . invalid++newline :: Char -> Bool+newline = (== '\x0A')++space :: Char -> Bool+space = (== '\x20')++tab :: Char -> Bool+tab = (== '\x09')++white :: Char -> Bool+white c = space c || tab c++nonNewline :: Char -> Bool+nonNewline c = unicode c && (not . newline $ c)++nonNewlineNonSpace :: Char -> Bool+nonNewlineNonSpace c = nonNewline c && (not . white $ c)++upperCaseAscii :: Char -> Bool+upperCaseAscii c = c >= 'A' && c <= 'Z'++lowerCaseAscii :: Char -> Bool+lowerCaseAscii c = c >= 'a' && c <= 'z'++digit :: Char -> Bool+digit c = c >= '0' && c <= '9'++nonAscii :: Char -> Bool+nonAscii c = nonNewline c && c > '\x7F'++letter :: Char -> Bool+letter c = upperCaseAscii c || lowerCaseAscii c || nonAscii c++dollar :: Char -> Bool+dollar = (== '$')++colon :: Char -> Bool+colon = (== ':')++openCurly :: Char -> Bool+openCurly = (== '{')++closeCurly :: Char -> Bool+closeCurly = (== '}')++openSquare :: Char -> Bool+openSquare = (== '[')++closeSquare :: Char -> Bool+closeSquare = (== ']')++doubleQuote :: Char -> Bool+doubleQuote = (== '"')++period :: Char -> Bool+period = (== '.')++hash :: Char -> Bool+hash = (== '#')++thinSpace :: Char -> Bool+thinSpace = (== '\x2009')++dateSep :: Char -> Bool+dateSep c = c == '/' || c == '-'++plus :: Char -> Bool+plus = (== '+')++minus :: Char -> Bool+minus = (== '-')++lessThan :: Char -> Bool+lessThan = (== '<')++greaterThan :: Char -> Bool+greaterThan = (== '>')++openParen :: Char -> Bool+openParen = (== '(')++closeParen :: Char -> Bool+closeParen = (== ')')++semicolon :: Char -> Bool+semicolon = (== ';')++apostrophe :: Char -> Bool+apostrophe = (== '\x27')++tilde :: Char -> Bool+tilde = (== '~')++underscore :: Char -> Bool+underscore = (== '_')++asterisk :: Char -> Bool+asterisk = (== '*')++lvl1AcctChar :: Char -> Bool+lvl1AcctChar c = nonNewline c && (not . closeCurly $ c)+                 && (not . colon $ c)++lvl2AcctOtherChar :: Char -> Bool+lvl2AcctOtherChar c =+  nonNewline c && (not . white $ c) && (not . colon $ c)+  && (not . asterisk $ c) && (not . greaterThan $ c)+  && (not . lessThan $ c)++lvl1CmdtyChar :: Char -> Bool+lvl1CmdtyChar c =+  nonNewline c && (not . doubleQuote $ c)++lvl2CmdtyFirstChar :: Char -> Bool+lvl2CmdtyFirstChar c = letter c || dollar c++lvl2CmdtyOtherChar :: Char -> Bool+lvl2CmdtyOtherChar c = nonNewline c && (not . white $ c)++lvl3CmdtyChar :: Char -> Bool+lvl3CmdtyChar c = letter c || dollar c++flagChar :: Char -> Bool+flagChar c = nonNewline c && (not . closeSquare $ c)++numberChar :: Char -> Bool+numberChar c = nonNewline c && (not . closeParen $ c)++quotedPayeeChar :: Char -> Bool+quotedPayeeChar c = nonNewline c && (not . tilde $ c)++tagChar :: Char -> Bool+tagChar c = nonNewlineNonSpace c && (not . asterisk $ c)+  && (not . greaterThan $ c) && (not . lessThan $ c)++atSign :: Char -> Bool+atSign = (== '@')
+ lib/Penny/Liberty.hs view
@@ -0,0 +1,777 @@+{-# LANGUAGE OverloadedStrings, CPP #-}++-- | 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 (+  MatcherFactory,+  FilteredNum(FilteredNum, unFilteredNum),+  SortedNum(SortedNum, unSortedNum),+  LibertyMeta(filteredNum, sortedNum),+  xactionsToFiltered,+  ListLength(ListLength, unListLength),+  ItemIndex(ItemIndex, unItemIndex),+  PostFilterFn,+  parseComparer,+  processPostFilters,+  parsePredicate,+  parseInt,+  parseInfix,+  parseRPN,+  exprDesc,+  showExpression,+  verboseFilter,++  -- * Parsers+  Operand,+  operandSpecs,+  postFilterSpecs,+  matcherSelectSpecs,+  caseSelectSpecs,+  operatorSpecs,++  -- * Version+  version,+++  -- * Output+  output,+  processOutput,++  -- * Errors+  Error++  ) where++import Control.Arrow (first, second)+import Control.Applicative ((<*>), (<$>), pure, Applicative)+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Char (toUpper)+import Data.Monoid ((<>))+import Data.List (sortBy)+import Data.Text (Text, pack)+import qualified Data.Text as X+import qualified Data.Text.IO as TIO+import qualified Data.Time as Time+import Data.Tuple (swap)+import qualified System.Console.MultiArg as MA+import qualified System.Console.MultiArg.Combinator as C+import System.Console.MultiArg.Combinator (OptSpec)+import Text.Parsec (parse)++import qualified Penny.Copper.Parsec as Pc++import qualified Penny.Lincoln.Predicates as P+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Lincoln.Predicates.Siblings as PS+import qualified Data.Prednote.Pdct as E+import qualified Penny.Lincoln as L+import qualified System.Console.Rainbow as C+import qualified Data.Prednote.Expressions as X++import Text.Matchers (+  CaseSensitive(Sensitive, Insensitive))+import qualified Text.Matchers as TM++#ifdef incabal+import qualified Paths_penny_lib as PPL+#endif+import qualified Data.Version as V+import qualified System.Exit as Exit++-- | A multiline Text that holds an error message.+type Error = Text++-- | 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 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.ExprDesc+  -> [X.Token a]+  -> Ex.Exceptional Error (E.Pdct a)+parsePredicate d ls = case ls of+  [] -> return E.always+  _ -> X.parseExpression d ls++-- | 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. Also returns Chunks+-- containing a description of the evalutation process.++xactionsToFiltered++  :: P.LPdct+  -- ^ The predicate to filter the transactions++  -> [PostFilterFn]+  -- ^ Post filter specs++  -> (L.Posting -> L.Posting -> Ordering)+  -- ^ The sorter++  -> [L.Transaction]+  -- ^ The transactions to work on (probably parsed in from Copper)++  -> ([C.Chunk], [(LibertyMeta, L.Posting)])+  -- ^ Sorted, filtered postings++xactionsToFiltered pdct postFilts srtr+  = second (processPostings srtr postFilts)+  . mainFilter pdct+  . concatMap L.transactionToPostings++processPostings+  :: (L.Posting -> L.Posting -> Ordering)+  -> [PostFilterFn]+  -> [L.Posting]+  -> [(LibertyMeta, L.Posting)]+processPostings srtr postFilters+  = (map . first . uncurry $ LibertyMeta)+  . addSortedNum+  . sortBy (\p1 p2 -> srtr (snd p1) (snd p2))+  . processPostFilters postFilters+  . addFilteredNum++mainFilter :: P.LPdct -> [L.Posting] -> ([C.Chunk], [L.Posting])+mainFilter pdct = swap . E.filter indentAmt True 0 L.display pdct++addFilteredNum :: [a] -> [(FilteredNum, a)]+addFilteredNum = L.serialItems (\s p -> (FilteredNum s, p))++addSortedNum :: [(a, b)] -> [((a, SortedNum), b)]+addSortedNum = L.serialItems (\s (a, b) -> ((a, SortedNum s), b))++indentAmt :: E.IndentAmt+indentAmt = 4++type MatcherFactory+  = CaseSensitive+  -> Text+  -> Ex.Exceptional Text TM.Matcher++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..]+++------------------------------------------------------------+-- Operands+------------------------------------------------------------++-- | Given a String from the command line which represents a pattern,+-- the current case sensitivity, and a MatcherFactory, 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 TM.Matcher++getMatcher s cs f+  = Ex.mapException mkError+  $ f cs (pack s)+  where+    mkError eMsg = "bad pattern: \"" <> pack s <> " - " <> eMsg+      <> "\n"+++-- | Parses comparers given on command line to a function. Fails if+-- the string given is invalid.+parseComparer+  :: String+  -> (Ordering -> E.Pdct a)+  -> Ex.Exceptional Error (E.Pdct a)+parseComparer s f = Ex.fromMaybe ("bad comparer: " <> pack s <> "\n")+                  $ E.parseComparer (pack s) f++-- | Parses a date from the command line. On failure, throws back the+-- error message from the failed parse.+parseDate :: String -> Ex.Exceptional Error Time.UTCTime+parseDate arg =+  Ex.mapExceptional err L.toUTC+  . Ex.fromEither+  . parse Pc.dateTime ""+  . pack+  $ arg+  where+    err msg = "bad date: \"" <> pack arg <> "\" - " <> (pack . show $ msg)++type Operand = E.Pdct L.Posting++-- | OptSpec for a date.+date :: OptSpec (Ex.Exceptional Error Operand)+date = C.OptSpec ["date"] ['d'] (C.TwoArg f)+  where+    f a1 a2 = do+      utct <- parseDate a2+      parseComparer a1 (flip P.date utct)+++current :: L.DateTime -> OptSpec Operand+current dt = C.OptSpec ["current"] [] (C.NoArg f)+  where+    f = E.or [P.date LT (L.toUTC dt), P.date EQ (L.toUTC dt)]++-- | Parses exactly one integer; fails if it cannot read exactly one.+parseInt :: String -> Ex.Exceptional Error Int+parseInt t =+  case reads t of+    ((i, ""):[]) -> return i+    _ -> Ex.throw $ "could not parse integer: \"" <> pack t <> "\"\n"+++-- | 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++  -> (TM.Matcher -> P.LPdct)+  -- ^ When applied to a Matcher, this function returns a predicate.++  -> OptSpec ( CaseSensitive+               -> MatcherFactory+               -> Ex.Exceptional Error Operand )+patternOption str mc f = C.OptSpec [str] so (C.OneArg g)+  where+    so = maybe [] (:[]) mc+    g a1 cs fty = f <$> getMatcher a1 cs fty+++-- | The account option; matches if the pattern given matches the+-- colon-separated account name.+account :: OptSpec ( CaseSensitive+                   -> MatcherFactory+                   -> Ex.Exceptional Error Operand )+account = C.OptSpec ["account"] "a" (C.OneArg f)+  where+    f a1 cs fty+      = fmap P.account+      $ getMatcher a1 cs fty+++-- | The account-level option; matches if the account at the given+-- level matches.+accountLevel :: OptSpec ( CaseSensitive+                        -> MatcherFactory+                        -> Ex.Exceptional Error Operand)+accountLevel = C.OptSpec ["account-level"] "" (C.TwoArg f)+  where+    f a1 a2 cs fty+      = P.accountLevel <$> parseInt a1 <*> getMatcher a2 cs fty+++-- | The accountAny option; returns True if the matcher given matches+-- a single sub-account name at any level.+accountAny :: OptSpec ( CaseSensitive+                        -> MatcherFactory+                        -> Ex.Exceptional Error Operand )+accountAny = patternOption "account-any" Nothing P.accountAny++-- | The payee option; returns True if the matcher matches the payee+-- name.+payee :: OptSpec ( CaseSensitive+                 -> MatcherFactory+                 -> Ex.Exceptional Error Operand )+payee = patternOption "payee" (Just 'p') P.payee++tag :: OptSpec ( CaseSensitive+                 -> MatcherFactory+                 -> Ex.Exceptional Error Operand)+tag = patternOption "tag" (Just 't') P.tag++number :: OptSpec ( CaseSensitive+                    -> MatcherFactory+                    -> Ex.Exceptional Error Operand )+number = patternOption "number" (Just 'n') P.number++flag :: OptSpec ( CaseSensitive+                  -> MatcherFactory+                  -> Ex.Exceptional Error Operand)+flag = patternOption "flag" (Just 'f') P.flag++commodity :: OptSpec ( CaseSensitive+                       -> MatcherFactory+                       -> Ex.Exceptional Error Operand)+commodity = patternOption "commodity" (Just 'y') P.commodity++filename :: OptSpec ( CaseSensitive+                      -> MatcherFactory+                      -> Ex.Exceptional Error Operand )+filename = patternOption "filename" Nothing P.filename++postingMemo :: OptSpec ( CaseSensitive+                         -> MatcherFactory+                         -> Ex.Exceptional Error Operand)+postingMemo = patternOption "posting-memo" Nothing P.postingMemo++transactionMemo :: OptSpec ( CaseSensitive+                             -> MatcherFactory+                             -> Ex.Exceptional Error Operand)+transactionMemo = patternOption "transaction-memo"+                  Nothing P.transactionMemo++debit :: OptSpec Operand+debit = C.OptSpec ["debit"] [] (C.NoArg P.debit)++credit :: OptSpec Operand+credit = C.OptSpec ["credit"] [] (C.NoArg P.credit)++qtyOption :: OptSpec (Ex.Exceptional Error Operand)+qtyOption = C.OptSpec ["qty"] "q" (C.TwoArg f)+  where+    f a1 a2 = do+      qt <- parseQty a2+      parseComparer a1 (flip P.qty qt)+    parseQty a = case parse Pc.quantity "" (pack a) of+      Left e -> Ex.throw $ "could not parse quantity: "+        <> pack a <> " - "+        <> (pack . show $ e)+      Right g -> pure g+++-- | Creates two options suitable for comparison of serial numbers,+-- one for ascending, one for descending.+serialOption ::++  (L.Posting -> Maybe L.Serial)+  -- ^ Function that, when applied to a Posting, returns the serial+  -- you are interested in.++  -> String+  -- ^ Name of the command line option, such as @global-transaction@++  -> ( OptSpec (Ex.Exceptional Error Operand)+     , OptSpec (Ex.Exceptional Error Operand) )+  -- ^ Parses both descending and ascending serial options.++serialOption getSerial n = (osA, osD)+  where+    osA = C.OptSpec [n] []+          (C.TwoArg (f n L.forward))+    osD = let name = addPrefix "rev" n+          in C.OptSpec [name] []+             (C.TwoArg (f name L.backward))+    f name getInt a1 a2 = do+      num <- parseInt a2+      let getPdct = E.compareByMaybe (pack . show $ num) (pack name) cmp+          cmp l = case getSerial l of+            Nothing -> Nothing+            Just ser -> Just $ compare (getInt ser) num+      parseComparer a1 getPdct+++-- | Creates two options suitable for comparison of sibling serial+-- numbers. Similar to serialOption.+siblingSerialOption+  :: String+  -- ^ Name of the command line option, such as @global-posting@++  -> (Int -> Ordering -> E.Pdct L.Posting)+  -- ^ Function that returns a Pdct for forward serial++  -> (Int -> Ordering -> E.Pdct L.Posting)+  -- ^ Function that returns a Pdct for reverse serial++  -> ( OptSpec (Ex.Exceptional Error Operand)+     , OptSpec (Ex.Exceptional Error Operand) )+  -- ^ Parses both descending and ascending serial options.++siblingSerialOption n fFwd fBak = (osA, osD)+  where+    osA = C.OptSpec ["s-" ++ n] [] (C.TwoArg (f fFwd))+    osD = let name = addPrefix "rev" n+          in C.OptSpec ["s-" ++ name] [] (C.TwoArg (f fBak))+    f getPdct a1 a2 = do+      num <- parseInt a2+      parseComparer a1 (getPdct num)+++-- | 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 :: ( OptSpec (Ex.Exceptional Error Operand)+                     , OptSpec (Ex.Exceptional Error Operand) )+globalTransaction =+  let f = fmap L.unGlobalTransaction . Q.globalTransaction+  in serialOption f "globalTransaction"++globalPosting :: ( OptSpec (Ex.Exceptional Error Operand)+                 , OptSpec (Ex.Exceptional Error Operand) )+globalPosting =+  let f = fmap L.unGlobalPosting . Q.globalPosting+  in serialOption f "globalPosting"++filePosting :: ( OptSpec (Ex.Exceptional Error Operand)+               , OptSpec (Ex.Exceptional Error Operand) )+filePosting =+  let f = fmap L.unFilePosting . Q.filePosting+  in serialOption f "filePosting"++fileTransaction :: ( OptSpec (Ex.Exceptional Error Operand)+                   , OptSpec (Ex.Exceptional Error Operand) )+fileTransaction =+  let f = fmap L.unFileTransaction . Q.fileTransaction+  in serialOption f "fileTransaction"++-- | All operand OptSpec.+operandSpecs+  :: L.DateTime+  -> [OptSpec (CaseSensitive+               -> MatcherFactory+               -> Ex.Exceptional Error Operand)]++operandSpecs dt =+  [ fmap (const . const) date+  , fmap (const . const . pure) (current dt)+  , account+  , accountLevel+  , accountAny+  , payee+  , tag+  , number+  , flag+  , commodity+  , postingMemo+  , transactionMemo+  , filename+  , fmap (const . const . pure) debit+  , fmap (const . const . pure) credit+  , fmap (const . const) qtyOption++  , sAccount+  , sAccountLevel+  , sAccountAny+  , sPayee+  , sTag+  , sNumber+  , sFlag+  , sCommodity+  , sPostingMemo+  , fmap (const . const . pure) sDebit+  , fmap (const . const. pure) sCredit+  , fmap (const . const) sQtyOption+  ]+  ++ serialSpecs++serialSpecs :: [OptSpec (CaseSensitive+                        -> MatcherFactory+                        -> Ex.Exceptional Error Operand)]+serialSpecs+  = concat+  $ [unDouble]+  <*> [ globalTransaction, globalPosting,+        filePosting, fileTransaction,+        sGlobalPosting, sFilePosting,+        sGlobalTransaction, sFileTransaction ]++unDouble+  :: Functor f+  => (f (Ex.Exceptional Error a),+      f (Ex.Exceptional Error a ))+  -> [ f (x -> y -> Ex.Exceptional Error a) ]+unDouble (o1, o2) = [fmap (const . const) o1, fmap (const . const) o2]+++------------------------------------------------------------+-- Post filters+------------------------------------------------------------++-- | The user passed a bad number for the head or tail option. The+-- argument is the bad number passed.+data BadHeadTailError = BadHeadTailError Text+  deriving Show++optHead :: OptSpec (Ex.Exceptional Error PostFilterFn)+optHead = C.OptSpec ["head"] [] (C.OneArg f)+  where+    f a = do+      num <- parseInt a+      let g _ ii = ii < (ItemIndex num)+      return g++optTail :: OptSpec (Ex.Exceptional Error PostFilterFn)+optTail = C.OptSpec ["tail"] [] (C.OneArg f)+  where+    f a = do+      num <- parseInt a+      let g (ListLength len) (ItemIndex ii) = ii >= len - num+      return g++postFilterSpecs+  :: ( OptSpec (Ex.Exceptional Error PostFilterFn)+     , OptSpec (Ex.Exceptional Error PostFilterFn))+postFilterSpecs = (optHead, optTail)++------------------------------------------------------------+-- Matcher control+------------------------------------------------------------++parseInsensitive :: OptSpec CaseSensitive+parseInsensitive =+  C.OptSpec ["case-insensitive"] ['i'] (C.NoArg Insensitive)+++parseSensitive :: OptSpec CaseSensitive+parseSensitive =+  C.OptSpec ["case-sensitive"] ['I'] (C.NoArg Sensitive)+++within :: OptSpec MatcherFactory+within =+  C.OptSpec ["within"] "w" . C.NoArg $ \c t ->+    return (TM.within c t)++pcre :: OptSpec MatcherFactory+pcre = C.OptSpec ["pcre"] "r" (C.NoArg TM.pcre)++posix :: OptSpec MatcherFactory+posix = C.OptSpec ["posix"] "" (C.NoArg TM.tdfa)++exact :: OptSpec MatcherFactory+exact = C.OptSpec ["exact"] "x" . C.NoArg $ \c t ->+        return (TM.exact c t)++matcherSelectSpecs :: [OptSpec MatcherFactory]+matcherSelectSpecs = [within, pcre, posix, exact]++caseSelectSpecs :: [OptSpec CaseSensitive]+caseSelectSpecs = [parseInsensitive, parseSensitive]++------------------------------------------------------------+-- Operators+------------------------------------------------------------++-- | Open parentheses+open :: OptSpec (X.Token a)+open = C.OptSpec ["open"] "(" (C.NoArg X.openParen)++-- | Close parentheses+close :: OptSpec (X.Token a)+close = C.OptSpec ["close"] ")" (C.NoArg X.closeParen)++-- | and operator+parseAnd :: OptSpec (X.Token a)+parseAnd = C.OptSpec ["and"] "A" (C.NoArg X.opAnd)++-- | or operator+parseOr :: OptSpec (X.Token a)+parseOr = C.OptSpec ["or"] "O" (C.NoArg X.opOr)++-- | not operator+parseNot :: OptSpec (X.Token a)+parseNot = C.OptSpec ["not"] "N" (C.NoArg X.opNot)++operatorSpecs :: [OptSpec (X.Token a)]+operatorSpecs =+  [open, close, parseAnd, parseOr, parseNot]++-- Infix and RPN expression selectors++parseInfix :: OptSpec X.ExprDesc+parseInfix = C.OptSpec ["infix"] "" (C.NoArg X.Infix)++parseRPN :: OptSpec X.ExprDesc+parseRPN = C.OptSpec ["rpn"] "" (C.NoArg X.RPN)++-- | Both Infix and RPN options.+exprDesc :: [OptSpec X.ExprDesc]+exprDesc = [ parseInfix, parseRPN ]++showExpression :: OptSpec ()+showExpression = C.OptSpec ["show-expression"] "" (C.NoArg ())++verboseFilter :: OptSpec ()+verboseFilter = C.OptSpec ["verbose-filter"] "" (C.NoArg ())++--+-- Siblings+--++sGlobalPosting :: ( OptSpec (Ex.Exceptional Error Operand)+                  , OptSpec (Ex.Exceptional Error Operand) )+sGlobalPosting =+  siblingSerialOption "globalPosting"+                      PS.fwdGlobalPosting PS.backGlobalPosting++sFilePosting :: ( OptSpec (Ex.Exceptional Error Operand)+                  , OptSpec (Ex.Exceptional Error Operand) )+sFilePosting =+  siblingSerialOption "filePosting"+                      PS.fwdFilePosting PS.backFilePosting++sGlobalTransaction :: ( OptSpec (Ex.Exceptional Error Operand)+                  , OptSpec (Ex.Exceptional Error Operand) )+sGlobalTransaction =+  siblingSerialOption "globalTransaction"+                      PS.fwdGlobalTransaction PS.backGlobalTransaction++sFileTransaction :: ( OptSpec (Ex.Exceptional Error Operand)+                  , OptSpec (Ex.Exceptional Error Operand) )+sFileTransaction =+  siblingSerialOption "filePosting"+                      PS.fwdFileTransaction PS.backFileTransaction+++sAccount :: OptSpec ( CaseSensitive+                    -> MatcherFactory+                    -> Ex.Exceptional Error Operand )+sAccount = C.OptSpec ["s-account"] "" (C.OneArg f)+  where+    f a1 cs fty = fmap PS.account+                  $ getMatcher a1 cs fty++sAccountLevel :: OptSpec ( CaseSensitive+                         -> MatcherFactory+                         -> Ex.Exceptional Error Operand )+sAccountLevel = C.OptSpec ["s-account-level"] "" (C.TwoArg f)+  where+    f a1 a2 cs fty+      = PS.accountLevel <$> parseInt a1 <*> getMatcher a2 cs fty++sAccountAny :: OptSpec ( CaseSensitive+                        -> MatcherFactory+                        -> Ex.Exceptional Error Operand )+sAccountAny = patternOption "s-account-any" Nothing PS.accountAny++-- | The payee option; returns True if the matcher matches the payee+-- name.+sPayee :: OptSpec ( CaseSensitive+                 -> MatcherFactory+                 -> Ex.Exceptional Error Operand )+sPayee = patternOption "s-payee" (Just 'p') PS.payee++sTag :: OptSpec ( CaseSensitive+                 -> MatcherFactory+                 -> Ex.Exceptional Error Operand)+sTag = patternOption "s-tag" (Just 't') PS.tag++sNumber :: OptSpec ( CaseSensitive+                    -> MatcherFactory+                    -> Ex.Exceptional Error Operand )+sNumber = patternOption "s-number" Nothing PS.number++sFlag :: OptSpec ( CaseSensitive+                  -> MatcherFactory+                  -> Ex.Exceptional Error Operand)+sFlag = patternOption "s-flag" Nothing PS.flag++sCommodity :: OptSpec ( CaseSensitive+                       -> MatcherFactory+                       -> Ex.Exceptional Error Operand)+sCommodity = patternOption "s-commodity" Nothing PS.commodity++sPostingMemo :: OptSpec ( CaseSensitive+                         -> MatcherFactory+                         -> Ex.Exceptional Error Operand)+sPostingMemo = patternOption "s-posting-memo" Nothing PS.postingMemo++sDebit :: OptSpec Operand+sDebit = C.OptSpec ["s-debit"] [] (C.NoArg PS.debit)++sCredit :: OptSpec Operand+sCredit = C.OptSpec ["s-credit"] [] (C.NoArg PS.credit)++sQtyOption :: OptSpec (Ex.Exceptional Error Operand)+sQtyOption = C.OptSpec ["s-qty"] [] (C.TwoArg f)+  where+    f a1 a2 = do+      qt <- parseQty a2+      parseComparer a1 (flip PS.qty qt)+    parseQty a = case parse Pc.quantity "" (pack a) of+      Left e -> Ex.throw $ "could not parse quantity: "+        <> pack a <> " - "+        <> (pack . show $ e)+      Right g -> pure g++--+-- Versions+--++-- | Parses the @--version@ option and returns an IO action that+-- prints it and exits successfully. You supply the version of the+-- executable, as there is no easy way to get that automatically.++version+  :: V.Version+  -- ^ Version of binary+  -> OptSpec (IO a)+version v = C.OptSpec ["version"] [] (C.NoArg f)+  where+    f = do+      pn <- MA.getProgName+      putStrLn $ pn ++ " version " ++ V.showVersion v+#ifdef incabal+      putStrLn $ "using version " ++ V.showVersion PPL.version+#else+      putStrLn $ "using testing version"+#endif+                 ++ " of penny-lib"+      Exit.exitSuccess++-- | An option for where the user would like to send output.+output :: MA.OptSpec (X.Text -> IO ())+output = MA.OptSpec ["output"] "o" . MA.OneArg $ \s ->+  if s == "-"+    then TIO.putStr+    else TIO.writeFile s+++-- | Given a list of output options, returns a single IO action to+-- write to all given files. If the list was empty, returns an IO+-- action that writes to standard output.++processOutput :: [X.Text -> IO ()] -> X.Text -> IO ()+processOutput ls x =+  if null ls+  then TIO.putStr x+  else sequence_ . map ($ x) $ ls+
+ lib/Penny/Lincoln.hs view
@@ -0,0 +1,72 @@+-- | 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+  ( module Penny.Lincoln.Balance+  , module Penny.Lincoln.Bits+  , module Penny.Lincoln.Builders+  , module Penny.Lincoln.Ents+  , module Penny.Lincoln.Equivalent+  , module Penny.Lincoln.HasText+  , module Penny.Lincoln.Matchers+  , module Penny.Lincoln.PriceDb+  , module Penny.Lincoln.Serial+  , display+  ) where++import Penny.Lincoln.Bits+import Penny.Lincoln.Ents+import Penny.Lincoln.Balance+import Penny.Lincoln.Builders+import Penny.Lincoln.Equivalent+import Penny.Lincoln.HasText+import Penny.Lincoln.Matchers+import Penny.Lincoln.PriceDb+import Penny.Lincoln.Serial++import Data.List (intersperse)+import Data.Text (Text)+import qualified Data.Text as X+import qualified Penny.Lincoln.Queries as Q+import qualified Data.Time as Time+import System.Locale (defaultTimeLocale)++--+-- Display+--++-- | Displays a PostFam in a one line format.+--+-- Format:+--+-- File LineNo Date Payee Acct DrCr Cmdty Qty+display :: Posting -> Text+display p = X.pack $ concat (intersperse " " ls)+  where+    ls = [file, lineNo, dt, pye, acct, dc, cmdty, qt]+    file = maybe (labelNo "filename") (X.unpack . unFilename)+           (fmap tFilename . tlFileMeta . fst . unPosting $ p)+    lineNo = maybe (labelNo "line number")+             (show . unPostingLine)+             (Q.postingLine p)+    dateFormat = "%Y-%m-%d %T %z"+    dt = Time.formatTime defaultTimeLocale dateFormat+         . Time.utctDay+         . toUTC+         . Q.dateTime+         $ p+    pye = maybe (labelNo "payee")+            (X.unpack . text) (Q.payee p)+    acct = X.unpack . X.intercalate (X.singleton ':')+           . map unSubAccount . unAccount . Q.account $ p+    dc = case Q.drCr p of+      Debit -> "Dr"+      Credit -> "Cr"+    cmdty = X.unpack . unCommodity . Q.commodity $ p+    qt = show . Q.qty $ p+    labelNo s = "(no " ++ s ++ ")"
+ lib/Penny/Lincoln/Balance.hs view
@@ -0,0 +1,109 @@+module Penny.Lincoln.Balance (+    Balance+  , unBalance+  , Balanced(Balanced, Inferable, NotInferable)+  , isBalanced+  , entryToBalance+  , addBalances+  , removeZeroCommodities+  , BottomLine(Zero, NonZero)+  , Column(Column, colDrCr, colQty)+  ) 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'.+newtype Balance = Balance (Map B.Commodity BottomLine)+                  deriving (Show, Eq)++-- | Returns a map where the keys are the commodities in the balance+-- and the values are the balance for each commodity. If there is no+-- balance at all, this map can be 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 colDrCr col of+          B.Debit -> B.Credit+          B.Credit -> B.Debit+        q = colQty 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 { colDrCr :: B.DrCr+                     , colQty :: 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++instance Monoid Balance where+  mempty = Balance M.empty+  mappend = addBalances++-- | Removes zero balances from a Balance.+removeZeroCommodities :: Balance -> Balance+removeZeroCommodities (Balance m) =+  let p b = case b of+        Zero -> False+        _ -> True+      m' = M.filter p m+  in Balance m'
+ lib/Penny/Lincoln/Bits.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE DeriveGeneric #-}++-- | Essential data types used to make Transactions and Postings.+module Penny.Lincoln.Bits+  ( module Penny.Lincoln.Bits.Open+  , module Penny.Lincoln.Bits.DateTime+  , module Penny.Lincoln.Bits.Price+  , module Penny.Lincoln.Bits.Qty+  , PricePoint ( .. )++  -- * Aggregates+  , TopLineCore(..)+  , emptyTopLineCore+  , TopLineFileMeta(..)+  , TopLineData(..)+  , emptyTopLineData+  , PostingCore(..)+  , emptyPostingCore+  , PostingFileMeta(..)+  , PostingData(..)+  , emptyPostingData+  ) where+++import Data.Monoid (mconcat)+import Penny.Lincoln.Bits.Open+import Penny.Lincoln.Bits.DateTime+import Penny.Lincoln.Bits.Qty+import Penny.Lincoln.Bits.Price++import qualified Penny.Lincoln.Bits.Open as O+import qualified Penny.Lincoln.Bits.DateTime as DT+import qualified Penny.Lincoln.Bits.Price as Pr+import qualified Penny.Lincoln.Equivalent as Ev+import Penny.Lincoln.Equivalent ((==~))+import qualified Data.Binary as B+import GHC.Generics (Generic)++data PricePoint = PricePoint { dateTime :: DT.DateTime+                             , price :: Pr.Price+                             , ppSide :: Maybe O.Side+                             , ppSpaceBetween :: Maybe O.SpaceBetween+                             , priceLine :: Maybe O.PriceLine }+                  deriving (Eq, Show, Generic)++instance B.Binary PricePoint++-- | PricePoint are equivalent if the dateTime and the Price are+-- equivalent. Other elements of the PricePoint are ignored.+instance Ev.Equivalent PricePoint where+  equivalent (PricePoint dx px _ _ _) (PricePoint dy py _ _ _) =+    dx ==~ dy && px ==~ py+  compareEv (PricePoint dx px _ _ _) (PricePoint dy py _ _ _) =+    mconcat [ Ev.compareEv dx dy+            , Ev.compareEv px py ]++-- | All the data that a TopLine might have.+data TopLineData = TopLineData+  { tlCore :: TopLineCore+  , tlFileMeta :: Maybe TopLineFileMeta+  , tlGlobal :: Maybe O.GlobalTransaction+  } deriving (Eq, Show, Generic)++emptyTopLineData :: DT.DateTime -> TopLineData+emptyTopLineData dt = TopLineData (emptyTopLineCore dt) Nothing Nothing++instance B.Binary TopLineData++-- | Every TopLine has this data.+data TopLineCore = TopLineCore+  { tDateTime :: DT.DateTime+  , tNumber :: Maybe O.Number+  , tFlag :: Maybe O.Flag+  , tPayee :: Maybe O.Payee+  , tMemo :: Maybe O.Memo+  } deriving (Eq, Show, Generic)++-- | TopLineCore are equivalent if their dates are equivalent and if+-- everything else is equal.+instance Ev.Equivalent TopLineCore where+  equivalent x y =+    tDateTime x ==~ tDateTime y+    && tNumber x == tNumber y+    && tFlag x == tFlag y+    && tPayee x == tPayee y+    && tMemo x == tMemo y++  compareEv x y = mconcat+    [ Ev.compareEv (tDateTime x) (tDateTime y)+    , compare (tNumber x) (tNumber y)+    , compare (tFlag x) (tFlag y)+    , compare (tPayee x) (tPayee y)+    , compare (tMemo x) (tMemo y)+    ]++emptyTopLineCore :: DT.DateTime -> TopLineCore+emptyTopLineCore dt = TopLineCore dt Nothing Nothing Nothing Nothing++instance B.Binary TopLineCore++-- | TopLines from files have this metadata.+data TopLineFileMeta = TopLineFileMeta+  { tFilename :: O.Filename+  , tTopLineLine :: O.TopLineLine+  , tTopMemoLine :: Maybe O.TopMemoLine+  , tFileTransaction :: O.FileTransaction+  } deriving (Eq, Show, Generic)++instance B.Binary TopLineFileMeta+++-- | All Postings have this data.+data PostingCore = PostingCore+  { pPayee :: Maybe O.Payee+  , pNumber :: Maybe O.Number+  , pFlag :: Maybe O.Flag+  , pAccount :: O.Account+  , pTags :: O.Tags+  , pMemo :: Maybe O.Memo+  , pSide :: Maybe O.Side+  , pSpaceBetween :: Maybe O.SpaceBetween+  } deriving (Eq, Show, Generic)++-- | Two PostingCore are equivalent if the Tags are equivalent and the+-- other data is equal, exlucing the Side and the SpaceBetween, which are not considered at all.+instance Ev.Equivalent PostingCore where+  equivalent (PostingCore p1 n1 f1 a1 t1 m1 _ _)+             (PostingCore p2 n2 f2 a2 t2 m2 _ _)+    = p1 == p2 && n1 == n2 && f1 == f2+    && a1 == a2 && t1 ==~ t2 && m1 == m2++  compareEv (PostingCore p1 n1 f1 a1 t1 m1 _ _)+            (PostingCore p2 n2 f2 a2 t2 m2 _ _)+    = mconcat+        [ compare p1 p2+        , compare n1 n2+        , compare f1 f2+        , compare a1 a2+        , Ev.compareEv t1 t2+        , compare m1 m2+        ]++emptyPostingCore :: O.Account -> PostingCore+emptyPostingCore ac = PostingCore+  { pPayee = Nothing+  , pNumber = Nothing+  , pFlag = Nothing+  , pAccount = ac+  , pTags = O.Tags []+  , pMemo = Nothing+  , pSide = Nothing+  , pSpaceBetween = Nothing+  }++instance B.Binary PostingCore++-- | Postings from files have this additional data.+data PostingFileMeta = PostingFileMeta+  { pPostingLine :: O.PostingLine+  , pFilePosting :: O.FilePosting+  } deriving (Eq, Show, Generic)++instance B.Binary PostingFileMeta++-- | All the data that a Posting might have.+data PostingData = PostingData+  { pdCore :: PostingCore+  , pdFileMeta :: Maybe PostingFileMeta+  , pdGlobal :: Maybe O.GlobalPosting+  } deriving (Eq, Show, Generic)++emptyPostingData :: O.Account -> PostingData+emptyPostingData ac = PostingData+  { pdCore = emptyPostingCore ac+  , pdFileMeta = Nothing+  , pdGlobal = Nothing+  }++instance B.Binary PostingData+
+ lib/Penny/Lincoln/Bits/DateTime.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE DeriveGeneric #-}++module Penny.Lincoln.Bits.DateTime+  ( TimeZoneOffset ( offsetToMins )+  , minsToOffset+  , noOffset+  , Hours ( unHours )+  , intToHours+  , zeroHours+  , Minutes ( unMinutes )+  , intToMinutes+  , zeroMinutes+  , Seconds ( unSeconds )+  , intToSeconds+  , zeroSeconds+  , midnight+  , DateTime ( .. )+  , dateTimeMidnightUTC+  , toUTC+  , toZonedTime+  , fromZonedTime+  , sameInstant+  , showDateTime+  ) where++import qualified Control.Monad as M+import qualified Data.Time as T+import qualified Data.Binary as B+import Data.Binary (get, put)+import GHC.Generics (Generic)+import qualified Penny.Lincoln.Equivalent as Ev++-- | 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, Generic)++instance B.Binary TimeZoneOffset++-- | 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++newtype Hours = Hours { unHours :: Int }+                deriving (Eq, Ord, Show, Generic)++instance B.Binary Hours++newtype Minutes = Minutes { unMinutes :: Int }+                  deriving (Eq, Ord, Show, Generic)++instance B.Binary Minutes++newtype Seconds = Seconds { unSeconds :: Int }+                  deriving (Eq, Ord, Show, Generic)++instance B.Binary Seconds++-- | succeeds if 0 <= x < 24+intToHours :: Int -> Maybe Hours+intToHours h =+  if h >= 0 && h < 24 then Just . Hours $ h else Nothing++zeroHours :: Hours+zeroHours = Hours 0++-- | succeeds if 0 <= x < 60+intToMinutes :: Int -> Maybe Minutes+intToMinutes m =+  if m >= 0 && m < 60 then Just . Minutes $ m else Nothing++zeroMinutes :: Minutes+zeroMinutes = Minutes 0++-- | succeeds if 0 <= x < 61 (to allow for leap seconds)+intToSeconds :: Int -> Maybe Seconds+intToSeconds s =+  if s >= 0 && s < 61+  then Just . Seconds $ s+  else Nothing++zeroSeconds :: Seconds+zeroSeconds = Seconds 0++midnight :: (Hours, Minutes, Seconds)+midnight = (zeroHours, zeroMinutes, zeroSeconds)++-- | A DateTime is a a local date and time, along with a time zone+-- offset.  The Eq and Ord instances are derived; therefore, two+-- DateTime instances will not be equivalent if the time zone offsets+-- are different, even if they are the same instant. To compare one+-- DateTime to another, you probably want to use 'toUTC' and compare+-- those. To see if two DateTime are the same instant, use+-- 'sameInstant'.+data DateTime = DateTime+  { day :: T.Day+  , hours :: Hours+  , minutes :: Minutes+  , seconds :: Seconds+  , timeZone :: TimeZoneOffset+  } deriving (Eq, Ord, Show)++instance B.Binary DateTime where+  get = M.liftM5 DateTime (fmap T.ModifiedJulianDay B.get)+                 get get get get+  put (DateTime d h m s t) =+    put (T.toModifiedJulianDay d) >> put h >> put m >> put s >> put t++dateTimeMidnightUTC :: T.Day -> DateTime+dateTimeMidnightUTC d = DateTime d h m s z+  where+    (h, m, s) = midnight+    z = noOffset++toZonedTime :: DateTime -> T.ZonedTime+toZonedTime dt = T.ZonedTime lt tz+  where+    d = day dt+    lt = T.LocalTime d tod+    tod = T.TimeOfDay (unHours . hours $ dt) (unMinutes . minutes $ dt)+          (fromIntegral . unSeconds . seconds $ dt)+    tz = T.TimeZone (offsetToMins . timeZone $ dt) False ""++fromZonedTime :: T.ZonedTime -> Maybe DateTime+fromZonedTime (T.ZonedTime (T.LocalTime d tod) tz) = do+  h <- intToHours . T.todHour $ tod+  m <- intToMinutes . T.todMin $ tod+  let (sWhole, _) = properFraction . T.todSec $ tod+  s <- intToSeconds sWhole+  tzo <- minsToOffset . T.timeZoneMinutes $ tz+  return $ DateTime d h m s tzo++toUTC :: DateTime -> T.UTCTime+toUTC dt = T.localTimeToUTC tz lt+  where+    tz = T.minutesToTimeZone . offsetToMins . timeZone $ dt+    tod = T.TimeOfDay (unHours h) (unMinutes m)+          (fromIntegral . unSeconds $ s)+    DateTime d h m s _ = dt+    lt = T.LocalTime d tod++-- | Are these DateTimes the same instant in time, after adjusting for+-- local timezones?++sameInstant :: DateTime -> DateTime -> Bool+sameInstant t1 t2 = toUTC t1 == toUTC t2++instance Ev.Equivalent DateTime where+  equivalent = sameInstant+  compareEv x y = compare (toUTC x) (toUTC y)++-- | Shows a DateTime in a pretty way.+showDateTime :: DateTime -> String+showDateTime (DateTime d h m s tz) =+  ds ++ " " ++ hmss ++ " " ++ showOffset+  where+    ds = show d+    hmss = hs ++ ":" ++ ms ++ ":" ++ ss+    hs = pad0 . show . unHours $ h+    ms = pad0 . show . unMinutes $ m+    ss = pad0 . show . unSeconds $ s+    pad0 str = if length str < 2 then '0':str else str+    showOffset =+      let (zoneHr, zoneMin) = abs (offsetToMins tz) `divMod` 60+          sign = if offsetToMins tz < 0 then "-" else "+"+      in sign ++ pad0 (show zoneHr) ++ pad0 (show zoneMin)+
+ lib/Penny/Lincoln/Bits/Open.hs view
@@ -0,0 +1,214 @@+{-# LANGUAGE DeriveGeneric #-}++-- | These are the bits that are "open"; that is, their constructors+-- are exported. This includes most bits. Some bits that have open+-- constructors are not in this module because they include other bits+-- that do not have exported constructors.++module Penny.Lincoln.Bits.Open where++import Data.List (sort)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as X+import qualified Data.Text.Encoding as XE+import GHC.Generics (Generic)+import qualified Penny.Lincoln.Equivalent as Ev+import Penny.Lincoln.Equivalent ((==~))+import qualified Penny.Lincoln.Serial as S+import qualified Penny.Lincoln.Bits.Qty as Q+import qualified Data.Binary as B++newtype SubAccount =+  SubAccount { unSubAccount :: Text }+  deriving (Eq, Ord, Show)++instance B.Binary SubAccount where+  put = B.put . XE.encodeUtf8 . unSubAccount+  get = fmap (SubAccount . XE.decodeUtf8) B.get++newtype Account = Account { unAccount :: [SubAccount] }+                  deriving (Eq, Show, Ord, Generic)++instance B.Binary Account++data Amount = Amount { qty :: Q.Qty+                     , commodity :: Commodity }+              deriving (Eq, Show, Ord, Generic)++instance B.Binary Amount++instance Ev.Equivalent Amount where+  equivalent (Amount q1 c1) (Amount q2 c2) =+    q1 ==~ q2 && c1 == c2+  compareEv (Amount q1 c1) (Amount q2 c2) =+    Ev.compareEv q1 q2 <> c1 `compare` c2++newtype Commodity =+  Commodity { unCommodity :: Text }+  deriving (Eq, Ord, Show)++instance B.Binary Commodity where+  get = fmap (Commodity . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unCommodity++data DrCr = Debit | Credit deriving (Eq, Show, Ord, Generic)++instance B.Binary DrCr++-- | Debit returns Credit; Credit returns Debit+opposite :: DrCr -> DrCr+opposite d = case d of+  Debit -> Credit+  Credit -> Debit++data Entry = Entry { drCr :: DrCr+                   , amount :: Amount }+             deriving (Eq, Show, Ord, Generic)++instance B.Binary Entry++instance Ev.Equivalent Entry where+  equivalent (Entry d1 a1) (Entry d2 a2) =+    d1 == d2 && a1 ==~ a2+  compareEv (Entry d1 a1) (Entry d2 a2) =+    d1 `compare` d2 <> Ev.compareEv a1 a2++newtype Flag = Flag { unFlag :: Text }+             deriving (Eq, Show, Ord)++instance B.Binary Flag where+  get = fmap (Flag . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unFlag++-- | There is one item in the list for each line of the memo. Do not+-- include newlines in the texts themselves. However there is nothing+-- to enforce this convention.+newtype Memo = Memo { unMemo :: [Text] }+             deriving (Eq, Show, Ord)++instance B.Binary Memo where+  get = fmap (Memo . map XE.decodeUtf8) B.get+  put = B.put . map XE.encodeUtf8 . unMemo++newtype Number = Number { unNumber :: Text }+                 deriving (Eq, Show, Ord)++instance B.Binary Number where+  get = fmap (Number . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unNumber+++newtype Payee = Payee { unPayee :: Text }+              deriving (Eq, Show, Ord)++instance B.Binary Payee where+  get = fmap (Payee . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unPayee++newtype Tag = Tag { unTag :: Text }+                  deriving (Eq, Show, Ord)++instance B.Binary Tag where+  get = fmap (Tag . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unTag++newtype Tags = Tags { unTags :: [Tag] }+               deriving (Eq, Show, Ord, Generic)++-- | Tags are equivalent if they have the same tags (even if in a+-- different order).+instance Ev.Equivalent Tags where+  equivalent (Tags t1) (Tags t2) = sort t1 == sort t2+  compareEv (Tags t1) (Tags t2) =+    compare (sort t1) (sort t2)++instance B.Binary Tags++-- Metadata++-- | The line number that the TopLine starts on (excluding the memo+-- accompanying the TopLine).+newtype TopLineLine = TopLineLine { unTopLineLine :: Int }+                      deriving (Eq, Show, Generic)++instance B.Binary TopLineLine++-- | The line number that the memo accompanying the TopLine starts on.+newtype TopMemoLine = TopMemoLine { unTopMemoLine :: Int }+                      deriving (Eq, Show, Generic)++instance B.Binary TopMemoLine++-- | The commodity and and the quantity may appear with the commodity+-- on the left (e.g. USD 2.14) or with the commodity on the right+-- (e.g. 2.14 USD).+data Side+  = CommodityOnLeft+  | CommodityOnRight+  deriving (Eq, Show, Ord, Generic)++instance B.Binary Side++-- | There may or may not be a space in between the commodity and the+-- quantity.+data SpaceBetween+  = SpaceBetween+  | NoSpaceBetween+  deriving (Eq, Show, Ord, Generic)++instance B.Binary SpaceBetween++-- | The name of the file in which a transaction appears.+newtype Filename = Filename { unFilename :: X.Text }+                   deriving (Eq, Show)++instance B.Binary Filename where+  get = fmap (Filename . XE.decodeUtf8) B.get+  put = B.put . XE.encodeUtf8 . unFilename+++-- | The line number on which a price appears.+newtype PriceLine = PriceLine { unPriceLine :: Int }+                    deriving (Eq, Show, Generic)++instance B.Binary PriceLine++-- | The line number on which a posting appears.+newtype PostingLine = PostingLine { unPostingLine :: Int }+                      deriving (Eq, Show, Generic)++instance B.Binary PostingLine++-- | All postings are numbered in order, beginning with the first+-- posting in the first file and ending with the last posting+-- in the last file.+newtype GlobalPosting =+  GlobalPosting { unGlobalPosting :: S.Serial }+  deriving (Eq, Show, Generic)++instance B.Binary GlobalPosting++-- | The postings in each file are numbered in order.+newtype FilePosting =+  FilePosting { unFilePosting :: S.Serial }+  deriving (Eq, Show, Generic)++instance B.Binary FilePosting++-- | All transactions are numbered in order, beginning with the first+-- transaction in the first file and ending with the last transaction+-- in the last file.+newtype GlobalTransaction =+  GlobalTransaction { unGlobalTransaction :: S.Serial }+  deriving (Eq, Show, Generic)++instance B.Binary GlobalTransaction++-- | The transactions in each file are numbered in order.+newtype FileTransaction =+  FileTransaction { unFileTransaction :: S.Serial }+  deriving (Eq, Show, Generic)++instance B.Binary FileTransaction+
+ lib/Penny/Lincoln/Bits/Price.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-}++module Penny.Lincoln.Bits.Price (+    From ( From, unFrom )+  , To ( To, unTo )+  , CountPerUnit ( CountPerUnit, unCountPerUnit )+  , Price ( from, to, countPerUnit )+  , newPrice+  ) where++import Data.Monoid (mconcat)+import qualified Penny.Lincoln.Equivalent as Ev+import Penny.Lincoln.Equivalent ((==~))+import qualified Penny.Lincoln.Bits.Open as O+import Penny.Lincoln.Bits.Qty (Qty)+import GHC.Generics (Generic)+import qualified Data.Binary as B++newtype From = From { unFrom :: O.Commodity }+  deriving (Eq, Ord, Show, Generic)++instance B.Binary From++newtype To = To { unTo :: O.Commodity }+  deriving (Eq, Ord, Show, Generic)++instance B.Binary To++newtype CountPerUnit = CountPerUnit { unCountPerUnit :: Qty }+  deriving (Eq, Ord, Show, Generic)++instance Ev.Equivalent CountPerUnit where+  equivalent (CountPerUnit x) (CountPerUnit y) = x ==~ y+  compareEv (CountPerUnit x) (CountPerUnit y) = Ev.compareEv x y++instance B.Binary CountPerUnit++data Price = Price { from :: From+                   , to :: To+                   , countPerUnit :: CountPerUnit }+             deriving (Eq, Ord, Show, Generic)++instance B.Binary Price++-- | Two Price are equivalent if the From and To are equal and the+-- CountPerUnit is equivalent.++instance Ev.Equivalent Price where+  equivalent (Price xf xt xc) (Price yf yt yc) =+    xf == yf && xt == yt && xc ==~ yc++  compareEv (Price xf xt xc) (Price yf yt yc) = mconcat+    [ compare xf yf+    , compare xt yt+    , Ev.compareEv xc yc+    ]++-- | 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
+ lib/Penny/Lincoln/Bits/Qty.hs view
@@ -0,0 +1,360 @@+{-# LANGUAGE DeriveGeneric #-}+-- | 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+  , NumberStr(..)+  , toQty+  , mantissa+  , places+  , prettyShowQty+  , compareQty+  , newQty+  , Mantissa+  , Places+  , add+  , mult+  , Difference(LeftBiggerBy, RightBiggerBy, Equal)+  , difference+  , allocate+  , TotSeats+  , PartyVotes+  , SeatsWon+  , largestRemainderMethod+  , qtyOne+  ) where++import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Binary as B+import GHC.Generics (Generic)+import Data.List (genericLength, genericReplicate, genericSplitAt, sortBy)+import Data.Ord (comparing)+import qualified Penny.Lincoln.Equivalent as Ev++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+++-- | Converts strings to Qty. Fails if any of the strings have+-- non-digits, or if any are negative, or if the result is not greater+-- than zero, or if the strings are empty.+toQty :: NumberStr -> Maybe Qty+toQty ns = case ns of+  Whole s -> fmap (\m -> Qty m 0) (readInteger s)+  WholeRad s -> fmap (\m -> Qty m 0) (readInteger s)+  WholeRadFrac w f -> fromWholeRadFrac w f+  RadFrac f -> fromWholeRadFrac "0" f+  where+    fromWholeRadFrac w f =+      fmap (\m -> Qty m (genericLength f)) (readInteger (w ++ f))++-- | Reads non-negative integers only.+readInteger :: String -> Maybe Integer+readInteger s = case reads s of+  (i, ""):[] -> if i < 0 then Nothing else Just i+  _ -> Nothing++-- | 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.+--+-- /WARNING/ - before doing comparisons or equality tests+--+-- The Eq instance is derived. Therefore q1 == q2 only if q1 and q2+-- have both the same mantissa and the same exponent. You may instead+-- want 'equivalent'. Similarly, the Ord instance is derived. It+-- compares based on the integral value of the mantissa and of the+-- exponent. You may instead want 'compareQty', which compares after+-- equalizing the exponents.+data Qty = Qty { mantissa :: !Integer+               , places :: !Integer+               } deriving (Eq, Generic, Show, Ord)++-- | Shows a quantity, nicely formatted after accounting for both the+-- mantissa and decimal places, e.g. @0.232@ or @232.12@ or whatever.+prettyShowQty :: Qty -> String+prettyShowQty q =+  let man = show . mantissa $ q+      e = places q+      len = genericLength man+      small = "0." ++ ((genericReplicate (e - len) '0') ++ man)+  in case compare e len of+      GT -> small+      EQ -> small+      LT ->+        let (b, end) = genericSplitAt (len - e) man+        in if e == 0+           then man+           else b ++ ['.'] ++ end++instance Ev.Equivalent Qty where+  equivalent x y = x' == y'+    where+      (x', y') = equalizeExponents x y+  compareEv x y = compare x' y'+    where+      (x', y') = equalizeExponents x y++instance B.Binary Qty++type Mantissa = Integer+type Places = Integer++-- | Mantissa 1, exponent 0+qtyOne :: Qty+qtyOne = Qty 1 0+++++newQty :: Mantissa -> Places -> Maybe Qty+newQty m p+  | m > 0  && p >= 0 = Just $ Qty m p+  | otherwise = Nothing++++-- | Compares Qty after equalizing their exponents.+--+-- > compareQty (newQty 15 1) (newQty 1500 3) == EQ+compareQty :: Qty -> Qty -> Ordering+compareQty q1 q2 = compare (mantissa q1') (mantissa q2')+  where+    (q1', q2') = equalizeExponents q1 q2+++-- | Adjust the exponents on two Qty so they are equivalent+-- before, but now have the same exponent.+equalizeExponents :: Qty -> Qty -> (Qty, Qty)+equalizeExponents x y = (x', y')+  where+    (ex, ey) = (places x, places y)+    (x', y') = case compare ex ey of+      GT -> (x, increaseExponent (ex - ey) y)+      LT -> (increaseExponent (ey - ex) x, y)+      EQ -> (x, y)++-- | Increase the exponent by the amount given, so that the new Qty is+-- equivalent to the old one. Takes the absolute value of the+-- adjustment argument.+increaseExponent :: Integer -> Qty -> Qty+increaseExponent i (Qty m e) = Qty m' e'+  where+    amt = abs i+    m' = m * 10 ^ amt+    e' = e + amt++-- | Increases the exponent to the given amount. Does nothing if the+-- exponent is already at or higher than this amount.+increaseExponentTo :: Integer -> Qty -> Qty+increaseExponentTo i q@(Qty _ e) =+  let diff = i - e+  in if diff >= 0 then increaseExponent diff q else q++data Difference =+  LeftBiggerBy Qty+  | RightBiggerBy Qty+  | Equal+  deriving (Eq, Show)++-- | Subtract the second Qty from the first, after equalizing their+-- exponents.+difference :: Qty -> Qty -> Difference+difference x y =+  let (x', y') = equalizeExponents x y+      (mx, my) = (mantissa x', mantissa y')+  in case compare mx my of+    GT -> LeftBiggerBy (Qty (mx - my) (places x'))+    LT -> RightBiggerBy (Qty (my - mx) (places x'))+    EQ -> Equal++add :: Qty -> Qty -> Qty+add x y =+  let ((Qty xm e), (Qty ym _)) = equalizeExponents x y+  in Qty (xm + ym) e++++mult :: Qty -> Qty -> Qty+mult (Qty xm xe) (Qty ym ye) = Qty (xm * ym) (xe + ye)+++--+-- Allocation+--+-- The steps of allocation:+--+-- Adjust all exponents, both on the amount to be allocated and on all+-- the votes, so that the exponents are all equal.+--+-- Allocate the mantissas.+--+-- Return the quantities with the original exponents.++-- | Allocate a Qty proportionally so that the sum of the results adds+-- up to a given Qty. Fails if the allocation cannot be made (e.g. if+-- it is impossible to allocate without overflowing Decimal.) The+-- result will always add up to the given sum.+allocate :: Qty -> (Qty, [Qty]) -> (Qty, [Qty])+allocate tot (q1, qs) = case allocate' tot (q1:qs) of+  [] -> error "allocate error"+  x:xs -> (x, xs)++allocate'+  :: Qty+  -- ^ The result will add up to this Qty.++  -> [Qty]+  -- ^ Allocate using these Qty (there must be at least one).++  -> [Qty]+  -- ^ The length of this list will be equal to the length of the list+  -- of allocations. Each item will correspond to the original+  -- allocation.++allocate' tot ls =+  let ((tot':ls'), e) = sameExponent (tot:ls)+      (moreE, (_, ss)) =+        multRemainderAllResultsAtLeast1 (mantissa tot')+        (map mantissa ls')+      totE = e + moreE+  in map (\m -> Qty m totE) ss++++-- | Given a list of Decimals, and a single Decimal, return Decimals+-- that are equivalent to the original Decimals, but where all+-- Decimals have the same exponent. Also returns new exponent.+sameExponent+  :: [Qty]+  -> ([Qty], Integer)+sameExponent ls =+  let newExp = maximum . fmap places $ ls+  in (map (increaseExponentTo newExp) ls, newExp)++++++type Multiplier = Integer++multLargestRemainder+  :: TotSeats+  -> [PartyVotes]+  -> Multiplier+  -> (TotSeats, [SeatsWon])+multLargestRemainder ts pv m =+  let ts' = ts * 10 ^ m+      pv' = map (\x -> x * 10 ^ m) pv+  in (ts', largestRemainderMethod ts' pv')++increasingMultRemainder+  :: TotSeats+  -> [PartyVotes]+  -> [(Multiplier, (TotSeats, [SeatsWon]))]+increasingMultRemainder ts pv =+  zip [0..] (map (multLargestRemainder ts pv) [0..])++multRemainderAllResultsAtLeast1+  :: TotSeats+  -> [PartyVotes]+  -> (Multiplier, (TotSeats, [SeatsWon]))+multRemainderAllResultsAtLeast1 ts pv+  = head+  . dropWhile (any (< 1) . snd . snd)+  $ increasingMultRemainder ts pv++-- Largest remainder method: votes for one party is divided by+-- (total votes / number of seats). Result is an integer and a+-- remainder. Each party gets the number of seats indicated by its+-- integer. Parties are then ranked on the basis of the remainders, and+-- those with the largest remainders get an additional seat until all+-- seats have been distributed.+type AutoSeats = Integer+type PartyVotes = Integer+type TotVotes = Integer+type TotSeats = Integer+type Remainder = Rational+type SeatsWon = Integer++-- | Allocates integers using the largest remainder method. This is+-- the method used to allocate parliamentary seats in many countries,+-- so the types are named accordingly.+largestRemainderMethod+  :: TotSeats+  -- ^ Total number of seats in the legislature. This is the integer+  -- that will be allocated. This number must be positive or this+  -- function will fail at runtime.++  -> [PartyVotes]+  -- ^ The total seats will be allocated proportionally depending on+  -- how many votes each party received. The sum of this list must be+  -- positive, and each member of the list must be at least zero;+  -- otherwise a runtime error will occur.++  -> [SeatsWon]+  -- ^ The sum of this list will always be equal to the total number+  -- of seats, and its length will always be equal to length of the+  -- PartyVotes list.++largestRemainderMethod ts pvs =+  let err s = error $ "largestRemainderMethod: error: " ++ s+  in Ex.resolve err $ do+    Ex.assert "TotalSeats not positive" (ts > 0)+    Ex.assert "sum of [PartyVotes] not positive" (sum pvs > 0)+    Ex.assert "negative member of [PartyVotes]" (minimum pvs >= 0)+    return (allocRemainder ts . allocAuto ts $ pvs)+++autoAndRemainder+  :: TotSeats -> TotVotes -> PartyVotes -> (AutoSeats, Remainder)+autoAndRemainder ts tv pv =+  let fI = fromIntegral :: Integer -> Rational+      quota = if ts == 0+              then error "autoAndRemainder: zero total seats"+              else if tv == 0+                   then error "autoAndRemainder: zero total votes"+                   else fI tv / fI ts+  in properFraction (fI pv / quota)+++allocAuto :: TotSeats -> [PartyVotes] -> [(AutoSeats, Remainder)]+allocAuto ts pvs = map (autoAndRemainder ts (sum pvs)) pvs++allocRemainder+  :: TotSeats+  -> [(AutoSeats, Remainder)]+  -> [SeatsWon]+allocRemainder ts ls =+  let totLeft = ts - (sum . map fst $ ls)+      (leftForEach, stillLeft) = totLeft `divMod` genericLength ls+      wIndex = zip ([0..] :: [Integer]) ls+      sorted = sortBy (comparing (snd . snd)) wIndex+      wOrder = zip [0..] sorted+      awarder (ord, (ix, (as, _))) =+        if ord < stillLeft+        then (ix, as + leftForEach + 1)+        else (ix, as + leftForEach)+      awarded = map awarder wOrder+  in map snd . sortBy (comparing fst) $ awarded
+ lib/Penny/Lincoln/Builders.hs view
@@ -0,0 +1,28 @@+-- | 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+  ( account+  ) where++import qualified Penny.Lincoln.Bits as B+import qualified Data.Text as X++-- | Create an Account. You supply a single Text, with colons to+-- separate the different sub-accounts.+account :: X.Text -> B.Account+account s =+  if X.null s+  then B.Account []+  else B.Account . map B.SubAccount . X.splitOn (X.singleton ':') $ s+
+ lib/Penny/Lincoln/Ents.hs view
@@ -0,0 +1,278 @@+{-# LANGUAGE DeriveGeneric, DeriveFunctor #-}++-- | Containers for entries.+--+-- This module is the key guardian of the core principle of+-- double-entry accounting, which is that debits and credits must+-- always balance. An 'Ent' is a container for an 'Entry'. An 'Entry'+-- holds a 'DrCr' and an 'Amount' which, in turn, holds a 'Commodity'+-- and a 'Qty'. For a given 'Commodity' in a particular transaction,+-- the sum of the debits must always be equal to the sum of the+-- credits.+--+-- In addition to the 'Entry', the 'Ent' holds information about+-- whether the particular 'Entry' it holds is inferred or not. An Ent+-- is @inferred@ if the user did not supply the entry, but Penny was+-- able to deduce its 'Entry' because proper entries were supplied for+-- all the other postings in the transaction. The 'Ent' also holds+-- arbitrary metadata--which will typically be other information about+-- the particular posting, such as the payee, account, etc.+--+-- A collection of 'Ent' is an 'Ents'. This module will only create an+-- 'Ent' as part of an 'Ents' (though you can later separate the 'Ent'+-- from its other 'Ents' if you like.) In any given 'Ents', all of the+-- 'Ent' collectively have a zero balance.+--+-- This module also contains type synonyms used to represent a+-- Posting, which is an Ent bundled with its sibling Ents, and a+-- Transaction.++module Penny.Lincoln.Ents+  ( -- * Ent+    Inferred(..)+  , Ent+  , entry+  , inferred+  , meta++  -- * Ents+  , Ents+  , unEnts+  , tupleEnts+  , mapEnts+  , traverseEnts+  , ents+  , rEnts+  , headEnt+  , tailEnts++  -- * Postings and transactions+  , Posting(..)+  , Transaction(..)+  , transactionToPostings+  , views+  , unrollSnd+  ) where++import Control.Applicative+import Control.Arrow (second)+import Data.Binary (Binary)+import GHC.Generics (Generic)+import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.Balance as Bal+import Control.Monad (guard)+import qualified Penny.Lincoln.Equivalent as Ev+import Penny.Lincoln.Equivalent ((==~))+import Data.Monoid (mconcat, (<>))+import Data.List (foldl', unfoldr, sortBy)+import Data.Maybe (isNothing, catMaybes)+import qualified Data.Traversable as Tr+import qualified Data.Foldable as Fdbl++-- | An Ent is inferred if the user did not supply an entry for it and+-- Penny was able to infer the correct entry. Otherwise it is not+-- inferred.+data Inferred = Inferred | NotInferred+  deriving (Eq, Ord, Show, Generic)++instance Binary Inferred++-- | Information about an entry, along with whether it is inferred and+-- its metadata.+data Ent m = Ent+  { entry :: B.Entry+  -- ^ The entry from an Ent+  , inferred :: Inferred+  -- ^ Whether the entry was inferred+  , meta :: m+  -- ^ The metadata accompanying an Ent+  } deriving (Eq, Ord, Show, Generic)++-- | Two Ents are equivalent if the entries are equivalent and the+-- metadata is equivalent (whether the Ent is inferred or not is+-- ignored.)+instance Ev.Equivalent m => Ev.Equivalent (Ent m) where+  equivalent (Ent e1 _ m1) (Ent e2 _ m2) =+    e1 ==~ e2 && m1 ==~ m2+  compareEv (Ent e1 _ m1) (Ent e2 _ m2) =+    Ev.compareEv e1 e2 <> Ev.compareEv m1 m2++instance Functor Ent where+  fmap f (Ent e i m) = Ent e i (f m)++instance Binary m => Binary (Ent m)++newtype Ents m = Ents { unEnts :: [Ent m] }+  deriving (Eq, Ord, Show, Generic, Functor)++-- | Ents are equivalent if the content Ents of each are+-- equivalent. The order of the ents is insignificant.+instance Ev.Equivalent m => Ev.Equivalent (Ents m) where+  equivalent (Ents e1) (Ents e2) =+    let (e1', e2') = (sortBy Ev.compareEv e1, sortBy Ev.compareEv e2)+    in and $ (length e1 == length e2)+           : zipWith Ev.equivalent e1' e2'++  compareEv (Ents e1) (Ents e2) =+    let (e1', e2') = (sortBy Ev.compareEv e1, sortBy Ev.compareEv e2)+    in mconcat $ compare (length e1) (length e2)+               : zipWith Ev.compareEv e1' e2'++instance Fdbl.Foldable Ents where+  foldr f z (Ents ls) = case ls of+    [] -> z+    x:xs -> f (meta x) (Fdbl.foldr f z (map meta xs))++instance Tr.Traversable Ents where+  sequenceA = fmap Ents . Tr.sequenceA . map seqEnt . unEnts++-- | Alter the metadata Ents, while examining the Ents themselves. If+-- you only want to change the metadata and you don't need to examine+-- the other contents of the Ent, use the Functor instance. You cannot+-- change non-metadata aspects of the Ent.+mapEnts :: (Ent a -> b) -> Ents a -> Ents b+mapEnts f = Ents . map f' . unEnts where+  f' e = e { meta = f e }++-- | Alter the metadata of Ents while examing their contents. If you+-- do not need to examine their contents, use the Traversable+-- instance.+traverseEnts :: Applicative f => (Ent a -> f b) -> Ents a -> f (Ents b)+traverseEnts f = fmap Ents . Tr.traverse f' . unEnts where+  f' en@(Ent e i _) = Ent <$> pure e <*> pure i <*> f en++seqEnt :: Applicative f => Ent (f a) -> f (Ent a)+seqEnt (Ent e i m) = Ent <$> pure e <*> pure i <*> m++-- | Every Ents alwas contains at least two ents, and possibly+-- additional ones.+tupleEnts :: Ents m -> (Ent m, Ent m, [Ent m])+tupleEnts (Ents ls) = case ls of+  t1:t2:ts -> (t1, t2, ts)+  _ -> error "tupleEnts: ents does not have two ents"++instance Binary m => Binary (Ents m)++-- | In a Posting, the Ent at the front of the list of Ents is the+-- main posting. There are additional postings. This function+-- rearranges the Ents multiple times so that each posting is at the+-- head of the list exactly once.+views :: Ents m -> [Ents m]+views = map Ents . orderedPermute . unEnts++-- | > unrollSnd (undefined, []) == []+--   > unrollSnd (1, [1,2,3]) = [(1,1), (1,2), (1,3)]++unrollSnd :: (a, [b]) -> [(a, b)]+unrollSnd = unfoldr f where+  f (_, []) = Nothing+  f (a, b:bs) = Just ((a, b), (a, bs))++-- | Splits a Transaction into Postings.+transactionToPostings :: Transaction -> [Posting]+transactionToPostings =+  map Posting . unrollSnd . second views . unTransaction++-- | Get information from the head posting in the View, which is the+-- one you are most likely interested in. This never fails, as every+-- Ents has at least two postings.+headEnt :: Ents m -> Ent m+headEnt (Ents ls) = case ls of+  [] -> error "ents: empty view"+  x:_ -> x++-- | Get information on sibling Ents.+tailEnts :: Ents m -> (Ent m, [Ent m])+tailEnts (Ents ls) = case ls of+  [] -> error "ents: tailEnts: empty view"+  _:xs -> case xs of+    [] -> error "ents: tailEnts: only one sibling"+    s2:ss -> (s2, ss)++-- | A Transaction and a Posting are identical on the inside, but they+-- have different semantic meanings so they are wrapped in newtypes.+newtype Transaction = Transaction+  { unTransaction :: ( B.TopLineData, Ents B.PostingData ) }+  deriving (Eq, Show)++-- | In a Posting, the Ent yielded by 'headEnt' will be the posting of+-- interest. The other sibling postings are also available for+-- inspection.+newtype Posting = Posting+  { unPosting :: ( B.TopLineData, Ents B.PostingData ) }+  deriving (Eq, Show)++-- | Returns a list of lists where each element in the original list+-- is in the front of a new list once.+--+-- > orderedPermute [1,2,3] == [[1,2,3], [2,3,1], [3,1,2]]+orderedPermute :: [a] -> [[a]]+orderedPermute ls = take (length ls) (iterate toTheBack ls)+  where+    toTheBack [] = []+    toTheBack (a:as) = as ++ [a]++-- | Creates an 'Ents'. At most, one of the Maybe Entry can be Nothing+-- and this function will infer the remaining Entry. This function+-- fails if it cannot create a balanced Ents.+ents+  :: [(Maybe B.Entry, m)]+  -> Maybe (Ents m)+ents ls = do+  guard . not . null $ ls+  let makePstg = makeEnt (inferredVal . map fst $ ls)+  fmap Ents $ mapM makePstg ls++-- | Creates 'Ents'. Unlike 'ents' this function never fails because+-- you are restricted in the inputs that you can give it. It will+-- always infer the last Entry. All Entries except one will have the+-- same DrCr; the last, inferred one will have the opposite DrCr.+rEnts+  :: B.Commodity+  -- ^ Commodity for all postings+  -> B.DrCr+  -- ^ DrCr for all non-inferred postings+  -> (B.Qty, m)+  -- ^ Non-inferred posting 1+  -> [(B.Qty, m)]+  -- ^ Remaining non-inferred postings+  -> m+  -- ^ Metadata for inferred posting+  -> Ents m+rEnts com dc (q1, m1) nonInfs lastMeta =+  let tot = foldl' B.add q1 . map fst $ nonInfs+      p1 = makePstg (q1, m1)+      ps = map makePstg nonInfs+      makePstg (q, m) = Ent (B.Entry dc (B.Amount q com))+                                NotInferred m+      lastPstg = Ent (B.Entry (B.opposite dc) (B.Amount tot com))+                         Inferred lastMeta+  in Ents $ p1:ps ++ [lastPstg]+++-- | Changes Maybe Entries into Postings. Uses the inferred value if+-- the Maybe Entry is Nothing. If there is no inferred value, returns+-- Nothing.+makeEnt+  :: Maybe B.Entry+  -- ^ Inferred value+  -> (Maybe B.Entry, m)+  -> Maybe (Ent m)+makeEnt mayInf (mayEn, m) = case mayEn of+  Nothing -> case mayInf of+    Nothing -> Nothing+    Just inf -> return $ Ent inf Inferred m+  Just en -> return $ Ent en NotInferred m+++-- | Gets a single inferred entry from a balance, if possible.+inferredVal :: [Maybe B.Entry] -> Maybe B.Entry+inferredVal ls = do+  guard ((length . filter id . map isNothing $ ls) == 1)+  let bal = mconcat+            . map Bal.entryToBalance+            . catMaybes+            $ ls+  case Bal.isBalanced bal of+    Bal.Inferable e -> Just e+    _ -> Nothing
+ lib/Penny/Lincoln/Equivalent.hs view
@@ -0,0 +1,21 @@+module Penny.Lincoln.Equivalent where++import Data.Monoid ((<>))++-- | Comparisons for equivalency. Two items are equivalent if they+-- have the same semantic meaning, even if the data in the two items+-- is different.+class Equivalent a where+  equivalent :: a -> a -> Bool++  -- | Compares based on equivalency.+  compareEv :: a -> a -> Ordering++(==~) :: Equivalent a => a -> a -> Bool+(==~) = equivalent+infix 4 ==~++instance (Equivalent a, Equivalent b) => Equivalent (a, b) where+  equivalent (a1, b1) (a2, b2) = a1 ==~ a2 && b1 ==~ b2+  compareEv (a1, b1) (a2, b2) =+    compareEv a1 a2 <> compareEv b1 b2
+ lib/Penny/Lincoln/HasText.hs view
@@ -0,0 +1,58 @@+module Penny.Lincoln.HasText where++import Data.Text (Text)+import qualified Data.Text as X++import qualified Penny.Lincoln.Bits as B++class HasText a where+  text :: a -> Text++instance HasText Text where+  text = id++instance HasText B.SubAccount where+  text = B.unSubAccount++instance HasText B.Flag where+  text = B.unFlag++instance HasText B.Commodity where+  text = B.unCommodity++instance HasText B.Number where+  text = B.unNumber++instance HasText B.Payee where+  text = B.unPayee++instance HasText B.Tag where+  text = B.unTag++instance HasText B.Filename where+  text = B.unFilename++class HasTextList a where+  textList :: a -> [Text]++-- | Wraps instances of HasTextList and provides a delimiter; the+-- result is an instance of HasText.+data Delimited a = Delimited+  { delimiter :: Text+  , delimited :: a+  } deriving (Eq, Show)++instance HasTextList a => HasTextList (Delimited a) where+  textList = textList . delimited++instance HasTextList a => HasText (Delimited a) where+  text a = X.intercalate (delimiter a) . textList . delimited $ a++instance HasTextList B.Account where+  textList = map text . B.unAccount++instance HasTextList B.Tags where+  textList = map text . B.unTags++instance HasTextList B.Memo where+  textList = B.unMemo
+ lib/Penny/Lincoln/Matchers.hs view
@@ -0,0 +1,21 @@+-- | Type synonyms for functions dealing with text matching.++module Penny.Lincoln.Matchers where++import qualified Data.Text as X+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Text.Matchers as MT++-- | A function that makes Matchers.+type Factory+  = MT.CaseSensitive+  -- ^ Will this matcher be case sensitive?++  -> X.Text+  -- ^ The pattern to use when testing for a match. For example, this+  -- might be a regular expression, or simply the text to be matched.++  -> Ex.Exceptional X.Text MT.Matcher+  -- ^ Sometimes producing a matcher might fail; for example, the user+  -- might have supplied a bad pattern. If so, an exception is+  -- returned. On success, a Matcher is returned.
+ lib/Penny/Lincoln/Predicates.hs view
@@ -0,0 +1,323 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Functions that return a boolean based upon some criterion that+-- matches something, often a PostFam. Useful when filtering+-- Postings.+module Penny.Lincoln.Predicates+  ( LPdct+  , MakePdct+  , payee+  , number+  , flag+  , postingMemo+  , transactionMemo+  , date+  , qty+  , drCr+  , debit+  , credit+  , commodity+  , account+  , accountLevel+  , accountAny+  , tag+  , reconciled+  , filename++  -- * Serials+  , serialPdct+  , MakeSerialPdct+  , fwdGlobalPosting+  , backGlobalPosting+  , fwdFilePosting+  , backFilePosting+  , fwdGlobalTransaction+  , backGlobalTransaction+  , fwdFileTransaction+  , backFileTransaction+  ) where+++import Data.List (intersperse)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as X+import qualified Data.Time as Time+import qualified Penny.Lincoln.Bits as B+import Penny.Lincoln.HasText (HasText, text, HasTextList, textList)+import qualified Penny.Lincoln.Queries as Q+import Penny.Lincoln.Ents (Posting)+import qualified Penny.Lincoln.Ents as E+import qualified Text.Matchers as M+import qualified Data.Prednote.Pdct as P+import Penny.Lincoln.Serial (forward, backward)++type LPdct = P.Pdct Posting++type MakePdct = M.Matcher -> LPdct++-- * Matching helpers+match+  :: HasText a+  => Text+  -- ^ Description of this field+  -> (Posting -> a)+  -- ^ Function that returns the field being matched+  -> M.Matcher+  -> LPdct+match t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = M.match m . text . f++matchMaybe+  :: HasText a+  => Text+  -- ^ Description of this field+  -> (Posting -> Maybe a)+  -> M.Matcher+  -> LPdct+matchMaybe t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = maybe False (M.match m . text) . f++makeDesc :: Text -> M.Matcher -> Text+makeDesc t m+  = "subject: " <> t+  <> " matcher: " <> M.matchDesc m++-- | Does the given matcher match any of the elements of the Texts in+-- a HasTextList?+matchAny+  :: HasTextList a+  => Text+  -> (Posting -> a)+  -> M.Matcher+  -> LPdct+matchAny t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = any (M.match m) . textList . f++-- | Does the given matcher match the text that is at the given+-- element of a HasTextList? If the HasTextList does not have a+-- sufficent number of elements to perform this test, returns False.+matchLevel+  :: HasTextList a+  => Int+  -> Text+  -> (Posting -> a)+  -> M.Matcher+  -> LPdct+matchLevel l d f m = P.operand desc pd+  where+    desc = makeDesc ("level " <> X.pack (show l) <> " of " <> d) m+    pd pf = let ts = textList (f pf)+            in if l < 0 || l >= length ts+               then False+               else M.match m (ts !! l)++-- | Does the matcher match the text of the memo? Joins each line of+-- the memo with a space.+matchMemo+  :: Text+  -> (Posting -> Maybe B.Memo)+  -> M.Matcher+  -> LPdct+matchMemo t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = maybe False doMatch . f+    doMatch = M.match m+              . X.intercalate (X.singleton ' ')+              . B.unMemo++matchDelimited+  :: HasTextList a+  => Text+  -- ^ Separator+  -> Text+  -- ^ Label+  -> (Posting -> a)+  -> M.Matcher+  -> LPdct+matchDelimited sep lbl f m = match lbl f' m+  where+    f' = X.concat . intersperse sep . textList . f++-- * Pattern matching fields++payee :: MakePdct+payee = matchMaybe "payee" Q.payee++number :: MakePdct+number = matchMaybe "number" Q.number++flag :: MakePdct+flag = matchMaybe "flag" Q.flag++postingMemo :: MakePdct+postingMemo = matchMemo "posting memo" Q.postingMemo++transactionMemo :: MakePdct+transactionMemo = matchMemo "transaction memo" Q.transactionMemo++-- * Date++date+  :: Ordering+  -> Time.UTCTime+  -> LPdct+date ord u = P.compareBy (X.pack . show $ u)+             "UTC date and time"+           (\l -> compare (B.toUTC . Q.dateTime $ l) u) ord+++qty :: Ordering -> B.Qty -> LPdct+qty o q = P.compareBy (X.pack . show $ q) "quantity"+          (\l -> B.compareQty (Q.qty l) q) o+++drCr :: B.DrCr -> LPdct+drCr dc = P.operand desc pd+  where+    desc = "entry is a " <> s+    s = case dc of { B.Debit -> "debit"; B.Credit -> "credit" }+    pd pf = Q.drCr pf == dc++debit :: LPdct+debit = drCr B.Debit++credit :: LPdct+credit = drCr B.Credit++commodity :: M.Matcher -> LPdct+commodity = match "commodity" Q.commodity++account :: M.Matcher -> LPdct+account = matchDelimited ":" "account" Q.account++accountLevel :: Int -> M.Matcher -> LPdct+accountLevel i = matchLevel i "account" Q.account++accountAny :: M.Matcher -> LPdct+accountAny = matchAny "any sub-account" Q.account++tag :: M.Matcher -> LPdct+tag = matchAny "any tag" Q.tags++-- | True if a posting is reconciled; that is, its flag is exactly+-- @R@.+reconciled :: LPdct+reconciled = P.operand d p+  where+    d = "posting flag is exactly \"R\" (is reconciled)"+    p = maybe False ((== X.singleton 'R') . B.unFlag) . Q.flag++filename :: M.Matcher -> LPdct+filename = matchMaybe "filename" Q.filename++-- | Makes Pdct based on comparisons against a particular serial.++serialPdct+  :: Text+  -- ^ Name of the serial, e.g. @globalPosting@++  -> (a -> Maybe Int)+  -- ^ How to obtain the serial from the item being examined++  -> Int+  -- ^ The right hand side++  -> Ordering+  -- ^ The Pdct returned will be Just True if the item has a serial+  -- and @compare ser rhs@ returns this Ordering; Just False if the+  -- item has a srerial and @compare@ does not return this Ordering;+  -- Nothing if the item does not have a serial.++  -> P.Pdct a++serialPdct name getSer i o = P.Pdct n (P.Operand f)+  where+    n = "serial " <> name <> " is " <> descCmp <> " "+        <> X.pack (show i)+    descCmp = case o of+      EQ -> "equal to"+      LT -> "less than"+      GT -> "greater than"+    f = fmap (\ser -> compare ser i == o) . getSer++type MakeSerialPdct = Int -> Ordering -> P.Pdct Posting++fwdGlobalPosting :: MakeSerialPdct+fwdGlobalPosting =+  serialPdct "fwdGlobalPosting"+  $ fmap (forward . B.unGlobalPosting)+  . B.pdGlobal+  . E.meta+  . E.headEnt+  . snd+  . E.unPosting++backGlobalPosting :: MakeSerialPdct+backGlobalPosting =+  serialPdct "revGlobalPosting"+  $ fmap (backward . B.unGlobalPosting)+  . B.pdGlobal+  . E.meta+  . E.headEnt+  . snd+  . E.unPosting++fwdFilePosting :: MakeSerialPdct+fwdFilePosting+  = serialPdct "fwdFilePosting"+  $ fmap (forward . B.unFilePosting . B.pFilePosting)+  . B.pdFileMeta+  . E.meta+  . E.headEnt+  . snd+  . E.unPosting++backFilePosting :: MakeSerialPdct+backFilePosting+  = serialPdct "revFilePosting"+  $ fmap (backward . B.unFilePosting . B.pFilePosting)+  . B.pdFileMeta+  . E.meta+  . E.headEnt+  . snd+  . E.unPosting++fwdGlobalTransaction :: MakeSerialPdct+fwdGlobalTransaction+  = serialPdct "fwdGlobalTransaction"+  $ fmap (forward . B.unGlobalTransaction)+  . B.tlGlobal+  . fst+  . E.unPosting++backGlobalTransaction :: MakeSerialPdct+backGlobalTransaction+  = serialPdct "backGlobalTransaction"+  $ fmap (backward . B.unGlobalTransaction)+  . B.tlGlobal+  . fst+  . E.unPosting++fwdFileTransaction :: MakeSerialPdct+fwdFileTransaction+  = serialPdct "fwdFileTransaction"+  $ fmap (forward . B.unFileTransaction . B.tFileTransaction)+  . B.tlFileMeta+  . fst+  . E.unPosting++backFileTransaction :: MakeSerialPdct+backFileTransaction+  = serialPdct "backFileTransaction"+  $ fmap (backward . B.unFileTransaction . B.tFileTransaction)+  . B.tlFileMeta+  . fst+  . E.unPosting
+ lib/Penny/Lincoln/Predicates/Siblings.hs view
@@ -0,0 +1,328 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Functions that return a boolean based upon some criterion that+-- matches something, often a PostFam. Useful when filtering+-- Postings.+module Penny.Lincoln.Predicates.Siblings+  ( LPdct+  , MakePdct+  , payee+  , number+  , flag+  , postingMemo+  , qty+  , parseQty+  , drCr+  , debit+  , credit+  , commodity+  , account+  , accountLevel+  , accountAny+  , tag+  , reconciled++  -- * Serials+  , serialPdct+  , MakeSerialPdct+  , fwdGlobalPosting+  , backGlobalPosting+  , fwdFilePosting+  , backFilePosting+  , fwdGlobalTransaction+  , backGlobalTransaction+  , fwdFileTransaction+  , backFileTransaction+  ) where+++import Control.Arrow (second)+import Data.List (intersperse)+import Data.Maybe (catMaybes)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as X+import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.Ents as E+import Penny.Lincoln.Serial (forward, backward)+import Penny.Lincoln.HasText (HasText, text, HasTextList, textList)+import qualified Penny.Lincoln.Queries.Siblings as Q+import Penny.Lincoln.Ents (Posting)+import qualified Text.Matchers as M+import qualified Data.Prednote.Pdct as P++type LPdct = P.Pdct Posting++type MakePdct = M.Matcher -> LPdct++-- * Matching helpers+match+  :: HasText a+  => Text+  -- ^ Description of this field+  -> (Posting -> [a])+  -- ^ Function that returns the field being matched+  -> M.Matcher+  -> LPdct+match t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = any (M.match m) . map text . f++matchMaybe+  :: HasText a+  => Text+  -- ^ Description of this field+  -> (Posting -> [Maybe a])+  -> M.Matcher+  -> LPdct+matchMaybe t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = any (== (Just True))+         . map (fmap (M.match m . text))+         . f++makeDesc :: Text -> M.Matcher -> Text+makeDesc t m+  = "subject: " <> t <> " (any sibling posting) matcher: "+  <> M.matchDesc m++-- | Does the given matcher match any of the elements of the Texts in+-- a HasTextList?+matchAny+  :: HasTextList a+  => Text+  -> (Posting -> [a])+  -> M.Matcher+  -> LPdct+matchAny t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = any (any (M.match m)) . map textList . f++-- | Does the given matcher match the text that is at the given+-- element of a HasTextList? If the HasTextList does not have a+-- sufficent number of elements to perform this test, returns False.+matchLevel+  :: HasTextList a+  => Int+  -> Text+  -> (Posting -> [a])+  -> M.Matcher+  -> LPdct+matchLevel l d f m = P.operand desc pd+  where+    desc = makeDesc ("level " <> X.pack (show l) <> " of " <> d) m+    pd pf = let doMatch list = if l < 0 || l >= length list+                               then False+                               else M.match m (list !! l)+            in any doMatch . map textList . f $ pf++-- | Does the matcher match the text of the memo? Joins each line of+-- the memo with a space.+matchMemo+  :: Text+  -> (Posting -> [Maybe B.Memo])+  -> M.Matcher+  -> LPdct+matchMemo t f m = P.operand desc pd+  where+    desc = makeDesc t m+    pd = any (maybe False doMatch) . f+    doMatch = M.match m+              . X.intercalate (X.singleton ' ')+              . B.unMemo++matchDelimited+  :: HasTextList a+  => Text+  -- ^ Separator+  -> Text+  -- ^ Label+  -> (Posting -> [a])+  -> M.Matcher+  -> LPdct+matchDelimited sep lbl f m = match lbl f' m+  where+    f' = map (X.concat . intersperse sep . textList) . f++-- * Pattern matching fields++payee :: MakePdct+payee = matchMaybe "payee" Q.payee++number :: MakePdct+number = matchMaybe "number" Q.number++flag :: MakePdct+flag = matchMaybe "flag" Q.flag++postingMemo :: MakePdct+postingMemo = matchMemo "posting memo" Q.postingMemo++-- | A Pdct that returns True if @compare subject qty@ returns the+-- given Ordering.+qty :: Ordering -> B.Qty -> LPdct+qty o q = P.operand desc pd+  where+    desc = "quantity of any sibling is " <> dd <> " " <> X.pack (show q)+    dd = case o of+      LT -> "less than"+      GT -> "greater than"+      EQ -> "equal to"+    pd = any ((== o) . (`compare` q)) . Q.qty++parseQty+  :: X.Text+  -> Maybe (B.Qty -> LPdct)+parseQty x+  | x == "==" = Just (qty EQ)+  | x == "=" = Just (qty EQ)+  | x == ">" = Just (qty GT)+  | x == "<" = Just (qty LT)+  | x == "/=" = Just (\q -> P.not (qty EQ q))+  | x == "!=" = Just (\q -> P.not (qty EQ q))+  | x == ">=" = Just (\q -> P.or [qty GT q, qty EQ q])+  | x == "<=" = Just (\q -> P.or [qty LT q, qty EQ q])+  | otherwise = Nothing++drCr :: B.DrCr -> LPdct+drCr dc = P.operand desc pd+  where+    desc = "entry of any sibling is a " <> s+    s = case dc of { B.Debit -> "debit"; B.Credit -> "credit" }+    pd = any (== dc) . Q.drCr++debit :: LPdct+debit = drCr B.Debit++credit :: LPdct+credit = drCr B.Credit++commodity :: M.Matcher -> LPdct+commodity = match "commodity" Q.commodity++account :: M.Matcher -> LPdct+account = matchDelimited ":" "account" Q.account++accountLevel :: Int -> M.Matcher -> LPdct+accountLevel i = matchLevel i "account" Q.account++accountAny :: M.Matcher -> LPdct+accountAny = matchAny "any sub-account" Q.account++tag :: M.Matcher -> LPdct+tag = matchAny "any tag" Q.tags++-- | True if a posting is reconciled; that is, its flag is exactly+-- @R@.+reconciled :: LPdct+reconciled = P.operand d p+  where+    d = "posting flag is exactly \"R\" (is reconciled)"+    p = any (maybe False ((== X.singleton 'R') . B.unFlag))+        . Q.flag++--+-- Serials+--++-- | Makes Pdct based on comparisons against a particular serial.++serialPdct+  :: Text+  -- ^ Name of the serial, e.g. @globalPosting@++  -> ((B.TopLineData, E.Ent B.PostingData) -> Maybe Int)+  -- ^ How to obtain the serial from the item being examined++  -> Int+  -- ^ The right hand side++  -> Ordering+  -- ^ The Pdct returned will be Just True if the item has a serial+  -- and @compare ser rhs@ returns this Ordering; Just False if the+  -- item has a srerial and @compare@ does not return this Ordering;+  -- Nothing if the item does not have a serial.++  -> P.Pdct E.Posting++serialPdct name getSer i o = P.Pdct n (P.Operand f)+  where+    n = "serial " <> name <> " is " <> descCmp <> " "+        <> X.pack (show i)+    descCmp = case o of+      EQ -> "equal to"+      LT -> "less than"+      GT -> "greater than"+    f = Just+        . any (\ser -> compare ser i == o )+        . catMaybes+        . map getSer+        . E.unrollSnd+        . second (\(x, xs) -> (x:xs))+        . second E.tailEnts+        . E.unPosting++type MakeSerialPdct = Int -> Ordering -> P.Pdct Posting++fwdGlobalPosting :: MakeSerialPdct+fwdGlobalPosting =+  serialPdct "fwdGlobalPosting"+  $ fmap (forward . B.unGlobalPosting)+  . B.pdGlobal+  . E.meta+  . snd++backGlobalPosting :: MakeSerialPdct+backGlobalPosting =+  serialPdct "revGlobalPosting"+  $ fmap (backward . B.unGlobalPosting)+  . B.pdGlobal+  . E.meta+  . snd++fwdFilePosting :: MakeSerialPdct+fwdFilePosting+  = serialPdct "fwdFilePosting"+  $ fmap (forward . B.unFilePosting . B.pFilePosting)+  . B.pdFileMeta+  . E.meta+  . snd++backFilePosting :: MakeSerialPdct+backFilePosting+  = serialPdct "revFilePosting"+  $ fmap (backward . B.unFilePosting . B.pFilePosting)+  . B.pdFileMeta+  . E.meta+  . snd++fwdGlobalTransaction :: MakeSerialPdct+fwdGlobalTransaction+  = serialPdct "fwdGlobalTransaction"+  $ fmap (forward . B.unGlobalTransaction)+  . B.tlGlobal+  . fst++backGlobalTransaction :: MakeSerialPdct+backGlobalTransaction+  = serialPdct "backGlobalTransaction"+  $ fmap (backward . B.unGlobalTransaction)+  . B.tlGlobal+  . fst++fwdFileTransaction :: MakeSerialPdct+fwdFileTransaction+  = serialPdct "fwdFileTransaction"+  $ fmap (forward . B.unFileTransaction . B.tFileTransaction)+  . B.tlFileMeta+  . fst++backFileTransaction :: MakeSerialPdct+backFileTransaction+  = serialPdct "backFileTransaction"+  $ fmap (backward . B.unFileTransaction . B.tFileTransaction)+  . B.tlFileMeta+  . fst
+ lib/Penny/Lincoln/PriceDb.hs view
@@ -0,0 +1,98 @@+-- | 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),+  convertAsOf+  ) where++import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Map as M+import qualified Data.Time as T+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 (M.Map B.From ToMap)++-- | An empty PriceDb+emptyDb :: PriceDb+emptyDb = PriceDb M.empty++-- | Add a single price to the PriceDb.+addPrice :: PriceDb -> B.PricePoint -> PriceDb+addPrice (PriceDb db) (B.PricePoint dt pr _ _ _) = PriceDb m'+  where+    m' = M.alter f (B.from pr) db+    utc = B.toUTC dt+    cpu = B.countPerUnit pr+    f k = case k of+      Nothing -> Just $ M.singleton (B.to pr) cpuMap+        where+          cpuMap = M.singleton utc cpu+      Just tm -> Just tm'+        where+          tm' = M.alter g (B.to pr) tm+          g maybeTo = case maybeTo of+            Nothing -> Just $ M.singleton utc cpu+            Just cpuMap -> Just $ M.insert utc cpu cpuMap++++-- | 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.+--+-- 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.++getPrice ::+  PriceDb+  -> B.From+  -> B.To+  -> B.DateTime+  -> Ex.Exceptional PriceDbError B.CountPerUnit+getPrice (PriceDb db) fr to dt = do+  let utc = B.toUTC dt+  toMap <- Ex.fromMaybe FromNotFound $ M.lookup fr db+  cpuMap <- Ex.fromMaybe ToNotFound $ M.lookup to toMap+  let (lower, exact, _) = M.splitLookup utc cpuMap+  case exact of+    Just c -> return c+    Nothing ->+      if M.null lower+      then Ex.throw CpuNotFound+      else return . snd . M.findMax $ lower+++-- | 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.+convertAsOf ::+  PriceDb+  -> B.DateTime+  -> B.To+  -> B.Amount+  -> Ex.Exceptional PriceDbError B.Qty+convertAsOf db dt to (B.Amount qt fr)+  | fr == B.unTo to = return qt+  | otherwise = do+    cpu <- fmap B.unCountPerUnit (getPrice db (B.From fr) to dt)+    let qt' = B.mult cpu qt+    return qt'
+ lib/Penny/Lincoln/Queries.hs view
@@ -0,0 +1,104 @@+-- | Examining a Posting for a particular component of the main+-- posting (as opposed to the sibling postings) in the Posting. For+-- some components, such as the payee, the posting might have one+-- piece of data while the TopLine has something else. These functions+-- will examine the Posting first and, if it has no information, use+-- the data from the TopLine if it is there.+module Penny.Lincoln.Queries where++import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.Ents as E+import Penny.Lincoln.Balance (Balance, entryToBalance)+import qualified Data.Time as Time++-- | Uses the data from the Posting if it is set; otherwise, use the+-- data from the TopLine.+best+  :: (B.TopLineData -> Maybe a)+  -> (E.Ents B.PostingData -> Maybe a)+  -> E.Posting+  -> Maybe a+best fp ft vp = case fp . fst . E.unPosting $ vp of+  Nothing -> ft . snd . E.unPosting $ vp+  Just r -> Just r++payee :: E.Posting -> Maybe B.Payee+payee = best (B.tPayee . B.tlCore)+             (B.pPayee . B.pdCore . E.meta . E.headEnt)++number :: E.Posting -> Maybe B.Number+number = best (B.tNumber . B.tlCore)+              (B.pNumber . B.pdCore . E.meta . E.headEnt)++flag :: E.Posting -> Maybe B.Flag+flag = best (B.tFlag . B.tlCore)+            (B.pFlag . B.pdCore . E.meta . E.headEnt)++postingMemo :: E.Posting -> Maybe B.Memo+postingMemo = B.pMemo . B.pdCore . E.meta . E.headEnt . snd . E.unPosting++transactionMemo :: E.Posting -> Maybe B.Memo+transactionMemo =  B.tMemo . B.tlCore . fst . E.unPosting++dateTime :: E.Posting -> B.DateTime+dateTime = B.tDateTime . B.tlCore . fst . E.unPosting++localDay :: E.Posting -> Time.Day+localDay = B.day . dateTime++account :: E.Posting -> B.Account+account = B.pAccount . B.pdCore . E.meta . E.headEnt . snd . E.unPosting++tags :: E.Posting -> B.Tags+tags = B.pTags . B.pdCore . E.meta . E.headEnt . snd . E.unPosting++entry :: E.Posting -> B.Entry+entry = E.entry . E.headEnt . snd . E.unPosting++balance :: E.Posting -> Balance+balance = entryToBalance . entry++drCr :: E.Posting -> B.DrCr+drCr = B.drCr . entry++amount :: E.Posting -> B.Amount+amount = B.amount . entry++qty :: E.Posting -> B.Qty+qty = B.qty . amount++commodity :: E.Posting -> B.Commodity+commodity = B.commodity . amount++topMemoLine :: E.Posting -> Maybe B.TopMemoLine+topMemoLine p = (B.tlFileMeta . fst . E.unPosting $ p) >>= B.tTopMemoLine++topLineLine :: E.Posting -> Maybe B.TopLineLine+topLineLine = fmap B.tTopLineLine . B.tlFileMeta . fst . E.unPosting++globalTransaction :: E.Posting -> Maybe B.GlobalTransaction+globalTransaction = B.tlGlobal . fst . E.unPosting++fileTransaction :: E.Posting -> Maybe B.FileTransaction+fileTransaction = fmap B.tFileTransaction . B.tlFileMeta . fst . E.unPosting++globalPosting :: E.Posting -> Maybe B.GlobalPosting+globalPosting = B.pdGlobal . E.meta . E.headEnt . snd . E.unPosting++filePosting :: E.Posting -> Maybe B.FilePosting+filePosting = fmap B.pFilePosting . B.pdFileMeta . E.meta+                   . E.headEnt . snd . E.unPosting++postingLine :: E.Posting -> Maybe B.PostingLine+postingLine = fmap B.pPostingLine . B.pdFileMeta+              . E.meta . E.headEnt . snd . E.unPosting++side :: E.Posting -> Maybe B.Side+side = B.pSide . B.pdCore . E.meta . E.headEnt . snd . E.unPosting++spaceBetween :: E.Posting -> Maybe B.SpaceBetween+spaceBetween = B.pSpaceBetween . B.pdCore+               . E.meta . E.headEnt . snd . E.unPosting++filename :: E.Posting -> Maybe B.Filename+filename = fmap B.tFilename . B.tlFileMeta . fst . E.unPosting
+ lib/Penny/Lincoln/Queries/Siblings.hs view
@@ -0,0 +1,102 @@+-- | Like 'Penny.Lincoln.Queries' but instead of querying the main+-- posting of the PostFam, queries the siblings. Therefore, these+-- functions return a list, with each entry in the list containing the+-- best answer for each sibling. There is one item in the list for+-- each sibling, even if all these items contain the same data (for+-- instance, a posting might have five siblings, but all five siblings+-- might have the same payee. Nonetheless the 'payee' function will+-- return a list of five items.)+module Penny.Lincoln.Queries.Siblings where++import Control.Arrow (second, first)+import qualified Penny.Lincoln.Bits as B+import qualified Penny.Lincoln.Ents as E+import Penny.Lincoln.Balance (Balance, entryToBalance)++-- | For all siblings, uses information from the Posting if it is set;+-- otherwise, uses data from the TopLine.+bestSibs+  :: (B.PostingCore -> Maybe a)+  -> (B.TopLineCore -> Maybe a)+  -> E.Posting+  -> [Maybe a]+bestSibs fp ft =+  map f+  . map (second (B.pdCore . E.meta))+  . E.unrollSnd+  . second (\(x, xs) -> (x:xs))+  . second E.tailEnts+  . first B.tlCore+  . E.unPosting+  where+    f (tl, vw) = maybe (ft tl) Just (fp vw)+++-- | For all siblings, get the information from the Posting if it+-- exists; otherwise Nothing.+sibs+  :: (E.Ent B.PostingData -> a)+  -> E.Posting+  -> [a]+sibs fp = map fp . snd . fmap ((\(x, xs) -> (x:xs)) . E.tailEnts)+          . E.unPosting++payee :: E.Posting -> [Maybe B.Payee]+payee = bestSibs B.pPayee B.tPayee++number :: E.Posting -> [Maybe B.Number]+number = bestSibs B.pNumber B.tNumber++flag :: E.Posting -> [Maybe B.Flag]+flag = bestSibs B.pFlag B.tFlag++postingMemo :: E.Posting -> [Maybe B.Memo]+postingMemo = sibs (B.pMemo . B.pdCore . E.meta)++account :: E.Posting -> [B.Account]+account = sibs (B.pAccount . B.pdCore . E.meta)++tags :: E.Posting -> [B.Tags]+tags = sibs (B.pTags . B.pdCore . E.meta)++entry :: E.Posting -> [B.Entry]+entry = sibs E.entry++balance :: E.Posting -> [Balance]+balance = map entryToBalance . entry++drCr :: E.Posting -> [B.DrCr]+drCr = map B.drCr . entry++amount :: E.Posting -> [B.Amount]+amount = map B.amount . entry++qty :: E.Posting -> [B.Qty]+qty = map B.qty . amount++commodity :: E.Posting -> [B.Commodity]+commodity = map B.commodity . amount++postingLine :: E.Posting -> [Maybe B.PostingLine]+postingLine = sibs (fmap B.pPostingLine . B.pdFileMeta . E.meta)++side :: E.Posting -> [Maybe B.Side]+side = sibs (B.pSide . B.pdCore . E.meta)++spaceBetween :: E.Posting -> [Maybe B.SpaceBetween]+spaceBetween = sibs (B.pSpaceBetween . B.pdCore . E.meta)++globalPosting :: E.Posting -> [Maybe B.GlobalPosting]+globalPosting = sibs (B.pdGlobal . E.meta)++filePosting :: E.Posting -> [Maybe B.FilePosting]+filePosting = sibs (fmap B.pFilePosting . B.pdFileMeta . E.meta)++globalTransaction :: E.Posting -> [Maybe B.GlobalTransaction]+globalTransaction =+  map B.tlGlobal+  . map fst+  . E.unrollSnd+  . second (\(x, xs) -> (x:xs))+  . second E.tailEnts+  . E.unPosting
+ lib/Penny/Lincoln/Serial.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE DeriveGeneric #-}++module Penny.Lincoln.Serial (+  Serial, forward, backward, serialItems, serialSomeItems,+  serialNestedItems) where++import Control.Applicative (Applicative, (<*>), pure, (*>))+import Control.Monad (ap, liftM, replicateM_)+import Data.Traversable (Traversable)+import qualified Data.Traversable as Tr+import qualified Data.Foldable as Fdbl+import GHC.Generics (Generic)+import Data.Binary (Binary)++data SerialSt = SerialSt+  { nextFwd :: Int+  , nextBack :: Int+  } deriving Show+++data Serial = Serial+  { forward :: Int+  , backward :: Int+  } deriving (Eq, Show, Ord, Generic)++instance Binary Serial++newtype GenSerial a = GenSerial (SerialSt -> (a, SerialSt))++instance Functor GenSerial where+  fmap = liftM++instance Applicative GenSerial where+  pure = return+  (<*>) = ap++instance Monad GenSerial where+  return a = GenSerial $ \s -> (a, s)+  (GenSerial k) >>= f = GenSerial $ \s ->+    let (a, s') = k s+        GenSerial g = f a+    in g s'++incrementBack :: GenSerial ()+incrementBack = GenSerial $ \s ->+  let s' = SerialSt (nextFwd s) (nextBack s + 1)+  in ((), s')++getSerial :: GenSerial Serial+getSerial = GenSerial $ \s ->+  let s' = SerialSt (nextFwd s + 1) (nextBack s - 1)+  in (Serial (nextFwd s) (nextBack s), s')++makeSerials :: GenSerial a -> a+makeSerials (GenSerial k) =+  let (r, _) = k (SerialSt 0 0) in r++serialItems :: (Serial -> a -> b) -> [a] -> [b]+serialItems f as = zipWith f (nSerials (length as)) as++nSerials :: Int -> [Serial]+nSerials n =+  makeSerials $+  (sequence . replicate n $ incrementBack)+  *> (sequence . replicate n $ getSerial)++serialSomeItems+  :: (a -> Either b (Serial -> b))+  -> [a]+  -> [b]+serialSomeItems f as = makeSerials k+  where+    k = do+      let doIncr i = case f i of+            Left _ -> return ()+            Right _ -> incrementBack+      mapM_ doIncr as+      let addSer i = case f i of+            Left b -> return b+            Right add -> getSerial >>= return . add+      mapM addSer as++-- | Adds serials to items that are nested within other items.+serialNestedItems+  :: Traversable f+  => (a -> Either b ((f c), (Serial -> c -> d), (f d -> b)))+  -- ^ When applied to each item, this function returns Left if the+  -- item does not need a serial, or Right if it has items that need+  -- serials. In the Right is the container with items that need+  -- serials, the function that applies serials to each item, and a+  -- function to re-wrap the container with the serialed items.++  -> [a]+  -> [b]+serialNestedItems getEi as = makeSerials k+  where+    k = do+      serialNestedIncrBack getEi as+      mapM (serialNestedAddSerials getEi) as++-- | Increments the back serial by the needed number of items.+serialNestedIncrBack+  :: Fdbl.Foldable f+  => (a -> Either b (f c, x, y))+  -> [a]+  -> GenSerial ()+serialNestedIncrBack f = mapM_ doIncr where+  doIncr i = case f i of+    Left _ -> return ()+    Right (ctnr, _, _) ->+      let len = length . Fdbl.toList $ ctnr+      in replicateM_ len incrementBack++-- | Assigns serials to nested items.+serialNestedAddSerials+  :: Tr.Traversable f+  => (a -> Either b (f c, (Serial -> c -> d), f d -> b))+  -> a+  -> GenSerial b+serialNestedAddSerials f a = case f a of+  Left b -> return b+  Right (ctnr, addSer, rewrap) -> do+    let adder i = do+          s <- getSerial+          return $ addSer s i+    fmap rewrap $ Tr.mapM adder ctnr
+ lib/Penny/Shield.hs view
@@ -0,0 +1,106 @@+-- | 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),+  Runtime,+  environment,+  currentTime,+  output,+  screenLines,+  screenWidth,+  Term,+  term,+  runtime,+  termFromEnv,+  autoTerm)+  where++import Control.Applicative ((<$>), (<*>))+import qualified Data.Time as T+import System.Environment (getEnvironment)+import System.IO (hIsTerminalDevice, stdout)+import qualified System.Console.Rainbow as C++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 deriving (Eq, Ord, Show)++newtype Term = Term { unTerm :: String } deriving Show++-- | Information about the runtime environment.+data Runtime = Runtime { environment :: [(String, String)]+                       , currentTime :: B.DateTime+                       , output :: Output }++runtime :: IO Runtime+runtime = Runtime+          <$> getEnvironment+          <*> (toDT <$> T.getZonedTime)+          <*> findOutput+          where+            toDT t = case B.fromZonedTime t of+              Nothing -> error "time conversion error"+              Just ti -> ti++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++-- | Determines which Chunk Term to use based on the TERM environment+-- variable, regardless of whether standard output is a terminal. Uses+-- Dumb if TERM is not set.+termFromEnv :: Runtime -> C.Term+termFromEnv rt = case term rt of+  Just t -> C.TermName . unTerm $ t+  Nothing -> C.Dumb++-- | Determines which Chunk Term to use based on whether standard+-- output is a terminal. Uses Dumb if standard output is not a+-- terminal; otherwise, uses the TERM environment variable.+autoTerm :: Runtime -> C.Term+autoTerm rt = case output rt of+  IsTTY -> termFromEnv rt+  NotTTY -> C.Dumb+
+ lib/Penny/Steel.hs view
@@ -0,0 +1,3 @@+-- | Steel - independent Penny utilities++module Penny.Steel where
+ lib/Penny/Steel/NestedMap.hs view
@@ -0,0 +1,275 @@+-- | 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.Steel.NestedMap (+  NestedMap ( NestedMap, unNestedMap ),+  empty,+  relabel,+  descend,+  insert,+  cumulativeTotal,+  traverse,+  traverseWithTrail,+  toForest ) where++import Control.Applicative ((<*>), (<$>))+import Data.Map ( Map )+import qualified Data.Foldable as F+import qualified Data.Traversable as T+import qualified Data.Tree as E+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'))++-- | Convert a NestedMap to a Forest.+toForest :: Ord k => NestedMap k l -> E.Forest (k, l)+toForest = map toNode . M.assocs . unNestedMap+  where+    toNode (k, (l, m)) = E.Node (k, l) (toForest 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 ()
+ lib/Penny/Steel/Sums.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE DeriveGeneric #-}++-- | Anonymous sum types.++module Penny.Steel.Sums where++import Data.Binary (Binary)+import GHC.Generics (Generic)++data S3 a b c+  = S3a a+  | S3b b+  | S3c c+  deriving (Eq, Ord, Show, Generic)++instance (Binary a, Binary b, Binary c) => Binary (S3 a b c)++data S4 a b c d+  = S4a a+  | S4b b+  | S4c c+  | S4d d+  deriving (Eq, Ord, Show, Generic)++instance (Binary a, Binary b, Binary c, Binary d) => Binary (S4 a b c d)++partitionS3 :: [S3 a b c] -> ([a], [b], [c])+partitionS3 = foldr f ([], [], [])+  where+    f i (as, bs, cs) = case i of+      S3a a -> (a:as, bs, cs)+      S3b b -> (as, b:bs, cs)+      S3c c -> (as, bs, c:cs)++partitionS4 :: [S4 a b c d] -> ([a], [b], [c], [d])+partitionS4 = foldr f ([], [], [], [])+  where+    f i (as, bs, cs, ds) = case i of+      S4a a -> (a:as, bs, cs, ds)+      S4b b -> (as, b:bs, cs, ds)+      S4c c -> (as, bs, c:cs, ds)+      S4d d -> (as, bs, cs, d:ds)++caseS3 :: (a -> d) -> (b -> d) -> (c -> d) -> S3 a b c -> d+caseS3 fa fb fc s3 = case s3 of+  S3a a -> fa a+  S3b b -> fb b+  S3c c -> fc c++caseS4 :: (a -> e) -> (b -> e) -> (c -> e) -> (d -> e) -> S4 a b c d -> e+caseS4 fa fb fc fd s4 = case s4 of+  S4a a -> fa a+  S4b b -> fb b+  S4c c -> fc c+  S4d d -> fd d++mapS3 :: (a -> a1) -> (b -> b1) -> (c -> c1) -> S3 a b c -> S3 a1 b1 c1+mapS3 fa fb fc = caseS3 (S3a . fa) (S3b . fb) (S3c . fc)++mapS4+  :: (a -> a1) -> (b -> b1) -> (c -> c1) -> (d -> d1)+  -> S4 a b c d+  -> S4 a1 b1 c1 d1+mapS4 a b c d = caseS4 (S4a . a) (S4b . b) (S4c . c) (S4d . d)++mapS3a+  :: Functor f+  => (a -> f a1) -> (b -> f b1) -> (c -> f c1) -> S3 a b c -> f (S3 a1 b1 c1)+mapS3a a b c = caseS3 (fmap S3a . a) (fmap S3b . b) (fmap S3c . c)++mapS4a+  :: Functor f+  => (a -> f a1) -> (b -> f b1) -> (c -> f c1) -> (d -> f d1)+  -> S4 a b c d -> f (S4 a1 b1 c1 d1)++mapS4a a b c d = caseS4 (fmap S4a . a) (fmap S4b . b) (fmap S4c . c)+                        (fmap S4d . d)+
+ lib/Penny/Wheat.hs view
@@ -0,0 +1,367 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Wheat - Penny ledger tests+--+-- Wheat helps you build tests to check all the postings in your+-- ledger. Perhaps you want to make sure all the account names are+-- valid, or that your checking account has no unreconciled+-- transactions. With Wheat you can easily build a command line+-- program that will check all the postings in a ledger for you+-- against criteria that you specify.++module Penny.Wheat+  ( -- * Configuration+    WheatConf(..)++    -- * Tests+  , eachPostingMustBeTrue+  , atLeastNPostings++    -- * Convenience functions+  , futureFirstsOfTheMonth++    -- * Running tests+  , main+  ) where++import Control.Monad (when)+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Either (partitionEithers)+import Data.Maybe (mapMaybe)+import qualified Penny.Copper as Cop+import qualified Penny.Copper.Parsec as CP+import qualified Penny.Lincoln as L+import qualified Penny.Liberty as Ly+import qualified Data.Text as X+import qualified Data.Time as Time+import qualified Text.Matchers as M+import qualified Text.Parsec as Parsec+import qualified System.Exit as Exit+import qualified System.IO as IO+import qualified Penny.Shield as S+import qualified Penny.Steel.Sums as Su++import qualified Data.Version as V+import qualified Data.Prednote.TestTree as TT+import qualified Data.Prednote.Pdct as Pe+import qualified System.Console.Rainbow as Rb+import qualified System.Console.MultiArg as MA+import System.Locale (defaultTimeLocale)++------------------------------------------------------------+-- Other conveniences+------------------------------------------------------------+++-- | A non-terminating list of starting with the first day of the+-- first month following the given day, followed by successive first+-- days of the month.+futureFirstsOfTheMonth :: Time.Day -> [Time.Day]+futureFirstsOfTheMonth d = iterate (Time.addGregorianMonthsClip 1) d1+  where+    d1 = Time.fromGregorian yr mo 1+    (yr, mo, _) = Time.toGregorian $ Time.addGregorianMonthsClip 1 d++------------------------------------------------------------+-- CLI+------------------------------------------------------------++-- | Record holding all data to configure Wheat.+data WheatConf = WheatConf+  { briefDescription :: String+    -- ^ This is displayed at the beginning of the online help. It+    -- should be a one-line description of what this program does--for+    -- example, what it checks for.++  , moreHelp :: [String]+    -- ^ Displayed at the end of the online help. It should be a list+    -- of lines, wich each line not terminated by a newline+    -- character. It is displayed at the end of the online help.++  , tests :: [Time.UTCTime -> TT.TestTree L.Posting]+    -- ^ The actual tests to run. The UTCTime is the @base time@. Each+    -- test may decide what to do with the base time--for example, the+    -- test might say that all postings have to have a date on or+    -- before that date. Or the test might just ignore the base time.++  , indentAmt :: Pe.IndentAmt+    -- ^ How many spaces to indent each level in a tree of tests.++  , passVerbosity :: TT.Verbosity+    -- ^ Verbosity for tests that pass++  , failVerbosity :: TT.Verbosity+    -- ^ Verbosity for tests that fail++  , groupPred :: TT.Name -> Bool+    -- ^ Group names are filtered with this function; a group is only+    -- run if this function returns True.++  , testPred :: TT.Name -> Bool+    -- ^ Test names are filtered with this function; a test is only+    -- run if this function returns True.++  , showSkippedTests :: Bool+    -- ^ Some tests might be skipped; see 'testPred'. This controls+    -- whether you want to see a notification of tests that were+    -- skipped. (Does not affect skipped groups; see 'groupVerbosity'+    -- for that.)++  , groupVerbosity :: TT.GroupVerbosity+    -- ^ Show group names? Even if you do not show the names of+    -- groups, tests within the group will still be indented.++  , stopOnFail :: Bool+    -- ^ If True, then tests will stop running immediately after a+    -- single test fails. If False, all tests are always run.++  , colorToFile :: Bool+    -- ^ Use colors even if stdout is not a file?++  , baseTime :: Time.UTCTime+    -- ^ Tests may use this date and time as they wish; see+    -- 'tests'. Typically you will set this to the current instant.++  , ledgers :: [String]+    -- ^ Ledger files to read in from disk.+  }++data Parsed = Parsed+  { p_indentAmt :: Pe.IndentAmt+  , p_passVerbosity :: TT.Verbosity+  , p_failVerbosity :: TT.Verbosity+  , p_groupPred :: TT.Name -> Bool+  , p_testPred :: TT.Name -> Bool+  , p_showSkippedTests :: Bool+  , p_groupVerbosity :: TT.GroupVerbosity+  , p_stopOnFail :: Bool+  , p_colorToFile :: Bool+  , p_baseTime :: Time.UTCTime+  , p_help :: Bool+  , p_ledgers :: [String]+  }++parseBaseTime :: String -> Ex.Exceptional MA.InputError Time.UTCTime+parseBaseTime s = case Parsec.parse CP.dateTime  "" (X.pack s) of+  Left e -> Ex.throw (MA.ErrorMsg $ "could not parse date: " ++ show e)+  Right g -> return . L.toUTC $ g++parseRegexp :: String -> Ex.Exceptional MA.InputError (TT.Name -> Bool)+parseRegexp s = case M.pcre M.Sensitive (X.pack s) of+  Ex.Exception e -> Ex.throw . MA.ErrorMsg $+    "could not parse regular expression: " ++ X.unpack e+  Ex.Success m -> return . M.match $ m++parseArg :: String -> Parsed -> Parsed+parseArg s p = p { p_ledgers = p_ledgers p ++ [s] }++allOpts :: [MA.OptSpec (Parsed -> Parsed)]+allOpts =+  let allChoices =+        [ ("silent", \p -> p { p_failVerbosity = TT.Silent })+        , ("minimal", \p -> p { p_failVerbosity = TT.PassFail })+        , ("false", \p -> p { p_failVerbosity = TT.FalseSubjects })+        , ("true", \p -> p { p_failVerbosity = TT.TrueSubjects })+        , ("all", \p -> p { p_failVerbosity = TT.Discards })+        ] in+  [ MA.OptSpec ["indentation"] "i"+    (fmap (\i p -> p { p_indentAmt = i }) (MA.OneArgE MA.reader))++  , MA.OptSpec ["pass-verbosity"] "p" $ MA.ChoiceArg allChoices++  , MA.OptSpec ["fail-verbosity"] "f" $ MA.ChoiceArg allChoices++  , MA.OptSpec ["group-regexp"] "g"+    (fmap (\f p -> p { p_groupPred = f }) (MA.OneArgE parseRegexp))++  , MA.OptSpec ["test-regexp"] "t"+    (fmap (\f p -> p { p_testPred = f }) (MA.OneArgE parseRegexp))++  , MA.OptSpec ["show-skipped-tests"] ""+    ( MA.NoArg (\p -> p { p_showSkippedTests+                          = not (p_showSkippedTests p) }))++  , MA.OptSpec ["group-verbosity"] "G" $ MA.ChoiceArg+    [ ("silent", \p -> p { p_groupVerbosity = TT.NoGroups })+    , ("active", \p -> p { p_groupVerbosity = TT.ActiveGroups })+    , ("all", \p -> p { p_groupVerbosity = TT.AllGroups })+    ]++  , MA.OptSpec ["stop-on-failure"] ""+    ( MA.NoArg (\p -> p { p_stopOnFail+                          = not (p_stopOnFail p) }))++  , MA.OptSpec ["color-to-file"] ""+    ( MA.NoArg (\p -> p { p_colorToFile+                          = not (p_colorToFile p) }))++  , MA.OptSpec ["base-date"] ""+    (fmap (\d p -> p { p_baseTime = d }) (MA.OneArgE parseBaseTime))+  ]++getTTOpts :: [a] -> Parsed -> TT.TestOpts a+getTTOpts as o = TT.TestOpts+  { TT.tIndentAmt = p_indentAmt o+  , TT.tPassVerbosity = p_passVerbosity o+  , TT.tFailVerbosity = p_failVerbosity o+  , TT.tGroupPred = p_groupPred o+  , TT.tTestPred = p_testPred o+  , TT.tShowSkippedTests = p_showSkippedTests o+  , TT.tGroupVerbosity = p_groupVerbosity o+  , TT.tSubjects = as+  , TT.tStopOnFail = p_stopOnFail o+  }++-- | Runs Wheat tests. Prints the result to standard output. Exits+-- unsuccessfully if the user gave bad command line options or if at+-- least a single test failed; exits successfully if all tests+-- succeeded. Shows the version number and exits successfully if that+-- was requested.+main+  :: V.Version+  -- ^ Version of the binary+  -> (S.Runtime -> WheatConf) -> IO ()+main ver getWc = do+  rt <- S.runtime+  let wc = getWc rt+  parsed <- MA.simpleWithHelp (help wc) MA.Intersperse+         (fmap Left (Ly.version ver) : (map (fmap Right) allOpts))+         (return . (fmap Right parseArg))+  let (showVers, fns) = partitionEithers parsed+  case showVers of+    [] -> return ()+    x:_ -> x+  let fn = foldl (flip (.)) id fns+      psd = fn (getParsedFromWheatConf wc)+  term <- Rb.smartTermFromEnv (p_colorToFile psd) IO.stdout+  pfs <- getItems (p_ledgers psd)+  let ttOpts = getTTOpts pfs psd+      tts = zipWith ($) (tests wc) (repeat (p_baseTime psd))+      (cks, _, nFail) = TT.runTests ttOpts 0 tts+  Rb.printChunks term cks+  when (nFail > 0) Exit.exitFailure++getParsedFromWheatConf :: WheatConf -> Parsed+getParsedFromWheatConf w = Parsed+  { p_indentAmt = indentAmt w+  , p_passVerbosity = passVerbosity w+  , p_failVerbosity = failVerbosity w+  , p_groupPred = groupPred w+  , p_testPred = testPred w+  , p_showSkippedTests = showSkippedTests w+  , p_groupVerbosity = groupVerbosity w+  , p_stopOnFail = stopOnFail w+  , p_colorToFile = colorToFile w+  , p_baseTime = baseTime w+  , p_help = False+  , p_ledgers = ledgers w+  }++getItems :: [String] -> IO [L.Posting]+getItems ss = fmap f $ Cop.open ss+  where+    f = concatMap L.transactionToPostings+        . mapMaybe ( let cn = const Nothing+                     in Su.caseS4 Just cn cn cn)++--+-- Tests+--++-- | Passes only if each posting is True.+eachPostingMustBeTrue+  :: TT.Name+  -> Pe.Pdct L.Posting+  -> TT.TestTree L.Posting+eachPostingMustBeTrue n = TT.eachSubjectMustBeTrue n L.display++-- | Passes if at least a particular number of postings is True.+atLeastNPostings+  :: Int+  -- ^ The number of postings that must be true for the test to pass+  -> TT.Name+  -> Pe.Pdct L.Posting+  -> TT.TestTree L.Posting+atLeastNPostings i n = TT.nSubjectsMustBeTrue n L.display i++--+-- Help+--++help+  :: WheatConf+  -> String+  -- ^ Program name+  -> String+help wc pn = unlines+  [ "usage: " ++ pn ++ " [options] [FILE...]"+  , ""+  , briefDescription wc+  , ""+  , "Options:"+  , "  -i, --indentation AMT"+  , "    Indent each level by this many spaces"+  , "    " ++ dflt (show . indentAmt $ wc)+  , "  -p, --pass-verbosity VERBOSITY"+  , "    Verbosity for tests that pass. Argument may be:"+  , "      silent - show nothing at all"+  , "      minimal - show whether the test passed or failed"+  , "      false - show subjects that are false"+  , "      true - show subjects that are true or false"+  , "      all - show all subjects"+  , "      " ++ dflt (showVerbosity . passVerbosity $ wc)+  , "  -f, --fail-verbosity VERBOSITY"+  , "    Verbosity for tests that fail."+  , "    (uses same VERBOSITY options as --pass-verbosity)"+  , "    " ++ dflt (showVerbosity . failVerbosity $ wc)+  , "  -g, --group-regexp REGEXP"+  , "    Run only groups whose name matches the given"+  , "    Perl-compatible regular expression"+  , "    (overrides the compiled-in default)"+  , "  -t, --test-regexp REGEXP"+  , "    Run only tests whose name matches the given"+  , "    Perl-compatible regular expression"+  , "    (overrides the compiled-in default)"+  , "  --show-skipped-tests"+  , "    Toggle whether to show tests that are skipped"+  , "    using the --test-regexp option"+  , "    (does not affect groups that are skipped; see next option)"+  , "    " ++ dflt (show . showSkippedTests $ wc)+  , "  --G, group-verbosity ARG"+  , "    Control which group names are shown. Argument may be:"+  , "      silent - do not show any group names"+  , "      active - show group names that were not skipped"+  , "      all - show all group names, including skipped ones"+  , "      " ++ dflt (showGroupVerbosity . groupVerbosity $ wc)+  , "  --stop-on-failure"+  , "    Stop running tests after a single test fails"+  , "    " ++ dflt (show . stopOnFail $ wc)+  , "  --color-to-file"+  , "    Use color even when standard output is not a terminal"+  , "    " ++ dflt (show . colorToFile $ wc)+  , "  --base-date DATE"+  , "    Use this date as a basis for checks"+  , "    " ++ dflt ( Time.formatTime defaultTimeLocale "%c"+                     . baseTime $ wc)+  , ""+  ]+  ++ unlines (moreHelp wc)++dflt :: String -> String+dflt s = "(default: " ++ s ++ ")"++showVerbosity :: TT.Verbosity -> String+showVerbosity v = case v of+  TT.Silent -> "silent"+  TT.PassFail -> "minimal"+  TT.FalseSubjects -> "false"+  TT.TrueSubjects -> "true"+  TT.Discards -> "all"++showGroupVerbosity :: TT.GroupVerbosity -> String+showGroupVerbosity v = case v of+  TT.NoGroups -> "silent"+  TT.ActiveGroups -> "active"+  TT.AllGroups -> "all"++
+ lib/Penny/Zinc.hs view
@@ -0,0 +1,746 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Zinc - the Penny command-line interface+module Penny.Zinc+  ( Defaults(..)+  , ColorToFile(..)+  , Matcher(..)+  , SortField(..)+  , runZinc+  ) where++import qualified Penny.Cabin.Interface as I+import qualified Penny.Cabin.Parsers as P+import qualified Penny.Cabin.Scheme as E+import qualified Penny.Cabin.Scheme.Schemes as Schemes+import qualified Penny.Copper as C+import qualified Penny.Liberty as Ly+import qualified Data.Prednote.Expressions as X+import qualified Data.Prednote.Pdct as Pe+import qualified Penny.Lincoln as L+import qualified Penny.Lincoln.Queries as Q+import qualified Penny.Shield as S+import qualified Penny.Steel.Sums as Su++import Control.Applicative ((<*>), pure, (<$))+import Control.Monad (join)+import qualified Control.Monad.Trans.State as St+import qualified Control.Monad.Exception.Synchronous as Ex+import Data.Char (toUpper, toLower)+import Data.Either (partitionEithers)+import Data.List (isPrefixOf)+import Data.Maybe (mapMaybe, catMaybes, fromMaybe)+import Data.Monoid (mappend, mconcat, (<>))+import Data.Ord (comparing)+import Data.Text (Text, pack)+import Data.Version (Version)+import qualified Data.Text.IO as TIO+import qualified System.Console.MultiArg as MA+import qualified System.Exit as Exit+import qualified System.IO as IO+import qualified Text.Matchers as M+import qualified System.Console.Rainbow as R++runZinc+  :: Version+  -- ^ Version of the executable+  -> Defaults+  -> S.Runtime+  -> [I.Report]+  -> IO ()+runZinc ver df rt rs = do+  let ord = sortPairsToFn . sorter $ df+      hlp = helpText df rt rs+  join $ MA.modesWithHelp hlp (allOpts ver (S.currentTime rt) df)+    (processGlobal rt ord df rs)+++-- | Whether to use color when standard output is not a terminal.+newtype ColorToFile = ColorToFile { unColorToFile :: Bool }+  deriving (Eq, Show)++data Matcher+  = Within+  | Exact+  | TDFA+  | PCRE+  deriving (Eq, Show)++data SortField+  = Payee+  | Date+  | Flag+  | Number+  | Account+  | DrCr+  | Qty+  | Commodity+  | PostingMemo+  | TransactionMemo+  deriving (Eq, Show, Ord)++data Defaults = Defaults+  { sensitive :: M.CaseSensitive+  , matcher :: Matcher+  , colorToFile :: ColorToFile+  , defaultScheme :: Maybe E.Scheme+    -- ^ If Nothing, no default scheme. If the user does not pick a+    -- scheme, no colors are used.+  , moreSchemes :: [E.Scheme]+  , sorter :: [(SortField, P.SortOrder)]+    -- ^ For example, to sort by date and then by payee if the dates+    -- are equal, use+    --+    -- > [(Date, Ascending), (Payee, Ascending)]++  , exprDesc :: X.ExprDesc+  }++sortPairToFn :: (SortField, P.SortOrder) -> Orderer+sortPairToFn (s, d) = if d == P.Descending then flipOrder r else r+  where+    r = case s of+      Payee -> comparing Q.payee+      Date -> comparing Q.dateTime+      Flag -> comparing Q.flag+      Number -> comparing Q.number+      Account -> comparing Q.account+      DrCr -> comparing Q.drCr+      Qty -> comparing Q.qty+      Commodity -> comparing Q.commodity+      PostingMemo -> comparing Q.postingMemo+      TransactionMemo -> comparing Q.transactionMemo++descPair :: (SortField, P.SortOrder) -> String+descPair (i, d) = desc ++ ", " ++ dir+  where+    dir = case d of+      P.Ascending -> "ascending"+      P.Descending -> "descending"+    desc = case show i of+      [] -> []+      x:xs -> toLower x : xs++descSortList :: [(SortField, P.SortOrder)] -> [String]+descSortList ls = case ls of+  [] -> ["    No sorting performed by default"]+  x:xs -> descFirst x : map descRest xs++descFirst :: (SortField, P.SortOrder) -> String+descFirst p = "  Default sort order: " ++ descPair p++descRest :: (SortField, P.SortOrder) -> String+descRest p = "    then: " ++ descPair p++sortPairsToFn :: [(SortField, P.SortOrder)] -> Orderer+sortPairsToFn = mconcat . map sortPairToFn++--+-- ## Option parsing+--++--+-- ## OptResult, and functions dealing with it+--+newtype ShowExpression = ShowExpression Bool+  deriving (Show, Eq)++newtype VerboseFilter = VerboseFilter Bool+  deriving (Show, Eq)++type Error = Text++data OptResult+  = ROperand (M.CaseSensitive+             -> Ly.MatcherFactory+             -> Ex.Exceptional Ly.Error Ly.Operand)+  | RPostFilter (Ex.Exceptional Ly.Error Ly.PostFilterFn)+  | RMatcherSelect Ly.MatcherFactory+  | RCaseSelect M.CaseSensitive+  | ROperator (X.Token L.Posting)+  | RSortSpec (Ex.Exceptional Error Orderer)+  | RColorToFile ColorToFile+  | RScheme E.Changers+  | RExprDesc X.ExprDesc+  | RShowExpression+  | RVerboseFilter+  | RShowVersion (IO ())++getPostFilters+  :: [OptResult]+  -> Ex.Exceptional Ly.Error [Ly.PostFilterFn]+getPostFilters =+  sequence+  . mapMaybe f+  where+    f o = case o of+      RPostFilter pf -> Just pf+      _ -> Nothing++getExprDesc+  :: Defaults+  -> [OptResult]+  -> X.ExprDesc+getExprDesc df os = case mapMaybe f os of+  [] -> exprDesc df+  xs -> last xs+  where+    f (RExprDesc d) = Just d+    f _ = Nothing++getSortSpec+  :: Orderer+  -> [OptResult]+  -> Ex.Exceptional Error Orderer+getSortSpec i ls =+  let getSpec o = case o of+        RSortSpec x -> Just x+        _ -> Nothing+      exSpecs = mapMaybe getSpec ls+  in if null exSpecs+     then return i+     else fmap mconcat . sequence $ exSpecs++getShowVersion :: [OptResult] -> Maybe (IO ())+getShowVersion ls = case mapMaybe f ls of+  [] -> Nothing+  xs -> Just $ last xs+  where+    f o = case o of { RShowVersion i -> Just i; _ -> Nothing }++type Factory = M.CaseSensitive+             -> Text -> Ex.Exceptional Text M.Matcher++makeToken+  :: OptResult+  -> St.State (M.CaseSensitive, Factory)+              (Maybe (Ex.Exceptional Ly.Error (X.Token L.Posting)))+makeToken o = case o of+  ROperand f -> do+    (s, fty) <- St.get+    let g = fmap X.operand (f s fty)+    return (Just g)+  RMatcherSelect f -> do+    (c, _) <- St.get+    St.put (c, f)+    return Nothing+  RCaseSelect c -> do+    (_, f) <- St.get+    St.put (c, f)+    return Nothing+  ROperator t -> return . Just . return $ t+  _ -> return Nothing+++makeTokens+  :: Defaults+  -> [OptResult]+  -> Ex.Exceptional Ly.Error ( [X.Token L.Posting]+                             , (M.CaseSensitive, Factory) )+makeTokens df os =+  let initSt = (sensitive df, fty)+      fty = case matcher df of+        Within -> \c t -> return (M.within c t)+        Exact -> \c t -> return (M.exact c t)+        TDFA -> M.tdfa+        PCRE -> M.pcre+      lsSt = mapM makeToken os+      (ls, st') = St.runState lsSt initSt+  in fmap (\xs -> (xs, st')) . sequence . catMaybes $ ls+++allOpts :: Version -> L.DateTime -> Defaults -> [MA.OptSpec OptResult]+allOpts ver dt df =+  map (fmap ROperand) (Ly.operandSpecs dt)+  ++ [fmap RPostFilter . fst $ Ly.postFilterSpecs]+  ++ [fmap RPostFilter . snd $ Ly.postFilterSpecs]+  ++ map (fmap RMatcherSelect) Ly.matcherSelectSpecs+  ++ map (fmap RCaseSelect) Ly.caseSelectSpecs+  ++ map (fmap ROperator) Ly.operatorSpecs+  ++ [fmap RSortSpec sortSpecs]+  ++ [ optColorToFile ]+  ++ let ss = moreSchemes df+     in (if not . null $ ss then [optScheme ss] else [])+  ++ map (fmap RExprDesc) Ly.exprDesc+  ++ [ RShowExpression <$ Ly.showExpression+     , RVerboseFilter <$ Ly.verboseFilter+     , fmap RShowVersion (Ly.version ver)+     ]++optColorToFile :: MA.OptSpec OptResult+optColorToFile = MA.OptSpec ["color-to-file"] "" (MA.ChoiceArg ls)+  where+    ls = [ ("yes", RColorToFile $ ColorToFile True)+         , ("no", RColorToFile $ ColorToFile False) ]++getColorToFile :: Defaults -> [OptResult] -> ColorToFile+getColorToFile d ls =+  case mapMaybe getOpt ls of+    [] -> colorToFile d+    xs -> last xs+  where+    getOpt o = case o of+      RColorToFile c -> Just c+      _ -> Nothing++optScheme :: [E.Scheme] -> MA.OptSpec OptResult+optScheme ss = MA.OptSpec ["scheme"] "" (MA.ChoiceArg ls)+  where+    ls = map f ss+    f (E.Scheme n _ s) = (n, RScheme s)++getScheme :: Defaults -> [OptResult] -> Maybe E.Changers+getScheme d ls =+  case mapMaybe getOpt ls of+    [] -> fmap E.changers $ defaultScheme d+    xs -> Just $ last xs+  where+    getOpt o = case o of+      RScheme s -> Just s+      _ -> Nothing++getShowExpression :: [OptResult] -> ShowExpression+getShowExpression ls = case mapMaybe f ls of+  [] -> ShowExpression False+  _ -> ShowExpression True+  where+    f o = case o of { RShowExpression -> Just (); _ -> Nothing }++getVerboseFilter :: [OptResult] -> VerboseFilter+getVerboseFilter ls = case mapMaybe f ls of+  [] -> VerboseFilter False+  _ -> VerboseFilter True+  where+    f o = case o of { RVerboseFilter -> Just (); _ -> Nothing }++-- | Indicates the result of a successful parse of filtering options.+data FilterOpts = FilterOpts+  { foResultFactory :: Factory+    -- ^ The factory indicated, so that it can be used in+    -- subsequent parses of the same command line.++  , foResultSensitive :: M.CaseSensitive+    -- ^ Indicated case sensitivity, so that it can be used in+    -- subsequent parses of the command line.++  , foSorterFilterer :: [L.Transaction]+                    -> ([R.Chunk], [(Ly.LibertyMeta, L.Posting)])+    -- ^ Applied to a list of Transaction, will sort and filter+    -- the transactions and assign them LibertyMeta.++  , foTextSpecs :: Maybe E.Changers++  , foColorToFile :: ColorToFile+  , foExprDesc :: X.ExprDesc+  , foPredicate :: Pe.Pdct L.Posting+  , foShowExpression :: ShowExpression+  , foVerboseFilter :: VerboseFilter+  }++processGlobal+  :: S.Runtime+  -> Orderer+  -> Defaults+  -> [I.Report]+  -> [OptResult]+  -> Either (a -> IO ()) [MA.Mode (IO ())]+processGlobal rt srt df rpts os+  = case processFiltOpts srt df os of+      Ex.Exception s -> Left $ (const $ handleTextError s)+      Ex.Success mayFo -> case mayFo of+        Left i -> Left . const $ i+        Right fo -> Right $ map (makeMode rt fo) rpts++processFiltOpts+  :: Orderer+  -> Defaults+  -> [OptResult]+  -> Ex.Exceptional Error (Either (IO ()) FilterOpts)+  -- ^ Left if the user asked to see the version; Right with the+  -- FilterOpts otherwise.+processFiltOpts ord df os = case getShowVersion os of+  Just i -> return $ Left i+  Nothing -> do+    postFilts <- getPostFilters os+    sortSpec <- getSortSpec ord os+    (toks, (rs, rf)) <- makeTokens df os+    let ctf = getColorToFile df os+        sch = getScheme df os+        expDsc = getExprDesc df os+        showExpr = getShowExpression os+        verbFilt = getVerboseFilter os+    pdct <- Ly.parsePredicate expDsc toks+    let sf = Ly.xactionsToFiltered pdct postFilts sortSpec+    return . Right $ FilterOpts rf rs sf sch+                                ctf expDsc pdct showExpr verbFilt++makeMode+  :: S.Runtime+  -> FilterOpts+  -> I.Report+  -> MA.Mode (IO ())+makeMode rt fo r = fmap makeIO mode+  where+    mode = snd (r rt) (foResultSensitive fo) (foResultFactory fo)+           (fromMaybe Schemes.plainLabels . foTextSpecs $ fo)+           (foExprDesc fo) (fmap snd (foSorterFilterer fo))+    makeIO parseResult = do+      (posArgs, printRpt) <-+        Ex.switch handleTextError return parseResult+      (txns, pps) <- fmap splitLedger $ C.open posArgs+      let term = if unColorToFile (foColorToFile fo)+                 then S.termFromEnv rt+                 else S.autoTerm rt+          printer = R.printChunks term+          verbFiltChunks = fst . foSorterFilterer fo $ txns+      showFilterExpression printer (foShowExpression fo) (foPredicate fo)+      showVerboseFilter printer (foVerboseFilter fo) verbFiltChunks+      Ex.switch handleTextError (R.printChunks term)+        $ printRpt txns pps+++handleTextError :: Text -> IO a+handleTextError x = do+  pn <- MA.getProgName+  TIO.hPutStr IO.stderr $ (pack pn) <> ": error: " <> x+  Exit.exitFailure++indentAmt :: Pe.IndentAmt+indentAmt = 4++blankLine :: R.Chunk+blankLine = R.plain "\n"++showFilterExpression+  :: ([R.Chunk] -> IO ())+  -> ShowExpression+  -> Pe.Pdct L.Posting+  -> IO ()+showFilterExpression ptr (ShowExpression se) pdct =+  if not se+  then return ()+  else ptr $ info : blankLine :+             (Pe.showPdct indentAmt 0 pdct ++ [blankLine])+  where+    info = R.plain "Posting filter expression:\n"++showVerboseFilter+  :: ([R.Chunk] -> IO ())+  -> VerboseFilter+  -> [R.Chunk]+  -> IO ()+showVerboseFilter ptr (VerboseFilter vb) cks =+  if not vb+  then return ()+  else ptr $ info : blankLine : (cks ++ [blankLine])+  where+    info = R.plain "Filtering information:\n"++-- | Splits a Ledger into its Transactions and PricePoints.+splitLedger :: [C.LedgerItem] -> ([L.Transaction], [L.PricePoint])+splitLedger = partitionEithers . mapMaybe toEither+  where+    toEither = Su.caseS4 (Just  . Left) (Just . Right)+                         (const Nothing) (const Nothing)++helpText+  :: Defaults+  -> S.Runtime+  -> [I.Report]+  -> String+  -> String+helpText df rt pairMakers pn =+  mappend (help df pn) . mconcat . map addHdr . fmap fst $ pairs+  where+    pairs = pairMakers <*> pure rt+    addHdr s = hdr ++ s+    hdr = unlines [ "", replicate 50 '=' ]+++------------------------------------------------------------+-- ## Sorting+------------------------------------------------------------++-- The monoid instance of Ordering takes the first non-EQ item. For+-- example:+--+-- mconcat [EQ, LT, GT] == LT.+--+-- If b is a monoid, then (a -> b) is also a monoid. Therefore (a -> a+-- -> Ordering) is also a monoid. So for example to compare the first+-- element of a pair and then by the second element only if the first+-- element is equal:+--+-- mconcat [comparing fst, comparing snd]++type Orderer = L.Posting -> L.Posting -> Ordering++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", comparing Q.payee)+  , ("date", comparing Q.dateTime)+  , ("flag", comparing Q.flag)+  , ("number", comparing Q.number)+  , ("account", comparing Q.account)+  , ("drCr", comparing Q.drCr)+  , ("qty", comparing Q.qty)+  , ("commodity", comparing Q.commodity)+  , ("postingMemo", comparing Q.postingMemo)+  , ("transactionMemo", comparing Q.transactionMemo) ]++ords :: [(String, Orderer)]+ords = ordPairs ++ uppers ++ [none] where+  uppers = map toReversed ordPairs+  toReversed (s, f) =+    (capitalizeFirstLetter s, flipOrder f)+  none = ("none", const . const $ EQ)+++-- | True if the first argument matches the second argument. The match+-- on the first letter is case sensitive; the match on the other+-- letters is not case sensitive. True if both strings are empty.+argMatch :: String -> String -> Bool+argMatch s1 s2 = case (s1, s2) of+  (x:xs, y:ys) ->+    (x == y) && ((map toUpper xs) `isPrefixOf` (map toUpper ys))+  _ -> True++sortSpecs :: MA.OptSpec (Ex.Exceptional Error Orderer)+sortSpecs = MA.OptSpec ["sort"] ['s'] (MA.OneArg f)+  where+    f a =+      let matches = filter (\p -> a `argMatch` (fst p)) ords+      in case matches of+        x:[] -> return $ snd x+        _ -> Ex.throw $ "bad sort specification: " <> pack a <> "\n"++++------------------------------------------------------------+-- ## Help+------------------------------------------------------------++help :: Defaults -> String -> String+help d pn = unlines $+  [ "usage: " ++ pn ++ " [posting filters] report [report options] file . . ."+  , ""+  , "Posting filters"+  , "------------------------------------------"+  , ""+  , "Dates"+  , "-----"+  , ""+  , "-d, --date 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"+  , "-n, --number pattern"+  , "  Number must match pattern"+  , "-f, --flag pattern"+  , "  Flag must match pattern"+  , "-y, --commodity pattern"+  , "  Pattern must match commodity name"+  , "--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"+  , "-q, --qty cmp number"+  , "  Entry quantity must fall within given range"+  , "--filename pattern"+  , "  Filename of posting must match pattern"+  , ""+  , "Filtering based upon sibling postings"+  , "-------------------------------------"+  , "--s-globalPosting"+  , "--s-revGlobalPosting"+  , "--s-filePosting"+  , "--s-revFilePosting"+  , "--s-account"+  , "--s-account-level"+  , "--s-account-any"+  , "--s-payee"+  , "--s-tag"+  , "--s-number"+  , "--s-flag"+  , "--s-commodity"+  , "--s-posting-memo"+  , "--s-debit"+  , "--s-credit"+  , "--s-qty"+  , ""+  , "Options affecting patterns"+  , "--------------------------"+  , ""++  , "-i, --case-insensitive"+  , "  Be case insensitive"+    ++ ifDefault (sensitive d == M.Insensitive)++  , "-I, --case-sensitive"+  , "  Be case sensitive"+    ++ ifDefault (sensitive d == M.Sensitive)++  , ""++  , "-w, --within"+  , "  Use \"within\" matcher"+    ++ ifDefault (matcher d == Within)++  , "-r, --pcre"+  , "  Use \"pcre\" matcher"+    ++ ifDefault (matcher d == PCRE)++  , "--posix"+  , "  Use \"posix\" matcher"+    ++ ifDefault (matcher d == TDFA)++  , "-x, --exact"+  , "  Use \"exact\" matcher"+    ++ ifDefault (matcher d == Exact)+  , ""+  , "Infix or RPN selection"+  , "----------------------"+  , "--infix - use infix notation"+    ++ ifDefault (exprDesc d == X.Infix)+  , "--rpn - use reverse polish notation"+    ++ ifDefault (exprDesc d == X.RPN)+  , ""+  , "Infix Operators - from highest to lowest precedence"+  , "(all are left associative)"+  , "--------------------------"+  , "--open expr --close"+  , "-( expr -)"+  , "  Force precedence (as in \"open\" and \"close\" parentheses)"+  , "--not, -N expr"+  , "  True if expr is false"+  , "expr1 --and expr2"+  , "expr -A expr2"+  , "  True if expr and expr2 are both true"+  , "expr1 --or expr2"+  , "expr1 -O expr2"+  , "  True if either expr1 or expr2 is true"+  , ""+  , "RPN Operators"+  , "-------------"+  , "--not, -N"+  , "--and, -A"+  , "--or, -O"+  , "  RPN counterparts to the infix operators"+  , "  are postfix and manipulate the RPN stack accordingly"+  , ""+  , "Showing expressions"+  , "-------------------"+  , "--show-expression"+  , "  Show the parsed filter expression"+  , "--verbose-filter"+  , "  Verbosely show filtering results"+  , ""+  , "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."+  , "  (use \"none\" to leave postings in ledger file order)"+  , ""+  ] ++ descSortList (sorter d) +++  [ ""+  , "Colors"+  , "------"+  , "default scheme:"+  ,  maybe "    (none)" descScheme (defaultScheme d)+  , ""+  ]+  ++ let schs = moreSchemes d+     in (if not . null $ schs+        then+          [ "--scheme SCHEME_NAME"+          , "  use color scheme for report. Available schemes:"+          ] ++ map descScheme schs+        else [])+  +++  [ ""+  , "--color-to-file no|yes"+  , "  Whether to use color when standard output is not a"+  , "  terminal (default: " +++    if unColorToFile . colorToFile $ d then "yes)" else "no)"+  , ""+  , "Meta"+  , "----"+  , "--help, -h - show this help and exit"+  , "--version - show version and exit"+  ]+++descScheme :: E.Scheme -> String+descScheme (E.Scheme n d _) = "    " ++ n ++ " - " ++ d++-- | The string @ (default)@ if the condition is True; otherwise,+-- nothing.+ifDefault :: Bool -> String+ifDefault b = if b then " (default)" else ""
penny-lib.cabal view
@@ -1,5 +1,5 @@ Name: penny-lib-Version: 0.12.0.0+Version: 0.14.0.0 Cabal-version: >=1.8 Build-Type: Simple License: BSD3@@ -39,13 +39,16 @@ Library   Build-depends:       base ==4.*+    , action-permutations ==0.0.0.0+    , binary ==0.7.*     , bytestring ==0.10.*     , cereal ==0.3.*     , containers ==0.5.*     , explicit-exception ==0.1.*     , matchers ==0.6.*     , monad-loops ==0.3.*-    , multiarg ==0.14.*+    , multiarg ==0.16.*+    , ofx ==0.2.*     , old-locale ==1.0.*     , parsec >= 3.1.2 && < 3.2     , pcre-light ==0.4.*@@ -63,12 +66,12 @@   Exposed-modules:       Penny     , Penny.Brenner-    , Penny.Brenner.Amex-    , Penny.Brenner.BofA     , Penny.Brenner.Clear     , Penny.Brenner.Database     , Penny.Brenner.Import+    , Penny.Brenner.Info     , Penny.Brenner.Merge+    , Penny.Brenner.OFX     , Penny.Brenner.Print     , Penny.Brenner.Types     , Penny.Brenner.Util@@ -101,10 +104,10 @@     , Penny.Cabin.Scheme.Schemes     , Penny.Cabin.TextFormat     , Penny.Copper+    , Penny.Copper.Interface     , Penny.Copper.Parsec     , Penny.Copper.Render     , Penny.Copper.Terminals-    , Penny.Copper.Types     , Penny.Liberty     , Penny.Lincoln     , Penny.Lincoln.Balance@@ -114,10 +117,8 @@     , Penny.Lincoln.Bits.Price     , Penny.Lincoln.Bits.Qty     , Penny.Lincoln.Builders-    , Penny.Lincoln.Family-    , Penny.Lincoln.Family.Child-    , Penny.Lincoln.Family.Family-    , Penny.Lincoln.Family.Siblings+    , Penny.Lincoln.Ents+    , Penny.Lincoln.Equivalent     , Penny.Lincoln.HasText     , Penny.Lincoln.Matchers     , Penny.Lincoln.Predicates@@ -126,23 +127,129 @@     , Penny.Lincoln.Queries     , Penny.Lincoln.Queries.Siblings     , Penny.Lincoln.Serial-    , Penny.Lincoln.Transaction-    , Penny.Lincoln.Transaction.Unverified     , Penny.Shield     , Penny.Steel     , Penny.Steel.NestedMap+    , Penny.Steel.Sums     , Penny.Wheat     , Penny.Zinc    Other-modules:       Paths_penny_lib +  hs-source-dirs: lib +  if flag(incabal)+    cpp-options: -Dincabal+   ghc-options: -Wall   if flag(debug)     ghc-options: -auto-all -caf-all +  if ! flag (buildlib)+    buildable: False++-- I'm going to not list the exposed-modules for the test, and+-- hope it works :) it seems cabal only uses this information for+-- executables to determine what to bundle into the dist tarball.+-- Since the test modules are all listed above for the library,+-- this should not be a problem. However, if there are extra+-- test modules that are not in the library, list them here.+Executable penny-test+  Main-is: penny-test.hs+  hs-source-dirs: tests lib+  Build-depends:+      base ==4.*+    , action-permutations ==0.0.0.0+    , binary ==0.7.*+    , bytestring ==0.10.*+    , cereal ==0.3.*+    , containers ==0.5.*+    , explicit-exception ==0.1.*+    , matchers ==0.6.*+    , monad-loops ==0.3.*+    , multiarg ==0.16.*+    , ofx ==0.2.*+    , old-locale ==1.0.*+    , parsec >= 3.1.2 && < 3.2+    , pcre-light ==0.4.*+    , prednote == 0.8.*+    , pretty-show ==1.5.*+    , rainbow ==0.2.*+    , semigroups ==0.9.*+    , split ==0.2.*+    , strict ==0.3.*+    , terminfo == 0.3.*+    , text ==0.11.*+    , time ==1.4.*+    , transformers == 0.3.*+++    -- Test dependencies. test-framework has issues with newer versions,+    -- see+    -- https://github.com/batterseapower/test-framework/issues/34+    , QuickCheck ==2.5.*+    , random-shuffle ==0.0.4++  if ! flag(test)+    buildable: False++  ghc-options: -Wall++Executable penny-gibberish+  Main-is: penny-gibberish.hs+  hs-source-dirs: tests lib+  Build-depends:+      base ==4.*+    , action-permutations ==0.0.0.0+    , binary ==0.7.*+    , bytestring ==0.10.*+    , cereal ==0.3.*+    , containers ==0.5.*+    , explicit-exception ==0.1.*+    , matchers ==0.6.*+    , monad-loops ==0.3.*+    , multiarg ==0.16.*+    , ofx ==0.2.*+    , old-locale ==1.0.*+    , parsec >= 3.1.2 && < 3.2+    , pcre-light ==0.4.*+    , prednote == 0.8.*+    , pretty-show ==1.5.*+    , rainbow ==0.2.*+    , semigroups ==0.9.*+    , split ==0.2.*+    , strict ==0.3.*+    , terminfo == 0.3.*+    , text ==0.11.*+    , time ==1.4.*+    , transformers == 0.3.*+++    -- Test dependencies. test-framework has issues with newer versions,+    -- see+    -- https://github.com/batterseapower/test-framework/issues/34+    , QuickCheck ==2.5.*+    , random-shuffle ==0.0.4+    , random ==1.0.*++  if ! flag(test)+    buildable: False++  ghc-options: -Wall+ Flag debug   Description: turns on some debugging options   Default: False +Flag test+  Description: enables QuickCheck tests+  Default: False++Flag incabal+  Description: enables imports that only Cabal makes available+  Default: True++Flag buildlib+  Description: build library+  Default: True
+ tests/penny-gibberish.hs view
@@ -0,0 +1,98 @@+module Main where++import qualified System.Console.MultiArg as MA+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Test.QuickCheck.Gen as G+import qualified System.Random as Rand+import Control.Monad (replicateM)+import qualified Gibberish.Parsers as P+import qualified Penny.Copper.Render as R+import qualified System.Exit as Exit+import qualified Data.Text.IO as TIO+import qualified System.IO as IO++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ " [options]"+  , "Print a nonsensical but valid Penny file to standard output."+  , "Uses modified generators that only make printable ASCII."+  , "Options:"+  , "  -s, --size INT"+  , "      QuickCheck size parameter. Bigger numbers give more"+  , "      gibberish. (default: 5)"+  , "  -c, --count INT"+  , "      Number of items (transactions, comments, prices, and"+  , "      blank lines, total) to output. (default: 100)"+  , "  -l, --left GROUP_SPEC"+  , "      Group left of the decimal point (default: none)"+  , "  -r, --right GROUP_SPEC"+  , "      Group right of the decimal point (default: none)"+  , "      where GROUP_SPEC is:"+  , "        none - no digit grouping"+  , "        large - group if greater than 9,999 left of decimal,"+  , "                or more than 4 decimal places right of decimal"+  , "        all - group whenever there are at least 4 places"+  ]++data Opts = Opts+  { optSize :: Int+  , optCount :: Int+  , optLeft :: R.GroupSpec+  , optRight :: R.GroupSpec+  } deriving Show++defaultOpts :: Opts+defaultOpts = Opts 5 100 R.NoGrouping R.NoGrouping++options :: [MA.OptSpec (Opts -> Opts)]+options =+  [ MA.OptSpec ["size"] "s" . MA.OneArgE $ \s -> do+      i <- MA.reader s+      if i < 1+        then Ex.throw (MA.ErrorMsg "non-positive size parameter")+        else return (\os -> os { optSize = i })++  , MA.OptSpec ["count"] "c" . MA.OneArgE $ \s -> do+      i <- MA.reader s+      if i < 1+        then Ex.throw (MA.ErrorMsg "non-positive count parameter")+        else return (\os -> os { optCount = i })++  , MA.OptSpec ["left"] "l" . MA.ChoiceArg+    . map (\(str, spec) -> (str, \os -> os { optLeft = spec }))+    $ groupSpecs++  , MA.OptSpec ["right"] "r" . MA.ChoiceArg+    . map (\(str, spec) -> (str, \os -> os { optRight = spec }))+    $ groupSpecs+  ]++groupSpecs :: [(String, R.GroupSpec)]+groupSpecs = [ ("none",  R.NoGrouping)+             , ("large", R.GroupLarge)+             , ("all",   R.GroupAll  ) ]++posArg :: a -> Ex.Exceptional MA.InputError b+posArg _ = Ex.throw (MA.ErrorMsg "no non-option arguments accepted")++parse :: [(Opts -> Opts)] -> Opts+parse os = foldl (flip (.)) id os defaultOpts++main :: IO ()+main = do+  pn <- MA.getProgName+  os <- fmap parse $ MA.simpleWithHelp help MA.Intersperse options+                     posArg+  gen <- Rand.getStdGen+  let is = (\g -> G.unGen g gen (optSize os))+           . fmap (map fst)+           . replicateM (optCount os)+           $ P.item+      gs = R.GroupSpecs (optLeft os) (optRight os)+      x = mapM (R.item gs) is+  case x of+    Nothing -> do+      IO.hPutStrLn IO.stderr $ pn ++ ": error: could not render ledger."+      IO.hPutStrLn IO.stderr $ pn ++ "bad ledger: " ++ show is+      Exit.exitFailure+    Just strs -> mapM_ TIO.putStr strs >> Exit.exitSuccess
+ tests/penny-test.hs view
@@ -0,0 +1,50 @@+module Main where++import qualified Lincoln as L+import qualified Copper as C+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified System.Console.MultiArg as MA+import qualified System.Exit as Exit+import qualified Test.QuickCheck as Q++help :: String -> String+help pn = unlines+  [ "usage: " ++ pn ++ "[options]"+  , "runs all QuickCheck tests for Penny."+  , "Returns 0 if all tests succeeded, non-zero otherwise."+  , "Options:"+  , "--size, -s INT"+  , "  Limit QuickCheck size parameter to INT"+  , "--count, -n INT"+  , "  Maximum number of successful tests needed"+  ]++options :: [MA.OptSpec (Q.Args -> Q.Args)]+options =+  [ MA.OptSpec ["size"] "s" . MA.OneArgE $ \s -> do+      i <- MA.reader s+      let f a = a { Q.maxSize = i }+      return f++  , MA.OptSpec ["count"] "n" . MA.OneArgE $ \s -> do+      i <- MA.reader s+      let f a = a { Q.maxSuccess = i }+      return f+  ]++main :: IO ()+main = do+  opts <- MA.simpleWithHelp help MA.Intersperse+          options+          ( const . Ex.Exception . MA.ErrorMsg+            $ "this command does not accept positional arguments")+  let args = foldl (flip (.)) id opts Q.stdArgs+      runner = Q.quickCheckWithResult args+      acts = map ($ runner) allTests+  bools <- sequence acts+  if and bools+    then Exit.exitSuccess+    else Exit.exitFailure++allTests :: [(Q.Property -> IO Q.Result) -> IO Bool]+allTests = [ L.runTests ] ++ C.tests