Initial QSfera import
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
.github
|
||||
lab
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Oleku Konko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
# 🐱 `cat` - The Fast & Fluent String Concatenation Library for Go
|
||||
|
||||
> **"Because building strings shouldn't feel like herding cats"** 😼
|
||||
|
||||
## Why `cat`?
|
||||
|
||||
Go's `strings.Builder` is great, but building complex strings often feels clunky. `cat` makes string concatenation:
|
||||
|
||||
- **Faster** - Optimized paths for common types, zero-allocation conversions
|
||||
- **Fluent** - Chainable methods for beautiful, readable code
|
||||
- **Flexible** - Handles any type, nested structures, and custom formatting
|
||||
- **Smart** - Automatic pooling, size estimation, and separator handling
|
||||
|
||||
```go
|
||||
// Without cat
|
||||
var b strings.Builder
|
||||
b.WriteString("Hello, ")
|
||||
b.WriteString(user.Name)
|
||||
b.WriteString("! You have ")
|
||||
b.WriteString(strconv.Itoa(count))
|
||||
b.WriteString(" new messages.")
|
||||
result := b.String()
|
||||
|
||||
// With cat
|
||||
result := cat.Concat("Hello, ", user.Name, "! You have ", count, " new messages.")
|
||||
```
|
||||
|
||||
## 🔥 Hot Features
|
||||
|
||||
### 1. Fluent Builder API
|
||||
|
||||
Build strings like a boss with method chaining:
|
||||
|
||||
```go
|
||||
s := cat.New(", ").
|
||||
Add("apple").
|
||||
If(user.IsVIP, "golden kiwi").
|
||||
Add("orange").
|
||||
Sep(" | "). // Change separator mid-way
|
||||
Add("banana").
|
||||
String()
|
||||
// "apple, golden kiwi, orange | banana"
|
||||
```
|
||||
|
||||
### 2. Zero-Allocation Magic
|
||||
|
||||
- **Pooled builders** (optional) reduce GC pressure
|
||||
- **Unsafe byte conversions** (opt-in) avoid `[]byte`→`string` copies
|
||||
- **Stack buffers** for numbers instead of heap allocations
|
||||
|
||||
```go
|
||||
// Enable performance features
|
||||
cat.Pool(true) // Builder pooling
|
||||
cat.SetUnsafeBytes(true) // Zero-copy []byte conversion
|
||||
```
|
||||
|
||||
### 3. Handles Any Type - Even Nested Ones!
|
||||
|
||||
No more manual type conversions:
|
||||
|
||||
```go
|
||||
data := map[string]any{
|
||||
"id": 12345,
|
||||
"tags": []string{"go", "fast", "efficient"},
|
||||
}
|
||||
|
||||
fmt.Println(cat.JSONPretty(data))
|
||||
// {
|
||||
// "id": 12345,
|
||||
// "tags": ["go", "fast", "efficient"]
|
||||
// }
|
||||
```
|
||||
|
||||
### 4. Concatenation for Every Use Case
|
||||
|
||||
```go
|
||||
// Simple joins
|
||||
cat.With(", ", "apple", "banana", "cherry") // "apple, banana, cherry"
|
||||
|
||||
// File paths
|
||||
cat.Path("dir", "sub", "file.txt") // "dir/sub/file.txt"
|
||||
|
||||
// CSV
|
||||
cat.CSV(1, 2, 3) // "1,2,3"
|
||||
|
||||
// Conditional elements
|
||||
cat.Start("Hello").If(user != nil, " ", user.Name) // "Hello" or "Hello Alice"
|
||||
|
||||
// Repeated patterns
|
||||
cat.RepeatWith("-+", "X", 3) // "X-+X-+X"
|
||||
```
|
||||
|
||||
### 5. Smarter Than Your Average String Lib
|
||||
|
||||
```go
|
||||
// Automatic nesting handling
|
||||
nested := []any{"a", []any{"b", "c"}, "d"}
|
||||
cat.FlattenWith(",", nested) // "a,b,c,d"
|
||||
|
||||
// Precise size estimation (minimizes allocations)
|
||||
b := cat.New(", ").Grow(estimatedSize) // Preallocate exactly what you need
|
||||
|
||||
// Reflection support for any type
|
||||
cat.Reflect(anyComplexStruct) // "{Field1:value Field2:[1 2 3]}"
|
||||
```
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
```bash
|
||||
go get github.com/your-repo/cat
|
||||
```
|
||||
|
||||
```go
|
||||
import "github.com/your-repo/cat"
|
||||
|
||||
func main() {
|
||||
// Simple concatenation
|
||||
msg := cat.Concat("User ", userID, " has ", count, " items")
|
||||
|
||||
// Pooled builder (for high-performance loops)
|
||||
builder := cat.New(", ")
|
||||
defer builder.Release() // Return to pool
|
||||
result := builder.Add(items...).String()
|
||||
}
|
||||
```
|
||||
|
||||
## 🤔 Why Not Just Use...?
|
||||
|
||||
- `fmt.Sprintf` - Slow, many allocations
|
||||
- `strings.Join` - Only works with strings
|
||||
- `bytes.Buffer` - No separator support, manual type handling
|
||||
- `string +` - Even worse performance, especially in loops
|
||||
|
||||
## 💡 Pro Tips
|
||||
|
||||
1. **Enable pooling** in high-throughput scenarios
|
||||
2. **Preallocate** with `.Grow()` when you know the final size
|
||||
3. Use **`If()`** for conditional elements in fluent chains
|
||||
4. Try **`SetUnsafeBytes(true)`** if you can guarantee byte slices won't mutate
|
||||
5. **Release builders** when pooling is enabled
|
||||
|
||||
## 🐱👤 Advanced Usage
|
||||
|
||||
```go
|
||||
// Custom value formatting
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
return cat.With(" ", u.Name, cat.Wrap("(", u.Age, ")"))
|
||||
}
|
||||
|
||||
// JSON-like output
|
||||
func JSONPretty(v any) string {
|
||||
return cat.WrapWith(",\n ", "{\n ", "\n}", prettyFields(v))
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
/\_/\
|
||||
( o.o ) > Concatenate with purr-fection!
|
||||
> ^ <
|
||||
|
||||
```
|
||||
|
||||
**`cat`** - Because life's too short for ugly string building code. 😻
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package cat
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Builder is a fluent concatenation helper. It is safe for concurrent use by
|
||||
// multiple goroutines only if each goroutine uses a distinct *Builder.
|
||||
// If pooling is enabled via Pool(true), call Release() when done.
|
||||
// The Builder uses an internal strings.Builder for efficient string concatenation
|
||||
// and manages a separator that is inserted between added values.
|
||||
// It supports chaining methods for a fluent API style.
|
||||
type Builder struct {
|
||||
buf strings.Builder
|
||||
sep string
|
||||
needsSep bool
|
||||
}
|
||||
|
||||
// New begins a new Builder with a separator. If pooling is enabled,
|
||||
// the Builder is reused and MUST be released with b.Release() when done.
|
||||
// If sep is empty, uses DefaultSep().
|
||||
// Optional initial arguments x are added immediately after creation.
|
||||
// Pooling is controlled globally via Pool(true/false); when enabled, Builders
|
||||
// are recycled to reduce allocations in high-throughput scenarios.
|
||||
func New(sep string, x ...any) *Builder {
|
||||
var b *Builder
|
||||
if poolEnabled.Load() {
|
||||
b = builderPool.Get().(*Builder)
|
||||
b.buf.Reset()
|
||||
b.sep = sep
|
||||
b.needsSep = false
|
||||
} else {
|
||||
b = &Builder{sep: sep}
|
||||
}
|
||||
|
||||
// Process initial arguments *after* the builder is prepared.
|
||||
if len(x) > 0 {
|
||||
b.Add(x...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Start begins a new Builder with no separator (using an empty string as sep).
|
||||
// It is a convenience function that wraps New(empty, x...), where empty is a constant empty string.
|
||||
// This allows starting a concatenation without any separator between initial or subsequent additions.
|
||||
// If pooling is enabled via Pool(true), the returned Builder MUST be released with b.Release() when done.
|
||||
// Optional variadic arguments x are passed directly to New and added immediately after creation.
|
||||
// Useful for fluent chains where no default separator is desired from the start.
|
||||
func Start(x ...any) *Builder {
|
||||
return New(empty, x...)
|
||||
}
|
||||
|
||||
// Grow pre-sizes the internal buffer.
|
||||
// This can be used to preallocate capacity based on an estimated total size,
|
||||
// reducing reallocations during subsequent Add calls.
|
||||
// It chains, returning the Builder for fluent use.
|
||||
func (b *Builder) Grow(n int) *Builder { b.buf.Grow(n); return b }
|
||||
|
||||
// Add appends values to the builder.
|
||||
// It inserts the current separator before each new value if needed (i.e., after the first addition).
|
||||
// Values are converted to strings using the optimized write function, which handles
|
||||
// common types efficiently without allocations where possible.
|
||||
// Supports any number of arguments of any type.
|
||||
// Chains, returning the Builder for fluent use.
|
||||
func (b *Builder) Add(args ...any) *Builder {
|
||||
for _, arg := range args {
|
||||
if b.needsSep && b.sep != empty {
|
||||
b.buf.WriteString(b.sep)
|
||||
}
|
||||
write(&b.buf, arg)
|
||||
b.needsSep = true
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// If appends values to the builder only if the condition is true.
|
||||
// Behaves like Add when condition is true; does nothing otherwise.
|
||||
// Useful for conditional concatenation in chains.
|
||||
// Chains, returning the Builder for fluent use.
|
||||
func (b *Builder) If(condition bool, args ...any) *Builder {
|
||||
if condition {
|
||||
b.Add(args...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Sep changes the separator for subsequent additions.
|
||||
// Future Add calls will use this new separator.
|
||||
// Does not affect already added content.
|
||||
// If sep is empty, no separator will be added between future values.
|
||||
// Chains, returning the Builder for fluent use.
|
||||
func (b *Builder) Sep(sep string) *Builder { b.sep = sep; return b }
|
||||
|
||||
// String returns the concatenated result.
|
||||
// This does not release the Builder; if pooling is enabled, call Release separately
|
||||
// if you are done with the Builder.
|
||||
// Can be called multiple times; the internal buffer remains unchanged.
|
||||
func (b *Builder) String() string { return b.buf.String() }
|
||||
|
||||
// Output returns the concatenated result and releases the Builder if pooling is enabled.
|
||||
// This is a convenience method to get the string and clean up in one call.
|
||||
// After Output, the Builder should not be used further if pooled, as it may be recycled.
|
||||
// If pooling is disabled, it behaves like String without release.
|
||||
func (b *Builder) Output() string {
|
||||
out := b.buf.String()
|
||||
b.Release() // Release takes care of the poolEnabled check
|
||||
return out
|
||||
}
|
||||
|
||||
// Release returns the Builder to the pool if pooling is enabled.
|
||||
// You should call this exactly once per New() when Pool(true) is active.
|
||||
// Resets the internal state (buffer, separator, needsSep) before pooling to avoid
|
||||
// retaining data or large allocations.
|
||||
// If pooling is disabled, this is a no-op.
|
||||
// Safe to call multiple times, but typically called once at the end of use.
|
||||
func (b *Builder) Release() {
|
||||
if poolEnabled.Load() {
|
||||
// Avoid retaining large buffers.
|
||||
b.buf.Reset()
|
||||
b.sep = empty
|
||||
b.needsSep = false
|
||||
builderPool.Put(b)
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Package cat provides efficient and flexible string concatenation utilities.
|
||||
// It includes optimized functions for concatenating various types, builders for fluent chaining,
|
||||
// and configuration options for defaults, pooling, and unsafe optimizations.
|
||||
// The package aims to minimize allocations and improve performance in string building scenarios.
|
||||
package cat
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Constants used throughout the package for separators, defaults, and configuration.
|
||||
// These include common string literals for separators, empty strings, and special representations,
|
||||
// as well as limits like recursion depth. Defining them as constants allows for compile-time
|
||||
// optimizations, readability, and consistent usage in functions like Space, Path, CSV, and reflection handlers.
|
||||
// cat.go (updated constants section)
|
||||
const (
|
||||
empty = "" // Empty string constant, used for checks and defaults.
|
||||
space = " " // Single space, default separator.
|
||||
slash = "/" // Forward slash, for paths.
|
||||
dot = "." // Period, for extensions or decimals.
|
||||
comma = "," // Comma, for CSV or lists.
|
||||
equal = "=" // Equals, for comparisons.
|
||||
newline = "\n" // Newline, for multi-line strings.
|
||||
|
||||
// SQL-specific constants
|
||||
and = "AND" // AND operator, for SQL conditions.
|
||||
inOpen = " IN (" // Opening for SQL IN clause
|
||||
inClose = ")" // Closing for SQL IN clause
|
||||
asSQL = " AS " // SQL AS for aliasing
|
||||
count = "COUNT(" // SQL COUNT function prefix
|
||||
sum = "SUM(" // SQL SUM function prefix
|
||||
avg = "AVG(" // SQL AVG function prefix
|
||||
maxOpen = "MAX(" // SQL MAX function prefix
|
||||
minOpen = "MIN(" // SQL MIN function prefix
|
||||
caseSQL = "CASE " // SQL CASE keyword
|
||||
when = "WHEN " // SQL WHEN clause
|
||||
then = " THEN " // SQL THEN clause
|
||||
elseSQL = " ELSE " // SQL ELSE clause
|
||||
end = " END" // SQL END for CASE
|
||||
countAll = "COUNT(*)" // SQL COUNT(*) for all rows
|
||||
parenOpen = "(" // Opening parenthesis
|
||||
parenClose = ")" // Closing parenthesis
|
||||
|
||||
maxRecursionDepth = 32 // Maximum recursion depth for nested structure handling.
|
||||
nilString = "<nil>" // String representation for nil values.
|
||||
unexportedString = "<?>" // Placeholder for unexported fields.
|
||||
)
|
||||
|
||||
// Numeric is a generic constraint interface for numeric types.
|
||||
// It includes all signed/unsigned integers and floats.
|
||||
// Used in generic functions like Number and NumberWith to constrain to numbers.
|
||||
type Numeric interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64
|
||||
}
|
||||
|
||||
// poolEnabled controls whether New() reuses Builder instances from a pool.
|
||||
// Atomic.Bool for thread-safe toggle.
|
||||
// When true, Builders from New must be Released to avoid leaks.
|
||||
var poolEnabled atomic.Bool
|
||||
|
||||
// builderPool stores reusable *Builder to reduce GC pressure on hot paths.
|
||||
// Uses sync.Pool for efficient allocation/reuse.
|
||||
// New func creates a fresh &Builder when pool is empty.
|
||||
var builderPool = sync.Pool{
|
||||
New: func() any { return &Builder{} },
|
||||
}
|
||||
|
||||
// Pool enables or disables Builder pooling for New()/Release().
|
||||
// When enabled, you MUST call b.Release() after b.String() to return it.
|
||||
// Thread-safe via atomic.Store.
|
||||
// Enable for high-throughput scenarios to reduce allocations.
|
||||
func Pool(enable bool) { poolEnabled.Store(enable) }
|
||||
|
||||
// unsafeBytesFlag controls zero-copy []byte -> string behavior via atomics.
|
||||
// Int32 used for atomic operations: 1 = enabled, 0 = disabled.
|
||||
// Affects bytesToString function for zero-copy conversions using unsafe.
|
||||
var unsafeBytesFlag atomic.Int32 // 1 = true, 0 = false
|
||||
|
||||
// SetUnsafeBytes toggles zero-copy []byte -> string conversions globally.
|
||||
// When enabled, bytesToString uses unsafe.String for zero-allocation conversion.
|
||||
// Thread-safe via atomic.Store.
|
||||
// Use with caution: assumes the byte slice is not modified after conversion.
|
||||
// Compatible with Go 1.20+; fallback to string(bts) if disabled.
|
||||
func SetUnsafeBytes(enable bool) {
|
||||
if enable {
|
||||
unsafeBytesFlag.Store(1)
|
||||
} else {
|
||||
unsafeBytesFlag.Store(0)
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnsafeBytes reports whether zero-copy []byte -> string is enabled.
|
||||
// Thread-safe via atomic.Load.
|
||||
// Returns true if flag is 1, false otherwise.
|
||||
// Useful for checking current configuration.
|
||||
func IsUnsafeBytes() bool { return unsafeBytesFlag.Load() == 1 }
|
||||
|
||||
// deterministicMaps controls whether map keys are sorted for deterministic output in string conversions.
|
||||
// It uses atomic.Bool for thread-safe access.
|
||||
var deterministicMaps atomic.Bool
|
||||
|
||||
// SetDeterministicMaps controls whether map keys are sorted for deterministic output
|
||||
// in reflection-based handling (e.g., in writeReflect for maps).
|
||||
// When enabled, keys are sorted using a string-based comparison for consistent string representations.
|
||||
// Thread-safe via atomic.Store.
|
||||
// Useful for reproducible outputs in testing or logging.
|
||||
func SetDeterministicMaps(enable bool) {
|
||||
deterministicMaps.Store(enable)
|
||||
}
|
||||
|
||||
// IsDeterministicMaps returns current map sorting setting.
|
||||
// Thread-safe via atomic.Load.
|
||||
// Returns true if deterministic sorting is enabled, false otherwise.
|
||||
func IsDeterministicMaps() bool {
|
||||
return deterministicMaps.Load()
|
||||
}
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
package cat
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Append appends args to dst and returns the grown slice.
|
||||
// Callers can reuse dst across calls to amortize allocs.
|
||||
// It uses an internal Builder for efficient concatenation of the args (no separators),
|
||||
// then appends the result to the dst byte slice.
|
||||
// Preallocates based on a size estimate to minimize reallocations.
|
||||
// Benefits from Builder pooling if enabled.
|
||||
// Useful for building byte slices incrementally without separators.
|
||||
func Append(dst []byte, args ...any) []byte {
|
||||
return AppendWith(empty, dst, args...)
|
||||
}
|
||||
|
||||
// AppendWith appends args to dst and returns the grown slice.
|
||||
// Callers can reuse dst across calls to amortize allocs.
|
||||
// Similar to Append, but inserts the specified sep between each arg.
|
||||
// Preallocates based on a size estimate including separators.
|
||||
// Benefits from Builder pooling if enabled.
|
||||
// Useful for building byte slices incrementally with custom separators.
|
||||
func AppendWith(sep string, dst []byte, args ...any) []byte {
|
||||
if len(args) == 0 {
|
||||
return dst
|
||||
}
|
||||
b := New(sep)
|
||||
b.Grow(estimateWith(sep, args))
|
||||
b.Add(args...)
|
||||
out := b.Output()
|
||||
return append(dst, out...)
|
||||
}
|
||||
|
||||
// AppendBytes joins byte slices without separators.
|
||||
// Only for compatibility with low-level byte processing.
|
||||
// Directly appends each []byte arg to dst without any conversion or separators.
|
||||
// Efficient for pure byte concatenation; no allocations if dst has capacity.
|
||||
// Returns the extended dst slice.
|
||||
// Does not use Builder, as it's simple append operations.
|
||||
func AppendBytes(dst []byte, args ...[]byte) []byte {
|
||||
if len(args) == 0 {
|
||||
return dst
|
||||
}
|
||||
for _, b := range args {
|
||||
dst = append(dst, b...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendTo writes arguments to an existing strings.Builder.
|
||||
// More efficient than creating new builders.
|
||||
// Appends each arg to the provided strings.Builder using the optimized write function.
|
||||
// No separators are added; for direct concatenation.
|
||||
// Useful when you already have a strings.Builder and want to add more values efficiently.
|
||||
// Does not use cat.Builder, as it appends to an existing strings.Builder.
|
||||
func AppendTo(b *strings.Builder, args ...any) {
|
||||
for _, arg := range args {
|
||||
write(b, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// AppendStrings writes strings to an existing strings.Builder.
|
||||
// Directly writes each string arg to the provided strings.Builder.
|
||||
// No type checks or conversions; assumes all args are strings.
|
||||
// Efficient for appending known strings without separators.
|
||||
// Does not use cat.Builder, as it appends to an existing strings.Builder.
|
||||
func AppendStrings(b *strings.Builder, ss ...string) {
|
||||
for _, s := range ss {
|
||||
b.WriteString(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Between concatenates values wrapped between x and y (no separator between args).
|
||||
// Equivalent to BetweenWith with an empty separator.
|
||||
func Between(x, y any, args ...any) string {
|
||||
return BetweenWith(empty, x, y, args...)
|
||||
}
|
||||
|
||||
// BetweenWith concatenates values wrapped between x and y, using sep between x, args, and y.
|
||||
// Uses a pooled Builder if enabled; releases it after use.
|
||||
// Equivalent to With(sep, x, args..., y).
|
||||
func BetweenWith(sep string, x, y any, args ...any) string {
|
||||
b := New(sep)
|
||||
// Estimate size for all parts to avoid re-allocation.
|
||||
b.Grow(estimate([]any{x, y}) + estimateWith(sep, args))
|
||||
|
||||
b.Add(x)
|
||||
b.Add(args...)
|
||||
b.Add(y)
|
||||
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// CSV joins arguments with "," separators (no space).
|
||||
// Convenience wrapper for With using a comma as separator.
|
||||
// Useful for simple CSV string generation without spaces.
|
||||
func CSV(args ...any) string { return With(comma, args...) }
|
||||
|
||||
// Comma joins arguments with ", " separators.
|
||||
// Convenience wrapper for With using ", " as separator.
|
||||
// Useful for human-readable lists with comma and space.
|
||||
func Comma(args ...any) string { return With(comma+space, args...) }
|
||||
|
||||
// Concat concatenates any values (no separators).
|
||||
// Usage: cat.Concat("a", 1, true) → "a1true"
|
||||
// Equivalent to With with an empty separator.
|
||||
func Concat(args ...any) string {
|
||||
return With(empty, args...)
|
||||
}
|
||||
|
||||
// ConcatWith concatenates any values with separator.
|
||||
// Alias for With; joins args with the provided sep.
|
||||
func ConcatWith(sep string, args ...any) string {
|
||||
return With(sep, args...)
|
||||
}
|
||||
|
||||
// Flatten joins nested values into a single concatenation using empty.
|
||||
// Convenience for FlattenWith using empty.
|
||||
func Flatten(args ...any) string {
|
||||
return FlattenWith(empty, args...)
|
||||
}
|
||||
|
||||
// FlattenWith joins nested values into a single concatenation with sep, avoiding
|
||||
// intermediate slice allocations where possible.
|
||||
// It recursively flattens any nested []any arguments, concatenating all leaf items
|
||||
// with sep between them. Skips empty nested slices to avoid extra separators.
|
||||
// Leaf items (non-slices) are converted using the optimized write function.
|
||||
// Uses a pooled Builder if enabled; releases it after use.
|
||||
// Preallocates based on a recursive estimate for efficiency.
|
||||
// Example: FlattenWith(",", 1, []any{2, []any{3,4}}, 5) → "1,2,3,4,5"
|
||||
func FlattenWith(sep string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return empty
|
||||
}
|
||||
|
||||
// Recursive estimate for preallocation.
|
||||
totalSize := recursiveEstimate(sep, args)
|
||||
|
||||
b := New(sep)
|
||||
b.Grow(totalSize)
|
||||
recursiveAdd(b, args)
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Group joins multiple groups with empty between groups (no intra-group separators).
|
||||
// Convenience for GroupWith using empty.
|
||||
func Group(groups ...[]any) string {
|
||||
return GroupWith(empty, groups...)
|
||||
}
|
||||
|
||||
// GroupWith joins multiple groups with a separator between groups (no intra-group separators).
|
||||
// Concatenates each group internally without separators, then joins non-empty groups with sep.
|
||||
// Preestimates total size for allocation; uses pooled Builder if enabled.
|
||||
// Optimized for single group: direct Concat.
|
||||
// Useful for grouping related items with inter-group separation.
|
||||
func GroupWith(sep string, groups ...[]any) string {
|
||||
if len(groups) == 0 {
|
||||
return empty
|
||||
}
|
||||
if len(groups) == 1 {
|
||||
return Concat(groups[0]...)
|
||||
}
|
||||
|
||||
total := 0
|
||||
nonEmpty := 0
|
||||
for _, g := range groups {
|
||||
if len(g) == 0 {
|
||||
continue
|
||||
}
|
||||
if nonEmpty > 0 {
|
||||
total += len(sep)
|
||||
}
|
||||
total += estimate(g)
|
||||
nonEmpty++
|
||||
}
|
||||
|
||||
b := New(empty)
|
||||
b.Grow(total)
|
||||
first := true
|
||||
for _, g := range groups {
|
||||
if len(g) == 0 {
|
||||
continue
|
||||
}
|
||||
if !first && sep != empty {
|
||||
b.buf.WriteString(sep)
|
||||
}
|
||||
first = false
|
||||
for _, a := range g {
|
||||
write(&b.buf, a)
|
||||
}
|
||||
}
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Indent prefixes the concatenation of args with depth levels of two spaces per level.
|
||||
// Example: Indent(2, "hello") => " hello"
|
||||
// If depth <= 0, equivalent to Concat(args...).
|
||||
// Uses " " repeated depth times as prefix, followed by concatenated args (no separators).
|
||||
// Benefits from pooling via Concat.
|
||||
func Indent(depth int, args ...any) string {
|
||||
if depth <= 0 {
|
||||
return Concat(args...)
|
||||
}
|
||||
prefix := strings.Repeat(" ", depth)
|
||||
return Prefix(prefix, args...)
|
||||
}
|
||||
|
||||
// Join joins strings (matches stdlib strings.Join behavior).
|
||||
// Usage: cat.Join("a", "b") → "a b" (using empty)
|
||||
// Joins the variadic string args with the current empty.
|
||||
// Useful for compatibility with stdlib but using package default sep.
|
||||
func Join(elems ...string) string {
|
||||
return strings.Join(elems, empty)
|
||||
}
|
||||
|
||||
// JoinWith joins strings with separator (variadic version).
|
||||
// Directly uses strings.Join on the variadic string args with sep.
|
||||
// Efficient for known strings; no conversions needed.
|
||||
func JoinWith(sep string, elems ...string) string {
|
||||
return strings.Join(elems, sep)
|
||||
}
|
||||
|
||||
// Lines joins arguments with newline separators.
|
||||
// Convenience for With using "\n" as separator.
|
||||
// Useful for building multi-line strings.
|
||||
func Lines(args ...any) string { return With(newline, args...) }
|
||||
|
||||
// Number concatenates numeric values without separators.
|
||||
// Generic over Numeric types.
|
||||
// Equivalent to NumberWith with empty sep.
|
||||
func Number[T Numeric](a ...T) string {
|
||||
return NumberWith(empty, a...)
|
||||
}
|
||||
|
||||
// NumberWith concatenates numeric values with the provided separator.
|
||||
// Generic over Numeric types.
|
||||
// If no args, returns empty string.
|
||||
// Uses pooled Builder if enabled, with rough growth estimate (8 bytes per item).
|
||||
// Relies on valueToString for numeric conversion.
|
||||
func NumberWith[T Numeric](sep string, a ...T) string {
|
||||
if len(a) == 0 {
|
||||
return empty
|
||||
}
|
||||
|
||||
b := New(sep)
|
||||
b.Grow(len(a) * 8)
|
||||
for _, v := range a {
|
||||
b.Add(v)
|
||||
}
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Path joins arguments with "/" separators.
|
||||
// Convenience for With using "/" as separator.
|
||||
// Useful for building file paths or URLs.
|
||||
func Path(args ...any) string { return With(slash, args...) }
|
||||
|
||||
// Prefix concatenates with a prefix (no separator).
|
||||
// Equivalent to PrefixWith with empty sep.
|
||||
func Prefix(p any, args ...any) string {
|
||||
return PrefixWith(empty, p, args...)
|
||||
}
|
||||
|
||||
// PrefixWith concatenates with a prefix and separator.
|
||||
// Adds p, then sep (if args present and sep not empty), then joins args with sep.
|
||||
// Uses pooled Builder if enabled.
|
||||
func PrefixWith(sep string, p any, args ...any) string {
|
||||
b := New(sep)
|
||||
b.Grow(estimateWith(sep, args) + estimate([]any{p}))
|
||||
b.Add(p)
|
||||
b.Add(args...)
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// PrefixEach applies the same prefix to each argument and joins the pairs with sep.
|
||||
// Example: PrefixEach("pre-", ",", "a","b") => "pre-a,pre-b"
|
||||
// Preestimates size including prefixes and seps.
|
||||
// Uses pooled Builder if enabled; manually adds sep between pairs, no sep between p and a.
|
||||
// Returns empty if no args.
|
||||
func PrefixEach(p any, sep string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return empty
|
||||
}
|
||||
pSize := estimate([]any{p})
|
||||
total := len(sep)*(len(args)-1) + estimate(args) + pSize*len(args)
|
||||
|
||||
b := New(empty)
|
||||
b.Grow(total)
|
||||
for i, a := range args {
|
||||
if i > 0 && sep != empty {
|
||||
b.buf.WriteString(sep)
|
||||
}
|
||||
write(&b.buf, p)
|
||||
write(&b.buf, a)
|
||||
}
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Pair joins exactly two values (no separator).
|
||||
// Equivalent to PairWith with empty sep.
|
||||
func Pair(a, b any) string {
|
||||
return PairWith(empty, a, b)
|
||||
}
|
||||
|
||||
// PairWith joins exactly two values with a separator.
|
||||
// Optimized for two args: uses With(sep, a, b).
|
||||
func PairWith(sep string, a, b any) string {
|
||||
return With(sep, a, b)
|
||||
}
|
||||
|
||||
// Quote wraps each argument in double quotes, separated by spaces.
|
||||
// Equivalent to QuoteWith with '"' as quote.
|
||||
func Quote(args ...any) string {
|
||||
return QuoteWith('"', args...)
|
||||
}
|
||||
|
||||
// QuoteWith wraps each argument with the specified quote byte, separated by spaces.
|
||||
// Wraps each arg with quote, writes arg, closes with quote; joins with space.
|
||||
// Preestimates with quotes and spaces.
|
||||
// Uses pooled Builder if enabled.
|
||||
func QuoteWith(quote byte, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return empty
|
||||
}
|
||||
total := estimate(args) + 2*len(args) + len(space)*(len(args)-1)
|
||||
|
||||
b := New(empty)
|
||||
b.Grow(total)
|
||||
need := false
|
||||
for _, a := range args {
|
||||
if need {
|
||||
b.buf.WriteString(space)
|
||||
}
|
||||
b.buf.WriteByte(quote)
|
||||
write(&b.buf, a)
|
||||
b.buf.WriteByte(quote)
|
||||
need = true
|
||||
}
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Repeat concatenates val n times (no sep between instances).
|
||||
// Equivalent to RepeatWith with empty sep.
|
||||
func Repeat(val any, n int) string {
|
||||
return RepeatWith(empty, val, n)
|
||||
}
|
||||
|
||||
// RepeatWith concatenates val n times with sep between each instance.
|
||||
// If n <= 0, returns an empty string.
|
||||
// Optimized to make exactly one allocation; converts val once.
|
||||
// Uses pooled Builder if enabled.
|
||||
func RepeatWith(sep string, val any, n int) string {
|
||||
if n <= 0 {
|
||||
return empty
|
||||
}
|
||||
if n == 1 {
|
||||
return valueToString(val)
|
||||
}
|
||||
b := New(sep)
|
||||
b.Grow(n*estimate([]any{val}) + (n-1)*len(sep))
|
||||
for i := 0; i < n; i++ {
|
||||
b.Add(val)
|
||||
}
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Reflect converts a reflect.Value to its string representation.
|
||||
// It handles all kinds of reflected values including primitives, structs, slices, maps, etc.
|
||||
// For nil values, it returns the nilString constant ("<nil>").
|
||||
// For unexported or inaccessible fields, it returns unexportedString ("<?>").
|
||||
// The output follows Go's syntax conventions where applicable (e.g., slices as [a, b], maps as {k:v}).
|
||||
func Reflect(r reflect.Value) string {
|
||||
if !r.IsValid() {
|
||||
return nilString
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
writeReflect(&b, r.Interface(), 0)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Space concatenates arguments with space separators.
|
||||
// Convenience for With using " " as separator.
|
||||
func Space(args ...any) string { return With(space, args...) }
|
||||
|
||||
// Dot concatenates arguments with dot separators.
|
||||
// Convenience for With using " " as separator.
|
||||
func Dot(args ...any) string { return With(dot, args...) }
|
||||
|
||||
// Suffix concatenates with a suffix (no separator).
|
||||
// Equivalent to SuffixWith with empty sep.
|
||||
func Suffix(s any, args ...any) string {
|
||||
return SuffixWith(empty, s, args...)
|
||||
}
|
||||
|
||||
// SuffixWith concatenates with a suffix and separator.
|
||||
// Joins args with sep, then adds sep (if args present and sep not empty), then s.
|
||||
// Uses pooled Builder if enabled.
|
||||
func SuffixWith(sep string, s any, args ...any) string {
|
||||
b := New(sep)
|
||||
b.Grow(estimateWith(sep, args) + estimate([]any{s}))
|
||||
b.Add(args...)
|
||||
b.Add(s)
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// SuffixEach applies the same suffix to each argument and joins the pairs with sep.
|
||||
// Example: SuffixEach("-suf", " | ", "a","b") => "a-suf | b-suf"
|
||||
// Preestimates size including suffixes and seps.
|
||||
// Uses pooled Builder if enabled; manually adds sep between pairs, no sep between a and s.
|
||||
// Returns empty if no args.
|
||||
func SuffixEach(s any, sep string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return empty
|
||||
}
|
||||
sSize := estimate([]any{s})
|
||||
total := len(sep)*(len(args)-1) + estimate(args) + sSize*len(args)
|
||||
|
||||
b := New(empty)
|
||||
b.Grow(total)
|
||||
for i, a := range args {
|
||||
if i > 0 && sep != empty {
|
||||
b.buf.WriteString(sep)
|
||||
}
|
||||
write(&b.buf, a)
|
||||
write(&b.buf, s)
|
||||
}
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Sprint concatenates any values (no separators).
|
||||
// Usage: Sprint("a", 1, true) → "a1true"
|
||||
// Equivalent to Concat or With with an empty separator.
|
||||
func Sprint(args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return empty
|
||||
}
|
||||
if len(args) == 1 {
|
||||
return valueToString(args[0])
|
||||
}
|
||||
|
||||
// For multiple args, use the existing Concat functionality
|
||||
return Concat(args...)
|
||||
}
|
||||
|
||||
// Trio joins exactly three values (no separator).
|
||||
// Equivalent to TrioWith with empty sep
|
||||
func Trio(a, b, c any) string {
|
||||
return TrioWith(empty, a, b, c)
|
||||
}
|
||||
|
||||
// TrioWith joins exactly three values with a separator.
|
||||
// Optimized for three args: uses With(sep, a, b, c).
|
||||
func TrioWith(sep string, a, b, c any) string {
|
||||
return With(sep, a, b, c)
|
||||
}
|
||||
|
||||
// With concatenates arguments with the specified separator.
|
||||
// Core concatenation function with sep.
|
||||
// Optimized for zero or one arg: empty or direct valueToString.
|
||||
// Fast path for all strings: exact preallocation, direct writes via raw strings.Builder (minimal branches/allocs).
|
||||
// Fallback: pooled Builder with estimateWith, adds args with sep.
|
||||
// Benefits from pooling if enabled for mixed types.
|
||||
func With(sep string, args ...any) string {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return empty
|
||||
case 1:
|
||||
return valueToString(args[0])
|
||||
}
|
||||
|
||||
// Fast path for all strings: use raw strings.Builder for speed, no pooling needed.
|
||||
allStrings := true
|
||||
totalLen := len(sep) * (len(args) - 1)
|
||||
for _, a := range args {
|
||||
if s, ok := a.(string); ok {
|
||||
totalLen += len(s)
|
||||
} else {
|
||||
allStrings = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allStrings {
|
||||
var b strings.Builder
|
||||
b.Grow(totalLen)
|
||||
b.WriteString(args[0].(string))
|
||||
for i := 1; i < len(args); i++ {
|
||||
if sep != empty {
|
||||
b.WriteString(sep)
|
||||
}
|
||||
b.WriteString(args[i].(string))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Fallback for mixed types: use pooled Builder.
|
||||
b := New(sep)
|
||||
b.Grow(estimateWith(sep, args))
|
||||
b.Add(args...)
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Wrap encloses concatenated args between before and after strings (no inner separator).
|
||||
// Equivalent to Concat(before, args..., after).
|
||||
func Wrap(before, after string, args ...any) string {
|
||||
b := Start()
|
||||
b.Grow(len(before) + len(after) + estimate(args))
|
||||
|
||||
b.Add(before)
|
||||
b.Add(args...)
|
||||
b.Add(after)
|
||||
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// WrapEach wraps each argument individually with before/after, concatenated without separators.
|
||||
// Applies before + arg + after to each arg.
|
||||
// Preestimates size; uses pooled Builder if enabled.
|
||||
// Returns empty if no args.
|
||||
// Useful for wrapping multiple items identically without joins.
|
||||
func WrapEach(before, after string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return empty
|
||||
}
|
||||
total := (len(before)+len(after))*len(args) + estimate(args)
|
||||
|
||||
b := Start() // Use pooled builder, but we will write manually.
|
||||
b.Grow(total)
|
||||
for _, a := range args {
|
||||
write(&b.buf, before)
|
||||
write(&b.buf, a)
|
||||
write(&b.buf, after)
|
||||
}
|
||||
// No separators were ever added, so this is safe.
|
||||
b.needsSep = true // Correctly set state in case of reuse.
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// WrapWith encloses concatenated args between before and after strings,
|
||||
// joining the arguments with the provided separator.
|
||||
// If no args, returns before + after.
|
||||
// Builds inner with With(sep, args...), then Concat(before, inner, after).
|
||||
// Benefits from pooling via With and Concat.
|
||||
func WrapWith(sep, before, after string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return before + after
|
||||
}
|
||||
// First, efficiently build the inner part.
|
||||
inner := With(sep, args...)
|
||||
|
||||
// Then, wrap it without allocating another slice.
|
||||
b := Start()
|
||||
b.Grow(len(before) + len(inner) + len(after))
|
||||
|
||||
b.Add(before)
|
||||
b.Add(inner)
|
||||
b.Add(after)
|
||||
|
||||
return b.Output()
|
||||
}
|
||||
|
||||
// Pad surrounds a string with spaces on both sides.
|
||||
// Ensures proper spacing for SQL operators like "=", "AND", etc.
|
||||
// Example: Pad("=") returns " = " for cleaner formatting.
|
||||
func Pad(s string) string {
|
||||
return Concat(space, s, space)
|
||||
}
|
||||
|
||||
// PadWith adds a separator before the string and a space after it.
|
||||
// Useful for formatting SQL parts with custom leading separators.
|
||||
// Example: PadWith(",", "column") returns ",column ".
|
||||
func PadWith(sep, s string) string {
|
||||
return Concat(sep, s, space)
|
||||
}
|
||||
|
||||
// Parens wraps content in parentheses
|
||||
// Useful for grouping SQL conditions or expressions
|
||||
// Example: Parens("a = b AND c = d") → "(a = b AND c = d)"
|
||||
func Parens(content string) string {
|
||||
return Concat(parenOpen, content, parenClose)
|
||||
}
|
||||
|
||||
// ParensWith wraps multiple arguments in parentheses with a separator
|
||||
// Example: ParensWith(" AND ", "a = b", "c = d") → "(a = b AND c = d)"
|
||||
func ParensWith(sep string, args ...any) string {
|
||||
return Concat(parenOpen, With(sep, args...), parenClose)
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
package cat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// write writes a value to the given strings.Builder using fast paths to avoid temporary allocations.
|
||||
// It handles common types like strings, byte slices, integers, floats, and booleans directly for efficiency.
|
||||
// For other types, it falls back to fmt.Fprint, which may involve allocations.
|
||||
// This function is optimized for performance in string concatenation scenarios, prioritizing
|
||||
// common cases like strings and numbers at the top of the type switch for compiler optimization.
|
||||
// Note: For integers and floats, it uses stack-allocated buffers and strconv.Append* functions to
|
||||
// convert numbers to strings without heap allocations.
|
||||
func write(b *strings.Builder, arg any) {
|
||||
writeValue(b, arg, 0)
|
||||
}
|
||||
|
||||
// writeValue appends the string representation of arg to b, handling recursion with a depth limit.
|
||||
// It serves as a recursive helper for write, directly handling primitives and delegating complex
|
||||
// types to writeReflect. The depth parameter prevents excessive recursion in deeply nested structures.
|
||||
func writeValue(b *strings.Builder, arg any, depth int) {
|
||||
// Handle recursion depth limit
|
||||
if depth > maxRecursionDepth {
|
||||
b.WriteString("...")
|
||||
return
|
||||
}
|
||||
|
||||
// Handle nil values
|
||||
if arg == nil {
|
||||
b.WriteString(nilString)
|
||||
return
|
||||
}
|
||||
|
||||
// Fast path type switch for all primitive types
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
b.WriteString(v)
|
||||
case []byte:
|
||||
b.WriteString(bytesToString(v))
|
||||
case int:
|
||||
var buf [20]byte
|
||||
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
|
||||
case int64:
|
||||
var buf [20]byte
|
||||
b.Write(strconv.AppendInt(buf[:0], v, 10))
|
||||
case int32:
|
||||
var buf [11]byte
|
||||
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
|
||||
case int16:
|
||||
var buf [6]byte
|
||||
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
|
||||
case int8:
|
||||
var buf [4]byte
|
||||
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
|
||||
case uint:
|
||||
var buf [20]byte
|
||||
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
|
||||
case uint64:
|
||||
var buf [20]byte
|
||||
b.Write(strconv.AppendUint(buf[:0], v, 10))
|
||||
case uint32:
|
||||
var buf [10]byte
|
||||
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
|
||||
case uint16:
|
||||
var buf [5]byte
|
||||
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
|
||||
case uint8:
|
||||
var buf [3]byte
|
||||
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
|
||||
case float64:
|
||||
var buf [24]byte
|
||||
b.Write(strconv.AppendFloat(buf[:0], v, 'f', -1, 64))
|
||||
case float32:
|
||||
var buf [24]byte
|
||||
b.Write(strconv.AppendFloat(buf[:0], float64(v), 'f', -1, 32))
|
||||
case bool:
|
||||
if v {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
case fmt.Stringer:
|
||||
b.WriteString(v.String())
|
||||
case error:
|
||||
b.WriteString(v.Error())
|
||||
default:
|
||||
// Fallback to reflection-based handling
|
||||
writeReflect(b, arg, depth)
|
||||
}
|
||||
}
|
||||
|
||||
// writeReflect handles all complex types safely.
|
||||
func writeReflect(b *strings.Builder, arg any, depth int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
b.WriteString("[!reflect panic!]")
|
||||
}
|
||||
}()
|
||||
|
||||
val := reflect.ValueOf(arg)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
b.WriteString(nilString)
|
||||
return
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
b.WriteByte('[')
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
if i > 0 {
|
||||
b.WriteString(", ") // Use comma-space for readability
|
||||
}
|
||||
writeValue(b, val.Index(i).Interface(), depth+1)
|
||||
}
|
||||
b.WriteByte(']')
|
||||
|
||||
case reflect.Struct:
|
||||
typ := val.Type()
|
||||
b.WriteByte('{') // Use {} for structs to follow Go convention
|
||||
first := true
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
fieldValue := val.Field(i)
|
||||
if !fieldValue.CanInterface() {
|
||||
continue // Skip unexported fields
|
||||
}
|
||||
if !first {
|
||||
b.WriteByte(' ') // Use space as separator
|
||||
}
|
||||
first = false
|
||||
b.WriteString(typ.Field(i).Name)
|
||||
b.WriteByte(':')
|
||||
|
||||
writeValue(b, fieldValue.Interface(), depth+1)
|
||||
}
|
||||
b.WriteByte('}')
|
||||
|
||||
case reflect.Map:
|
||||
b.WriteByte('{')
|
||||
keys := val.MapKeys()
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
// A simple string-based sort for keys
|
||||
return fmt.Sprint(keys[i].Interface()) < fmt.Sprint(keys[j].Interface())
|
||||
})
|
||||
for i, key := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte(' ') // Use space as separator
|
||||
}
|
||||
writeValue(b, key.Interface(), depth+1)
|
||||
b.WriteByte(':')
|
||||
writeValue(b, val.MapIndex(key).Interface(), depth+1)
|
||||
}
|
||||
b.WriteByte('}')
|
||||
|
||||
case reflect.Interface:
|
||||
if val.IsNil() {
|
||||
b.WriteString(nilString)
|
||||
return
|
||||
}
|
||||
writeValue(b, val.Elem().Interface(), depth+1)
|
||||
|
||||
default:
|
||||
fmt.Fprint(b, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// valueToString converts any value to a string representation.
|
||||
// It uses optimized paths for common types to avoid unnecessary allocations.
|
||||
// For types like integers and floats, it directly uses strconv functions.
|
||||
// This function is useful for single-argument conversions or as a helper in other parts of the package.
|
||||
// Unlike write, it returns a string instead of appending to a builder.
|
||||
func valueToString(arg any) string {
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return bytesToString(v)
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
case uint64:
|
||||
return strconv.FormatUint(v, 10)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
case error:
|
||||
return v.Error()
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
|
||||
// estimateWith calculates a conservative estimate of the total string length when concatenating
|
||||
// the given arguments with a separator. This is used for preallocating capacity in strings.Builder
|
||||
// to minimize reallocations during building.
|
||||
// It accounts for the length of separators and estimates the length of each argument based on its type.
|
||||
// If no arguments are provided, it returns 0.
|
||||
func estimateWith(sep string, args []any) int {
|
||||
if len(args) == 0 {
|
||||
return 0
|
||||
}
|
||||
size := len(sep) * (len(args) - 1)
|
||||
size += estimate(args)
|
||||
return size
|
||||
}
|
||||
|
||||
// estimate calculates a conservative estimate of the combined string length of the given arguments.
|
||||
// It iterates over each argument and adds an estimated length based on its type:
|
||||
// - Strings and byte slices: exact length.
|
||||
// - Numbers: calculated digit count using numLen or uNumLen.
|
||||
// - Floats and others: fixed conservative estimates (e.g., 16 or 24 bytes).
|
||||
// This helper is used internally by estimateWith and focuses solely on the arguments without separators.
|
||||
func estimate(args []any) int {
|
||||
var size int
|
||||
for _, a := range args {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
size += len(v)
|
||||
case []byte:
|
||||
size += len(v)
|
||||
case int:
|
||||
size += numLen(int64(v))
|
||||
case int8:
|
||||
size += numLen(int64(v))
|
||||
case int16:
|
||||
size += numLen(int64(v))
|
||||
case int32:
|
||||
size += numLen(int64(v))
|
||||
case int64:
|
||||
size += numLen(v)
|
||||
case uint:
|
||||
size += uNumLen(uint64(v))
|
||||
case uint8:
|
||||
size += uNumLen(uint64(v))
|
||||
case uint16:
|
||||
size += uNumLen(uint64(v))
|
||||
case uint32:
|
||||
size += uNumLen(uint64(v))
|
||||
case uint64:
|
||||
size += uNumLen(v)
|
||||
case float32:
|
||||
size += 16
|
||||
case float64:
|
||||
size += 24
|
||||
case bool:
|
||||
size += 5 // "false"
|
||||
case fmt.Stringer, error:
|
||||
size += 16 // conservative
|
||||
default:
|
||||
size += 16 // conservative
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// numLen returns the number of characters required to represent the signed integer n as a string.
|
||||
// It handles negative numbers by adding 1 for the '-' sign and uses a loop to count digits.
|
||||
// Special handling for math.MinInt64 to avoid overflow when negating.
|
||||
// Returns 1 for 0, and up to 20 for the largest values.
|
||||
func numLen(n int64) int {
|
||||
if n == 0 {
|
||||
return 1
|
||||
}
|
||||
c := 0
|
||||
if n < 0 {
|
||||
c = 1 // for '-'
|
||||
// NOTE: math.MinInt64 negated overflows; handle by adding one digit and returning 20.
|
||||
if n == -1<<63 {
|
||||
return 20
|
||||
}
|
||||
n = -n
|
||||
}
|
||||
for n > 0 {
|
||||
n /= 10
|
||||
c++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// uNumLen returns the number of characters required to represent the unsigned integer n as a string.
|
||||
// It uses a loop to count digits.
|
||||
// Returns 1 for 0, and up to 20 for the largest uint64 values.
|
||||
func uNumLen(n uint64) int {
|
||||
if n == 0 {
|
||||
return 1
|
||||
}
|
||||
c := 0
|
||||
for n > 0 {
|
||||
n /= 10
|
||||
c++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// bytesToString converts a byte slice to a string efficiently.
|
||||
// If the package's UnsafeBytes flag is set (via IsUnsafeBytes()), it uses unsafe operations
|
||||
// to create a string backed by the same memory as the byte slice, avoiding a copy.
|
||||
// This is zero-allocation when unsafe is enabled.
|
||||
// Falls back to standard string(bts) conversion otherwise.
|
||||
// For empty slices, it returns a constant empty string.
|
||||
// Compatible with Go 1.20+ unsafe functions like unsafe.String and unsafe.SliceData.
|
||||
func bytesToString(bts []byte) string {
|
||||
if len(bts) == 0 {
|
||||
return empty
|
||||
}
|
||||
if IsUnsafeBytes() {
|
||||
// Go 1.20+: unsafe.String with SliceData (1.20 introduced, 1.22 added SliceData).
|
||||
return unsafe.String(unsafe.SliceData(bts), len(bts))
|
||||
}
|
||||
return string(bts)
|
||||
}
|
||||
|
||||
// recursiveEstimate calculates the estimated string length for potentially nested arguments,
|
||||
// including the lengths of separators between elements. It recurses on nested []any slices,
|
||||
// flattening the structure while accounting for separators only between non-empty subparts.
|
||||
// This function is useful for preallocating capacity in builders for nested concatenation operations.
|
||||
func recursiveEstimate(sep string, args []any) int {
|
||||
if len(args) == 0 {
|
||||
return 0
|
||||
}
|
||||
size := 0
|
||||
needsSep := false
|
||||
for _, a := range args {
|
||||
switch v := a.(type) {
|
||||
case []any:
|
||||
subSize := recursiveEstimate(sep, v)
|
||||
if subSize > 0 {
|
||||
if needsSep {
|
||||
size += len(sep)
|
||||
}
|
||||
size += subSize
|
||||
needsSep = true
|
||||
}
|
||||
default:
|
||||
if needsSep {
|
||||
size += len(sep)
|
||||
}
|
||||
size += estimate([]any{a})
|
||||
needsSep = true
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// recursiveAdd appends the string representations of potentially nested arguments to the builder.
|
||||
// It recurses on nested []any slices, effectively flattening the structure by adding leaf values
|
||||
// directly via b.Add without inserting separators (separators are handled externally if needed).
|
||||
// This function is designed for efficient concatenation of nested argument lists.
|
||||
func recursiveAdd(b *Builder, args []any) {
|
||||
for _, a := range args {
|
||||
switch v := a.(type) {
|
||||
case []any:
|
||||
recursiveAdd(b, v)
|
||||
default:
|
||||
b.Add(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package cat
|
||||
|
||||
// On builds a SQL ON clause comparing two columns across tables.
|
||||
// Formats as: "table1.column1 = table2.column2" with proper spacing.
|
||||
// Useful in JOIN conditions to match keys between tables.
|
||||
func On(table1, column1, table2, column2 string) string {
|
||||
return With(space,
|
||||
With(dot, table1, column1),
|
||||
Pad(equal),
|
||||
With(dot, table2, column2),
|
||||
)
|
||||
}
|
||||
|
||||
// Using builds a SQL condition comparing two aliased columns.
|
||||
// Formats as: "alias1.column1 = alias2.column2" for JOINs or filters.
|
||||
// Helps when working with table aliases in complex queries.
|
||||
func Using(alias1, column1, alias2, column2 string) string {
|
||||
return With(space,
|
||||
With(dot, alias1, column1),
|
||||
Pad(equal),
|
||||
With(dot, alias2, column2),
|
||||
)
|
||||
}
|
||||
|
||||
// And joins multiple SQL conditions with the AND operator.
|
||||
// Adds spacing to ensure clean SQL output (e.g., "cond1 AND cond2").
|
||||
// Accepts variadic arguments for flexible condition chaining.
|
||||
func And(conditions ...any) string {
|
||||
return With(Pad(and), conditions...)
|
||||
}
|
||||
|
||||
// In creates a SQL IN clause with properly quoted values
|
||||
// Example: In("status", "active", "pending") → "status IN ('active', 'pending')"
|
||||
// Handles value quoting and comma separation automatically
|
||||
func In(column string, values ...string) string {
|
||||
if len(values) == 0 {
|
||||
return Concat(column, inOpen, inClose)
|
||||
}
|
||||
|
||||
quotedValues := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
quotedValues[i] = "'" + v + "'"
|
||||
}
|
||||
return Concat(column, inOpen, JoinWith(comma+space, quotedValues...), inClose)
|
||||
}
|
||||
|
||||
// As creates an aliased SQL expression
|
||||
// Example: As("COUNT(*)", "total_count") → "COUNT(*) AS total_count"
|
||||
func As(expression, alias string) string {
|
||||
return Concat(expression, asSQL, alias)
|
||||
}
|
||||
|
||||
// Count creates a COUNT expression with optional alias
|
||||
// Example: Count("id") → "COUNT(id)"
|
||||
// Example: Count("id", "total") → "COUNT(id) AS total"
|
||||
// Example: Count("DISTINCT user_id", "unique_users") → "COUNT(DISTINCT user_id) AS unique_users"
|
||||
func Count(column string, alias ...string) string {
|
||||
expression := Concat(count, column, parenClose)
|
||||
if len(alias) == 0 {
|
||||
return expression
|
||||
}
|
||||
return As(expression, alias[0])
|
||||
}
|
||||
|
||||
// CountAll creates COUNT(*) with optional alias
|
||||
// Example: CountAll() → "COUNT(*)"
|
||||
// Example: CountAll("total") → "COUNT(*) AS total"
|
||||
func CountAll(alias ...string) string {
|
||||
if len(alias) == 0 {
|
||||
return countAll
|
||||
}
|
||||
return As(countAll, alias[0])
|
||||
}
|
||||
|
||||
// Sum creates a SUM expression with optional alias
|
||||
// Example: Sum("amount") → "SUM(amount)"
|
||||
// Example: Sum("amount", "total") → "SUM(amount) AS total"
|
||||
func Sum(column string, alias ...string) string {
|
||||
expression := Concat(sum, column, parenClose)
|
||||
if len(alias) == 0 {
|
||||
return expression
|
||||
}
|
||||
return As(expression, alias[0])
|
||||
}
|
||||
|
||||
// Avg creates an AVG expression with optional alias
|
||||
// Example: Avg("score") → "AVG(score)"
|
||||
// Example: Avg("score", "average") → "AVG(score) AS average"
|
||||
func Avg(column string, alias ...string) string {
|
||||
expression := Concat(avg, column, parenClose)
|
||||
if len(alias) == 0 {
|
||||
return expression
|
||||
}
|
||||
return As(expression, alias[0])
|
||||
}
|
||||
|
||||
// Max creates a MAX expression with optional alias
|
||||
// Example: Max("price") → "MAX(price)"
|
||||
// Example: Max("price", "max_price") → "MAX(price) AS max_price"
|
||||
func Max(column string, alias ...string) string {
|
||||
expression := Concat(maxOpen, column, parenClose)
|
||||
if len(alias) == 0 {
|
||||
return expression
|
||||
}
|
||||
return As(expression, alias[0])
|
||||
}
|
||||
|
||||
// Min creates a MIN expression with optional alias
|
||||
// Example: Min("price") → "MIN(price)"
|
||||
// Example: Min("price", "min_price") → "MIN(price) AS min_price"
|
||||
func Min(column string, alias ...string) string {
|
||||
expression := Concat(minOpen, column, parenClose)
|
||||
if len(alias) == 0 {
|
||||
return expression
|
||||
}
|
||||
return As(expression, alias[0])
|
||||
}
|
||||
|
||||
// Case creates a SQL CASE expression with optional alias
|
||||
// Example: Case("WHEN status = 'active' THEN 1 ELSE 0 END", "is_active") → "CASE WHEN status = 'active' THEN 1 ELSE 0 END AS is_active"
|
||||
func Case(expression string, alias ...string) string {
|
||||
caseExpr := Concat(caseSQL, expression)
|
||||
if len(alias) == 0 {
|
||||
return caseExpr
|
||||
}
|
||||
return As(caseExpr, alias[0])
|
||||
}
|
||||
|
||||
// CaseWhen creates a complete SQL CASE expression from individual parts with proper value handling
|
||||
// Example: CaseWhen("status =", "'active'", "1", "0", "is_active") → "CASE WHEN status = 'active' THEN 1 ELSE 0 END AS is_active"
|
||||
// Example: CaseWhen("age >", "18", "'adult'", "'minor'", "age_group") → "CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END AS age_group"
|
||||
func CaseWhen(conditionPart string, conditionValue, thenValue, elseValue any, alias ...string) string {
|
||||
condition := Concat(conditionPart, valueToString(conditionValue))
|
||||
expression := Concat(
|
||||
when, condition, then, valueToString(thenValue), elseSQL, valueToString(elseValue), end,
|
||||
)
|
||||
return Case(expression, alias...)
|
||||
}
|
||||
|
||||
// CaseWhenMulti creates a SQL CASE expression with multiple WHEN clauses
|
||||
// Example: CaseWhenMulti([]string{"status =", "age >"}, []any{"'active'", 18}, []any{1, "'adult'"}, 0, "result") → "CASE WHEN status = 'active' THEN 1 WHEN age > 18 THEN 'adult' ELSE 0 END AS result"
|
||||
func CaseWhenMulti(conditionParts []string, conditionValues, thenValues []any, elseValue any, alias ...string) string {
|
||||
if len(conditionParts) != len(conditionValues) || len(conditionParts) != len(thenValues) {
|
||||
return "" // or handle error
|
||||
}
|
||||
|
||||
var whenClauses []string
|
||||
for i := 0; i < len(conditionParts); i++ {
|
||||
condition := Concat(conditionParts[i], valueToString(conditionValues[i]))
|
||||
whenClause := Concat(when, condition, then, valueToString(thenValues[i]))
|
||||
whenClauses = append(whenClauses, whenClause)
|
||||
}
|
||||
|
||||
expression := Concat(
|
||||
JoinWith(space, whenClauses...),
|
||||
elseSQL,
|
||||
valueToString(elseValue),
|
||||
end,
|
||||
)
|
||||
return Case(expression, alias...)
|
||||
}
|
||||
Reference in New Issue
Block a user