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 +0/−457
- Penny/Brenner.hs +0/−259
- Penny/Brenner/Amex.hs +0/−121
- Penny/Brenner/BofA.hs +0/−226
- Penny/Brenner/Clear.hs +0/−175
- Penny/Brenner/Database.hs +0/−49
- Penny/Brenner/Import.hs +0/−106
- Penny/Brenner/Merge.hs +0/−347
- Penny/Brenner/Print.hs +0/−65
- Penny/Brenner/Types.hs +0/−298
- Penny/Brenner/Util.hs +0/−87
- Penny/Cabin.hs +0/−7
- Penny/Cabin/Balance.hs +0/−21
- Penny/Cabin/Balance/Convert.hs +0/−334
- Penny/Cabin/Balance/Convert/Chunker.hs +0/−239
- Penny/Cabin/Balance/Convert/Options.hs +0/−43
- Penny/Cabin/Balance/Convert/Parser.hs +0/−87
- Penny/Cabin/Balance/MultiCommodity.hs +0/−177
- Penny/Cabin/Balance/MultiCommodity/Chunker.hs +0/−222
- Penny/Cabin/Balance/MultiCommodity/Parser.hs +0/−29
- Penny/Cabin/Balance/Util.hs +0/−228
- Penny/Cabin/Interface.hs +0/−77
- Penny/Cabin/Meta.hs +0/−38
- Penny/Cabin/Options.hs +0/−16
- Penny/Cabin/Parsers.hs +0/−24
- Penny/Cabin/Posts.hs +0/−622
- Penny/Cabin/Posts/Allocated.hs +0/−402
- Penny/Cabin/Posts/BottomRows.hs +0/−649
- Penny/Cabin/Posts/Chunk.hs +0/−152
- Penny/Cabin/Posts/Fields.hs +0/−199
- Penny/Cabin/Posts/Growers.hs +0/−596
- Penny/Cabin/Posts/Meta.hs +0/−79
- Penny/Cabin/Posts/Parser.hs +0/−285
- Penny/Cabin/Posts/Spacers.hs +0/−32
- Penny/Cabin/Posts/Types.hs +0/−4
- Penny/Cabin/Row.hs +0/−138
- Penny/Cabin/Scheme.hs +0/−140
- Penny/Cabin/Scheme/Schemes.hs +0/−87
- Penny/Cabin/TextFormat.hs +0/−173
- Penny/Copper.hs +0/−211
- Penny/Copper/Parsec.hs +0/−418
- Penny/Copper/Render.hs +0/−523
- Penny/Copper/Terminals.hs +0/−145
- Penny/Copper/Types.hs +0/−59
- Penny/Liberty.hs +0/−788
- Penny/Lincoln.hs +0/−272
- Penny/Lincoln/Balance.hs +0/−109
- Penny/Lincoln/Bits.hs +0/−97
- Penny/Lincoln/Bits/DateTime.hs +0/−152
- Penny/Lincoln/Bits/Open.hs +0/−124
- Penny/Lincoln/Bits/Price.hs +0/−42
- Penny/Lincoln/Bits/Qty.hs +0/−300
- Penny/Lincoln/Builders.hs +0/−28
- Penny/Lincoln/Family.hs +0/−104
- Penny/Lincoln/Family/Child.hs +0/−9
- Penny/Lincoln/Family/Family.hs +0/−76
- Penny/Lincoln/Family/Siblings.hs +0/−54
- Penny/Lincoln/HasText.hs +0/−44
- Penny/Lincoln/Matchers.hs +0/−21
- Penny/Lincoln/Predicates.hs +0/−254
- Penny/Lincoln/Predicates/Siblings.hs +0/−210
- Penny/Lincoln/PriceDb.hs +0/−98
- Penny/Lincoln/Queries.hs +0/−100
- Penny/Lincoln/Queries/Siblings.hs +0/−87
- Penny/Lincoln/Serial.hs +0/−58
- Penny/Lincoln/Transaction.hs +0/−499
- Penny/Lincoln/Transaction/Unverified.hs +0/−131
- Penny/Shield.hs +0/−105
- Penny/Steel.hs +0/−3
- Penny/Steel/NestedMap.hs +0/−275
- Penny/Wheat.hs +0/−365
- Penny/Zinc.hs +0/−747
- lib/Penny.hs +455/−0
- lib/Penny/Brenner.hs +263/−0
- lib/Penny/Brenner/Clear.hs +185/−0
- lib/Penny/Brenner/Database.hs +41/−0
- lib/Penny/Brenner/Import.hs +140/−0
- lib/Penny/Brenner/Info.hs +139/−0
- lib/Penny/Brenner/Merge.hs +360/−0
- lib/Penny/Brenner/OFX.hs +68/−0
- lib/Penny/Brenner/Print.hs +59/−0
- lib/Penny/Brenner/Types.hs +340/−0
- lib/Penny/Brenner/Util.hs +106/−0
- lib/Penny/Cabin.hs +7/−0
- lib/Penny/Cabin/Balance.hs +21/−0
- lib/Penny/Cabin/Balance/Convert.hs +334/−0
- lib/Penny/Cabin/Balance/Convert/Chunker.hs +239/−0
- lib/Penny/Cabin/Balance/Convert/Options.hs +43/−0
- lib/Penny/Cabin/Balance/Convert/Parser.hs +87/−0
- lib/Penny/Cabin/Balance/MultiCommodity.hs +177/−0
- lib/Penny/Cabin/Balance/MultiCommodity/Chunker.hs +222/−0
- lib/Penny/Cabin/Balance/MultiCommodity/Parser.hs +29/−0
- lib/Penny/Cabin/Balance/Util.hs +230/−0
- lib/Penny/Cabin/Interface.hs +77/−0
- lib/Penny/Cabin/Meta.hs +13/−0
- lib/Penny/Cabin/Options.hs +16/−0
- lib/Penny/Cabin/Parsers.hs +24/−0
- lib/Penny/Cabin/Posts.hs +621/−0
- lib/Penny/Cabin/Posts/Allocated.hs +401/−0
- lib/Penny/Cabin/Posts/BottomRows.hs +648/−0
- lib/Penny/Cabin/Posts/Chunk.hs +152/−0
- lib/Penny/Cabin/Posts/Fields.hs +199/−0
- lib/Penny/Cabin/Posts/Growers.hs +595/−0
- lib/Penny/Cabin/Posts/Meta.hs +82/−0
- lib/Penny/Cabin/Posts/Parser.hs +286/−0
- lib/Penny/Cabin/Posts/Spacers.hs +32/−0
- lib/Penny/Cabin/Posts/Types.hs +4/−0
- lib/Penny/Cabin/Row.hs +138/−0
- lib/Penny/Cabin/Scheme.hs +140/−0
- lib/Penny/Cabin/Scheme/Schemes.hs +87/−0
- lib/Penny/Cabin/TextFormat.hs +173/−0
- lib/Penny/Copper.hs +163/−0
- lib/Penny/Copper/Interface.hs +62/−0
- lib/Penny/Copper/Parsec.hs +415/−0
- lib/Penny/Copper/Render.hs +530/−0
- lib/Penny/Copper/Terminals.hs +145/−0
- lib/Penny/Liberty.hs +777/−0
- lib/Penny/Lincoln.hs +72/−0
- lib/Penny/Lincoln/Balance.hs +109/−0
- lib/Penny/Lincoln/Bits.hs +180/−0
- lib/Penny/Lincoln/Bits/DateTime.hs +177/−0
- lib/Penny/Lincoln/Bits/Open.hs +214/−0
- lib/Penny/Lincoln/Bits/Price.hs +63/−0
- lib/Penny/Lincoln/Bits/Qty.hs +360/−0
- lib/Penny/Lincoln/Builders.hs +28/−0
- lib/Penny/Lincoln/Ents.hs +278/−0
- lib/Penny/Lincoln/Equivalent.hs +21/−0
- lib/Penny/Lincoln/HasText.hs +58/−0
- lib/Penny/Lincoln/Matchers.hs +21/−0
- lib/Penny/Lincoln/Predicates.hs +323/−0
- lib/Penny/Lincoln/Predicates/Siblings.hs +328/−0
- lib/Penny/Lincoln/PriceDb.hs +98/−0
- lib/Penny/Lincoln/Queries.hs +104/−0
- lib/Penny/Lincoln/Queries/Siblings.hs +102/−0
- lib/Penny/Lincoln/Serial.hs +126/−0
- lib/Penny/Shield.hs +106/−0
- lib/Penny/Steel.hs +3/−0
- lib/Penny/Steel/NestedMap.hs +275/−0
- lib/Penny/Steel/Sums.hs +78/−0
- lib/Penny/Wheat.hs +367/−0
- lib/Penny/Zinc.hs +746/−0
- penny-lib.cabal +118/−11
- tests/penny-gibberish.hs +98/−0
- tests/penny-test.hs +50/−0
− 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