From fd87bb6dda93f4f7ff8ce61407a8a1009667cfcc Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Mon, 10 Aug 2020 22:23:46 -0700 Subject: [PATCH] Add output format options for holdings command --- README.md | 6 ++ cointop/cmd/holdings.go | 4 ++ cointop/cmd/test.go | 2 +- cointop/portfolio.go | 132 ++++++++++++++++++++++++++++++++++------ 4 files changed, 123 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d3a0899..3ac6e3d 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,12 @@ Just run the `cointop` command to get started: $ cointop ``` +To see all the available commands and options run `help` flag: + +```bash +$ cointop --help +``` + ### Navigation - Easiest way to navigate up and down is using the arrow keys and , respectively diff --git a/cointop/cmd/holdings.go b/cointop/cmd/holdings.go index fba2c3f..9604ab2 100644 --- a/cointop/cmd/holdings.go +++ b/cointop/cmd/holdings.go @@ -15,6 +15,7 @@ func HoldingsCmd() *cobra.Command { var config string var sortBy string var sortDesc bool + var format string = "table" var humanReadable bool holdingsCmd := &cobra.Command{ @@ -37,6 +38,7 @@ func HoldingsCmd() *cobra.Command { if total { return ct.PrintTotalHoldings(&cointop.TablePrintOptions{ HumanReadable: humanReadable, + Format: format, }) } @@ -44,6 +46,7 @@ func HoldingsCmd() *cobra.Command { SortBy: sortBy, SortDesc: sortDesc, HumanReadable: humanReadable, + Format: format, }) }, } @@ -55,6 +58,7 @@ func HoldingsCmd() *cobra.Command { holdingsCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath)) holdingsCmd.Flags().StringVarP(&sortBy, "sort-by", "s", sortBy, `Sort by column. Options are "name", "symbol", "price", "holdings", "balance", "24h"`) holdingsCmd.Flags().BoolVarP(&sortDesc, "sort-desc", "d", sortDesc, "Sort in descending order") + holdingsCmd.Flags().StringVarP(&format, "format", "f", format, `Ouput format. Options are "table", "csv", "json"`) return holdingsCmd } diff --git a/cointop/cmd/test.go b/cointop/cmd/test.go index 10eea7f..3c24af2 100644 --- a/cointop/cmd/test.go +++ b/cointop/cmd/test.go @@ -9,7 +9,7 @@ import ( func TestCmd() *cobra.Command { testCmd := &cobra.Command{ Use: "test", - Short: "Runs tests", + Short: "Runs tests for Homebrew", Long: `The test command runs tests for Homebrew`, Run: func(cmd *cobra.Command, args []string) { doTest() diff --git a/cointop/portfolio.go b/cointop/portfolio.go index 659dc92..3074f52 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -1,6 +1,8 @@ package cointop import ( + "encoding/csv" + "encoding/json" "fmt" "math" "os" @@ -320,9 +322,17 @@ type TablePrintOptions struct { SortBy string SortDesc bool HumanReadable bool + Format string } -// portfolioColumns is list of column keys for portfolio +// outputFormats is list of valid output formats +var outputFormats = map[string]bool{ + "table": true, + "csv": true, + "json": true, +} + +// portfolioColumns is list of valid column keys for portfolio var portfolioColumns = map[string]bool{ "name": true, "symbol": true, @@ -335,34 +345,46 @@ var portfolioColumns = map[string]bool{ // PrintHoldingsTable prints the holdings in an ASCII table func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error { ct.debuglog("printHoldingsTable()") - ct.RefreshPortfolioCoins() - if options == nil { options = &TablePrintOptions{} } + ct.RefreshPortfolioCoins() + + sortBy := options.SortBy + sortDesc := options.SortDesc + format := options.Format + humanReadable := options.HumanReadable holdings := ct.GetPortfolioSlice() - if options.SortBy != "" { - if _, ok := portfolioColumns[options.SortBy]; !ok { - return fmt.Errorf("The option %q is not a valid column name", options.SortBy) + if format == "" { + format = "table" + } + + if sortBy != "" { + if _, ok := portfolioColumns[sortBy]; !ok { + return fmt.Errorf("The option %q is not a valid column name", sortBy) } - ct.Sort(options.SortBy, options.SortDesc, holdings, true) + ct.Sort(sortBy, sortDesc, holdings, true) + } + + if _, ok := outputFormats[format]; !ok { + return fmt.Errorf("The option %q is not a valid format type", format) } total := ct.GetPortfolioTotal() - data := make([][]string, len(holdings)) + records := make([][]string, len(holdings)) symbol := ct.CurrencySymbol() - for _, entry := range holdings { + for i, entry := range holdings { percentHoldings := (entry.Balance / total) * 1e2 if math.IsNaN(percentHoldings) { percentHoldings = 0 } - if options.HumanReadable { - data = append(data, []string{ + if humanReadable { + records[i] = []string{ entry.Name, entry.Symbol, fmt.Sprintf("%s%s", symbol, humanize.Commaf(entry.Price)), @@ -370,9 +392,9 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error { fmt.Sprintf("%s%s", symbol, humanize.Commaf(entry.Balance)), fmt.Sprintf("%.2f%%", entry.PercentChange24H), fmt.Sprintf("%.2f%%", percentHoldings), - }) + } } else { - data = append(data, []string{ + records[i] = []string{ entry.Name, entry.Symbol, strconv.FormatFloat(entry.Price, 'f', -1, 64), @@ -380,14 +402,53 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error { strconv.FormatFloat(entry.Balance, 'f', -1, 64), fmt.Sprintf("%.2f", entry.PercentChange24H), fmt.Sprintf("%.2f", percentHoldings), - }) + } } } - alignment := []int{-1, -1, 1, 1, 1, 1, 1} headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"} + + if format == "csv" { + csvWriter := csv.NewWriter(os.Stdout) + if err := csvWriter.Write(headers); err != nil { + return err + } + + for _, record := range records { + if err := csvWriter.Write(record); err != nil { + return err + } + } + + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + return err + } + + return nil + } else if format == "json" { + list := make([]map[string]string, len(records)) + for i, record := range records { + obj := make(map[string]string, len(record)) + for j, column := range record { + obj[headers[j]] = column + } + + list[i] = obj + } + + output, err := json.Marshal(list) + if err != nil { + return err + } + + fmt.Println(string(output)) + return nil + } + + alignment := []int{-1, -1, 1, 1, 1, 1, 1} table := asciitable.NewAsciiTable(&asciitable.Input{ - Data: data, + Data: records, Headers: headers, Alignment: alignment, }) @@ -404,15 +465,46 @@ func (ct *Cointop) PrintTotalHoldings(options *TablePrintOptions) error { } ct.RefreshPortfolioCoins() + + humanReadable := options.HumanReadable total := ct.GetPortfolioTotal() symbol := ct.CurrencySymbol() + format := options.Format - if options.HumanReadable { - fmt.Fprintf(os.Stdout, "%s%s\n", symbol, humanize.Commaf(total)) - } else { - fmt.Fprintf(os.Stdout, "%s\n", strconv.FormatFloat(total, 'f', -1, 64)) + value := strconv.FormatFloat(total, 'f', -1, 64) + + if humanReadable { + value = fmt.Sprintf("%s%s", symbol, humanize.Commaf(total)) + } + + if format == "csv" { + csvWriter := csv.NewWriter(os.Stdout) + if err := csvWriter.Write([]string{"total"}); err != nil { + return err + } + if err := csvWriter.Write([]string{value}); err != nil { + return err + } + + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + return err + } + + return nil + } else if format == "json" { + obj := map[string]string{"total": value} + output, err := json.Marshal(obj) + if err != nil { + return err + } + + fmt.Println(string(output)) + return nil } + fmt.Println(value) + return nil }