1package numfmt23import (4 "math"5 "strconv"6 "strings"78 "src.rest/cat/012/internal/locale"9)1011// numLayout is where a number section puts each part of the number: the12// token indexes of its integer, decimal and exponent placeholders, its13// decimal point and exponent, and how it scales the value.14type numLayout struct {15 toks []ptok16 intPH, fracPH, expPH []int17 dot, exp int // token index, or -1 for none18 percents int // each % multiplies by 10019 general bool20 // grouped is set by commas between the integer placeholders, unless21 // literals sit among them too (a phone number pattern).22 grouped bool23 scale int // commas right after the last integer placeholder divide by 1000 each24 loc *locale.Locale25}2627func layoutNumber(toks []ptok) numLayout {28 l := numLayout{toks: toks, dot: -1, exp: -1}29 for i, t := range toks {30 switch t.kind {31 case ptDigit:32 l.addPlaceholder(i)33 case ptDot:34 if l.dot < 0 && l.exp < 0 {35 l.dot = i36 }37 case ptExp:38 if l.exp < 0 {39 l.exp = i40 }41 case ptPercent:42 l.percents++43 case ptGeneral:44 l.general = true45 }46 }47 if len(l.intPH) > 0 {48 first, last := l.intPH[0], l.intPH[len(l.intPH)-1]49 commas, literals := false, false50 for i := first + 1; i < last; i++ {51 commas = commas || toks[i].kind == ptComma52 literals = literals || toks[i].kind == ptLit53 }54 l.grouped = commas && !literals55 for i := last + 1; i < len(toks) && toks[i].kind == ptComma; i++ {56 l.scale++57 }58 }59 return l60}6162// addPlaceholder files the digit placeholder at token i under the part of63// the number it falls in.64func (l *numLayout) addPlaceholder(i int) {65 switch {66 case l.exp >= 0:67 l.expPH = append(l.expPH, i)68 case l.dot >= 0:69 l.fracPH = append(l.fracPH, i)70 default:71 l.intPH = append(l.intPH, i)72 }73}7475// numDigits are the digits of a number as a section shows it.76type numDigits struct {77 whole string // integer digits, "" for zero78 frac string // one digit per decimal placeholder79 exp string // exponent digits, zero-padded to the exponent's placeholders80 expNeg bool81}8283// digits rounds v to what the layout shows.84func (l *numLayout) digits(v float64) numDigits {85 switch {86 case l.general && len(l.intPH) == 0:87 // General shows its own decimals; placeholders after it get zeros.88 return numDigits{whole: Localize(General(v), l.loc), frac: strings.Repeat("0", len(l.fracPH))}89 case l.exp >= 0:90 return l.scientific(v)91 }92 whole, frac := Fixed(v, len(l.fracPH), HalfUp)93 return numDigits{whole: whole, frac: frac}94}9596// scientific splits v into a mantissa of one integer digit and an97// exponent.98func (l *numLayout) scientific(v float64) numDigits {99 e := 0100 if v != 0 {101 e = int(math.Floor(math.Log10(v)))102 }103 whole, frac := Fixed(v/math.Pow(10, float64(e)), len(l.fracPH), HalfUp)104 if whole == "10" { // rounding carried into another digit105 e++106 whole, frac = Fixed(v/math.Pow(10, float64(e)), len(l.fracPH), HalfUp)107 }108 exp := strconv.Itoa(max(e, -e))109 if len(exp) < len(l.expPH) {110 exp = strings.Repeat("0", len(l.expPH)-len(exp)) + exp111 }112 return numDigits{whole: whole, frac: frac, exp: exp, expNeg: e < 0}113}114115// place spreads the digits over the integer and decimal placeholders,116// returning each placeholder's text by token index.117func (l *numLayout) place(d numDigits) []string {118 out := make([]string, len(l.toks))119 // Right to left; the leftmost placeholder takes any remaining digits.120 rem := d.whole121 for k := len(l.intPH) - 1; k >= 0; k-- {122 i := l.intPH[k]123 switch ph := l.toks[i].s; {124 case k == 0 && rem != "":125 out[i], rem = rem, ""126 case rem != "":127 out[i], rem = rem[len(rem)-1:], rem[:len(rem)-1]128 case ph == "0":129 out[i] = "0"130 case ph == "?":131 out[i] = " "132 }133 }134 // Trailing zeros disappear under # and turn to spaces under ?.135 trimming := true136 for k := len(l.fracPH) - 1; k >= 0; k-- {137 i := l.fracPH[k]138 digit := d.frac[k : k+1]139 switch ph := l.toks[i].s; {140 case trimming && digit == "0" && ph == "#":141 case trimming && digit == "0" && ph == "?":142 out[i] = " "143 default:144 trimming = false145 out[i] = digit146 }147 }148 return out149}150151// formatNumber renders |v| with a number section as shown in loc; neg152// adds a minus sign.153func formatNumber(v float64, neg bool, toks []ptok, loc *locale.Locale) string {154 l := layoutNumber(toks)155 l.loc = loc156 for range l.percents {157 v *= 100158 }159 for range l.scale {160 v /= 1000161 }162 d := l.digits(v)163 if neg && strings.Trim(d.whole+d.frac, "0") == "" {164 neg = false // don't show -0.00165 }166 placed := l.place(d)167 var b strings.Builder168 if neg {169 b.WriteByte('-')170 }171 for i, t := range toks {172 l.write(&b, i, t, d, placed)173 }174 return b.String()175}176177// write writes token i of the section.178func (l *numLayout) write(b *strings.Builder, i int, t ptok, d numDigits, placed []string) {179 switch t.kind {180 case ptLit:181 b.WriteString(t.s)182 case ptDigit:183 l.writeDigit(b, i, d, placed)184 case ptDot:185 if i == l.dot {186 b.WriteByte(l.loc.Decimal)187 }188 case ptPercent:189 b.WriteByte('%')190 case ptExp:191 if i == l.exp {192 b.WriteString(expSign(t.s, d.expNeg))193 }194 case ptGeneral:195 if len(l.intPH) == 0 {196 b.WriteString(d.whole)197 }198 case ptComma:199 // Grouping and scaling commas are consumed; others are literal.200 if len(l.intPH) == 0 || i < l.intPH[0] || (l.dot >= 0 && i > l.dot) {201 b.WriteByte(',')202 }203 }204}205206// writeDigit writes the digit placeholder at token i: the integer part207// grouped in thousands at the first integer placeholder, the exponent at208// the first exponent placeholder, otherwise the placeholder's own digit.209func (l *numLayout) writeDigit(b *strings.Builder, i int, d numDigits, placed []string) {210 inInt := len(l.intPH) > 0 && i >= l.intPH[0] && i <= l.intPH[len(l.intPH)-1]211 switch {212 case inInt && l.grouped && i == l.intPH[0]:213 var digits strings.Builder214 for _, p := range l.intPH {215 digits.WriteString(placed[p])216 }217 b.WriteString(group(digits.String(), l.loc.Group))218 case inInt:219 if !l.grouped {220 b.WriteString(placed[i])221 }222 case len(l.expPH) > 0 && i == l.expPH[0]:223 b.WriteString(d.exp)224 case len(l.expPH) > 0 && i > l.expPH[0]:225 default:226 b.WriteString(placed[i])227 }228}229230// expSign writes an exponent marker: E- for negative exponents, E+ when231// the pattern asks for the sign.232func expSign(sign string, neg bool) string {233 switch {234 case neg:235 return "E-"236 case sign == "+":237 return "E+"238 }239 return "E"240}241242// group inserts thousands separators sep into a run of digits (and243// leading spaces from ? placeholders).244func group(s, sep string) string {245 lead := len(s) - len(strings.TrimLeft(s, " "))246 digits := s[lead:]247 if len(digits) <= 3 {248 return s249 }250 var b strings.Builder251 b.WriteString(s[:lead])252 first := len(digits) % 3253 if first > 0 {254 b.WriteString(digits[:first])255 }256 for i := first; i < len(digits); i += 3 {257 if i > 0 {258 b.WriteString(sep)259 }260 b.WriteString(digits[i : i+3])261 }262 return b.String()263}