"
25 |
26 | // 3. Localization (using LocStr)
27 | // Uses global default language if not specified
28 | Html("", D.User, "").String()
29 | // -> "User" (EN)
30 | // -> "Usuario" (ES)
31 |
32 | // 4. Explicit Language Selection
33 | // Pass language as first argument (lang constant)
34 | Html(ES, "
", D.Hello, "
").String()
35 | // -> "
Hola
"
36 |
37 | // 5. Zero-allocation with pointers (like Translate)
38 | c := Html("", &D.Format, "")
39 | defer c.PutConv() // Manual cleanup if not calling .String()
40 | html := c.String() // Auto-releases to pool
41 |
42 | // 6. Multiline Component (using format specifiers)
43 | Html(`
44 |
%L
45 |
%v
46 |
`, D.Hello, 42).String()
47 | // -> "
48 | //
Hello
49 | //
42
50 | //
"
51 |
52 | // 7. Multiline Component with explicit language
53 | Html(ES, `
54 |
%L
55 |
%v
56 |
`, D.Hello, 42).String()
57 | // -> "
58 | //
Hola
59 | //
42
60 | //
"
61 | ```
62 |
63 | ## HTML Escaping
64 |
65 | fmt provides two convenience helpers to escape text for HTML:
66 |
67 | - `Convert(...).EscapeAttr()` — escape a value for safe inclusion inside an HTML attribute value.
68 | - `Convert(...).EscapeHTML()` — escape a value for safe inclusion inside HTML content.
69 |
70 | Both functions perform simple string replacements and will escape the characters: `&`, `<`, `>`, `"`, and `'`.
71 | Note that existing HTML entities will be escaped again (for example `&` -> `&`). This library follows a simple replace-based escaping strategy — if you need entity-aware unescaping/escaping, consider using a full HTML parser.
72 |
73 | ### Examples
74 |
75 | ```go
76 | Convert(`Tom & Jerry's "House" `).EscapeAttr()
77 | // -> `Tom & Jerry's "House" <tag>`
78 |
79 | Convert(`
1 & 2
`).EscapeHTML()
80 | // -> `<div>1 & 2</div>`
81 | ```
--------------------------------------------------------------------------------
/docs/betterment/ISSUE_MEMORY_TOOLS.md:
--------------------------------------------------------------------------------
1 |
2 | ````markdown
3 | # Go Memory Analysis CLI Tools
4 |
5 | This document lists command-line tools and techniques to analyze memory allocations in Go code. It is intended for LLMs or automation tools to follow and extract relevant commands and concepts.
6 |
7 | ---
8 |
9 | ## 🔍 1. `go build -gcflags="-m"`
10 |
11 | **Purpose:** Detect variables that escape to the heap.
12 |
13 | **Command:**
14 | ```bash
15 | go build -gcflags="-m" ./...
16 | ````
17 |
18 | **Output Example:**
19 |
20 | ```
21 | ./main.go:10:6: moved to heap: myStruct
22 | ```
23 |
24 | ---
25 |
26 | ## 📊 2. `pprof` – Memory Profiling
27 |
28 | **Purpose:** Profile memory usage and find functions with most allocations.
29 |
30 | **Usage with HTTP server:**
31 |
32 | 1. Import `net/http/pprof`
33 | 2. Launch server:
34 |
35 | ```go
36 | import _ "net/http/pprof"
37 | import "net/http"
38 |
39 | go func() {
40 | log.Println(http.ListenAndServe("localhost:6060", nil))
41 | }()
42 | ```
43 |
44 | 3. Run app, then collect profile:
45 |
46 | ```bash
47 | go tool pprof http://localhost:6060/debug/pprof/heap
48 | ```
49 |
50 | **Visualize:**
51 |
52 | ```bash
53 | go tool pprof -http=:8080 binary heap.prof
54 | ```
55 |
56 | ---
57 |
58 | ## 🧪 3. Bench-based Profiling
59 |
60 | **Purpose:** Profile memory from benchmarks.
61 |
62 | **Command:**
63 |
64 | ```bash
65 | go test -bench=. -benchmem -memprofile=mem.prof
66 | go tool pprof -text ./yourbinary.test mem.prof
67 | ```
68 |
69 | ---
70 |
71 | ## 📏 4. Manual Memory Inspection
72 |
73 | **Purpose:** Check memory stats before and after code blocks.
74 |
75 | **Code:**
76 |
77 | ```go
78 | import "runtime"
79 |
80 | var m runtime.MemStats
81 | runtime.ReadMemStats(&m)
82 | fmt.Printf("Alloc = %v MiB\n", m.Alloc/1024/1024)
83 | ```
84 |
85 | ---
86 |
87 | ## 📉 5. `benchstat`
88 |
89 | **Purpose:** Compare memory usage between versions.
90 |
91 | **Steps:**
92 |
93 | ```bash
94 | go test -bench=. -benchmem > old.txt
95 | # Make changes
96 | go test -bench=. -benchmem > new.txt
97 | benchstat old.txt new.txt
98 | ```
99 |
100 | ---
101 |
102 | ## 🔦 6. allocsnoop (Google tool)
103 |
104 | **Purpose:** Real-time memory allocation tracing.
105 |
106 | **Repo:** [https://github.com/google/allocsnoop](https://github.com/google/allocsnoop)
107 |
108 | ---
109 |
110 | ## 🐞 7. Delve (debugger)
111 |
112 | **Purpose:** Inspect memory and data at runtime.
113 |
114 | **Command:**
115 |
116 | ```bash
117 | dlv debug
118 | ```
119 |
120 | ---
121 |
122 | ## Notes
123 |
124 | * For long-running services, prefer `pprof` with HTTP endpoints.
125 | * For short-lived CLI tools, use `go test -benchmem -memprofile`.
126 | * Use `-gcflags="-m"` early to avoid unintended heap usage.
127 | * Use `benchstat` to automate regression detection.
128 |
129 | ```
130 |
131 |
132 |
--------------------------------------------------------------------------------
/benchmark/bench-memory-alloc/README.md:
--------------------------------------------------------------------------------
1 | # Memory Allocation Benchmarks
2 |
3 | This directory contains memory allocation benchmarks comparing standard Go libraries vs fmt implementations.
4 |
5 | ## Structure
6 |
7 | ```
8 | memory-bench/
9 | ├── standard/ # Standard library implementation
10 | │ ├── main.go # Main program with standard library operations
11 | │ ├── main_test.go # Benchmark tests for standard library
12 | │ └── go.mod # Go module without external dependencies
13 | └── tinystring/ # fmt implementation
14 | ├── main.go # Main program with fmt operations
15 | ├── main_test.go # Benchmark tests for fmt (including pointer optimization)
16 | └── go.mod # Go module with fmt dependency
17 | ```
18 |
19 | ## Benchmarks Included
20 |
21 | ### String Processing
22 | - **Standard**: Uses `strings.ToLower`, `strings.ReplaceAll`, `strings.Fields`, etc.
23 | - **fmt**: Uses `Tilde()`, `CamelLow()` chaining
24 | - **fmt (Pointers)**: Uses pointer optimization with `Apply()` method
25 |
26 | ### Number Processing
27 | - **Standard**: Uses `fmt.Sprintf`, `strings.Split` for number formatting
28 | - **fmt**: Uses `Round()`, `Thousands()` chaining
29 |
30 | ### Mixed Operations
31 | - **Standard**: Combines string and number operations using standard library
32 | - **fmt**: Uses unified fmt API for all data types
33 |
34 | ## Running Benchmarks
35 |
36 | ```bash
37 | # Run all memory benchmarks and update README
38 | ../scripts/memory-benchmark.sh
39 |
40 | # Run only standard library benchmarks
41 | cd standard && go test -bench=. -benchmem
42 |
43 | # Run only fmt benchmarks
44 | cd tinystring && go test -bench=. -benchmem
45 |
46 | # Compare specific benchmark
47 | cd standard && go test -bench=BenchmarkStringProcessing -benchmem
48 | cd ../tinystring && go test -bench=BenchmarkStringProcessing -benchmem
49 | ```
50 |
51 | ## Benchmark Output Fmt
52 |
53 | Go benchmark output format:
54 | ```
55 | BenchmarkName-N iterations ns/op bytes/op allocs/op
56 | ```
57 |
58 | Where:
59 | - **iterations**: Number of benchmark iterations
60 | - **ns/op**: Nanoseconds per operation
61 | - **bytes/op**: Bytes allocated per operation
62 | - **allocs/op**: Number of heap allocations per operation
63 |
64 | ## Integration
65 |
66 | The memory benchmark tool (`../memory-tool/main.go`) automatically:
67 | 1. Runs benchmarks in both directories
68 | 2. Parses benchmark output
69 | 3. Calculates improvement percentages
70 | 4. Updates the main README.md with real data
71 |
72 | ## Expected Results
73 |
74 | fmt typically shows:
75 | - **20-35% less memory allocation** per operation
76 | - **30-45% fewer heap allocations** per operation
77 | - **Similar or better execution time** performance
78 | - **Significant improvement with pointer optimization**
79 |
80 | These improvements come from:
81 | - Manual implementations avoiding standard library overhead
82 | - Pointer optimization reducing string copies
83 | - Chainable API reducing intermediate allocations
84 | - Unified type conversion system
85 |
--------------------------------------------------------------------------------
/benchmark/MEMORY_ALLOCATION_FASTHTTP_TIPS.md:
--------------------------------------------------------------------------------
1 |
2 | # Memory Allocation Tips (from fasthttp)
3 |
4 | ## Zero-Allocation Conversions entre `[]byte` y `string`
5 |
6 | En código crítico para el rendimiento, convertir entre `[]byte` y `string` usando las funciones estándar de Go puede ser ineficiente por las asignaciones de memoria. Para evitarlo, se pueden usar conversiones **unsafe** que no asignan memoria:
7 |
8 | ```go
9 | // Convierte []byte a string sin asignación
10 | func UnsafeString(b []byte) string {
11 | // #nosec G103
12 | return *(*string)(unsafe.Pointer(&b))
13 | }
14 |
15 | // Convierte string a []byte sin asignación
16 | func UnsafeBytes(s string) []byte {
17 | // #nosec G103
18 | return unsafe.Slice(unsafe.StringData(s), len(s))
19 | }
20 | ```
21 |
22 | **Advertencia:** Estas conversiones rompen la seguridad de tipos de Go. No modifiques el `[]byte` retornado por `UnsafeBytes(s string)` si la string original sigue en uso, ya que las strings son inmutables en Go y pueden ser compartidas en tiempo de ejecución.
23 |
24 | ## Trucos con buffers `[]byte`
25 |
26 | - Las funciones estándar de Go aceptan buffers nil:
27 |
28 | ```go
29 | var src []byte
30 | // Ambos buffers no inicializados
31 |
32 | dst = append(dst, src...) // válido si dst y/o src son nil
33 | copy(dst, src) // válido si dst y/o src son nil
34 | (string(src) == "") // true si src es nil
35 | (len(src) == 0) // true si src es nil
36 | src = src[:0] // funciona con src nil
37 |
38 | for i, ch := range src { // no entra si src es nil
39 | doSomething(i, ch)
40 | }
41 | ```
42 |
43 | - No es necesario hacer nil checks para buffers `[]byte`:
44 |
45 | ```go
46 | srcLen := len(src) // en vez de if src != nil { srcLen = len(src) }
47 | ```
48 |
49 | - Puedes hacer append de un string a un buffer `[]byte`:
50 |
51 | ```go
52 | dst = append(dst, "foobar"...)
53 | ```
54 |
55 | - Un buffer `[]byte` puede extenderse hasta su capacidad:
56 |
57 | ```go
58 | buf := make([]byte, 100)
59 | a := buf[:10] // len(a) == 10, cap(a) == 100
60 | b := a[:100] // válido, cap(a) == 100
61 | ```
62 |
63 | - Todas las funciones de fasthttp aceptan buffers nil:
64 |
65 | ```go
66 | statusCode, body, err := fasthttp.Get(nil, "http://google.com/")
67 | uintBuf := fasthttp.AppendUint(nil, 1234)
68 | ```
69 |
70 | - Conversiones sin asignación entre string y `[]byte`:
71 |
72 | ```go
73 | func b2s(b []byte) string {
74 | return *(*string)(unsafe.Pointer(&b))
75 | }
76 |
77 | func s2b(s string) (b []byte) {
78 | bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
79 | return b
80 | }
81 | ```
82 |
83 | **Advertencia:** Es un método unsafe, el string y el buffer comparten los mismos bytes. ¡No modifiques el buffer si el string sigue vivo!
84 |
85 | ## Buenas prácticas para evitar asignaciones
86 |
87 | - Reutiliza objetos y buffers `[]byte` tanto como sea posible.
88 | - Usa [sync.Pool](https://pkg.go.dev/sync#Pool) para reutilización eficiente.
89 | - Evita conversiones innecesarias entre `[]byte` y `string`.
90 | - Verifica tu código con el [race detector](https://go.dev/doc/articles/race_detector.html).
91 | - Escribe tests y benchmarks para los caminos críticos.
92 |
--------------------------------------------------------------------------------
/split_test.go:
--------------------------------------------------------------------------------
1 | package fmt_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/tinywasm/fmt"
7 | )
8 |
9 | func TestStringSplit(t *testing.T) {
10 | // Test cases with explicit separator
11 | testCasesWithSeparator := []struct {
12 | data string
13 | separator string
14 | expected []string
15 | }{
16 | // Original test cases
17 | {"texto1,texto2", ",", []string{"texto1", "texto2"}},
18 | {"apple,banana,cherry", ",", []string{"apple", "banana", "cherry"}},
19 | {"one.two.three.four", ".", []string{"one", "two", "three", "four"}},
20 | {"hello world", " ", []string{"hello", "world"}},
21 | {"hello. world", ".", []string{"hello", " world"}},
22 | {"h.", ".", []string{"h."}},
23 |
24 | // Edge condition test cases
25 | {"", ",", []string{""}}, // Empty string
26 | {"abc", "", []string{"a", "b", "c"}}, // Empty separator
27 | {"ab", ",", []string{"ab"}}, // String shorter than 3 chars
28 | {"a", "a", []string{"a"}}, // Single char string equals separator
29 | {"aaa", "a", []string{"", "", "", ""}}, // All chars are separators
30 | {"abc,def,", ",", []string{"abc", "def", ""}}, // Trailing separator
31 | {",abc,def", ",", []string{"", "abc", "def"}}, // Leading separator
32 | {"abc,,def", ",", []string{"abc", "", "def"}}, // Adjacent separators
33 | {"abc", "abc", []string{"", ""}}, // Separator equals data
34 | {"abcdefghi", "def", []string{"abc", "ghi"}}, // Separator in the middle
35 | {"abc:::def:::ghi", ":::", []string{"abc", "def", "ghi"}}, // Multi-char separator
36 | }
37 |
38 | // Test cases for whitespace splitting (no explicit separator)
39 | testCasesWhitespace := []struct {
40 | data string
41 | expected []string
42 | }{
43 | {"hello world", []string{"hello", "world"}},
44 | {" hello world ", []string{"hello", "world"}},
45 | {"hello\tworld", []string{"hello", "world"}},
46 | {"hello\nworld", []string{"hello", "world"}},
47 | {"hello\rworld", []string{"hello", "world"}},
48 | {"hello world test", []string{"hello", "world", "test"}},
49 | {"", []string{}}, // Empty string
50 | {"hello", []string{"hello"}}, // Single word
51 | {" ", []string{}}, // Only whitespace
52 | {"hello\n\tworld", []string{"hello", "world"}}, // Mixed whitespace
53 | }
54 |
55 | // Test with explicit separator
56 | for _, tc := range testCasesWithSeparator {
57 | out := Convert(tc.data).Split(tc.separator)
58 |
59 | if !areStringSlicesEqual(out, tc.expected) {
60 | t.Errorf("Split(%q, %q) = %v; expected %v", tc.data, tc.separator, out, tc.expected)
61 | }
62 | }
63 |
64 | // Test with default whitespace separator
65 | for _, tc := range testCasesWhitespace {
66 | out := Convert(tc.data).Split()
67 |
68 | if !areStringSlicesEqual(out, tc.expected) {
69 | t.Errorf("Split(%q) = %v; expected %v", tc.data, out, tc.expected)
70 | }
71 | }
72 | }
73 |
74 | func areStringSlicesEqual(a, b []string) bool {
75 | if len(a) != len(b) {
76 | return false
77 | }
78 | for i := range a {
79 | if a[i] != b[i] {
80 | return false
81 | }
82 | }
83 | return true
84 | }
85 |
--------------------------------------------------------------------------------
/join.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // Join concatenates the elements of a string slice to create a single string.
4 | // If no separator is provided, it uses a space as default.
5 | // Can be called with varargs to specify a custom separator.
6 | // eg: Convert([]string{"Hello", "World"}).Join() => "Hello World"
7 | // eg: Convert([]string{"Hello", "World"}).Join("-") => "Hello-World"
8 | func (c *Conv) Join(sep ...string) *Conv {
9 | separator := " " // default separator is space
10 | if len(sep) > 0 {
11 | separator = sep[0]
12 | }
13 |
14 | // Handle case when we have a string slice stored (DEFERRED CONVERSION)
15 | if c.kind == K.Slice && c.dataPtr != nil {
16 | // Use proper unsafe.Pointer to []string reconstruction
17 | slice := *(*[]string)(c.dataPtr)
18 | if len(slice) > 0 {
19 | c.ResetBuffer(BuffOut)
20 | for i, s := range slice {
21 | if i > 0 {
22 | c.AnyToBuff(BuffOut, separator)
23 | }
24 | c.AnyToBuff(BuffOut, s)
25 | }
26 | }
27 | return c
28 | }
29 |
30 | // For other types, convert to string first using AnyToBuff through GetString
31 | // OPTIMIZED: Check if content is ASCII for fast path
32 | if c.outLen == 0 {
33 | return c
34 | }
35 |
36 | // Check if buffer contains only ASCII for fast processing
37 | isASCII := true
38 | for i := 0; i < c.outLen; i++ {
39 | if c.out[i] > 127 {
40 | isASCII = false
41 | break
42 | }
43 | }
44 |
45 | if isASCII {
46 | // Fast path: ASCII-only content - process buffer directly
47 | var parts [][]byte
48 | start := 0
49 |
50 | for i := 0; i < c.outLen; i++ {
51 | ch := c.out[i]
52 | if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
53 | if i > start {
54 | part := make([]byte, i-start)
55 | copy(part, c.out[start:i])
56 | parts = append(parts, part)
57 | }
58 | start = i + 1
59 | }
60 | }
61 | if start < c.outLen {
62 | part := make([]byte, c.outLen-start)
63 | copy(part, c.out[start:])
64 | parts = append(parts, part)
65 | }
66 |
67 | // Join parts with the separator using buffer operations
68 | if len(parts) > 0 {
69 | c.ResetBuffer(BuffOut) // Reset output buffer
70 | for i, part := range parts {
71 | if i > 0 {
72 | c.AnyToBuff(BuffOut, separator)
73 | }
74 | c.wrBytes(BuffOut, part)
75 | }
76 | }
77 | } else {
78 | // Unicode fallback: use string processing
79 | str := c.GetString(BuffOut)
80 | if str != "" {
81 | // Split content by whitespace and rejoin with new separator
82 | var parts []string
83 | runes := []rune(str)
84 | start := 0
85 |
86 | for i, r := range runes {
87 | if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
88 | if i > start {
89 | parts = append(parts, string(runes[start:i]))
90 | }
91 | start = i + 1
92 | }
93 | }
94 | if start < len(runes) {
95 | parts = append(parts, string(runes[start:]))
96 | }
97 |
98 | // Join parts with the separator using AnyToBuff only
99 | if len(parts) > 0 {
100 | c.ResetBuffer(BuffOut) // Reset output buffer
101 | for i, part := range parts {
102 | if i > 0 {
103 | c.AnyToBuff(BuffOut, separator)
104 | }
105 | c.AnyToBuff(BuffOut, part)
106 | }
107 | }
108 | }
109 | }
110 |
111 | return c
112 | }
113 |
--------------------------------------------------------------------------------
/bool_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import "testing"
4 |
5 | func TestToBool(t *testing.T) {
6 | tests := []struct {
7 | name string
8 | input any
9 | expected bool
10 | hasContent bool
11 | }{
12 | {
13 | name: "String true",
14 | input: "true",
15 | expected: true,
16 | hasContent: false,
17 | },
18 | {
19 | name: "String false",
20 | input: "false",
21 | expected: false,
22 | hasContent: false,
23 | },
24 | {
25 | name: "String 1",
26 | input: "1",
27 | expected: true,
28 | hasContent: false,
29 | },
30 | {
31 | name: "String 0",
32 | input: "0",
33 | expected: false,
34 | hasContent: false,
35 | },
36 | {
37 | name: "Boolean true",
38 | input: true,
39 | expected: true,
40 | hasContent: false,
41 | },
42 | {
43 | name: "Boolean false",
44 | input: false,
45 | expected: false,
46 | hasContent: false,
47 | },
48 | {
49 | name: "Integer 1",
50 | input: 1,
51 | expected: true,
52 | hasContent: false,
53 | },
54 | {
55 | name: "Integer 0",
56 | input: 0,
57 | expected: false,
58 | hasContent: false,
59 | },
60 | {
61 | name: "Integer non-zero",
62 | input: 42,
63 | expected: true,
64 | hasContent: false,
65 | },
66 | {
67 | name: "Invalid string",
68 | input: "invalid",
69 | expected: false,
70 | hasContent: true,
71 | },
72 | {
73 | name: "Nil input",
74 | input: nil,
75 | expected: false,
76 | hasContent: true,
77 | },
78 | }
79 |
80 | for _, tt := range tests {
81 | t.Run(tt.name, func(t *testing.T) {
82 | out, err := Convert(tt.input).Bool()
83 |
84 | if tt.hasContent {
85 | if err == nil {
86 | t.Errorf("Expected error, but got none")
87 | }
88 | } else {
89 | if err != nil {
90 | t.Errorf("Unexpected error: %v", err)
91 | }
92 | if out != tt.expected {
93 | t.Errorf("Expected %v, got %v", tt.expected, out)
94 | }
95 | }
96 | })
97 | }
98 | }
99 |
100 | func TestFromBool(t *testing.T) {
101 | tests := []struct {
102 | name string
103 | input bool
104 | expected string
105 | }{
106 | {
107 | name: "True to string",
108 | input: true,
109 | expected: "true",
110 | },
111 | {
112 | name: "False to string",
113 | input: false,
114 | expected: "false",
115 | },
116 | }
117 |
118 | for _, tt := range tests {
119 | t.Run(tt.name, func(t *testing.T) {
120 | out := Convert(tt.input).String()
121 | if out != tt.expected {
122 | t.Errorf("Expected %q, got %q", tt.expected, out)
123 | }
124 | })
125 | }
126 | }
127 |
128 | func TestBoolChaining(t *testing.T) {
129 | // Test chaining with boolean operations
130 | out := Convert(true).ToUpper().String()
131 | expected := "TRUE"
132 | if out != expected {
133 | t.Errorf("Expected %q, got %q", expected, out)
134 | }
135 |
136 | // Test converting back
137 | boolVal, err := Convert("TRUE").ToLower().Bool()
138 | if err != nil {
139 | t.Errorf("Unexpected error: %v", err)
140 | }
141 | if !boolVal {
142 | t.Errorf("Expected true, got false")
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/split.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // Split divides a string by a separator and returns a slice of substrings.
4 | // Usage: Convert("Hello World").Split() => []string{"Hello", "World"}
5 | // Usage with separator: Convert("Hello;World").Split(";") => []string{"Hello", "World"}
6 | // If no separator is provided, splits by whitespace (similar to strings.Fields).
7 | // Uses the Conv work buffer for memory efficiency. The global Split function is deprecated; always use Convert(...).Split(...).
8 |
9 | func (c *Conv) Split(separator ...string) []string {
10 | src := c.GetString(BuffOut)
11 | return c.splitStr(src, separator...)
12 | }
13 |
14 | // splitStr is a reusable internal method for splitting a string by a separator (empty = by character, default whitespace).
15 | func (c *Conv) splitStr(src string, separator ...string) []string {
16 | var sep string
17 | if len(separator) == 0 {
18 | // Whitespace split: mimic strings.Fields
19 | out := make([]string, 0, len(src)/2+1)
20 | fieldStart := -1
21 | for i, r := range src {
22 | isSpace := r == ' ' || r == '\t' || r == '\n' || r == '\r'
23 | if !isSpace {
24 | if fieldStart == -1 {
25 | fieldStart = i
26 | }
27 | } else {
28 | if fieldStart != -1 {
29 | out = append(out, src[fieldStart:i])
30 | fieldStart = -1
31 | }
32 | }
33 | }
34 | if fieldStart != -1 {
35 | out = append(out, src[fieldStart:])
36 | }
37 | return out
38 | } else {
39 | sep = separator[0]
40 | }
41 | // Special case: split by character (empty separator)
42 | if len(sep) == 0 {
43 | if len(src) == 0 {
44 | return []string{}
45 | }
46 | out := make([]string, 0, len(src))
47 | for _, ch := range src {
48 | // OPTIMIZED: Direct string conversion without buffer operations
49 | out = append(out, string(ch))
50 | }
51 | return out
52 | }
53 | // Handle string shorter than 3 chars (legacy behavior)
54 | if len(src) < 3 {
55 | return []string{src}
56 | }
57 | // If src is empty, return [""] (legacy behavior)
58 | if len(src) == 0 {
59 | return []string{""}
60 | }
61 | // Use splitByDelimiterWithBuffer for all splits
62 | var out []string
63 | first := true
64 | orig := src
65 | for {
66 | before, after, found := c.splitByDelimiterWithBuffer(src, sep)
67 | out = append(out, before)
68 | if !found {
69 | // Legacy: if separator not found at all, return original string as single element
70 | if first && len(out) == 1 && out[0] == orig {
71 | return []string{orig}
72 | }
73 | break
74 | }
75 | src = after
76 | first = false
77 | }
78 | return out
79 | }
80 |
81 | // splitByDelimiterWithBuffer splits a string by the first occurrence of the delimiter.
82 | // Returns the parts (before and after the delimiter). If not found, found=false.
83 | // OPTIMIZED: Direct substring operations without buffer usage
84 | func (c *Conv) splitByDelimiterWithBuffer(s, delim string) (before, after string, found bool) {
85 | di := -1
86 | for i := 0; i <= len(s)-len(delim); i++ {
87 | if s[i:i+len(delim)] == delim {
88 | di = i
89 | break
90 | }
91 | }
92 | if di < 0 {
93 | return s, "", false
94 | }
95 | // OPTIMIZED: Direct substring without buffer operations
96 | before = s[:di]
97 | after = s[di+len(delim):]
98 | return before, after, true
99 | }
100 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // Custom error messages to avoid importing standard library packages like "errors" or "fmt"
4 | // This keeps the binary size minimal for embedded systems and WebAssembly
5 |
6 | // Err creates a new error message with support for multilingual translations
7 | // Supports LocStr types for translations and lang types for language specification
8 | // eg:
9 | // fmt.Err("invalid format") returns "invalid format"
10 | // fmt.Err(D.Format, D.Invalid) returns "invalid format"
11 | // fmt.Err(ES,D.Format, D.Invalid) returns "formato inválido"
12 |
13 | func Err(msgs ...any) *Conv {
14 | // UNIFIED PROCESSING: Use same intermediate function as Translate() but write to BuffErr
15 | return GetConv().SmartArgs(BuffErr, " ", true, false, msgs...)
16 | }
17 |
18 | // Errf creates a new Conv instance with error formatting similar to fmt.Errf
19 | // Example: fmt.Errf("invalid value: %s", value).Error()
20 | func Errf(format string, args ...any) *Conv {
21 | return GetConv().wrFormat(BuffErr, getCurrentLang(), format, args...)
22 | }
23 |
24 | // StringErr returns the content of the Conv along with any error and auto-releases to pool
25 | func (c *Conv) StringErr() (out string, err error) {
26 | // If there's an error, return empty string and the error object (do NOT release to pool)
27 | if c.hasContent(BuffErr) {
28 | return "", c
29 | }
30 |
31 | // Otherwise return the string content and no error (safe to release to pool)
32 | out = c.GetString(BuffOut)
33 | c.putConv()
34 | return out, nil
35 | }
36 |
37 | // wrErr writes error messages with support for int, string and LocStr
38 | // ENHANCED: Now supports int, string and LocStr parameters
39 | // Used internally by AnyToBuff for type error messages
40 | func (c *Conv) wrErr(msgs ...any) *Conv {
41 | // Write messages using default language (no detection needed)
42 | for i, msg := range msgs {
43 | if i > 0 {
44 | // Add space between words
45 | c.WrString(BuffErr, " ")
46 | }
47 | // fmt.Printf("wrErr: Processing message part: %v\n", msg) // Depuración
48 |
49 | switch v := msg.(type) {
50 | case LocStr:
51 | // Translate LocStr using default language
52 | c.wrTranslation(v, getCurrentLang(), BuffErr)
53 | case string:
54 | // Direct string write
55 | c.WrString(BuffErr, v)
56 | case int:
57 | // Convert int to string and write - simple conversion for errors
58 | if v == 0 {
59 | c.WrString(BuffErr, "0")
60 | } else {
61 | // Simple int to string conversion for error messages
62 | var buf [20]byte // Enough for 64-bit int
63 | n := len(buf)
64 | negative := v < 0
65 | if negative {
66 | v = -v
67 | }
68 | for v > 0 {
69 | n--
70 | buf[n] = byte(v%10) + '0'
71 | v /= 10
72 | }
73 | if negative {
74 | n--
75 | buf[n] = '-'
76 | }
77 | c.WrString(BuffErr, string(buf[n:]))
78 | }
79 | default:
80 | // For other types, convert to string representation
81 | c.WrString(BuffErr, "")
82 | }
83 | }
84 | // fmt.Printf("wrErr: Final error buffer content: %q, errLen: %d\n", c.GetString(BuffErr), c.errLen) // Depuración
85 | return c
86 | }
87 |
88 | func (c *Conv) getError() string {
89 | if !c.hasContent(BuffErr) { // ✅ Use API method instead of len(c.err)
90 | return ""
91 | }
92 | return c.GetString(BuffErr) // ✅ Use API method instead of direct string(c.err)
93 | }
94 |
95 | func (c *Conv) Error() string {
96 | return c.getError()
97 | }
98 |
--------------------------------------------------------------------------------
/docs/issues/BUG_FMT_CUSTOM_TYPE.md:
--------------------------------------------------------------------------------
1 | # BUG: Fmt() Doesn't Handle Custom Types with String() Method
2 |
3 | **Date**: 2025-10-31
4 | **Status**: Confirmed
5 | **Severity**: High
6 | **Impact**: Breaks fpdf PDF generation (missing `%PDF` header)
7 |
8 | ## Problem
9 |
10 | `tinystring.Fmt()` returns empty string when formatting custom types with `%s`, even if they implement `String()` method. This breaks compatibility with standard `fmt.Sprintf()` behavior.
11 |
12 | ## Root Cause
13 |
14 | In `fmt_template.go:426-433`, the `%s` case only accepts native `string` type:
15 |
16 | ```go
17 | case 's':
18 | if strVal, ok := arg.(string); ok {
19 | return strVal
20 | } else {
21 | c.wrInvalidTypeErr(formatSpec)
22 | return "" // ❌ Returns empty string for custom types
23 | }
24 | ```
25 |
26 | ## Test Case (Confirmed)
27 |
28 | ```go
29 | type customType string
30 | func (c customType) String() string { return string(c) }
31 |
32 | Fmt("Version: %s", customType("1.3")) // Returns: "" (should be "Version: 1.3")
33 | ```
34 |
35 | **Real-world impact**: `fpdf` calls `outf("%%PDF-%s", pdfVersion)` where `pdfVersion` is custom type → PDF has no header → "Failed to load PDF document"
36 |
37 | ## Proposed Solution
38 |
39 | Modify `formatValue()` case `'s'` to check for types with `String()` method:
40 |
41 | ```go
42 | case 's':
43 | // Handle native string
44 | if strVal, ok := arg.(string); ok {
45 | return strVal
46 | }
47 | // Handle types with String() method (fmt.Stringer interface)
48 | if stringer, ok := arg.(interface{ String() string }); ok {
49 | return stringer.String()
50 | }
51 | // Fallback: try AnyToBuff (already handles String() internally)
52 | c.ResetBuffer(BuffWork)
53 | c.AnyToBuff(BuffWork, arg)
54 | if c.hasContent(BuffErr) {
55 | c.wrInvalidTypeErr(formatSpec)
56 | return ""
57 | }
58 | return c.GetString(BuffWork)
59 | ```
60 |
61 | ## Alternative (Simpler)
62 |
63 | Reuse existing `AnyToBuff()` which already handles `String()` method:
64 |
65 | ```go
66 | case 's':
67 | if strVal, ok := arg.(string); ok {
68 | return strVal
69 | }
70 | // Let AnyToBuff handle it (supports String() method)
71 | c.ResetBuffer(BuffWork)
72 | c.AnyToBuff(BuffWork, arg)
73 | if c.hasContent(BuffErr) {
74 | return ""
75 | }
76 | return c.GetString(BuffWork)
77 | ```
78 |
79 | ## Testing
80 |
81 | Run: `cd tinystring && go test -v -run TestFmtWithCustomTypeString`
82 |
83 | Expected after fix:
84 | - ✅ `customType("1.3")` with `%s` → "1.3"
85 | - ✅ `Fmt("%%PDF-%s", pdfVersion)` → "%PDF-1.4"
86 | - ✅ PDF generation works in browser
87 |
88 | ## Impact Analysis
89 |
90 | **Before fix:**
91 | - `Fmt("%s", customType)` → `""` (empty string)
92 | - PDF generation broken (no header)
93 | - Incompatible with `fmt.Sprintf()` behavior
94 |
95 | **After fix:**
96 | - `Fmt("%s", customType)` → calls `String()` method
97 | - PDF generation works
98 | - Compatible with `fmt.Sprintf()` behavior
99 |
100 | ## Files to Modify
101 |
102 | 1. `tinystring/fmt_template.go` - line 426-433 (formatValue case 's')
103 | 2. Verify all tests pass: `go test ./...`
104 |
105 | ## Notes
106 |
107 | - `%v` format already works via `AnyToBuff()`
108 | - `%d`, `%f` etc. have similar issues with custom numeric types
109 | - This fix should apply to all format specifiers that reject custom types
110 |
--------------------------------------------------------------------------------
/string_ptr_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import (
4 | std_fmt "fmt"
5 | "testing"
6 | )
7 |
8 | func TestStringPointer(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | initialValue string
12 | transform func(*Conv) *Conv
13 | expectedValue string
14 | }{
15 | {
16 | name: "Remove tildes from string pointer",
17 | initialValue: "áéíóúÁÉÍÓÚ",
18 | transform: func(t *Conv) *Conv {
19 | return t.Tilde()
20 | },
21 | expectedValue: "aeiouAEIOU",
22 | },
23 | {
24 | name: "Convert to lowercase with string pointer",
25 | initialValue: "HELLO WORLD",
26 | transform: func(t *Conv) *Conv {
27 | return t.ToLower()
28 | },
29 | expectedValue: "hello world",
30 | },
31 | {
32 | name: "Convert to camelCase with string pointer",
33 | initialValue: "hello world example",
34 | transform: func(t *Conv) *Conv {
35 | return t.CamelLow()
36 | },
37 | expectedValue: "helloWorldExample",
38 | },
39 | {
40 | name: "Multiple transforms with string pointer",
41 | initialValue: "Él Múrcielago Rápido",
42 | transform: func(t *Conv) *Conv {
43 | return t.Tilde().CamelLow()
44 | },
45 | expectedValue: "elMurcielagoRapido",
46 | },
47 | }
48 | for _, tt := range tests {
49 | t.Run(tt.name, func(t *testing.T) {
50 | // Create string pointer with initial value
51 | originalPtr := tt.initialValue
52 |
53 | // Convert using string pointer and apply changes
54 | tt.transform(Convert(&originalPtr)).Apply()
55 |
56 | // Check if original pointer was updated correctly
57 | if originalPtr != tt.expectedValue {
58 | t.Errorf("\noriginalPtr = %q\nwant %q", originalPtr, tt.expectedValue)
59 | }
60 | })
61 | }
62 | }
63 |
64 | // Estos ejemplos ilustran cómo usar los punteros a strings para evitar asignaciones adicionales
65 | func Example_stringPointerBasic() {
66 | // Creamos una variable string que queremos modificar
67 | myText := "héllô wórld"
68 |
69 | // En lugar de crear una nueva variable con el resultado,
70 | // modificamos directamente la variable original usando Apply()
71 | Convert(&myText).Tilde().ToLower().Apply()
72 |
73 | // La variable original ha sido modificada
74 | std_fmt.Println(myText)
75 | // Output: hello world
76 | }
77 |
78 | func Example_stringPointerCamelCase() {
79 | // Ejemplo de uso con múltiples transformaciones
80 | originalText := "Él Múrcielago Rápido"
81 |
82 | // Las transformaciones modifican la variable original directamente
83 | // usando el método Apply() para actualizar el puntero
84 | Convert(&originalText).Tilde().CamelLow().Apply()
85 |
86 | std_fmt.Println(originalText)
87 | // Output: elMurcielagoRapido
88 | }
89 |
90 | func Example_stringPointerEfficiency() {
91 | // En aplicaciones de alto rendimiento, reducir asignaciones de memoria
92 | // puede ser importante para evitar la presión sobre el garbage collector
93 | // Método tradicional (crea nuevas asignaciones de memoria)
94 | traditionalText := "Texto con ACENTOS"
95 | processedText := Convert(traditionalText).Tilde().ToLower().String()
96 | std_fmt.Println(processedText)
97 |
98 | // Método con punteros (modifica directamente la variable original)
99 | directText := "Otro TEXTO con ACENTOS"
100 | Convert(&directText).Tilde().ToLower().Apply()
101 | std_fmt.Println(directText)
102 |
103 | // Output:
104 | // texto con acentos
105 | // otro texto con acentos
106 | }
107 |
--------------------------------------------------------------------------------
/benchmark/common.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 |
7 | . "github.com/tinywasm/fmt"
8 | )
9 |
10 | // BinaryInfo represents information about a compiled binary file
11 | type BinaryInfo struct {
12 | Name string
13 | Size int64
14 | SizeStr string
15 | Type string // "native" or "wasm"
16 | Library string // "standard" or "tinystring"
17 | OptLevel string // "default", "ultra", "speed", "debug"
18 | }
19 |
20 | // OptimizationConfig represents a TinyGo optimization configuration
21 | type OptimizationConfig struct {
22 | Name string
23 | Flags string
24 | Description string
25 | Suffix string
26 | }
27 |
28 | // FormatSize converts bytes to human-readable format (moved from existing code)
29 | func FormatSize(bytes int64) string {
30 | const unit = 1024
31 | if bytes < unit {
32 | return Fmt("%d B", bytes)
33 | }
34 | div, exp := int64(unit), 0
35 | for n := bytes / unit; n >= unit; n /= unit {
36 | div *= unit
37 | exp++
38 | }
39 | return Fmt("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
40 | }
41 |
42 | // FileExists checks if a file exists
43 | func FileExists(filename string) bool {
44 | _, err := os.Stat(filename)
45 | return !os.IsNotExist(err)
46 | }
47 |
48 | // FindBinaries searches for binary files in the specified directory
49 | func FindBinaries(dir string, patterns []string) ([]BinaryInfo, error) {
50 | var binaries []BinaryInfo
51 |
52 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
53 | if err != nil {
54 | return err
55 | }
56 |
57 | if info.IsDir() {
58 | return nil
59 | }
60 |
61 | filename := info.Name()
62 | for _, pattern := range patterns {
63 | if Contains(filename, pattern) {
64 | binary := BinaryInfo{
65 | Name: filename,
66 | Size: info.Size(),
67 | SizeStr: FormatSize(info.Size()),
68 | OptLevel: extractOptLevel(filename),
69 | }
70 |
71 | // Determine type and library from filename/path
72 | if Contains(filename, ".wasm") {
73 | binary.Type = "wasm"
74 | } else {
75 | binary.Type = "native"
76 | }
77 |
78 | if Contains(path, "standard") {
79 | binary.Library = "standard"
80 | } else if Contains(path, "tinystring") {
81 | binary.Library = "tinystring"
82 | }
83 |
84 | binaries = append(binaries, binary)
85 | break
86 | }
87 | }
88 |
89 | return nil
90 | })
91 |
92 | return binaries, err
93 | }
94 |
95 | // extractOptLevel extracts optimization level from filename
96 | func extractOptLevel(filename string) string {
97 | if Contains(filename, "-ultra") {
98 | return "ultra"
99 | } else if Contains(filename, "-speed") {
100 | return "speed"
101 | } else if Contains(filename, "-debug") {
102 | return "debug"
103 | }
104 | return "default"
105 | }
106 |
107 | // LogStep prints a formatted step message
108 | func LogStep(message string) {
109 | os.Stdout.Write([]byte(Fmt("📋 %s\n", message)))
110 | }
111 |
112 | // LogSuccess prints a formatted success message
113 | func LogSuccess(message string) {
114 | os.Stdout.Write([]byte(Fmt("✅ %s\n", message)))
115 | }
116 |
117 | // LogError prints a formatted error message
118 | func LogError(message string) {
119 | os.Stdout.Write([]byte(Fmt("❌ %s\n", message)))
120 | }
121 |
122 | // LogInfo prints a formatted info message
123 | func LogInfo(message string) {
124 | os.Stdout.Write([]byte(Fmt("ℹ️ %s\n", message)))
125 | }
126 |
--------------------------------------------------------------------------------
/dictionary_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestDictionaryBasicFunctionality(t *testing.T) {
9 | // Test default English
10 | err := Err(D.Format, D.Invalid).Error()
11 | expected := Translate(D.Format, D.Invalid).String()
12 | if err != expected {
13 | t.Errorf("Expected '%s', got '%s'", expected, err)
14 | }
15 | }
16 |
17 | func TestDictionarySpanishTranslation(t *testing.T) {
18 | // Set Spanish as default
19 | OutLang(ES)
20 |
21 | err := Err(D.Format, D.Invalid).Error()
22 | expected := Translate(ES, D.Format, D.Invalid).String()
23 | if err != expected {
24 | t.Errorf("Expected '%s', got '%s'", expected, err)
25 | }
26 |
27 | // Reset to English
28 | OutLang(EN)
29 | }
30 |
31 | func TestDictionaryInlineLanguage(t *testing.T) {
32 | // Use French inline
33 | err := Err(FR, D.Empty, D.String).Error()
34 | expected := "Vide Chaîne"
35 | if err != expected {
36 | t.Errorf("Expected '%s', got '%s'", expected, err)
37 | }
38 | }
39 |
40 | func TestDictionaryMixedWithRegularStrings(t *testing.T) {
41 | // Mix dictionary words with regular strings
42 | err := Err(D.Invalid, "custom", D.Value).Error()
43 | expected := "Invalid custom Value"
44 | if err != expected {
45 | t.Errorf("Expected '%s', got '%s'", expected, err)
46 | }
47 | }
48 |
49 | func TestOLFallbackToEnglish(t *testing.T) {
50 | // Test fallback when translation is empty
51 | testOL := LocStr{"test", "", "", "", "", "", "", "", ""}
52 | // Inline get logic for testing
53 | out := func() string {
54 | if int(ES) < len(testOL) && testOL[ES] != "" {
55 | return testOL[ES]
56 | }
57 | return testOL[EN] // Fallback to English
58 | }()
59 | expected := "test"
60 | if out != expected {
61 | t.Errorf("Expected fallback to English 'test', got '%s'", out)
62 | }
63 | }
64 |
65 | func TestLanguageDetection(t *testing.T) {
66 | c := &Conv{}
67 | // Test that getSystemLang returns a valid language
68 | lang := c.getSystemLang()
69 | if lang > ZH {
70 | t.Errorf("Invalid language detected: %d", lang)
71 | }
72 | }
73 |
74 | func TestComplexErrorComposition(t *testing.T) {
75 | // Test complex error message composition as per design document
76 | OutLang(ES)
77 |
78 | // Test: errNegativeUnsigned → D.Negative + D.Numbers + D.Not + D.Supported + D.To + D.Unsigned + D.Integer
79 | err := Err(D.Numbers, D.Negative, D.Not, D.Supported).Error()
80 |
81 | if len(err) == 0 {
82 | t.Error("Complex error composition should not be empty")
83 | }
84 |
85 | t.Logf("Complex error out: %s", err)
86 |
87 | // Reset to English
88 | OutLang(EN)
89 | }
90 |
91 | func TestDictionaryConsistency(t *testing.T) {
92 | typeOfD := reflect.TypeOf(D)
93 | valueOfD := reflect.ValueOf(D)
94 | for i := 0; i < typeOfD.NumField(); i++ {
95 | field := typeOfD.Field(i)
96 | fieldName := field.Name
97 | fieldValue := valueOfD.Field(i).Interface().(LocStr)
98 | lowerFieldName := Convert(fieldName).ToLower().String()
99 | eng := Convert(fieldValue[EN]).ToLower().String()
100 | // Tomar los 2 primeros y 2 últimos caracteres
101 | fnLen := len(lowerFieldName)
102 | engLen := len(eng)
103 | if fnLen >= 2 && engLen >= 2 {
104 | fnFirst := lowerFieldName[:2]
105 | fnLast := lowerFieldName[fnLen-2:]
106 | engFirst := eng[:2]
107 | engLast := eng[engLen-2:]
108 | if fnFirst != engFirst || fnLast != engLast {
109 | t.Fatalf("Field '%s' value '%s' does not match first/last 2 chars of field name '%s'", fieldName, eng, lowerFieldName)
110 | }
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/docs/betterment/ISSUE_SUMMARY_TINYSTRING.md:
--------------------------------------------------------------------------------
1 | # fmt - Library Context for LLM Maintenance
2 |
3 | ## What is fmt?
4 |
5 | Lightweight Go library for string manipulation with fluid API, specifically designed for small devices and web applications using TinyGo compiler.
6 |
7 | ## Core Problem
8 |
9 | **Excessive binary sizes in WebAssembly when using Go standard library**, specifically:
10 | - Large binaries slow web app loading
11 | - Standard library (`fmt`, `strings`, `strconv`) adds significant overhead
12 | - Memory constraints on small devices and edge computing
13 | - Universal need for string manipulation in all projects
14 |
15 | ## Primary Goal
16 |
17 | Enable Go WebAssembly adoption by reducing binary size while providing essential string operations through manual implementations that avoid standard library imports.
18 |
19 | ## Key Features
20 |
21 | - Fluid chainable API
22 | - Zero standard library dependencies
23 | - TinyGo compatible
24 | - Universal type conversion (string, int, float, bool)
25 | - Manual implementations replace: `strconv.ParseFloat`, `strconv.FormatFloat`, `strconv.ParseInt`, `strconv.FormatInt`, `strings.IndexByte`, `fmt.Sprintf`
26 |
27 | ## Basic Usage Pattern
28 |
29 | ```go
30 | import "github.com/tinywasm/fmt"
31 |
32 | // Basic string processing
33 | result := tinystring.Convert("MÍ téxtO").Tilde().String()
34 | // Output: "MI textO"
35 |
36 | // Type conversion and chaining
37 | result := tinystring.Convert(42).ToUpper().String()
38 | // Output: "42"
39 |
40 | // Complex chaining
41 | result := tinystring.Convert("Él Múrcielago Rápido")
42 | .Tilde()
43 | .CamelLow()
44 | .String()
45 | // Output: "elMurcielagoRapido"
46 |
47 | // Memory optimization with pointers
48 | text := "Él Múrcielago Rápido"
49 | tinystring.Convert(&text).Tilde().CamelLow().Apply()
50 | // text is now: "elMurcielagoRapido"
51 | ```
52 |
53 | ## Core Operations
54 |
55 | **Initialization & Output:**
56 | - `Convert(v any)` - Initialize with any type
57 | - `String()` - Get result as string
58 | - `Apply()` - Modify original string pointer
59 |
60 | **Text Transformations:**
61 | - `Tilde()` - Remove accents/diacritics
62 | - `ToLower()`, `ToUpper()` - Case conversion
63 | - `Capitalize()` - First letter of each word
64 | - `CamelLow()`, `CamelUp()` - camelCase conversion
65 | - `SnakeLow()`, `SnakeUp()` - snake_case conversion
66 |
67 | **String Operations:**
68 | - `Split(data, separator)` - Split strings
69 | - `Join(sep...)` - Join string slices
70 | - `Replace(old, new, n...)` - Replace substrings
71 | - `TrimPrefix()`, `TrimSuffix()`, `TrimSpace()` - TrimSpace operations
72 | - `Contains()`, `Count()` - Search operations
73 | - `Repeat(n)` - Repeat strings
74 |
75 | **Advanced Features:**
76 | - `Truncate(maxWidth, reservedChars...)` - Smart truncation
77 | - `TruncateName(maxCharsPerWord, maxWidth)` - Name truncation for UI
78 | - `Round(decimals)` - Numeric rounding with `Down()` modifier
79 | - `Thousands()` - Thousand separators
80 | - `Fmt(format, args...)` - sprintf-style formatting
81 | - `Quote()` - Add quotes with escaping
82 |
83 | **Type Conversions:**
84 | - `Bool()` - Convert to boolean
85 | - `Int(base...)`, `Uint(base...)` - Integer conversion
86 | - `Float64()` - Float conversion
87 | - `StringErr()` - Get result with error handling
88 |
89 | ## Installation
90 |
91 | ```bash
92 | go get github.com/tinywasm/fmt
93 | ```
94 |
95 | ## Architecture Notes
96 |
97 | - Manual implementations avoid standard library bloat
98 | - Optimized for binary size over runtime performance
99 | - Thread-safe operations
100 | - Supports pointer optimization to reduce allocations
101 | - WebAssembly-first design philosophy
102 |
--------------------------------------------------------------------------------
/docs/API_STRINGS.md:
--------------------------------------------------------------------------------
1 | # Strings Package Equivalents
2 |
3 | Replace common `strings` package functions with fmt equivalents:
4 |
5 | | Go Standard | fmt Equivalent |
6 | |-------------|----------------------|
7 | | `strings.Builder` | `c:= Convert() c.Write(a) c.Write(b) c.String()` |
8 | | `strings.Contains()` | `Contains(s, substr)` |
9 | | `strings.Index()` | `Index(s, substr)` |
10 | | `strings.LastIndex()` | `LastIndex(s, substr)` |
11 | | `strings.Join()` | `Convert(slice).Join(sep).String()` |
12 | | `strings.Repeat()` | `Convert(s).Repeat(n).String()` |
13 | | `strings.Replace()` | `Convert(s).Replace(old, new).String()` |
14 | | `strings.Split()` | `Convert(s).Split(sep).String()` |
15 | | `strings.ToLower()` | `Convert(s).ToLower().String()` |
16 | | `strings.ToUpper()` | `Convert(s).ToUpper().String()` |
17 | | `strings.TrimSpace()` | `Convert(s).TrimSpace().String()` |
18 | | `strings.TrimPrefix()` | `Convert(s).TrimPrefix(prefix).String()` |
19 | | `strings.TrimSuffix()` | `Convert(s).TrimSuffix(suffix).String()` |
20 | | `strings.HasPrefix()` | `HasPrefix(s, prefix)` |
21 | | `strings.HasSuffix()` | `HasSuffix(s, suffix)` |
22 |
23 | ## Other String Transformations
24 |
25 | ```go
26 | Convert("hello world").CamelLow().String() // out: "helloWorld"
27 | Convert("hello world").CamelUp().String() // out: "HelloWorld"
28 | Convert("hello world").SnakeLow().String() // out: "hello_world"
29 | Convert("hello world").SnakeUp().String() // out: "HELLO_WORLD"
30 | ```
31 |
32 | ## String Search & Operations
33 |
34 | ```go
35 | // Search and count
36 | pos := Index("hello world", "world") // out: 6 (first occurrence)
37 | found := Contains("hello world", "world") // out: true
38 | count := Count("abracadabra", "abra") // out: 2
39 |
40 | // Prefix / Suffix checks
41 | isPref := HasPrefix("hello", "he") // out: true
42 | isSuf := HasSuffix("file.txt", ".txt") // out: true
43 |
44 | // Note: this library follows the standard library semantics for prefixes/suffixes:
45 | // an empty prefix or suffix is considered a match (HasPrefix(s, "") == true,
46 | // HasSuffix(s, "") == true).
47 |
48 | // Find last occurrence (useful for file extensions)
49 | pos := LastIndex("image.backup.jpg", ".") // out: 12
50 | if pos >= 0 {
51 | extension := "image.backup.jpg"[pos+1:] // out: "jpg"
52 | }
53 |
54 | // ⚠️ Note: Index, Contains and LastIndex are global functions, not methods.
55 | // Do NOT use: Convert(s).Contains(substr) // ❌ Incorrect, will not compile
56 | // Use: Index(s, substr) // ✅ Correct
57 | // Contains(s, substr) // ✅ Correct
58 | // LastIndex(s, substr) // ✅ Correct
59 |
60 | // Replace operations
61 | Convert("hello world").Replace("world", "Go").String() // out: "hello Go"
62 | Convert("test 123 test").Replace(123, 456).String() // out: "test 456 test"
63 | ```
64 |
65 | ## String Splitting & Joining
66 |
67 | ```go
68 | // Split strings (always use Convert(...).Split(...))
69 | parts := Convert("apple,banana,cherry").Split(",")
70 | // out: []string{"apple", "banana", "cherry"}
71 |
72 | parts := Convert("hello world new").Split() // Handles whitespace
73 | // out: []string{"hello", "world", "new"}
74 |
75 | // Join slices
76 | Convert([]string{"Hello", "World"}).Join().String() // out: "Hello World"
77 | Convert([]string{"a", "b", "c"}).Join("-").String() // out: "a-b-c"
78 | ```
79 |
80 | ## String Trimming & Cleaning
81 |
82 | ```go
83 | // TrimSpace operations
84 | Convert(" hello ").TrimSpace().String() // out: "hello"
85 | Convert("prefix-data").TrimPrefix("prefix-").String() // out: "data"
86 | Convert("file.txt").TrimSuffix(".txt").String() // out: "file"
87 |
88 | // Repeat strings
89 | Convert("Go").Repeat(3).String() // out: "GoGoGo"
90 | ```
--------------------------------------------------------------------------------
/docs/img/badges.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
84 |
--------------------------------------------------------------------------------
/repeat_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import "testing"
4 |
5 | func TestDebugRepeat(t *testing.T) {
6 | // Test: Convert("test").Repeat(0) should be ""
7 |
8 | result := Convert("test").Repeat(0).String()
9 | expected := ""
10 |
11 | if result != expected {
12 | t.Errorf("Convert(\"test\").Repeat(0) = %q, want %q", result, expected)
13 | } else {
14 | t.Logf("Success: Convert(\"test\").Repeat(0) = %q", result)
15 | }
16 | }
17 |
18 | func TestRepeat(t *testing.T) {
19 | tests := []struct {
20 | name string
21 | input string
22 | count int
23 | expected string
24 | }{
25 | {
26 | name: "Repeat a single character",
27 | input: "x",
28 | count: 3,
29 | expected: "xxx",
30 | },
31 | {
32 | name: "Repeat a word",
33 | input: "hello ",
34 | count: 2,
35 | expected: "hello hello ",
36 | },
37 | {
38 | name: "Zero repetitions",
39 | input: "test",
40 | count: 0,
41 | expected: "",
42 | },
43 | {
44 | name: "Negative repetitions",
45 | input: "test",
46 | count: -1,
47 | expected: "",
48 | },
49 | {
50 | name: "Empty string",
51 | input: "",
52 | count: 5,
53 | expected: "",
54 | },
55 | {
56 | name: "Repeat once",
57 | input: "once",
58 | count: 1,
59 | expected: "once",
60 | },
61 | }
62 |
63 | for _, tt := range tests {
64 | t.Run(tt.name, func(t *testing.T) {
65 | out := Convert(tt.input).Repeat(tt.count).String()
66 | if out != tt.expected {
67 | t.Errorf("Convert(%q).Repeat(%d) = %q, want %q",
68 | tt.input, tt.count, out, tt.expected)
69 | }
70 | })
71 | }
72 | }
73 |
74 | // TestRepeatChain tests the chaining capabilities of the Repeat method with other methods
75 | func TestRepeatChain(t *testing.T) {
76 | tests := []struct {
77 | name string
78 | input string
79 | want string
80 | function func(*Conv) *Conv
81 | }{
82 | {
83 | name: "Repeat and convert to upper",
84 | input: "hello",
85 | want: "HELLOHELLOHELLO",
86 | function: func(t *Conv) *Conv {
87 | return t.Repeat(3).ToUpper()
88 | },
89 | },
90 | {
91 | name: "Repeat and convert to lower",
92 | input: "WORLD",
93 | want: "worldworld",
94 | function: func(t *Conv) *Conv {
95 | return t.Repeat(2).ToLower()
96 | },
97 | },
98 | {
99 | name: "Multiple operations with repeat",
100 | input: "Test",
101 | want: "testtesttest",
102 | function: func(t *Conv) *Conv {
103 | return t.ToLower().Repeat(3)
104 | },
105 | },
106 | {
107 | name: "Repeat with CamelCase",
108 | input: "hello world",
109 | want: "helloWorldhelloWorld",
110 | function: func(t *Conv) *Conv {
111 | return t.CamelLow().Repeat(2)
112 | },
113 | },
114 | {
115 | name: "Empty after repeat zero",
116 | input: "Conv",
117 | want: "",
118 | function: func(t *Conv) *Conv {
119 | return t.Repeat(0).ToUpper()
120 | },
121 | },
122 | {
123 | name: "Repeat with accents and remove tildes",
124 | input: "ñandú",
125 | want: "ñanduñanduñandu",
126 | function: func(t *Conv) *Conv {
127 | return t.Tilde().Repeat(3)
128 | },
129 | },
130 | {
131 | name: "SnakeCase and Repeat",
132 | input: "Hello World Example",
133 | want: "hello_world_examplehello_world_example",
134 | function: func(t *Conv) *Conv {
135 | return t.SnakeLow().Repeat(2)
136 | },
137 | },
138 | {
139 | name: "Complex chaining",
140 | input: "Él Múrcielago",
141 | want: "ELMURCIELAGOELMURCIELAGO",
142 | function: func(t *Conv) *Conv {
143 | return t.Tilde().CamelLow().ToUpper().Repeat(2)
144 | },
145 | },
146 | }
147 |
148 | for _, tt := range tests {
149 | t.Run(tt.name, func(t *testing.T) {
150 | got := tt.function(Convert(tt.input)).String()
151 | if got != tt.want {
152 | t.Fatalf("\n🎯Test: %q\ninput: %q\n got: %q\n want: %q", tt.name, tt.input, got, tt.want)
153 | }
154 | })
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fmt
2 |
3 |
4 |
5 |
6 | fmt is a lightweight Go library that provides comprehensive string manipulation, type conversion, formatting, and multilingual error handling with a fluid API, specifically designed for small devices and web applications using TinyGo as the target compiler.
7 |
8 | ## Key Features
9 |
10 | - 🚀 **Fluid and chainable API** - Easy to use and readable operations
11 | - 📝 **Complete string toolkit** - Transformations, conversions, formatting, and error handling
12 | - 🌍 **Multilingual error messages** - Built-in dictionary system with 9 languages
13 | - 🧵 **Concurrency safe** - Thread-safe operations for concurrent environments
14 | - 📦 **Zero dependencies** - No `fmt`, `strings`, `strconv`, or `errors` imports
15 | - 🎯 **TinyGo optimized** - Manual implementations for minimal binary size
16 | - 🌐 **WebAssembly-first** - Designed for modern web deployment
17 | - 🔄 **Universal type support** - Works with strings, numbers, booleans, and slices
18 | - ⚡ **Performance focused** - Predictable allocations and custom optimizations
19 |
20 | ## [Why fmt?](docs/WHY.md)
21 |
22 | ## Installation
23 |
24 | ```bash
25 | go get github.com/tinywasm/fmt
26 | ```
27 |
28 | ## Usage
29 |
30 | ```go
31 | import . "github.com/tinywasm/fmt"
32 |
33 | // Quick start - Basic conversion and transformation
34 | text := Convert("Hóla Múndo").Tilde().ToLower().String() // out: "hola mundo"
35 |
36 | // Working with different data types
37 | numText := Convert(42).String() // out: "42"
38 | boolText := Convert(true).String() // out: "true"
39 |
40 | // Memory-efficient approach using string pointers
41 | original := "Él Múrcielago Rápido"
42 | Convert(&original).Tilde().CamelLow().Apply()
43 | // original is now: "elMurcielagoRapido"
44 |
45 | // Efficient builder and chaining with accent normalization
46 | items := []string{" ÁPPLE ", " banána ", " piñata "," ÑANDÚ "}
47 | builder := Convert() // without params reused buffer = optimal performance
48 | for i, item := range items {
49 | processed := Convert(item).
50 | TrimSpace(). // TrimSpace whitespace
51 | Tilde(). // Normalize accents
52 | ToLower(). // Convert to lowercase
53 | Capitalize(). // Capitalize first letter
54 | String() // Finalize the string
55 | builder.Write(processed)
56 | if i < len(items)-1 {
57 | builder.Write(" - ")
58 | }
59 | }
60 |
61 | out := builder.String() // Finalize the string hiding the error
62 | out, err := builder.StringErr() // OR finalize with error handling
63 |
64 | // out: "Apple - Banana - Piñata - Ñandu", err: nil
65 | ```
66 |
67 | ## Documentation
68 |
69 | - [Errors Package Equivalents](docs/API_ERRORS.md) - Replace errors package functions
70 | - [Filepath Package Equivalents](docs/API_FILEPATH.md) - Replace filepath package functions
71 | - [Fmt Package Equivalents](docs/API_FMT.md) - Replace fmt package functions
72 | - [HTML Generation & Escaping](docs/API_HTML.md) - HTML generation and escaping utilities
73 | - [ID and Primary Key Detection](docs/ID_PRIMARY_KEY.md) - Field naming conventions
74 | - [Key-Value Parsing](docs/API_PARSING.md) - Parse key-value strings
75 | - [Message Types](docs/MESSAGE_TYPES.md) - Message classification system
76 | - [Smart Truncation](docs/TRUNCATION.md) - Text truncation utilities
77 | - [Strings Package Equivalents](docs/API_STRINGS.md) - Replace strings package functions
78 | - [Strconv Package Equivalents](docs/API_STRCONV.md) - Replace strconv package functions
79 | - [Struct Tag Extraction](docs/STRUCT_TAGS.md) - Extract values from struct tags
80 | - [Translation Guide](docs/TRANSLATE.md) - Multilingual error messages
81 |
82 |
83 | ## Benchmarking
84 | - [Benchmarking](benchmark/README.md)
85 |
86 | ## Examples
87 |
88 | - [WebAssembly Html Code](example/web/client.go)
89 |
90 | ---
91 | ## [Contributing](https://github.com/tinywasm/cdvelop/blob/main/CONTRIBUTING.md)
92 | ---
93 | ## [License](LICENSE)
--------------------------------------------------------------------------------
/translation_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import "testing"
4 |
5 | func TestT_LanguageDetection(t *testing.T) {
6 | // Use real dictionary words: Format (EN: "format", ES: "formato", FR: "format")
7 | t.Run("lang constant ES", func(t *testing.T) {
8 | got := Translate(ES, D.Format).String()
9 | if got != "Formato" {
10 | t.Errorf("expected 'Formato', got '%s'", got)
11 | }
12 | })
13 |
14 | t.Run("lang string ES", func(t *testing.T) {
15 | got := Translate("es", D.Format).String()
16 | if got != "Formato" {
17 | t.Errorf("expected 'Formato', got '%s'", got)
18 | }
19 | })
20 |
21 | t.Run("lang constant FR", func(t *testing.T) {
22 | got := Translate(FR, D.Format).String()
23 | if got != "Format" {
24 | t.Errorf("expected 'Format', got '%s'", got)
25 | }
26 | })
27 |
28 | t.Run("lang string FR", func(t *testing.T) {
29 | got := Translate("FR", D.Format).String()
30 | if got != "Format" {
31 | t.Errorf("expected 'Format', got '%s'", got)
32 | }
33 | })
34 |
35 | t.Run("default lang EN", func(t *testing.T) {
36 | got := Translate(D.Format).String()
37 | if got != "Format" {
38 | t.Errorf("expected 'Format', got '%s'", got)
39 | }
40 | })
41 |
42 | // Test phrase composition
43 | t.Run("phrase ES", func(t *testing.T) {
44 | got := Translate("ES", D.Format, D.Invalid).String()
45 | if got != "Formato Inválido" {
46 | t.Errorf("expected 'Formato Inválido', got '%s'", got)
47 | }
48 | })
49 |
50 | t.Run("phrase EN", func(t *testing.T) {
51 | got := Translate(D.Format, D.Invalid).String()
52 | if got != "Format Invalid" {
53 | t.Errorf("expected 'Format Invalid', got '%s'", got)
54 | }
55 | })
56 | }
57 |
58 | func TestTranslationFormatting(t *testing.T) {
59 |
60 | t.Run("no leading space, custom format", func(t *testing.T) {
61 | // Simula una frase compleja con LocStr y strings, sin espacios extra antes de la primera palabra ni después de los separadores
62 | // Ejemplo: Translate(D.Fields, ":", D.Cancel, ")")
63 | got := Translate(D.Fields, ":", D.Cancel, ")").String()
64 | want := "Fields: Cancel)" // Asumiendo EN por defecto
65 | if got != want {
66 | t.Errorf("expected '%s', got '%s'", want, got)
67 | }
68 | })
69 |
70 | t.Run("no space before colon, phrase with punctuation", func(t *testing.T) {
71 | // Simula formato con puntuación pegada
72 | got := Translate(D.Format, ":", D.Invalid).String()
73 | want := "Format: Invalid"
74 | if got != want {
75 | t.Errorf("expected '%s', got '%s'", want, got)
76 | }
77 | })
78 |
79 | t.Run("newline with translated field alignment", func(t *testing.T) {
80 | // Reproduce el caso del shortcuts.go donde después de newline viene D.Fields
81 | // y debe quedar alineado sin espacio extra
82 | got := Translate("Tabs:\n", D.Fields, ":").String()
83 | want := "Tabs:\nFields:"
84 | if got != want {
85 | t.Errorf("expected '%s', got '%s'", want, got)
86 | }
87 | })
88 | }
89 |
90 | func BenchmarkTranslate(b *testing.B) {
91 | b.ReportAllocs()
92 | b.Run("Simple", func(b *testing.B) {
93 | for i := 0; i < b.N; i++ {
94 | c := Translate(D.Format)
95 | c.PutConv()
96 | }
97 | })
98 | b.Run("WithLang", func(b *testing.B) {
99 | for i := 0; i < b.N; i++ {
100 | c := Translate(ES, D.Format)
101 | c.PutConv()
102 | }
103 | })
104 | b.Run("Complex", func(b *testing.B) {
105 | for i := 0; i < b.N; i++ {
106 | c := Translate(ES, D.Format, ":", D.Invalid)
107 | c.PutConv()
108 | }
109 | })
110 | }
111 |
112 | func BenchmarkTranslatePointer(b *testing.B) {
113 | b.ReportAllocs()
114 | b.Run("SimplePtr", func(b *testing.B) {
115 | for i := 0; i < b.N; i++ {
116 | c := Translate(&D.Format)
117 | c.PutConv()
118 | }
119 | })
120 | b.Run("WithLangPtr", func(b *testing.B) {
121 | for i := 0; i < b.N; i++ {
122 | c := Translate(ES, &D.Format)
123 | c.PutConv()
124 | }
125 | })
126 | b.Run("ComplexPtr", func(b *testing.B) {
127 | for i := 0; i < b.N; i++ {
128 | c := Translate(ES, &D.Format, ":", &D.Invalid)
129 | c.PutConv()
130 | }
131 | })
132 | }
133 |
--------------------------------------------------------------------------------
/docs/issues/FEAT_MESSAGETYPE_SSE.md:
--------------------------------------------------------------------------------
1 | # Feature: Extend MessageType for SSE Error Types
2 |
3 | > **Status:** Approved
4 | > **Created:** December 2025
5 | > **Related:** `tinysse` package SSE implementation
6 |
7 | ## Context
8 |
9 | El paquete `tinysse` necesita tipos de error para SSE. En lugar de crear nuevos tipos, extendemos `MessageType` existente para reutilizar la infraestructura.
10 |
11 | ## Current State
12 |
13 | ```go
14 | // messagetype.go
15 | var Msg = struct {
16 | Normal MessageType // 0
17 | Info MessageType // 1
18 | Error MessageType // 2
19 | Warning MessageType // 3
20 | Success MessageType // 4
21 | }{0, 1, 2, 3, 4}
22 | ```
23 |
24 | ## Proposed Extension
25 |
26 | Agregar tipos específicos para errores de red/SSE:
27 |
28 | ```go
29 | var Msg = struct {
30 | Normal MessageType // 0 - Default
31 | Info MessageType // 1 - Information
32 | Error MessageType // 2 - General error
33 | Warning MessageType // 3 - Warning
34 | Success MessageType // 4 - Success
35 |
36 | // Network/SSE specific (new)
37 | Connect MessageType // 5 - Connection error
38 | Auth MessageType // 6 - Authentication error
39 | Parse MessageType // 7 - Parse/decode error
40 | Timeout MessageType // 8 - Timeout error
41 | Broadcast MessageType // 9 - Broadcast/send error
42 | }{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
43 | ```
44 |
45 | ## Usage in TinySSE
46 |
47 | ```go
48 | // tinysse/error.go
49 | import . "github.com/tinywasm/fmt"
50 |
51 | type SSEError struct {
52 | Type MessageType // Reuse tinystring.MessageType
53 | Err error
54 | Context any // clientID, raw data, etc.
55 | }
56 |
57 | // Config callback
58 | type Config struct {
59 | OnError func(SSEError)
60 | }
61 |
62 | // Usage
63 | cfg.OnError = func(e SSEError) {
64 | switch {
65 | case e.Type == Msg.Auth:
66 | // Redirect to login
67 | case e.Type == Msg.Connect:
68 | // Show offline banner
69 | case e.Type.IsError():
70 | // General error handling
71 | }
72 | }
73 | ```
74 |
75 | ## Benefits
76 |
77 | 1. **Reutilización** - No duplicar tipos
78 | 2. **Consistencia** - Mismo sistema en todo el stack
79 | 3. **Zero allocations** - `MessageType` es `uint8`
80 | 4. **Helpers existentes** - `IsError()`, `String()`, etc.
81 |
82 | ## New Helper Methods
83 |
84 | ```go
85 | // Add to messagetype.go
86 | func (t MessageType) IsConnect() bool { return t == Msg.Connect }
87 | func (t MessageType) IsAuth() bool { return t == Msg.Auth }
88 | func (t MessageType) IsParse() bool { return t == Msg.Parse }
89 | func (t MessageType) IsTimeout() bool { return t == Msg.Timeout }
90 | func (t MessageType) IsBroadcast() bool { return t == Msg.Broadcast }
91 |
92 | // IsNetworkError returns true for any network-related error type
93 | func (t MessageType) IsNetworkError() bool {
94 | return t == Msg.Connect || t == Msg.Auth || t == Msg.Timeout || t == Msg.Broadcast
95 | }
96 |
97 | // Update String() method
98 | func (t MessageType) String() string {
99 | switch t {
100 | case Msg.Info:
101 | return "Info"
102 | case Msg.Error:
103 | return "Error"
104 | case Msg.Warning:
105 | return "Warning"
106 | case Msg.Success:
107 | return "Success"
108 | case Msg.Connect:
109 | return "Connect"
110 | case Msg.Auth:
111 | return "Auth"
112 | case Msg.Parse:
113 | return "Parse"
114 | case Msg.Timeout:
115 | return "Timeout"
116 | case Msg.Broadcast:
117 | return "Broadcast"
118 | default:
119 | return "Normal"
120 | }
121 | }
122 | ```
123 |
124 | ## Implementation Steps
125 |
126 | 1. [ ] Extend `Msg` struct in `messagetype.go`
127 | 2. [ ] Add helper methods (`IsConnect()`, etc.)
128 | 3. [ ] Update `String()` method
129 | 4. [ ] Add tests for new types
130 | 5. [ ] Use in `tinysse` package
131 |
132 | ## Backward Compatibility
133 |
134 | ✅ **Fully compatible** - Only adds new constants, existing code unchanged.
135 |
--------------------------------------------------------------------------------
/search.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // Index finds the first occurrence of substr in s, returns -1 if not found.
4 | // This is the base primitive that other functions will reuse.
5 | //
6 | // Examples:
7 | //
8 | // Index("hello world", "world") // returns 6
9 | // Index("hello world", "lo") // returns 3
10 | // Index("hello world", "xyz") // returns -1 (not found)
11 | // Index("hello world", "") // returns 0 (empty string)
12 | // Index("data\x00more", "\x00") // returns 4 (null byte)
13 | func Index(s, substr string) int {
14 | n := len(substr)
15 | if n == 0 {
16 | return 0 // Standard behavior: empty string is found at position 0
17 | }
18 | if n == 1 {
19 | // Optimized single byte search
20 | for i := 0; i < len(s); i++ {
21 | if s[i] == substr[0] {
22 | return i
23 | }
24 | }
25 | return -1
26 | }
27 |
28 | // Brute force for longer strings
29 | for i := 0; i <= len(s)-n; i++ {
30 | if s[i:i+n] == substr {
31 | return i
32 | }
33 | }
34 | return -1
35 | }
36 |
37 | // Count checks how many times the string 'search' is present in 'Conv'.
38 | // Uses Index internally for consistency and maintainability.
39 | //
40 | // Examples:
41 | //
42 | // Count("abracadabra", "abra") // returns 2
43 | // Count("hello world", "l") // returns 3
44 | // Count("golang", "go") // returns 1
45 | // Count("test", "xyz") // returns 0 (not found)
46 | // Count("anything", "") // returns 0 (empty search)
47 | // Count("a\x00b\x00c", "\x00") // returns 2 (null bytes)
48 | func Count(Conv, search string) int {
49 | if len(search) == 0 {
50 | return 0
51 | }
52 |
53 | count := 0
54 | s := Conv
55 | for {
56 | i := Index(s, search)
57 | if i == -1 {
58 | break
59 | }
60 | count++
61 | s = s[i+len(search):] // Skip past this match
62 | }
63 | return count
64 | }
65 |
66 | // Contains checks if the string 'search' is present in 'Conv'.
67 | // Uses Index internally for efficient single-pass detection.
68 | //
69 | // Examples:
70 | //
71 | // Contains("hello world", "world") // returns true
72 | // Contains("hello world", "xyz") // returns false
73 | // Contains("", "test") // returns false (empty string)
74 | // Contains("test", "") // returns false (empty search)
75 | // Contains("data\x00more", "\x00") // returns true (null byte)
76 | // Contains("Case", "case") // returns false (case sensitive)
77 | func Contains(Conv, search string) bool {
78 | if len(search) == 0 {
79 | return false // Cadena vacía no se considera contenida
80 | }
81 | return Index(Conv, search) != -1
82 | }
83 |
84 | // HasPrefix reports whether the string 'conv' begins with 'prefix'.
85 | // Implemented using Index for consistency with other helpers in this package.
86 | //
87 | // Examples:
88 | //
89 | // HasPrefix("hello", "he") // returns true
90 | // HasPrefix("hello", "hello") // returns true
91 | // HasPrefix("hello", "") // returns false (empty prefix)
92 | // HasPrefix("a", "abc") // returns false (prefix longer than string)
93 | func HasPrefix(conv, prefix string) bool {
94 | if len(prefix) == 0 {
95 | return true // Follow stdlib semantics: empty string is a prefix of any string
96 | }
97 | if len(conv) < len(prefix) {
98 | return false
99 | }
100 | // Search only in the slice that should match the prefix; Index must return 0.
101 | return Index(conv[:len(prefix)], prefix) == 0
102 | }
103 |
104 | // HasSuffix reports whether the string 'Conv' ends with 'suffix'.
105 | // Implemented using Index by checking the tail of the string.
106 | //
107 | // Examples:
108 | //
109 | // HasSuffix("testing", "ing") // returns true
110 | // HasSuffix("file.txt", ".txt") // returns true
111 | // HasSuffix("hello", "") // returns false (empty suffix)
112 | // HasSuffix("go", "golang") // returns false
113 | func HasSuffix(conv, suffix string) bool {
114 | if len(suffix) == 0 {
115 | return true // Follow stdlib semantics: empty string is a suffix of any string
116 | }
117 | if len(conv) < len(suffix) {
118 | return false
119 | }
120 | // The suffix should be at the start of the tail slice
121 | return Index(conv[len(conv)-len(suffix):], suffix) == 0
122 | }
123 |
--------------------------------------------------------------------------------
/messagetype.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // MessageType represents the classification of message types in the system.
4 | type MessageType uint8
5 |
6 | // Msg exposes the MessageType constants for external use, following fmt naming convention.
7 | // Msg exposes the MessageType constants for external use, following fmt naming convention.
8 | var Msg = struct {
9 | Normal MessageType
10 | Info MessageType
11 | Error MessageType
12 | Warning MessageType
13 | Success MessageType
14 |
15 | // Network/SSE specific (new)
16 | Connect MessageType // Connection error
17 | Auth MessageType // Authentication error
18 | Parse MessageType // Parse/decode error
19 | Timeout MessageType // Timeout error
20 | Broadcast MessageType // Broadcast/send error
21 | }{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
22 |
23 | // Helper methods for MessageType
24 | func (t MessageType) IsNormal() bool { return t == Msg.Normal }
25 | func (t MessageType) IsInfo() bool { return t == Msg.Info }
26 | func (t MessageType) IsError() bool { return t == Msg.Error }
27 | func (t MessageType) IsWarning() bool { return t == Msg.Warning }
28 | func (t MessageType) IsSuccess() bool { return t == Msg.Success }
29 |
30 | // Network/SSE helper methods
31 | func (t MessageType) IsConnect() bool { return t == Msg.Connect }
32 | func (t MessageType) IsAuth() bool { return t == Msg.Auth }
33 | func (t MessageType) IsParse() bool { return t == Msg.Parse }
34 | func (t MessageType) IsTimeout() bool { return t == Msg.Timeout }
35 | func (t MessageType) IsBroadcast() bool { return t == Msg.Broadcast }
36 |
37 | // IsNetworkError returns true for any network-related error type
38 | func (t MessageType) IsNetworkError() bool {
39 | return t == Msg.Connect || t == Msg.Auth || t == Msg.Timeout || t == Msg.Broadcast
40 | }
41 |
42 | func (t MessageType) String() string {
43 | switch t {
44 | case Msg.Info:
45 | return "Info"
46 | case Msg.Error:
47 | return "Error"
48 | case Msg.Warning:
49 | return "Warning"
50 | case Msg.Success:
51 | return "Success"
52 | case Msg.Connect:
53 | return "Connect"
54 | case Msg.Auth:
55 | return "Auth"
56 | case Msg.Parse:
57 | return "Parse"
58 | case Msg.Timeout:
59 | return "Timeout"
60 | case Msg.Broadcast:
61 | return "Broadcast"
62 | default:
63 | return "Normal"
64 | }
65 | }
66 |
67 | // Pre-compiled patterns for efficient buffer matching
68 | var (
69 | errorPatterns = [][]byte{
70 | []byte("error"), []byte("failed"), []byte("exit status 1"),
71 | []byte("undeclared"), []byte("undefined"), []byte("fatal"),
72 | }
73 | warningPatterns = [][]byte{
74 | []byte("warning"), []byte("warn"), []byte("debug"),
75 | }
76 | successPatterns = [][]byte{
77 | []byte("success"), []byte("completed"), []byte("successful"), []byte("done"),
78 | }
79 | infoPatterns = [][]byte{
80 | []byte("info"), []byte("starting"), []byte("initializing"),
81 | }
82 | )
83 |
84 | // StringType returns the string from BuffOut and its detected MessageType, then auto-releases the Conv
85 | func (c *Conv) StringType() (string, MessageType) {
86 | // Get string content FIRST (before detection modifies buffer)
87 | out := c.GetString(BuffOut)
88 | // Detect type from BuffOut content
89 | msgType := c.detectMessageTypeFromBuffer(BuffOut)
90 | // Auto-release
91 | c.putConv()
92 | return out, msgType
93 | }
94 |
95 | // detectMessageTypeFromBuffer analyzes the buffer content and returns the detected MessageType (zero allocations)
96 | func (c *Conv) detectMessageTypeFromBuffer(dest BuffDest) MessageType {
97 | // 1. Copy content directly to work buffer using swapBuff (zero allocations)
98 | c.swapBuff(dest, BuffWork)
99 | // 2. Convert to lowercase in work buffer using existing method
100 | c.changeCase(true, BuffWork)
101 | // 3. Direct buffer pattern matching - NO Contains() allocations
102 | if c.bufferContainsPattern(BuffWork, errorPatterns) {
103 | return Msg.Error
104 | }
105 | if c.bufferContainsPattern(BuffWork, warningPatterns) {
106 | return Msg.Warning
107 | }
108 | if c.bufferContainsPattern(BuffWork, successPatterns) {
109 | return Msg.Success
110 | }
111 | if c.bufferContainsPattern(BuffWork, infoPatterns) {
112 | return Msg.Info
113 | }
114 | return Msg.Normal
115 | }
116 |
--------------------------------------------------------------------------------
/benchmark/run-all-benchmarks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # run-all-benchmarks.sh - Run all fmt benchmarks and generate reports
4 | # This script runs both binary size analysis and memory allocation benchmarks
5 |
6 | set -e
7 |
8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 | cd "$SCRIPT_DIR"
10 |
11 | # Function to get the correct analyzer binary name based on OS
12 | get_analyzer_name() {
13 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
14 | echo "analyzer.exe"
15 | else
16 | echo "analyzer"
17 | fi
18 | }
19 |
20 | ANALYZER_BINARY=$(get_analyzer_name)
21 |
22 | echo "🚀 Starting fmt Benchmark Suite"
23 | echo "======================================="
24 |
25 | # Build the analyzer tool
26 | echo "📋 Building analyzer tool..."
27 | if ! go build -o "$ANALYZER_BINARY" *.go; then
28 | echo "❌ Failed to build analyzer tool"
29 | exit 1
30 | fi
31 | echo "✅ Analyzer built successfully"
32 |
33 | # Function to run binary size benchmarks
34 | run_binary_benchmarks() {
35 | echo ""
36 | echo "📋 Running binary size benchmarks..."
37 |
38 | # Check if binary directories exist
39 | if [[ ! -d "bench-binary-size" ]]; then
40 | echo "❌ Binary benchmark directory not found: bench-binary-size"
41 | echo "ℹ️ Run build-and-measure.sh first to create binary samples"
42 | return 1
43 | fi
44 |
45 | # Run binary analysis
46 | if ./"$ANALYZER_BINARY" binary; then
47 | echo "✅ Binary size analysis completed"
48 | return 0
49 | else
50 | echo "❌ Binary size analysis failed"
51 | return 1
52 | fi
53 | }
54 |
55 | # Function to run memory allocation benchmarks
56 | run_memory_benchmarks() {
57 | echo ""
58 | echo "📋 Running memory allocation benchmarks..."
59 |
60 | # Check if memory benchmark directories exist
61 | if [[ ! -d "bench-memory-alloc" ]]; then
62 | echo "❌ Memory benchmark directory not found: bench-memory-alloc"
63 | echo "ℹ️ Memory benchmarks directory structure may need setup"
64 | return 1
65 | fi
66 |
67 | # Run memory analysis
68 | if ./"$ANALYZER_BINARY" memory; then
69 | echo "✅ Memory allocation analysis completed"
70 | return 0
71 | else
72 | echo "❌ Memory allocation analysis failed"
73 | return 1
74 | fi
75 | }
76 |
77 | # Function to run all benchmarks
78 | run_all_benchmarks() {
79 | echo ""
80 | echo "📋 Running complete benchmark suite..."
81 |
82 | if ./"$ANALYZER_BINARY" all; then
83 | echo "✅ Complete benchmark suite completed"
84 | return 0
85 | else
86 | echo "❌ Complete benchmark suite failed"
87 | return 1
88 | fi
89 | }
90 |
91 | # Parse command line arguments
92 | case "${1:-all}" in
93 | "binary")
94 | run_binary_benchmarks
95 | ;;
96 | "memory")
97 | run_memory_benchmarks
98 | ;;
99 | "all")
100 | run_all_benchmarks
101 | ;;
102 | "help"|"-h"|"--help")
103 | echo "Usage: $0 [binary|memory|all|help]"
104 | echo ""
105 | echo "Commands:"
106 | echo " binary - Run only binary size benchmarks"
107 | echo " memory - Run only memory allocation benchmarks"
108 | echo " all - Run all benchmarks (default)"
109 | echo " help - Show this help message"
110 | echo ""
111 | echo "Prerequisites:"
112 | echo " - Go 1.19+ installed"
113 | echo " - TinyGo installed (for binary size benchmarks)"
114 | echo " - Binary samples built (run build-and-measure.sh first)"
115 | exit 0
116 | ;;
117 | *)
118 | echo "❌ Unknown command: $1"
119 | echo "Use '$0 help' for usage information"
120 | exit 1
121 | ;;
122 | esac
123 |
124 | RESULT=$?
125 |
126 | echo ""
127 | if [[ $RESULT -eq 0 ]]; then
128 | echo "🎉 Benchmark suite completed successfully!"
129 | echo "📋 Results have been updated in README.md"
130 | else
131 | echo "❌ Benchmark suite completed with errors"
132 | echo "ℹ️ Check the output above for details"
133 | fi
134 |
135 | echo "======================================="
136 | exit $RESULT
137 |
--------------------------------------------------------------------------------
/messagetype_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestStringTypeDetection(t *testing.T) {
8 | t.Run("Empty string", func(t *testing.T) {
9 | msg, msgType := Convert("").StringType()
10 | if msgType != Msg.Normal {
11 | t.Errorf("Expected Normal for empty string, got %v", msgType)
12 | }
13 | if msg != "" {
14 | t.Errorf("Expected empty string, got %q", msg)
15 | }
16 | })
17 |
18 | t.Run("Error keywords", func(t *testing.T) {
19 | errorKeywords := []string{
20 | "This is an error message",
21 | "Operation failed",
22 | "exit status 1",
23 | "variable undeclared",
24 | "function undefined",
25 | "fatal exception",
26 | }
27 | for _, keyword := range errorKeywords {
28 | msg, msgType := Convert(keyword).StringType()
29 | if msgType != Msg.Error {
30 | t.Errorf("Expected Error for keyword %q, got %v", keyword, msgType)
31 | }
32 | if msg != keyword {
33 | t.Errorf("Expected message to be unchanged, got %q", msg)
34 | }
35 | }
36 | })
37 |
38 | t.Run("Success keywords", func(t *testing.T) {
39 | successKeywords := []string{
40 | "Success! Operation completed",
41 | "success",
42 | "Operation completed",
43 | "Build successful",
44 | "Task done",
45 | }
46 | for _, keyword := range successKeywords {
47 | msg, msgType := Convert(keyword).StringType()
48 | if msgType != Msg.Success {
49 | t.Errorf("Expected Success for keyword %q, got %v", keyword, msgType)
50 | }
51 | if msg != keyword {
52 | t.Errorf("Expected message to be unchanged, got %q", msg)
53 | }
54 | }
55 | })
56 |
57 | t.Run("Info keywords", func(t *testing.T) {
58 | infoKeywords := []string{
59 | "Info: Starting process",
60 | "... initializing ...",
61 | "starting up",
62 | "initializing system",
63 | }
64 | for _, keyword := range infoKeywords {
65 | _, msgType := Convert(keyword).StringType()
66 | if msgType != Msg.Info {
67 | t.Errorf("Expected Info for keyword %q, got %v", keyword, msgType)
68 | }
69 | }
70 | })
71 |
72 | t.Run("Warning keywords", func(t *testing.T) {
73 | warningKeywords := []string{
74 | "Warning: disk space low",
75 | "warn user",
76 | }
77 | for _, keyword := range warningKeywords {
78 | _, msgType := Convert(keyword).StringType()
79 | if msgType != Msg.Warning {
80 | t.Errorf("Expected Warning for keyword %q, got %v", keyword, msgType)
81 | }
82 | }
83 | })
84 |
85 | t.Run("Normal message", func(t *testing.T) {
86 | _, msgType := Convert("Hello world").StringType()
87 | if msgType != Msg.Normal {
88 | t.Errorf("Expected Normal for generic message, got %v", msgType)
89 | }
90 | })
91 | }
92 |
93 | func TestSSERelatedTypes(t *testing.T) {
94 | tests := []struct {
95 | name string
96 | msgType MessageType
97 | check func(MessageType) bool
98 | expected string
99 | }{
100 | {"Connect", Msg.Connect, func(t MessageType) bool { return t.IsConnect() }, "Connect"},
101 | {"Auth", Msg.Auth, func(t MessageType) bool { return t.IsAuth() }, "Auth"},
102 | {"Parse", Msg.Parse, func(t MessageType) bool { return t.IsParse() }, "Parse"},
103 | {"Timeout", Msg.Timeout, func(t MessageType) bool { return t.IsTimeout() }, "Timeout"},
104 | {"Broadcast", Msg.Broadcast, func(t MessageType) bool { return t.IsBroadcast() }, "Broadcast"},
105 | }
106 |
107 | for _, tt := range tests {
108 | t.Run(tt.name, func(t *testing.T) {
109 | if !tt.check(tt.msgType) {
110 | t.Errorf("Expected check for %s to be true", tt.name)
111 | }
112 | if tt.msgType.String() != tt.expected {
113 | t.Errorf("Expected String() for %s to be %q, got %q", tt.name, tt.expected, tt.msgType.String())
114 | }
115 | })
116 | }
117 |
118 | t.Run("IsNetworkError", func(t *testing.T) {
119 | networkTypes := []MessageType{Msg.Connect, Msg.Auth, Msg.Timeout, Msg.Broadcast}
120 | for _, nt := range networkTypes {
121 | if !nt.IsNetworkError() {
122 | t.Errorf("Expected %v to be a network error", nt)
123 | }
124 | }
125 |
126 | nonNetworkTypes := []MessageType{Msg.Normal, Msg.Info, Msg.Error, Msg.Warning, Msg.Success, Msg.Parse}
127 | for _, nnt := range nonNetworkTypes {
128 | if nnt.IsNetworkError() {
129 | t.Errorf("Expected %v NOT to be a network error", nnt)
130 | }
131 | }
132 | })
133 | }
134 |
--------------------------------------------------------------------------------
/fmt_fprintf_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestFprintf(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | format string
13 | args []any
14 | expected string
15 | }{
16 | {
17 | name: "simple string",
18 | format: "Hello %s",
19 | args: []any{"world"},
20 | expected: "Hello world",
21 | },
22 | {
23 | name: "integer formatting",
24 | format: "Number: %d",
25 | args: []any{42},
26 | expected: "Number: 42",
27 | },
28 | {
29 | name: "multiple args",
30 | format: "Hello %s, you have %d messages",
31 | args: []any{"John", 5},
32 | expected: "Hello John, you have 5 messages",
33 | },
34 | {
35 | name: "float formatting",
36 | format: "Value: %.2f",
37 | args: []any{3.14159},
38 | expected: "Value: 3.14",
39 | },
40 | {
41 | name: "boolean formatting",
42 | format: "Active: %t",
43 | args: []any{true},
44 | expected: "Active: true",
45 | },
46 | {
47 | name: "hex formatting",
48 | format: "Hex: %x",
49 | args: []any{255},
50 | expected: "Hex: ff",
51 | },
52 | {
53 | name: "no args",
54 | format: "Simple text",
55 | args: []any{},
56 | expected: "Simple text",
57 | },
58 | }
59 |
60 | for _, tt := range tests {
61 | t.Run(tt.name, func(t *testing.T) {
62 | var buf bytes.Buffer
63 | n, err := Fprintf(&buf, tt.format, tt.args...)
64 |
65 | if err != nil {
66 | t.Errorf("Fprintf() error = %v", err)
67 | return
68 | }
69 |
70 | result := buf.String()
71 | if result != tt.expected {
72 | t.Errorf("Fprintf() = %q, want %q", result, tt.expected)
73 | }
74 |
75 | if n != len(tt.expected) {
76 | t.Errorf("Fprintf() returned n = %d, want %d", n, len(tt.expected))
77 | }
78 | })
79 | }
80 | }
81 |
82 | func TestFprintf_Errors(t *testing.T) {
83 | tests := []struct {
84 | name string
85 | format string
86 | args []any
87 | }{
88 | {
89 | name: "missing argument",
90 | format: "Hello %s %d",
91 | args: []any{"world"}, // Missing second argument
92 | },
93 | {
94 | name: "invalid format",
95 | format: "Hello %z", // %z is not supported
96 | args: []any{"world"},
97 | },
98 | {
99 | name: "wrong type for %d",
100 | format: "Number: %d",
101 | args: []any{"not a number"},
102 | },
103 | }
104 |
105 | for _, tt := range tests {
106 | t.Run(tt.name, func(t *testing.T) {
107 | var buf bytes.Buffer
108 | n, err := Fprintf(&buf, tt.format, tt.args...)
109 |
110 | if err == nil {
111 | t.Errorf("Fprintf() expected error but got none, result: %q", buf.String())
112 | }
113 |
114 | if n != 0 {
115 | t.Errorf("Fprintf() on error returned n = %d, want 0", n)
116 | }
117 | })
118 | }
119 | }
120 |
121 | func TestFprintf_WriterError(t *testing.T) {
122 | // Test with a writer that always returns an error
123 | errorWriter := &errorOnlyWriter{}
124 |
125 | n, err := Fprintf(errorWriter, "Hello %s", "world")
126 |
127 | if err == nil {
128 | t.Error("Fprintf() expected write error but got none")
129 | }
130 |
131 | if n != 0 {
132 | t.Errorf("Fprintf() on write error returned n = %d, want 0", n)
133 | }
134 |
135 | // Error should be from the writer, not from formatting
136 | if !strings.Contains(err.Error(), "write error") {
137 | t.Errorf("Fprintf() error = %v, want write error", err)
138 | }
139 | }
140 |
141 | // Helper writer that always returns an error
142 | type errorOnlyWriter struct{}
143 |
144 | func (w *errorOnlyWriter) Write(p []byte) (n int, err error) {
145 | return 0, &writeError{"write error"}
146 | }
147 |
148 | type writeError struct {
149 | msg string
150 | }
151 |
152 | func (e *writeError) Error() string {
153 | return e.msg
154 | }
155 |
156 | func BenchmarkFprintf(b *testing.B) {
157 | var buf bytes.Buffer
158 |
159 | b.ResetTimer()
160 | for i := 0; i < b.N; i++ {
161 | buf.Reset()
162 | Fprintf(&buf, "Hello %s, number %d, float %.2f", "world", 42, 3.14159)
163 | }
164 | }
165 |
166 | func BenchmarkFprintf_vs_Fmt(b *testing.B) {
167 | var buf bytes.Buffer
168 | format := "Hello %s, number %d, float %.2f"
169 | args := []any{"world", 42, 3.14159}
170 |
171 | b.Run("Fprintf", func(b *testing.B) {
172 | for i := 0; i < b.N; i++ {
173 | buf.Reset()
174 | Fprintf(&buf, format, args...)
175 | }
176 | })
177 |
178 | b.Run("Fmt+Write", func(b *testing.B) {
179 | for i := 0; i < b.N; i++ {
180 | buf.Reset()
181 | result := Fmt(format, args...)
182 | buf.WriteString(result)
183 | }
184 | })
185 | }
186 |
--------------------------------------------------------------------------------
/fmt_custom_type_test.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import "testing"
4 |
5 | // customType simula un tipo personalizado como pdfVersion en fpdf
6 | type customType string
7 |
8 | func (c customType) String() string {
9 | return string(c)
10 | }
11 |
12 | // customInt simula un tipo numérico personalizado con String()
13 | type customInt int
14 |
15 | func (c customInt) String() string {
16 | return Convert(int(c)).String()
17 | }
18 |
19 | // TestFmtWithCustomTypeString verifica que Fmt maneje correctamente tipos personalizados con método String()
20 | func TestFmtWithCustomTypeString(t *testing.T) {
21 | tests := []struct {
22 | name string
23 | format string
24 | args []any
25 | expected string
26 | bug bool // true si este caso representa el bug actual
27 | }{
28 | {
29 | name: "string literal works",
30 | format: "Value: %s",
31 | args: []any{"hello"},
32 | expected: "Value: hello",
33 | bug: false,
34 | },
35 | {
36 | name: "custom string type with String() - BUG",
37 | format: "Version: %s",
38 | args: []any{customType("1.3")},
39 | expected: "Version: 1.3",
40 | bug: true, // Este es el bug - actualmente devuelve ""
41 | },
42 | {
43 | name: "custom int type with String() - BUG",
44 | format: "Count: %s",
45 | args: []any{customInt(42)},
46 | expected: "Count: 42",
47 | bug: true, // Este es el bug - actualmente devuelve ""
48 | },
49 | {
50 | name: "PDF version format - BUG (real world case)",
51 | format: "%%PDF-%s",
52 | args: []any{customType("1.4")},
53 | expected: "%PDF-1.4",
54 | bug: true, // Este es exactamente el problema de fpdf
55 | },
56 | {
57 | name: "multiple custom types - BUG",
58 | format: "%s version %s",
59 | args: []any{customType("PDF"), customType("1.5")},
60 | expected: "PDF version 1.5",
61 | bug: true,
62 | },
63 | }
64 |
65 | for _, tt := range tests {
66 | t.Run(tt.name, func(t *testing.T) {
67 | result := Fmt(tt.format, tt.args...)
68 |
69 | if tt.bug {
70 | // Para casos con bug, verificamos el comportamiento ACTUAL (incorrecto)
71 | if result == tt.expected {
72 | t.Logf("✅ BUG FIXED: Expected %q, got %q (bug was fixed!)", tt.expected, result)
73 | } else {
74 | t.Logf("🐛 BUG CONFIRMED: Expected %q, got %q (bug still exists)", tt.expected, result)
75 | // No fallamos el test porque sabemos que es un bug conocido
76 | // Esto permite que el test pase mientras documentamos el bug
77 | }
78 | } else {
79 | // Para casos sin bug, verificamos que funcione correctamente
80 | if result != tt.expected {
81 | t.Errorf("Expected %q, got %q", tt.expected, result)
82 | }
83 | }
84 | })
85 | }
86 | }
87 |
88 | // TestFmtCustomTypeWithOtherFormats verifica que otros formatos (%d, %v) funcionen con custom types
89 | func TestFmtCustomTypeWithOtherFormats(t *testing.T) {
90 | tests := []struct {
91 | name string
92 | format string
93 | args []any
94 | expected string
95 | }{
96 | {
97 | name: "custom int with %d",
98 | format: "Count: %d",
99 | args: []any{customInt(42)},
100 | expected: "Count: 42",
101 | },
102 | {
103 | name: "custom type with %v (should work)",
104 | format: "Version: %v",
105 | args: []any{customType("1.3")},
106 | expected: "Version: 1.3",
107 | },
108 | }
109 |
110 | for _, tt := range tests {
111 | t.Run(tt.name, func(t *testing.T) {
112 | result := Fmt(tt.format, tt.args...)
113 | if result != tt.expected {
114 | t.Errorf("Expected %q, got %q", tt.expected, result)
115 | } else {
116 | t.Logf("✅ Working: %q", result)
117 | }
118 | })
119 | }
120 | }
121 |
122 | // TestFmtStringerInterface verifica comportamiento con diferentes tipos que implementan String()
123 | func TestFmtStringerInterface(t *testing.T) {
124 | type version struct {
125 | major int
126 | minor int
127 | }
128 |
129 | // Implementar String() para version dentro del test
130 | var _ = func(v version) string {
131 | return Fmt("%d.%d", v.major, v.minor)
132 | }
133 |
134 | // version con método String()
135 | v := version{1, 4}
136 |
137 | // Intentar formatear con %v (debería funcionar si AnyToBuff maneja String())
138 | result := Fmt("PDF Version: %v", v)
139 | t.Logf("Result with %%v: %q", result)
140 |
141 | // Intentar formatear con %s (actualmente fallará)
142 | result2 := Fmt("PDF Version: %s", v)
143 | t.Logf("Result with %%s: %q", result2)
144 |
145 | if result2 == "" {
146 | t.Log("BUG CONFIRMED: percentage-s format doesn't work with custom types (returns empty string)")
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/benchmark/phase13-analysis.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # fmt Phase 13 Memory Analysis Script
4 | # Automated analysis for allocation optimization progress
5 |
6 | set -e
7 |
8 | echo "🎯 fmt Phase 13 - Performance Recovery Analysis"
9 | echo "=================================================="
10 |
11 | WORKSPACE_DIR="/c/Users/Cesar/Packages/Internal/tinystring"
12 | BENCHMARK_DIR="$WORKSPACE_DIR/benchmark"
13 | RESULTS_DIR="$BENCHMARK_DIR/phase13-analysis"
14 |
15 | # Create results directory
16 | mkdir -p "$RESULTS_DIR"
17 |
18 | cd "$WORKSPACE_DIR"
19 |
20 | echo "📊 Step 1: Baseline Performance Measurement"
21 | echo "-------------------------------------------"
22 |
23 | # Run benchmarks with memory profiling
24 | echo "Running memory allocation benchmarks..."
25 | go test -bench=BenchmarkStringOperations -benchmem -memprofile="$RESULTS_DIR/memory_baseline.prof" > "$RESULTS_DIR/benchmark_baseline.txt" 2>&1
26 |
27 | echo "Running number processing benchmarks..."
28 | go test -bench=BenchmarkNumberProcessing -benchmem -memprofile="$RESULTS_DIR/memory_numbers.prof" >> "$RESULTS_DIR/benchmark_baseline.txt" 2>&1
29 |
30 | echo "Running builder operations benchmarks..."
31 | go test -bench=BenchmarkBuilderOperations -benchmem >> "$RESULTS_DIR/benchmark_baseline.txt" 2>&1
32 |
33 | echo "🔍 Step 2: Memory Allocation Analysis"
34 | echo "-------------------------------------"
35 |
36 | # Analyze memory allocations with pprof
37 | echo "Analyzing memory hotspots..."
38 | go tool pprof -text "$RESULTS_DIR/memory_baseline.prof" | head -30 > "$RESULTS_DIR/memory_hotspots.txt"
39 |
40 | echo "Top allocation sources:"
41 | cat "$RESULTS_DIR/memory_hotspots.txt" | head -15
42 |
43 | echo "🔬 Step 3: Escape Analysis"
44 | echo "-------------------------"
45 |
46 | # Analyze what escapes to heap
47 | echo "Analyzing heap escapes..."
48 | go build -gcflags="-m=2" . 2>&1 | grep -E "(escapes|moved to heap|too large)" > "$RESULTS_DIR/escape_analysis.txt" || true
49 |
50 | echo "Top escape analysis results:"
51 | head -20 "$RESULTS_DIR/escape_analysis.txt"
52 |
53 | echo "🧪 Step 4: String Allocation Detection"
54 | echo "--------------------------------------"
55 |
56 | # Focus on string allocations specifically
57 | echo "Analyzing string allocations..."
58 | go build -gcflags="-m=3" . 2>&1 | grep -E "string.*allocation|GetString.*alloc|WrString.*alloc" > "$RESULTS_DIR/string_allocations.txt" || true
59 |
60 | if [ -s "$RESULTS_DIR/string_allocations.txt" ]; then
61 | echo "String allocation hotspots found:"
62 | cat "$RESULTS_DIR/string_allocations.txt"
63 | else
64 | echo "No explicit string allocation warnings found in escape analysis."
65 | fi
66 |
67 | echo "🎯 Step 5: Performance Baseline Summary"
68 | echo "---------------------------------------"
69 |
70 | # Extract key metrics from benchmark results
71 | echo "Extracting baseline metrics..."
72 |
73 | # Parse benchmark results for key metrics
74 | grep -E "BenchmarkStringOperations.*allocs/op|BenchmarkBuilderOperations.*allocs/op" "$RESULTS_DIR/benchmark_baseline.txt" | head -10 > "$RESULTS_DIR/baseline_metrics.txt"
75 |
76 | echo "Current Phase 12 Performance Baseline:"
77 | echo "======================================"
78 | cat "$RESULTS_DIR/baseline_metrics.txt"
79 |
80 | # Calculate averages (simplified)
81 | TOTAL_MEMORY=$(grep -oE "[0-9]+ B/op" "$RESULTS_DIR/baseline_metrics.txt" | grep -oE "[0-9]+" | awk '{sum+=$1; count++} END {if(count>0) printf "%.0f", sum/count}')
82 | TOTAL_ALLOCS=$(grep -oE "[0-9]+ allocs/op" "$RESULTS_DIR/baseline_metrics.txt" | grep -oE "[0-9]+" | awk '{sum+=$1; count++} END {if(count>0) printf "%.0f", sum/count}')
83 |
84 | echo ""
85 | echo "📈 Baseline Averages:"
86 | echo "Memory/op: ${TOTAL_MEMORY:-"N/A"} B"
87 | echo "Allocs/op: ${TOTAL_ALLOCS:-"N/A"}"
88 |
89 | echo ""
90 | echo "🎯 Phase 13 Targets:"
91 | echo "Memory/op: 600 B (Target: 20% improvement)"
92 | echo "Allocs/op: 38 (Target: 32% improvement)"
93 | echo "Speed: 2900 ns/op (Target: 11% improvement)"
94 |
95 | echo ""
96 | echo "📁 Analysis files generated in: $RESULTS_DIR"
97 | echo " - benchmark_baseline.txt (Full benchmark results)"
98 | echo " - memory_hotspots.txt (Memory allocation analysis)"
99 | echo " - escape_analysis.txt (Heap escape analysis)"
100 | echo " - string_allocations.txt (String-specific allocations)"
101 | echo " - baseline_metrics.txt (Key performance metrics)"
102 |
103 | echo ""
104 | echo "🔧 Next Steps:"
105 | echo "1. Review memory_hotspots.txt for optimization targets"
106 | echo "2. Implement Stage 1: String Caching optimization"
107 | echo "3. Re-run this script after each optimization stage"
108 | echo "4. Compare results with: benchstat baseline.txt optimized.txt"
109 |
110 | echo ""
111 | echo "✅ Phase 13 baseline analysis complete!"
112 |
--------------------------------------------------------------------------------
/benchmark/clean-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # clean-all.sh - Clean all benchmark artifacts and temporary files
4 | # This script removes generated binaries, test artifacts, and build cache
5 |
6 | set -e
7 |
8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 | cd "$SCRIPT_DIR"
10 |
11 | echo "🧹 Cleaning fmt Benchmark Artifacts"
12 | echo "==========================================="
13 |
14 | # Function to clean binary artifacts
15 | clean_binary_artifacts() {
16 | echo "📋 Cleaning binary size benchmark artifacts..."
17 |
18 | if [[ -d "bench-binary-size" ]]; then
19 | # Clean binary files in subdirectories
20 | find bench-binary-size -type f \( -name "standard*" -o -name "tinystring*" \) ! -name "*.go" ! -name "*.mod" -exec rm -f {} \;
21 |
22 | # Clean WASM files specifically
23 | find bench-binary-size -name "*.wasm" -exec rm -f {} \;
24 |
25 | echo "✅ Binary artifacts cleaned"
26 | else
27 | echo "ℹ️ Binary benchmark directory not found, skipping"
28 | fi
29 | }
30 |
31 | # Function to clean memory benchmark artifacts
32 | clean_memory_artifacts() {
33 | echo "📋 Cleaning memory benchmark artifacts..."
34 |
35 | if [[ -d "bench-memory-alloc" ]]; then
36 | # Clean test cache and binaries
37 | find bench-memory-alloc -name "*.test" -exec rm -f {} \;
38 | find bench-memory-alloc -name "*.out" -exec rm -f {} \;
39 |
40 | echo "✅ Memory benchmark artifacts cleaned"
41 | else
42 | echo "ℹ️ Memory benchmark directory not found, skipping"
43 | fi
44 | }
45 |
46 | # Function to clean build artifacts
47 | clean_build_artifacts() {
48 | echo "📋 Cleaning build artifacts..."
49 |
50 | # Remove analyzer binaries (both Unix and Windows versions)
51 | if [[ -f "analyzer" ]]; then
52 | rm -f analyzer
53 | echo "✅ Analyzer binary (Unix) removed"
54 | fi
55 |
56 | if [[ -f "analyzer.exe" ]]; then
57 | rm -f analyzer.exe
58 | echo "✅ Analyzer binary (Windows) removed"
59 | fi
60 |
61 | # Remove benchmark-test binary
62 | if [[ -f "benchmark-test" ]]; then
63 | rm -f benchmark-test
64 | echo "✅ Benchmark-test binary removed"
65 | fi
66 |
67 | # Clean Go build cache for this module
68 | go clean -cache -testcache
69 | echo "✅ Go build cache cleaned"
70 | }
71 |
72 | # Function to clean temporary files
73 | clean_temp_files() {
74 | echo "📋 Cleaning temporary files..."
75 |
76 | # Remove temporary README files
77 | find . -name "*.tmp" -exec rm -f {} \;
78 | find . -name "README.md.tmp" -exec rm -f {} \;
79 |
80 | # Remove log files
81 | find . -name "*.log" -exec rm -f {} \;
82 |
83 | # Remove backup files
84 | find . -name "*~" -exec rm -f {} \;
85 | find . -name "*.bak" -exec rm -f {} \;
86 |
87 | echo "✅ Temporary files cleaned"
88 | }
89 |
90 | # Function to clean memory tool artifacts (legacy)
91 | clean_legacy_artifacts() {
92 | echo "📋 Cleaning legacy artifacts..."
93 |
94 | if [[ -d "memory-tool" ]]; then
95 | find memory-tool -name "*.test" -exec rm -f {} \;
96 | find memory-tool -name "*.out" -exec rm -f {} \;
97 | echo "✅ Legacy memory-tool artifacts cleaned"
98 | fi
99 | }
100 |
101 | # Parse command line arguments
102 | case "${1:-all}" in
103 | "binary")
104 | clean_binary_artifacts
105 | ;;
106 | "memory")
107 | clean_memory_artifacts
108 | ;;
109 | "build")
110 | clean_build_artifacts
111 | ;;
112 | "temp")
113 | clean_temp_files
114 | ;;
115 | "all")
116 | clean_binary_artifacts
117 | clean_memory_artifacts
118 | clean_build_artifacts
119 | clean_temp_files
120 | clean_legacy_artifacts
121 | ;;
122 | "help"|"-h"|"--help")
123 | echo "Usage: $0 [binary|memory|build|temp|all|help]"
124 | echo ""
125 | echo "Commands:"
126 | echo " binary - Clean only binary size artifacts"
127 | echo " memory - Clean only memory benchmark artifacts"
128 | echo " build - Clean only build artifacts (binaries, cache)"
129 | echo " temp - Clean only temporary files (logs, backups)"
130 | echo " all - Clean everything (default)"
131 | echo " help - Show this help message"
132 | echo ""
133 | echo "Note: This will remove all generated binaries and test results."
134 | echo "You'll need to rebuild using build-and-measure.sh and run-all-benchmarks.sh"
135 | exit 0
136 | ;;
137 | *)
138 | echo "❌ Unknown command: $1"
139 | echo "Use '$0 help' for usage information"
140 | exit 1
141 | ;;
142 | esac
143 |
144 | echo "✅ Cleanup completed successfully!"
145 | echo "==========================================="
146 |
--------------------------------------------------------------------------------
/fmt_number.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // =============================================================================
4 | // FORMAT NUMBER OPERATIONS - Number formatting with separators and display
5 | // =============================================================================
6 |
7 | // Thousands formats the number with thousand separators.
8 | // By default (no param), uses EU style: 1.234.567,89
9 | // If anglo is true, uses Anglo style: 1,234,567.89
10 | func (t *Conv) Thousands(anglo ...bool) *Conv {
11 | if t.hasContent(BuffErr) {
12 | return t
13 | }
14 |
15 | useAnglo := false
16 | if len(anglo) > 0 && anglo[0] {
17 | useAnglo = true
18 | }
19 |
20 | if t.hasContent(BuffOut) {
21 | str := t.GetString(BuffOut)
22 | if t.isNumericString(str) {
23 | // Store string in buffer and use parseFloatBase
24 | t.ResetBuffer(BuffOut)
25 | t.WrString(BuffOut, str)
26 | floatVal := t.parseFloatBase()
27 | if !t.hasContent(BuffErr) {
28 | t.ResetBuffer(BuffOut)
29 | if floatVal == float64(int64(floatVal)) {
30 | t.wrIntBase(BuffOut, int64(floatVal), 10, true)
31 | } else {
32 | t.wrFloat64(BuffOut, floatVal)
33 | t.removeTrailingZeros(BuffOut)
34 | }
35 | }
36 | t.addThousandSeparatorsCustom(BuffOut, useAnglo)
37 | }
38 | return t
39 | }
40 | return t
41 | }
42 |
43 | // addThousandSeparatorsCustom adds thousand separators to the numeric string in buffer.
44 | // If anglo is true: 1,234,567.89; if false: 1.234.567,89
45 | func (c *Conv) addThousandSeparatorsCustom(dest BuffDest, anglo bool) {
46 | str := c.GetString(dest)
47 | if len(str) <= 3 {
48 | return
49 | }
50 |
51 | // Find decimal point if it exists
52 | dotIndex := -1
53 | for i, char := range str {
54 | if char == '.' {
55 | dotIndex = i
56 | break
57 | }
58 | }
59 |
60 | intPart := str
61 | decPart := ""
62 | if dotIndex != -1 {
63 | intPart = str[:dotIndex]
64 | decPart = str[dotIndex+1:]
65 | }
66 |
67 | intLen := len(intPart)
68 | if intPart[0] == '-' {
69 | if intLen <= 4 {
70 | return
71 | }
72 | } else {
73 | if intLen <= 3 {
74 | return
75 | }
76 | }
77 |
78 | c.ResetBuffer(dest)
79 | start := 0
80 | if intPart[0] == '-' {
81 | c.wrByte(dest, '-')
82 | start = 1
83 | }
84 |
85 | remainingDigits := intLen - start
86 | firstGroupSize := remainingDigits % 3
87 | if firstGroupSize == 0 {
88 | firstGroupSize = 3
89 | }
90 |
91 | for i := start; i < start+firstGroupSize; i++ {
92 | c.wrByte(dest, intPart[i])
93 | }
94 |
95 | sep := byte('.')
96 | if anglo {
97 | sep = ','
98 | }
99 |
100 | pos := start + firstGroupSize
101 | for pos < intLen {
102 | c.wrByte(dest, sep)
103 | for i := 0; i < 3 && pos < intLen; i++ {
104 | c.wrByte(dest, intPart[pos])
105 | pos++
106 | }
107 | }
108 |
109 | // Add decimal part if it exists
110 | if decPart != "" {
111 | if anglo {
112 | c.wrByte(dest, '.')
113 | c.WrString(dest, decPart)
114 | } else {
115 | c.wrByte(dest, ',')
116 | c.WrString(dest, decPart)
117 | }
118 | }
119 | }
120 |
121 | // removeTrailingZeros removes trailing zeros from decimal numbers in buffer
122 | // Universal method with dest-first parameter order - follows buffer API architecture
123 | func (c *Conv) removeTrailingZeros(dest BuffDest) {
124 | str := c.GetString(dest)
125 | if len(str) == 0 {
126 | return
127 | }
128 |
129 | // Find decimal point
130 | dotIndex := -1
131 | for i := 0; i < len(str); i++ {
132 | if str[i] == '.' {
133 | dotIndex = i
134 | break
135 | }
136 | }
137 |
138 | if dotIndex == -1 {
139 | return // No decimal point
140 | }
141 |
142 | // Find last non-zero digit
143 | lastNonZero := len(str) - 1
144 | for i := len(str) - 1; i > dotIndex; i-- {
145 | if str[i] != '0' {
146 | lastNonZero = i
147 | break
148 | }
149 | }
150 |
151 | // Remove trailing zeros (and decimal point if all zeros)
152 | var result string
153 | if lastNonZero == dotIndex {
154 | result = str[:dotIndex] // Remove decimal point too
155 | } else {
156 | result = str[:lastNonZero+1]
157 | }
158 |
159 | c.ResetBuffer(dest)
160 | c.WrString(dest, result)
161 | }
162 |
163 | // isNumericString checks if a string represents a valid number
164 | // Universal helper method - follows buffer API architecture
165 | func (c *Conv) isNumericString(str string) bool {
166 | if len(str) == 0 {
167 | return false
168 | }
169 |
170 | i := 0
171 | // Handle sign
172 | if str[0] == '-' || str[0] == '+' {
173 | i = 1
174 | if i >= len(str) {
175 | return false // Just a sign is not a number
176 | }
177 | }
178 |
179 | hasDigit := false
180 | hasDecimal := false
181 |
182 | for ; i < len(str); i++ {
183 | if str[i] >= '0' && str[i] <= '9' {
184 | hasDigit = true
185 | } else if str[i] == '.' && !hasDecimal {
186 | hasDecimal = true
187 | } else {
188 | return false // Invalid character
189 | }
190 | }
191 |
192 | return hasDigit // Must have at least one digit
193 | }
194 |
--------------------------------------------------------------------------------
/docs/TRANSLATE.md:
--------------------------------------------------------------------------------
1 | # 🌍 fmt: Multilingual Message System
2 |
3 | **fmt** is a lightweight, dependency-free multilingual dictionary for generating composable error and validation messages. It supports 9 major languages:
4 |
5 | **Supported Languages:**
6 |
7 | - 🇺🇸 EN (English, default)
8 | - 🇪🇸 ES (Spanish)
9 | - 🇨🇳 ZH (Chinese)
10 | - 🇮🇳 HI (Hindi)
11 | - 🇸🇦 AR (Arabic)
12 | - 🇧🇷 PT (Portuguese)
13 | - 🇫🇷 FR (French)
14 | - 🇩🇪 DE (German)
15 | - 🇷🇺 RU (Russian)
16 |
17 | ---
18 |
19 | ## 🚀 Features
20 |
21 | - ✅ 9 Languages with 35+ essential terms
22 | - 🧱 Composable error messages from dictionary words
23 | - 🌐 Auto-detects system/browser language
24 | - 🛠️ Language override (global or inline)
25 | - 🧩 Custom dictionaries for domain-specific terms
26 | - 🔒 Zero external dependencies
27 | - ⚙️ Compatibility: Go + TinyGo (WASM ready)
28 |
29 |
30 | ---
31 |
32 | ## 🌍 Basic Usage
33 |
34 | ```go
35 | // Set global language to Spanish (using lang constant), returns "ES"
36 | code := OutLang(ES) // returns "ES"
37 | code = OutLang() // auto-detects and returns code (e.g. "EN")
38 | // If an error occurs or the language is not recognized, "EN" is always returned by default
39 |
40 | // Usage examples:
41 |
42 | // return strings
43 | // Force to Spanish (ES) only for this response, not globally.
44 | // Useful for personalized user replies.
45 | msg := Translate(ES, D.Format, D.Invalid).String()
46 | // → "formato inválido"
47 |
48 | // Capitalize translation (first letter of each word uppercase)
49 | msgCap := Translate(ES, D.Format, D.Invalid).Capitalize().String()
50 | // → "Formato Inválido"
51 |
52 | // Force French
53 | err = Err(FR, D.Empty, D.String)
54 | // → "vide chaîne" (forced French)
55 |
56 |
57 | // Use global language (e.g. Spanish) for error messages
58 | // return error
59 | err := Err(D.Format, D.Invalid)
60 | // → "formato inválido"
61 |
62 | err = Err(D.Number, D.Negative, D.Not, D.Supported)
63 | // → "número negativo no soportado"
64 |
65 | err = Err(D.Cannot, D.Round, D.Value, D.NonNumeric)
66 | // → "no se puede redondear valor no numérico"
67 | ```
68 |
69 |
70 | ---
71 |
72 | ## ⚡ Memory Management
73 |
74 | `Translate` returns a pooled `*Conv` object for high performance.
75 |
76 | - **Automatic Release**: Calling `.String()` or `.Apply()` automatically returns the object to the pool.
77 | - **Manual Release**: If you use `.Bytes()` or keep the object, you **MUST** call `.PutConv()` manually.
78 |
79 | ```go
80 | // ✅ Automatic release (Recommended)
81 | msg := Translate(D.Format).String()
82 |
83 | // ⚠️ Manual release required
84 | c := Translate(D.Format)
85 | bytes := c.Bytes()
86 | // ... use bytes ...
87 | c.PutConv() // Don't forget this!
88 | ```
89 |
90 | ### 🚀 Zero-Allocation Performance
91 |
92 | For hot paths requiring zero allocations, pass **pointers** to `LocStr`:
93 |
94 | ```go
95 | // Standard usage (1 alloc/op)
96 | msg := Translate(D.Format, D.Invalid).String()
97 |
98 | // Zero-allocation usage (0 allocs/op)
99 | msg := Translate(&D.Format, &D.Invalid).String()
100 | ```
101 |
102 | **Benchmark Results:**
103 | - `Translate(D.Format)`: 1 alloc/op, 144 B/op
104 | - `Translate(&D.Format)`: **0 allocs/op**, 0 B/op
105 |
106 | This optimization is useful when allocation-free operation is critical.
107 |
108 | ---
109 |
110 |
111 |
112 | ## 🌐 Minimal HTTP API Example
113 |
114 | ```go
115 | import (
116 | "encoding/json"
117 | "net/http"
118 | . "github.com/tinywasm/fmt"
119 | )
120 |
121 | func handler(w http.ResponseWriter, r *http.Request) {
122 | lang := r.URL.Query().Get("lang") // e.g. ?lang=ES
123 | resp := map[string]string{
124 | "error": Translate(lang, D.Format, D.Invalid).String(),
125 | }
126 | w.Header().Set("Content-Type", "application/json")
127 | json.NewEncoder(w).Encode(resp)
128 | }
129 | ```
130 |
131 | ## 🧩 Custom Dictionary
132 |
133 | Define domain-specific words:
134 |
135 | ```go
136 | type MyDict struct {
137 | User LocStr
138 | Email LocStr
139 | }
140 |
141 | var MD = MyDict{
142 | User: LocStr{"user", "usuario", "usuário", "utilisateur", "пользователь", "Benutzer", "utente", "उपयोगकर्ता", "用户"},
143 | Email: LocStr{"email", "correo", "email", "email", "البريد الإلكتروني", "Courriel", "Эл. адрес", "电邮", "ईमेल"},
144 | }
145 |
146 | // Usage with custom dictionary
147 | err := Err("es",D.Format, MD.Email, MD.User, D.Invalid)
148 | // → "formato correo usuario inválido"
149 | ```
150 |
151 | ---
152 |
153 | ## ✅ Validation Example
154 |
155 | ```go
156 | validate := func(input string) error {
157 | if input == "" {
158 | return Err(D.Empty, D.String, D.Not, D.Supported)
159 | }
160 | if _, err := Convert(input).Int(); err != nil {
161 | return Err(D.Invalid, D.Number, D.Format)
162 | }
163 | return nil
164 | }
165 | ```
166 |
167 | ---
168 |
169 | ## 🔍 Dictionary Reference
170 |
171 | See [`dictionary.go`](../dictionary.go) for built-in words.
172 | Combine `D.` (default terms) and custom dictionaries for flexible messaging.
173 |
174 |
175 |
--------------------------------------------------------------------------------
/docs/betterment/ISSUE_ISSUE_MEMORY_SUMMARY.md:
--------------------------------------------------------------------------------
1 | # 📊 fmt Phase 13 - Executive Summary
2 |
3 | ## 🎯 **Situación Actual y Objetivos**
4 |
5 | Basado en el análisis completo realizado el 23 de junio de 2025, se ha identificado el estado actual de rendimiento de fmt después de la Fase 12 (Corrección de Race Condition) y se ha desarrollado un plan específico de optimización de memoria para la Fase 13.
6 |
7 | ### **Estado Actual (Fase 12 - Post Race Condition Fix):**
8 | - ✅ **Thread Safety:** 100% libre de race conditions
9 | - ⚠️ **Memory:** 726 B/op promedio (17.5% mejor que stdlib pero degradado vs Fase 11)
10 | - ❌ **Allocations:** 31 allocs/op promedio, pero Replace=56 (peor caso crítico)
11 | - ⚠️ **Speed:** 1061-5885 ns/op (rango muy amplio, inconsistente)
12 |
13 | ## 🔍 **Problemas Críticos Identificados**
14 |
15 | ### **1. Allocaciones de String (CRÍTICO)**
16 | - **Problema:** `GetString()` crea allocaciones en heap en **CADA LLAMADA**
17 | - **Evidencia:** Escape analysis confirma `string(buffer[:length])` escapa en líneas 63, 68, 112
18 | - **Impacto:** 70% de las allocaciones de string vienen de esta función
19 |
20 | ### **2. Operación Replace (CRÍTICO)**
21 | - **Problema:** 56 allocs/op en Replace vs 8 allocs/op en Split
22 | - **Impacto:** Peor performance en biblioteca, 700% más allocaciones que mejor caso
23 |
24 | ### **3. Pool de Objetos (MODERADO)**
25 | - **Problema:** Inicialización de 3×64B slices al heap en cada Conv
26 | - **Evidencia:** Escape analysis líneas 9-11 confirma heap escape
27 | - **Impacto:** Overhead constante en todas las operaciones
28 |
29 | ## 🚀 **Plan de Optimización Fase 13**
30 |
31 | ### **Estrategia Prioritizada (5 Etapas):**
32 |
33 | **🏆 PRIORIDAD 1: String Caching**
34 | - Implementar caché de strings con `unsafe.String()`
35 | - Eliminar `string(buffer[:length])` allocations
36 | - **Target:** -70% allocaciones de string
37 |
38 | **🎯 PRIORIDAD 2: Replace Algorithm**
39 | - Optimizar algoritmo Replace con pre-allocación
40 | - Reducir 56→28 allocs/op en Replace operations
41 | - **Target:** -50% allocaciones en Replace
42 |
43 | **⚡ PRIORIDAD 3: Pool Optimization**
44 | - Lazy initialization de buffers en Conv pool
45 | - Reducir overhead de inicialización
46 | - **Target:** -25% overhead de pool
47 |
48 | **🔧 PRIORIDAD 4: Type Conversion Fast Path**
49 | - Fast path para tipos comunes (string, int)
50 | - Minimizar any storage
51 | - **Target:** -40% overhead de conversión
52 |
53 | **📈 PRIORIDAD 5: Buffer Growth Management**
54 | - Smart pre-allocation con cache invalidation
55 | - Prevenir reallocaciones futuras
56 | - **Target:** -20% reallocaciones de buffer
57 |
58 | ## 📊 **Objetivos Cuantificados**
59 |
60 | ### **Metas de Performance:**
61 | | Métrica | Actual | Objetivo | Mejora | vs Go Stdlib |
62 | |---------|--------|----------|--------|--------------|
63 | | **Memory** | 726 B/op | **580 B/op** | **-20%** | **36% mejor** |
64 | | **Allocs** | 31 avg/56 max | **25 avg/30 max** | **-19%** | **40% mejor** |
65 | | **Speed** | 3280 ns/op | **2800 ns/op** | **-15%** | **16% más lento** |
66 |
67 | ### **Criterios de Éxito:**
68 | - ✅ Replace < 30 allocs/op (actualmente 56)
69 | - ✅ Eliminación total de heap escapes en GetString()
70 | - ✅ Todas las operaciones < 1000 B/op
71 | - ✅ Mantener 100% thread safety
72 |
73 | ## 🛠️ **Metodología de Implementación**
74 |
75 | ### **Proceso por Etapas:**
76 | 1. **Análisis baseline** con profiling detallado ✅ **COMPLETADO**
77 | 2. **Implementación incremental** (una optimización por vez)
78 | 3. **Validación continua** (tests + race detection + benchmarks)
79 | 4. **Medición de impacto** (benchstat comparisons)
80 | 5. **Documentación de resultados**
81 |
82 | ### **Herramientas de Monitoreo:**
83 | - Escape analysis: `go build -gcflags="-m=3"`
84 | - Memory profiling: `go test -benchmem -memprofile`
85 | - Race detection: `go test -race ./...`
86 | - Performance tracking: `benchstat` comparisons
87 |
88 | ## 🎯 **Timeline y Riesgos**
89 |
90 | ### **Cronograma Estimado:**
91 | - **Semana 1:** String Caching + Replace optimization
92 | - **Semana 2:** Pool optimization + Type conversion fast path
93 | - **Semana 3:** Buffer growth management + testing
94 | - **Semana 4:** Validation + documentation + performance recovery verification
95 |
96 | ### **Nivel de Riesgo:** 🟡 **MEDIO**
97 | - **Mitigación:** Implementación incremental con rollback capability
98 | - **Restricción:** Mantener thread safety absoluta (sin excepciones)
99 | - **Compatibilidad:** Zero breaking changes en API
100 |
101 | ## ✅ **Estado de Preparación**
102 |
103 | **LISTO PARA IMPLEMENTACIÓN:**
104 | - ✅ Baseline establecido con datos concretos
105 | - ✅ Problemas específicos identificados con evidencia
106 | - ✅ Soluciones técnicas validadas para WebAssembly/TinyGo
107 | - ✅ Herramientas de análisis configuradas
108 | - ✅ Criterios de éxito cuantificados
109 | - ✅ Proceso de validación definido
110 |
111 | **PRÓXIMO PASO:** Implementar Prioridad 1 (String Caching) con validación inmediata.
112 |
113 | ---
114 | **Documento completo:** `ISSUE_MEMORY_OPTIMIZATION_PHASE13.md`
115 | **Análisis de datos:** `benchmark/phase13-analysis/`
116 | **Metodología:** Basada en evidencia empírica y herramientas de profiling de Go
117 |
--------------------------------------------------------------------------------
/num_float.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | import "reflect"
4 |
5 | // =============================================================================
6 | // FLOAT OPERATIONS - All float parsing, conversion and formatting
7 | // =============================================================================
8 |
9 | // Float64 converts the value to a float64.
10 | // Returns the converted float64 and any error that occurred during conversion.
11 | func (c *Conv) Float64() (float64, error) {
12 | val := c.parseFloatBase()
13 | if c.hasContent(BuffErr) {
14 | return 0, c
15 | }
16 | return val, nil
17 | }
18 |
19 | // toFloat64 converts various float types to float64
20 | func (c *Conv) toFloat64(arg any) (float64, bool) {
21 | switch v := arg.(type) {
22 | case float64:
23 | return v, true
24 | case float32:
25 | return float64(v), true
26 | default:
27 | // Try reflection for custom types (e.g., type customFloat float64)
28 | return c.toFloat64Reflect(arg)
29 | }
30 | }
31 |
32 | // toFloat64Reflect uses reflection to extract float64 from custom types
33 | func (c *Conv) toFloat64Reflect(arg any) (float64, bool) {
34 | rv := reflect.ValueOf(arg)
35 | switch rv.Kind() {
36 | case reflect.Float32, reflect.Float64:
37 | return rv.Float(), true
38 | default:
39 | return 0, false
40 | }
41 | }
42 |
43 | // Float32 converts the value to a float32.
44 | // Returns the converted float32 and any error that occurred during conversion.
45 | func (c *Conv) Float32() (float32, error) {
46 | val := c.parseFloatBase()
47 | if c.hasContent(BuffErr) {
48 | return 0, c
49 | }
50 | if val > 3.4028235e+38 {
51 | return 0, c.wrErr(D.Number, D.Overflow)
52 | }
53 | return float32(val), nil
54 | }
55 |
56 | // parseFloatBase parses the buffer as a float64, similar to parseIntBase for ints.
57 | // It always uses the buffer output and handles errors internally.
58 | func (c *Conv) parseFloatBase() float64 {
59 | c.ResetBuffer(BuffErr)
60 |
61 | s := c.GetString(BuffOut)
62 | if len(s) == 0 {
63 | c.wrErr(D.String, D.Empty)
64 | return 0
65 | }
66 |
67 | var result float64
68 | var negative bool
69 | var hasDecimal bool
70 | var decimalPlaces int
71 | i := 0
72 |
73 | // Handle sign
74 | switch s[0] {
75 | case '-':
76 | negative = true
77 | i = 1
78 | if len(s) == 1 {
79 | c.wrErr(D.Format, D.Invalid)
80 | return 0
81 | }
82 | case '+':
83 | i = 1
84 | if len(s) == 1 {
85 | c.wrErr(D.Format, D.Invalid)
86 | return 0
87 | }
88 | }
89 |
90 | // Parse integer part
91 | for ; i < len(s) && s[i] != '.'; i++ {
92 | if s[i] < '0' || s[i] > '9' {
93 | c.wrErr(D.Character, D.Invalid)
94 | return 0
95 | }
96 | result = result*10 + float64(s[i]-'0')
97 | }
98 |
99 | // Parse decimal part if present
100 | if i < len(s) && s[i] == '.' {
101 | hasDecimal = true
102 | i++ // Skip decimal point
103 | for ; i < len(s); i++ {
104 | if s[i] < '0' || s[i] > '9' {
105 | c.wrErr(D.Character, D.Invalid)
106 | return 0
107 | }
108 | decimalPlaces++
109 | result = result*10 + float64(s[i]-'0')
110 | }
111 | }
112 |
113 | // Apply decimal places
114 | if hasDecimal {
115 | for j := 0; j < decimalPlaces; j++ {
116 | result /= 10
117 | }
118 | }
119 |
120 | if negative {
121 | result = -result
122 | }
123 |
124 | return result
125 | }
126 |
127 | // wrFloat32 writes a float32 to the buffer destination.
128 | func (c *Conv) wrFloat32(dest BuffDest, val float32) {
129 | c.wrFloatBase(dest, float64(val), 3.4028235e+38)
130 | }
131 |
132 | // wrFloat64 writes a float64 to the buffer destination.
133 | func (c *Conv) wrFloat64(dest BuffDest, val float64) {
134 | c.wrFloatBase(dest, float64(val), 1.7976931348623157e+308)
135 | }
136 |
137 | // wrFloatBase contains the shared logic for writing float values.
138 | func (c *Conv) wrFloatBase(dest BuffDest, val float64, maxInf float64) {
139 | // Handle special cases
140 | if val != val { // NaN
141 | c.WrString(dest, "NaN")
142 | return
143 | }
144 | if val == 0 {
145 | c.WrString(dest, "0")
146 | return
147 | }
148 |
149 | // Handle infinity
150 | if val > maxInf {
151 | c.WrString(dest, "+Inf")
152 | return
153 | }
154 | if val < -maxInf {
155 | c.WrString(dest, "-Inf")
156 | return
157 | }
158 |
159 | // Handle negative numbers
160 | negative := val < 0
161 | if negative {
162 | c.WrString(dest, "-")
163 | val = -val
164 | }
165 |
166 | // Check if it's effectively an integer
167 | if val < 1e15 && val == float64(int64(val)) {
168 | c.wrIntBase(dest, int64(val), 10, false)
169 | return
170 | }
171 |
172 | // For numbers with decimal places, use a precision-limited approach
173 | // Round to 6 decimal places to avoid precision issues
174 | scaled := val * 1000000
175 | rounded := int64(scaled + 0.5)
176 |
177 | intPart := rounded / 1000000
178 | fracPart := rounded % 1000000
179 |
180 | // Write integer part
181 | c.wrIntBase(dest, intPart, 10, false)
182 |
183 | // Write fractional part if non-zero
184 | if fracPart > 0 {
185 | c.WrString(dest, ".")
186 |
187 | // Build fractional string using local array to avoid buffer conflicts
188 | var digits [6]byte
189 | temp := fracPart
190 | for i := 0; i < 6; i++ {
191 | digits[i] = byte(temp%10) + '0'
192 | temp /= 10
193 | }
194 |
195 | // Find the start position (skip leading zeros in the array)
196 | start := 0
197 | for start < 6 && digits[start] == '0' {
198 | start++
199 | }
200 |
201 | // Write digits in reverse order (correct order), skipping leading zeros
202 | if start < 6 {
203 | for i := 5; i >= start; i-- {
204 | c.wrByte(dest, digits[i])
205 | }
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/docs/issues/REM_GET_STRING.md:
--------------------------------------------------------------------------------
1 | # MEMORY OPTIMIZATION REPORT: GetString() Usage Analysis
2 |
3 | ## EXECUTIVE SUMMARY
4 | The tinystring library has extensive usage of `GetString()` across multiple files, causing unnecessary memory allocations through `[]byte` to `string` conversions. This report identifies optimization opportunities to eliminate these allocations, following the pattern established in `capitalizeASCIIOptimized()` and the optimized `Quote()` method.
5 |
6 | ## METHODOLOGY
7 | - Searched for all `GetString()` usages across the codebase
8 | - Analyzed each usage context and optimization potential
9 | - Classified into optimization categories based on feasibility
10 |
11 | ## FINDINGS
12 | - **Total `GetString()` calls found:** 33 instances across 11 files
13 | - **Optimization candidates:** 28 high-impact cases
14 |
15 | ## FILES ANALYZED
16 |
17 | ### split.go (3 instances)
18 | - Line 10: `src := c.GetString(BuffOut)` — OPTIMIZE: Direct buffer access
19 | - Line 50: `out = append(out, c.GetString(BuffWork))` — OPTIMIZE: Use getBytes()
20 | - Line 97: `before = c.GetString(BuffWork)` — OPTIMIZE: Direct buffer access
21 |
22 | ### error.go (3 instances)
23 | - Line 36: `out = c.GetString(BuffOut)` — OPTIMIZE: Direct buffer access
24 | - Line 88: Debug comment — SKIP: Debug code
25 | - Line 96: `return c.GetString(BuffErr)` — KEEP: Public API return
26 |
27 | ### fmt_template.go (7 instances)
28 | - Multiple lines: `str = c.GetString(BuffWork)` — OPTIMIZE: Direct buffer processing
29 |
30 | ### convert.go (2 instances)
31 | - Line 190: `*strPtr = t.GetString(BuffOut)` — KEEP: External pointer assignment
32 | - Line 206: `out := c.GetString(BuffOut)` — OPTIMIZE: Direct buffer access
33 |
34 | ### replace.go (6 instances)
35 | - Several lines: `str := c.GetString(BuffOut)` — HIGH PRIORITY: String iteration
36 | - Several lines: `old := c.GetString(BuffWork)` — OPTIMIZE: Direct buffer comparison
37 | - Several lines: `newStr := c.GetString(BuffWork)` — OPTIMIZE: Direct buffer processing
38 |
39 | ### join.go (2 instances)
40 | - Line 31: `str := c.GetString(BuffOut)` — HIGH PRIORITY: String iteration
41 |
42 | ### repeat.go (2 instances)
43 | - Line 17: `str := t.GetString(BuffOut)` — HIGH PRIORITY: String iteration
44 |
45 | ### truncate.go (2 instances)
46 | - Line 84: `Conv := t.GetString(BuffOut)` — HIGH PRIORITY: String iteration
47 | - Line 139: `if len(t.GetString(BuffOut)) == 0` — OPTIMIZE: Use `c.outLen == 0`
48 |
49 | ### capitalize.go (4 instances)
50 | - Line 66: `str := t.GetString(BuffOut)` — OPTIMIZED: Has ASCII fast path already
51 | - Line 92: `result := t.GetString(BuffWork)` — OPTIMIZE: Direct buffer swap
52 | - Line 166: `str := t.GetString(dest)` — OPTIMIZE: Direct buffer access
53 | - Line 235: `str := t.GetString(BuffOut)` — HIGH PRIORITY: String iteration
54 |
55 | ## OPTIMIZATION STRATEGIES
56 |
57 | ### PATTERN 1: ASCII-First Optimization (Recommended for HIGH PRIORITY cases)
58 | ```go
59 | // Before:
60 | str := c.GetString(BuffOut)
61 | for _, char := range str { ... }
62 |
63 | // After:
64 | for i := 0; i < c.outLen; i++ {
65 | char := c.out[i]
66 | // ... process byte directly
67 | }
68 | ```
69 |
70 | ### PATTERN 2: Length Check Optimization
71 | ```go
72 | // Before:
73 | if len(c.GetString(BuffOut)) == 0
74 |
75 | // After:
76 | if c.outLen == 0
77 | ```
78 |
79 | ### PATTERN 3: Buffer-to-Buffer Operations
80 | ```go
81 | // Before:
82 | result := c.GetString(BuffWork)
83 | c.ResetBuffer(BuffOut)
84 | c.WrString(BuffOut, result)
85 |
86 | // After:
87 | c.swapBuff(BuffWork, BuffOut)
88 | ```
89 |
90 | ### PATTERN 4: Direct Byte Processing
91 | ```go
92 | // Before:
93 | str := c.GetString(BuffWork)
94 | // ... string processing
95 |
96 | // After:
97 | data := c.work[:c.workLen]
98 | // ... byte processing
99 | ```
100 |
101 | ## IMPLEMENTATION PRIORITY
102 | - **HIGH PRIORITY (String iteration loops):**
103 | - replace.go: Lines 13, 83, 100, 117
104 | - join.go: Line 31
105 | - repeat.go: Line 17
106 | - truncate.go: Line 84
107 | - capitalize.go: Line 235
108 | - **MEDIUM PRIORITY (Buffer operations):**
109 | - fmt_template.go: All 7 instances
110 | - split.go: Lines 10, 50, 97
111 | - error.go: Line 36
112 | - convert.go: Line 206
113 | - capitalize.go: Lines 92, 166
114 | - **LOW PRIORITY (Length checks):**
115 | - truncate.go: Line 139
116 | - **SKIP (Required for API compatibility):**
117 | - error.go: Line 96
118 | - convert.go: Line 190
119 |
120 | ## ESTIMATED PERFORMANCE IMPACT
121 | - Memory allocations reduced: ~28 allocations per operation chain
122 | - Performance improvement: 15–40% faster for string processing operations
123 | - Memory pressure: Significantly reduced GC pressure
124 | - Compatibility: 100% backward compatible (internal optimizations only)
125 |
126 | ## IMPLEMENTATION NOTES
127 | 1. Follow the `capitalizeASCIIOptimized()` pattern for ASCII-first optimization
128 | 2. Use the `Quote()` method optimization as a reference implementation
129 | 3. Maintain Unicode compatibility with fallback paths when necessary
130 | 4. Test thoroughly with both ASCII and Unicode content
131 | 5. Preserve all public API behavior
132 |
133 | ## CONCLUSION
134 | Implementing these optimizations will eliminate the majority of unnecessary string allocations in the tinystring library, following the zero-allocation FastHTTP optimization patterns already established in `memory.go`. The changes are internal optimizations that maintain full API compatibility while significantly improving performance.
135 |
136 | ---
137 |
138 | This file serves as documentation for `GetString()` optimization opportunities across the tinystring library. No executable code is contained within.
139 |
--------------------------------------------------------------------------------
/mapping.go:
--------------------------------------------------------------------------------
1 | package fmt
2 |
3 | // Shared constants for maximum code reuse and minimal binary size
4 | const (
5 |
6 | // Common punctuation
7 | dotStr = "."
8 | spaceStr = " "
9 | ellipsisStr = "..."
10 | quoteStr = "\"\""
11 |
12 | // ASCII case conversion constant
13 | asciiCaseDiff = 32
14 | // Buffer capacity constants
15 | defaultBufCap = 16 // default buffer size
16 | )
17 |
18 | // Index-based character mapping for maximum efficiency
19 | var (
20 | // Accented characters (lowercase)
21 | aL = []rune{'á', 'à', 'ã', 'â', 'ä', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ó', 'ò', 'õ', 'ô', 'ö', 'ú', 'ù', 'û', 'ü', 'ý'}
22 | // Base characters (lowercase)
23 | bL = []rune{'a', 'a', 'a', 'a', 'a', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y'}
24 | // Accented characters (uppercase)
25 | aU = []rune{'Á', 'À', 'Ã', 'Â', 'Ä', 'É', 'È', 'Ê', 'Ë', 'Í', 'Ì', 'Î', 'Ï', 'Ó', 'Ò', 'Õ', 'Ô', 'Ö', 'Ú', 'Ù', 'Û', 'Ü', 'Ý'}
26 | // Base characters (uppercase)
27 | bU = []rune{'A', 'A', 'A', 'A', 'A', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'Y'}
28 | )
29 |
30 | // toUpperRune converts a single rune to uppercase using optimized lookup
31 | func toUpperRune(r rune) rune {
32 | // ASCII fast path
33 | if r >= 'a' && r <= 'z' {
34 | return r - asciiCaseDiff
35 | }
36 | // Accent conversion using index lookup
37 | for i, char := range aL {
38 | if r == char {
39 | return aU[i]
40 | }
41 | }
42 | return r
43 | }
44 |
45 | // toLowerRune converts a single rune to lowercase using optimized lookup
46 | func toLowerRune(r rune) rune {
47 | // ASCII fast path
48 | if r >= 'A' && r <= 'Z' {
49 | return r + asciiCaseDiff
50 | }
51 | // Accent conversion using index lookup
52 | for i, char := range aU {
53 | if r == char {
54 | return aL[i]
55 | }
56 | }
57 | return r
58 | }
59 |
60 | // Tilde removes accents and diacritics using index-based lookup
61 | // OPTIMIZED: Uses work buffer to eliminate temporary allocations
62 | func (t *Conv) Tilde() *Conv {
63 | // Check for error chain interruption
64 | if t.hasContent(BuffErr) {
65 | return t
66 | }
67 |
68 | if t.outLen == 0 {
69 | return t
70 | }
71 |
72 | // Use work buffer instead of temporary allocation
73 | t.ResetBuffer(BuffWork)
74 |
75 | // Fast path: ASCII-only optimization
76 | if t.isASCIIOnlyOut() {
77 | // For ASCII, just copy the buffer (no accent processing needed)
78 | t.work = append(t.work[:0], t.out[:t.outLen]...)
79 | t.workLen = t.outLen
80 |
81 | } else {
82 | // Unicode path: process accents using work buffer
83 | t.tildeUnicodeOptimized()
84 | }
85 |
86 | // Swap work buffer to out buffer (zero-copy swap)
87 | t.swapBuff(BuffWork, BuffOut)
88 | return t
89 | }
90 |
91 | // isASCIIOnlyOut checks if out buffer contains only ASCII characters
92 | func (t *Conv) isASCIIOnlyOut() bool {
93 | for i := 0; i < t.outLen; i++ {
94 | if t.out[i] > 127 {
95 | return false
96 | }
97 | }
98 | return true
99 | }
100 |
101 | // tildeUnicodeOptimized processes Unicode accents using work buffer
102 | func (t *Conv) tildeUnicodeOptimized() {
103 | // Convert from out buffer to work buffer with accent processing
104 | str := t.GetString(BuffOut)
105 |
106 | for _, r := range str {
107 | // Find accent and replace with base character using index lookup
108 | found := false
109 | // Check lowercase accents
110 | for i, char := range aL {
111 | if r == char {
112 | t.addRuneToWork(bL[i])
113 | found = true
114 | break
115 | }
116 | }
117 | // Check uppercase accents if not found in lowercase
118 | if !found {
119 | for i, char := range aU {
120 | if r == char {
121 | t.addRuneToWork(bU[i])
122 | found = true
123 | break
124 | }
125 | }
126 | }
127 | if !found {
128 | t.addRuneToWork(r)
129 | }
130 | }
131 | }
132 |
133 | // =============================================================================
134 | // CENTRALIZED WORD SEPARATOR DETECTION - SHARED BY CAPITALIZE AND TRANSLATION
135 | // =============================================================================
136 |
137 | // isWordSeparator checks if a character is a word separator
138 | // UNIFIED FUNCTION: Handles byte, rune, and string inputs in a single function
139 | // OPTIMIZED: Uses isWordSeparatorChar as single source of truth
140 | func isWordSeparator(input any) bool {
141 | switch v := input.(type) {
142 | case byte:
143 | return isWordSeparatorChar(rune(v))
144 | case rune:
145 | return isWordSeparatorChar(v)
146 | case string:
147 | // Handle empty strings
148 | if len(v) == 0 {
149 | return false
150 | }
151 | // Multi-char strings: check if they start with space or newline (translation context)
152 | if len(v) > 1 && (v[0] == ' ' || v[0] == '\t' || v[0] == '\n') {
153 | return true
154 | }
155 | // Single character strings using the centralized logic
156 | if len(v) == 1 {
157 | return isWordSeparatorChar(rune(v[0]))
158 | }
159 | // Check if string ends with newline (separator behavior for translation)
160 | return v[len(v)-1] == '\n'
161 | }
162 | return false
163 | }
164 |
165 | // isWordSeparatorChar is the core separator detection logic
166 | // CENTRALIZED: Single source of truth for what constitutes a word separator
167 | // OPTIMIZED: Handles both ASCII and Unicode characters efficiently
168 | func isWordSeparatorChar(r rune) bool {
169 | return r == ' ' || r == '\t' || r == '\n' || r == '\r' ||
170 | r == '/' || r == '+' || r == '-' || r == '_' || r == '.' ||
171 | r == ',' || r == ';' || r == ':' || r == '!' || r == '?' ||
172 | r == '(' || r == ')' || r == '[' || r == ']' || r == '{' || r == '}'
173 | }
174 |
--------------------------------------------------------------------------------