├── .github ├── FUNDING.yml ├── instructions │ └── gopu.sh.md └── badges.css ├── benchmark ├── phase13-analysis │ ├── memory_hotspots.txt │ └── string_allocations.txt ├── shared │ ├── go.mod │ └── testdata.go ├── analyzer ├── bench-binary-size │ ├── standard-lib │ │ ├── go.mod │ │ └── main.go │ └── tinystring-lib │ │ ├── go.mod │ │ └── main.go ├── bench-memory-alloc │ ├── standard │ │ ├── standard_benchmark │ │ ├── go.mod │ │ ├── standard_results.txt │ │ ├── main_test.go │ │ └── main.go │ ├── tinystring │ │ ├── go.mod │ │ ├── main_test.go │ │ └── main.go │ └── README.md ├── tinygo.unsafe.pointer.md ├── memory-benchmark.sh ├── MEMORY_ALLOCATION_FASTHTTP_TIPS.md ├── common.go ├── run-all-benchmarks.sh ├── phase13-analysis.sh └── clean-all.sh ├── example └── web │ ├── theme │ ├── main.js │ └── index.html │ ├── public │ ├── main.wasm │ └── index.html │ ├── server.go │ └── client.go ├── go.mod ├── env.back.go ├── docs ├── TRUNCATION.md ├── API_PARSING.md ├── STRUCT_TAGS.md ├── ID_PRIMARY_KEY.md ├── WHY.md ├── API_ERRORS.md ├── betterment │ ├── ISSUE_PROMPT.md │ ├── ISSUE_MEMORY_TOOLS.md │ ├── ISSUE_SUMMARY_TINYSTRING.md │ └── ISSUE_ISSUE_MEMORY_SUMMARY.md ├── MESSAGE_TYPES.md ├── API_FMT.md ├── API_STRCONV.md ├── API_FILEPATH.md ├── API_HTML.md ├── issues │ ├── BUG_FMT_CUSTOM_TYPE.md │ ├── FEAT_MESSAGETYPE_SSE.md │ └── REM_GET_STRING.md ├── API_STRINGS.md ├── img │ └── badges.svg └── TRANSLATE.md ├── .gitignore ├── env.front.go ├── kind_test.go ├── compact_float.go ├── LICENSE ├── null_pointer_test.go ├── quote.go ├── repeat.go ├── operations.go ├── parse.go ├── searchHasPrefixSuffix_test.go ├── capitalize_translate_test.go ├── fmt_precision_test.go ├── capitalize_test.go ├── quote_test.go ├── IDorPrimaryKey.go ├── bool.go ├── fmt_sci.go ├── html.go ├── num_uint.go ├── kind.go ├── IDorPrimaryKey_test.go ├── memory.GetStringZeroCopy_test.go ├── join_test.go ├── split_test.go ├── join.go ├── bool_test.go ├── split.go ├── error.go ├── string_ptr_test.go ├── dictionary_test.go ├── repeat_test.go ├── README.md ├── translation_test.go ├── search.go ├── messagetype.go ├── messagetype_test.go ├── fmt_fprintf_test.go ├── fmt_custom_type_test.go ├── fmt_number.go ├── num_float.go └── mapping.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [cdvelop] 2 | -------------------------------------------------------------------------------- /benchmark/phase13-analysis/memory_hotspots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmark/phase13-analysis/string_allocations.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/web/theme/main.js: -------------------------------------------------------------------------------- 1 | console.log("Theme Hello, PWA!"); -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinywasm/fmt 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /benchmark/shared/go.mod: -------------------------------------------------------------------------------- 1 | module benchmark/shared 2 | 3 | go 1.25.2 4 | -------------------------------------------------------------------------------- /benchmark/analyzer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinywasm/fmt/HEAD/benchmark/analyzer -------------------------------------------------------------------------------- /benchmark/bench-binary-size/standard-lib/go.mod: -------------------------------------------------------------------------------- 1 | module standard-example 2 | 3 | go 1.25.2 4 | -------------------------------------------------------------------------------- /example/web/public/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinywasm/fmt/HEAD/example/web/public/main.wasm -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/standard/standard_benchmark: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinywasm/fmt/HEAD/benchmark/bench-memory-alloc/standard/standard_benchmark -------------------------------------------------------------------------------- /benchmark/bench-binary-size/tinystring-lib/go.mod: -------------------------------------------------------------------------------- 1 | module tinystring-example 2 | 3 | go 1.25.2 4 | 5 | require github.com/tinywasm/fmt v0.0.0 6 | 7 | replace github.com/tinywasm/fmt => ../../.. 8 | -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/standard/go.mod: -------------------------------------------------------------------------------- 1 | module memory-bench-standard 2 | 3 | go 1.25.2 4 | 5 | require benchmark/shared v0.0.0 6 | 7 | // Use local shared module 8 | replace benchmark/shared => ../../shared 9 | -------------------------------------------------------------------------------- /example/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebAssembly 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/web/theme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebAssembly 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/tinystring/go.mod: -------------------------------------------------------------------------------- 1 | module memory-bench-tinystring 2 | 3 | go 1.25.2 4 | 5 | require ( 6 | benchmark/shared v0.0.0 7 | github.com/tinywasm/fmt v0.0.0 8 | ) 9 | 10 | // Use local fmt module 11 | replace github.com/tinywasm/fmt => ../../.. 12 | 13 | // Use local shared module 14 | replace benchmark/shared => ../../shared 15 | -------------------------------------------------------------------------------- /env.back.go: -------------------------------------------------------------------------------- 1 | //go:build !wasm 2 | 3 | package fmt 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | // getSystemLang detects system language from environment variables 10 | func (c *Conv) getSystemLang() lang { 11 | // Use the centralized parser with common environment variables. 12 | return c.langParser( 13 | os.Getenv("LANG"), 14 | os.Getenv("LANGUAGE"), 15 | os.Getenv("LC_ALL"), 16 | os.Getenv("LC_MESSAGES"), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /docs/TRUNCATION.md: -------------------------------------------------------------------------------- 1 | # Smart Truncation 2 | 3 | ```go 4 | // Basic truncation with ellipsis 5 | Convert("Hello, World!").Truncate(10).String() 6 | // out: "Hello, ..." 7 | 8 | // Name truncation for UI display 9 | Convert("Jeronimo Dominguez").TruncateName(3, 15).String() 10 | // out: "Jer. Dominguez" 11 | 12 | // Advanced name handling 13 | Convert("Juan Carlos Rodriguez").TruncateName(3, 20).String() 14 | // out: "Jua. Car. Rodriguez" 15 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #ISSUE*.md 3 | *.exe 4 | *.test.exe 5 | # Benchmark binaries (generated files, should not be tracked) 6 | benchmark/bench-binary-size/standard-lib/standard 7 | benchmark/bench-binary-size/standard-lib/*.wasm 8 | benchmark/bench-binary-size/tinystring-lib/tinystring 9 | benchmark/bench-binary-size/tinystring-lib/*.wasm 10 | benchmark/benchmark_results.md 11 | *.prof 12 | example/pwa/theme 13 | example/pwa/main.server 14 | example/*.log 15 | .vscode 16 | *.code-workspace 17 | server -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/standard/standard_results.txt: -------------------------------------------------------------------------------- 1 | goos: windows 2 | goarch: amd64 3 | pkg: memory-bench-standard 4 | cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz 5 | BenchmarkStringProcessing-16 347995 3118 ns/op 1200 B/op 48 allocs/op 6 | BenchmarkNumberProcessing-16 286244 4204 ns/op 1200 B/op 132 allocs/op 7 | BenchmarkMixedOperations-16 571129 2143 ns/op 546 B/op 44 allocs/op 8 | PASS 9 | ok memory-bench-standard 3.632s 10 | -------------------------------------------------------------------------------- /env.front.go: -------------------------------------------------------------------------------- 1 | //go:build wasm 2 | 3 | package fmt 4 | 5 | import ( 6 | "syscall/js" 7 | ) 8 | 9 | // getSystemLang detects browser language from navigator.language 10 | func (c *Conv) getSystemLang() lang { 11 | // Get browser language 12 | navigator := js.Global().Get("navigator") 13 | if navigator.IsUndefined() { 14 | return EN 15 | } 16 | 17 | language := navigator.Get("language") 18 | if language.IsUndefined() { 19 | return EN 20 | } 21 | 22 | // Use the centralized parser. 23 | return c.langParser(language.String()) 24 | } 25 | -------------------------------------------------------------------------------- /docs/API_PARSING.md: -------------------------------------------------------------------------------- 1 | # Key-Value Parsing 2 | 3 | fmt provides key-value parsing functionality to extract values from strings with separators. 4 | 5 | ## Usage 6 | 7 | ```go 8 | // Key-value parsing with the new API: 9 | value, err := Convert("user:admin").ExtractValue() // out: "admin", nil 10 | value, err := Convert("count=42").ExtractValue("=") // out: "42", nil 11 | ``` 12 | 13 | The `ExtractValue()` method splits the string on the first occurrence of the separator (default ":") and returns the value part. If a custom separator is provided, it uses that instead. -------------------------------------------------------------------------------- /docs/STRUCT_TAGS.md: -------------------------------------------------------------------------------- 1 | # Struct Tag Extraction 2 | 3 | fmt allows extracting values from struct field tags, useful for parsing metadata like JSON tags or custom labels. 4 | 5 | ## Usage 6 | 7 | ```go 8 | // Struct tag value extraction (TagValue): 9 | value, found := Convert(`json:"name" Label:"Nombre"`).TagValue("Label") // out: "Nombre", true 10 | value, found := Convert(`json:"name" Label:"Nombre"`).TagValue("xml") // out: "", false 11 | ``` 12 | 13 | The `TagValue()` method parses the tag string and extracts the value for a specific key. It returns the value and a boolean indicating if the key was found. -------------------------------------------------------------------------------- /kind_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import "testing" 4 | 5 | func TestKindString(t *testing.T) { 6 | // Build a slice of all Kind values from the Kind struct, matching the new order 7 | kindVals := []Kind{ 8 | K.Invalid, K.Bool, K.Int, K.Int8, K.Int16, K.Int32, K.Int64, K.Uint, K.Uint8, K.Uint16, K.Uint32, K.Uint64, K.Uintptr, 9 | K.Float32, K.Float64, K.Complex64, K.Complex128, K.Array, K.Chan, K.Func, K.Interface, K.Map, K.Pointer, K.Slice, K.String, K.Struct, K.UnsafePointer, 10 | } 11 | for i, want := range kindNames { 12 | k := kindVals[i] 13 | got := k.String() 14 | if got != want { 15 | t.Errorf("K.%s.String() = %q, want %q", want, got, want) 16 | } 17 | } 18 | 19 | // Test out-of-range (too large) 20 | invalids := []Kind{Kind(len(kindNames)), 255} 21 | for _, k := range invalids { 22 | if k.String() != "invalid" { 23 | t.Errorf("Kind(%d).String() = %q, want 'invalid'", int(k), k.String()) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/ID_PRIMARY_KEY.md: -------------------------------------------------------------------------------- 1 | # ID and Primary Key Detection 2 | 3 | The `IDorPrimaryKey` function determines if a field is an ID field and/or a primary key field based on naming conventions. 4 | 5 | #### Parameters 6 | - `tableName`: The name of the table or entity that the field belongs to 7 | - `fieldName`: The name of the field to analyze 8 | 9 | #### Returns 10 | - `isID`: `true` if the field is an ID field (starts with "id") 11 | - `isPK`: `true` if the field is a primary key (matches specific patterns) 12 | 13 | #### Examples 14 | - `IDorPrimaryKey("user", "id")` returns `(true, true)` 15 | - `IDorPrimaryKey("user", "iduser")` returns `(true, true)` 16 | - `IDorPrimaryKey("user", "userId")` returns `(true, true)` 17 | - `IDorPrimaryKey("user", "id_user")` returns `(true, true)` 18 | - `IDorPrimaryKey("user", "user_ID")` returns `(true, true)` 19 | - `IDorPrimaryKey("user", "idaddress")` returns `(true, false)` 20 | - `IDorPrimaryKey("user", "name")` returns `(false, false)` -------------------------------------------------------------------------------- /benchmark/shared/testdata.go: -------------------------------------------------------------------------------- 1 | // Package shared provides common test data for benchmark consistency 2 | package shared 3 | 4 | // TestTexts contains sample text strings for benchmarking 5 | var TestTexts = []string{ 6 | "Él Múrcielago Rápido", 7 | "PROCESANDO textos LARGOS", 8 | "Optimización de MEMORIA", 9 | "Rendimiento en APLICACIONES", 10 | "Reducción de ASIGNACIONES", 11 | "Análisis de RENDIMIENTO", 12 | "Gestión de RECURSOS", 13 | "Eficiencia OPERACIONAL", 14 | } 15 | 16 | // TestNumbers contains sample numeric values for benchmarking 17 | var TestNumbers = []float64{ 18 | 123456.789, 19 | 987654.321, 20 | 555555.555, 21 | 111111.111, 22 | 999999.999, 23 | 777777.777, 24 | 333333.333, 25 | 888888.888, 26 | } 27 | 28 | // TestMixedData contains mixed data types for complex benchmarking scenarios 29 | var TestMixedData = map[string]any{ 30 | "Número": 12345.67, 31 | "Texto": "Información IMPORTANTE", 32 | "Valor": 98765.43, 33 | "Título": "Análisis de RENDIMIENTO", 34 | } 35 | -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/standard/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "benchmark/shared" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkStringProcessing(b *testing.B) { 9 | b.ResetTimer() 10 | for i := 0; i < b.N; i++ { 11 | _ = processTextWithStandardLib(shared.TestTexts) 12 | } 13 | } 14 | 15 | func BenchmarkNumberProcessing(b *testing.B) { 16 | b.ResetTimer() 17 | for i := 0; i < b.N; i++ { 18 | _ = processNumbersWithStandardLib(shared.TestNumbers) 19 | } 20 | } 21 | 22 | func BenchmarkMixedOperations(b *testing.B) { 23 | b.ResetTimer() 24 | for i := 0; i < b.N; i++ { 25 | results := make(map[string]string) 26 | for key, value := range shared.TestMixedData { 27 | switch v := value.(type) { 28 | case string: 29 | processed := processTextWithStandardLib([]string{v})[0] 30 | results[key] = processed 31 | case float64: 32 | processed := processNumbersWithStandardLib([]float64{v})[0] 33 | results[key] = processed 34 | } 35 | } 36 | _ = results 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/tinystring/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "benchmark/shared" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkStringProcessing(b *testing.B) { 9 | b.ResetTimer() 10 | for i := 0; i < b.N; i++ { 11 | _ = processTextWithTinyString(shared.TestTexts) 12 | } 13 | } 14 | 15 | func BenchmarkNumberProcessing(b *testing.B) { 16 | b.ResetTimer() 17 | for i := 0; i < b.N; i++ { 18 | _ = processNumbersWithTinyString(shared.TestNumbers) 19 | } 20 | } 21 | 22 | func BenchmarkMixedOperations(b *testing.B) { 23 | b.ResetTimer() 24 | for i := 0; i < b.N; i++ { 25 | results := make(map[string]string) 26 | for key, value := range shared.TestMixedData { 27 | switch v := value.(type) { 28 | case string: 29 | processed := processTextWithTinyString([]string{v})[0] 30 | results[key] = processed 31 | case float64: 32 | processed := processNumbersWithTinyString([]float64{v})[0] 33 | results[key] = processed 34 | } 35 | } 36 | _ = results 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/instructions/gopu.sh.md: -------------------------------------------------------------------------------- 1 | --- 2 | applyTo: '*' 3 | --- 4 | I use a custom shell script named `gopu.sh` to streamline my development workflow. This script is globally available in my Git Bash terminal, so I can invoke it directly without needing to specify a path (e.g., no `./` prefix). 5 | 6 | The `gopu.sh` script automates the following sequence of actions: 7 | 1. **Adds all changed files** to the Git staging area (similar to `git add .`). 8 | 2. **Runs Go tests** (equivalent to `go test ./...`). 9 | 3. **Checks for race conditions** in the Go code (equivalent to `go test -race ./...`). 10 | 4. **Commits the staged changes** using the message provided as an argument to the script. 11 | 5. **Pushes the commit** to the remote repository. 12 | 6. **Creates and pushes a tag** (the specifics of tag generation would be handled within the script). 13 | 14 | The command to use the script is: 15 | `gopu.sh "Your detailed commit message here"` 16 | 17 | For example: 18 | `gopu.sh "feat: implement user authentication module"` -------------------------------------------------------------------------------- /compact_float.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // formatCompactFloat mimics Go's %g/%G: uses %f for normal range, %e/%E for very small/large, trims trailing zeros. 4 | func formatCompactFloat(f float64, precision int, upper bool) string { 5 | if precision < 0 { 6 | precision = 6 7 | } 8 | absf := f 9 | if absf < 0 { 10 | absf = -absf 11 | } 12 | // Use scientific for very small or large numbers 13 | if absf != 0 && (absf < 1e-4 || absf >= 1e6) { 14 | return formatScientific(f, precision, upper) 15 | } 16 | // Use %f, then trim trailing zeros and dot 17 | mult := 1.0 18 | for i := 0; i < precision; i++ { 19 | mult *= 10 20 | } 21 | val := int64(f*mult + 0.5) 22 | intPart := val / int64(mult) 23 | fracPart := val % int64(mult) 24 | res := itoa(int(intPart)) 25 | if precision > 0 { 26 | frac := itoaPad(int(fracPart), precision) 27 | // TrimSpace trailing zeros 28 | end := len(frac) 29 | for end > 0 && frac[end-1] == '0' { 30 | end-- 31 | } 32 | if end > 0 { 33 | res += "." + frac[:end] 34 | } 35 | } 36 | return res 37 | } 38 | -------------------------------------------------------------------------------- /benchmark/bench-binary-size/tinystring-lib/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/tinywasm/fmt" 4 | 5 | func main() { 6 | // EQUIVALENT FUNCTIONALITY TEST - Same operations, same complexity 7 | // Both implementations should do EXACTLY the same work 8 | 9 | // Test 1: Basic string operations 10 | text1 := "Hello World Example" 11 | result1 := fmt.Convert(text1).ToLower().Replace(" ", "_").String() 12 | 13 | // Test 2: Number formatting 14 | num1 := 1234.567 15 | result2 := fmt.Convert(num1).Round(2).String() 16 | 17 | // Test 3: Multiple string operations 18 | text2 := "Processing Multiple Strings" 19 | result3 := fmt.Convert(text2).ToUpper().Replace(" ", "-").String() 20 | 21 | // Test 4: Join operations 22 | items := []string{"item1", "item2", "item3"} 23 | result4 := fmt.Convert(items).Join(", ").String() 24 | 25 | // Test 5: Fmt operations 26 | result5 := fmt.Fmt("Result: %s | Number: %s | Upper: %s | List: %s", 27 | result1, result2, result3, result4) 28 | 29 | // Use results to prevent optimization 30 | _ = result5 31 | } 32 | -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/tinystring/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/tinywasm/fmt" 5 | ) 6 | 7 | // processTextWithTinyString simulates text processing using fmt (equivalent to standard lib) 8 | func processTextWithTinyString(texts []string) []string { 9 | results := make([]string, len(texts)) 10 | for i, text := range texts { 11 | out := Convert(text). 12 | ToLower(). 13 | Tilde(). 14 | Capitalize(). 15 | String() 16 | results[i] = out 17 | } 18 | return results 19 | } 20 | 21 | // processNumbersWithTinyString simulates number processing (equivalent to standard lib) 22 | func processNumbersWithTinyString(numbers []float64) []string { 23 | results := make([]string, len(numbers)) 24 | for i, num := range numbers { 25 | // EQUIVALENT OPERATIONS: Same formatting as standard library 26 | formatted := Convert(num). 27 | Round(2). 28 | Thousands(). 29 | String() 30 | results[i] = formatted 31 | } 32 | return results 33 | } 34 | 35 | func main() { 36 | println("fmt benchmark main function - used for testing only") 37 | } 38 | -------------------------------------------------------------------------------- /docs/WHY.md: -------------------------------------------------------------------------------- 1 | ## Why fmt? 2 | 3 | **Go's WebAssembly potential is incredible**, but traditional applications face a critical challenge: **massive binary sizes** that make web deployment impractical. 4 | 5 | ### The Problem 6 | Every Go project needs string manipulation, type conversion, and error handling - but importing standard library packages (`fmt`, `strings`, `strconv`, `errors`) creates significant binary bloat that hurts: 7 | 8 | - 🌐 **Web app performance** - Slow loading times and poor user experience 9 | - � **Edge deployment** - Resource constraints on small devices 10 | - 🚀 **Distribution efficiency** - Large binaries for simple operations 11 | 12 | ### The Solution 13 | fmt replaces multiple standard library packages with **lightweight, manual implementations** that deliver: 14 | 15 | - 🏆 **ToUpper to smaller binaries** - Dramatic size reduction for WebAssembly 16 | - ✅ **Full TinyGo compatibility** - No compilation issues or warnings 17 | - 🎯 **Predictable performance** - No hidden allocations or overhead 18 | - 🔧 **Familiar API** - Drop-in replacement for standard library functions 19 | 20 | -------------------------------------------------------------------------------- /benchmark/bench-binary-size/standard-lib/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func main() { 10 | // EQUIVALENT FUNCTIONALITY TEST - Same operations, same complexity 11 | // Both implementations should do EXACTLY the same work 12 | 13 | // Test 1: Basic string operations 14 | text1 := "Hello World Example" 15 | result1 := strings.ToLower(text1) 16 | result1 = strings.ReplaceAll(result1, " ", "_") 17 | 18 | // Test 2: Number formatting 19 | num1 := 1234.567 20 | result2 := strconv.FormatFloat(num1, 'f', 2, 64) 21 | 22 | // Test 3: Multiple string operations 23 | text2 := "Processing Multiple Strings" 24 | result3 := strings.ToUpper(text2) 25 | result3 = strings.ReplaceAll(result3, " ", "-") 26 | 27 | // Test 4: Join operations 28 | items := []string{"item1", "item2", "item3"} 29 | result4 := strings.Join(items, ", ") 30 | 31 | // Test 5: Fmt operations 32 | result5 := fmt.Sprintf("Result: %s | Number: %s | Upper: %s | List: %s", 33 | result1, result2, result3, result4) 34 | 35 | // Use results to prevent optimization 36 | _ = result5 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cesar Solis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/API_ERRORS.md: -------------------------------------------------------------------------------- 1 | # Errors Package Equivalents 2 | 3 | Replace `errors` package functions for error handling with multilingual support: 4 | 5 | | Go Standard | fmt Equivalent | 6 | |-------------|----------------------| 7 | | `errors.New()` | `Err(message)` | 8 | | `fmt.Errorf()` | `Errf(format, args...)` | 9 | 10 | ## Error Creation 11 | 12 | ```go 13 | // Multiple error messages and types 14 | err := Err("invalid format", "expected number", 404) 15 | // out: "invalid format expected number 404" 16 | 17 | // Formatted errors (like fmt.Errorf) 18 | err := Errf("invalid value: %s at position %d", "abc", 5) 19 | // out: "invalid value: abc at position 5" 20 | ``` 21 | 22 | ## Multilingual Error Messages 23 | 24 | For multilingual error messages using dictionary terms that can be translated into multiple languages, see the [Translation Guide](TRANSLATE.md). 25 | 26 | ```go 27 | // Using dictionary terms for translatable errors 28 | err := Err(D.Format, D.Invalid) 29 | // → "invalid format" (in English) or translated based on global language setting 30 | 31 | // Force specific language 32 | err := Err(ES, D.Format, D.Invalid) 33 | // → "formato inválido" 34 | ``` -------------------------------------------------------------------------------- /null_pointer_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import "testing" 4 | 5 | // TestNullPointerProtection tests null pointer verification for *string 6 | func TestNullPointerProtection(t *testing.T) { 7 | // Test null string pointer 8 | var nullStrPtr *string = nil 9 | 10 | // This should not panic and should set an error 11 | c := Convert(nullStrPtr) 12 | if !c.hasContent(BuffErr) { 13 | t.Error("Convert with null *string should set an error") 14 | } 15 | 16 | // String() should return empty due to error 17 | result := c.String() 18 | if result != "" { 19 | t.Errorf("Convert with null *string should return empty string, got: %q", result) 20 | } 21 | } 22 | 23 | // TestValidPointerHandling tests that valid pointers still work 24 | func TestValidPointerHandling(t *testing.T) { 25 | str := "test string" 26 | strPtr := &str 27 | 28 | // This should work normally 29 | c := Convert(strPtr) 30 | if c.hasContent(BuffErr) { 31 | t.Error("Convert with valid *string should not set an error") 32 | } 33 | 34 | result := c.String() 35 | expected := "test string" 36 | if result != expected { 37 | t.Errorf("Convert with valid *string: got %q, want %q", result, expected) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /quote.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // Quote wraps a string in double quotes and escapes any special characters 4 | // Example: Quote("hello \"world\"") returns "\"hello \\\"world\\\"\"" 5 | func (c *Conv) Quote() *Conv { 6 | if c.hasContent(BuffErr) { 7 | return c // Error chain interruption 8 | } 9 | if c.outLen == 0 { 10 | c.ResetBuffer(BuffOut) 11 | c.WrString(BuffOut, quoteStr) 12 | return c 13 | } 14 | 15 | // Use work buffer to build quoted string, then swap to output 16 | c.ResetBuffer(BuffWork) 17 | c.wrByte(BuffWork, '"') 18 | 19 | // Process buffer directly without string allocation (like capitalizeASCIIOptimized) 20 | for i := 0; i < c.outLen; i++ { 21 | char := c.out[i] 22 | switch char { 23 | case '"': 24 | c.wrByte(BuffWork, '\\') 25 | c.wrByte(BuffWork, '"') 26 | case '\\': 27 | c.wrByte(BuffWork, '\\') 28 | c.wrByte(BuffWork, '\\') 29 | case '\n': 30 | c.wrByte(BuffWork, '\\') 31 | c.wrByte(BuffWork, 'n') 32 | case '\r': 33 | c.wrByte(BuffWork, '\\') 34 | c.wrByte(BuffWork, 'r') 35 | case '\t': 36 | c.wrByte(BuffWork, '\\') 37 | c.wrByte(BuffWork, 't') 38 | default: 39 | c.wrByte(BuffWork, char) 40 | } 41 | } 42 | 43 | c.wrByte(BuffWork, '"') 44 | c.swapBuff(BuffWork, BuffOut) 45 | return c 46 | } 47 | -------------------------------------------------------------------------------- /repeat.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // Repeat repeats the Conv content n times 4 | // If n is 0 or negative, it clears the Conv content 5 | // eg: Convert("abc").Repeat(3) => "abcabcabc" 6 | func (t *Conv) Repeat(n int) *Conv { 7 | if t.hasContent(BuffErr) { 8 | return t // Error chain interruption 9 | } 10 | if n <= 0 { 11 | // Clear buffer for empty out and clear dataPtr to prevent reconstruction 12 | t.ResetBuffer(BuffOut) 13 | t.dataPtr = nil // Clear pointer to prevent GetString from reconstructing 14 | return t 15 | } 16 | 17 | // OPTIMIZED: Direct length check 18 | if t.outLen == 0 { 19 | // Clear buffer for empty out 20 | t.ResetBuffer(BuffOut) 21 | return t 22 | } 23 | 24 | // OPTIMIZED: Use buffer copy for efficiency 25 | originalLen := t.outLen 26 | originalData := make([]byte, originalLen) 27 | copy(originalData, t.out[:originalLen]) 28 | 29 | // Calculate total size needed 30 | totalSize := originalLen * n 31 | if cap(t.out) < totalSize { 32 | // Expand buffer if needed 33 | newBuf := make([]byte, 0, totalSize) 34 | t.out = newBuf 35 | } 36 | 37 | // Reset and fill buffer efficiently 38 | t.outLen = 0 39 | t.out = t.out[:0] 40 | 41 | // Write original data n times 42 | for range n { 43 | t.out = append(t.out, originalData...) 44 | } 45 | t.outLen = len(t.out) 46 | 47 | return t 48 | } 49 | -------------------------------------------------------------------------------- /operations.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // LastIndex returns the index of the last instance of substr in s, or -1 if substr is not present in s. 4 | // 5 | // Special cases: 6 | // - If substr is empty, LastIndex returns len(s). 7 | // - If substr is not found in s, LastIndex returns -1. 8 | // - If substr is longer than s, LastIndex returns -1. 9 | // 10 | // Examples: 11 | // 12 | // LastIndex("hello world", "world") // returns 6 13 | // LastIndex("hello world hello", "hello") // returns 12 (last occurrence) 14 | // LastIndex("image.backup.jpg", ".") // returns 12 (useful for file extensions) 15 | // LastIndex("hello", "xyz") // returns -1 (not found) 16 | // LastIndex("hello", "") // returns 5 (len("hello")) 17 | // LastIndex("", "hello") // returns -1 (not found in empty string) 18 | // 19 | // Common use case - extracting file extensions: 20 | // 21 | // filename := "document.backup.pdf" 22 | // pos := LastIndex(filename, ".") 23 | // if pos >= 0 { 24 | // extension := filename[pos+1:] // "pdf" 25 | // } 26 | func LastIndex(s, substr string) int { 27 | n := len(substr) 28 | 29 | // Handle edge cases 30 | if n == 0 { 31 | return len(s) 32 | } 33 | if n > len(s) { 34 | return -1 35 | } 36 | 37 | // Simple reverse loop 38 | for i := len(s) - n; i >= 0; i-- { 39 | if s[i:i+n] == substr { 40 | return i 41 | } 42 | } 43 | 44 | return -1 45 | } 46 | -------------------------------------------------------------------------------- /docs/betterment/ISSUE_PROMPT.md: -------------------------------------------------------------------------------- 1 | # fmt Code Reduction and Refactoring Prompt 2 | 3 | ## Objective 4 | Reduce code lines and eliminate unnecessary code while maximizing reusability through small functions. Priority is code reduction and architecture improvement for better maintainability. 5 | 6 | ## Key Requirements 7 | 8 | 1. **Code Reduction Focus**: Eliminate unnecessary lines of code 9 | 2. **Maximum Reusability**: Use small, reusable functions 10 | 3. **Minimal Private Elements**: Reduce private methods and variables to minimum 11 | 4. **Architecture Improvement**: Simplicity, fewer lines of code, and good performance 12 | 5. **Public API Stability**: Public methods must not change, only private ones 13 | 6. **Error Handling Consolidation**: Keep error handling in `error.go`, improve ErrorF and Fmt/sprintf design 14 | 15 | ## Current Problems Identified 16 | - Redundant and repetitive code in Fmt/sprintf functions 17 | - Poor error handling design between ErrorF and Fmt 18 | - Repetitive type switches that can be replaced with generics 19 | - Excessive private methods and helper functions 20 | 21 | ## Success Metrics 22 | - Reduce total lines of code by 30-50% 23 | - Maintain or improve WebAssembly binary size reduction 24 | - Simplify architecture for easier maintenance 25 | - Maintain all existing public API functionality 26 | 27 | ## Strategy Reference 28 | See `ISSUE_GENERIC_ARQ.md` for detailed implementation strategy and progress tracking. 29 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // ExtractValue extracts the value after the first delimiter. If not found, returns an error. 4 | // Usage: Convert("key:value").ExtractValue(":") => "value", nil 5 | // If no delimiter is provided, uses ":" by default. 6 | func (c *Conv) ExtractValue(delimiters ...string) (string, error) { 7 | src := c.String() 8 | d := ":" 9 | if len(delimiters) > 0 && delimiters[0] != "" { 10 | d = delimiters[0] 11 | } 12 | if src == d { 13 | return "", nil 14 | } 15 | _, after, found := c.splitByDelimiterWithBuffer(src, d) 16 | if !found { 17 | return "", c.wrErr(D.Format, D.Invalid, D.Delimiter, D.Not, D.Found) 18 | } 19 | return after, nil 20 | } 21 | 22 | // TagValue searches for the value of a key in a Go struct tag-like string. 23 | // Example: Convert(`json:"name" Label:"Nombre"`).TagValue("Label") => "Nombre", true 24 | func (c *Conv) TagValue(key string) (string, bool) { 25 | src := c.GetString(BuffOut) 26 | 27 | // Reutilizar splitStr para dividir por espacios 28 | parts := c.splitStr(src) 29 | 30 | for _, part := range parts { 31 | // Split by ':' using existing function 32 | k, v, found := c.splitByDelimiterWithBuffer(part, ":") 33 | if !found { 34 | continue 35 | } 36 | 37 | if k == key { 38 | // Remove quotes if present 39 | if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { 40 | v = v[1 : len(v)-1] 41 | } 42 | return v, true 43 | } 44 | } 45 | return "", false 46 | } 47 | -------------------------------------------------------------------------------- /.github/badges.css: -------------------------------------------------------------------------------- 1 | /* GitHub Project Badges CSS - Generated by devscripts */ 2 | .project-badges { 3 | display: inline-block; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; 5 | font-size: 11px; 6 | font-weight: 600; 7 | line-height: 1; 8 | margin: 10px 0; 9 | } 10 | 11 | .badge-group { 12 | display: inline-block; 13 | margin-right: 1ch; 14 | border-radius: 3px; 15 | overflow: hidden; 16 | vertical-align: top; 17 | } 18 | 19 | .badge-label, .badge-value { 20 | display: inline-block; 21 | padding: 4px 6px; 22 | color: white; 23 | text-decoration: none; 24 | } 25 | 26 | .badge-label { 27 | background-color: #555; 28 | } 29 | 30 | /* License Badge */ 31 | .license { background-color: #007ec6; } 32 | 33 | /* Go Version Badge */ 34 | .go-version { background-color: #00add8; } 35 | 36 | /* Test Status Badges */ 37 | .tests-passing { background-color: #4c1; } 38 | .tests-failing { background-color: #e05d44; } 39 | 40 | /* Coverage Badges */ 41 | .coverage-high { background-color: #4c1; } 42 | .coverage-medium { background-color: #dfb317; } 43 | .coverage-low { background-color: #fe7d37; } 44 | .coverage-none { background-color: #e05d44; } 45 | 46 | /* Race Detection Badges */ 47 | .race-clean { background-color: #4c1; } 48 | .race-detected { background-color: #e05d44; } 49 | 50 | /* Vet Badges */ 51 | .vet-ok { background-color: #4c1; } 52 | .vet-issues { background-color: #e05d44; } 53 | -------------------------------------------------------------------------------- /searchHasPrefixSuffix_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import "testing" 4 | 5 | func TestHasPrefix(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | s string 9 | prefix string 10 | want bool 11 | }{ 12 | {"empty prefix", "hello", "", true}, 13 | {"exact match", "hello", "hello", true}, 14 | {"short prefix", "hello", "he", true}, 15 | {"not a prefix", "hello", "ello", false}, 16 | {"prefix longer than string", "hi", "hello", false}, 17 | {"single byte", "abc", "a", true}, 18 | {"null byte prefix", "a\x00b", "a\x00", true}, 19 | {"unicode prefix", "ñandú", "ñan", true}, 20 | } 21 | 22 | for _, tc := range tests { 23 | t.Run(tc.name, func(t *testing.T) { 24 | got := HasPrefix(tc.s, tc.prefix) 25 | if got != tc.want { 26 | t.Fatalf("HasPrefix(%q, %q) = %v; want %v", tc.s, tc.prefix, got, tc.want) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func TestHasSuffix(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | s string 36 | suffix string 37 | want bool 38 | }{ 39 | {"empty suffix", "hello", "", true}, 40 | {"exact match", "hello", "hello", true}, 41 | {"short suffix", "hello", "lo", true}, 42 | {"not a suffix", "hello", "hel", false}, 43 | {"suffix longer than string", "go", "golang", false}, 44 | {"single byte", "abc", "c", true}, 45 | {"null byte suffix", "a\x00b", "\x00b", true}, 46 | {"unicode suffix", "pingüino", "üino", true}, 47 | } 48 | 49 | for _, tc := range tests { 50 | t.Run(tc.name, func(t *testing.T) { 51 | got := HasSuffix(tc.s, tc.suffix) 52 | if got != tc.want { 53 | t.Fatalf("HasSuffix(%q, %q) = %v; want %v", tc.s, tc.suffix, got, tc.want) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/MESSAGE_TYPES.md: -------------------------------------------------------------------------------- 1 | # Message Type Detection 2 | 3 | fmt provides automatic message type classification to help identify the nature of text content. The system detects common message types like errors, warnings, success messages, and information using zero-allocation buffer-based pattern matching. 4 | 5 | ```go 6 | // Before: messagetype library usage 7 | message := Translate(msgs...).String() 8 | msgType := messagetype.DetectMessageType(message) 9 | 10 | // After: tinystring Single operation with StringType() (zero allocations) 11 | message, msgType := Translate(msgs...).StringType() 12 | 13 | // Real example - Progress callback with message classification 14 | progressCallback := func(msgs ...any) { 15 | message, msgType := Translate(msgs...).StringType() 16 | if msgType.IsError() { 17 | handleError(message) 18 | } else { 19 | logMessage(message, msgType) 20 | } 21 | } 22 | 23 | // Message type constants available via Msg struct 24 | if msgType.IsError() { 25 | // Handle error case 26 | } 27 | 28 | // Available message types: 29 | // Msg.Normal - Default type for general content 30 | // Msg.Info - Information messages 31 | // Msg.Error - Error messages and failures 32 | // Msg.Warning - Warning and caution messages 33 | // Msg.Success - Success and completion messages 34 | // 35 | // Network/SSE specific: 36 | // Msg.Connect - Connection error 37 | // Msg.Auth - Authentication error 38 | // Msg.Parse - Parse/decode error 39 | // Msg.Timeout - Timeout error 40 | // Msg.Broadcast - Broadcast/send error 41 | 42 | // Zero allocations - reuses existing conversion buffers 43 | // Perfect for logging, UI status messages, and error handling 44 | ``` -------------------------------------------------------------------------------- /example/web/server.go: -------------------------------------------------------------------------------- 1 | //go:build !wasm 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | 13 | publicDir := "public" // Template variable 14 | 15 | // Debug: Print working directory and check if public exists 16 | _, err := os.Getwd() 17 | if err != nil { 18 | log.Printf("Error getting working directory: %v", err) 19 | } 20 | 21 | if _, err := os.Stat(publicDir); os.IsNotExist(err) { 22 | log.Printf("WARNING: Public directory '%s' does not exist!", publicDir) 23 | } 24 | 25 | // Serve static files with no-cache headers 26 | fs := http.FileServer(http.Dir(publicDir)) 27 | 28 | // Middleware to disable caching for static files (useful in dev/test) 29 | noCache := func(h http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | // Prevent browser caching 32 | w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") 33 | w.Header().Set("Pragma", "no-cache") 34 | w.Header().Set("Expires", "0") 35 | h.ServeHTTP(w, r) 36 | }) 37 | } 38 | 39 | // Use a dedicated ServeMux so we can pass it to an http.Server 40 | mux := http.NewServeMux() 41 | mux.Handle("/", noCache(fs)) 42 | 43 | // Health check endpoint 44 | mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 45 | w.WriteHeader(http.StatusOK) 46 | 47 | w.Write([]byte("Server is running 3")) 48 | }) 49 | 50 | // Create http.Server with Addr and Handler set 51 | server := &http.Server{ 52 | Addr: ":4430", 53 | Handler: mux, 54 | } 55 | 56 | if err := server.ListenAndServe(); err != nil { 57 | log.Fatal("Server failed to start:", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /capitalize_translate_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCapitalizeWithMultilineTranslation(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | appName string 11 | lang string 12 | expected string 13 | description string 14 | }{ 15 | { 16 | name: "Simple multiline with Capitalize", 17 | appName: "TestApp", 18 | lang: "EN", 19 | expected: "Testapp Shortcuts Keyboard (\"en\"):\n\nTabs:\n • Tab/Shift+Tab - Switch Tabs\n\nFields :\n • Left/Right - Navigate Fields\n • Enter - Edit/Execute\n • Esc - Cancel \n\nLanguage Supported : En, Es, Zh, Hi, Ar, Pt, Fr, De, Ru", 20 | description: "Test that Capitalize preserves multiline structure", 21 | }, 22 | } 23 | 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | // Simulate the generateHelpContent method but simplified 27 | result := generateSimplifiedHelpContent(tt.appName, tt.lang) 28 | 29 | // Check if the result maintains proper formatting 30 | if result != tt.expected { 31 | t.Errorf("Test %s failed.\nExpected: %q\nGot: %q", tt.name, tt.expected, result) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | // generateSimplifiedHelpContent simulates the PROBLEMATIC method WITH Capitalize 38 | func generateSimplifiedHelpContent(appName, lang string) string { 39 | // Test the core issue: preserving spaces in mixed translated/non-translated content 40 | return Translate(appName, D.Shortcuts, D.Keyboard, "(\""+lang+"\"):\n\nTabs:\n • Tab/Shift+Tab -", D.Switch, " tabs\n\n", D.Fields, ":\n • Left/Right - Navigate fields\n • Enter - ", D.Edit, "/", D.Execute, "\n • Esc - ", D.Cancel, " \n\n", D.Language, D.Supported, ": EN, ES, ZH, HI, AR, PT, FR, DE, RU").Capitalize().String() 41 | } 42 | -------------------------------------------------------------------------------- /docs/API_FMT.md: -------------------------------------------------------------------------------- 1 | # Fmt Package Equivalents 2 | 3 | Replace `fmt` package functions for formatting: 4 | 5 | | Go Standard | fmt Equivalent | 6 | |-------------|----------------------| 7 | | `fmt.Sprintf()` | `Fmt(format, args...)` | 8 | | `fmt.Sprint()` | `Convert(v).String()` | 9 | | `fmt.Fprintf()` | `Fprintf(w, format, args...)` | 10 | | `fmt.Sscanf()` | `Sscanf(src, format, args...)` | 11 | 12 | ## String Formatting 13 | 14 | ```go 15 | // Printf-style formatting 16 | result := Fmt("Hello %s, you have %d messages", "John", 5) 17 | // out: "Hello John, you have 5 messages" 18 | 19 | // Multiple format specifiers 20 | result := Fmt("Number: %d, Float: %.2f, Bool: %v", 42, 3.14159, true) 21 | // out: "Number: 42, Float: 3.14, Bool: true" 22 | 23 | // Advanced formatting (hex, binary, octal) 24 | result := Fmt("Hex: %x, Binary: %b, Octal: %o", 255, 10, 8) 25 | // out: "Hex: ff, Binary: 1010, Octal: 10" 26 | 27 | // Write formatted output to io.Writer 28 | var buf bytes.Buffer 29 | Fprintf(&buf, "Hello %s, count: %d\n", "world", 42) 30 | 31 | // Write to file 32 | file, _ := os.Create("output.txt") 33 | Fprintf(file, "Data: %v\n", someData) 34 | 35 | // Parse formatted text from string (like fmt.Sscanf) 36 | var pos int 37 | var name string 38 | n, err := Sscanf("!3F question", "!%x %s", &pos, &name) 39 | // n = 2, pos = 63, name = "question", err = nil 40 | 41 | // Parse complex formats 42 | var code, unicode int 43 | var word string 44 | n, err := Sscanf("!3F U+003F question", "!%x U+%x %s", &code, &unicode, &word) 45 | // n = 3, code = 63, unicode = 63, word = "question", err = nil 46 | 47 | // Localized string formatting 48 | // Uses the current global language or default (EN) 49 | Fmt("Error: %L", D.Invalid) 50 | // out (EN): "Error: invalid" 51 | // out (ES): "Error: inválido" 52 | ``` 53 | 54 | For more details on translation and `LocStr` usage, see [TRANSLATE.md](TRANSLATE.md). -------------------------------------------------------------------------------- /benchmark/bench-memory-alloc/standard/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // processTextWithStandardLib simulates text processing using standard library 10 | func processTextWithStandardLib(texts []string) []string { 11 | results := make([]string, len(texts)) 12 | for i, text := range texts { 13 | lowered := strings.ToLower(text) 14 | replaced := strings.ReplaceAll(lowered, "á", "a") 15 | replaced = strings.ReplaceAll(replaced, "é", "e") 16 | replaced = strings.ReplaceAll(replaced, "í", "i") 17 | replaced = strings.ReplaceAll(replaced, "ó", "o") 18 | replaced = strings.ReplaceAll(replaced, "ú", "u") 19 | replaced = strings.ReplaceAll(replaced, "ñ", "n") 20 | 21 | // Capitalizar solo la primera letra del string completo 22 | out := replaced 23 | if len(out) > 0 { 24 | out = strings.ToUpper(out[:1]) + out[1:] 25 | } 26 | results[i] = out 27 | } 28 | return results 29 | } 30 | 31 | // processNumbersWithStandardLib simulates number processing 32 | func processNumbersWithStandardLib(numbers []float64) []string { 33 | results := make([]string, len(numbers)) 34 | for i, num := range numbers { 35 | // EQUIVALENT OPERATIONS: Fmt with 2 decimals + add thousand separators 36 | formatted := strconv.FormatFloat(num, 'f', 2, 64) 37 | 38 | // Add thousand separators (equivalent to Thousands) 39 | parts := strings.Split(formatted, ".") 40 | integer := parts[0] 41 | decimal := parts[1] 42 | 43 | // Simple thousand separator logic 44 | if len(integer) > 3 { 45 | var out strings.Builder 46 | for j, char := range integer { 47 | if j > 0 && (len(integer)-j)%3 == 0 { 48 | out.WriteString(".") 49 | } 50 | out.WriteRune(char) 51 | } 52 | formatted = out.String() + "," + decimal 53 | } 54 | results[i] = formatted 55 | } 56 | return results 57 | } 58 | 59 | func main() { 60 | fmt.Println("Standard library benchmark main function - used for testing only") 61 | } 62 | -------------------------------------------------------------------------------- /fmt_precision_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRoundDecimalsUnified(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | input any 11 | decimals int 12 | down bool // true = truncate, false = round 13 | want string 14 | }{ 15 | {"Int to 2 decimals", 3, 2, false, "3.00"}, 16 | {"Float to 2 decimals", 3.12221, 2, false, "3.12"}, 17 | {"Float to 3 decimals", 3.1415926, 3, false, "3.142"}, 18 | {"Float to 0 decimals", 3.6, 0, false, "4"}, 19 | {"Negative float to 2 decimals", -3.12221, 2, false, "-3.12"}, 20 | {"String float input", "3.12221", 2, false, "3.12"}, 21 | {"Non-numeric string input", "hello", 2, false, "0.00"}, 22 | {"Int to 2 decimals (truncate)", 3, 2, true, "3.00"}, 23 | {"Float to 2 decimals (truncate)", 3.14159, 2, true, "3.14"}, 24 | {"Float to 3 decimals (truncate)", 3.1415926, 3, true, "3.141"}, 25 | {"Float to 0 decimals (truncate)", 3.9, 0, true, "3"}, 26 | {"Negative float to 2 decimals (truncate)", -3.12221, 2, true, "-3.12"}, 27 | {"String float input (truncate)", "3.12221", 2, true, "3.12"}, 28 | {"Non-numeric string input (truncate)", "hello", 2, true, "0.00"}, 29 | // Enhanced/edge cases 30 | {"Round up default", "3.154", 2, false, "3.15"}, 31 | {"Round down explicit", "3.154", 2, true, "3.15"}, 32 | {"Round up default zero decimals", "3.7", 0, false, "4"}, 33 | {"Round down zero decimals", "3.7", 0, true, "3"}, 34 | {"Negative number round up", "-3.154", 2, false, "-3.15"}, 35 | {"Negative number round down", "-3.154", 2, true, "-3.15"}, 36 | {"Negative number with 5 - round up", "-3.155", 2, false, "-3.16"}, 37 | {"Negative number with 5 - round down", "-3.155", 2, true, "-3.15"}, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | c := Convert(tt.input) 43 | if tt.down { 44 | c.Round(tt.decimals, true) 45 | } else { 46 | c.Round(tt.decimals) 47 | } 48 | out := c.String() 49 | if out != tt.want { 50 | t.Errorf("%s: got = %v, want %v", tt.name, out, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/web/client.go: -------------------------------------------------------------------------------- 1 | //go:build wasm 2 | 3 | package main 4 | 5 | import ( 6 | "syscall/js" 7 | 8 | . "github.com/tinywasm/fmt" 9 | ) 10 | 11 | func main() { 12 | // Your WebAssembly code here ok 13 | 14 | // Crear el elemento div 15 | dom := js.Global().Get("document").Call("createElement", "div") 16 | 17 | // Demonstrate conversion processes like in README line 48 18 | items := []string{" ÁPPLE ", " banána ", " piñata ", " ÑANDÚ "} 19 | 20 | buf := Convert(). 21 | Write("

fmt Conversion Processes

"). 22 | Write("

Original Items:

"). 23 | Write(""). 31 | Write("

After Processing:

"). 32 | Write(""). 51 | Write("

Conversion Steps Applied:

"). 52 | Write("
    "). 53 | Write("
  1. TrimSpace() - Remove leading/trailing whitespace
  2. "). 54 | Write("
  3. Tilde() - Normalize accents (á→a, ñ→n, etc.)
  4. "). 55 | Write("
  5. ToLower() - Convert to lowercase
  6. "). 56 | Write("
  7. Capitalize() - Capitalize first letter
  8. "). 57 | Write("
") 58 | 59 | dom.Set("innerHTML", buf.String()) 60 | 61 | // Obtener el body del documento y agregar el elemento 62 | body := js.Global().Get("document").Get("body") 63 | body.Call("appendChild", dom) 64 | 65 | logger := func(msg ...any) { 66 | js.Global().Get("console").Call("log", Translate(msg...).String()) 67 | } 68 | 69 | logger("hello tinystring:", 123, 45.67, true, []string{"a", "b", "c"}) 70 | 71 | select {} 72 | } 73 | -------------------------------------------------------------------------------- /docs/API_STRCONV.md: -------------------------------------------------------------------------------- 1 | # Strconv Package Equivalents 2 | 3 | Replace `strconv` package functions for type conversions: 4 | 5 | | Go Standard | fmt Equivalent | 6 | |-------------|----------------------| 7 | | `strconv.Itoa()` | `Convert(i).String()` | 8 | | `strconv.Atoi()` | `Convert(s).Int()` | 9 | | `strconv.ParseFloat()` | `Convert(s).Float64()` | 10 | | `strconv.ParseBool()` | `Convert(s).Bool()` | 11 | | `strconv.FormatFloat()` | `Convert(f).Round(n).String()` | 12 | | `strconv.Quote()` | `Convert(s).Quote().String()` | 13 | 14 | ## Type Conversions 15 | 16 | ```go 17 | // String to numbers => Int,Int32,Int64,Uint,Uint32,Uint64,Float32,Float64 eg: 18 | result, err := Convert("123").Int() // out: 123, nil 19 | result, err := Convert("456").Uint() // out: 456, nil 20 | result, err := Convert("3.14").Float64() // out: 3.14, nil 21 | 22 | // Numbers to string 23 | Convert(42).String() // out: "42" 24 | Convert(3.14159).String() // out: "3.14159" 25 | 26 | // Boolean conversions 27 | result, err := Convert("true").Bool() // out: true, nil 28 | result, err := Convert(42).Bool() // out: true, nil (non-zero = true) 29 | result, err := Convert(0).Bool() // out: false, nil 30 | 31 | // String quoting 32 | Convert("hello").Quote().String() // out: "\"hello\"" 33 | Convert("say \"hello\"").Quote().String() // out: "\"say \\\"hello\\\"\"" 34 | ``` 35 | 36 | ## Number Formatting 37 | 38 | ```go 39 | // Decimal rounding: keep N decimals, round or truncate 40 | // By default, rounds using "round half to even" (bankers rounding) 41 | // Pass true as the second argument to truncate (no rounding), e.g.: 42 | Convert("3.14159").Round(2).String() // "3.14" (rounded) 43 | Convert("3.155").Round(2).String() // "3.16" (rounded) 44 | Convert("3.14159").Round(2, true).String() // "3.14" (truncated, NOT rounded) 45 | Convert("3.159").Round(2, true).String() // "3.15" (truncated, NOT rounded) 46 | 47 | // Formatting with thousands separator (EU default) 48 | Convert(2189009.00).Thousands().String() // out: "2.189.009" 49 | // Anglo/US style (comma, dot) 50 | Convert(2189009.00).Thousands(true).String() // out: "2,189,009" 51 | ``` -------------------------------------------------------------------------------- /capitalize_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import "testing" 4 | 5 | func TestCapitalize(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | input string 9 | expected string 10 | }{ 11 | { 12 | name: "Simple Conv", 13 | input: "hello world", 14 | expected: "Hello World", 15 | }, 16 | { 17 | name: "Already capitalized", 18 | input: "Hello World", 19 | expected: "Hello World", 20 | }, 21 | { 22 | name: "Mixed case", 23 | input: "hELLo wORLd", 24 | expected: "Hello World", 25 | }, 26 | { 27 | name: "Extra spaces", 28 | input: " hello world ", 29 | expected: " Hello World ", 30 | }, 31 | { 32 | name: "With numbers", 33 | input: "hello 123 world", 34 | expected: "Hello 123 World", 35 | }, 36 | { 37 | name: "With special characters", 38 | input: "héllö wörld", 39 | expected: "Héllö Wörld", 40 | }, 41 | { 42 | name: "Empty string", 43 | input: "", 44 | expected: "", 45 | }, 46 | { 47 | name: "Single word", 48 | input: "hello", 49 | expected: "Hello", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | out := Convert(tt.input).Capitalize().String() 56 | if out != tt.expected { 57 | t.Errorf("Capitalize() = %q, want %q", out, tt.expected) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestCapitalizeChaining(t *testing.T) { 64 | tests := []struct { 65 | name string 66 | input string 67 | expected string 68 | chain func(*Conv) *Conv 69 | }{ 70 | { 71 | name: "With Tilde", 72 | input: "hólá múndo", 73 | expected: "Hola Mundo", 74 | chain: func(Conv *Conv) *Conv { 75 | return Conv.Tilde().Capitalize() 76 | }, 77 | }, 78 | { 79 | name: "After ToLower", 80 | input: "HELLO WORLD", 81 | expected: "Hello World", 82 | chain: func(Conv *Conv) *Conv { 83 | return Conv.ToLower().Capitalize() 84 | }, 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | out := tt.chain(Convert(tt.input)).String() 91 | if out != tt.expected { 92 | t.Errorf("%s = %q, want %q", tt.name, out, tt.expected) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /quote_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import "testing" 4 | 5 | func TestQuote(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | input string 9 | expected string 10 | }{ 11 | { 12 | name: "Simple string", 13 | input: "hello", 14 | expected: `"hello"`, 15 | }, 16 | { 17 | name: "String with spaces", 18 | input: "hello world", 19 | expected: `"hello world"`, 20 | }, 21 | { 22 | name: "String with quotes", 23 | input: `say "hello"`, 24 | expected: `"say \"hello\""`, 25 | }, 26 | { 27 | name: "String with backslash", 28 | input: `path\to\file`, 29 | expected: `"path\\to\\file"`, 30 | }, 31 | { 32 | name: "String with newline", 33 | input: "line1\nline2", 34 | expected: `"line1\nline2"`, 35 | }, 36 | { 37 | name: "String with tab", 38 | input: "before\tafter", 39 | expected: `"before\tafter"`, 40 | }, 41 | { 42 | name: "Empty string", 43 | input: "", 44 | expected: `""`, 45 | }, 46 | { 47 | name: "String with carriage return", 48 | input: "before\rafter", 49 | expected: `"before\rafter"`, 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | out := Convert(tt.input).Quote().String() 56 | if out != tt.expected { 57 | t.Errorf("Quote() = %q, want %q", out, tt.expected) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestQuoteWithError(t *testing.T) { 64 | // Test quote functionality with error handling 65 | out, err := Convert("test").Quote().StringErr() 66 | if err != nil { 67 | t.Errorf("Unexpected error: %v", err) 68 | } 69 | expected := `"test"` 70 | if out != expected { 71 | t.Errorf("Quote() = %q, want %q", out, expected) 72 | } 73 | } 74 | 75 | func TestQuoteChaining(t *testing.T) { 76 | // Test chaining quote with other operations 77 | out := Convert("hello").Quote().String() 78 | expected := `"hello"` 79 | if out != expected { 80 | t.Errorf("Quote chaining = %q, want %q", out, expected) 81 | } 82 | 83 | // Test quote after conversion 84 | result2 := Convert(123).Quote().String() 85 | expected2 := `"123"` 86 | if result2 != expected2 { 87 | t.Errorf("Quote after conversion = %q, want %q", result2, expected2) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /IDorPrimaryKey.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // IDorPrimaryKey determines if a field is an ID field and/or a primary key field. 4 | // This function analyzes field names to identify ID fields and primary keys based on naming conventions. 5 | // 6 | // Parameters: 7 | // - tableName: The name of the table or entity that the field belongs to 8 | // - fieldName: The name of the field to analyze 9 | // 10 | // Returns: 11 | // - isID: true if the field is an ID field (starts with "id") 12 | // - isPK: true if the field is a primary key (matches specific patterns) 13 | // 14 | // Examples: 15 | // - IDorPrimaryKey("user", "id") returns (true, true) 16 | // - IDorPrimaryKey("user", "iduser") returns (true, true) 17 | // - IDorPrimaryKey("user", "userID") returns (true, true) 18 | // - IDorPrimaryKey("user", "USER_id") returns (true, true) 19 | // - IDorPrimaryKey("user", "id_user") returns (true, true) 20 | // - IDorPrimaryKey("user", "idaddress") returns (true, false) 21 | // - IDorPrimaryKey("user", "name") returns (false, false) 22 | func IDorPrimaryKey(tableName, fieldName string) (isID, isPK bool) { 23 | c := GetConv() 24 | defer c.PutConv() 25 | 26 | // Convert tableName to lower in work buffer 27 | c.ResetBuffer(BuffOut) 28 | c.WrString(BuffOut, tableName) 29 | c.ToLower() 30 | c.swapBuff(BuffOut, BuffWork) 31 | tableLower := c.GetString(BuffWork) 32 | 33 | // Convert fieldName to lower in out buffer 34 | c.ResetBuffer(BuffOut) 35 | c.WrString(BuffOut, fieldName) 36 | c.ToLower() 37 | fieldLower := c.GetString(BuffOut) 38 | 39 | // Check if it's an ID field (starts with "id") 40 | if HasPrefix(fieldLower, "id") { 41 | isID = true 42 | } 43 | 44 | // Check for primary key patterns 45 | if fieldLower == "id" && tableName != "" { 46 | isPK = true 47 | return 48 | } 49 | 50 | if HasPrefix(fieldLower, "id_") && fieldLower[3:] == tableLower && tableName != "" { 51 | isPK = true 52 | return 53 | } 54 | 55 | if HasPrefix(fieldLower, "id") && fieldLower[2:] == tableLower && tableName != "" { 56 | isPK = true 57 | return 58 | } 59 | 60 | if HasSuffix(fieldLower, "id") && fieldLower[:len(fieldLower)-2] == tableLower && tableName != "" { 61 | isPK = true 62 | return 63 | } 64 | 65 | if HasSuffix(fieldLower, "_id") && fieldLower[:len(fieldLower)-3] == tableLower && tableName != "" { 66 | isPK = true 67 | return 68 | } 69 | 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /bool.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // Bool converts the Conv content to a boolean value using internal implementations 4 | // Returns the boolean value and any error that occurred 5 | func (c *Conv) Bool() (bool, error) { 6 | if c.hasContent(BuffErr) { 7 | return false, c 8 | } 9 | 10 | // Optimized: Direct byte comparison without string allocation 11 | if c.bytesEqual(BuffOut, []byte("true")) || c.bytesEqual(BuffOut, []byte("True")) || 12 | c.bytesEqual(BuffOut, []byte("TRUE")) || c.bytesEqual(BuffOut, []byte("1")) || 13 | c.bytesEqual(BuffOut, []byte("t")) || c.bytesEqual(BuffOut, []byte("Translate")) { 14 | c.kind = K.Bool 15 | return true, nil 16 | } 17 | if c.bytesEqual(BuffOut, []byte("false")) || c.bytesEqual(BuffOut, []byte("False")) || 18 | c.bytesEqual(BuffOut, []byte("FALSE")) || c.bytesEqual(BuffOut, []byte("0")) || 19 | c.bytesEqual(BuffOut, []byte("f")) || c.bytesEqual(BuffOut, []byte("F")) { 20 | c.kind = K.Bool 21 | return false, nil 22 | } 23 | 24 | // Try to parse as integer using direct buffer access (eliminates GetString allocation) 25 | inp := c.GetString(BuffOut) // Still needed for parseIntString compatibility 26 | intVal := c.parseIntString(inp, 10, true) 27 | if !c.hasContent(BuffErr) { 28 | c.kind = K.Bool 29 | return intVal != 0, nil 30 | } else { 31 | // Limpia el error generado por el intento fallido usando la API 32 | c.ResetBuffer(BuffErr) 33 | } 34 | 35 | // Try basic float patterns (optimized byte comparison) 36 | if c.bytesEqual(BuffOut, []byte("0.0")) || c.bytesEqual(BuffOut, []byte("0.00")) || 37 | c.bytesEqual(BuffOut, []byte("+0")) || c.bytesEqual(BuffOut, []byte("-0")) { 38 | c.kind = K.Bool 39 | return false, nil 40 | } 41 | 42 | // Optimized: Check for non-zero starting digit without string allocation 43 | if !c.bytesEqual(BuffOut, []byte("0")) && c.outLen > 0 && 44 | (c.out[0] >= '1' && c.out[0] <= '9') { 45 | // Non-zero number starting with digit 1-9, likely true 46 | c.kind = K.Bool 47 | return true, nil 48 | } 49 | 50 | // Keep inp for error reporting (this is the final usage) 51 | inp = c.GetString(BuffOut) // Only allocation for error case 52 | c.wrErr("Bool", D.Value, D.Invalid, inp) 53 | return false, c 54 | } 55 | 56 | // wrBool writes boolean value to specified buffer destination 57 | func (c *Conv) wrBool(dest BuffDest, val bool) { 58 | if val { 59 | c.WrString(dest, "true") 60 | } else { 61 | c.WrString(dest, "false") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /fmt_sci.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // formatScientific formats a float64 in scientific notation (e.g., 1.234000e+03) 4 | // precision: number of digits after decimal point, -1 for default (6) 5 | // upper: true for 'E', false for 'e' 6 | func formatScientific(f float64, precision int, upper bool) string { 7 | if f == 0 { 8 | if precision < 0 { 9 | precision = 6 10 | } 11 | mantissa := "0." 12 | for i := 0; i < precision; i++ { 13 | mantissa += "0" 14 | } 15 | if upper { 16 | return mantissa + "E+00" 17 | } 18 | return mantissa + "e+00" 19 | } 20 | neg := false 21 | if f < 0 { 22 | neg = true 23 | f = -f 24 | } 25 | // Find exponent 26 | exp := 0 27 | for f >= 10 { 28 | f /= 10 29 | exp++ 30 | } 31 | for f > 0 && f < 1 { 32 | f *= 10 33 | exp-- 34 | } 35 | // Round mantissa to precision 36 | if precision < 0 { 37 | precision = 6 38 | } 39 | mult := 1.0 40 | for i := 0; i < precision; i++ { 41 | mult *= 10 42 | } 43 | mant := int64(f*mult + 0.5) 44 | intPart := mant / int64(mult) 45 | fracPart := mant % int64(mult) 46 | // Build mantissa string 47 | mantissa := "" 48 | if neg { 49 | mantissa = "-" 50 | } 51 | mantissa += itoa(int(intPart)) 52 | if precision > 0 { 53 | mantissa += "." 54 | frac := itoaPad(int(fracPart), precision) 55 | mantissa += frac 56 | } 57 | // Exponent 58 | sign := "+" 59 | if exp < 0 { 60 | sign = "-" 61 | exp = -exp 62 | } 63 | if upper { 64 | return mantissa + "E" + sign + pad2(exp) 65 | } 66 | return mantissa + "e" + sign + pad2(exp) 67 | } 68 | 69 | // itoa converts int to string (manual, no stdlib) 70 | func itoa(n int) string { 71 | if n == 0 { 72 | return "0" 73 | } 74 | neg := false 75 | if n < 0 { 76 | neg = true 77 | n = -n 78 | } 79 | buf := [20]byte{} 80 | pos := len(buf) 81 | for n > 0 { 82 | pos-- 83 | buf[pos] = byte('0' + n%10) 84 | n /= 10 85 | } 86 | if neg { 87 | pos-- 88 | buf[pos] = '-' 89 | } 90 | return string(buf[pos:]) 91 | } 92 | 93 | // itoaPad pads int with leading zeros to width 94 | func itoaPad(n int, width int) string { 95 | s := itoa(n) 96 | for len(s) < width { 97 | s = "0" + s 98 | } 99 | return s 100 | } 101 | 102 | // pad2 pads int to 2 digits with leading zero 103 | func pad2(n int) string { 104 | if n < 10 { 105 | return "0" + itoa(n) 106 | } 107 | return itoa(n) 108 | } 109 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // EscapeAttr returns a string safe to place inside an HTML attribute value. 4 | // 5 | // It escapes the following characters: 6 | // 7 | // & -> & 8 | // " -> " 9 | // ' -> ' 10 | // < -> < 11 | // > -> > 12 | // 13 | // Example: 14 | // 15 | // s := Convert(`Tom & Jerry's "House" `).EscapeAttr() 16 | // // s == `Tom & Jerry's "House" <tag>` 17 | // 18 | // Note: this method performs plain string replacements and does not detect 19 | // existing HTML entities. Calling EscapeAttr on a string that already 20 | // contains entities (for example `&`) will produce double-escaped 21 | // output (`&amp;`). This behavior is intentional and matches a simple 22 | // escape-for-attribute semantics. 23 | func (c *Conv) EscapeAttr() string { 24 | return c.Replace("&", "&"). 25 | Replace("\"", """). 26 | Replace("'", "'"). 27 | Replace("<", "<"). 28 | Replace(">", ">"). 29 | String() 30 | } 31 | 32 | // EscapeHTML returns a string safe for inclusion into HTML content. 33 | // 34 | // It escapes the following characters: 35 | // 36 | // & -> & 37 | // < -> < 38 | // > -> > 39 | // " -> " 40 | // ' -> ' 41 | // 42 | // Example: 43 | // 44 | // s := Convert(`
Tom & Jerry's
`).EscapeHTML() 45 | // // s == `<div class="x">Tom & Jerry's</div>` 46 | // 47 | // Like EscapeAttr, this method uses simple replacements and will double-escape 48 | // existing entities. 49 | func (c *Conv) EscapeHTML() string { 50 | return c.Replace("&", "&"). 51 | Replace("<", "<"). 52 | Replace(">", ">"). 53 | Replace("\"", """). 54 | Replace("'", "'"). 55 | String() 56 | } 57 | 58 | // Html creates a string for HTML content, similar to Translate but without automatic spacing. 59 | // It supports two modes: 60 | // 1. Format mode: If the first argument is a string containing '%', it behaves like Fmt. 61 | // 2. Concatenation mode: Otherwise, it concatenates arguments (translating LocStr) without spaces. 62 | // 63 | // Usage: 64 | // Html("div", "span").String() -> "divspan" 65 | // Html("
", "foo").String() -> "
" 66 | func Html(values ...any) *Conv { 67 | // Use unified smart processing 68 | // separator="", allowStringCode=false (to support 2-letter tags), detectFormat=true 69 | return GetConv().SmartArgs(BuffOut, "", false, true, values...) 70 | } 71 | -------------------------------------------------------------------------------- /num_uint.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // wrUintBase writes an unsigned integer in the given base to the buffer 4 | func (c *Conv) wrUintBase(dest BuffDest, value uint64, base int) { 5 | if base < 2 || base > 36 { 6 | c.WrString(dest, "0") 7 | return 8 | } 9 | if value == 0 { 10 | c.wrByte(dest, '0') 11 | return 12 | } 13 | var buf [65]byte 14 | pos := len(buf) 15 | for value > 0 { 16 | pos-- 17 | digit := value % uint64(base) 18 | if digit < 10 { 19 | buf[pos] = byte('0' + digit) 20 | } else { 21 | buf[pos] = byte('a' + digit - 10) 22 | } 23 | value /= uint64(base) 24 | } 25 | c.wrBytes(dest, buf[pos:]) 26 | } 27 | 28 | // toUint64 converts various integer types to uint64 with validation 29 | func (c *Conv) toUint64(arg any) (uint64, bool) { 30 | switch v := arg.(type) { 31 | case uint: 32 | return uint64(v), true 33 | case uint8: 34 | return uint64(v), true 35 | case uint16: 36 | return uint64(v), true 37 | case uint32: 38 | return uint64(v), true 39 | case uint64: 40 | return v, true 41 | case int: 42 | return uint64(v), true 43 | case int8: 44 | return uint64(v), true 45 | case int16: 46 | return uint64(v), true 47 | case int32: 48 | return uint64(v), true 49 | case int64: 50 | return uint64(v), true 51 | default: 52 | return 0, false 53 | } 54 | } 55 | 56 | // Uint converts the value to an unsigned integer with optional base specification. 57 | // If no base is provided, base 10 is used. Supports bases 2-36. 58 | // Returns the converted uint and any error that occurred during conversion. 59 | func (c *Conv) Uint(base ...int) (uint, error) { 60 | val := c.parseIntBase(base...) 61 | if val < 0 || val > 4294967295 { 62 | return 0, c.wrErr(D.Number, D.Overflow) 63 | } 64 | if c.hasContent(BuffErr) { 65 | return 0, c 66 | } 67 | return uint(val), nil 68 | } 69 | 70 | // Uint32 extrae el valor del buffer de salida y lo convierte a uint32. 71 | func (c *Conv) Uint32(base ...int) (uint32, error) { 72 | val := c.parseIntBase(base...) 73 | if val < 0 || val > 4294967295 { 74 | return 0, c.wrErr(D.Number, D.Overflow) 75 | } 76 | if c.hasContent(BuffErr) { 77 | return 0, c 78 | } 79 | return uint32(val), nil 80 | } 81 | 82 | // Uint64 extrae el valor del buffer de salida y lo convierte a uint64. 83 | func (c *Conv) Uint64(base ...int) (uint64, error) { 84 | val := c.parseIntBase(base...) 85 | if c.hasContent(BuffErr) { 86 | return 0, c 87 | } 88 | return uint64(val), nil 89 | } 90 | -------------------------------------------------------------------------------- /benchmark/tinygo.unsafe.pointer.md: -------------------------------------------------------------------------------- 1 | TinyGo 0.37.0, al compilar a WebAssembly con interoperabilidad con JavaScript usando syscall/js, soporta el uso de `unsafe.Pointer`, `unsafe.String()` y `unsafe.SliceData()` de la librería estándar. 2 | 3 | # Soporte en TinyGo 0.37.0 4 | 5 | TinyGo 0.37.0 (Go 1.24) **sí soporta** `unsafe.Pointer`, `unsafe.String` y `unsafe.SliceData`. De hecho, estas funciones (añadidas en Go 1.20) fueron incorporadas al compilador TinyGo en enero de 2023. La documentación oficial de TinyGo indica explícitamente que estas funciones están *“fully supported”* (es decir, “totalmente compatibles”). Se recomienda usarlas para convertir entre punteros y slices/strings sin copiar, en lugar de usar los antiguos `reflect.StringHeader` o `reflect.SliceHeader`. 6 | 7 | # Restricciones y errores conocidos 8 | 9 | No hay reportes de **errores de compilación** específicos en el target WASM/js al usar estas funciones. TinyGo compila el código WASM estándar con `syscall/js` sin rechazar `unsafe.Pointer`, `unsafe.String` o `unsafe.SliceData`. Un detalle importante es que TinyGo define internamente los campos `Len`/`Cap` de un slice como `uintptr` en lugar de `int`, por lo que cualquier código legacy que manipule directamente los *SliceHeader* puede fallar. Por ello la guía advierte **no usar** esos *Header* y sí las funciones `unsafe.SliceData`/`unsafe.String` como se indicó arriba. En la interoperabilidad WASM–JavaScript se debe respetar la semántica de memoria de WebAssembly (por ejemplo, pasar offsets o usar `syscall/js.CopyBytesToJS`), pero esto es independiente del soporte de TinyGo para las funciones `unsafe`. 10 | 11 | # Workarounds documentados 12 | 13 | La “solución” oficial consiste en usar las funciones `unsafe.Slice`, `unsafe.SliceData` y `unsafe.String` para hacer conversiones sin copiar datos. Por ejemplo, TinyGo muestra cómo convertir un `string` a `[]byte` con `unsafe.Slice(unsafe.StringData(s), len(s))`. No se requieren hacks adicionales: basta usar estas funciones de `unsafe` como indica la documentación. En resumen, TinyGo 0.37.0 admite plenamente `unsafe.Pointer`, `unsafe.String()` y `unsafe.SliceData()` en proyectos WASM/JS; solo hay que tener en cuenta la recomendación de TinyGo de evitar los reflect.Headers y preferir las funciones `unsafe` mencionadas. 14 | 15 | **Fuentes:** Documentación oficial y notas de TinyGo sobre compatibilidad y el cambio Go 1.20; changelog de TinyGo (enero 2023) incorporando `unsafe.SliceData` y `unsafe.String`. 16 | -------------------------------------------------------------------------------- /docs/API_FILEPATH.md: -------------------------------------------------------------------------------- 1 | # Filepath Package Equivalents 2 | 3 | Replace common `filepath` package functions with fmt equivalents: 4 | 5 | | Go Standard | fmt Equivalent | 6 | |-------------|----------------------| 7 | | `filepath.Base()` | `Convert(path).PathBase().String()` | 8 | | `filepath.Join()` | `PathJoin("a", "b", "c").String()` — variadic function, zero heap allocation for ≤8 elements | 9 | 10 | ## PathBase (fluent API) 11 | 12 | Use `Convert(path).PathBase().String()` to get the last element of a path. 13 | 14 | Examples: 15 | 16 | ```go 17 | Convert("/a/b/c.txt").PathBase().String() // -> "c.txt" 18 | Convert("folder/file.txt").PathBase().String() // -> "file.txt" 19 | Convert("").PathBase().String() // -> "." 20 | Convert(`c:\\file program\\app.exe`).PathBase().String() // -> "app.exe" 21 | ``` 22 | 23 | ## PathJoin (cross-platform path joining) 24 | 25 | Standalone function with variadic string arguments. 26 | Returns *Conv for method chaining with transformations like ToLower(). 27 | Uses fixed array for zero heap allocation (≤8 elements). 28 | Detects separator ("/" or "\\") automatically and avoids duplicates. 29 | 30 | Examples: 31 | 32 | ```go 33 | PathJoin("a", "b", "c").String() // -> "a/b/c" 34 | PathJoin("/root", "sub", "file").String() // -> "/root/sub/file" 35 | PathJoin(`C:\dir`, "file").String() // -> `C:\dir\file` 36 | PathJoin(`\\server`, "share", "file").String() // -> `\\server\share\file` 37 | 38 | // Typical use: normalize path case with ToLower() in the same chain 39 | PathJoin("A", "B", "C").ToLower().String() // -> "a/b/c" 40 | ``` 41 | 42 | ## Path Extension 43 | 44 | Get the file extension (including the leading dot) from a path. Use the 45 | fluent API form `Convert(path).PathExt().String()` which reads the path 46 | from the Conv buffer and returns only the extension (or empty string). 47 | 48 | Examples: 49 | 50 | ```go 51 | Convert("file.txt").PathExt().String() // -> ".txt" 52 | Convert("/path/to/archive.tar.gz").PathExt().String() // -> ".gz" 53 | Convert(".bashrc").PathExt().String() // -> "" (hidden file, no ext) 54 | Convert("noext").PathExt().String() // -> "" 55 | Convert(`C:\\dir\\app.exe`).PathExt().String() // -> ".exe" 56 | 57 | // Typical use: normalize extension case in the same chain. For example, 58 | // when the extension is uppercase you can lower-case it immediately: 59 | Convert("file.TXT").PathExt().ToLower().String() // -> ".txt" 60 | ``` -------------------------------------------------------------------------------- /kind.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | // Kind represents the specific Kind of type that a Type represents (private) 4 | // Unified with convert.go Kind, using K prefix for fmt naming convention. 5 | // 6 | // IMPORTANT: The order and values of Kind must NOT be changed. 7 | // These values are used in tinyreflect, a minimal version of reflectlite from the Go standard library. 8 | // Keeping the order and values identical ensures compatibility with code and data shared between tinystring and tinyreflect. 9 | type Kind uint8 10 | 11 | // Kind exposes the Kind constants as fields for external use, while keeping the underlying type and values private. 12 | var K = struct { 13 | Invalid Kind 14 | Bool Kind 15 | Int Kind 16 | Int8 Kind 17 | Int16 Kind 18 | Int32 Kind 19 | Int64 Kind 20 | Uint Kind 21 | Uint8 Kind 22 | Uint16 Kind 23 | Uint32 Kind 24 | Uint64 Kind 25 | Uintptr Kind 26 | Float32 Kind 27 | Float64 Kind 28 | Complex64 Kind 29 | Complex128 Kind 30 | Array Kind 31 | Chan Kind 32 | Func Kind 33 | Interface Kind 34 | Map Kind 35 | Pointer Kind 36 | Slice Kind 37 | String Kind 38 | Struct Kind 39 | UnsafePointer Kind 40 | }{ 41 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 42 | } 43 | 44 | // kindNames provides string representations for each Kind value 45 | var kindNames = []string{ 46 | "invalid", // 0 47 | "bool", // 1 48 | "int", // 2 49 | "int8", // 3 50 | "int16", // 4 51 | "int32", // 5 52 | "int64", // 6 53 | "uint", // 7 54 | "uint8", // 8 55 | "uint16", // 9 56 | "uint32", // 10 57 | "uint64", // 11 58 | "uintptr", // 12 59 | "float32", // 13 60 | "float64", // 14 61 | "complex64", // 15 62 | "complex128", // 16 63 | "array", // 17 64 | "chan", // 18 65 | "func", // 19 66 | "interface", // 20 67 | "map", // 21 68 | "ptr", // 22 69 | "slice", // 23 70 | "string", // 24 71 | "struct", // 25 72 | "unsafe.Pointer", // 26 73 | } 74 | 75 | // String returns the name of the Kind as a string 76 | func (k Kind) String() string { 77 | if int(k) >= 0 && int(k) < len(kindNames) { 78 | return kindNames[k] 79 | } 80 | return "invalid" 81 | } 82 | -------------------------------------------------------------------------------- /IDorPrimaryKey_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import "testing" 4 | 5 | // IDorPrimaryKeyTestCases returns test cases for IDorPrimaryKey function 6 | func IDorPrimaryKeyTestCases() []struct { 7 | tableName string 8 | fieldName string 9 | expectedID bool 10 | expectedPK bool 11 | } { 12 | return []struct { 13 | tableName string 14 | fieldName string 15 | expectedID bool 16 | expectedPK bool 17 | }{ 18 | // Test "ID" field (case insensitive) 19 | {"user", "ID", true, true}, 20 | {"user", "id", true, true}, 21 | {"user", "Id", true, true}, 22 | {"user", "iD", true, true}, 23 | 24 | // Test "id_TABLE_NAME" pattern (case insensitive) 25 | {"user", "id_user", true, true}, 26 | {"user", "ID_USER", true, true}, 27 | {"user", "Id_User", true, true}, 28 | {"product", "id_product", true, true}, 29 | {"product", "ID_PRODUCT", true, true}, 30 | 31 | // Test "idTABLE_NAME" pattern (case insensitive) 32 | {"user", "iduser", true, true}, 33 | {"user", "IDUSER", true, true}, 34 | {"user", "Iduser", true, true}, 35 | {"product", "idproduct", true, true}, 36 | {"product", "IDPRODUCT", true, true}, 37 | 38 | // Test "TABLE_NAMEid" pattern (case insensitive) 39 | {"user", "userid", false, true}, 40 | {"user", "USERID", false, true}, 41 | {"user", "Userid", false, true}, 42 | {"user", "user_id", false, true}, 43 | {"user", "USER_ID", false, true}, 44 | {"product", "productid", false, true}, 45 | {"product", "PRODUCTID", false, true}, 46 | 47 | // Test non-ID fields 48 | {"user", "name", false, false}, 49 | {"user", "email", false, false}, 50 | {"user", "id_other", true, false}, // Starts with id but not PK for user 51 | {"user", "", false, false}, // Empty field name 52 | {"", "ID", true, false}, 53 | {"i", "i", false, false}, 54 | } 55 | } 56 | 57 | func TestIDorPrimaryKey(t *testing.T) { 58 | testCases := IDorPrimaryKeyTestCases() 59 | 60 | for _, tc := range testCases { 61 | t.Run(tc.tableName+"_"+tc.fieldName, func(t *testing.T) { 62 | isID, isPK := IDorPrimaryKey(tc.tableName, tc.fieldName) 63 | if isID != tc.expectedID || isPK != tc.expectedPK { 64 | t.Errorf("IDorPrimaryKey(%q, %q) = (%v, %v); want (%v, %v)", 65 | tc.tableName, tc.fieldName, isID, isPK, tc.expectedID, tc.expectedPK) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func BenchmarkIDorPrimaryKey(b *testing.B) { 72 | testCases := IDorPrimaryKeyTestCases() 73 | 74 | b.ResetTimer() 75 | for i := 0; i < b.N; i++ { 76 | for _, tc := range testCases { 77 | IDorPrimaryKey(tc.tableName, tc.fieldName) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /benchmark/memory-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | BENCHMARK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | MEMORY_BENCH_DIR="$BENCHMARK_DIR/bench-memory-alloc" 7 | 8 | # Function to get the correct analyzer binary name based on OS 9 | get_analyzer_name() { 10 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then 11 | echo "analyzer.exe" 12 | else 13 | echo "analyzer" 14 | fi 15 | } 16 | 17 | ANALYZER_BINARY=$(get_analyzer_name) 18 | 19 | echo "🧠 Running memory allocation benchmarks..." 20 | 21 | # Run standard library benchmarks 22 | echo "📊 Running standard library benchmarks..." 23 | cd "$MEMORY_BENCH_DIR/standard" 24 | STANDARD_RESULTS=$(go test -bench=. -benchmem | grep -E '^Benchmark') 25 | 26 | # Run fmt benchmarks 27 | echo "📊 Running fmt benchmarks..." 28 | cd "$MEMORY_BENCH_DIR/tinystring" 29 | TINYSTRING_RESULTS=$(go test -bench=. -benchmem | grep -E '^Benchmark') 30 | 31 | # Generate memory benchmark section for README 32 | echo "📝 Generating memory benchmark results..." 33 | 34 | # Create temporary benchmark results file 35 | TEMP_RESULTS="$BENCHMARK_DIR/benchmark_results.md" 36 | 37 | cat > "$TEMP_RESULTS" << EOF 38 | 39 | ## Memory Allocation Benchmarks 40 | 41 | ### Standard Library vs fmt Performance 42 | 43 | #### String Processing Benchmarks 44 | \`\`\` 45 | Standard Library: 46 | $STANDARD_RESULTS 47 | 48 | fmt: 49 | $TINYSTRING_RESULTS 50 | \`\`\` 51 | 52 | ### Performance Analysis 53 | 54 | The benchmarks show memory allocation differences between: 55 | - **Standard Library**: Traditional Go string operations using \`strings\`, \`fmt\`, \`strconv\` packages 56 | - **fmt**: Custom implementations optimized for minimal allocations 57 | - **fmt Pointers**: Zero-allocation operations using string pointer modifications 58 | 59 | **Key Benefits:** 60 | - 🔥 **Reduced Allocations**: fmt minimizes memory allocations through efficient implementations 61 | - ⚡ **Pointer Optimization**: Using \`Apply()\` method with string pointers eliminates temporary allocations 62 | - 📦 **Memory Efficiency**: Lower memory footprint especially important for embedded systems and WebAssembly 63 | 64 | EOF 65 | 66 | echo "✅ Memory benchmark results generated" 67 | echo "📄 Results saved to: $TEMP_RESULTS" 68 | 69 | # Now update the main README with these results 70 | cd "$BENCHMARK_DIR" 71 | go build -o "$ANALYZER_BINARY" . 72 | ./"$ANALYZER_BINARY" memory "$TEMP_RESULTS" 73 | 74 | # Clean up temporary file 75 | # rm -f "$TEMP_RESULTS" 76 | 77 | echo "✅ Memory benchmarks completed and README updated" 78 | -------------------------------------------------------------------------------- /memory.GetStringZeroCopy_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "testing" 5 | "unsafe" 6 | ) 7 | 8 | func TestGetStringZeroCopy(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | dest BuffDest 12 | input string 13 | expected string 14 | }{ 15 | {"BuffOut basic", BuffOut, "hello world", "hello world"}, 16 | {"BuffWork basic", BuffWork, "test string", "test string"}, 17 | {"BuffErr basic", BuffErr, "error message", "error message"}, 18 | {"BuffOut empty", BuffOut, "", ""}, 19 | {"BuffWork empty", BuffWork, "", ""}, 20 | {"BuffErr empty", BuffErr, "", ""}, 21 | {"BuffOut unicode", BuffOut, "héllo wörld", "héllo wörld"}, 22 | {"BuffWork unicode", BuffWork, "tëst strïng", "tëst strïng"}, 23 | {"BuffErr unicode", BuffErr, "ërror mëssagë", "ërror mëssagë"}, 24 | } 25 | 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | c := GetConv() 29 | defer c.PutConv() 30 | 31 | // Write input to the specified buffer 32 | c.WrString(tt.dest, tt.input) 33 | 34 | // Get zero-copy string 35 | result := c.GetStringZeroCopy(tt.dest) 36 | 37 | // Check correctness 38 | if result != tt.expected { 39 | t.Errorf("GetStringZeroCopy(%v) = %q, want %q", tt.dest, result, tt.expected) 40 | } 41 | 42 | // Verify zero-allocation: string data should point to buffer 43 | if len(tt.input) > 0 { 44 | var bufferPtr unsafe.Pointer 45 | switch tt.dest { 46 | case BuffOut: 47 | bufferPtr = unsafe.Pointer(&c.out[0]) 48 | case BuffWork: 49 | bufferPtr = unsafe.Pointer(&c.work[0]) 50 | case BuffErr: 51 | bufferPtr = unsafe.Pointer(&c.err[0]) 52 | } 53 | stringPtr := unsafe.Pointer(unsafe.StringData(result)) 54 | if stringPtr != bufferPtr { 55 | t.Errorf("GetStringZeroCopy did not return zero-copy string for %v; pointers differ", tt.dest) 56 | } 57 | } 58 | 59 | // Ensure no corruption: string should remain valid 60 | // Since it's zero-copy, we don't modify buffer after getting string 61 | if result != tt.expected { 62 | t.Errorf("String corrupted after GetStringZeroCopy for %v", tt.dest) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func BenchmarkGetStringZeroCopy(b *testing.B) { 69 | c := GetConv() 70 | defer c.PutConv() 71 | 72 | // Prepare buffer with test data 73 | testString := "benchmark test string for zero copy" 74 | c.WrString(BuffOut, testString) 75 | 76 | b.ResetTimer() 77 | b.ReportAllocs() 78 | 79 | for i := 0; i < b.N; i++ { 80 | result := c.GetStringZeroCopy(BuffOut) 81 | _ = result // Prevent optimization 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /join_test.go: -------------------------------------------------------------------------------- 1 | package fmt_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/tinywasm/fmt" 7 | ) 8 | 9 | func TestJoinMethod(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input []string 13 | sep string 14 | expected string 15 | }{ 16 | { 17 | name: "Join with default space separator", 18 | input: []string{"Hello", "World"}, 19 | sep: "", 20 | expected: "Hello World", 21 | }, 22 | { 23 | name: "Join with custom separator", 24 | input: []string{"hello", "world", "example"}, 25 | sep: "-", 26 | expected: "hello-world-example", 27 | }, 28 | { 29 | name: "Join with semicolon separator", 30 | input: []string{"apple", "orange", "banana"}, 31 | sep: ";", 32 | expected: "apple;orange;banana", 33 | }, 34 | { 35 | name: "Empty slice", 36 | input: []string{}, 37 | sep: ",", 38 | expected: "", 39 | }, 40 | { 41 | name: "Single element", 42 | input: []string{"test"}, 43 | sep: ":", 44 | expected: "test", 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | var out string 51 | if tt.sep == "" { 52 | out = Convert(tt.input).Join().String() 53 | } else { 54 | out = Convert(tt.input).Join(tt.sep).String() 55 | } 56 | 57 | if out != tt.expected { 58 | t.Errorf("Join test %q: expected %q, got %q", 59 | tt.name, tt.expected, out) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestJoinChainMethods(t *testing.T) { 66 | tests := []struct { 67 | name string 68 | input []string 69 | function func([]string) string 70 | expected string 71 | }{ 72 | { 73 | name: "Join with ToUpper", 74 | input: []string{"hello", "world"}, 75 | expected: "HELLO WORLD", 76 | function: func(input []string) string { 77 | return Convert(input).Join().ToUpper().String() 78 | }, 79 | }, 80 | { 81 | name: "Join with custom separator and ToLower", 82 | input: []string{"HELLO", "WORLD"}, 83 | expected: "hello-world", 84 | function: func(input []string) string { 85 | return Convert(input).Join("-").ToLower().String() 86 | }, 87 | }, 88 | { 89 | name: "Join with Capitalize", 90 | input: []string{"hello", "world"}, 91 | expected: "Hello World", 92 | function: func(input []string) string { 93 | return Convert(input).Join().Capitalize().String() 94 | }, 95 | }, 96 | } 97 | 98 | for _, tt := range tests { 99 | t.Run(tt.name, func(t *testing.T) { 100 | out := tt.function(tt.input) 101 | if out != tt.expected { 102 | t.Errorf("Chain test %q: expected %q, got %q", 103 | tt.name, tt.expected, out) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /docs/API_HTML.md: -------------------------------------------------------------------------------- 1 | # HTML Generation & Escaping 2 | 3 | fmt provides utilities for generating and escaping HTML content safely and efficiently. 4 | 5 | ## HTML Generation 6 | 7 | The `Html` function creates HTML strings with support for concatenation, formatting, and localization. 8 | 9 | ### Features 10 | 11 | - **Concatenation**: Joins arguments without spaces (unlike `Translate` or `Err`). 12 | - **Formatting**: Supports `printf`-style formatting if the first argument is a format string. 13 | - **Localization**: Supports `LocStr` translation and explicit language selection. 14 | 15 | ### Usage 16 | 17 | ```go 18 | // 1. Simple Concatenation 19 | Html("
", "content", "
").String() 20 | // -> "
content
" 21 | 22 | // 2. Formatting (printf-style) 23 | Html("
", "my-class").String() 24 | // -> "
" 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 `&` -> `&amp;`). 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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | License 13 | 14 | MIT 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Go 26 | 27 | 1.22.0 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Tests 39 | 40 | Passing 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Coverage 52 | 53 | 75% 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Race 65 | 66 | Clean 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Vet 78 | 79 | OK 81 | 82 | 83 | 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 | Project Badges 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 | --------------------------------------------------------------------------------