diff --git a/README.md b/README.md index 8e134a7..535e87b 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,47 @@ Benchmark_CalculateTimestamp/fiber-12 1000000000 0.2935 ns/op Benchmark_CalculateTimestamp/default-12 15740576 73.79 ns/op 0 B/op 0 allocs/op Benchmark_CalculateTimestamp/default-12 15789036 71.12 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint/fiber-12 190390941 6.292 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint/fiber-12 187968758 6.400 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint/fiber_bytes-12 181957326 6.809 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint/fiber_bytes-12 182275550 6.558 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint/default-12 88281543 13.52 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint/default-12 88967146 13.41 ns/op 0 B/op 0 allocs/op + +Benchmark_ParseInt/fiber-12 181353142 6.723 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt/fiber-12 180631305 6.578 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt/fiber_bytes-12 175220041 6.892 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt/fiber_bytes-12 171838354 7.020 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt/default-12 76055068 15.77 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt/default-12 75963992 15.55 ns/op 0 B/op 0 allocs/op + +Benchmark_ParseInt32/fiber-12 179962680 6.631 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt32/fiber-12 181285437 6.570 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt32/fiber_bytes-12 173786900 6.901 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt32/fiber_bytes-12 171283489 7.069 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt32/default-12 69845103 15.75 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt32/default-12 76438194 15.66 ns/op 0 B/op 0 allocs/op + +Benchmark_ParseInt8/fiber-12 286492362 4.148 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt8/fiber-12 282957276 4.147 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt8/fiber_bytes-12 270179119 4.481 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt8/fiber_bytes-12 258238294 4.522 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt8/default-12 135063286 8.831 ns/op 0 B/op 0 allocs/op +Benchmark_ParseInt8/default-12 140703313 8.528 ns/op 0 B/op 0 allocs/op + +Benchmark_ParseUint32/fiber-12 184411585 6.568 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint32/fiber-12 184338627 6.543 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint32/fiber_bytes-12 178475793 6.759 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint32/fiber_bytes-12 178517788 7.052 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint32/default-12 83775481 13.41 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint32/default-12 88117585 13.51 ns/op 0 B/op 0 allocs/op + +Benchmark_ParseUint8/fiber-12 401799110 3.046 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint8/fiber-12 380578648 3.036 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint8/fiber_bytes-12 363442573 3.344 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint8/fiber_bytes-12 357869246 3.346 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint8/default-12 184238403 6.788 ns/op 0 B/op 0 allocs/op +Benchmark_ParseUint8/default-12 186525054 6.454 ns/op 0 B/op 0 allocs/op ``` See all the benchmarks under diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..9a2d8f5 --- /dev/null +++ b/parse.go @@ -0,0 +1,124 @@ +package utils + +import ( + "math" +) + +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// ParseUint parses a decimal ASCII string or byte slice into a uint64. +// It returns the parsed value and true on success. +// If the input contains non-digit characters, it returns 0 and false. +func ParseUint[S byteSeq](s S) (uint64, bool) { + return parseUnsigned[S, uint64](s, uint64(math.MaxUint64)) +} + +// ParseInt parses a decimal ASCII string or byte slice into an int64. +// Returns the parsed value and true on success, else 0 and false. +func ParseInt[S byteSeq](s S) (int64, bool) { + return parseSigned[S, int64](s, math.MinInt64, math.MaxInt64) +} + +// ParseInt32 parses a decimal ASCII string or byte slice into an int32. +func ParseInt32[S byteSeq](s S) (int32, bool) { + return parseSigned[S, int32](s, math.MinInt32, math.MaxInt32) +} + +// ParseInt8 parses a decimal ASCII string or byte slice into an int8. +func ParseInt8[S byteSeq](s S) (int8, bool) { + return parseSigned[S, int8](s, math.MinInt8, math.MaxInt8) +} + +// ParseUint32 parses a decimal ASCII string or byte slice into a uint32. +func ParseUint32[S byteSeq](s S) (uint32, bool) { + return parseUnsigned[S, uint32](s, uint32(math.MaxUint32)) +} + +// ParseUint8 parses a decimal ASCII string or byte slice into a uint8. +func ParseUint8[S byteSeq](s S) (uint8, bool) { + return parseUnsigned[S, uint8](s, uint8(math.MaxUint8)) +} + +// parseDigits parses a sequence of digits and returns the uint64 value and success. +// Returns (0, false) if any non-digit is encountered or overflow happens. +func parseDigits[S byteSeq](s S, i int) (uint64, bool) { + var n uint64 + for ; i < len(s); i++ { + c := s[i] - '0' + if c > 9 { + return 0, false + } + nn := n*10 + uint64(c) + if nn < n { + return 0, false + } + n = nn + } + return n, true +} + +// parseSigned parses a decimal ASCII string or byte slice into a signed integer type T. +// It supports optional '+' or '-' prefix, checks for overflow and underflow, and returns (0, false) on error. +func parseSigned[S byteSeq, T Signed](s S, minRange, maxRange T) (T, bool) { + if len(s) == 0 { + return 0, false + } + + neg := false + i := 0 + switch s[0] { + case '-': + neg = true + i++ + case '+': + i++ + } + if i == len(s) { + return 0, false + } + + // Parse digits + n, ok := parseDigits(s, i) + if !ok { + return 0, false + } + + if !neg { + // Check for overflow + if n > uint64(int64(maxRange)) { + return 0, false + } + return T(n), true + } + + // Check for underflow + minAbs := uint64(-int64(minRange)) + if n > minAbs { + return 0, false + } + + return T(-int64(n)), true +} + +// parseUnsigned parses a decimal ASCII string or byte slice into an unsigned integer type T. +// It does not support sign prefixes, checks for overflow, and returns (0, false) on error. +func parseUnsigned[S byteSeq, T Unsigned](s S, maxRange T) (T, bool) { + if len(s) == 0 { + return 0, false + } + + i := 0 + + // Parse digits + n, ok := parseDigits(s, i) + // Check for overflow + if !ok || n > uint64(maxRange) { + return 0, false + } + return T(n), true +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..9cdb7a0 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,392 @@ +package utils + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_ParseUint(t *testing.T) { + t.Parallel() + tests := []struct { + in string + val uint64 + success bool + }{ + {"0", 0, true}, + {"42", 42, true}, + {"123456789", 123456789, true}, + {"", 0, false}, + {"12a", 0, false}, + } + for _, tt := range tests { + v, ok := ParseUint(tt.in) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, v) + } + b, ok := ParseUint([]byte(tt.in)) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, b) + } + } +} + +func Benchmark_ParseUint(b *testing.B) { + input := "123456789" + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseUint(input) + if !ok { + b.Fatal("failed to parse uint") + } + } + }) + b.Run("fiber_bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseUint([]byte(input)) + if !ok { + b.Fatal("failed to parse uint from bytes") + } + } + }) + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, err := strconv.ParseUint(input, 10, 64) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func Test_ParseInt(t *testing.T) { + t.Parallel() + tests := []struct { + in string + val int64 + success bool + }{ + {"0", 0, true}, + {"42", 42, true}, + {"-42", -42, true}, + {"123456789", 123456789, true}, + {"", 0, false}, + {"12a", 0, false}, + {"-", 0, false}, + } + for _, tt := range tests { + v, ok := ParseInt(tt.in) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, v) + } + b, ok := ParseInt([]byte(tt.in)) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, b) + } + } +} + +func Test_ParseInt_SignOnly(t *testing.T) { + t.Parallel() + + tests := []string{"+", "-"} + for _, in := range tests { + v, ok := ParseInt(in) + require.False(t, ok) + require.Equal(t, int64(0), v) + b, ok := ParseInt([]byte(in)) + require.False(t, ok) + require.Equal(t, int64(0), b) + } +} + +func Test_ParseUnsigned_SignOnly(t *testing.T) { + t.Parallel() + + in := "+" + v, ok := ParseUint(in) + require.False(t, ok) + require.Equal(t, uint64(0), v) + b, ok := ParseUint([]byte(in)) + require.False(t, ok) + require.Equal(t, uint64(0), b) +} + +func Benchmark_ParseInt(b *testing.B) { + input := "123456789" + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseInt(input) + if !ok { + b.Fatal("failed to parse int") + } + } + }) + b.Run("fiber_bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseInt([]byte(input)) + if !ok { + b.Fatal("failed to parse int from bytes") + } + } + }) + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, err := strconv.ParseInt(input, 10, 64) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func Test_ParseInt32(t *testing.T) { + t.Parallel() + tests := []struct { + in string + val int32 + success bool + }{ + {"0", 0, true}, + {"42", 42, true}, + {"2147483647", 2147483647, true}, + {"-2147483648", -2147483648, true}, + {"2147483648", 0, false}, + {"-2147483649", 0, false}, + } + for _, tt := range tests { + v, ok := ParseInt32(tt.in) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, v) + } + b, ok := ParseInt32([]byte(tt.in)) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, b) + } + } +} + +func Benchmark_ParseInt32(b *testing.B) { + input := "123456789" + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseInt32(input) + if !ok { + b.Fatal("failed to parse int32") + } + } + }) + b.Run("fiber_bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseInt32([]byte(input)) + if !ok { + b.Fatal("failed to parse int32 from bytes") + } + } + }) + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, err := strconv.ParseInt(input, 10, 32) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func Test_ParseInt8(t *testing.T) { + t.Parallel() + tests := []struct { + in string + val int8 + success bool + }{ + {"0", 0, true}, + {"42", 42, true}, + {"127", 127, true}, + {"-128", -128, true}, + {"128", 0, false}, + {"-129", 0, false}, + } + for _, tt := range tests { + v, ok := ParseInt8(tt.in) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, v) + } + b, ok := ParseInt8([]byte(tt.in)) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, b) + } + } +} + +func Benchmark_ParseInt8(b *testing.B) { + input := "127" + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseInt8(input) + if !ok { + b.Fatal("failed to parse int8") + } + } + }) + b.Run("fiber_bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseInt8([]byte(input)) + if !ok { + b.Fatal("failed to parse int8 from bytes") + } + } + }) + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, err := strconv.ParseInt(input, 10, 8) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func Test_ParseUint32(t *testing.T) { + t.Parallel() + tests := []struct { + in string + val uint32 + success bool + }{ + {"0", 0, true}, + {"42", 42, true}, + {"4294967295", 4294967295, true}, + {"4294967296", 0, false}, + {"-1", 0, false}, + } + for _, tt := range tests { + v, ok := ParseUint32(tt.in) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, v) + } + b, ok := ParseUint32([]byte(tt.in)) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, b) + } + } +} + +func Benchmark_ParseUint32(b *testing.B) { + input := "123456789" + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseUint32(input) + if !ok { + b.Fatal("failed to parse uint32") + } + } + }) + b.Run("fiber_bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseUint32([]byte(input)) + if !ok { + b.Fatal("failed to parse uint32 from bytes") + } + } + }) + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, err := strconv.ParseUint(input, 10, 32) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func Test_ParseUint8(t *testing.T) { + t.Parallel() + tests := []struct { + in string + val uint8 + success bool + }{ + {"0", 0, true}, + {"42", 42, true}, + {"255", 255, true}, + {"256", 0, false}, + {"-1", 0, false}, + } + for _, tt := range tests { + v, ok := ParseUint8(tt.in) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, v) + } + b, ok := ParseUint8([]byte(tt.in)) + require.Equal(t, tt.success, ok) + if ok { + require.Equal(t, tt.val, b) + } + } +} + +func Benchmark_ParseUint8(b *testing.B) { + input := "255" + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseUint8(input) + if !ok { + b.Fatal("failed to parse uint8") + } + } + }) + b.Run("fiber_bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, ok := ParseUint8([]byte(input)) + if !ok { + b.Fatal("failed to parse uint8 from bytes") + } + } + }) + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, err := strconv.ParseUint(input, 10, 8) + if err != nil { + b.Fatal(err) + } + } + }) +}