From 82197ac9b992cef5b2d09aa7222ef892d176429b Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sun, 12 Oct 2025 21:11:51 +0400 Subject: [PATCH 1/3] feat: add IsNilOrZero utility function to common package --- README.md | 14 ++++++++++++++ common/compare.go | 10 ++++++++++ 2 files changed, 24 insertions(+) create mode 100644 common/compare.go diff --git a/README.md b/README.md index 7d0a745..1009c8d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,20 @@ num := 42 ptr := common.ToPtr(num) // *int ``` +**IsNilOrZero** - Check if a pointer is nil or points to a zero value +```go +import "github.com/aykhans/go-utils/common" + +var ptr *int +common.IsNilOrZero(ptr) // true (nil pointer) + +num := 0 +common.IsNilOrZero(&num) // true (zero value) + +num = 42 +common.IsNilOrZero(&num) // false (non-zero value) +``` + ### parser String parsing utilities with generic type support. diff --git a/common/compare.go b/common/compare.go new file mode 100644 index 0000000..a50944f --- /dev/null +++ b/common/compare.go @@ -0,0 +1,10 @@ +package common + +func IsNilOrZero[T comparable](value *T) bool { + if value == nil { + return true + } + + var zero T + return *value == zero +} From c29e082426496709aa39420d8215f9832b6104ad Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Tue, 28 Oct 2025 17:53:43 +0400 Subject: [PATCH 2/3] feat: add the number package --- README.md | 21 +++ number/base.go | 26 ++++ number/base_test.go | 343 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 number/base.go create mode 100644 number/base_test.go diff --git a/README.md b/README.md index 1009c8d..6008707 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,27 @@ maps.UpdateMap(&old, new) // old is now: {"a": 1, "b": 3, "c": 4} ``` +### number + +Number utility functions. + +**NumLen** - Calculate the number of digits in an integer +```go +import "github.com/aykhans/go-utils/number" + +number.NumLen(42) // returns 2 +number.NumLen(-128) // returns 3 +number.NumLen(0) // returns 1 +number.NumLen(1000) // returns 4 + +// Works with all integer types +number.NumLen(int8(99)) // returns 2 +number.NumLen(int64(123456789)) // returns 9 +number.NumLen(uint32(4294967295)) // returns 10 +``` + +Supported types: `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `uintptr` + ### errors Advanced error handling utilities. diff --git a/number/base.go b/number/base.go new file mode 100644 index 0000000..9eafdb8 --- /dev/null +++ b/number/base.go @@ -0,0 +1,26 @@ +package number + +type Number interface { + int | int8 | int16 | int32 | int64 | + uint | uint8 | uint16 | uint32 | uint64 | uintptr +} + +func NumLen[T Number](number T) T { + if number == 0 { + return 1 + } + + var count T = 0 + if number < 0 { + for number < 0 { + number /= 10 + count++ + } + } else { + for number > 0 { + number /= 10 + count++ + } + } + return count +} diff --git a/number/base_test.go b/number/base_test.go new file mode 100644 index 0000000..4f41b9a --- /dev/null +++ b/number/base_test.go @@ -0,0 +1,343 @@ +package number + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNumLen(t *testing.T) { + t.Run("NumLen with zero", func(t *testing.T) { + assert.Equal(t, 1, NumLen(0)) + assert.Equal(t, int8(1), NumLen(int8(0))) + assert.Equal(t, int16(1), NumLen(int16(0))) + assert.Equal(t, int32(1), NumLen(int32(0))) + assert.Equal(t, int64(1), NumLen(int64(0))) + assert.Equal(t, uint(1), NumLen(uint(0))) + assert.Equal(t, uint8(1), NumLen(uint8(0))) + assert.Equal(t, uint16(1), NumLen(uint16(0))) + assert.Equal(t, uint32(1), NumLen(uint32(0))) + assert.Equal(t, uint64(1), NumLen(uint64(0))) + }) + + t.Run("NumLen with single digit positive numbers", func(t *testing.T) { + tests := []struct { + name string + input int + expected int + }{ + {"one", 1, 1}, + {"five", 5, 1}, + {"nine", 9, 1}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with single digit negative numbers", func(t *testing.T) { + tests := []struct { + name string + input int + expected int + }{ + {"negative one", -1, 1}, + {"negative five", -5, 1}, + {"negative nine", -9, 1}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with multi-digit positive numbers", func(t *testing.T) { + tests := []struct { + name string + input int + expected int + }{ + {"two digits", 42, 2}, + {"three digits", 123, 3}, + {"four digits", 9999, 4}, + {"five digits", 12345, 5}, + {"six digits", 100000, 6}, + {"seven digits", 9876543, 7}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with multi-digit negative numbers", func(t *testing.T) { + tests := []struct { + name string + input int + expected int + }{ + {"negative two digits", -42, 2}, + {"negative three digits", -123, 3}, + {"negative four digits", -9999, 4}, + {"negative five digits", -12345, 5}, + {"negative six digits", -100000, 6}, + {"negative seven digits", -9876543, 7}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with int8", func(t *testing.T) { + tests := []struct { + name string + input int8 + expected int8 + }{ + {"zero", 0, 1}, + {"positive single", 9, 1}, + {"positive double", 99, 2}, + {"positive triple", 127, 3}, + {"negative single", -9, 1}, + {"negative double", -99, 2}, + {"negative triple", -128, 3}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with int16", func(t *testing.T) { + tests := []struct { + name string + input int16 + expected int16 + }{ + {"zero", 0, 1}, + {"positive single", 5, 1}, + {"positive double", 42, 2}, + {"positive triple", 999, 3}, + {"positive max", 32767, 5}, + {"negative single", -5, 1}, + {"negative double", -42, 2}, + {"negative triple", -999, 3}, + {"negative min", -32768, 5}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with int32", func(t *testing.T) { + tests := []struct { + name string + input int32 + expected int32 + }{ + {"zero", 0, 1}, + {"positive single", 7, 1}, + {"positive five", 12345, 5}, + {"positive max", 2147483647, 10}, + {"negative single", -7, 1}, + {"negative five", -12345, 5}, + {"negative min", -2147483648, 10}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with int64", func(t *testing.T) { + tests := []struct { + name string + input int64 + expected int64 + }{ + {"zero", 0, 1}, + {"positive single", 3, 1}, + {"positive ten", 1234567890, 10}, + {"positive max", 9223372036854775807, 19}, + {"negative single", -3, 1}, + {"negative ten", -1234567890, 10}, + {"negative min", -9223372036854775808, 19}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with uint", func(t *testing.T) { + tests := []struct { + name string + input uint + expected uint + }{ + {"zero", 0, 1}, + {"single digit", 5, 1}, + {"double digit", 42, 2}, + {"triple digit", 999, 3}, + {"large number", 123456789, 9}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with uint8", func(t *testing.T) { + tests := []struct { + name string + input uint8 + expected uint8 + }{ + {"zero", 0, 1}, + {"single digit", 9, 1}, + {"double digit", 99, 2}, + {"max value", 255, 3}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with uint16", func(t *testing.T) { + tests := []struct { + name string + input uint16 + expected uint16 + }{ + {"zero", 0, 1}, + {"single digit", 8, 1}, + {"double digit", 50, 2}, + {"triple digit", 500, 3}, + {"max value", 65535, 5}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with uint32", func(t *testing.T) { + tests := []struct { + name string + input uint32 + expected uint32 + }{ + {"zero", 0, 1}, + {"single digit", 6, 1}, + {"five digits", 54321, 5}, + {"max value", 4294967295, 10}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with uint64", func(t *testing.T) { + tests := []struct { + name string + input uint64 + expected uint64 + }{ + {"zero", 0, 1}, + {"single digit", 4, 1}, + {"ten digits", 9876543210, 10}, + {"max value", 18446744073709551615, 20}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := NumLen(test.input) + assert.Equal(t, test.expected, result) + }) + } + }) + + t.Run("NumLen with boundary values", func(t *testing.T) { + t.Run("powers of 10", func(t *testing.T) { + assert.Equal(t, 1, NumLen(1)) + assert.Equal(t, 2, NumLen(10)) + assert.Equal(t, 3, NumLen(100)) + assert.Equal(t, 4, NumLen(1000)) + assert.Equal(t, 5, NumLen(10000)) + assert.Equal(t, 6, NumLen(100000)) + assert.Equal(t, 7, NumLen(1000000)) + }) + + t.Run("one less than powers of 10", func(t *testing.T) { + assert.Equal(t, 1, NumLen(9)) + assert.Equal(t, 2, NumLen(99)) + assert.Equal(t, 3, NumLen(999)) + assert.Equal(t, 4, NumLen(9999)) + assert.Equal(t, 5, NumLen(99999)) + assert.Equal(t, 6, NumLen(999999)) + }) + + t.Run("negative powers of 10", func(t *testing.T) { + assert.Equal(t, 1, NumLen(-1)) + assert.Equal(t, 2, NumLen(-10)) + assert.Equal(t, 3, NumLen(-100)) + assert.Equal(t, 4, NumLen(-1000)) + assert.Equal(t, 5, NumLen(-10000)) + }) + }) + + t.Run("NumLen consistency across types", func(t *testing.T) { + // Same value, different types should return same digit count + assert.Equal(t, 2, NumLen(42)) + assert.Equal(t, int8(2), NumLen(int8(42))) + assert.Equal(t, int16(2), NumLen(int16(42))) + assert.Equal(t, int32(2), NumLen(int32(42))) + assert.Equal(t, int64(2), NumLen(int64(42))) + assert.Equal(t, uint(2), NumLen(uint(42))) + assert.Equal(t, uint8(2), NumLen(uint8(42))) + assert.Equal(t, uint16(2), NumLen(uint16(42))) + assert.Equal(t, uint32(2), NumLen(uint32(42))) + assert.Equal(t, uint64(2), NumLen(uint64(42))) + }) +} From 70b1a8300fc5dffae546b8311063342e3272f295 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Tue, 28 Oct 2025 18:00:40 +0400 Subject: [PATCH 3/3] test: add tests for the 'IsNilOrZero' function --- common/compare_test.go | 350 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 common/compare_test.go diff --git a/common/compare_test.go b/common/compare_test.go new file mode 100644 index 0000000..bc3b86d --- /dev/null +++ b/common/compare_test.go @@ -0,0 +1,350 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsNilOrZero(t *testing.T) { + t.Run("nil pointer returns true", func(t *testing.T) { + var intPtr *int + assert.True(t, IsNilOrZero(intPtr)) + + var strPtr *string + assert.True(t, IsNilOrZero(strPtr)) + + var boolPtr *bool + assert.True(t, IsNilOrZero(boolPtr)) + + var floatPtr *float64 + assert.True(t, IsNilOrZero(floatPtr)) + }) + + t.Run("pointer to zero value returns true", func(t *testing.T) { + t.Run("int zero", func(t *testing.T) { + val := 0 + assert.True(t, IsNilOrZero(&val)) + }) + + t.Run("string zero", func(t *testing.T) { + val := "" + assert.True(t, IsNilOrZero(&val)) + }) + + t.Run("bool zero", func(t *testing.T) { + val := false + assert.True(t, IsNilOrZero(&val)) + }) + + t.Run("float64 zero", func(t *testing.T) { + val := 0.0 + assert.True(t, IsNilOrZero(&val)) + }) + + t.Run("uint zero", func(t *testing.T) { + val := uint(0) + assert.True(t, IsNilOrZero(&val)) + }) + }) + + t.Run("pointer to non-zero value returns false", func(t *testing.T) { + t.Run("int non-zero", func(t *testing.T) { + val := 42 + assert.False(t, IsNilOrZero(&val)) + + negVal := -1 + assert.False(t, IsNilOrZero(&negVal)) + }) + + t.Run("string non-zero", func(t *testing.T) { + val := "hello" + assert.False(t, IsNilOrZero(&val)) + + spaceVal := " " + assert.False(t, IsNilOrZero(&spaceVal)) + }) + + t.Run("bool non-zero", func(t *testing.T) { + val := true + assert.False(t, IsNilOrZero(&val)) + }) + + t.Run("float64 non-zero", func(t *testing.T) { + val := 3.14 + assert.False(t, IsNilOrZero(&val)) + + negVal := -2.5 + assert.False(t, IsNilOrZero(&negVal)) + }) + + t.Run("uint non-zero", func(t *testing.T) { + val := uint(100) + assert.False(t, IsNilOrZero(&val)) + }) + }) + + t.Run("works with all integer types", func(t *testing.T) { + t.Run("int8", func(t *testing.T) { + var nilPtr *int8 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := int8(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := int8(127) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("int16", func(t *testing.T) { + var nilPtr *int16 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := int16(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := int16(1000) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("int32", func(t *testing.T) { + var nilPtr *int32 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := int32(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := int32(100000) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("int64", func(t *testing.T) { + var nilPtr *int64 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := int64(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := int64(9223372036854775807) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("uint8", func(t *testing.T) { + var nilPtr *uint8 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := uint8(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := uint8(255) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("uint16", func(t *testing.T) { + var nilPtr *uint16 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := uint16(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := uint16(65535) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("uint32", func(t *testing.T) { + var nilPtr *uint32 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := uint32(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := uint32(4294967295) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("uint64", func(t *testing.T) { + var nilPtr *uint64 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := uint64(0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := uint64(18446744073709551615) + assert.False(t, IsNilOrZero(&nonZero)) + }) + }) + + t.Run("works with float types", func(t *testing.T) { + t.Run("float32", func(t *testing.T) { + var nilPtr *float32 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := float32(0.0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := float32(1.5) + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("float64", func(t *testing.T) { + var nilPtr *float64 + assert.True(t, IsNilOrZero(nilPtr)) + + zero := float64(0.0) + assert.True(t, IsNilOrZero(&zero)) + + nonZero := float64(123.456) + assert.False(t, IsNilOrZero(&nonZero)) + }) + }) + + t.Run("works with struct types", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + t.Run("nil struct pointer", func(t *testing.T) { + var nilPtr *Person + assert.True(t, IsNilOrZero(nilPtr)) + }) + + t.Run("pointer to zero struct", func(t *testing.T) { + zero := Person{} + assert.True(t, IsNilOrZero(&zero)) + }) + + t.Run("pointer to non-zero struct", func(t *testing.T) { + nonZero := Person{Name: "John", Age: 30} + assert.False(t, IsNilOrZero(&nonZero)) + + partiallyFilled := Person{Name: "Jane"} + assert.False(t, IsNilOrZero(&partiallyFilled)) + + onlyAge := Person{Age: 25} + assert.False(t, IsNilOrZero(&onlyAge)) + }) + }) + + t.Run("works with array types", func(t *testing.T) { + t.Run("nil array pointer", func(t *testing.T) { + var nilPtr *[3]int + assert.True(t, IsNilOrZero(nilPtr)) + }) + + t.Run("pointer to zero array", func(t *testing.T) { + zero := [3]int{0, 0, 0} + assert.True(t, IsNilOrZero(&zero)) + }) + + t.Run("pointer to non-zero array", func(t *testing.T) { + nonZero := [3]int{1, 2, 3} + assert.False(t, IsNilOrZero(&nonZero)) + + partiallyFilled := [3]int{1, 0, 0} + assert.False(t, IsNilOrZero(&partiallyFilled)) + }) + }) + + t.Run("edge cases", func(t *testing.T) { + t.Run("pointer to negative number", func(t *testing.T) { + val := -1 + assert.False(t, IsNilOrZero(&val)) + }) + + t.Run("pointer to whitespace string", func(t *testing.T) { + val := " " + assert.False(t, IsNilOrZero(&val)) + + tab := "\t" + assert.False(t, IsNilOrZero(&tab)) + + newline := "\n" + assert.False(t, IsNilOrZero(&newline)) + }) + + t.Run("pointer to byte (uint8) zero", func(t *testing.T) { + val := byte(0) + assert.True(t, IsNilOrZero(&val)) + + nonZero := byte('A') + assert.False(t, IsNilOrZero(&nonZero)) + }) + + t.Run("pointer to rune (int32) zero", func(t *testing.T) { + val := rune(0) + assert.True(t, IsNilOrZero(&val)) + + nonZero := rune('Z') + assert.False(t, IsNilOrZero(&nonZero)) + }) + }) + + t.Run("consistency with ToPtr", func(t *testing.T) { + t.Run("ToPtr of zero value is nil or zero", func(t *testing.T) { + intPtr := ToPtr(0) + assert.True(t, IsNilOrZero(intPtr)) + + strPtr := ToPtr("") + assert.True(t, IsNilOrZero(strPtr)) + + boolPtr := ToPtr(false) + assert.True(t, IsNilOrZero(boolPtr)) + }) + + t.Run("ToPtr of non-zero value is not nil or zero", func(t *testing.T) { + intPtr := ToPtr(42) + assert.False(t, IsNilOrZero(intPtr)) + + strPtr := ToPtr("hello") + assert.False(t, IsNilOrZero(strPtr)) + + boolPtr := ToPtr(true) + assert.False(t, IsNilOrZero(boolPtr)) + }) + }) + + t.Run("real-world usage scenarios", func(t *testing.T) { + t.Run("optional configuration value", func(t *testing.T) { + type Config struct { + Port *int + Timeout *int + } + + // No port configured (nil) + config1 := Config{} + assert.True(t, IsNilOrZero(config1.Port)) + + // Port explicitly set to 0 + zero := 0 + config2 := Config{Port: &zero} + assert.True(t, IsNilOrZero(config2.Port)) + + // Port configured to 8080 + port := 8080 + config3 := Config{Port: &port} + assert.False(t, IsNilOrZero(config3.Port)) + }) + + t.Run("optional string field", func(t *testing.T) { + type User struct { + Name string + Nickname *string + } + + // No nickname provided + user1 := User{Name: "John"} + assert.True(t, IsNilOrZero(user1.Nickname)) + + // Nickname explicitly empty + emptyNick := "" + user2 := User{Name: "Jane", Nickname: &emptyNick} + assert.True(t, IsNilOrZero(user2.Nickname)) + + // Nickname provided + nick := "Johnny" + user3 := User{Name: "John", Nickname: &nick} + assert.False(t, IsNilOrZero(user3.Nickname)) + }) + }) +}