From 8651b20735a1d97e25d87238cd4d3c609ec700e5 Mon Sep 17 00:00:00 2001 From: sgmoore Date: Sun, 3 Oct 2021 20:39:40 -0700 Subject: [PATCH 1/4] Update install.md (#202) Spelling fix at lines 10 and 72 --- docs/content/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/install.md b/docs/content/install.md index 0d4e269..ee6d365 100644 --- a/docs/content/install.md +++ b/docs/content/install.md @@ -7,7 +7,7 @@ draft: false There are multiple ways you can install cointop depending on the platform you're on. -## From source (always latest and recommeded) +## From source (always latest and recommended) Make sure to have [go](https://golang.org/) (1.12+) installed, then do: @@ -69,7 +69,7 @@ Note: snaps don't work in Windows WSL. See this [issue thread](https://forum.sna cointop is available as a [copr](https://copr.fedorainfracloud.org/coprs/miguelmota/cointop/) package. -First, enable the respository +First, enable the repository ```bash sudo dnf copr enable miguelmota/cointop -y From ff24fb3b690ee4129a3a2c7e0793be3397683ddc Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Mon, 4 Oct 2021 16:49:53 +1100 Subject: [PATCH 2/4] Add simple test workflow (#201) * Add simple test workflow * main->master --- .github/workflows/go.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..08d4d24 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,25 @@ +name: Go + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... From e1aded93e84141df5f6181ac4b24e6ab945fe1b2 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Tue, 5 Oct 2021 08:01:42 +1100 Subject: [PATCH 3/4] More minor cleanups (no functional change) (#198) * Redundant type conversions * Remove redudant type declarations. Inline one-line constructors. Remove unnecessary brackets. * Simplify name - it's used via package name anyway * Simplify variable initializations * Change `var x uint = Y` to `x := uint(Y)` * More shorthand initialization --- cmd/commands/holdings.go | 2 +- cmd/commands/server.go | 18 +++++++++--------- cointop/chart.go | 29 ++++++++++++++--------------- cointop/cointop.go | 8 ++++---- cointop/config.go | 6 +++--- cointop/keybindings.go | 2 +- cointop/marketbar.go | 5 ++--- cointop/menu.go | 3 +-- cointop/navigation.go | 2 +- cointop/portfolio.go | 2 +- cointop/search.go | 6 ++---- cointop/statusbar.go | 3 +-- cointop/table.go | 3 +-- cointop/table_header.go | 3 +-- pkg/api/impl/coingecko/coingecko.go | 8 ++++---- pkg/levenshtein/levenshtein.go | 2 +- pkg/table/align/align.go | 12 ++++++------ pkg/table/table.go | 12 ++++++------ pkg/termui/gauge.go | 2 +- 19 files changed, 60 insertions(+), 68 deletions(-) diff --git a/cmd/commands/holdings.go b/cmd/commands/holdings.go index 91d2967..8a01c8b 100644 --- a/cmd/commands/holdings.go +++ b/cmd/commands/holdings.go @@ -17,7 +17,7 @@ func HoldingsCmd() *cobra.Command { var config string var sortBy string var sortDesc bool - var format string = "table" + var format = "table" var humanReadable bool var filter []string var cols []string diff --git a/cmd/commands/server.go b/cmd/commands/server.go index 4fab961..91a06dc 100644 --- a/cmd/commands/server.go +++ b/cmd/commands/server.go @@ -14,15 +14,15 @@ import ( // ServerCmd ... func ServerCmd() *cobra.Command { - var port uint = 22 - var address string = "0.0.0.0" - var idleTimeout uint = 0 - var maxTimeout uint = 0 - var maxSessions uint = 0 - var executableBinary string = "cointop" - var hostKeyFile string = cssh.DefaultHostKeyFile - var userConfigType string = cssh.UserConfigTypePublicKey - var colorsDir string = os.Getenv("COINTOP_COLORS_DIR") + port := uint(22) + address := "0.0.0.0" + idleTimeout := uint(0) + maxTimeout := uint(0) + maxSessions := uint(0) + executableBinary := "cointop" + hostKeyFile := cssh.DefaultHostKeyFile + userConfigType := cssh.UserConfigTypePublicKey + colorsDir := os.Getenv("COINTOP_COLORS_DIR") serverCmd := &cobra.Command{ Use: "server", diff --git a/cointop/chart.go b/cointop/chart.go index 25875fd..dcca3f4 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -25,8 +25,7 @@ type ChartView = ui.View // NewChartView returns a new chart view func NewChartView() *ChartView { - var view *ChartView = ui.NewView("chart") - return view + return ui.NewView("chart") } var chartLock sync.Mutex @@ -50,17 +49,17 @@ func ChartRanges() []string { // ChartRangesMap returns map of chart range time ranges func ChartRangesMap() map[string]time.Duration { return map[string]time.Duration{ - "All Time": time.Duration(10 * 365 * 24 * time.Hour), - "YTD": time.Duration(1 * time.Second), // this will be calculated - "1Y": time.Duration(365 * 24 * time.Hour), - "6M": time.Duration(365 / 2 * 24 * time.Hour), - "3M": time.Duration(365 / 4 * 24 * time.Hour), - "1M": time.Duration(365 / 12 * 24 * time.Hour), - "7D": time.Duration(24 * 7 * time.Hour), - "3D": time.Duration(24 * 3 * time.Hour), - "24H": time.Duration(24 * time.Hour), - "6H": time.Duration(6 * time.Hour), - "1H": time.Duration(1 * time.Hour), + "All Time": 10 * 365 * 24 * time.Hour, + "YTD": 1 * time.Second, // this will be calculated + "1Y": 365 * 24 * time.Hour, + "6M": 365 / 2 * 24 * time.Hour, + "3M": 365 / 4 * 24 * time.Hour, + "1M": 365 / 12 * 24 * time.Hour, + "7D": 24 * 7 * time.Hour, + "3D": 24 * 3 * time.Hour, + "24H": 24 * time.Hour, + "6H": 6 * time.Hour, + "1H": 1 * time.Hour, } } @@ -119,7 +118,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange] if ct.State.selectedChartRange == "YTD" { - ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) + ytd := time.Now().Unix() - timeutil.BeginningOfYear().Unix() rangeseconds = time.Duration(ytd) * time.Second } @@ -216,7 +215,7 @@ func (ct *Cointop) PortfolioChart() error { selectedChartRange := ct.State.selectedChartRange // cache here rangeseconds := ct.chartRangesMap[selectedChartRange] if selectedChartRange == "YTD" { - ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) + ytd := time.Now().Unix() - timeutil.BeginningOfYear().Unix() rangeseconds = time.Duration(ytd) * time.Second } diff --git a/cointop/cointop.go b/cointop/cointop.go index 8f47d68..fa54c78 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -182,19 +182,19 @@ var DefaultCurrency = "USD" var DefaultChartRange = "1Y" // DefaultMaxChartWidth ... -var DefaultMaxChartWidth int = 175 +var DefaultMaxChartWidth = 175 // DefaultChartHeight ... -var DefaultChartHeight int = 10 +var DefaultChartHeight = 10 // DefaultSortBy ... var DefaultSortBy = "rank" // DefaultPerPage ... -var DefaultPerPage uint = 100 +var DefaultPerPage = uint(100) // DefaultMaxPages ... -var DefaultMaxPages uint = 35 +var DefaultMaxPages = uint(35) // DefaultColorscheme ... var DefaultColorscheme = "cointop" diff --git a/cointop/config.go b/cointop/config.go index 8b32c04..02cc058 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -227,9 +227,9 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { if !ok || entry.Coin == "" { continue } - var amount string = strconv.FormatFloat(entry.Holdings, 'f', -1, 64) - var coinName string = entry.Coin - var tuple []string = []string{coinName, amount} + amount := strconv.FormatFloat(entry.Holdings, 'f', -1, 64) + coinName := entry.Coin + tuple := []string{coinName, amount} holdingsIfc = append(holdingsIfc, tuple) } sort.Slice(holdingsIfc, func(i, j int) bool { diff --git a/cointop/keybindings.go b/cointop/keybindings.go index cb2993c..2f0b6b3 100644 --- a/cointop/keybindings.go +++ b/cointop/keybindings.go @@ -377,7 +377,7 @@ func (ct *Cointop) SetKeybindings() error { // TODO: use scrolling table keys := ct.SortedSupportedCurrencyConversions() for i, k := range keys { - ct.SetKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.Menu.Name()) + ct.SetKeybindingMod(alphanumericcharacters[i], gocui.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.Menu.Name()) } return nil diff --git a/cointop/marketbar.go b/cointop/marketbar.go index 95a7d96..d43478a 100644 --- a/cointop/marketbar.go +++ b/cointop/marketbar.go @@ -19,8 +19,7 @@ type MarketbarView = ui.View // NewMarketbarView returns a new marketbar view func NewMarketbarView() *MarketbarView { - var view *MarketbarView = ui.NewView("marketbar") - return view + return ui.NewView("marketbar") } // UpdateMarketbar updates the market bar view @@ -54,7 +53,7 @@ func (ct *Cointop) UpdateMarketbar() error { var percentChange24H float64 for _, p := range ct.GetPortfolioSlice() { - n := ((p.Balance / total) * p.PercentChange24H) + n := (p.Balance / total) * p.PercentChange24H if math.IsNaN(n) { continue } diff --git a/cointop/menu.go b/cointop/menu.go index 4da3640..c138b26 100644 --- a/cointop/menu.go +++ b/cointop/menu.go @@ -10,8 +10,7 @@ type MenuView = ui.View // NewMenuView returns a new menu view func NewMenuView() *MenuView { - var view *MenuView = ui.NewView("menu") - return view + return ui.NewView("menu") } // HideMenu hides the menu view diff --git a/cointop/navigation.go b/cointop/navigation.go index 8d98aa6..abbd2be 100644 --- a/cointop/navigation.go +++ b/cointop/navigation.go @@ -413,7 +413,7 @@ func (ct *Cointop) GoToGlobalIndex(idx int) error { l := ct.TableRowsLen() atpage := idx / l ct.SetPage(atpage) - rowIndex := (idx % l) + rowIndex := idx % l ct.HighlightRow(rowIndex) ct.UpdateTable() return nil diff --git a/cointop/portfolio.go b/cointop/portfolio.go index 9ce4564..722c217 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -983,7 +983,7 @@ func (ct *Cointop) PrintHoldings24HChange(options *TablePrintOptions) error { } } - n := ((entry.Balance / total) * entry.PercentChange24H) + n := (entry.Balance / total) * entry.PercentChange24H if math.IsNaN(n) { continue } diff --git a/cointop/search.go b/cointop/search.go index 7e95ff3..9643cce 100644 --- a/cointop/search.go +++ b/cointop/search.go @@ -14,8 +14,7 @@ type SearchFieldView = ui.View // NewSearchFieldView returns a new search field view func NewSearchFieldView() *SearchFieldView { - var view *SearchFieldView = ui.NewView("searchfield") - return view + return ui.NewView("searchfield") } // InputView is structure for help view @@ -23,8 +22,7 @@ type InputView = ui.View // NewInputView returns a new help view func NewInputView() *InputView { - var view *InputView = ui.NewView("input") - return view + return ui.NewView("input") } // OpenSearch opens the search field diff --git a/cointop/statusbar.go b/cointop/statusbar.go index 65fcaa4..5bd1e29 100644 --- a/cointop/statusbar.go +++ b/cointop/statusbar.go @@ -15,8 +15,7 @@ type StatusbarView = ui.View // NewStatusbarView returns a new statusbar view func NewStatusbarView() *StatusbarView { - var view *StatusbarView = ui.NewView("statusbar") - return view + return ui.NewView("statusbar") } // UpdateStatusbar updates the statusbar view diff --git a/cointop/table.go b/cointop/table.go index 9cc0575..bfdf8ab 100644 --- a/cointop/table.go +++ b/cointop/table.go @@ -14,8 +14,7 @@ type TableView = ui.View // NewTableView returns a new table view func NewTableView() *TableView { - var view *TableView = ui.NewView("table") - return view + return ui.NewView("table") } const dots = "..." diff --git a/cointop/table_header.go b/cointop/table_header.go index c3d36ed..ded019e 100644 --- a/cointop/table_header.go +++ b/cointop/table_header.go @@ -128,8 +128,7 @@ type TableHeaderView = ui.View // NewTableHeaderView returns a new table header view func NewTableHeaderView() *TableHeaderView { - var view *TableHeaderView = ui.NewView("table_header") - return view + return ui.NewView("table_header") } // GetActiveTableHeaders returns the list of active table headers diff --git a/pkg/api/impl/coingecko/coingecko.go b/pkg/api/impl/coingecko/coingecko.go index 8990292..e196870 100644 --- a/pkg/api/impl/coingecko/coingecko.go +++ b/pkg/api/impl/coingecko/coingecko.go @@ -37,10 +37,10 @@ type Service struct { // NewCoinGecko new service func NewCoinGecko(config *Config) *Service { - var maxResultsPerPage uint = 250 // absolute max - var maxResults uint = 0 - var maxPages uint = 10 - var perPage uint = 100 + maxResultsPerPage := 250 // absolute max + maxResults := uint(0) + maxPages := uint(10) + perPage := uint(100) if config.PerPage > 0 { perPage = config.PerPage } diff --git a/pkg/levenshtein/levenshtein.go b/pkg/levenshtein/levenshtein.go index f855d0c..f76bfb2 100644 --- a/pkg/levenshtein/levenshtein.go +++ b/pkg/levenshtein/levenshtein.go @@ -45,7 +45,7 @@ func DamerauLevenshteinDistance(s1, s2 string) int { // min returns the minimum number of passed int slices. func min(is ...int) int { - min := int(math.MaxInt32) + min := math.MaxInt32 for _, v := range is { if min > v { min = v diff --git a/pkg/table/align/align.go b/pkg/table/align/align.go index 5e0c97d..78568c0 100644 --- a/pkg/table/align/align.go +++ b/pkg/table/align/align.go @@ -8,8 +8,8 @@ import ( "github.com/acarl005/stripansi" ) -// AlignLeft align left -func AlignLeft(t string, n int) string { +// Left align left +func Left(t string, n int) string { s := stripansi.Strip(t) slen := utf8.RuneCountInString(s) if slen > n { @@ -19,8 +19,8 @@ func AlignLeft(t string, n int) string { return fmt.Sprintf("%s%s", t, strings.Repeat(" ", n-slen)) } -// AlignRight align right -func AlignRight(t string, n int) string { +// Right align right +func Right(t string, n int) string { s := stripansi.Strip(t) slen := utf8.RuneCountInString(s) if slen > n { @@ -30,8 +30,8 @@ func AlignRight(t string, n int) string { return fmt.Sprintf("%s%s", strings.Repeat(" ", n-slen), t) } -// AlignCenter align center -func AlignCenter(t string, n int) string { +// Center align center +func Center(t string, n int) string { s := stripansi.Strip(t) slen := utf8.RuneCountInString(s) if slen > n { diff --git a/pkg/table/table.go b/pkg/table/table.go index fea28e0..341bfa0 100644 --- a/pkg/table/table.go +++ b/pkg/table/table.go @@ -205,11 +205,11 @@ func (t *Table) Fprint(w io.Writer) { var s string switch c.align { case AlignLeft: - s = align.AlignLeft(c.name+" ", c.width) + s = align.Left(c.name+" ", c.width) case AlignRight: - s = align.AlignRight(c.name+" ", c.width) + s = align.Right(c.name+" ", c.width) case AlignCenter: - s = align.AlignCenter(c.name+" ", c.width) + s = align.Center(c.name+" ", c.width) } fmt.Fprintf(w, "%s", s) @@ -237,11 +237,11 @@ func (t *Table) Fprint(w io.Writer) { var s string switch c.align { case AlignLeft: - s = align.AlignLeft(v, c.width) + s = align.Left(v, c.width) case AlignRight: - s = align.AlignRight(v, c.width) + s = align.Right(v, c.width) case AlignCenter: - s = align.AlignCenter(v, c.width) + s = align.Center(v, c.width) } fmt.Fprintf(w, "%s", s) diff --git a/pkg/termui/gauge.go b/pkg/termui/gauge.go index 9f6ce3a..ca57975 100644 --- a/pkg/termui/gauge.go +++ b/pkg/termui/gauge.go @@ -21,7 +21,7 @@ import ( g.PercentColor = termui.ColorBlue */ -const ColorUndef Attribute = Attribute(^uint16(0)) +const ColorUndef = Attribute(^uint16(0)) type Gauge struct { Block From 3b37cc34c35a0216e360980d4eb0ac9754e47921 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 7 Oct 2021 08:02:21 +1100 Subject: [PATCH 4/4] Scale large numbers by adding Million Billion Trillion suffix (#200) Add option for scaling Thousand Million Billion Trillion numbers by adding suffix. --- cointop/coins_table.go | 12 ++++++++ cointop/cointop.go | 12 ++++++++ cointop/config.go | 37 ++++++++++++++++++++---- cointop/marketbar.go | 14 +++++++-- cointop/table_header.go | 34 ++++++++++++++++++++-- docs/content/faq.md | 6 ++++ pkg/humanize/humanize.go | 53 ++++++++++++++++++++++++++++++++--- pkg/humanize/humanize_test.go | 41 +++++++++++++++++++++++++++ 8 files changed, 195 insertions(+), 14 deletions(-) diff --git a/cointop/coins_table.go b/cointop/coins_table.go index e8ac01f..79dff1b 100644 --- a/cointop/coins_table.go +++ b/cointop/coins_table.go @@ -136,6 +136,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table { }) case "24h_volume": text := humanize.Monetaryf(coin.Volume24H, 0) + if ct.IsActiveTableCompactNotation() { + text = humanize.ScaleNumericf(coin.Volume24H, 3) + } ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -243,6 +246,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table { }) case "market_cap": text := humanize.Monetaryf(coin.MarketCap, 0) + if ct.IsActiveTableCompactNotation() { + text = humanize.ScaleNumericf(coin.MarketCap, 3) + } ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -255,6 +261,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table { }) case "total_supply": text := humanize.Numericf(coin.TotalSupply, 0) + if ct.IsActiveTableCompactNotation() { + text = humanize.ScaleNumericf(coin.TotalSupply, 3) + } ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -267,6 +276,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table { }) case "available_supply": text := humanize.Numericf(coin.AvailableSupply, 0) + if ct.IsActiveTableCompactNotation() { + text = humanize.ScaleNumericf(coin.AvailableSupply, 3) + } ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, diff --git a/cointop/cointop.go b/cointop/cointop.go index fa54c78..ca3afec 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -89,6 +89,11 @@ type State struct { priceAlerts *PriceAlerts priceAlertEditID string priceAlertNewID string + + compactNotation bool + tableCompactNotation bool + favoritesCompactNotation bool + portfolioCompactNotation bool } // Cointop cointop @@ -181,6 +186,9 @@ var DefaultCurrency = "USD" // DefaultChartRange ... var DefaultChartRange = "1Y" +// DefaultCompactNotation ... +var DefaultCompactNotation = false + // DefaultMaxChartWidth ... var DefaultMaxChartWidth = 175 @@ -291,6 +299,10 @@ func NewCointop(config *Config) (*Cointop, error) { Entries: make([]*PriceAlert, 0), SoundEnabled: true, }, + compactNotation: DefaultCompactNotation, + tableCompactNotation: DefaultCompactNotation, + favoritesCompactNotation: DefaultCompactNotation, + portfolioCompactNotation: DefaultCompactNotation, }, Views: &Views{ Chart: NewChartView(), diff --git a/cointop/config.go b/cointop/config.go index 02cc058..dbe1f94 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -48,6 +48,7 @@ type ConfigFileConfig struct { Colorscheme interface{} `toml:"colorscheme"` RefreshRate interface{} `toml:"refresh_rate"` CacheDir interface{} `toml:"cache_dir"` + CompactNotation interface{} `toml:"compact_notation"` Table map[string]interface{} `toml:"table"` Chart map[string]interface{} `toml:"chart"` } @@ -70,6 +71,7 @@ func (ct *Cointop) SetupConfig() error { ct.loadColorschemeFromConfig, ct.loadRefreshRateFromConfig, ct.loadCacheDirFromConfig, + ct.loadCompactNotationFromConfig, ct.loadPriceAlertsFromConfig, ct.loadPortfolioFromConfig, } @@ -215,10 +217,11 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { var favoritesBySymbolIfc []interface{} favoritesMapIfc := map[string]interface{}{ // DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility. - "symbols": favoritesBySymbolIfc, - "names": favoritesIfc, - "columns": ct.State.favoritesTableColumns, - "character": ct.State.favoriteChar, + "symbols": favoritesBySymbolIfc, + "names": favoritesIfc, + "columns": ct.State.favoritesTableColumns, + "character": ct.State.favoriteChar, + "compact_notation": ct.State.favoritesCompactNotation, } var holdingsIfc [][]string @@ -236,8 +239,9 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { return holdingsIfc[i][0] < holdingsIfc[j][0] }) portfolioIfc := map[string]interface{}{ - "holdings": holdingsIfc, - "columns": ct.State.portfolioTableColumns, + "holdings": holdingsIfc, + "columns": ct.State.portfolioTableColumns, + "compact_notation": ct.State.portfolioCompactNotation, } cmcIfc := map[string]interface{}{ @@ -264,6 +268,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { tableMapIfc := map[string]interface{}{ "columns": ct.State.coinsTableColumns, "keep_row_focus_on_sort": ct.State.keepRowFocusOnSort, + "compact_notation": ct.State.tableCompactNotation, } chartMapIfc := map[string]interface{}{ @@ -286,6 +291,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { CacheDir: ct.State.cacheDir, Table: tableMapIfc, Chart: chartMapIfc, + CompactNotation: ct.State.compactNotation, } var b bytes.Buffer @@ -310,6 +316,11 @@ func (ct *Cointop) loadTableConfig() error { if ok { ct.State.keepRowFocusOnSort = keepRowFocusOnSortIfc.(bool) } + + if compactNotation, ok := ct.config.Table["compact_notation"]; ok { + ct.State.tableCompactNotation = compactNotation.(bool) + } + return nil } @@ -458,6 +469,16 @@ func (ct *Cointop) loadCacheDirFromConfig() error { return nil } +// loadCompactNotationFromConfig loads compact-notation setting from config file to struct +func (ct *Cointop) loadCompactNotationFromConfig() error { + log.Debug("loadCompactNotationFromConfig()") + if compactNotation, ok := ct.config.CompactNotation.(bool); ok { + ct.State.compactNotation = compactNotation + } + + return nil +} + // LoadAPIChoiceFromConfig loads API choices from config file to struct func (ct *Cointop) loadAPIChoiceFromConfig() error { log.Debug("loadAPIKeysFromConfig()") @@ -480,6 +501,8 @@ func (ct *Cointop) loadFavoritesFromConfig() error { } ct.State.favoriteChar = favoriteChar } + } else if k == "compact_notation" { + ct.State.favoritesCompactNotation = valueIfc.(bool) } ifcs, ok := valueIfc.([]interface{}) if !ok { @@ -568,6 +591,8 @@ func (ct *Cointop) loadPortfolioFromConfig() error { return err } } + } else if key == "compact_notation" { + ct.State.portfolioCompactNotation = valueIfc.(bool) } else { // Backward compatibility < v1.6.0 holdings, err := ct.InterfaceToFloat64(valueIfc) diff --git a/cointop/marketbar.go b/cointop/marketbar.go index d43478a..a381573 100644 --- a/cointop/marketbar.go +++ b/cointop/marketbar.go @@ -40,6 +40,9 @@ func (ct *Cointop) UpdateMarketbar() error { total = math.Round(total*1e2) / 1e2 totalstr = humanize.Monetaryf(total, 2) } + if ct.State.compactNotation { + totalstr = humanize.ScaleNumericf(total, 3) + } timeframe := ct.State.selectedChartRange chartname := ct.SelectedCoinName() @@ -153,12 +156,19 @@ func (ct *Cointop) UpdateMarketbar() error { separator2 = "\n" + offset } + marketCapStr := humanize.Monetaryf(market.TotalMarketCapUSD, 0) + volumeStr := humanize.Monetaryf(market.Total24HVolumeUSD, 0) + if ct.State.compactNotation { + marketCapStr = humanize.ScaleNumericf(market.TotalMarketCapUSD, 3) + volumeStr = humanize.ScaleNumericf(market.Total24HVolumeUSD, 3) + } + content = fmt.Sprintf( "%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%", chartInfo, - fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.TotalMarketCapUSD, 0)), + fmt.Sprintf("%s%s", ct.CurrencySymbol(), marketCapStr), separator1, - fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.Total24HVolumeUSD, 0)), + fmt.Sprintf("%s%s", ct.CurrencySymbol(), volumeStr), separator2, market.BitcoinPercentageOfMarketCap, ) diff --git a/cointop/table_header.go b/cointop/table_header.go index ded019e..ffd222f 100644 --- a/cointop/table_header.go +++ b/cointop/table_header.go @@ -21,6 +21,7 @@ var ArrowDown = "▼" type HeaderColumn struct { Slug string Label string + ShortLabel string // only columns with a ShortLabel can be scaled? PlainLabel string } @@ -69,11 +70,13 @@ var HeaderColumns = map[string]*HeaderColumn{ "market_cap": { Slug: "market_cap", Label: "[m]arket cap", + ShortLabel: "[m]cap", PlainLabel: "market cap", }, "24h_volume": { Slug: "24h_volume", Label: "24H [v]olume", + ShortLabel: "24[v]", PlainLabel: "24H volume", }, "1h_change": { @@ -104,11 +107,13 @@ var HeaderColumns = map[string]*HeaderColumn{ "total_supply": { Slug: "total_supply", Label: "[t]otal supply", + ShortLabel: "[t]ot", PlainLabel: "total supply", }, "available_supply": { Slug: "available_supply", Label: "[a]vailable supply", + ShortLabel: "[a]vl", PlainLabel: "available supply", }, "percent_holdings": { @@ -123,6 +128,15 @@ var HeaderColumns = map[string]*HeaderColumn{ }, } +// GetLabel fetch the label to use for the heading (depends on configuration) +func (ct *Cointop) GetLabel(h *HeaderColumn) string { + // TODO: technically this should support nosort + if ct.IsActiveTableCompactNotation() && h.ShortLabel != "" { + return h.ShortLabel + } + return h.Label +} + // TableHeaderView is structure for table header view type TableHeaderView = ui.View @@ -145,6 +159,22 @@ func (ct *Cointop) GetActiveTableHeaders() []string { return cols } +// GetActiveTableHeaders returns the list of active table headers +func (ct *Cointop) IsActiveTableCompactNotation() bool { + var compact bool + switch ct.State.selectedView { + case PortfolioView: + compact = ct.State.portfolioCompactNotation + case CoinsView: + compact = ct.State.tableCompactNotation + case FavoritesView: + compact = ct.State.favoritesCompactNotation + default: + compact = ct.State.tableCompactNotation + } + return compact +} + // UpdateTableHeader renders the table header func (ct *Cointop) UpdateTableHeader() error { log.Debug("UpdateTableHeader()") @@ -175,7 +205,7 @@ func (ct *Cointop) UpdateTableHeader() error { } } } - label := hc.Label + label := ct.GetLabel(hc) if noSort { label = hc.PlainLabel } @@ -240,7 +270,7 @@ func (ct *Cointop) SetTableColumnWidth(header string, width int) { prev = prevIfc.(int) } else { hc := HeaderColumns[header] - prev = utf8.RuneCountInString(hc.Label) + 1 + prev = utf8.RuneCountInString(ct.GetLabel(hc)) + 1 switch header { case "price", "balance": prev++ diff --git a/docs/content/faq.md b/docs/content/faq.md index 00fe0f2..8a700a2 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -357,6 +357,12 @@ draft: false Supported columns relating to price change are `1h_change`, `24h_change`, `7d_change`, `30d_change`, `1y_change` +## How can I use K (thousand), M (million), B (billion), T (trillion) suffixes for shorter numbers? + + There is a setting at the top-level of the configuration file called `compact_notation=true` which changes the marketbar values `market cap`, `volume` and `portfolio total value`. + + The same setting can be applied at in the `[table]` section to impact the `24h_volume`, `market_cap`, `total_supply`, `available_supply` columns in the main coin view; and in the `[favorites]` section to change the same columns. The setting also changes the column names to be shorter. + ## How can use a different config file other than the default? Run cointop with the `--config` flag, eg `cointop --config="/path/to/config.toml"`, to use the specified file as the config. diff --git a/pkg/humanize/humanize.go b/pkg/humanize/humanize.go index 61a0660..1d45354 100644 --- a/pkg/humanize/humanize.go +++ b/pkg/humanize/humanize.go @@ -2,6 +2,7 @@ package humanize import ( "fmt" + "math" "os" "strconv" "strings" @@ -12,7 +13,7 @@ import ( // Numericf produces a string from of the given number with give fixed precision // in base 10 with thousands separators after every three orders of magnitude -// using a thousands and decimal spearator according to LC_NUMERIC; defaulting "en". +// using thousands and decimal separator according to LC_NUMERIC; defaulting "en". // // e.g. Numericf(834142.32, 2) -> "834,142.32" func Numericf(value float64, precision int) string { @@ -21,16 +22,16 @@ func Numericf(value float64, precision int) string { // Monetaryf produces a string from of the given number give minimum precision // in base 10 with thousands separators after every three orders of magnitude -// using thousands and decimal spearator according to LC_MONETARY; defaulting "en". +// using thousands and decimal separator according to LC_MONETARY; defaulting "en". // // e.g. Monetaryf(834142.3256, 2) -> "834,142.3256" func Monetaryf(value float64, precision int) string { return f(value, precision, "LC_MONETARY", false) } -// f formats given value v, with d decimal places using thousands and decimal +// f formats given value, with precision decimal places using thousands and decimal // separator according to language found in given locale environment variable e. -// If r is true the decimal places are fixed to the given d otherwise d is the +// If fixed is true the decimal places are fixed to the given precision otherwise d is the // minimum of decimal places until the first 0. func f(value float64, precision int, envvar string, fixed bool) string { parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".") @@ -51,3 +52,47 @@ func f(value float64, precision int, envvar string, fixed bool) string { format := fmt.Sprintf("%%.%df", precision) return message.NewPrinter(lang).Sprintf(format, value) } + +// Scale returns a scaled-down version of value and a suffix to add (M,B,etc.) +func Scale(value float64) (float64, string) { + type scalingUnit struct { + value float64 + suffix string + } + + // quadrillion, quintrillion, sextillion, septillion, octillion, nonillion, and decillion + var scales = [...]scalingUnit{ + {value: 1e12, suffix: "T"}, + {value: 1e9, suffix: "B"}, + {value: 1e6, suffix: "M"}, + {value: 1e3, suffix: "K"}, + } + + for _, scale := range scales { + if math.Abs(value) > scale.value { + return value / scale.value, scale.suffix + } + } + return value, "" +} + +// ScaleNumericf scales a large number down using a suffix, then formats it with the +// prescribed number of significant digits. +func ScaleNumericf(value float64, digits int) string { + value, suffix := Scale(value) + + // Round the scaled value to a certain number of significant figures + var s string + if math.Abs(value) < 1 { + s = Numericf(value, digits) + } else { + numDigits := len(fmt.Sprintf("%.0f", math.Abs(value))) + if numDigits >= digits { + s = Numericf(value, 0) + } else { + s = Numericf(value, digits-numDigits) + } + } + + return s + suffix +} diff --git a/pkg/humanize/humanize_test.go b/pkg/humanize/humanize_test.go index e7c3de8..4051382 100644 --- a/pkg/humanize/humanize_test.go +++ b/pkg/humanize/humanize_test.go @@ -1,6 +1,7 @@ package humanize import ( + "fmt" "testing" ) @@ -10,3 +11,43 @@ func TestMonetary(t *testing.T) { t.FailNow() } } + +func TestScale(t *testing.T) { + scaleTests := map[float64]string{ + 5.54 * 1e12: "5.5T", + 4.44 * 1e9: "4.4B", + 3.34 * 1e6: "3.3M", + 2.24 * 1e3: "2.2K", + 1.1: "1.1", + 0.06: "0.1", + 0.04: "0.0", + -5.54 * 1e12: "-5.5T", + } + + for value, expected := range scaleTests { + volScale, volSuffix := Scale(value) + result := fmt.Sprintf("%.1f%s", volScale, volSuffix) + if result != expected { + t.Fatalf("Expected %f to scale to '%s' but got '%s'\n", value, expected, result) + } + } +} + +func TestScaleNumeric(t *testing.T) { + scaleTests := map[float64]string{ + 5.54 * 1e12: "5.5T", + 4.44 * 1e9: "4.4B", + 3.34 * 1e6: "3.3M", + 2.24 * 1e3: "2.2K", + 1.1: "1.1", + 0.0611: "0.06", + -5.5432 * 1e12: "-5.5T", + } + + for value, expected := range scaleTests { + result := ScaleNumericf(value, 2) + if result != expected { + t.Fatalf("Expected %f to scale to '%s' but got '%s'\n", value, expected, result) + } + } +}