diff --git a/cmd/ncdu/ncdu.go b/cmd/ncdu/ncdu.go index b3cce5f69..792e7e68f 100644 --- a/cmd/ncdu/ncdu.go +++ b/cmd/ncdu/ncdu.go @@ -373,7 +373,7 @@ func (u *UI) Draw() error { extras := "" if u.showCounts { if count > 0 { - extras += fmt.Sprintf("%8v ", fs.SizeSuffix(count)) + extras += fmt.Sprintf("%8v ", fs.CountSuffix(count)) } else { extras += " " } diff --git a/fs/countsuffix.go b/fs/countsuffix.go new file mode 100644 index 000000000..93870a208 --- /dev/null +++ b/fs/countsuffix.go @@ -0,0 +1,189 @@ +package fs + +// CountSuffix is parsed by flag with k/M/G decimal suffixes +import ( + "fmt" + "math" + "sort" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// CountSuffix is an int64 with a friendly way of printing setting +type CountSuffix int64 + +// Common multipliers for SizeSuffix +const ( + CountSuffixBase CountSuffix = 1 + Kilo = 1000 * CountSuffixBase + Mega = 1000 * Kilo + Giga = 1000 * Mega + Tera = 1000 * Giga + Peta = 1000 * Tera + Exa = 1000 * Peta +) +const ( + // CountSuffixMax is the largest CountSuffix multiplier + CountSuffixMax = Exa + // CountSuffixMaxValue is the largest value that can be used to create CountSuffix + CountSuffixMaxValue = math.MaxInt64 + // CountSuffixMinValue is the smallest value that can be used to create CountSuffix + CountSuffixMinValue = math.MinInt64 +) + +// Turn CountSuffix into a string and a suffix +func (x CountSuffix) string() (string, string) { + scaled := float64(0) + suffix := "" + switch { + case x < 0: + return "off", "" + case x == 0: + return "0", "" + case x < Kilo: + scaled = float64(x) + suffix = "" + case x < Mega: + scaled = float64(x) / float64(Kilo) + suffix = "k" + case x < Giga: + scaled = float64(x) / float64(Mega) + suffix = "M" + case x < Tera: + scaled = float64(x) / float64(Giga) + suffix = "G" + case x < Peta: + scaled = float64(x) / float64(Tera) + suffix = "T" + case x < Exa: + scaled = float64(x) / float64(Peta) + suffix = "P" + default: + scaled = float64(x) / float64(Exa) + suffix = "E" + } + if math.Floor(scaled) == scaled { + return fmt.Sprintf("%.0f", scaled), suffix + } + return fmt.Sprintf("%.3f", scaled), suffix +} + +// String turns CountSuffix into a string +func (x CountSuffix) String() string { + val, suffix := x.string() + return val + suffix +} + +// Unit turns CountSuffix into a string with a unit +func (x CountSuffix) Unit(unit string) string { + val, suffix := x.string() + if val == "off" { + return val + } + var suffixUnit string + if suffix != "" && unit != "" { + suffixUnit = suffix + unit + } else { + suffixUnit = suffix + unit + } + return val + " " + suffixUnit +} + +func (x *CountSuffix) multiplierFromSymbol(s byte) (found bool, multiplier float64) { + switch s { + case 'k', 'K': + return true, float64(Kilo) + case 'm', 'M': + return true, float64(Mega) + case 'g', 'G': + return true, float64(Giga) + case 't', 'T': + return true, float64(Tera) + case 'p', 'P': + return true, float64(Peta) + case 'e', 'E': + return true, float64(Exa) + default: + return false, float64(CountSuffixBase) + } +} + +// Set a CountSuffix +func (x *CountSuffix) Set(s string) error { + if len(s) == 0 { + return errors.New("empty string") + } + if strings.ToLower(s) == "off" { + *x = -1 + return nil + } + suffix := s[len(s)-1] + suffixLen := 1 + multiplierFound := false + var multiplier float64 + switch suffix { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.': + suffixLen = 0 + multiplier = float64(Kilo) + case 'b', 'B': + if len(s) > 1 { + suffix = s[len(s)-2] + if multiplierFound, multiplier = x.multiplierFromSymbol(suffix); multiplierFound { + suffixLen = 2 + } + } else { + multiplier = float64(CountSuffixBase) + } + default: + if multiplierFound, multiplier = x.multiplierFromSymbol(suffix); !multiplierFound { + return errors.Errorf("bad suffix %q", suffix) + } + } + s = s[:len(s)-suffixLen] + value, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + if value < 0 { + return errors.Errorf("size can't be negative %q", s) + } + value *= multiplier + *x = CountSuffix(value) + return nil +} + +// Type of the value +func (x *CountSuffix) Type() string { + return "CountSuffix" +} + +// Scan implements the fmt.Scanner interface +func (x *CountSuffix) Scan(s fmt.ScanState, ch rune) error { + token, err := s.Token(true, nil) + if err != nil { + return err + } + return x.Set(string(token)) +} + +// CountSuffixList is a slice CountSuffix values +type CountSuffixList []CountSuffix + +func (l CountSuffixList) Len() int { return len(l) } +func (l CountSuffixList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l CountSuffixList) Less(i, j int) bool { return l[i] < l[j] } + +// Sort sorts the list +func (l CountSuffixList) Sort() { + sort.Sort(l) +} + +// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON +func (x *CountSuffix) UnmarshalJSON(in []byte) error { + return UnmarshalJSONFlag(in, x, func(i int64) error { + *x = CountSuffix(i) + return nil + }) +} diff --git a/fs/countsuffix_test.go b/fs/countsuffix_test.go new file mode 100644 index 000000000..e05921c5b --- /dev/null +++ b/fs/countsuffix_test.go @@ -0,0 +1,147 @@ +package fs + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Check it satisfies the interface +var _ flagger = (*CountSuffix)(nil) + +func TestCountSuffixString(t *testing.T) { + for _, test := range []struct { + in float64 + want string + }{ + {0, "0"}, + {102, "102"}, + {1000, "1k"}, + {1000 * 1000, "1M"}, + {1000 * 1000 * 1000, "1G"}, + {10 * 1000 * 1000 * 1000, "10G"}, + {10.1 * 1000 * 1000 * 1000, "10.100G"}, + {-1, "off"}, + {-100, "off"}, + } { + ss := CountSuffix(test.in) + got := ss.String() + assert.Equal(t, test.want, got) + } +} + +func TestCountSuffixUnit(t *testing.T) { + for _, test := range []struct { + in float64 + want string + }{ + {0, "0 Byte"}, + {102, "102 Byte"}, + {1000, "1 kByte"}, + {1000 * 1000, "1 MByte"}, + {1000 * 1000 * 1000, "1 GByte"}, + {10 * 1000 * 1000 * 1000, "10 GByte"}, + {10.1 * 1000 * 1000 * 1000, "10.100 GByte"}, + {10 * 1000 * 1000 * 1000 * 1000, "10 TByte"}, + {10 * 1000 * 1000 * 1000 * 1000 * 1000, "10 PByte"}, + {1 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000, "1 EByte"}, + {-1, "off"}, + {-100, "off"}, + } { + ss := CountSuffix(test.in) + got := ss.Unit("Byte") + assert.Equal(t, test.want, got) + } +} + +func TestCountSuffixSet(t *testing.T) { + for _, test := range []struct { + in string + want int64 + err bool + }{ + {"0", 0, false}, + {"1b", 1, false}, + {"100B", 100, false}, + {"0.1k", 100, false}, + {"0.1", 100, false}, + {"1K", 1000, false}, + {"1k", 1000, false}, + {"1KB", 1000, false}, + {"1kB", 1000, false}, + {"1kb", 1000, false}, + {"1", 1000, false}, + {"2.5", 1000 * 2.5, false}, + {"1M", 1000 * 1000, false}, + {"1MB", 1000 * 1000, false}, + {"1.g", 1000 * 1000 * 1000, false}, + {"10G", 10 * 1000 * 1000 * 1000, false}, + {"10T", 10 * 1000 * 1000 * 1000 * 1000, false}, + {"10P", 10 * 1000 * 1000 * 1000 * 1000 * 1000, false}, + {"off", -1, false}, + {"OFF", -1, false}, + {"", 0, true}, + {"1q", 0, true}, + {"1.q", 0, true}, + {"1q", 0, true}, + {"-1K", 0, true}, + {"1i", 0, true}, + {"1iB", 0, true}, + {"1Ki", 0, true}, + {"1KiB", 0, true}, + } { + ss := CountSuffix(0) + err := ss.Set(test.in) + if test.err { + require.Error(t, err, test.in) + } else { + require.NoError(t, err, test.in) + } + assert.Equal(t, test.want, int64(ss)) + } +} + +func TestCountSuffixScan(t *testing.T) { + var v CountSuffix + n, err := fmt.Sscan(" 17M ", &v) + require.NoError(t, err) + assert.Equal(t, 1, n) + assert.Equal(t, CountSuffix(17000000), v) +} + +func TestCountSuffixUnmarshalJSON(t *testing.T) { + for _, test := range []struct { + in string + want int64 + err bool + }{ + {`"0"`, 0, false}, + {`"102B"`, 102, false}, + {`"1K"`, 1000, false}, + {`"2.5"`, 1000 * 2.5, false}, + {`"1M"`, 1000 * 1000, false}, + {`"1.g"`, 1000 * 1000 * 1000, false}, + {`"10G"`, 10 * 1000 * 1000 * 1000, false}, + {`"off"`, -1, false}, + {`""`, 0, true}, + {`"1q"`, 0, true}, + {`"-1K"`, 0, true}, + {`0`, 0, false}, + {`102`, 102, false}, + {`1000`, 1000, false}, + {`1000000000`, 1000000000, false}, + {`1.1.1`, 0, true}, + } { + var ss CountSuffix + err := json.Unmarshal([]byte(test.in), &ss) + if test.err { + require.Error(t, err, test.in) + } else { + require.NoError(t, err, test.in) + } + assert.Equal(t, test.want, int64(ss)) + } +}