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 / fraction.go
fraction.go 4.4K
1package numfmt23import (4 "math"5 "strconv"6 "strings"78 "src.rest/cat/012/internal/locale"9)1011// Fraction patterns: "# ?/?" shows 1.75 as 1 3/4, "# ??/??" allows12// denominators up to 99, "# ?/8" shows eighths, and "?/?" with no whole13// part shows 7/4. Placeholders pad as in numbers: ? with a space, 0 with14// a zero, # with nothing.1516// fracLayout is where a fraction section puts each part: the tokens of17// the whole number (with anything before it), those between it and the18// numerator, the numerator's and denominator's placeholders, and the19// rest.20type fracLayout struct {21 whole, gap, num, den, rest []ptok22 fixed int // the denominator written in the pattern, or 023}2425// layoutFraction finds a fraction in a number section: digit26// placeholders, a /, and placeholders or digits for the denominator.27func layoutFraction(toks []ptok) (fracLayout, bool) {28 slash := -129 for i := 1; i+1 < len(toks); i++ {30 if toks[i].kind == ptLit && toks[i].s == "/" && toks[i-1].kind == ptDigit && denToken(toks[i+1]) {31 slash = i32 break33 }34 }35 if slash < 0 {36 return fracLayout{}, false37 }38 start := slash39 for start > 0 && toks[start-1].kind == ptDigit {40 start--41 }42 end := slash + 143 for end < len(toks) && denToken(toks[end]) {44 end++45 }46 l := fracLayout{num: toks[start:slash], den: toks[slash+1 : end], rest: toks[end:]}47 last := start - 148 for last >= 0 && toks[last].kind != ptDigit {49 last--50 }51 l.whole, l.gap = toks[:last+1], toks[last+1:start]52 var d strings.Builder53 for _, t := range l.den {54 d.WriteString(t.s)55 }56 if n, err := strconv.Atoi(d.String()); err == nil && n > 0 {57 l.fixed = n58 }59 return l, true60}6162// denToken reports whether t can be part of a denominator: a digit63// placeholder or a digit written out.64func denToken(t ptok) bool {65 return t.kind == ptDigit || t.kind == ptLit && len(t.s) == 1 && t.s[0] >= '0' && t.s[0] <= '9'66}6768// hasWhole reports whether the section shows a whole number before the69// fraction.70func (l fracLayout) hasWhole() bool { return hasKind(l.whole, ptDigit) }7172// formatFraction renders |v| with a fraction section as shown in loc;73// neg adds a minus sign.74func formatFraction(v float64, neg bool, l fracLayout, loc *locale.Locale) string {75 whole := math.Floor(v)76 n, d := l.approximate(v - whole)77 if n == d {78 whole, n = whole+1, 079 }80 if !l.hasWhole() {81 n += int(whole) * d82 whole = 083 }84 var b strings.Builder85 if neg && (whole != 0 || n != 0) {86 b.WriteByte('-')87 }88 w := ""89 if l.hasWhole() {90 w = formatNumber(whole, false, l.whole, loc)91 } else {92 w = literals(l.whole)93 }94 switch {95 case n == 0 && l.hasWhole():96 if strings.TrimSpace(w) == "" {97 w = "0"98 }99 b.WriteString(w)100 case n == 0 && !l.hasWhole() && l.fixed == 0:101 b.WriteString(w + "0/1")102 default:103 b.WriteString(w)104 if strings.TrimSpace(w) != "" {105 b.WriteString(literals(l.gap))106 }107 b.WriteString(padDigits(strconv.Itoa(n), l.num, true))108 b.WriteByte('/')109 if l.fixed > 0 {110 b.WriteString(strconv.Itoa(l.fixed))111 } else {112 b.WriteString(padDigits(strconv.Itoa(d), l.den, false))113 }114 }115 b.WriteString(literals(l.rest))116 return b.String()117}118119// approximate is the fraction n/d nearest f (0 <= f < 1): with the120// pattern's denominator, or the nearest with a denominator of as many121// digits as its placeholders, the smallest such denominator on a tie.122func (l fracLayout) approximate(f float64) (n, d int) {123 if l.fixed > 0 {124 return int(math.Round(f * float64(l.fixed))), l.fixed125 }126 maxD := int(math.Pow10(min(len(l.den), 5))) - 1127 n, d = int(math.Round(f)), 1128 best := math.Abs(f - float64(n))129 for q := 2; q <= maxD && best > 0; q++ {130 p := math.Round(f * float64(q))131 if e := math.Abs(f - p/float64(q)); e < best {132 n, d, best = int(p), q, e133 }134 }135 return n, d136}137138// padDigits fills the placeholders phs with digits: those it has room139// for are padded, a ? with a space and a 0 with a zero, on the left of a140// numerator (right) or the right of a denominator (left).141func padDigits(digits string, phs []ptok, right bool) string {142 var pad strings.Builder143 for i := len(digits); i < len(phs); i++ {144 switch phs[i].s {145 case "?":146 pad.WriteByte(' ')147 case "0":148 pad.WriteByte('0')149 }150 }151 if right {152 return pad.String() + digits153 }154 return digits + pad.String()155}156157// literals is the text of the literal tokens among toks.158func literals(toks []ptok) string {159 var b strings.Builder160 for _, t := range toks {161 if t.kind == ptLit {162 b.WriteString(t.s)163 }164 }165 return b.String()166}