cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / numfmt / round.go
round.go 4.8K
1package numfmt23import (4 "math"5 "strconv"6 "strings"78 "src.rest/cat/012/internal/locale"9)1011// Rounding is how Fixed and Round treat the digits they drop.12type Rounding int1314const (15 HalfUp Rounding = iota // ROUND and number formats16 Up // away from zero: ROUNDUP17 Down // toward zero: ROUNDDOWN18)1920// Fixed renders |x| rounded to n decimal places (n may be negative) as21// its integer digits (no leading zeros, "" for zero) and exactly n22// decimal digits. It works on the 15 significant digits spreadsheets23// show, so 1.005 rounds to 1.01 as users expect rather than to the 1.0024// its binary value would give.25func Fixed(x float64, n int, mode Rounding) (whole, frac string) {26 x = math.Abs(x)27 nFrac := max(n, 0)28 if x == 0 || math.IsInf(x, 0) || math.IsNaN(x) {29 return "", strings.Repeat("0", nFrac)30 }31 s := strconv.FormatFloat(x, 'e', 14, 64)32 mant, e, _ := strings.Cut(s, "e")33 exp, _ := strconv.Atoi(e)34 d := []byte(strings.Replace(mant, ".", "", 1))35 point := exp + 1 // digits before the decimal point36 if point < 0 {37 d = append([]byte(strings.Repeat("0", -point)), d...)38 point = 039 }40 keep := point + n41 if keep < 0 {42 // Far smaller than the rounding unit.43 if mode == Up {44 return "1" + strings.Repeat("0", -n), strings.Repeat("0", nFrac)45 }46 return "", strings.Repeat("0", nFrac)47 }48 if keep < len(d) {49 if roundsUp(d[keep:], mode) {50 d, point = increment(d[:keep], point)51 } else {52 d = d[:keep]53 }54 }55 for len(d) < point+nFrac {56 d = append(d, '0')57 }58 whole = strings.TrimLeft(string(d[:point]), "0")59 frac = string(d[point : point+nFrac])60 return whole, frac61}6263// roundsUp reports whether dropping the digits dropped rounds the kept64// ones up.65func roundsUp(dropped []byte, mode Rounding) bool {66 switch mode {67 case HalfUp:68 return dropped[0] >= '5'69 case Up:70 return strings.Trim(string(dropped), "0") != ""71 }72 return false73}7475// increment adds one to the last of the digits d, carrying; a carry out76// of the first digit adds a digit before the point.77func increment(d []byte, point int) ([]byte, int) {78 for i := len(d) - 1; i >= 0; i-- {79 if d[i] != '9' {80 d[i]++81 return d, point82 }83 d[i] = '0'84 }85 return append([]byte{'1'}, d...), point + 186}8788// Round rounds x to places decimal places (negative places round to89// tens, hundreds...) with mode, on x's 15 significant digits.90func Round(x float64, places int, mode Rounding) float64 {91 whole, frac := Fixed(x, places, mode)92 s := whole93 if s == "" {94 s = "0"95 }96 if frac != "" {97 s += "." + frac98 }99 r, _ := strconv.ParseFloat(s, 64)100 if x < 0 {101 r = -r102 }103 return r104}105106// General renders a number the way Sheets shows it in General format and107// in text: up to 15 significant digits, no exponent for ordinary108// magnitudes.109func General(v float64) string {110 if v == 0 {111 return "0"112 }113 r, _ := strconv.ParseFloat(strconv.FormatFloat(v, 'g', 15, 64), 64)114 if a := math.Abs(r); a >= 1e21 || a < 1e-9 {115 return strconv.FormatFloat(r, 'E', -1, 64)116 }117 return strconv.FormatFloat(r, 'f', -1, 64)118}119120// Localize writes a number in General's form (digits, a point, an121// exponent, no grouping) with loc's decimal separator.122func Localize(s string, loc *locale.Locale) string {123 if loc.Decimal == '.' {124 return s125 }126 return strings.Replace(s, ".", string(loc.Decimal), 1)127}128129// GeneralIn is General as shown in loc: 1,5 in de-DE.130func GeneralIn(v float64, loc *locale.Locale) string { return Localize(General(v), loc) }131132// GeneralFitIn is GeneralFit as shown in loc.133func GeneralFitIn(v float64, width int, loc *locale.Locale) string {134 return Localize(GeneralFit(v, width), loc)135}136137// GeneralFit renders v in General format within width columns: as many138// digits as fit, rounding decimals first, then scientific notation, then139// #s.140func GeneralFit(v float64, width int) string {141 if width <= 0 {142 return ""143 }144 s := General(v)145 if strings.ContainsRune(s, 'E') {146 s = strconv.FormatFloat(v, 'f', -1, 64)147 }148 if len(s) <= width {149 return s150 }151 if f, ok := fewerDecimals(v, s, width); ok {152 return f153 }154 for prec := width; prec >= 0; prec-- {155 if e := strconv.FormatFloat(v, 'E', prec, 64); len(e) <= width {156 return e157 }158 }159 return strings.Repeat("#", width)160}161162// fewerDecimals rounds v, written in full as s, to the decimals that fit163// in width, but never rounds a non-zero number down to zero.164func fewerDecimals(v float64, s string, width int) (string, bool) {165 dot := strings.IndexByte(s, '.')166 if dot < 0 || dot > width {167 return "", false168 }169 for prec := max(width-dot-1, 0); prec >= 0; prec-- {170 f := strconv.FormatFloat(v, 'f', prec, 64)171 if len(f) > width {172 continue173 }174 if r, _ := strconv.ParseFloat(f, 64); r == 0 && v != 0 {175 return "", false176 }177 if strings.Contains(f, ".") {178 f = strings.TrimRight(strings.TrimRight(f, "0"), ".")179 }180 return f, true181 }182 return "", false183}