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
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default defineConfig({
{ text: 'os', link: '/os' },
{ text: 'runtime', link: '/runtime' },
{ text: 'sec', link: '/sec' },
{ text: 'slog', link: '/slog' },
{ text: 'slices', link: '/slices' },
{ text: 'strings', link: '/strings' },
{ text: 'testing', link: '/testing' },
Expand Down
6 changes: 3 additions & 3 deletions docs/os.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import osx "github.com/foomo/go/os"
## Package Variables

```go
var SliceSeperator = "," // delimiter for slice elements
var MapSeperator = "," // delimiter for map key-value pairs
var MapKVSeperator = ":" // delimiter between key and value
var SliceSeparator = "," // delimiter for slice elements
var MapSeparator = "," // delimiter for map key-value pairs
var MapKVSeparator = ":" // delimiter between key and value
```

## API
Expand Down
53 changes: 53 additions & 0 deletions docs/slog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# slog

Test-friendly `slog.Handler` that writes log output through `testing.TB`.

## Import

```go
import slogx "github.com/foomo/go/slog"
```

## API

### NewTestHandler

```go
func NewTestHandler(tb testing.TB, opts ...TestHandlerOption) slog.Handler
```

Returns an `slog.Handler` that writes log records to `tb.Output` in a compact format:

```
file.go:42: [LEVEL] msg key=value ...
```

Defaults to `slog.LevelDebug`. Supports `WithAttrs` and `WithGroup` for structured context.

### TestHandlerWithLevel

```go
func TestHandlerWithLevel(level slog.Leveler) TestHandlerOption
```

Sets the minimum log level for the test handler.

## Example

```go
func TestService(t *testing.T) {
logger := slog.New(slogx.NewTestHandler(t))
logger.Info("starting", "port", 8080)
// Output via t.Log: testfile_test.go:4: [INFO] starting port=8080
}
```

### With minimum level

```go
func TestServiceWarn(t *testing.T) {
logger := slog.New(slogx.NewTestHandler(t, slogx.TestHandlerWithLevel(slog.LevelWarn)))
logger.Debug("ignored") // not printed
logger.Warn("something happened", "err", "timeout")
}
```
56 changes: 55 additions & 1 deletion docs/strings.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# strings

String manipulation utilities including case conversions, padding, and substring removal.
String manipulation utilities including case conversions, padding, substring removal, validation, and prefix/suffix matching.

## Import

Expand Down Expand Up @@ -78,6 +78,41 @@ func Compose(s string, funcs ...func(string) string) string

Chains multiple string transformation functions, applying each in order.

### Validation

```go
func IsEmpty(s string) bool
func IsBlank(s string) bool
func IsAnyEmpty(s ...string) bool
func IsAnyBlank(strings ...string) bool
func IsAlpha(s string) bool
func IsAlphanumeric(s string) bool
func IsNumeric(s string) bool
func IsNumerical(s string) bool
```

| Function | Description |
|----------|-------------|
| `IsEmpty` | Returns `true` if the string has zero length. |
| `IsBlank` | Returns `true` if the string is empty or contains only whitespace. |
| `IsAnyEmpty` | Returns `true` if any of the provided strings is empty. |
| `IsAnyBlank` | Returns `true` if any of the provided strings is blank. |
| `IsAlpha` | Returns `true` if the string contains only Unicode letters. |
| `IsAlphanumeric` | Returns `true` if the string contains only Unicode letters and digits. |
| `IsNumeric` | Returns `true` if the string contains only Unicode digits. |
| `IsNumerical` | Returns `true` if the string represents a number (digits with an optional decimal point). |

Empty strings return `false` for `IsAlpha`, `IsAlphanumeric`, `IsNumeric`, and `IsNumerical`.

### Prefix / Suffix

```go
func HasAnyPrefix(s string, prefixes ...string) bool
func HasAnySuffix(s string, suffixes ...string) bool
```

Returns `true` if the string starts (or ends) with any of the provided values. Returns `false` when the string is empty or no candidates are given.

## Examples

### Case conversions
Expand Down Expand Up @@ -105,3 +140,22 @@ fmt.Printf("'%s'", stringsx.PadLeft("hello", 10))
result := stringsx.RemoveAll("hello world", "o", "l")
fmt.Println(result) // "he wrd"
```

### Validation

```go
stringsx.IsEmpty("") // true
stringsx.IsBlank(" \t") // true
stringsx.IsAnyEmpty("a", "") // true
stringsx.IsAlpha("Hello") // true
stringsx.IsAlphanumeric("Go1") // true
stringsx.IsNumeric("12345") // true
stringsx.IsNumerical("3.14") // true
```

### Prefix / Suffix

```go
stringsx.HasAnyPrefix("/api/users", "/api", "/admin") // true
stringsx.HasAnySuffix("image.png", ".jpg", ".png") // true
```
104 changes: 104 additions & 0 deletions strings/is.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package strings

import (
"regexp"
"slices"
"sync"
"unicode"
)

var isNumericalRegex = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^\d+.?\d*$`)
})

// IsEmpty checks if the given string is empty and returns true if it is, otherwise returns false.
func IsEmpty(s string) bool {
return s == ""
}

// IsBlank checks if the given string is blank (contains only whitespace characters) and returns true if it is, otherwise returns false.
func IsBlank(s string) bool {
if s == "" {
return true
}

for _, c := range s {
if !unicode.IsSpace(c) {
return false
}
}

return true
}

// IsAnyEmpty checks if any of the provided strings in the variadic argument is empty and returns true if so.
func IsAnyEmpty(s ...string) bool {
if len(s) == 0 {
return true
}

return slices.Contains(s, "")
}

// IsAnyBlank checks if any of the provided strings in the variadic argument is blank and returns true if so.
func IsAnyBlank(strings ...string) bool {
if len(strings) == 0 {
return true
}

return slices.ContainsFunc(strings, IsBlank)
}

// IsAlpha checks if the given string contains only alphabetic characters and returns true if it is, otherwise returns false.
func IsAlpha(s string) bool {
if s == "" {
return false
}

for _, v := range s {
if !unicode.IsLetter(v) {
return false
}
}

return true
}

// IsAlphanumeric checks if the given string is alphanumeric and returns true if it is, otherwise returns false.
func IsAlphanumeric(s string) bool {
if s == "" {
return false
}

for _, v := range s {
if !isAlphanumeric(v) {
return false
}
}

return true
}

// IsNumeric checks if the given string is numeric and returns true if it is, otherwise returns false.
func IsNumeric(s string) bool {
if s == "" {
return false
}

for _, v := range s {
if !unicode.IsDigit(v) {
return false
}
}

return true
}

// IsNumerical checks if the given string is numerical and returns true if it is, otherwise returns false.
func IsNumerical(s string) bool {
return isNumericalRegex().MatchString(s)
}

func isAlphanumeric(v rune) bool {
return unicode.IsDigit(v) || unicode.IsLetter(v)
}
47 changes: 47 additions & 0 deletions strings/is_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package strings_test

import (
"fmt"

"github.com/foomo/go/strings"
)

func ExampleIsEmpty() {
fmt.Println(strings.IsEmpty(""))
// Output: true
}

func ExampleIsBlank() {
fmt.Println(strings.IsBlank(" \t"))
// Output: true
}

func ExampleIsAnyEmpty() {
fmt.Println(strings.IsAnyEmpty("a", ""))
// Output: true
}

func ExampleIsAnyBlank() {
fmt.Println(strings.IsAnyBlank("a", " "))
// Output: true
}

func ExampleIsAlpha() {
fmt.Println(strings.IsAlpha("abc"))
// Output: true
}

func ExampleIsAlphanumeric() {
fmt.Println(strings.IsAlphanumeric("abc1"))
// Output: true
}

func ExampleIsNumeric() {
fmt.Println(strings.IsNumeric("123"))
// Output: true
}

func ExampleIsNumerical() {
fmt.Println(strings.IsNumerical("12.3"))
// Output: true
}
25 changes: 25 additions & 0 deletions strings/prefix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package strings

import (
"slices"
"strings"
)

// HasAnyPrefix checks if the given string has any of the provided prefixes and returns true if so.
func HasAnyPrefix(s string, prefixes ...string) bool {
if IsEmpty(s) || len(prefixes) == 0 {
return false
}

if slices.Contains(prefixes, s) {
return true
}

for _, prefix := range prefixes {
if strings.HasPrefix(s, prefix) {
return true
}
}

return false
}
12 changes: 12 additions & 0 deletions strings/prefix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package strings_test

import (
"fmt"

"github.com/foomo/go/strings"
)

func ExampleHasAnyPrefix() {
fmt.Println(strings.HasAnyPrefix("foobar", "foo", "baz"))
// Output: true
}
25 changes: 25 additions & 0 deletions strings/suffix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package strings

import (
"slices"
"strings"
)

// HasAnySuffix checks if the given string has any of the provided suffixes and returns true if so.
func HasAnySuffix(s string, suffixes ...string) bool {
if IsEmpty(s) || len(suffixes) == 0 {
return false
}

if slices.Contains(suffixes, s) {
return true
}

for _, suffix := range suffixes {
if strings.HasSuffix(s, suffix) {
return true
}
}

return false
}
12 changes: 12 additions & 0 deletions strings/suffix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package strings_test

import (
"fmt"

"github.com/foomo/go/strings"
)

func ExampleHasAnySuffix() {
fmt.Println(strings.HasAnySuffix("foobar", "bar", "baz"))
// Output: true
}
Loading