2
0
mirror of https://github.com/miguelmota/cointop synced 2024-11-10 13:10:26 +00:00

Scale large numbers by adding Million Billion Trillion suffix (#200)

Add option for scaling Thousand Million Billion Trillion numbers by adding suffix.
This commit is contained in:
Simon Roberts 2021-10-07 08:02:21 +11:00 committed by GitHub
parent e1aded93e8
commit 3b37cc34c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 14 deletions

View File

@ -136,6 +136,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "24h_volume": case "24h_volume":
text := humanize.Monetaryf(coin.Volume24H, 0) text := humanize.Monetaryf(coin.Volume24H, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.Volume24H, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -243,6 +246,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "market_cap": case "market_cap":
text := humanize.Monetaryf(coin.MarketCap, 0) text := humanize.Monetaryf(coin.MarketCap, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.MarketCap, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -255,6 +261,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "total_supply": case "total_supply":
text := humanize.Numericf(coin.TotalSupply, 0) text := humanize.Numericf(coin.TotalSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.TotalSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -267,6 +276,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "available_supply": case "available_supply":
text := humanize.Numericf(coin.AvailableSupply, 0) text := humanize.Numericf(coin.AvailableSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.AvailableSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,

View File

@ -89,6 +89,11 @@ type State struct {
priceAlerts *PriceAlerts priceAlerts *PriceAlerts
priceAlertEditID string priceAlertEditID string
priceAlertNewID string priceAlertNewID string
compactNotation bool
tableCompactNotation bool
favoritesCompactNotation bool
portfolioCompactNotation bool
} }
// Cointop cointop // Cointop cointop
@ -181,6 +186,9 @@ var DefaultCurrency = "USD"
// DefaultChartRange ... // DefaultChartRange ...
var DefaultChartRange = "1Y" var DefaultChartRange = "1Y"
// DefaultCompactNotation ...
var DefaultCompactNotation = false
// DefaultMaxChartWidth ... // DefaultMaxChartWidth ...
var DefaultMaxChartWidth = 175 var DefaultMaxChartWidth = 175
@ -291,6 +299,10 @@ func NewCointop(config *Config) (*Cointop, error) {
Entries: make([]*PriceAlert, 0), Entries: make([]*PriceAlert, 0),
SoundEnabled: true, SoundEnabled: true,
}, },
compactNotation: DefaultCompactNotation,
tableCompactNotation: DefaultCompactNotation,
favoritesCompactNotation: DefaultCompactNotation,
portfolioCompactNotation: DefaultCompactNotation,
}, },
Views: &Views{ Views: &Views{
Chart: NewChartView(), Chart: NewChartView(),

View File

@ -48,6 +48,7 @@ type ConfigFileConfig struct {
Colorscheme interface{} `toml:"colorscheme"` Colorscheme interface{} `toml:"colorscheme"`
RefreshRate interface{} `toml:"refresh_rate"` RefreshRate interface{} `toml:"refresh_rate"`
CacheDir interface{} `toml:"cache_dir"` CacheDir interface{} `toml:"cache_dir"`
CompactNotation interface{} `toml:"compact_notation"`
Table map[string]interface{} `toml:"table"` Table map[string]interface{} `toml:"table"`
Chart map[string]interface{} `toml:"chart"` Chart map[string]interface{} `toml:"chart"`
} }
@ -70,6 +71,7 @@ func (ct *Cointop) SetupConfig() error {
ct.loadColorschemeFromConfig, ct.loadColorschemeFromConfig,
ct.loadRefreshRateFromConfig, ct.loadRefreshRateFromConfig,
ct.loadCacheDirFromConfig, ct.loadCacheDirFromConfig,
ct.loadCompactNotationFromConfig,
ct.loadPriceAlertsFromConfig, ct.loadPriceAlertsFromConfig,
ct.loadPortfolioFromConfig, ct.loadPortfolioFromConfig,
} }
@ -219,6 +221,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
"names": favoritesIfc, "names": favoritesIfc,
"columns": ct.State.favoritesTableColumns, "columns": ct.State.favoritesTableColumns,
"character": ct.State.favoriteChar, "character": ct.State.favoriteChar,
"compact_notation": ct.State.favoritesCompactNotation,
} }
var holdingsIfc [][]string var holdingsIfc [][]string
@ -238,6 +241,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
portfolioIfc := map[string]interface{}{ portfolioIfc := map[string]interface{}{
"holdings": holdingsIfc, "holdings": holdingsIfc,
"columns": ct.State.portfolioTableColumns, "columns": ct.State.portfolioTableColumns,
"compact_notation": ct.State.portfolioCompactNotation,
} }
cmcIfc := map[string]interface{}{ cmcIfc := map[string]interface{}{
@ -264,6 +268,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
tableMapIfc := map[string]interface{}{ tableMapIfc := map[string]interface{}{
"columns": ct.State.coinsTableColumns, "columns": ct.State.coinsTableColumns,
"keep_row_focus_on_sort": ct.State.keepRowFocusOnSort, "keep_row_focus_on_sort": ct.State.keepRowFocusOnSort,
"compact_notation": ct.State.tableCompactNotation,
} }
chartMapIfc := map[string]interface{}{ chartMapIfc := map[string]interface{}{
@ -286,6 +291,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
CacheDir: ct.State.cacheDir, CacheDir: ct.State.cacheDir,
Table: tableMapIfc, Table: tableMapIfc,
Chart: chartMapIfc, Chart: chartMapIfc,
CompactNotation: ct.State.compactNotation,
} }
var b bytes.Buffer var b bytes.Buffer
@ -310,6 +316,11 @@ func (ct *Cointop) loadTableConfig() error {
if ok { if ok {
ct.State.keepRowFocusOnSort = keepRowFocusOnSortIfc.(bool) ct.State.keepRowFocusOnSort = keepRowFocusOnSortIfc.(bool)
} }
if compactNotation, ok := ct.config.Table["compact_notation"]; ok {
ct.State.tableCompactNotation = compactNotation.(bool)
}
return nil return nil
} }
@ -458,6 +469,16 @@ func (ct *Cointop) loadCacheDirFromConfig() error {
return nil 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 // LoadAPIChoiceFromConfig loads API choices from config file to struct
func (ct *Cointop) loadAPIChoiceFromConfig() error { func (ct *Cointop) loadAPIChoiceFromConfig() error {
log.Debug("loadAPIKeysFromConfig()") log.Debug("loadAPIKeysFromConfig()")
@ -480,6 +501,8 @@ func (ct *Cointop) loadFavoritesFromConfig() error {
} }
ct.State.favoriteChar = favoriteChar ct.State.favoriteChar = favoriteChar
} }
} else if k == "compact_notation" {
ct.State.favoritesCompactNotation = valueIfc.(bool)
} }
ifcs, ok := valueIfc.([]interface{}) ifcs, ok := valueIfc.([]interface{})
if !ok { if !ok {
@ -568,6 +591,8 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
return err return err
} }
} }
} else if key == "compact_notation" {
ct.State.portfolioCompactNotation = valueIfc.(bool)
} else { } else {
// Backward compatibility < v1.6.0 // Backward compatibility < v1.6.0
holdings, err := ct.InterfaceToFloat64(valueIfc) holdings, err := ct.InterfaceToFloat64(valueIfc)

View File

@ -40,6 +40,9 @@ func (ct *Cointop) UpdateMarketbar() error {
total = math.Round(total*1e2) / 1e2 total = math.Round(total*1e2) / 1e2
totalstr = humanize.Monetaryf(total, 2) totalstr = humanize.Monetaryf(total, 2)
} }
if ct.State.compactNotation {
totalstr = humanize.ScaleNumericf(total, 3)
}
timeframe := ct.State.selectedChartRange timeframe := ct.State.selectedChartRange
chartname := ct.SelectedCoinName() chartname := ct.SelectedCoinName()
@ -153,12 +156,19 @@ func (ct *Cointop) UpdateMarketbar() error {
separator2 = "\n" + offset 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( content = fmt.Sprintf(
"%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%", "%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%",
chartInfo, chartInfo,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.TotalMarketCapUSD, 0)), fmt.Sprintf("%s%s", ct.CurrencySymbol(), marketCapStr),
separator1, separator1,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.Total24HVolumeUSD, 0)), fmt.Sprintf("%s%s", ct.CurrencySymbol(), volumeStr),
separator2, separator2,
market.BitcoinPercentageOfMarketCap, market.BitcoinPercentageOfMarketCap,
) )

View File

@ -21,6 +21,7 @@ var ArrowDown = "▼"
type HeaderColumn struct { type HeaderColumn struct {
Slug string Slug string
Label string Label string
ShortLabel string // only columns with a ShortLabel can be scaled?
PlainLabel string PlainLabel string
} }
@ -69,11 +70,13 @@ var HeaderColumns = map[string]*HeaderColumn{
"market_cap": { "market_cap": {
Slug: "market_cap", Slug: "market_cap",
Label: "[m]arket cap", Label: "[m]arket cap",
ShortLabel: "[m]cap",
PlainLabel: "market cap", PlainLabel: "market cap",
}, },
"24h_volume": { "24h_volume": {
Slug: "24h_volume", Slug: "24h_volume",
Label: "24H [v]olume", Label: "24H [v]olume",
ShortLabel: "24[v]",
PlainLabel: "24H volume", PlainLabel: "24H volume",
}, },
"1h_change": { "1h_change": {
@ -104,11 +107,13 @@ var HeaderColumns = map[string]*HeaderColumn{
"total_supply": { "total_supply": {
Slug: "total_supply", Slug: "total_supply",
Label: "[t]otal supply", Label: "[t]otal supply",
ShortLabel: "[t]ot",
PlainLabel: "total supply", PlainLabel: "total supply",
}, },
"available_supply": { "available_supply": {
Slug: "available_supply", Slug: "available_supply",
Label: "[a]vailable supply", Label: "[a]vailable supply",
ShortLabel: "[a]vl",
PlainLabel: "available supply", PlainLabel: "available supply",
}, },
"percent_holdings": { "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 // TableHeaderView is structure for table header view
type TableHeaderView = ui.View type TableHeaderView = ui.View
@ -145,6 +159,22 @@ func (ct *Cointop) GetActiveTableHeaders() []string {
return cols 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 // UpdateTableHeader renders the table header
func (ct *Cointop) UpdateTableHeader() error { func (ct *Cointop) UpdateTableHeader() error {
log.Debug("UpdateTableHeader()") log.Debug("UpdateTableHeader()")
@ -175,7 +205,7 @@ func (ct *Cointop) UpdateTableHeader() error {
} }
} }
} }
label := hc.Label label := ct.GetLabel(hc)
if noSort { if noSort {
label = hc.PlainLabel label = hc.PlainLabel
} }
@ -240,7 +270,7 @@ func (ct *Cointop) SetTableColumnWidth(header string, width int) {
prev = prevIfc.(int) prev = prevIfc.(int)
} else { } else {
hc := HeaderColumns[header] hc := HeaderColumns[header]
prev = utf8.RuneCountInString(hc.Label) + 1 prev = utf8.RuneCountInString(ct.GetLabel(hc)) + 1
switch header { switch header {
case "price", "balance": case "price", "balance":
prev++ prev++

View File

@ -357,6 +357,12 @@ draft: false
Supported columns relating to price change are `1h_change`, `24h_change`, `7d_change`, `30d_change`, `1y_change` 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? ## 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. Run cointop with the `--config` flag, eg `cointop --config="/path/to/config.toml"`, to use the specified file as the config.

View File

@ -2,6 +2,7 @@ package humanize
import ( import (
"fmt" "fmt"
"math"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -12,7 +13,7 @@ import (
// Numericf produces a string from of the given number with give fixed precision // 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 // 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" // e.g. Numericf(834142.32, 2) -> "834,142.32"
func Numericf(value float64, precision int) string { 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 // Monetaryf produces a string from of the given number give minimum precision
// in base 10 with thousands separators after every three orders of magnitude // 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" // e.g. Monetaryf(834142.3256, 2) -> "834,142.3256"
func Monetaryf(value float64, precision int) string { func Monetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", false) 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. // 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. // minimum of decimal places until the first 0.
func f(value float64, precision int, envvar string, fixed bool) string { func f(value float64, precision int, envvar string, fixed bool) string {
parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".") 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) format := fmt.Sprintf("%%.%df", precision)
return message.NewPrinter(lang).Sprintf(format, value) 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
}

View File

@ -1,6 +1,7 @@
package humanize package humanize
import ( import (
"fmt"
"testing" "testing"
) )
@ -10,3 +11,43 @@ func TestMonetary(t *testing.T) {
t.FailNow() 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)
}
}
}