str

package module
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: MIT Imports: 7 Imported by: 6

Image README

CI Go Reference Go Report Card

str

Pipeline-first string toolkit for Go. Compose transformations, not calls.

normalize := str.Pipe(
    str.TrimSpace,
    str.CollapseWhitespace,
    str.ToSnakeCase,
    str.Truncate(64, "..."),
)
slug := normalize("  HelloWorld  ") // "hello_world"

Every function is func(string) string. Compose them with Pipe. Zero dependencies.

Install

go get github.com/schigh/str@latest

Compose

Pipe composes multiple transformers into a single function, applied left to right.

format := str.Pipe(str.TrimSpace, str.PadLeft("0", 8))
format("  42  ") // "00000042"

PipeErr composes fallible transformers. Short-circuits on the first error.

PipeIf applies a transformation only when a predicate is true. Otherwise passes through.

isLong := func(s string) bool { return len([]rune(s)) > 100 }
clean := str.Pipe(
    str.TrimSpace,
    str.PipeIf(isLong, str.Truncate(100, "...")),
)

PipeUnless is the inverse: applies when the predicate is false.

PipeMap splits a string, transforms each part, and rejoins.

trimLines := str.PipeMap(str.Lines, "\n", str.TrimSpace)
trimLines("  hello  \n  world  ") // "hello\nworld"

Built-in splitters: Lines (split on \n) and Words (split on whitespace).

Transform

ToSnakeCase / ToCamelCase / ToPascalCase / ToKebabCase / ToTitleCase / ToScreamingSnake

str.ToSnakeCase("HTMLParser")   // "html_parser"
str.ToCamelCase("hello_world")  // "helloWorld"
str.ToPascalCase("hello_world") // "HelloWorld"
str.ToKebabCase("HelloWorld")   // "hello-world"
str.ToTitleCase("hello_world")  // "Hello World"
str.ToScreamingSnake("hello")   // "HELLO"

Handles acronyms (ID, URL, HTTP, HTML, API, JSON, etc.), digit boundaries, and Unicode letters.

Every case function has a *With variant for custom acronyms:

toSnake := str.ToSnakeCaseWith(str.WithAcronyms("DAO", "NFT", "DEFI"))
toSnake("DAOVoting") // "dao_voting"

ToTitleCaseEnglish skips articles, conjunctions, and prepositions mid-sentence:

str.ToTitleCaseEnglish("a tale of two cities") // "A Tale of Two Cities"

Reverse reverses runes in a string.

str.Reverse("hello") // "olleh"

CollapseWhitespace collapses all Unicode whitespace to a single space.

str.CollapseWhitespace("hello   \t\n  world") // "hello world"

SlugifyASCII generates URL-safe ASCII slugs.

str.SlugifyASCII("Hello, World!") // "hello-world"

NaturalSortKey returns a key that sorts naturally (numbers in numeric order).

str.NaturalSortKey("file2")  // sorts before NaturalSortKey("file10")

Format

PadLeft / PadRight pad to a target width (runes). Multi-char pads are truncated to hit exact width.

str.PadLeft("0", 8)("1234")   // "00001234"
str.PadRight(".", 10)("hello") // "hello....."

Truncate truncates to a max length (runes) with an ellipsis. Length includes the ellipsis.

str.Truncate(8, "...")("hello world") // "hello..."

Substring extracts a substring with negative indexing support.

str.Substring(0, 3)("hello")  // "hel"
str.Substring(-3, 2)("abcde") // "cd"

Wrap wraps text at a given width with configurable behavior.

str.Wrap(40)("long text...")                           // wraps before words (default)
str.Wrap(40, str.WrapAfterWord)("long text...")        // wraps after words
str.Wrap(76, str.WrapHardBreak)("base64encoded...")    // hard break at width

Pluralize selects singular or plural form based on count.

str.Pluralize(1, "items")("item") // "item"
str.Pluralize(5, "items")("item") // "items"

Generate

RandomToken generates cryptographically random strings using crypto/rand.

str.RandomToken()                                                    // 20 alphanumeric chars
str.RandomToken(str.WithLength(32), str.WithCharset("0123456789"))   // 32 digits
str.RandomToken(str.WithPrefix("tok_"), str.WithLength(16))          // "tok_" + 16 random chars

FuzzyMatch scores haystack entries against a needle using JaroWinkler similarity. Returns matches with score >= 0.7, sorted by score descending. Case-insensitive.

matches := str.FuzzyMatch("helo", []string{"hello", "help", "world"})
// matches[0].Value == "hello", matches[0].Score ≈ 0.93

FuzzyMatchAll returns all non-zero scores (no threshold).

Measure

Levenshtein returns the edit distance between two strings.

str.Levenshtein("kitten", "sitting") // 3

JaroWinkler returns a similarity score between 0.0 and 1.0.

str.JaroWinkler("martha", "marhta") // ~0.96

Pipeline Patterns

// Normalize user input for storage
var normalizeInput = str.Pipe(
    str.TrimSpace,
    str.CollapseWhitespace,
)

// Generate URL slugs
var slugify = str.Pipe(
    str.TrimSpace,
    str.CollapseWhitespace,
    str.SlugifyASCII,
)

// Format log fields
var formatField = str.Pipe(
    str.TrimSpace,
    str.Truncate(50, "..."),
    str.PadRight(" ", 50),
)

Roadmap

Future releases may add: transliteration, grapheme-aware reverse, Unicode-aware slugify, word/line diff, and template helpers.

License

MIT

Image Documentation

Overview

Package str is a pipeline-first string toolkit for Go.

Every transformation function has the signature func(string) string, making them composable with Pipe. Functions that need configuration use partial application to return func(string) string.

All length, width, and index parameters count runes, not bytes. All functions are safe for concurrent use. No function panics for any input.

Compose

Pipe and PipeErr compose multiple transformations into a single function.

Transform

Case conversion, reversal, whitespace normalization, and slug generation.

Format

Padding, truncation, substring extraction, and word wrapping.

Measure

String distance and similarity algorithms.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func CollapseWhitespace

func CollapseWhitespace(s string) string

CollapseWhitespace collapses all runs of Unicode whitespace (spaces, tabs, newlines, vertical tabs, etc.) to a single ASCII space. Uses unicode.IsSpace for classification. Leading and trailing whitespace is also collapsed to a single space if present, then trimmed.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.CollapseWhitespace("hello   \t\n  world"))
}
Output:
hello world

func JaroWinkler

func JaroWinkler(a, b string) float64

JaroWinkler returns the Jaro-Winkler similarity between two strings. Returns a value between 0.0 (completely different) and 1.0 (identical). Uses the standard Winkler prefix bonus scaling factor p=0.1 with max prefix length 4.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Printf("%.2f\n", str.JaroWinkler("hello", "hello"))
}
Output:
1.00

func Levenshtein

func Levenshtein(a, b string) int

Levenshtein returns the edit distance between two strings. Uses a 2-row rolling array for O(min(m,n)) space complexity.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.Levenshtein("kitten", "sitting"))
}
Output:
3

func Lines

func Lines(s string) []string

Lines splits a string on newlines. Empty string returns []string{""}. Splits on "\n" only; "\r" is preserved if present.

func NaturalSortKey

func NaturalSortKey(s string) string

NaturalSortKey returns a string key that sorts naturally when compared lexicographically. Numeric segments (ASCII digits) are zero-padded to 20 digits. Case is folded to lowercase. "file2" sorts before "file10".

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.NaturalSortKey("file2"))
}
Output:
file00000000000000000002

func PadLeft

func PadLeft(padStr string, width int) func(string) string

PadLeft returns a function that pads the input string on the left with the given pad string until it reaches the specified width (in runes). Multi-character pad strings are truncated on the final repetition to hit exactly width. Empty pad string or width <= input length returns input unchanged.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.PadLeft("0", 8)("1234"))
}
Output:
00001234
Example (Pipe)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	format := str.Pipe(str.TrimSpace, str.PadLeft("0", 8))
	fmt.Println(format("  42  "))
}
Output:
00000042

func PadRight

func PadRight(padStr string, width int) func(string) string

PadRight returns a function that pads the input string on the right with the given pad string until it reaches the specified width (in runes).

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.PadRight(".", 10)("hello"))
}
Output:
hello.....

func Pipe

func Pipe(fns ...func(string) string) func(string) string

Pipe composes multiple string transformers into a single function. Functions are applied left to right. With zero functions, Pipe returns an identity function that returns the input unchanged.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	normalize := str.Pipe(
		str.TrimSpace,
		str.CollapseWhitespace,
		str.ToSnakeCase,
	)
	fmt.Println(normalize("  HelloWorld  "))
}
Output:
hello_world

func PipeErr

func PipeErr(fns ...func(string) (string, error)) func(string) (string, error)

PipeErr composes multiple fallible string transformers into a single function. Functions are applied left to right. Short-circuits on the first non-nil error; the error is returned unwrapped from the failing function. With zero functions, PipeErr returns an identity function that returns the input and nil.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	upper := func(s string) (string, error) { return str.Reverse(s), nil }
	fn := str.PipeErr(upper)
	result, _ := fn("hello")
	fmt.Println(result)
}
Output:
olleh

func PipeIf

func PipeIf(predicate func(string) bool, fn func(string) string) func(string) string

PipeIf returns a transformer that applies fn only when the predicate returns true. If the predicate returns false, the input passes through unchanged. Nil predicate is treated as always-false (no-op). Nil fn is a no-op.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	isLong := func(s string) bool { return len([]rune(s)) > 10 }
	shorten := str.Pipe(
		str.TrimSpace,
		str.PipeIf(isLong, str.Truncate(10, "...")),
	)
	fmt.Println(shorten("  hello  "))
	fmt.Println(shorten("  this is a long string  "))
}
Output:
hello
this is...

func PipeMap

func PipeMap(split func(string) []string, join string, fn func(string) string) func(string) string

PipeMap returns a transformer that splits the input using split, applies fn to each part, and rejoins with the join string. Nil split or nil fn returns the input unchanged.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	trimLines := str.PipeMap(str.Lines, "\n", str.TrimSpace)
	fmt.Println(trimLines("  hello  \n  world  "))
}
Output:
hello
world
Example (Words)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	shoutWords := str.PipeMap(str.Words, " ", str.ToScreamingSnake)
	fmt.Println(shoutWords("hello world"))
}
Output:
HELLO WORLD

func PipeUnless

func PipeUnless(predicate func(string) bool, fn func(string) string) func(string) string

PipeUnless returns a transformer that applies fn only when the predicate returns false. If the predicate returns true, the input passes through unchanged. Nil predicate is treated as always-false, so fn always applies. Nil fn is a no-op.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	isEmpty := func(s string) bool { return s == "" }
	fn := str.PipeUnless(isEmpty, str.SlugifyASCII)
	fmt.Println(fn("Hello World"))
	fmt.Println(fn(""))
}
Output:
hello-world

func Pluralize

func Pluralize(count int, plural string) func(string) string

Pluralize returns a transformer that selects between the input (singular) and the given plural form based on count. Returns singular when count == 1, plural otherwise (including count == 0).

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.Pluralize(1, "items")("item"))
	fmt.Println(str.Pluralize(5, "items")("item"))
}
Output:
item
items

func RandomToken

func RandomToken(opts ...TokenOption) string

RandomToken generates a cryptographically random string. Uses crypto/rand for secure randomness. Panics if the OS entropy source fails.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	tok := str.RandomToken()
	fmt.Println(len(tok)) // always 20
}
Output:
20
Example (Options)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	tok := str.RandomToken(str.WithPrefix("tok_"), str.WithLength(6), str.WithCharset("0123456789"))
	fmt.Println(len(tok)) // "tok_" (4) + 6 digits
}
Output:
10

func Reverse

func Reverse(s string) string

Reverse returns the input string with runes in reverse order. This is rune-aware but not grapheme-cluster-aware; combining characters may reorder.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.Reverse("hello"))
}
Output:
olleh
Example (Pipe)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	rev := str.Pipe(str.TrimSpace, str.Reverse)
	fmt.Println(rev("  hello  "))
}
Output:
olleh

func SlugifyASCII

func SlugifyASCII(s string) string

SlugifyASCII generates a URL-safe ASCII slug from the input string. Lowercases, replaces spaces and punctuation with hyphens, collapses consecutive hyphens, and strips all non-ASCII characters.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.SlugifyASCII("Hello, World!"))
}
Output:
hello-world
Example (Pipe)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	slugify := str.Pipe(str.TrimSpace, str.CollapseWhitespace, str.SlugifyASCII)
	fmt.Println(slugify("  Hello,  World!  "))
}
Output:
hello-world

func Substring

func Substring(start, length int) func(string) string

Substring returns a function that extracts a substring from the input. Start and length are in runes. Negative start counts from the end (Python-style). Negative length returns empty string. Out-of-bounds indices clamp to string boundaries.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.Substring(0, 3)("hello"))
	fmt.Println(str.Substring(-3, 2)("abcde"))
}
Output:
hel
cd

func ToCamelCase

func ToCamelCase(s string) string
Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToCamelCase("hello_world"))
	fmt.Println(str.ToCamelCase("HTMLParser"))
}
Output:
helloWorld
htmlParser

func ToCamelCaseWith

func ToCamelCaseWith(opts ...CaseOption) func(string) string

ToCamelCaseWith returns a camelCase transformer using custom options.

func ToKebabCase

func ToKebabCase(s string) string
Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToKebabCase("HelloWorld"))
}
Output:
hello-world

func ToKebabCaseWith

func ToKebabCaseWith(opts ...CaseOption) func(string) string

ToKebabCaseWith returns a kebab-case transformer using custom options.

func ToPascalCase

func ToPascalCase(s string) string
Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToPascalCase("hello_world"))
}
Output:
HelloWorld

func ToPascalCaseWith

func ToPascalCaseWith(opts ...CaseOption) func(string) string

ToPascalCaseWith returns a PascalCase transformer using custom options.

func ToScreamingSnake

func ToScreamingSnake(s string) string
Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToScreamingSnake("helloWorld"))
}
Output:
HELLO_WORLD

func ToScreamingSnakeWith

func ToScreamingSnakeWith(opts ...CaseOption) func(string) string

ToScreamingSnakeWith returns a SCREAMING_SNAKE transformer using custom options.

func ToSnakeCase

func ToSnakeCase(s string) string
Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToSnakeCase("HTMLParser"))
	fmt.Println(str.ToSnakeCase("getHTTPSURL"))
	fmt.Println(str.ToSnakeCase("UserID"))
}
Output:
html_parser
get_https_url
user_id
Example (Pipe)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	slugify := str.Pipe(str.TrimSpace, str.ToSnakeCase)
	fmt.Println(slugify("  Hello World  "))
}
Output:
hello_world

func ToSnakeCaseWith

func ToSnakeCaseWith(opts ...CaseOption) func(string) string

ToSnakeCaseWith returns a snake_case transformer using custom options.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fn := str.ToSnakeCaseWith(str.WithAcronyms("DAO", "NFT"))
	fmt.Println(fn("DAOVoting"))
	fmt.Println(fn("NFTMarket"))
}
Output:
dao_voting
nft_market

func ToTitleCase

func ToTitleCase(s string) string
Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToTitleCase("hello_world"))
}
Output:
Hello World

func ToTitleCaseEnglish

func ToTitleCaseEnglish(s string) string

ToTitleCaseEnglish returns English-aware title case. Capitalizes all words except short function words (articles, conjunctions, prepositions) when they appear mid-sentence. First and last word are always capitalized.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.ToTitleCaseEnglish("a tale of two cities"))
	fmt.Println(str.ToTitleCaseEnglish("back_to_the_future"))
}
Output:
A Tale of Two Cities
Back to the Future

func ToTitleCaseWith

func ToTitleCaseWith(opts ...CaseOption) func(string) string

ToTitleCaseWith returns a Title Case transformer using custom options.

func TrimSpace

func TrimSpace(s string) string

TrimSpace removes leading and trailing whitespace from the input string. This is a re-export of strings.TrimSpace for pipeline convenience.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.TrimSpace("  hello  "))
}
Output:
hello

func Truncate

func Truncate(maxLen int, ellipsis string) func(string) string

Truncate returns a function that truncates the input string to maxLen runes. If the input is longer than maxLen, it is truncated and the ellipsis is appended. maxLen includes the ellipsis length. Counts runes, not bytes.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.Truncate(8, "...")("hello world"))
}
Output:
hello...
Example (Pipe)
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	shorten := str.Pipe(str.TrimSpace, str.Truncate(10, "..."))
	fmt.Println(shorten("  a]very long string  "))
}
Output:
a]very ...

func Words

func Words(s string) []string

Words splits a string on whitespace, matching strings.Fields behavior. Empty string returns nil.

func Wrap

func Wrap(width int, opts ...WrapOption) func(string) string

Wrap returns a function that wraps text at the specified width (in runes). Default behavior is WrapBeforeWord with "\n" line breaks. Width of 0 returns input unchanged.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	fmt.Println(str.Wrap(10, str.WrapHardBreak)("abcdefghijklmno"))
}
Output:
abcdefghij
klmno

Types

type CaseOption

type CaseOption func(*caseConfig)

CaseOption configures case conversion behavior.

func WithAcronyms

func WithAcronyms(words ...string) CaseOption

WithAcronyms replaces the default acronym list entirely. Users who want defaults plus custom acronyms must include both.

type Match

type Match struct {
	Score float64 // 0.0 to 1.0 (JaroWinkler similarity)
	Index int     // position in the haystack
	Value string  // the matched string
}

Match represents a fuzzy match result.

func FuzzyMatch

func FuzzyMatch(needle string, haystack []string) []Match

FuzzyMatch scores each haystack entry against the needle using JaroWinkler. Returns entries with Score >= 0.7, sorted by Score descending. Comparison is case-insensitive. Empty needle or haystack returns nil.

Example
package main

import (
	"fmt"

	"github.com/schigh/str"
)

func main() {
	hay := []string{"hello", "help", "world"}
	matches := str.FuzzyMatch("helo", hay)
	if len(matches) > 0 {
		fmt.Println(matches[0].Value)
	}
}
Output:
hello

func FuzzyMatchAll

func FuzzyMatchAll(needle string, haystack []string) []Match

FuzzyMatchAll is like FuzzyMatch but returns all entries with Score > 0.

type TokenOption

type TokenOption func(*tokenConfig)

TokenOption configures RandomToken behavior.

func WithCharset

func WithCharset(cs string) TokenOption

WithCharset sets the character set to sample from (default alphanumeric). Duplicate characters are deduplicated.

func WithLength

func WithLength(n int) TokenOption

WithLength sets the number of random characters (default 20). Prefix is not included in this count.

func WithPrefix

func WithPrefix(p string) TokenOption

WithPrefix sets a prefix prepended to the random characters.

type WrapOption

type WrapOption func(*wrapConfig)

WrapOption configures the behavior of Wrap.

var WrapAfterWord WrapOption = func(c *wrapConfig) { c.behavior = behaviorAfterWord }

WrapAfterWord wraps after the word that extends past the line width.

var WrapBeforeWord WrapOption = func(c *wrapConfig) { c.behavior = behaviorBeforeWord }

WrapBeforeWord wraps before the word that would extend past the line width.

var WrapHardBreak WrapOption = func(c *wrapConfig) { c.behavior = behaviorHardBreak }

WrapHardBreak breaks mid-word at exactly the line width.

func WithIndent

func WithIndent(indent string) WrapOption

WithIndent sets a prefix for continuation lines.

func WithLineBreak

func WithLineBreak(lb string) WrapOption

WithLineBreak sets the line break string (default "\n").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL