diff --git a/.golangci.yml b/.golangci.yml index 4f24327..cd9a776 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -57,9 +57,8 @@ linters: disable-default-exclusions: true exclude-functions: - '(*bytes.Buffer).Write' # always returns nil error - - '(*github.com/valyala/bytebufferpool.ByteBuffer).Write' # always returns nil error - - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte' # always returns nil error - - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteString' # always returns nil error + - '(*strings.Builder).WriteByte' # always returns nil error + - '(*strings.Builder).WriteString' # always returns nil error errchkjson: report-no-exported: true @@ -200,7 +199,7 @@ linters: - name: unchecked-type-assertion disabled: true # TODO: Do not disable - name: unhandled-error - arguments: ['bytes\\.Buffer\\.Write'] + disabled: true - name: enforce-switch-style disabled: true - name: var-naming diff --git a/common.go b/common.go index 8a256c6..4acc51e 100644 --- a/common.go +++ b/common.go @@ -14,7 +14,6 @@ import ( "reflect" "runtime" "slices" - "strconv" "strings" "sync" "sync/atomic" @@ -137,29 +136,38 @@ func ConvertToBytes(humanReadableString string) int { return 0 } - var unitPrefixPos, lastNumberPos int - // loop backwards to find the last numeric character and the unit prefix + // Find the last digit position by scanning backwards + // Also identify the unit prefix position in the same pass + lastNumberPos := -1 + unitPrefixPos := 0 for i := strLen - 1; i >= 0; i-- { c := humanReadableString[i] if c >= '0' && c <= '9' { lastNumberPos = i break } - if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { + // Track the first letter position (unit prefix) from the end + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { unitPrefixPos = i } } + // No digits found + if lastNumberPos < 0 { + return 0 + } + numPart := humanReadableString[:lastNumberPos+1] var size float64 + if strings.IndexByte(numPart, '.') >= 0 { var err error - size, err = strconv.ParseFloat(numPart, 64) + size, err = ParseFloat64(numPart) if err != nil { return 0 } } else { - i64, err := strconv.ParseUint(numPart, 10, 64) + i64, err := ParseUint(numPart) if err != nil { return 0 } diff --git a/convert.go b/convert.go index c5f9e0b..bd4668f 100644 --- a/convert.go +++ b/convert.go @@ -85,8 +85,9 @@ func ByteSize(bytes uint64) string { } buf := make([]byte, 0, 16) + if div == 1 { - buf = strconv.AppendUint(buf, bytes, 10) + buf = AppendUint(buf, bytes) buf = append(buf, unit...) return UnsafeString(buf) } @@ -100,10 +101,10 @@ func ByteSize(bytes uint64) string { integer := scaled / 10 fractional := scaled % 10 - buf = strconv.AppendUint(buf, integer, 10) + buf = AppendUint(buf, integer) if fractional > 0 { buf = append(buf, '.') - buf = strconv.AppendUint(buf, fractional, 10) + buf = AppendUint(buf, fractional) } buf = append(buf, unit...) return UnsafeString(buf) @@ -113,25 +114,25 @@ func ByteSize(bytes uint64) string { func ToString(arg any, timeFormat ...string) string { switch v := arg.(type) { case int: - return strconv.Itoa(v) + return FormatInt(int64(v)) case int8: - return strconv.FormatInt(int64(v), 10) + return FormatInt8(v) case int16: - return strconv.FormatInt(int64(v), 10) + return FormatInt16(v) case int32: - return strconv.FormatInt(int64(v), 10) + return FormatInt32(v) case int64: - return strconv.FormatInt(v, 10) + return FormatInt(v) case uint: - return strconv.Itoa(int(v)) + return FormatUint(uint64(v)) case uint8: - return strconv.FormatInt(int64(v), 10) + return FormatUint8(v) case uint16: - return strconv.FormatInt(int64(v), 10) + return FormatUint16(v) case uint32: - return strconv.FormatInt(int64(v), 10) + return FormatUint32(v) case uint64: - return strconv.FormatInt(int64(v), 10) + return FormatUint(v) case string: return v case []byte: @@ -151,23 +152,91 @@ func ToString(arg any, timeFormat ...string) string { return ToString(v.Interface(), timeFormat...) case fmt.Stringer: return v.String() + // Handle common pointer types directly to avoid reflection + case *string: + if v != nil { + return *v + } + return "" + case *int: + if v != nil { + return FormatInt(int64(*v)) + } + return "0" + case *int64: + if v != nil { + return FormatInt(*v) + } + return "0" + case *uint64: + if v != nil { + return FormatUint(*v) + } + return "0" + case *float64: + if v != nil { + return strconv.FormatFloat(*v, 'f', -1, 64) + } + return "0" + case *bool: + if v != nil { + return strconv.FormatBool(*v) + } + return "false" + // Handle common slice types directly to avoid reflection + case []string: + if len(v) == 0 { + return "[]" + } + var buf strings.Builder + buf.Grow(len(v) * 8) // Pre-allocate approximate size + buf.WriteByte('[') + for i, s := range v { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteString(s) + } + buf.WriteByte(']') + return buf.String() + case []int: + if len(v) == 0 { + return "[]" + } + var buf strings.Builder + buf.Grow(len(v) * 4) // Pre-allocate approximate size + buf.WriteByte('[') + for i, n := range v { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteString(FormatInt(int64(n))) + } + buf.WriteByte(']') + return buf.String() default: // Check if the type is a pointer by using reflection rv := reflect.ValueOf(arg) - if rv.Kind() == reflect.Ptr && !rv.IsNil() { + kind := rv.Kind() + if kind == reflect.Ptr && !rv.IsNil() { // Dereference the pointer and recursively call ToString return ToString(rv.Elem().Interface(), timeFormat...) - } else if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { + } else if kind == reflect.Slice || kind == reflect.Array { // handle slices + n := rv.Len() + if n == 0 { + return "[]" + } var buf strings.Builder - buf.WriteString("[") //nolint:errcheck // no need to check error - for i := 0; i < rv.Len(); i++ { + buf.Grow(n * 8) // Pre-allocate approximate size + buf.WriteByte('[') + for i := range n { if i > 0 { - buf.WriteString(" ") //nolint:errcheck // no need to check error + buf.WriteByte(' ') } - buf.WriteString(ToString(rv.Index(i).Interface())) //nolint:errcheck // no need to check error + buf.WriteString(ToString(rv.Index(i).Interface())) } - buf.WriteString("]") //nolint:errcheck // no need to check error + buf.WriteByte(']') return buf.String() } diff --git a/convert_test.go b/convert_test.go index 122313b..df7e2f9 100644 --- a/convert_test.go +++ b/convert_test.go @@ -164,6 +164,32 @@ func Test_ToString(t *testing.T) { t.Parallel() require.Equal(t, "", ToString(nil)) }) + + // Test nil pointer handling - nil pointers return type-specific defaults + t.Run("nil pointer to string", func(t *testing.T) { + t.Parallel() + require.Empty(t, ToString((*string)(nil))) + }) + t.Run("nil pointer to int", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*int)(nil))) + }) + t.Run("nil pointer to int64", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*int64)(nil))) + }) + t.Run("nil pointer to uint64", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*uint64)(nil))) + }) + t.Run("nil pointer to float64", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*float64)(nil))) + }) + t.Run("nil pointer to bool", func(t *testing.T) { + t.Parallel() + require.Equal(t, "false", ToString((*bool)(nil))) + }) } func TestCopyBytes(t *testing.T) { diff --git a/format.go b/format.go new file mode 100644 index 0000000..04d7aaa --- /dev/null +++ b/format.go @@ -0,0 +1,204 @@ +package utils + +// smallInts contains precomputed string representations for small integers 0-99 +var smallInts [100]string + +// smallNegInts contains precomputed string representations for small negative integers -1 to -99 +var smallNegInts [100]string + +func init() { + for i := range 100 { + smallInts[i] = formatUintSmall(uint64(i)) + if i > 0 { + smallNegInts[i] = "-" + smallInts[i] + } + } +} + +func formatUintSmall(n uint64) string { + if n < 10 { + return string(byte(n) + '0') + } + return string([]byte{byte(n/10) + '0', byte(n%10) + '0'}) +} + +// formatUintBuf writes the digits of n into buf from the end and returns the start index. +// buf must be at least 20 bytes. +func formatUintBuf(buf *[20]byte, n uint64) int { + i := 20 + for n >= 10 { + i-- + buf[i] = byte(n%10) + '0' + n /= 10 + } + i-- + buf[i] = byte(n) + '0' + return i +} + +// FormatUint formats a uint64 as a decimal string. +// It is faster than strconv.FormatUint for most inputs. +func FormatUint(n uint64) string { + if n < 100 { + return smallInts[n] + } + var buf [20]byte + i := formatUintBuf(&buf, n) + return string(buf[i:]) +} + +// FormatInt formats an int64 as a decimal string. +// It is faster than strconv.FormatInt for most inputs. +func FormatInt(n int64) string { + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + if n >= 0 { + var buf [20]byte + i := formatUintBuf(&buf, uint64(n)) + return string(buf[i:]) + } + var buf [20]byte + i := formatUintBuf(&buf, uint64(-n)) + i-- + buf[i] = '-' + return string(buf[i:]) +} + +// FormatUint32 formats a uint32 as a decimal string. +func FormatUint32(n uint32) string { + if n < 100 { + return smallInts[n] + } + var buf [10]byte // max 4294967295 + i := 10 + for n >= 10 { + i-- + buf[i] = byte(n%10) + '0' //nolint:gosec // i is always in bounds: starts at 10, decrements max 10 times for uint32 + n /= 10 + } + i-- + buf[i] = byte(n) + '0' //nolint:gosec // i is always >= 0 after loop + return string(buf[i:]) +} + +// FormatInt32 formats an int32 as a decimal string. +func FormatInt32(n int32) string { + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + if n >= 0 { + return FormatUint32(uint32(n)) + } + var buf [11]byte // max -2147483648 + un := uint32(-n) + i := 11 + for un >= 10 { + i-- + buf[i] = byte(un%10) + '0' + un /= 10 + } + i-- + buf[i] = byte(un) + '0' + i-- + buf[i] = '-' + return string(buf[i:]) +} + +// FormatUint16 formats a uint16 as a decimal string. +func FormatUint16(n uint16) string { + if n < 100 { + return smallInts[n] + } + var buf [5]byte // max 65535 + i := 5 + for n >= 10 { + i-- + buf[i] = byte(n%10) + '0' //nolint:gosec // i is always in bounds: starts at 5, decrements max 5 times for uint16 + n /= 10 + } + i-- + buf[i] = byte(n) + '0' //nolint:gosec // i is always >= 0 after loop + return string(buf[i:]) +} + +// FormatInt16 formats an int16 as a decimal string. +func FormatInt16(n int16) string { + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + if n >= 0 { + return FormatUint16(uint16(n)) + } + var buf [6]byte // max -32768 + un := uint16(-n) + i := 6 + for un >= 10 { + i-- + buf[i] = byte(un%10) + '0' //nolint:gosec // i is always in bounds + un /= 10 + } + i-- + buf[i] = byte(un) + '0' //nolint:gosec // i is always >= 1 after loop + i-- + buf[i] = '-' //nolint:gosec // i is always >= 0 after decrement + return string(buf[i:]) +} + +// FormatUint8 formats a uint8 as a decimal string. +func FormatUint8(n uint8) string { + if n < 100 { + return smallInts[n] + } + // uint8 max is 255, so max 3 digits + return string([]byte{n/100 + '0', (n/10)%10 + '0', n%10 + '0'}) +} + +// FormatInt8 formats an int8 as a decimal string. +func FormatInt8(n int8) string { + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + // Only -128 to -100 and 100 to 127 reach here + if n >= 0 { + un := uint8(n) + return string([]byte{un/100 + '0', (un/10)%10 + '0', un%10 + '0'}) + } + // n is -128 to -100 + un := uint8(-n) + return string([]byte{'-', un/100 + '0', (un/10)%10 + '0', un%10 + '0'}) +} + +// AppendUint appends the decimal string representation of n to dst. +func AppendUint(dst []byte, n uint64) []byte { + if n < 100 { + return append(dst, smallInts[n]...) + } + var buf [20]byte + i := formatUintBuf(&buf, n) + return append(dst, buf[i:]...) +} + +// AppendInt appends the decimal string representation of n to dst. +func AppendInt(dst []byte, n int64) []byte { + if n >= 0 { + return AppendUint(dst, uint64(n)) + } + var buf [20]byte + i := formatUintBuf(&buf, uint64(-n)) + i-- + buf[i] = '-' + return append(dst, buf[i:]...) +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..13b51d2 --- /dev/null +++ b/format_test.go @@ -0,0 +1,313 @@ +package utils + +import ( + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_FormatUint(t *testing.T) { + t.Parallel() + tests := []uint64{ + 0, 1, 9, 10, 11, 99, 100, 101, 999, 1000, + 12345, 123456789, 9999999999, + math.MaxUint32, math.MaxUint64, + } + for _, tt := range tests { + expected := strconv.FormatUint(tt, 10) + result := FormatUint(tt) + require.Equal(t, expected, result, "FormatUint(%d)", tt) + } +} + +func Test_FormatInt(t *testing.T) { + t.Parallel() + tests := []int64{ + 0, 1, -1, 9, -9, 10, -10, 99, -99, 100, -100, + 12345, -12345, 123456789, -123456789, + math.MaxInt32, math.MinInt32, + math.MaxInt64, math.MinInt64, + } + for _, tt := range tests { + expected := strconv.FormatInt(tt, 10) + result := FormatInt(tt) + require.Equal(t, expected, result, "FormatInt(%d)", tt) + } +} + +func Test_FormatUint32(t *testing.T) { + t.Parallel() + tests := []uint32{0, 1, 99, 100, 12345, math.MaxUint32} + for _, tt := range tests { + expected := strconv.FormatUint(uint64(tt), 10) + result := FormatUint32(tt) + require.Equal(t, expected, result, "FormatUint32(%d)", tt) + } +} + +func Test_FormatInt32(t *testing.T) { + t.Parallel() + tests := []int32{0, 1, -1, 99, -99, math.MaxInt32, math.MinInt32} + for _, tt := range tests { + expected := strconv.FormatInt(int64(tt), 10) + result := FormatInt32(tt) + require.Equal(t, expected, result, "FormatInt32(%d)", tt) + } +} + +func Test_FormatUint16(t *testing.T) { + t.Parallel() + tests := []uint16{0, 1, 99, 100, 12345, math.MaxUint16} + for _, tt := range tests { + expected := strconv.FormatUint(uint64(tt), 10) + result := FormatUint16(tt) + require.Equal(t, expected, result, "FormatUint16(%d)", tt) + } +} + +func Test_FormatInt16(t *testing.T) { + t.Parallel() + tests := []int16{0, 1, -1, 99, -99, math.MaxInt16, math.MinInt16} + for _, tt := range tests { + expected := strconv.FormatInt(int64(tt), 10) + result := FormatInt16(tt) + require.Equal(t, expected, result, "FormatInt16(%d)", tt) + } +} + +func Test_FormatUint8(t *testing.T) { + t.Parallel() + tests := []uint8{0, 1, 99, 100, math.MaxUint8} + for _, tt := range tests { + expected := strconv.FormatUint(uint64(tt), 10) + result := FormatUint8(tt) + require.Equal(t, expected, result, "FormatUint8(%d)", tt) + } +} + +func Test_FormatInt8(t *testing.T) { + t.Parallel() + tests := []int8{0, 1, -1, 99, -99, math.MaxInt8, math.MinInt8} + for _, tt := range tests { + expected := strconv.FormatInt(int64(tt), 10) + result := FormatInt8(tt) + require.Equal(t, expected, result, "FormatInt8(%d)", tt) + } +} + +func Test_AppendUint(t *testing.T) { + t.Parallel() + tests := []uint64{0, 1, 99, 100, 12345, 123456789, math.MaxUint64} + for _, tt := range tests { + expected := strconv.AppendUint([]byte("prefix"), tt, 10) + result := AppendUint([]byte("prefix"), tt) + require.Equal(t, expected, result, "AppendUint(%d)", tt) + } +} + +func Test_AppendInt(t *testing.T) { + t.Parallel() + tests := []int64{0, 1, -1, 99, -99, 12345, -12345, math.MaxInt64, math.MinInt64} + for _, tt := range tests { + expected := strconv.AppendInt([]byte("prefix"), tt, 10) + result := AppendInt([]byte("prefix"), tt) + require.Equal(t, expected, result, "AppendInt(%d)", tt) + } +} + +// Benchmarks + +func Benchmark_FormatUint(b *testing.B) { + inputs := []struct { + name string + value uint64 + }{ + {"small", 42}, + {"medium", 123456789}, + {"large", math.MaxUint64}, + } + + for _, input := range inputs { + b.Run(input.name+"/fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint(input.value) + } + }) + b.Run(input.name+"/strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(input.value, 10) + } + }) + } +} + +func Benchmark_FormatInt(b *testing.B) { + inputs := []struct { + name string + value int64 + }{ + {"small_pos", 42}, + {"small_neg", -42}, + {"medium_pos", 123456789}, + {"medium_neg", -123456789}, + {"large_pos", math.MaxInt64}, + {"large_neg", math.MinInt64}, + } + + for _, input := range inputs { + b.Run(input.name+"/fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt(input.value) + } + }) + b.Run(input.name+"/strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(input.value, 10) + } + }) + } +} + +func Benchmark_FormatUint32(b *testing.B) { + input := uint32(123456789) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint32(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(uint64(input), 10) + } + }) +} + +func Benchmark_FormatInt32(b *testing.B) { + input := int32(-123456789) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt32(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(int64(input), 10) + } + }) +} + +func Benchmark_FormatUint16(b *testing.B) { + input := uint16(12345) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint16(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(uint64(input), 10) + } + }) +} + +func Benchmark_FormatInt16(b *testing.B) { + input := int16(-12345) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt16(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(int64(input), 10) + } + }) +} + +func Benchmark_FormatUint8(b *testing.B) { + input := uint8(255) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint8(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(uint64(input), 10) + } + }) +} + +func Benchmark_FormatInt8(b *testing.B) { + input := int8(-128) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt8(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(int64(input), 10) + } + }) +} + +func Benchmark_AppendUint(b *testing.B) { + input := uint64(123456789) + dst := make([]byte, 0, 32) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = AppendUint(dst, input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.AppendUint(dst, input, 10) + } + }) +} + +func Benchmark_AppendInt(b *testing.B) { + input := int64(-123456789) + dst := make([]byte, 0, 32) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = AppendInt(dst, input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.AppendInt(dst, input, 10) + } + }) +} diff --git a/http.go b/http.go index 37f1d63..db69c5d 100644 --- a/http.go +++ b/http.go @@ -16,25 +16,29 @@ func GetMIME(extension string) string { if len(extension) == 0 { return "" } - var foundMime string + + // Normalize extension once at the start to avoid repeated checks + var extWithoutDot string + var extWithDot string if extension[0] == '.' { - foundMime = mimeExtensions[extension[1:]] + extWithoutDot = extension[1:] + extWithDot = extension } else { - foundMime = mimeExtensions[extension] + extWithoutDot = extension + extWithDot = "." + extension } - if len(foundMime) == 0 { - if extension[0] != '.' { - foundMime = mime.TypeByExtension("." + extension) - } else { - foundMime = mime.TypeByExtension(extension) - } + // Single map lookup with normalized key + if foundMime := mimeExtensions[extWithoutDot]; len(foundMime) > 0 { + return foundMime + } - if foundMime == "" { - return MIMEOctetStream - } + // Fallback to mime package with pre-computed extension + if foundMime := mime.TypeByExtension(extWithDot); foundMime != "" { + return foundMime } - return foundMime + + return MIMEOctetStream } // ParseVendorSpecificContentType check if content type is vendor specific and @@ -70,7 +74,7 @@ func ParseVendorSpecificContentType(cType string, caseInsensitive ...bool) strin return cType } - return working[0:slashIndex+1] + parsableType + return working[:slashIndex+1] + parsableType } // limits for HTTP statuscodes diff --git a/parse.go b/parse.go index 5a3c96c..51e9c37 100644 --- a/parse.go +++ b/parse.go @@ -79,7 +79,7 @@ func parseDigits[S byteSeq](s S, i int) (uint64, error) { // It supports optional '+' or '-' prefix, checks for overflow and underflow, and returns (0, error) on error. func parseSigned[S byteSeq, T Signed](fn string, s S, minRange, maxRange T) (T, error) { if len(s) == 0 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } neg := false @@ -122,13 +122,11 @@ func parseSigned[S byteSeq, T Signed](fn string, s S, minRange, maxRange T) (T, // It does not support sign prefixes, checks for overflow, and returns (0, error) on error. func parseUnsigned[S byteSeq, T Unsigned](fn string, s S, maxRange T) (T, error) { if len(s) == 0 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } - i := 0 - - // Parse digits - n, err := parseDigits(s, i) + // Parse digits directly from index 0 + n, err := parseDigits(s, 0) // Check for overflow if err != nil { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} @@ -144,7 +142,7 @@ func parseUnsigned[S byteSeq, T Unsigned](fn string, s S, maxRange T) (T, error) // on error or overflow. func parseFloat[S byteSeq](fn string, s S) (float64, error) { if len(s) == 0 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } i := 0 neg := false diff --git a/strings_test.go b/strings_test.go index bb60f9c..a4e75ea 100644 --- a/strings_test.go +++ b/strings_test.go @@ -79,11 +79,8 @@ var benchmarkCases = []TestCase{ } func Test_ToUpper(t *testing.T) { - t.Parallel() for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() result := ToUpper(tc.input) require.Equal(t, tc.upper, result, "ToUpper failed for %s", tc.name) if tc.upperNoConv { @@ -108,11 +105,8 @@ func Test_ToUpper(t *testing.T) { } func Test_ToLower(t *testing.T) { - t.Parallel() for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() result := ToLower(tc.input) require.Equal(t, tc.lower, result, "ToLower failed for %s", tc.name) if tc.lowerNoConv { @@ -160,11 +154,19 @@ func Benchmark_ToUpper(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(len(tc.input))) b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = ToUpper(tc.input) - } - require.Equal(b, tc.upper, res) + var fiberRes, stdRes string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + fiberRes = ToUpper(tc.input) + } + require.Equal(b, tc.upper, fiberRes) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + stdRes = strings.ToUpper(tc.input) + } + require.Equal(b, tc.upper, stdRes) + }) }) } } @@ -175,41 +177,19 @@ func Benchmark_ToLower(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(len(tc.input))) b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = ToLower(tc.input) - } - require.Equal(b, tc.lower, res) - }) - } -} - -func Benchmark_StdToUpper(b *testing.B) { - for _, tc := range benchmarkCases { - b.Run(tc.name, func(b *testing.B) { - b.ReportAllocs() - b.SetBytes(int64(len(tc.input))) - b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = strings.ToUpper(tc.input) - } - require.Equal(b, tc.upper, res) - }) - } -} - -func Benchmark_StdToLower(b *testing.B) { - for _, tc := range benchmarkCases { - b.Run(tc.name, func(b *testing.B) { - b.ReportAllocs() - b.SetBytes(int64(len(tc.input))) - b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = strings.ToLower(tc.input) - } - require.Equal(b, tc.lower, res) + var fiberRes, stdRes string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + fiberRes = ToLower(tc.input) + } + require.Equal(b, tc.lower, fiberRes) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + stdRes = strings.ToLower(tc.input) + } + require.Equal(b, tc.lower, stdRes) + }) }) } }