v1.16.11
ADDED: * stringsx: ** RunesInString ** RuneMapFromString ** SquashConsec ** SquashConsecRunes ** SquashConsecRunesAll ** SquashMap ** SquashWhitespace
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
`unicode/utf8`
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -498,6 +499,462 @@ func Reverse(s string) (revS string) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
RunesInString returns an ordered slice of all unique runes in
|
||||
UTF-8 string `s`.
|
||||
|
||||
If `s` is an empty string, runes will be a non-nil empty slice.
|
||||
*/
|
||||
func RunesInString(s string) (runes []rune) {
|
||||
|
||||
var r rune
|
||||
var idx int
|
||||
var runeMap map[rune]uint64 = RuneMapFromString(s)
|
||||
|
||||
runes = make([]rune, len(runeMap))
|
||||
|
||||
idx = 0
|
||||
for r, _ = range runeMap {
|
||||
runes[idx] = r
|
||||
idx++
|
||||
}
|
||||
|
||||
slices.Sort(runes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
RuneMapFromString returns a map of unique runes found in
|
||||
UTF-8 string `s` along with a count of their occurrence.
|
||||
|
||||
If `s` is an empty string, runes will be a non-nil empty map.
|
||||
|
||||
Non-UTF-8 runes are skipped.
|
||||
*/
|
||||
func RuneMapFromString(s string) (runes map[rune]uint64) {
|
||||
|
||||
var r rune
|
||||
var runeSz int
|
||||
|
||||
runes = make(map[rune]uint64)
|
||||
|
||||
for idx := 0; idx < len(s); {
|
||||
r, runeSz = utf8.DecodeRuneInString(s[idx:])
|
||||
if r == utf8.RuneError && runeSz == 1 {
|
||||
idx += runeSz
|
||||
continue
|
||||
}
|
||||
runes[r]++
|
||||
idx += runeSz
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SquashConsec squashes/collapses consecutive instances of sequences
|
||||
seq to a single instance.
|
||||
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII).
|
||||
|
||||
If seq is nil/empty, any n+1 consecutive instance of *any rune*
|
||||
will be squashed to a single instance. This is *much* more performant
|
||||
(as it simply wraps [SquashConsecRunesAll]) at the cost of lack of scoping.
|
||||
|
||||
If you are trying to squash/collapse whitespace instead, [SquashWhitespace]
|
||||
may be more apropos.
|
||||
|
||||
If you want more fine-grained control over sequence replacement, see [SquashMap].
|
||||
|
||||
If s is an empty string, SquashConsec will return `s` as-is.
|
||||
*/
|
||||
func SquashConsec(s string, seq ...string) (squashed string) {
|
||||
|
||||
var idx int
|
||||
var sIdx int
|
||||
var kLen int
|
||||
var start int
|
||||
var seqIdx int
|
||||
var nextNum int
|
||||
var nextPos int
|
||||
var sb *strings.Builder
|
||||
|
||||
squashed = s
|
||||
|
||||
if len(s) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(seq) == 0 {
|
||||
squashed = SquashConsecRunesAll(s)
|
||||
return
|
||||
}
|
||||
|
||||
for seqIdx = range seq {
|
||||
if seq[seqIdx] == "" {
|
||||
continue
|
||||
}
|
||||
sb = new(strings.Builder)
|
||||
sb.Grow(len(squashed))
|
||||
kLen = len(seq[seqIdx])
|
||||
for idx = 0; idx < len(squashed); {
|
||||
sIdx = strings.Index(squashed[idx:], seq[seqIdx])
|
||||
if sIdx < 0 {
|
||||
sb.WriteString(squashed[idx:])
|
||||
break
|
||||
}
|
||||
start = idx + sIdx
|
||||
sb.WriteString(squashed[idx:start])
|
||||
nextNum = 0
|
||||
nextPos = start
|
||||
for strings.HasPrefix(squashed[nextPos:], seq[seqIdx]) {
|
||||
nextNum++
|
||||
nextPos += kLen
|
||||
}
|
||||
if nextNum >= 1 {
|
||||
sb.WriteString(seq[seqIdx])
|
||||
}
|
||||
idx = nextPos
|
||||
}
|
||||
|
||||
squashed = sb.String()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SquashConsecRunes squashes/collapses consecutive instances of sequences
|
||||
seq with a single instance.
|
||||
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII).
|
||||
|
||||
If seq is nil/empty, any n+1 consecutive instance of *any rune*
|
||||
will be squashed to a single instance. This is *much* more performant
|
||||
(as it simply wraps [SquashConsecRunesAll]) at the cost of lack of scoping.
|
||||
|
||||
If s is an empty string, SquashConsecRunes will return `s` as-is.
|
||||
*/
|
||||
func SquashConsecRunes(s string, seq ...rune) (squashed string) {
|
||||
|
||||
var r rune
|
||||
var idx int
|
||||
var prev rune
|
||||
var seqIdx int
|
||||
var runeSz int
|
||||
var sb *strings.Builder
|
||||
|
||||
squashed = s
|
||||
|
||||
if len(s) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(seq) == 0 {
|
||||
squashed = SquashConsecRunesAll(s)
|
||||
return
|
||||
}
|
||||
|
||||
for seqIdx = range seq {
|
||||
if seq[seqIdx] < 0 {
|
||||
continue
|
||||
}
|
||||
sb = new(strings.Builder)
|
||||
sb.Grow(len(squashed))
|
||||
prev = -1
|
||||
for idx = 0; idx < len(squashed); {
|
||||
r, runeSz = utf8.DecodeRuneInString(squashed[idx:])
|
||||
if r == utf8.RuneError && runeSz == 1 {
|
||||
prev = -1
|
||||
sb.WriteString(squashed[idx : idx+runeSz])
|
||||
} else {
|
||||
if r != seq[seqIdx] || prev != r {
|
||||
prev = r
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
idx += runeSz
|
||||
}
|
||||
|
||||
squashed = sb.String()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SquashConsecRunesAll squashes/condenses/collapses all consecutive occurrences
|
||||
of every rune in `s` to a single instance.
|
||||
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII);
|
||||
non-UTF-8 runes will be written as-is to squashed (even if duplicate).
|
||||
|
||||
That is to say, for an `s` of:
|
||||
|
||||
* `fo`
|
||||
* `foo`
|
||||
* `fooo`
|
||||
* `foooo`
|
||||
|
||||
SquashConsecRunesAll would return `fo` for all of them.
|
||||
|
||||
SquashConsecRunesAll will be a no-op and return an empty string
|
||||
if `s` is an empty string.
|
||||
*/
|
||||
func SquashConsecRunesAll(s string) (squashed string) {
|
||||
|
||||
var r rune
|
||||
var runeSz int
|
||||
var prev rune = -1
|
||||
var sb *strings.Builder
|
||||
|
||||
squashed = s
|
||||
|
||||
if len(s) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sb = new(strings.Builder)
|
||||
sb.Grow(len(s))
|
||||
for idx := 0; idx < len(squashed); {
|
||||
r, runeSz = utf8.DecodeRuneInString(squashed[idx:])
|
||||
if r == utf8.RuneError && runeSz == 1 {
|
||||
prev = -1
|
||||
sb.WriteString(squashed[idx : idx+runeSz])
|
||||
} else {
|
||||
if prev != r {
|
||||
prev = r
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
idx += runeSz
|
||||
}
|
||||
|
||||
squashed = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SquashMap offers replacement of consecutive string series.
|
||||
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII).
|
||||
|
||||
If single is set to true, *single/standalone* instances of a sequence
|
||||
in replaceMap keys will also be replaced with its corresponding value.
|
||||
|
||||
Be aware that this function is not ideal for very large replaceMap maps
|
||||
or for very large strings, as s will be passed over for each key in replaceMap.
|
||||
If dealing with very large strings or a very large explicit set of replacements,
|
||||
you may want to directly use [strings.Map] or [strings.NewReplacer] (if multi-rune
|
||||
replacement is needed) with your own logic.
|
||||
|
||||
Matching string replacements are done longest-to-shortest sequentially.
|
||||
This means that given the string `fooo` (and assuming single==true),
|
||||
and a replaceMap of:
|
||||
|
||||
map[string]string{
|
||||
"o": "a",
|
||||
"oo": "b",
|
||||
}
|
||||
|
||||
the result will be "fba", not:
|
||||
|
||||
* "faaa"
|
||||
* "fab"
|
||||
|
||||
Thus you may want to execute several calls if this is undesired.
|
||||
Keys that are the same length are ordered alphabetically/alphanumerically
|
||||
so this function is deterministic.
|
||||
|
||||
Empty keys in replaceMap (but NOT empty values) are skipped.
|
||||
Keys in replaceMap that are not UTF-8 are skipped.
|
||||
|
||||
Note that this function offers high-granularity control over replacements
|
||||
at the cost of complex setup.
|
||||
|
||||
If you want more simple squashing/collapsing of whitespace, [SquashWhitespace]
|
||||
may be more apropos.
|
||||
|
||||
If you want more simple squashing/collapsing of non-whitespace, [SquashConsec]
|
||||
may be more apropos.
|
||||
|
||||
If replaceMap is nil or empty, s will be returned as-is.
|
||||
If s is an empty string, SquashMap will no-op.
|
||||
*/
|
||||
func SquashMap(s string, replaceMap map[string]string, single bool) (squashed string) {
|
||||
|
||||
var idx int
|
||||
var sIdx int
|
||||
var k string
|
||||
var v string
|
||||
var start int
|
||||
var nextNum int
|
||||
var nextPos int
|
||||
var sb *strings.Builder
|
||||
type kLen struct {
|
||||
// k is the Key name.
|
||||
k string
|
||||
// r is the replacement string.
|
||||
r string
|
||||
// l is the length of k (in UTF-8 runes).
|
||||
l int
|
||||
// bl is the length of k (in bytes).
|
||||
bl int
|
||||
}
|
||||
var kl kLen
|
||||
var keysByLen []kLen
|
||||
|
||||
squashed = s
|
||||
|
||||
if len(replaceMap) == 0 || len(s) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
keysByLen = make([]kLen, 0, len(replaceMap))
|
||||
for k, v = range replaceMap {
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if !utf8.ValidString(k) {
|
||||
continue
|
||||
}
|
||||
keysByLen = append(
|
||||
keysByLen,
|
||||
kLen{
|
||||
k: k,
|
||||
r: v,
|
||||
l: utf8.RuneCountInString(k),
|
||||
bl: len(k),
|
||||
},
|
||||
)
|
||||
}
|
||||
slices.SortFunc(
|
||||
keysByLen,
|
||||
func(a, b kLen) (cmp int) {
|
||||
cmp = 0
|
||||
// Sort so that the longer comes first.
|
||||
if a.l > b.l {
|
||||
cmp = -1
|
||||
} else if a.l < b.l {
|
||||
cmp = 1
|
||||
} else if a.l == b.l {
|
||||
if a.k < b.k {
|
||||
cmp = -1
|
||||
} else if a.k > b.k {
|
||||
cmp = 1
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
|
||||
for _, kl = range keysByLen {
|
||||
switch strings.Count(squashed, kl.k) {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
if !single {
|
||||
continue
|
||||
}
|
||||
squashed = strings.ReplaceAll(squashed, kl.k, kl.r)
|
||||
default:
|
||||
sb = new(strings.Builder)
|
||||
sb.Grow(len(squashed))
|
||||
for idx = 0; idx < len(squashed); {
|
||||
if sIdx = strings.Index(squashed[idx:], kl.k); sIdx < 0 {
|
||||
sb.WriteString(squashed[idx:])
|
||||
break
|
||||
}
|
||||
start = idx + sIdx
|
||||
sb.WriteString(squashed[idx:start])
|
||||
nextNum = 0
|
||||
nextPos = start
|
||||
for strings.HasPrefix(squashed[nextPos:], kl.k) {
|
||||
nextNum++
|
||||
nextPos += kl.bl
|
||||
}
|
||||
if nextNum > 1 || (nextNum == 1 && single) {
|
||||
sb.WriteString(kl.r)
|
||||
} else {
|
||||
sb.WriteString(kl.k)
|
||||
}
|
||||
idx = nextPos
|
||||
}
|
||||
|
||||
squashed = sb.String()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SquashWhitespace is used to collapse/squash consecutive whitespace.
|
||||
It is expected that s is unicode or a compatible subset (e.g. ASCII).
|
||||
|
||||
If single is set to true, *single/standalone* instances of whitespace
|
||||
will also be replaced with ws.
|
||||
|
||||
ws is a string that should replace *consecutive* whitespace.
|
||||
ws does not necessarily have to be a whitespace string, and may be empty.
|
||||
|
||||
If you are trying to squash/collapse non-whitespace, [SquashConsec]
|
||||
is more apropos.
|
||||
|
||||
If you want more fine-grained control over sequence replacement, see [SquashMap].
|
||||
*/
|
||||
func SquashWhitespace(s, ws string, single bool) (squashed string) {
|
||||
|
||||
var r rune
|
||||
var runeSz int
|
||||
var wsStart int
|
||||
var invalid bool
|
||||
var prevWs uint64
|
||||
var sb *strings.Builder
|
||||
|
||||
squashed = s
|
||||
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
sb = new(strings.Builder)
|
||||
sb.Grow(len(s))
|
||||
|
||||
for rIdx := 0; rIdx < len(s); {
|
||||
r, runeSz = utf8.DecodeRuneInString(s[rIdx:])
|
||||
invalid = r == utf8.RuneError && runeSz == 1
|
||||
if invalid || !unicode.IsSpace(r) {
|
||||
if prevWs > 1 || (prevWs == 1 && single) {
|
||||
sb.WriteString(ws)
|
||||
} else if prevWs == 1 {
|
||||
sb.WriteString(s[wsStart:rIdx])
|
||||
}
|
||||
prevWs = 0
|
||||
if invalid {
|
||||
// Invalid UTF-8 rune; write byte as-is and advance index.
|
||||
sb.WriteString(s[rIdx : rIdx+runeSz])
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
rIdx += runeSz
|
||||
continue
|
||||
}
|
||||
if prevWs == 0 {
|
||||
wsStart = rIdx
|
||||
}
|
||||
prevWs++
|
||||
rIdx += runeSz
|
||||
}
|
||||
if prevWs > 1 || (prevWs == 1 && single) {
|
||||
sb.WriteString(ws)
|
||||
} else if prevWs == 1 {
|
||||
sb.WriteString(s[wsStart:])
|
||||
}
|
||||
|
||||
squashed = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
StripWhitespace removes all leading, trailing, and INNER whitespace
|
||||
from unicode string `s`.
|
||||
|
||||
Reference in New Issue
Block a user