diff --git a/app/Tables/BuySellTable.hs b/app/Features/BuySell/BuySellTable.hs similarity index 98% rename from app/Tables/BuySellTable.hs rename to app/Features/BuySell/BuySellTable.hs index 6d813e7..a55047c 100644 --- a/app/Tables/BuySellTable.hs +++ b/app/Features/BuySell/BuySellTable.hs @@ -1,4 +1,4 @@ -module Tables.BuySellTable +module Features.BuySell.BuySellTable ( createBuySellTable, ) where diff --git a/app/Features/GroupByCompany/GroupByCompanyArgParse.hs b/app/Features/GroupByCompany/GroupByCompanyArgParse.hs new file mode 100644 index 0000000..22f4fc3 --- /dev/null +++ b/app/Features/GroupByCompany/GroupByCompanyArgParse.hs @@ -0,0 +1,104 @@ +module Features.GroupByCompany.GroupByCompanyArgParse (ParsedGroupBuySellCliFlags (..), parseCliFlags, defaultGroupByCompanySortOption, allSortableColumnsKeys, sortableColumnToStr) where + +import Data.Char (toLower) +import Data.List (intercalate) +import qualified Data.Map as Map +import qualified Data.Text as T +import Features.GroupByCompany.GroupByCompanyTypes +import Text.Read (readMaybe) +import Types.Table.SortOption (SortOption (..), SortOrder (..), allSortKeys, parseOrder) + +data ParsedGroupBuySellCliFlags = ParsedGroupBuySellCliFlags + { sortOptions :: !GroupByCompanySortOption, + limit :: !(Maybe Int) + } + +defaultFlags :: ParsedGroupBuySellCliFlags +defaultFlags = + ParsedGroupBuySellCliFlags + { sortOptions = defaultGroupByCompanySortOption, + limit = Nothing + } + +defaultGroupByCompanySortOption :: GroupByCompanySortOption +defaultGroupByCompanySortOption = GroupByCompanySortOption (SortOption Company Descending) + +parseCliFlags :: [String] -> Either String ParsedGroupBuySellCliFlags +parseCliFlags args = go args defaultFlags + where + go [] acc = Right acc + go ("--sort" : col : ord : rest) acc = + case parseGroupByCompanySortOpts (col ++ " " ++ ord) of + Right opt -> go rest acc {sortOptions = opt} + Left err -> Left $ "Failed to parse --sort flag: " ++ err + go ("--sort" : col : rest) acc = + case parseGroupByCompanySortOpts col of + Right opt -> go rest acc {sortOptions = opt} + Left err -> Left $ "Failed to parse --sort flag: " ++ err + go ("--limit" : num : rest) acc = + case parseLimit num of + Just opt -> go rest acc {limit = Just opt} + Nothing -> + Left $ + "Failed to parse --limit flag due to invalid value: \"" + ++ num + ++ "\". Expected an integear larger than 1" + +parseGroupByCompanySortOpts :: String -> Either String GroupByCompanySortOption +parseGroupByCompanySortOpts input = + case words (map toLower input) of + [col, ord] -> do + column <- parseCol col + order <- parseOrd ord + Right $ GroupByCompanySortOption (SortOption column order) + -- Use default ordering + [col] -> do + column <- parseCol col + let order' = order (unGroupByCompanySortOption defaultGroupByCompanySortOption) + Right $ GroupByCompanySortOption (SortOption column order') + _ -> Left "Invalid input format. Use: (e.g., 'profit desc')." + where + parseCol col = + maybe + ( Left $ + "Invalid column \"" + ++ col + ++ "\". Please use one of the supported options: " + ++ intercalate ", " allSortableColumnsKeys + ) + Right + $ parseSortableColumnMap col + + parseOrd ord = + maybe + ( Left $ + "Invalid order: \"" + ++ ord + ++ "\". Please use one of the support options: " + ++ intercalate ", " allSortKeys + ) + Right + (parseOrder ord) + +parseLimit :: String -> Maybe Int +parseLimit str = do + n <- readMaybe str + if n > 0 then Just n else Nothing + +strToSortableColumnMap :: Map.Map String SortableColumn +strToSortableColumnMap = + Map.fromList $ + map (\col -> (lowercaseFirst (show col), col)) [minBound .. maxBound] + +parseSortableColumnMap :: String -> Maybe SortableColumn +parseSortableColumnMap key = Map.lookup key strToSortableColumnMap + +sortableColumnToStr :: SortableColumn -> String +sortableColumnToStr col = lowercaseFirst (show col) + +allSortableColumnsKeys :: [String] +allSortableColumnsKeys = Map.keys strToSortableColumnMap + +lowercaseFirst :: String -> String +lowercaseFirst [] = [] +lowercaseFirst (x : xs) = toLower x : xs \ No newline at end of file diff --git a/app/Features/GroupByCompany/GroupByCompanyTable.hs b/app/Features/GroupByCompany/GroupByCompanyTable.hs new file mode 100644 index 0000000..8e26a32 --- /dev/null +++ b/app/Features/GroupByCompany/GroupByCompanyTable.hs @@ -0,0 +1,167 @@ +module Features.GroupByCompany.GroupByCompanyTable + ( createGroupByCompanyTable, + defaultGroupByCompanySortOption, + ) +where + +import Calc + ( calcNumBought, + calcNumSold, + calcRemainingAmount, + calcTotalBuySellProfit, + calcTotalDividendProfit, + calcTotalProfitForCompany, + ) +import Data.Char (toLower) +import Data.Function (on) +import Data.List (sortBy) +import Data.Text (Text) +import qualified Data.Text as T +import Features.GroupByCompany.GroupByCompanyArgParse +import Features.GroupByCompany.GroupByCompanyTypes +import PrettyPrint + ( createPrettyTable, + intToText, + moneyToText, + ) +import Types.PrintableCell + ( FixedWidth (FixedWidth, l, s), + PrintableCell + ( textAlign, + width + ), + TextAlign + ( LeftAlign, + RightAlign + ), + createCellFromText, + createFilledCell, + createFixedCell, + ) +import Types.Table.SortOption (SortOption (..), SortOrder (..), parseOrder) +import Types.Transaction.GenericTransaction + ( getCompany, + ) +import Types.Transaction.ParsedTransaction + ( ParsedTransaction, + extractBuySellRows, + extractDividendRows, + extractGenericTransaction, + ) +import Types.UiSize (UiSize) +import Types.UtilTypes + ( SortedByDateList + ( getSortedByDateList + ), + ) + +createGroupByCompanyTable :: + UiSize -> Int -> [String] -> [SortedByDateList ParsedTransaction] -> Text +createGroupByCompanyTable uiSize termWidth cliFlags rows = + case parseCliFlags cliFlags of + Left errMsg -> T.pack errMsg + Right parsedCliFlags -> + let header = groupByCompanyHeader + mapContent row = createGroupByCompanyRow row header + groupByCompanyContentRows = map mapContent rows + + -- apply cli flags + + -- apply sort + sortedRows = sortGroupByCompanyRows (sortOptions parsedCliFlags) groupByCompanyContentRows + + -- apply limit + limitedRows = case limit parsedCliFlags of + Just n -> take n sortedRows + Nothing -> sortedRows + + content = map (groupByCompanyRowToCells header) limitedRows + spacing = map width header + in createPrettyTable + uiSize + termWidth + header + content + spacing + +-- | Företag | Köpt | Sålt | Nuv. balans | Vinst (kr) | Vinst sålda (kr) | +groupByCompanyHeader :: [PrintableCell] +groupByCompanyHeader = + [ createFilledCell "Företag" LeftAlign, + createFixedCell "Köpt" FixedWidth {s = 10, l = Just 10} RightAlign, + createFixedCell "Sålt" FixedWidth {s = 10, l = Just 10} RightAlign, + createFixedCell + "Nuv. balans" + FixedWidth {s = 10, l = Just 12} + RightAlign, + createFixedCell "Vinst (kr)" FixedWidth {s = 10, l = Just 12} RightAlign, + createFixedCell + "Utdelning (kr)" + FixedWidth {s = 10, l = Just 14} + RightAlign, + createFixedCell + "Vinst sålda (kr)" + FixedWidth {s = 15, l = Just 17} + RightAlign + ] + +-- | Transforms a list of parsed transactions that shoud belong to the same company into printable +-- cells +createGroupByCompanyRow :: + SortedByDateList ParsedTransaction -> [PrintableCell] -> GroupByCompanyContentRow +createGroupByCompanyRow rows header = + let company = + getCompany . extractGenericTransaction . head $ + getSortedByDateList + rows + buySellRows = extractBuySellRows rows + dividendRows = extractDividendRows rows + in GroupByCompanyContentRow + { company = company, + bought = calcNumBought buySellRows, + sold = calcNumSold buySellRows, + currentAmmount = calcRemainingAmount (getSortedByDateList rows), + profit = calcTotalBuySellProfit buySellRows, + dividence = calcTotalDividendProfit dividendRows, + profitForSold = calcTotalProfitForCompany rows + } + +sortGroupByCompanyRows :: + GroupByCompanySortOption -> + [GroupByCompanyContentRow] -> + [GroupByCompanyContentRow] +sortGroupByCompanyRows (GroupByCompanySortOption (SortOption column order)) rows = + case column of + Company -> applySort (compare `on` (T.toLower . company)) rows + Bought -> applySort (compare `on` bought) rows + Sold -> applySort (compare `on` sold) rows + CurrentAmount -> applySort (compare `on` currentAmmount) rows + Profit -> applySort (compare `on` profit) rows + Dividend -> applySort (compare `on` dividence) rows + ProfitForSold -> applySort (compare `on` profitForSold) rows + where + applySort cmp = case order of + Ascending -> sortBy cmp + Descending -> sortBy (flip cmp) + +groupByCompanyRowToCells :: [PrintableCell] -> GroupByCompanyContentRow -> [PrintableCell] +groupByCompanyRowToCells header row = + let tableRows = + [ company row, + intToText $ bought row, + intToText $ sold row, + intToText $ currentAmmount row, + moneyToText $ profit row, + moneyToText $ dividence row, + moneyToText $ profitForSold row + ] + in -- Use info in header to create cells with spacing + zipWith + ( \title headerCell -> + createCellFromText + title + (width headerCell) + (textAlign headerCell) + ) + tableRows + header diff --git a/app/Features/GroupByCompany/GroupByCompanyTypes.hs b/app/Features/GroupByCompany/GroupByCompanyTypes.hs new file mode 100644 index 0000000..90b3508 --- /dev/null +++ b/app/Features/GroupByCompany/GroupByCompanyTypes.hs @@ -0,0 +1,28 @@ +module Features.GroupByCompany.GroupByCompanyTypes (SortableColumn (..), GroupByCompanyContentRow (..), GroupByCompanySortOption (..)) where + +import Data.Text (Text) +import Types.Money (Money (..)) +import Types.Table.SortOption (SortOption (..), SortOrder (..)) + +data SortableColumn + = Company + | Bought + | Sold + | CurrentAmount + | Profit + | Dividend + | ProfitForSold + deriving (Show, Read, Enum, Bounded, Eq, Ord) + +newtype GroupByCompanySortOption = GroupByCompanySortOption {unGroupByCompanySortOption :: SortOption SortableColumn} + +-- columns: |Företag|Köpt|Sålt|Nuv. balans|Vinst (kr)|Utdelning (kr)| Vinst sålda (kr)| +data GroupByCompanyContentRow = GroupByCompanyContentRow + { company :: !Text, + bought :: !Int, + sold :: !Int, + currentAmmount :: !Int, + profit :: !Money, + dividence :: !Money, + profitForSold :: !Money + } \ No newline at end of file diff --git a/app/Main b/app/Main index 0d24e17..1fbc8db 100755 Binary files a/app/Main and b/app/Main differ diff --git a/app/Main.hs b/app/Main.hs index 14fb266..9f46c3c 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -35,12 +35,15 @@ import Data.Tuple snd, ) import qualified Data.Vector as V +import Features.BuySell.BuySellTable (createBuySellTable) +import Features.GroupByCompany.GroupByCompanyArgParse (sortableColumnToStr) +import qualified Features.GroupByCompany.GroupByCompanyArgParse as GbcArgParse +import Features.GroupByCompany.GroupByCompanyTable (createGroupByCompanyTable) +import qualified Features.GroupByCompany.GroupByCompanyTypes as GBC import Parse (parseCsvData) import qualified PrettyPrint as PP import System.Environment (getArgs) -import Tables.BuySellTable (createBuySellTable) -import Tables.GroupByCompanyTable (createGroupByCompanyTable) -import Types.CLITypes (CommandLineOption (..)) +import Types.CLITypes (CommandLineOption (..), FlagDoc (..)) import Types.Transaction.GenericTransaction ( Transaction, ) @@ -49,43 +52,64 @@ import Types.Transaction.ParsedTransaction ) import Types.UiSize (calcUiSize) import Types.UtilTypes (SortedByDateList (..)) -import Util (SortableByDate (..)) +import Util (SortableByDate (..), unsnoc) main :: IO () main = do args <- getArgs handleArg args -mainOptions :: [CommandLineOption] -mainOptions = +mainCommands :: [CommandLineOption] +mainCommands = [ CommandLineOption - "--buy-sell" - "-bs" + "buy-sell" + "bs" "Print buy and sell transactions" + [] printBuySellHistory, CommandLineOption - "--group-by-company" - "-gbc" + "group-by-company" + "gbc" "Print results grouped by company" + [ FlagDoc + { flag = "--sort [asc|desc] ", + flagDescriptionRows = + [ "Sort table by column (default: asc)", + -- table order + -- Företag, Köpt, Sålt, Nuv. balans, Vinst (kr), Utdelning (kr), Vinst sålda (kr) + "Columns: " ++ createGbcSortOpts [GBC.Company, GBC.Bought, GBC.Sold] ++ ",", + " " ++ createGbcSortOpts [GBC.CurrentAmount, GBC.Profit] ++ ",", + " " ++ createGbcSortOpts [GBC.Dividend, GBC.ProfitForSold] + ] + }, + FlagDoc + { flag = "--limit ", + flagDescriptionRows = ["Limit number of rows (must be > 1)"] + } + ] printGroupByComany ] + where + createGbcSortOpts xs = List.intercalate ", " (map sortableColumnToStr xs) -actionNoOp :: FilePath -> IO () -actionNoOp _ = return () +actionNoOp :: [String] -> FilePath -> IO () +actionNoOp _ _ = return () helpOption :: CommandLineOption -helpOption = CommandLineOption "--help" "-h" "Print help" actionNoOp +helpOption = CommandLineOption "--help" "-h" "Print help" [] actionNoOp handleArg :: [String] -> IO () handleArg [] = putStrLn "No arguments provided, write --help for instructions" -handleArg [flag] = printHelp (helpOption : mainOptions) -handleArg (flag : filePath : _) = - case find (\opt -> flag == longArg opt || flag == shortArg opt) mainOptions of - Just opt -> action opt filePath +handleArg [flag] = printHelp mainCommands [helpOption] +handleArg (cmd : args) = + case find (\opt -> cmd == longCmd opt || cmd == shortCmd opt) mainCommands of + Just opt -> case unsnoc args of + Just (extraArgs, filePath) -> action opt extraArgs filePath + Nothing -> error "Missing filepath" Nothing -> error "Unknown flag" -printBuySellHistory :: FilePath -> IO () -printBuySellHistory filePath = do +printBuySellHistory :: [String] -> FilePath -> IO () +printBuySellHistory cliFlags filePath = do rows <- readCsv filePath termWidth <- getAdjustedTerminalWidth let uiSize = calcUiSize termWidth @@ -99,8 +123,8 @@ printBuySellHistory filePath = do T.putStr printContent -printGroupByComany :: FilePath -> IO () -printGroupByComany filePath = do +printGroupByComany :: [String] -> FilePath -> IO () +printGroupByComany cliFlags filePath = do rows <- readCsv filePath termWidth <- getAdjustedTerminalWidth let uiSize = calcUiSize termWidth @@ -110,6 +134,7 @@ printGroupByComany filePath = do createGroupByCompanyTable uiSize (fromMaybe 0 termWidth) + cliFlags (Map.elems groupedByCompany) T.putStr printContent diff --git a/app/Tables/GroupByCompanyTable.hs b/app/Tables/GroupByCompanyTable.hs deleted file mode 100644 index 71f46a6..0000000 --- a/app/Tables/GroupByCompanyTable.hs +++ /dev/null @@ -1,111 +0,0 @@ -module Tables.GroupByCompanyTable - ( createGroupByCompanyTable, - ) -where - -import Calc - ( calcNumBought, - calcNumSold, - calcRemainingAmount, - calcTotalBuySellProfit, - calcTotalDividendProfit, - calcTotalProfitForCompany, - ) -import Data.Text (Text) -import PrettyPrint - ( createPrettyTable, - intToText, - moneyToText, - ) -import Types.PrintableCell - ( FixedWidth (FixedWidth, l, s), - PrintableCell - ( textAlign, - width - ), - TextAlign - ( LeftAlign, - RightAlign - ), - createCellFromText, - createFilledCell, - createFixedCell, - ) -import Types.Transaction.GenericTransaction - ( getCompany, - ) -import Types.Transaction.ParsedTransaction - ( ParsedTransaction, - extractBuySellRows, - extractDividendRows, - extractGenericTransaction, - ) -import Types.UiSize (UiSize) -import Types.UtilTypes - ( SortedByDateList - ( getSortedByDateList - ), - ) - -createGroupByCompanyTable :: - UiSize -> Int -> [SortedByDateList ParsedTransaction] -> Text -createGroupByCompanyTable uiSize termWidth rows = - let header = groupByCompanyHeader - mapContent row = groupByCompanyContent row header - content = map mapContent rows - spacing = map width header - in createPrettyTable - uiSize - termWidth - header - content - spacing - --- | Företag | Köpt | Sålt | Nuv. balans | Vinst (kr) | Vinst sålda (kr) | -groupByCompanyHeader :: [PrintableCell] -groupByCompanyHeader = - [ createFilledCell "Företag" LeftAlign, - createFixedCell "Köpt" FixedWidth {s = 10, l = Just 10} RightAlign, - createFixedCell "Sålt" FixedWidth {s = 10, l = Just 10} RightAlign, - createFixedCell - "Nuv. balans" - FixedWidth {s = 10, l = Just 12} - RightAlign, - createFixedCell "Vinst (kr)" FixedWidth {s = 10, l = Just 12} RightAlign, - createFixedCell - "Utdelning (kr)" - FixedWidth {s = 10, l = Just 14} - RightAlign, - createFixedCell - "Vinst sålda (kr)" - FixedWidth {s = 15, l = Just 17} - RightAlign - ] - -groupByCompanyContent :: - SortedByDateList ParsedTransaction -> [PrintableCell] -> [PrintableCell] -groupByCompanyContent rows header = - let company = - getCompany . extractGenericTransaction . head $ - getSortedByDateList - rows - buySellRows = extractBuySellRows rows - dividendRows = extractDividendRows rows - tableRows = - [ company, - intToText . calcNumBought $ buySellRows, - intToText . calcNumSold $ buySellRows, - intToText . calcRemainingAmount $ getSortedByDateList rows, - moneyToText . calcTotalBuySellProfit $ buySellRows, - moneyToText . calcTotalDividendProfit $ dividendRows, - moneyToText . calcTotalProfitForCompany $ rows - ] - in zipWith - ( \title headerCell -> - createCellFromText - title - (width headerCell) - (textAlign headerCell) - ) - tableRows - header diff --git a/hava.cabal b/hava.cabal index 45d7c7c..3d89d33 100644 --- a/hava.cabal +++ b/hava.cabal @@ -24,9 +24,11 @@ source-repository head library exposed-modules: + Features.BuySell.BuySellTable + Features.GroupByCompany.GroupByCompanyArgParse + Features.GroupByCompany.GroupByCompanyTable + Features.GroupByCompany.GroupByCompanyTypes Main - Tables.BuySellTable - Tables.GroupByCompanyTable Calc CLIHelp Parse @@ -34,6 +36,7 @@ library Types.CLITypes Types.Money Types.PrintableCell + Types.Table.SortOption Types.Transaction.GenericTransaction Types.Transaction.ParsedTransaction Types.Transaction.TransactionBuySell @@ -67,8 +70,10 @@ library executable hava-exe main-is: Main.hs other-modules: - Tables.BuySellTable - Tables.GroupByCompanyTable + Features.BuySell.BuySellTable + Features.GroupByCompany.GroupByCompanyArgParse + Features.GroupByCompany.GroupByCompanyTable + Features.GroupByCompany.GroupByCompanyTypes Paths_hava hs-source-dirs: app diff --git a/src/CLIHelp.hs b/src/CLIHelp.hs index b459a79..6fd7a44 100644 --- a/src/CLIHelp.hs +++ b/src/CLIHelp.hs @@ -7,6 +7,7 @@ module CLIHelp ) where +import qualified Data.List as List import qualified Data.Text as T import qualified Data.Text.IO as T import qualified PrettyPrint as PP @@ -14,10 +15,11 @@ import System.Console.Terminal.Size ( Window (..), size, ) -import Types.CLITypes (CommandLineOption (..)) +import Types.CLITypes (CommandLineOption (..), FlagDoc (..)) import Types.PrintableCell ( CellWidth (..), FixedWidth (..), + PrintableCell, createDefaultCell, ) import Types.UiSize @@ -25,16 +27,19 @@ import Types.UiSize calcUiSize, ) -printHelp :: [CommandLineOption] -> IO () -printHelp options = do +-- todo: separata commands and options..? +printHelp :: [CommandLineOption] -> [CommandLineOption] -> IO () +printHelp commands options = do printUsage T.putStrLn (T.pack "") - printCmdLineOpts options + printCmdLineOpts "Commands" commands + T.putStrLn (T.pack "") + printCmdLineOpts "Global options" options usages :: [(String, String)] usages = - [ ( "hava [option] [path-to-csv-file]", - "Run Hava in