Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ linters:
- third_party$
- builtin$
- examples$
rules:
- path: mangling/ucd
linters:
- mnd
formatters:
enable:
- gofmt
Expand Down
24 changes: 24 additions & 0 deletions mangling/go_ident_fallback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
package mangling

import (
"strings"
"testing"
"unicode"
"unicode/utf8"

"github.com/go-openapi/testify/v2/assert"
)
Expand Down Expand Up @@ -80,3 +83,24 @@ func TestGoIdentFallback(t *testing.T) {
assert.EqualT(t, "", m.Pascalize("日本")) // elided CJK runes
})
}

// TestGoIdentExtremeNumbers covers the sibling contract: whatever the magnitude of a number in the input, a Go
// identifier producer verbalizes it and never leaves a leading digit behind.
//
// The numbers engine spells an integer too large for int64 digit by digit, and -2^63 the same way.
func TestGoIdentExtremeNumbers(t *testing.T) {
t.Parallel()

g := MakeGoMangler()
for _, in := range []string{
"99999999999999999999.5", // integer part beyond int64
"-9223372036854775808 items", // -2^63, no positive int64 counterpart
"1" + strings.Repeat("0", 400), // beyond float64 range: ParseFloat overflows to +Inf
"1" + strings.Repeat("0", 400) + ".5", // same, with a fractional part
} {
got := g.IdentExported(in)
assert.NotEmptyf(t, got, "IdentExported(%q)", in)
first, _ := utf8.DecodeRuneInString(got)
assert.Truef(t, unicode.IsLetter(first), "IdentExported(%q) starts with %q: %.40q", in, first, got)
}
}
14 changes: 13 additions & 1 deletion mangling/numbers/cardinal.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

package numbers

import "strconv"
import (
"math"
"strconv"
)

// maxFullCardinal is the default magnitude up to which a number is spelled out in full.
//
Expand Down Expand Up @@ -78,6 +81,15 @@ func writeCardinal(b *buf, n int64, o numberOptions) {

if n < 0 {
_, _ = b.WriteString("minus ")

if n == math.MinInt64 {
// -2^63 has no positive int64 counterpart, so negating it would leave n negative and spell nothing at all.
// Spell its digits instead, as [writeSpellDecimal] does for an integer too large for int64.
writeDigitWords(b, "9223372036854775808")

return
}

n = -n
}

Expand Down
52 changes: 52 additions & 0 deletions mangling/numbers/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package numbers

import (
"math"
"strings"
"testing"

Expand All @@ -25,6 +26,57 @@ func TestNumberWordsOverflow(t *testing.T) {
assert.EqualT(t, "minus one"+strings.Repeat(" zero", 19), neg)
}

// TestNonFiniteValues covers the wording of NaN and the two infinities, which have no cardinal form.
//
// The generated numeral table holds finite values only (see the gen_numerals command), so this guards every other way
// a value reaches the verbalizer.
func TestNonFiniteValues(t *testing.T) {
t.Parallel()

var o numberOptions

assert.EqualT(t, "not a number", numberWords(math.NaN(), o))
assert.EqualT(t, "infinity", numberWords(math.Inf(1), o))
assert.EqualT(t, "minus infinity", numberWords(math.Inf(-1), o))

// The streaming form agrees with the string form.
for _, x := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
var b buf
writeNumberValue(&b, x, o)
assert.EqualTf(t, numberWords(x, o), string(b.b), "writeNumberValue(%v)", x)
}
}

// TestNumberWordsDecimalOverflow covers a decimal whose integer part is too large for int64.
//
// strconv.ParseInt clamps such an integer to int64 max, so the digits are spelled one by one instead — as
// [TestNumberWordsOverflow] does for a plain integer.
func TestNumberWordsDecimalOverflow(t *testing.T) {
t.Parallel()

m := MakeNumberMangler()

got := m.NumberWords("99999999999999999999.5") // 20 nines, > int64 max
assert.EqualT(t, strings.Repeat("nine ", 20)+"dot five", got)

// Beyond float64 range: strconv.ParseFloat returns +Inf with strconv.ErrRange.
huge := m.NumberWords("1" + strings.Repeat("0", 400) + ".5")
assert.Falsef(t, strings.ContainsAny(huge, "0123456789"), "overflowing decimal left raw digits: %q", huge)
assert.Truef(t, strings.HasSuffix(huge, " dot five"), "overflowing decimal lost its fractional part: %q", huge)
}

// TestCardinalMinInt64 covers -2^63, the one negative int64 with no positive counterpart: negating it leaves it
// negative, so writeCardinal spells its digits instead of emitting "minus " and nothing else.
func TestCardinalMinInt64(t *testing.T) {
t.Parallel()

//nolint:dupword // the repeated words are the digit-by-digit spelling of 9223372036854775808
const want = "minus nine two two three three seven two zero three six eight five four seven seven five eight zero eight"

assert.EqualT(t, want, cardinal(math.MinInt64, numberOptions{}))
assert.EqualT(t, want, MakeNumberMangler().NumberWords("-9223372036854775808"))
}

// TestWithNumberDetectPrecision covers the tolerance knob: a tighter precision stops a loose decimal from matching a
// simple fraction.
func TestWithNumberDetectPrecision(t *testing.T) {
Expand Down
54 changes: 48 additions & 6 deletions mangling/numbers/fraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package numbers

import (
"errors"
"math"
"strconv"
"strings"
Expand Down Expand Up @@ -132,12 +133,12 @@ func writeSpellDecimal(b *buf, s string, o numberOptions) {
}

x, err := strconv.ParseFloat(s, 64)
if err != nil {
_, _ = b.WriteString(s)
if errors.Is(err, strconv.ErrSyntax) {
_, _ = b.WriteString(s) // not a number at all: copy it through untouched

return
}
if x > -1 && x < 1 && x != 0 {
if err == nil && x > -1 && x < 1 && x != 0 {
if writeFraction(b, x, o) {
return
}
Expand All @@ -154,16 +155,51 @@ func writeSpellDecimal(b *buf, s string, o numberOptions) {
if intPart == "" {
intPart = "0"
}
intVal, _ := strconv.ParseInt(intPart, 10, 64)

writeCardinal(b, intVal, o)
if intVal, err := strconv.ParseInt(intPart, 10, 64); err == nil {
writeCardinal(b, intVal, o)
} else {
// The integer part overflows int64, so ParseFloat has returned +Inf or -Inf with strconv.ErrRange. Spell the
// digits one by one, as the integer branch above does, so the result never starts with a raw digit.
writeDigitWords(b, intPart)
}
_, _ = b.WriteString(" dot ")
writeDigitWords(b, fracPart)
}

// nonFiniteWords are the words for the three float64 values no cardinal covers.
const (
nanWords = "not a number"
infWords = "infinity"
negativeInfWords = "minus infinity"
)

// nonFinite returns the wording of NaN and the two infinities, and whether x is one of them.
//
// The generated numeral table holds finite values only, so no numeral rune reaches this. It guards the conversion in
// [numberWords] and [writeNumberValue]: int64(+Inf) is platform-defined (-2^63 on amd64), which spells out as "minus"
// and nothing else.
func nonFinite(x float64) (string, bool) {
switch {
case math.IsNaN(x):
return nanWords, true
case math.IsInf(x, 1):
return infWords, true
case math.IsInf(x, -1):
return negativeInfWords, true
default:
return "", false
}
}

// numberWords renders a numeric value as words: cardinal for integral values, fraction/decimal otherwise (the value is
// formatted to its shortest exact decimal string first).
//
// NaN and the infinities have no cardinal form and render as [nanWords], [infWords] and [negativeInfWords].
func numberWords(x float64, o numberOptions) string {
if w, ok := nonFinite(x); ok {
return w
}

if x == math.Trunc(x) {
return cardinal(int64(x), o)
}
Expand All @@ -174,6 +210,12 @@ func numberWords(x float64, o numberOptions) string {
// writeNumberValue streams the english wording of a numeric value into b — the streaming form of [numberWords], used
// to verbalize a Unicode numeral rune ('½' → "one half", 'Ⅶ' → "seven").
func writeNumberValue(b *buf, x float64, o numberOptions) {
if w, ok := nonFinite(x); ok {
_, _ = b.WriteString(w)

return
}

if x == math.Trunc(x) {
writeCardinal(b, int64(x), o)

Expand Down
3 changes: 3 additions & 0 deletions mangling/ucd/cmd/gen_asciifold/gen_asciifold.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ func run(pkg, outFile, ucdDir, buildTag string) error {
if err != nil {
return err
}
// The generated file records this path, so keep it slash-separated: regenerating on Windows must not rewrite
// "v15/DerivedName.txt" as "v15\DerivedName.txt" and churn every table.
inFile = filepath.ToSlash(inFile)

if err := emit(inFile, pkg, outFile, buildTag, out); err != nil {
return err
Expand Down
Loading
Loading