diff --git a/numbers.go b/numbers.go new file mode 100644 index 0000000..4fc16f8 --- /dev/null +++ b/numbers.go @@ -0,0 +1,235 @@ +package numbers + +import ( + "fmt" + "math" + "strconv" + "strings" +) + +func Scale(num float64, digits int) float64 { + if num == 0.0 { + return 0.0 + } + if math.IsNaN(num) { + return 0.0 + } + if math.IsInf(num, 0) { + return num + } + + original := num + num = math.Abs(num) + + d := math.Ceil(math.Log10(num)) + power := digits - int(d) + magnitude := math.Pow10(power) + shifted := math.Ceil(num * magnitude) + result := shifted / magnitude + + result = math.Copysign(result, original) + return result +} + +func SlideScale(num float64) float64 { + if math.Abs(num) < 100.0 { + return Scale(num, 1) + } + return Scale(num, 2) +} + +func ScaleDown(num float64, digits int) float64 { + if num == 0.0 { + return 0.0 + } + if math.IsNaN(num) { + return 0.0 + } + if math.IsInf(num, 0) { + return num + } + + original := num + num = math.Abs(num) + + result := 0.0 + + if num < 1.0 { + result = math.Floor(num*10.0) / 10.0 + } else { + d := math.Ceil(math.Log10(num)) + power := digits - int(d) + magnitude := math.Pow10(power) + shifted := math.Floor(num * magnitude) + result = shifted / magnitude + } + + result = math.Copysign(result, original) + return result +} + +func SlideScaleDown(num float64) float64 { + if math.Abs(num) < 100.0 { + return ScaleDown(num, 1) + } + return ScaleDown(num, 2) +} + +func CentsToDollars(cents float64) string { + dollars := cents / 100.0 + return fmt.Sprintf("$%.2f", dollars) +} + +func AddDelimiters(num float64) string { + s := strconv.FormatFloat(num, 'f', 3, 64) + if num < 1000.0 { + return s + } + pieces := strings.Split(s, ".") + digits := []string(nil) + + inum := pieces[0] + x := len(inum) + for x > 2 { + digits = append(digits, inum[x-3:x]) + x -= 3 + } + if x > 0 { + digits = append(digits, inum[0:x]) + } + + for i, j := 0, len(digits)-1; i < j; i, j = i+1, j-1 { + digits[i], digits[j] = digits[j], digits[i] + } + + // drop the fraction + // result := strings.Join(digits, ",") + "." + pieces[1] + result := strings.Join(digits, ",") + + return result +} + +func AddDelimitersInt(num int) string { + inum := strconv.Itoa(num) + digits := []string(nil) + + x := len(inum) + for x > 2 { + digits = append(digits, inum[x-3:x]) + x -= 3 + } + if x > 0 { + digits = append(digits, inum[0:x]) + } + + for i, j := 0, len(digits)-1; i < j; i, j = i+1, j-1 { + digits[i], digits[j] = digits[j], digits[i] + } + + // drop the fraction + // result := strings.Join(digits, ",") + "." + pieces[1] + result := strings.Join(digits, ",") + + return result +} + +func Display(num float64) string { + if num >= 10000.0 { + return Humanize(num) + } + if num >= 1000.0 { + return AddDelimiters(num) + } + + if num > 1.0 && math.Floor(num) == num { + return fmt.Sprintf("%.0f", num) + } + + if num == 0.0 { + return "0" + } + + return fmt.Sprintf("%.3f", num) +} + +func Humanize(num float64) string { + var exponent = 0.0 + if num != 0 { + exponent = math.Floor(math.Log10(math.Abs(num))) + } + + if exponent >= 3 { + unit := "K" + dispExponent := 3 + if exponent >= 15 { + unit = "Q" + dispExponent = 15 + } else if exponent >= 12 { + unit = "T" + dispExponent = 12 + } else if exponent >= 9 { + unit = "G" + dispExponent = 9 + } else if exponent >= 6 { + unit = "M" + dispExponent = 6 + } + num /= math.Pow10(dispExponent) + return fmt.Sprintf("%.2f%s", num, unit) + } + + return AddDelimiters(num) +} + +func Words(num float64) string { + var exponent = 0.0 + if num != 0 { + exponent = math.Floor(math.Log10(math.Abs(num))) + } + + if exponent >= 3 { + unit := "thousand" + dispExponent := 3 + if exponent >= 15 { + unit = "quadrillion" + dispExponent = 15 + } else if exponent >= 12 { + unit = "trillion" + dispExponent = 12 + } else if exponent >= 9 { + unit = "billion" + dispExponent = 9 + } else if exponent >= 6 { + unit = "million" + dispExponent = 6 + } + num /= math.Pow10(dispExponent) + return fmt.Sprintf("%.2f %s", num, unit) + } + + return AddDelimiters(num) +} + +func Percentage(current, old float64) float64 { + if old == 0.0 { + return 0.0 + } + return 100.0 * (current - old) / old +} + +func DisplayPercentage(percentage float64) string { + return Display(percentage) +} + +// using midpoint method +func PercentageMid(current, old float64) float64 { + mid := 0.5 * (current + old) + if mid == 0.0 { + return 0.0 + } + return 100.0 * (current - old) / mid +} + +func Megabytes(bytes uint64) float64 { + return float64(bytes) / (1024.0 * 1024.0) +} diff --git a/numbers_test.go b/numbers_test.go new file mode 100644 index 0000000..5813b1d --- /dev/null +++ b/numbers_test.go @@ -0,0 +1,120 @@ +package numbers + +import ( + "testing" +) + +func TestAddDelim(t *testing.T) { + s := AddDelimiters(32.3) + if s != "32.300" { + t.Errorf("expected 32.3, not '%s'", s) + } + + s = AddDelimiters(3032.3) + if s != "3,032" { + t.Errorf("expected 3,032.3000, not '%s'", s) + } + + s = AddDelimiters(123131231233032.3) + if s != "123,131,231,233,032" { + t.Errorf("expected 3,032.3000, not '%s'", s) + } + + s = AddDelimiters(1123131231233032.3) + if s != "1,123,131,231,233,032" { + t.Errorf("expected 3,032.300, not '%s'", s) + } + s = AddDelimiters(12123131231233032.3) + if s != "12,123,131,231,233,032" { + t.Errorf("expected 3,032.300, not '%s'", s) + } + + s = AddDelimitersInt(12123131231233032) + if s != "12,123,131,231,233,032" { + t.Errorf("expected 3,032.300, not '%s'", s) + } +} + +type dtest struct { + num float64 + result string +} + +var dispTests = []dtest{{999.0, "999"}, + {0.3145, "0.315"}, + {1300000, "1.30M"}, + {871300000, "871.30M"}, + {1300000000, "1.30G"}, + {13000000000, "13.00G"}, + {1300000000000, "1.30T"}, + {1300000000000000, "1.30Q"}, + {0.0, "0"}, + {0.0000, "0"}, + {1001.0, "1,001"}} + +func TestDisplay(t *testing.T) { + for i, v := range dispTests { + s := Display(v.num) + if s != v.result { + t.Errorf("%d: expected '%s', got '%s'", i, v.result, s) + } + } +} + +func TestPercentage(t *testing.T) { + if Percentage(100.0, 0.0) != 0.0 { + t.Errorf("expected 0, got %f", Percentage(100.0, 0.0)) + } + if PercentageMid(100.0, 0.0) != 200.0 { + t.Errorf("expected 200%%, got %f", PercentageMid(100.0, 0.0)) + } + if Percentage(110.0, 100.0) != 10.0 { + t.Errorf("expected 10%%, got %f", Percentage(110.0, 100.0)) + } + if PercentageMid(110.0, 100.0) != 9.523809523809524 { + t.Errorf("expected 10%%, got %f", PercentageMid(110.0, 100.0)) + } +} + +func TestScale(t *testing.T) { + if Scale(1.23, 1) != 2.0 { + t.Errorf("expected 2.0, got %f", Scale(1.23, 1)) + } + + if ScaleDown(1.23, 1) != 1.0 { + t.Errorf("down scale to 1.0, got %f", ScaleDown(1.23, 1)) + } + if ScaleDown(2.9, 1) != 2.0 { + t.Errorf("down scale to 2.0, got %f", ScaleDown(2.9, 1)) + } + if ScaleDown(2934.0, 1) != 2000.0 { + t.Errorf("down scale to 2000.0, got %f", ScaleDown(2934.0, 1)) + } + if Scale(2934.0, 1) != 3000.0 { + t.Errorf("up scale to 3000.0, got %f", Scale(2934.0, 1)) + } + if Scale(-2.3, 1) != -3.0 { + t.Errorf("expected -3.0, got %f", Scale(-2.3, 1)) + } + if Scale(0.25, 1) != 0.3 { + t.Errorf("expected 0.3, got %f", Scale(0.25, 1)) + } + if Scale(0.00003, 1) != 0.00003 { + t.Errorf("expected 0.00003, got %f", Scale(0.00003, 1)) + } + if Scale(0.000023, 1) != 0.00003 { + t.Errorf("expected 0.00003, got %f", Scale(0.000023, 1)) + } + if Scale(-0.000023, 1) != -0.00003 { + t.Errorf("expected -0.00003, got %f", Scale(-0.000023, 1)) + } + if Scale(6210.0, 2) != 6300.0 { + t.Errorf("Scale(6210, 2) == %v, expected 6300.0", Scale(6210.0, 2)) + } + if ScaleDown(6210.0, 2) != 6200.0 { + t.Errorf("ScaleDown(6210, 2) == %v, expected 6200.0", ScaleDown(6210.0, 2)) + } + if Scale(0.251, 2) != 0.26 { + t.Errorf("scale 0.251, 2 = %v, expected 0.26", Scale(0.251, 2)) + } +}