Awesome Go

strcase

CategoryMiscellaneous
SubcategoryStrings
Stars16

Case-insensitive implementation of the standard library's strings/bytes packages

About strcase

This large README is shown as a plain-text preview. Read the full README at the source

[![Tests](https://github.com/charlievieth/strcase/actions/workflows/test.yml/badge.svg)](https://github.com/charlievieth/strcase/actions/workflows/test.yml)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/charlievieth/strcase@master)
[![codecov](https://codecov.io/github/charlievieth/strcase/branch/master/graph/badge.svg?token=ZJ0FTBECVA)](https://codecov.io/github/charlievieth/strcase)

# strcase

Package [strcase](.) and [bytcase](bytcase/README.md) are case-insensitive and
Unicode aware implementations of the Go standard library's
[`strings`](https://pkg.go.dev/strings) and [`bytes`](https://pkg.go.dev/bytes)
packages that are accurate, fast, and never allocate memory.

Simple Unicode case-folding is used for all comparisons. This matches the behavior
of [strings.EqualFold](https://pkg.go.dev/strings#EqualFold) and [regexp.Regexp](https://pkg.go.dev/regexp#Regexp)
(when the pattern is compiled with the case-insensitive flag `(?i)`) and is more
accurate (and significantly more efficient) than using [strings.ToLower](https://pkg.go.dev/strings#ToLower) or
[strings.ToUpper](https://pkg.go.dev/strings#ToUpper) to normalize the needle /
haystack before searching.

**Note:** The bytcase package is analogous to the strcase package; whatever
applies to strcase also applies to bytcase. For simplicity, the documentation
primarily refers to strcase. Unless otherwise noted, all comments apply to both
packages.

## Overview

* Drop-in replacement for the [`strings`](https://pkg.go.dev/strings) and
  [`bytes`](https://pkg.go.dev/bytes) packages that provides Unicode
  aware case-insensitive matching.
* Simple Unicode case-folding is used for all comparisons - making it more
  accurate than using [strings.ToLower](https://pkg.go.dev/strings#ToLower) or
  [strings.ToUpper](https://pkg.go.dev/strings#ToUpper) for case-insensitivity.
  <!-- TODO: note the equivalent using the regexp package `(?i)` -->
* Any string matched by strcase or bytcase will also match with
  [strings.EqualFold](https://pkg.go.dev/strings#EqualFold) or
  [bytes.EqualFold](https://pkg.go.dev/bytes#EqualFold)
* Fast and optimized for amd64 and arm64. For non-pathological inputs strcase
  is only 25-50% slower than the strings package.
* On average strcase/bytcase are 25x faster than using than using a case-insensitive
  [regexp.Regexp](https://pkg.go.dev/regexp#Regexp) (see the below
  [benchmarks](#benchmarks) section).

## Installation

strcase is available using the standard `go get` command.

Install by running:
```sh
# strcase
go get github.com/charlievieth/strcase

# bytcase
go get github.com/charlievieth/strcase/bytcase
```

## Features

- Fast: strcase is optimized for `amd64` and `arm64` and strives to be no worse
  than 2x slower than the strings package and is often only 30-50% slower.
  - The `IndexByte`, `IndexNonASCII` and `Count` functions are implemented in
    assembly and use SSE/AVX2 on `amd64` and NEON on `arm64`.
  - Instead of using the [unicode](https://pkg.go.dev/unicode) package for case
    conversions strcase uses its own multiplicative hash tables that are roughly
    10x faster (at the cost of package size).
- Accurate: Unicode simple folding is used to determine equality.
  - Any matched text would also match with
    [`strings.EqualFold`](https://pkg.go.dev/strings#EqualFold).
- Zero allocation: none of the strcase functions allocate memory.
- Thoroughly tested and fuzzed.

## Additional Features

strcase provides two functons for checking if a string contains non-ASCII
characters that are highly optimized for amd64/arm64:

- [strcase.IndexNonASCII](https://pkg.go.dev/github.com/charlievieth/strcase#IndexNonASCII):
  IndexNonASCII returns the index of first non-ASCII rune in s, or -1 if s consists
  only of ASCII characters.
- [strcase.ContainsNonASCII](https://pkg.go.dev/github.com/charlievieth/strcase#ContainsNonASCII):
  ContainsNonASCII returns true if s contains any non-ASCII characters.

## Caveats

<!--
From: regexp
https://pkg.go.dev/regexp

All characters are UTF-8-encoded code points. Following utf8.DecodeRune,
each byte of an invalid UTF-8 sequence is treated as if it encoded utf8.RuneError (U+FFFD).
-->

All invalid UTF-8 sequences are considered equal. This is because Go converts
invalid UTF-8 sequences to the Unicode replacement character `0xFFFD`
([unicode.ReplacementChar](https://pkg.go.dev/unicode#pkg-constants) /
[utf8.RuneError](https://pkg.go.dev/unicode/utf8#pkg-constants)).
This occurs both when [ranging](https://go.dev/ref/spec#For_statements) over
a string or using the [utf8](https://pkg.go.dev/utf8) package's `Decode*`
functions.

```go
strcase.Compare("\xff", string(utf8.RuneError)) // returns 0
strcase.Index("a\xff", string(utf8.RuneError))  // returns 1
```

<!-- TODO: do we need this / mention that this does not really matter but is still tested -->
Thus it is the callers responsibility to ensure strcase functions are called
with valid UTF-8 strings and not arbitrary binary data.

## Performance

strcase aims to be seriously fast and can beat or match the performance of the
strings package in some benchmarks (EqualFold and IndexRune). Overall, strcase
tends to be only 30-50% slower than the strings package for non-pathological inputs.

#### Optimizations

- Instead of using the standard library's Unicode package, which uses a binary
  search for its lookup tables, strcase uses multiplicative hashing for its
  lookup tables. This is 10x faster at the cost of larger tables.
- Searching for runes (IndexRune) is a big determinant of the strcase packages
  performance. Instead of searching for runes by their first byte (like the
  strings package) strcase searches for the second byte, which is more unique,
  then looks backwards/forwards to complete the match.
   * **NB:** [CL 539116](https://go-review.googlesource.com/c/go/+/539116) added
   this logic to Go's strings/bytes packages and is available starting with Go 1.23.
- Package strcase is optimized for amd64 and arm64 and includes assembly
  implementations of `IndexByte`, `CountByte` and `IndexNonASCII` that
  leverage the same SIMD technologies used in the standard library (SSE, AVX2,
  NEON).
- On armv7l (Raspberry Pi), which we do not optimize for, the average
  performance penalty is only ~30%.

## Notes:
- All invalid Unicode points and invalid multibyte UTF-8 sequences are
 considered equal.
 This is because the [utf8](https://pkg.go.dev/unicode/utf8) package converts
 invalid runes and multibyte UTF-8 sequences to
 [`utf8.RuneError`](https://pkg.go.dev/unicode/utf8#RuneError).
  * This matches the behavior of [strings.EqualFold](https://pkg.go.dev/strings#EqualFold).
- `İ` (LATIN CAPITAL LETTER I WITH DOT ABOVE) and `ı`: (LATIN SMALL LETTER DOTLESS
  I) doe not fold to ASCII `[iI]` (U+0069 / U+0049)
   * This matches the behavior of [strings.EqualFold](https://pkg.go.dev/strings#EqualFold)
   * See [unicode.org/UCD/CaseFolding.txt](https://www.unicode.org/Public/UCD/latest/ucd/CaseFolding.txt)
     for an explanation, basically this folding is normally ignored for non-Turkic languages
- Kelvin `K` (U+212A) matches ASCII `K` and `k`
- Latin small letter long S `ſ` matches ASCII `S` and `s`

## Contributing / Hacking

Contributions are welcome. Please, see the [CONTRIBUTING](./CONTRIBUTING.md)
document for details.

## Benchmarks

<!-- WARN: make sure this renders correctly on GitHub -->

The following benchmarks were created using
[internal/benchtest](https://github.com/charlievieth/strcase/tree/master/internal/benchtest).
Additional, processor specific benchmarks can be found in
[internal/benchtest/results](https://github.com/charlievieth/strcase/tree/master/internal/benchtest/results).

<details>
<summary>regexp vs. strcase</summary>

```
goos: darwin
goarch: arm64
pkg: github.com/charlievieth/strcase/internal/benchtest
cpu: Apple M1 Max
                                 │  regexp.10.txt   │             strcase.10.txt             │
                                 │      sec/op      │    sec/op     vs base                  │
IndexRune-10                          587.45n ±  2%   11.15n ±  1%  -98.10% (p=0.000 n=10)
IndexRuneLongString-10                584.15n ±  2%   12.67n ±  1%  -97.83% (p=0.000 n=10)
IndexRuneFastPath-10                 673.300n ±  6%   5.130n ±  3%  -99.24% (p=0.000 n=10)
Index-10                             671.900n ±  2%   4.982n ±  1%  -99.26% (p=0.000 n=10)
EqualFold/ASCII-10                  2920.500n ±  1%   9.418n ±  2%  -99.68% (p=0.000 n=10)
EqualFold/UnicodePrefix-10           3892.00n ±  1%   32.35n ±  2%  -99.17% (p=0.000 n=10)
EqualFold/UnicodeSuffix-10           3885.50n ±  2%   26.17n ±  1%  -99.33% (p=0.000 n=10)
IndexHard1-10                          334.1µ ±  0%   340.4µ ±  0%   +1.90% (p=0.000 n=10)
IndexHard2-10                          9.963m ±  6%   1.362m ±  1%  -86.33% (p=0.000 n=10)
IndexHard3-10                         10.727m ±  0%   1.369m ±  1%  -87.24% (p=0.000 n=10)
IndexHard4-10                         10.385m ±  0%   1.361m ±  1%  -86.89% (p=0.000 n=10)
CountHard1-10                          338.1µ ±  1%   336.4µ ±  1%        ~ (p=0.218 n=10)
CountHard2-10                          9.844m ±  1%   1.365m ±  1%  -86.13% (p=0.000 n=10)
CountHard3-10                         10.684m ±  0%   1.362m ±  1%  -87.25% (p=0.000 n=10)
IndexTorture-10                     27787.99µ ±  1%   18.04µ ±  2%  -99.94% (p=0.000 n=10)
CountTorture-10                     27631.25µ ±  1%   17.86µ ±  2%  -99.94% (p=0.000 n=10)
CountTortureOverlapping-10         14204.297m ±  2%   4.058m ±  1%  -99.97% (p=0.000 n=10)
CountByte/10-10                      570.100n ±  1%   7.684n ±  1%  -98.65% (p=0.000 n=10)
CountByte/32-10                      964.050n ±  2%   4.501n ±  1%  -99.53% (p=0.000 n=10)
CountByte/4K-10                      63123.0n ±  1%   103.1n ±  0%  -99.84% (p=0.000 n=10)
CountByte/4M-10                     87646.31µ ±  0%   96.02µ ±  7%  -99.89% (p=0.000 n=10)
CountByte/64M-10                    1433.190m ±  1%   1.574m ±  1%  -99.89% (p=0.000 n=10)
IndexAnyASCII/1:1-10                 742.800n ±  2%   5.513n ±  1%  -99.26% (p=0.000 n=10)
IndexAnyASCII/1:2-10                 915.150n ±  2%   7.462n ±  0%  -99.18% (p=0.000 n=10)
IndexAnyASCII/1:4-10                1031.500n ±  7%   7.453n ±  0%  -99.28% (p=0.000 n=10)
IndexAnyASCII/1:8-10                1217.000n ±  2%   7.397n ±  0%  -99.39% (p=0.000 n=10)
IndexAnyASCII/1:16-10               1802.000n ±  2%   7.710n ±  1%  -99.57% (p=0.000 n=10)
IndexAnyASCII/1:32-10               3100.000n ±  4%   7.711n ±  0%  -99.75% (p=0.000 n=10)
IndexAnyASCII/1:64-10               5183.500n ±  2%   8.037n ±  0%  -99.84% (p=0.000 n=10)
IndexAnyASCII/16:1-10                731.000n ±  4%   5.457n ±  0%  -99.25% (p=0.000 n=10)
IndexAnyASCII/16:2-10                1096.00n ±  2%   13.44n ±  1%  -98.77% (p=0.000 n=10)
IndexAnyASCII/16:4-10                1231.00n ±  4%   15.39n ±  3%  -98.75% (p=0.000 n=10)
IndexAnyASCII/16:8-10                1429.50n ±  2%   19.38n ±  1%  -98.64% (p=0.000 n=10)
IndexAnyASCII/16:16-10               2009.50n ±  3%   34.40n ±  1%  -98.29% (p=0.000 n=10)
IndexAnyASCII/16:32-10               3325.00n ±  1%   61.90n ±  0%  -98.14% (p=0.000 n=10)
IndexAnyASCII/16:64-10                5380.5n ±  1%   124.8n ±  3%  -97.68% (p=0.000 n=10)
IndexAnyASCII/256:1-10               727.600n ±  2%   8.571n ±  3%  -98.82% (p=0.000 n=10)
IndexAnyASCII/256:2-10                4038.5n ±  1%   153.7n ±  0%  -96.19% (p=0.000 n=10)
IndexAnyASCII/256:4-10                4167.5n ±  1%   156.2n ±  1%  -96.25% (p=0.000 n=10)
IndexAnyASCII/256:8-10                4356.0n ±  1%   161.6n ±  0%  -96.29% (p=0.000 n=10)
IndexAnyASCII/256:16-10               4953.0n ±  1%   176.3n ±  3%  -96.44% (p=0.000 n=10)
IndexAnyASCII/256:32-10               6819.5n ±  4%   207.5n ±  1%  -96.96% (p=0.000 n=10)
IndexAnyASCII/256:64-10               8949.5n ±  6%   271.9n ±  0%  -96.96% (p=0.000 n=10)
IndexAnyUTF8/1:16-10                1860.500n ±  1%   7.296n ±  0%  -99.61% (p=0.000 n=10)
IndexAnyUTF8/16:16-10                2100.50n ±  1%   86.67n ±  0%  -95.87% (p=0.000 n=10)
IndexAnyUTF8/256:16-10                5902.0n ±  1%   119.8n ±  1%  -97.97% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic2-10      1451.35µ ±  1%   84.06µ ±  0%  -94.21% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic4-10      1529.07µ ±  1%   83.80µ ±  0%  -94.52% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic8-10      1349.62µ ± 18%   83.59µ ±  1%  -93.81% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic16-10     1234.51µ ±  1%   64.12µ ±  1%  -94.81% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic32-10     1196.96µ ±  1%   30.81µ ±  1%  -97.43% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic64-10     1169.90µ ±  8%   14.68µ ±  0%  -98.75% (p=0.000 n=10)
IndexByte_Bytes/10-10                  4.517n ± 20%
IndexByte_Bytes/32-10                  4.518n ±  1%
IndexByte_Bytes/4K-10                  81.42n ±  2%
IndexByte_Bytes/4M-10                  74.80µ ±  1%
IndexByte_Bytes/64M-10                 1.237m ±  5%
IndexRune_Bytes/10-10                  12.56n ±  1%   12.32n ±  0%   -1.91% (p=0.000 n=10)
IndexRune_Bytes/32-10                  12.59n ±  1%   12.32n ±  0%   -2.10% (p=0.000 n=10)
IndexRune_Bytes/4K-10                  85.06n ±  1%   83.56n ±  0%   -1.76% (p=0.000 n=10)
IndexRune_Bytes/4M-10                  65.05µ ±  1%   64.33µ ±  0%   -1.11% (p=0.030 n=10)
IndexRune_Bytes/64M-10                 1.149m ±  4%   1.169m ±  5%        ~ (p=0.089 n=10)
IndexRuneASCII_Bytes/10-10             6.439n ±  1%   6.388n ±  0%   -0.79% (p=0.025 n=10)
IndexRuneASCII_Bytes/32-10             6.462n ±  1%   6.369n ±  1%   -1.43% (p=0.000 n=10)
IndexRuneASCII_Bytes/4K-10             83.87n ±  0%   83.49n ± 11%        ~ (p=0.210 n=10)
IndexRuneASCII_Bytes/4M-10             75.19µ ±  2%   74.01µ ±  1%   -1.57% (p=0.023 n=10)
IndexRuneASCII_Bytes/64M-10            1.256m ±  5%   1.300m ±  2%        ~ (p=0.218 n=10)
IndexNonASCII_Bytes/10-10              3.063n ±  0%   2.967n ±  0%   -3.13% (p=0.000 n=10)
IndexNonASCII_Bytes/32-10              3.835n ±  2%   3.759n ±  0%   -1.98% (p=0.027 n=10)
IndexNonASCII_Bytes/4K-10              80.74n ±  1%   79.59n ±  0%   -1.44% (p=0.016 n=10)
IndexNonASCII_Bytes/4M-10              74.47µ ±  1%   74.51µ ±  1%        ~ (p=0.529 n=10)
IndexNonASCII_Bytes/64M-10             1.271m ±  4%   1.307m ±  2%        ~ (p=0.089 n=10)
geomean                                12.78µ         595.2n        -96.26%                ¹
¹ benchmark set differs from baseline; geomeans may not be comparable

                                 │ regexp.10.txt │                 strcase.10.txt                 │
                                 │      B/s      │       B/s         vs base                      │
IndexRune-10                       27.60Mi ±  2%    1454.81Mi ±  1%    +5171.18% (p=0.000 n=10)
IndexRuneLongString-10             191.0Mi ±  2%     8802.1Mi ±  1%    +4508.17% (p=0.000 n=10)
IndexRuneFastPath-10               25.50Mi ±  5%    3346.27Mi ±  3%   +13024.42% (p=0.000 n=10)
Index-10                                              3.365Gi ±  1%
EqualFold/ASCII-10                 3.920Mi ±  1%   1215.096Mi ±  2%   +30900.49% (p=0.000 n=10)
EqualFold/UnicodePrefix-10         4.411Mi ±  1%    530.591Mi ±  2%   +11929.51% (p=0.000 n=10)
EqualFold/UnicodeSuffix-10         4.416Mi ±  2%    656.018Mi ±  1%   +14757.13% (p=0.000 n=10)
IndexHard1-10                                         2.869Gi ±  0%
IndexHard2-10                                         734.1Mi ±  1%
IndexHard3-10                                         730.6Mi ±  1%
IndexHard4-10                                         734.5Mi ±  1%
CountHard1-10                      2.888Gi ±  1%      2.903Gi ±  1%            ~ (p=0.218 n=10)
CountHard2-10                      101.6Mi ±  1%      732.6Mi ±  1%     +621.19% (p=0.000 n=10)
CountHard3-10                      93.59Mi ±  0%     734.10Mi ±  1%     +684.35% (p=0.000 n=10)
IndexTorture-10                                       325.0Mi ±  2%
CountTorture-10                    214.8Ki ±  0%   336108.4Ki ±  2%  +156343.18% (p=0.000 n=10)
CountTortureOverlapping-10         214.8Ki ±  5%   756987.3Ki ±  1%  +352243.18% (p=0.000 n=10)
CountByte/10-10                    16.73Mi ±  1%    1240.98Mi ±  1%    +7318.84% (p=0.000 n=10)
CountByte/32-10                    31.66Mi ±  2%    6780.62Mi ±  1%   +21318.89% (p=0.000 n=10)
CountByte/4K-10                    61.88Mi ±  1%   37908.26Mi ±  0%   +61157.04% (p=0.000 n=10)
CountByte/4M-10                    45.64Mi ±  0%   41657.45Mi ±  7%   +91177.83% (p=0.000 n=10)
CountByte/64M-10                   44.66Mi ±  1%   40660.56Mi ±  1%   +90953.26% (p=0.000 n=10)
IndexPeriodic/IndexPeriodic2-10                       743.5Mi ±  0%
IndexPeriodic/IndexPeriodic4-10                       745.8Mi ±  0%
IndexPeriodic/IndexPeriodic8-10                       747.7Mi ±  1%
IndexPeriodic/IndexPeriodic16-10                      974.8Mi ±  1%
IndexPeriodic/IndexPeriodic32-10                      1.981Gi ±  1%
IndexPeriodic/IndexPeriodic64-10                      4.159Gi ±  0%
IndexByte_Bytes/10-10              2.062Gi ± 17%
IndexByte_Bytes/32-10              6.598Gi ±  1%
IndexByte_Bytes/4K-10              46.85Gi ±  2%
IndexByte_Bytes/4M-10              52.23Gi ±  1%
IndexByte_Bytes/64M-10             50.52Gi ±  5%
IndexRune_Bytes/10-10              759.2Mi ±  1%      773.8Mi ±  0%       +1.93% (p=0.000 n=10)
IndexRune_Bytes/32-10              2.367Gi ±  1%      2.418Gi ±  0%       +2.17% (p=0.000 n=10)
IndexRune_Bytes/4K-10              44.85Gi ±  1%      45.65Gi ±  0%       +1.79% (p=0.000 n=10)
IndexRune_Bytes/4M-10              60.05Gi ±  1%      60.72Gi ±  0%       +1.12% (p=0.035 n=10)
IndexRune_Bytes/64M-10             54.38Gi ±  4%      53.46Gi ±  5%            ~ (p=0.089 n=10)
IndexRuneASCII_Bytes/10-10         1.446Gi ±  1%      1.458Gi ±  0%       +0.80% (p=0.023 n=10)
IndexRuneASCII_Bytes/32-10         4.612Gi ±  1%      4.679Gi ±  1%       +1.46% (p=0.000 n=10)
IndexRuneASCII_Bytes/4K-10         45.48Gi ±  0%      45.69Gi ± 10%            ~ (p=0.218 n=10)
IndexRuneASCII_Bytes/4M-10         51.95Gi ±  2%      52.78Gi ±  1%       +1.60% (p=0.023 n=10)
IndexRuneASCII_Bytes/64M-10        49.75Gi ±  5%      48.09Gi ±  3%            ~ (p=0.218 n=10)
IndexNonASCII_Bytes/10-10          3.040Gi ±  0%      3.138Gi ±  0%       +3.23% (p=0.000 n=10)
IndexNonASCII_Bytes/32-10          7.772Gi ±  2%      7.928Gi ±  0%       +2.01% (p=0.023 n=10)
IndexNonASCII_Bytes/4K-10          47.24Gi ±  1%      47.93Gi ±  0%       +1.46% (p=0.019 n=10)
IndexNonASCII_Bytes/4M-10          52.46Gi ±  1%      52.43Gi ±  1%            ~ (p=0.529 n=10)
IndexNonASCII_Bytes/64M-10         49.19Gi ±  4%      47.81Gi ±  2%            ~ (p=0.089 n=10)
geomean                            832.0Mi            3.764Gi          +1121.52%                ¹
¹ benchmark set differs from baseline; geomeans may not be comparable

                                 │  regexp.10.txt   │               strcase.10.txt                │
                                 │       B/op       │     B/op      vs base                       │
IndexRune-10                           811.0 ± 0%         0.0 ± 0%  -100.00% (p=0.000 n=10)
IndexRuneLongString-10                 811.0 ± 0%         0.0 ± 0%  -100.00% (p=0.000 n=10)
IndexRuneFastPath-10                   791.0 ± 0%         0.0 ± 0%  -100.00% (p=0.000 n=10)
Index-10                               791.0 ± 0%         0.0 ± 0%  -100.00% (p=0.000 n=10)
EqualFold/ASCII-10                   4.658Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.000 n=10)
EqualFold/UnicodePrefix-10           6.480Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.000 n=10)
EqualFold/UnicodeSuffix-10           6.480Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.000 n=10)
IndexHard1-10                          914.0 ± 0%         0.0 ± 0%  -100.00% (p=0.000 n=10)

Frequently Asked Questions

What is strcase?

strcase is a Miscellaneous library for the Go programming language. Case-insensitive implementation of the standard library's strings/bytes packages

How do I install strcase?

Install strcase with the Go module system using `go get charlievieth/strcase`. Check the repository for the current installation instructions.

What category does strcase belong to?

strcase is listed under Miscellaneous, specifically Strings.

← Back to Strings