mirror of
https://github.com/miniflux/v2.git
synced 2024-08-31 00:27:24 +02:00
541 lines
14 KiB
Go
541 lines
14 KiB
Go
// Copyright 2017 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package number
|
|
|
|
import (
|
|
"strconv"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
// TODO:
|
|
// - grouping of fractions
|
|
// - allow user-defined superscript notation (such as <sup>4</sup>)
|
|
// - same for non-breaking spaces, like
|
|
|
|
// A VisibleDigits computes digits, comma placement and trailing zeros as they
|
|
// will be shown to the user.
|
|
type VisibleDigits interface {
|
|
Digits(buf []byte, t language.Tag, scale int) Digits
|
|
// TODO: Do we also need to add the verb or pass a format.State?
|
|
}
|
|
|
|
// Formatting proceeds along the following lines:
|
|
// 0) Compose rounding information from format and context.
|
|
// 1) Convert a number into a Decimal.
|
|
// 2) Sanitize Decimal by adding trailing zeros, removing leading digits, and
|
|
// (non-increment) rounding. The Decimal that results from this is suitable
|
|
// for determining the plural form.
|
|
// 3) Render the Decimal in the localized form.
|
|
|
|
// Formatter contains all the information needed to render a number.
|
|
type Formatter struct {
|
|
Pattern
|
|
Info
|
|
}
|
|
|
|
func (f *Formatter) init(t language.Tag, index []uint8) {
|
|
f.Info = InfoFromTag(t)
|
|
for ; ; t = t.Parent() {
|
|
if ci, ok := language.CompactIndex(t); ok {
|
|
f.Pattern = formats[index[ci]]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// InitPattern initializes a Formatter for the given Pattern.
|
|
func (f *Formatter) InitPattern(t language.Tag, pat *Pattern) {
|
|
f.Info = InfoFromTag(t)
|
|
f.Pattern = *pat
|
|
}
|
|
|
|
// InitDecimal initializes a Formatter using the default Pattern for the given
|
|
// language.
|
|
func (f *Formatter) InitDecimal(t language.Tag) {
|
|
f.init(t, tagToDecimal)
|
|
}
|
|
|
|
// InitScientific initializes a Formatter using the default Pattern for the
|
|
// given language.
|
|
func (f *Formatter) InitScientific(t language.Tag) {
|
|
f.init(t, tagToScientific)
|
|
f.Pattern.MinFractionDigits = 0
|
|
f.Pattern.MaxFractionDigits = -1
|
|
}
|
|
|
|
// InitEngineering initializes a Formatter using the default Pattern for the
|
|
// given language.
|
|
func (f *Formatter) InitEngineering(t language.Tag) {
|
|
f.init(t, tagToScientific)
|
|
f.Pattern.MinFractionDigits = 0
|
|
f.Pattern.MaxFractionDigits = -1
|
|
f.Pattern.MaxIntegerDigits = 3
|
|
f.Pattern.MinIntegerDigits = 1
|
|
}
|
|
|
|
// InitPercent initializes a Formatter using the default Pattern for the given
|
|
// language.
|
|
func (f *Formatter) InitPercent(t language.Tag) {
|
|
f.init(t, tagToPercent)
|
|
}
|
|
|
|
// InitPerMille initializes a Formatter using the default Pattern for the given
|
|
// language.
|
|
func (f *Formatter) InitPerMille(t language.Tag) {
|
|
f.init(t, tagToPercent)
|
|
f.Pattern.DigitShift = 3
|
|
}
|
|
|
|
func (f *Formatter) Append(dst []byte, x interface{}) []byte {
|
|
var d Decimal
|
|
r := f.RoundingContext
|
|
d.Convert(r, x)
|
|
return f.Render(dst, FormatDigits(&d, r))
|
|
}
|
|
|
|
func FormatDigits(d *Decimal, r RoundingContext) Digits {
|
|
if r.isScientific() {
|
|
return scientificVisibleDigits(r, d)
|
|
}
|
|
return decimalVisibleDigits(r, d)
|
|
}
|
|
|
|
func (f *Formatter) Format(dst []byte, d *Decimal) []byte {
|
|
return f.Render(dst, FormatDigits(d, f.RoundingContext))
|
|
}
|
|
|
|
func (f *Formatter) Render(dst []byte, d Digits) []byte {
|
|
var result []byte
|
|
var postPrefix, preSuffix int
|
|
if d.IsScientific {
|
|
result, postPrefix, preSuffix = appendScientific(dst, f, &d)
|
|
} else {
|
|
result, postPrefix, preSuffix = appendDecimal(dst, f, &d)
|
|
}
|
|
if f.PadRune == 0 {
|
|
return result
|
|
}
|
|
width := int(f.FormatWidth)
|
|
if count := utf8.RuneCount(result); count < width {
|
|
insertPos := 0
|
|
switch f.Flags & PadMask {
|
|
case PadAfterPrefix:
|
|
insertPos = postPrefix
|
|
case PadBeforeSuffix:
|
|
insertPos = preSuffix
|
|
case PadAfterSuffix:
|
|
insertPos = len(result)
|
|
}
|
|
num := width - count
|
|
pad := [utf8.UTFMax]byte{' '}
|
|
sz := 1
|
|
if r := f.PadRune; r != 0 {
|
|
sz = utf8.EncodeRune(pad[:], r)
|
|
}
|
|
extra := sz * num
|
|
if n := len(result) + extra; n < cap(result) {
|
|
result = result[:n]
|
|
copy(result[insertPos+extra:], result[insertPos:])
|
|
} else {
|
|
buf := make([]byte, n)
|
|
copy(buf, result[:insertPos])
|
|
copy(buf[insertPos+extra:], result[insertPos:])
|
|
result = buf
|
|
}
|
|
for ; num > 0; num-- {
|
|
insertPos += copy(result[insertPos:], pad[:sz])
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// decimalVisibleDigits converts d according to the RoundingContext. Note that
|
|
// the exponent may change as a result of this operation.
|
|
func decimalVisibleDigits(r RoundingContext, d *Decimal) Digits {
|
|
if d.NaN || d.Inf {
|
|
return Digits{digits: digits{Neg: d.Neg, NaN: d.NaN, Inf: d.Inf}}
|
|
}
|
|
n := Digits{digits: d.normalize().digits}
|
|
|
|
exp := n.Exp
|
|
exp += int32(r.DigitShift)
|
|
|
|
// Cap integer digits. Remove *most-significant* digits.
|
|
if r.MaxIntegerDigits > 0 {
|
|
if p := int(exp) - int(r.MaxIntegerDigits); p > 0 {
|
|
if p > len(n.Digits) {
|
|
p = len(n.Digits)
|
|
}
|
|
if n.Digits = n.Digits[p:]; len(n.Digits) == 0 {
|
|
exp = 0
|
|
} else {
|
|
exp -= int32(p)
|
|
}
|
|
// Strip leading zeros.
|
|
for len(n.Digits) > 0 && n.Digits[0] == 0 {
|
|
n.Digits = n.Digits[1:]
|
|
exp--
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rounding if not already done by Convert.
|
|
p := len(n.Digits)
|
|
if maxSig := int(r.MaxSignificantDigits); maxSig > 0 {
|
|
p = maxSig
|
|
}
|
|
if maxFrac := int(r.MaxFractionDigits); maxFrac >= 0 {
|
|
if cap := int(exp) + maxFrac; cap < p {
|
|
p = int(exp) + maxFrac
|
|
}
|
|
if p < 0 {
|
|
p = 0
|
|
}
|
|
}
|
|
n.round(r.Mode, p)
|
|
|
|
// set End (trailing zeros)
|
|
n.End = int32(len(n.Digits))
|
|
if n.End == 0 {
|
|
exp = 0
|
|
if r.MinFractionDigits > 0 {
|
|
n.End = int32(r.MinFractionDigits)
|
|
}
|
|
if p := int32(r.MinSignificantDigits) - 1; p > n.End {
|
|
n.End = p
|
|
}
|
|
} else {
|
|
if end := exp + int32(r.MinFractionDigits); end > n.End {
|
|
n.End = end
|
|
}
|
|
if n.End < int32(r.MinSignificantDigits) {
|
|
n.End = int32(r.MinSignificantDigits)
|
|
}
|
|
}
|
|
n.Exp = exp
|
|
return n
|
|
}
|
|
|
|
// appendDecimal appends a formatted number to dst. It returns two possible
|
|
// insertion points for padding.
|
|
func appendDecimal(dst []byte, f *Formatter, n *Digits) (b []byte, postPre, preSuf int) {
|
|
if dst, ok := f.renderSpecial(dst, n); ok {
|
|
return dst, 0, len(dst)
|
|
}
|
|
digits := n.Digits
|
|
exp := n.Exp
|
|
|
|
// Split in integer and fraction part.
|
|
var intDigits, fracDigits []byte
|
|
numInt := 0
|
|
numFrac := int(n.End - n.Exp)
|
|
if exp > 0 {
|
|
numInt = int(exp)
|
|
if int(exp) >= len(digits) { // ddddd | ddddd00
|
|
intDigits = digits
|
|
} else { // ddd.dd
|
|
intDigits = digits[:exp]
|
|
fracDigits = digits[exp:]
|
|
}
|
|
} else {
|
|
fracDigits = digits
|
|
}
|
|
|
|
neg := n.Neg
|
|
affix, suffix := f.getAffixes(neg)
|
|
dst = appendAffix(dst, f, affix, neg)
|
|
savedLen := len(dst)
|
|
|
|
minInt := int(f.MinIntegerDigits)
|
|
if minInt == 0 && f.MinSignificantDigits > 0 {
|
|
minInt = 1
|
|
}
|
|
// add leading zeros
|
|
for i := minInt; i > numInt; i-- {
|
|
dst = f.AppendDigit(dst, 0)
|
|
if f.needsSep(i) {
|
|
dst = append(dst, f.Symbol(SymGroup)...)
|
|
}
|
|
}
|
|
i := 0
|
|
for ; i < len(intDigits); i++ {
|
|
dst = f.AppendDigit(dst, intDigits[i])
|
|
if f.needsSep(numInt - i) {
|
|
dst = append(dst, f.Symbol(SymGroup)...)
|
|
}
|
|
}
|
|
for ; i < numInt; i++ {
|
|
dst = f.AppendDigit(dst, 0)
|
|
if f.needsSep(numInt - i) {
|
|
dst = append(dst, f.Symbol(SymGroup)...)
|
|
}
|
|
}
|
|
|
|
if numFrac > 0 || f.Flags&AlwaysDecimalSeparator != 0 {
|
|
dst = append(dst, f.Symbol(SymDecimal)...)
|
|
}
|
|
// Add trailing zeros
|
|
i = 0
|
|
for n := -int(n.Exp); i < n; i++ {
|
|
dst = f.AppendDigit(dst, 0)
|
|
}
|
|
for _, d := range fracDigits {
|
|
i++
|
|
dst = f.AppendDigit(dst, d)
|
|
}
|
|
for ; i < numFrac; i++ {
|
|
dst = f.AppendDigit(dst, 0)
|
|
}
|
|
return appendAffix(dst, f, suffix, neg), savedLen, len(dst)
|
|
}
|
|
|
|
func scientificVisibleDigits(r RoundingContext, d *Decimal) Digits {
|
|
if d.NaN || d.Inf {
|
|
return Digits{digits: digits{Neg: d.Neg, NaN: d.NaN, Inf: d.Inf}}
|
|
}
|
|
n := Digits{digits: d.normalize().digits, IsScientific: true}
|
|
|
|
// Normalize to have at least one digit. This simplifies engineering
|
|
// notation.
|
|
if len(n.Digits) == 0 {
|
|
n.Digits = append(n.Digits, 0)
|
|
n.Exp = 1
|
|
}
|
|
|
|
// Significant digits are transformed by the parser for scientific notation
|
|
// and do not need to be handled here.
|
|
maxInt, numInt := int(r.MaxIntegerDigits), int(r.MinIntegerDigits)
|
|
if numInt == 0 {
|
|
numInt = 1
|
|
}
|
|
|
|
// If a maximum number of integers is specified, the minimum must be 1
|
|
// and the exponent is grouped by this number (e.g. for engineering)
|
|
if maxInt > numInt {
|
|
// Correct the exponent to reflect a single integer digit.
|
|
numInt = 1
|
|
// engineering
|
|
// 0.01234 ([12345]e-1) -> 1.2345e-2 12.345e-3
|
|
// 12345 ([12345]e+5) -> 1.2345e4 12.345e3
|
|
d := int(n.Exp-1) % maxInt
|
|
if d < 0 {
|
|
d += maxInt
|
|
}
|
|
numInt += d
|
|
}
|
|
|
|
p := len(n.Digits)
|
|
if maxSig := int(r.MaxSignificantDigits); maxSig > 0 {
|
|
p = maxSig
|
|
}
|
|
if maxFrac := int(r.MaxFractionDigits); maxFrac >= 0 && numInt+maxFrac < p {
|
|
p = numInt + maxFrac
|
|
}
|
|
n.round(r.Mode, p)
|
|
|
|
n.Comma = uint8(numInt)
|
|
n.End = int32(len(n.Digits))
|
|
if minSig := int32(r.MinFractionDigits) + int32(numInt); n.End < minSig {
|
|
n.End = minSig
|
|
}
|
|
return n
|
|
}
|
|
|
|
// appendScientific appends a formatted number to dst. It returns two possible
|
|
// insertion points for padding.
|
|
func appendScientific(dst []byte, f *Formatter, n *Digits) (b []byte, postPre, preSuf int) {
|
|
if dst, ok := f.renderSpecial(dst, n); ok {
|
|
return dst, 0, 0
|
|
}
|
|
digits := n.Digits
|
|
numInt := int(n.Comma)
|
|
numFrac := int(n.End) - int(n.Comma)
|
|
|
|
var intDigits, fracDigits []byte
|
|
if numInt <= len(digits) {
|
|
intDigits = digits[:numInt]
|
|
fracDigits = digits[numInt:]
|
|
} else {
|
|
intDigits = digits
|
|
}
|
|
neg := n.Neg
|
|
affix, suffix := f.getAffixes(neg)
|
|
dst = appendAffix(dst, f, affix, neg)
|
|
savedLen := len(dst)
|
|
|
|
i := 0
|
|
for ; i < len(intDigits); i++ {
|
|
dst = f.AppendDigit(dst, intDigits[i])
|
|
if f.needsSep(numInt - i) {
|
|
dst = append(dst, f.Symbol(SymGroup)...)
|
|
}
|
|
}
|
|
for ; i < numInt; i++ {
|
|
dst = f.AppendDigit(dst, 0)
|
|
if f.needsSep(numInt - i) {
|
|
dst = append(dst, f.Symbol(SymGroup)...)
|
|
}
|
|
}
|
|
|
|
if numFrac > 0 || f.Flags&AlwaysDecimalSeparator != 0 {
|
|
dst = append(dst, f.Symbol(SymDecimal)...)
|
|
}
|
|
i = 0
|
|
for ; i < len(fracDigits); i++ {
|
|
dst = f.AppendDigit(dst, fracDigits[i])
|
|
}
|
|
for ; i < numFrac; i++ {
|
|
dst = f.AppendDigit(dst, 0)
|
|
}
|
|
|
|
// exp
|
|
buf := [12]byte{}
|
|
// TODO: use exponential if superscripting is not available (no Latin
|
|
// numbers or no tags) and use exponential in all other cases.
|
|
exp := n.Exp - int32(n.Comma)
|
|
exponential := f.Symbol(SymExponential)
|
|
if exponential == "E" {
|
|
dst = append(dst, "\u202f"...) // NARROW NO-BREAK SPACE
|
|
dst = append(dst, f.Symbol(SymSuperscriptingExponent)...)
|
|
dst = append(dst, "\u202f"...) // NARROW NO-BREAK SPACE
|
|
dst = f.AppendDigit(dst, 1)
|
|
dst = f.AppendDigit(dst, 0)
|
|
switch {
|
|
case exp < 0:
|
|
dst = append(dst, superMinus...)
|
|
exp = -exp
|
|
case f.Flags&AlwaysExpSign != 0:
|
|
dst = append(dst, superPlus...)
|
|
}
|
|
b = strconv.AppendUint(buf[:0], uint64(exp), 10)
|
|
for i := len(b); i < int(f.MinExponentDigits); i++ {
|
|
dst = append(dst, superDigits[0]...)
|
|
}
|
|
for _, c := range b {
|
|
dst = append(dst, superDigits[c-'0']...)
|
|
}
|
|
} else {
|
|
dst = append(dst, exponential...)
|
|
switch {
|
|
case exp < 0:
|
|
dst = append(dst, f.Symbol(SymMinusSign)...)
|
|
exp = -exp
|
|
case f.Flags&AlwaysExpSign != 0:
|
|
dst = append(dst, f.Symbol(SymPlusSign)...)
|
|
}
|
|
b = strconv.AppendUint(buf[:0], uint64(exp), 10)
|
|
for i := len(b); i < int(f.MinExponentDigits); i++ {
|
|
dst = f.AppendDigit(dst, 0)
|
|
}
|
|
for _, c := range b {
|
|
dst = f.AppendDigit(dst, c-'0')
|
|
}
|
|
}
|
|
return appendAffix(dst, f, suffix, neg), savedLen, len(dst)
|
|
}
|
|
|
|
const (
|
|
superMinus = "\u207B" // SUPERSCRIPT HYPHEN-MINUS
|
|
superPlus = "\u207A" // SUPERSCRIPT PLUS SIGN
|
|
)
|
|
|
|
var (
|
|
// Note: the digits are not sequential!!!
|
|
superDigits = []string{
|
|
"\u2070", // SUPERSCRIPT DIGIT ZERO
|
|
"\u00B9", // SUPERSCRIPT DIGIT ONE
|
|
"\u00B2", // SUPERSCRIPT DIGIT TWO
|
|
"\u00B3", // SUPERSCRIPT DIGIT THREE
|
|
"\u2074", // SUPERSCRIPT DIGIT FOUR
|
|
"\u2075", // SUPERSCRIPT DIGIT FIVE
|
|
"\u2076", // SUPERSCRIPT DIGIT SIX
|
|
"\u2077", // SUPERSCRIPT DIGIT SEVEN
|
|
"\u2078", // SUPERSCRIPT DIGIT EIGHT
|
|
"\u2079", // SUPERSCRIPT DIGIT NINE
|
|
}
|
|
)
|
|
|
|
func (f *Formatter) getAffixes(neg bool) (affix, suffix string) {
|
|
str := f.Affix
|
|
if str != "" {
|
|
if f.NegOffset > 0 {
|
|
if neg {
|
|
str = str[f.NegOffset:]
|
|
} else {
|
|
str = str[:f.NegOffset]
|
|
}
|
|
}
|
|
sufStart := 1 + str[0]
|
|
affix = str[1:sufStart]
|
|
suffix = str[sufStart+1:]
|
|
}
|
|
// TODO: introduce a NeedNeg sign to indicate if the left pattern already
|
|
// has a sign marked?
|
|
if f.NegOffset == 0 && (neg || f.Flags&AlwaysSign != 0) {
|
|
affix = "-" + affix
|
|
}
|
|
return affix, suffix
|
|
}
|
|
|
|
func (f *Formatter) renderSpecial(dst []byte, d *Digits) (b []byte, ok bool) {
|
|
if d.NaN {
|
|
return fmtNaN(dst, f), true
|
|
}
|
|
if d.Inf {
|
|
return fmtInfinite(dst, f, d), true
|
|
}
|
|
return dst, false
|
|
}
|
|
|
|
func fmtNaN(dst []byte, f *Formatter) []byte {
|
|
return append(dst, f.Symbol(SymNan)...)
|
|
}
|
|
|
|
func fmtInfinite(dst []byte, f *Formatter, d *Digits) []byte {
|
|
affix, suffix := f.getAffixes(d.Neg)
|
|
dst = appendAffix(dst, f, affix, d.Neg)
|
|
dst = append(dst, f.Symbol(SymInfinity)...)
|
|
dst = appendAffix(dst, f, suffix, d.Neg)
|
|
return dst
|
|
}
|
|
|
|
func appendAffix(dst []byte, f *Formatter, affix string, neg bool) []byte {
|
|
quoting := false
|
|
escaping := false
|
|
for _, r := range affix {
|
|
switch {
|
|
case escaping:
|
|
// escaping occurs both inside and outside of quotes
|
|
dst = append(dst, string(r)...)
|
|
escaping = false
|
|
case r == '\\':
|
|
escaping = true
|
|
case r == '\'':
|
|
quoting = !quoting
|
|
case quoting:
|
|
dst = append(dst, string(r)...)
|
|
case r == '%':
|
|
if f.DigitShift == 3 {
|
|
dst = append(dst, f.Symbol(SymPerMille)...)
|
|
} else {
|
|
dst = append(dst, f.Symbol(SymPercentSign)...)
|
|
}
|
|
case r == '-' || r == '+':
|
|
if neg {
|
|
dst = append(dst, f.Symbol(SymMinusSign)...)
|
|
} else if f.Flags&ElideSign == 0 {
|
|
dst = append(dst, f.Symbol(SymPlusSign)...)
|
|
} else {
|
|
dst = append(dst, ' ')
|
|
}
|
|
default:
|
|
dst = append(dst, string(r)...)
|
|
}
|
|
}
|
|
return dst
|
|
}
|