mirror of
https://github.com/odin-lang/Odin.git
synced 2026-05-03 03:24:41 +00:00
create lua strlib text package and tests
This commit is contained in:
725
core/text/lua/strlib.odin
Normal file
725
core/text/lua/strlib.odin
Normal file
@@ -0,0 +1,725 @@
|
||||
package strlib
|
||||
|
||||
import "core:strings"
|
||||
|
||||
MAXCAPTURES :: 32
|
||||
|
||||
Capture :: struct {
|
||||
init: int,
|
||||
len: int,
|
||||
}
|
||||
|
||||
Match :: struct {
|
||||
start, end: int,
|
||||
}
|
||||
|
||||
Error :: enum {
|
||||
OK,
|
||||
OOB,
|
||||
Invalid_Capture_Index,
|
||||
Invalid_Pattern_Capture,
|
||||
Unfinished_Capture,
|
||||
}
|
||||
|
||||
L_ESC :: '%'
|
||||
CAP_POSITION :: -2
|
||||
CAP_UNFINISHED :: -1
|
||||
INVALID :: -1
|
||||
|
||||
MatchState :: struct {
|
||||
src: string,
|
||||
pattern: string,
|
||||
level: int,
|
||||
capture: [MAXCAPTURES]Capture,
|
||||
}
|
||||
|
||||
match_class :: proc(c: u8, cl: u8) -> (res: bool) {
|
||||
switch tolower(cl) {
|
||||
case 'a': res = isalpha(c)
|
||||
case 'c': res = iscntrl(c)
|
||||
case 'd': res = isdigit(c)
|
||||
case 'g': res = isgraph(c)
|
||||
case 'l': res = islower(c)
|
||||
case 'p': res = ispunct(c)
|
||||
case 's': res = isspace(c)
|
||||
case 'u': res = isupper(c)
|
||||
case 'w': res = isalnum(c)
|
||||
case 'x': res = isxdigit(c)
|
||||
case: return cl == c
|
||||
}
|
||||
|
||||
return islower(cl) ? res : !res
|
||||
}
|
||||
|
||||
isalpha :: proc(c: u8) -> bool {
|
||||
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
|
||||
}
|
||||
|
||||
isdigit :: proc(c: u8) -> bool {
|
||||
return '0' <= c && c <= '9'
|
||||
}
|
||||
|
||||
isalnum :: proc(c: u8) -> bool {
|
||||
return isalpha(c) || isdigit(c)
|
||||
}
|
||||
|
||||
iscntrl :: proc(c: u8) -> bool {
|
||||
return c <= '\007' || (c >= '\010' && c <= '\017') || (c >= '\020' && c <= '\027') || (c >= '\030' && c <= '\037') || c == '\177'
|
||||
}
|
||||
|
||||
islower :: proc(c: u8) -> bool {
|
||||
return c >= 'a' && c <= 'z'
|
||||
}
|
||||
|
||||
isupper :: proc(c: u8) -> bool {
|
||||
return c >= 'A' && c <= 'Z'
|
||||
}
|
||||
|
||||
isgraph :: proc(c: u8) -> bool {
|
||||
return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
|
||||
}
|
||||
|
||||
ispunct :: proc(c: u8) -> bool {
|
||||
return (c >= '{' && c <= '~') || (c == '`') || (c >= '[' && c <= '_') || (c == '@') || (c >= ':' && c <= '?') || (c >= '(' && c <= '/') || (c >= '!' && c <= '\'')
|
||||
}
|
||||
|
||||
isxdigit :: proc(c: u8) -> bool {
|
||||
return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
|
||||
}
|
||||
|
||||
isspace :: proc(c: u8) -> bool {
|
||||
return c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r' || c == ' '
|
||||
}
|
||||
|
||||
// ascii safe
|
||||
tolower :: proc(c: u8) -> u8 {
|
||||
if c >= 65 && c <= 90 { // upper case
|
||||
return c + 32
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
check_capture :: proc(ms: ^MatchState, l: rune) -> (int, Error) {
|
||||
l := int(l - '1')
|
||||
|
||||
if l < 0 || l >= ms.level || ms.capture[l].len == CAP_UNFINISHED {
|
||||
return 0, .Invalid_Capture_Index
|
||||
}
|
||||
|
||||
return l, .OK
|
||||
}
|
||||
|
||||
capture_to_close :: proc(ms: ^MatchState) -> (int, Error) {
|
||||
level := ms.level - 1
|
||||
|
||||
for level >= 0 {
|
||||
if ms.capture[level].len == CAP_UNFINISHED {
|
||||
return level, .OK
|
||||
}
|
||||
|
||||
level -= 1
|
||||
}
|
||||
|
||||
return 0, .Invalid_Pattern_Capture
|
||||
}
|
||||
|
||||
classend :: proc(ms: ^MatchState, p: int) -> (int, Error) {
|
||||
ch := ms.pattern[p]
|
||||
p := p + 1
|
||||
|
||||
switch ch {
|
||||
case L_ESC: {
|
||||
// if > 0 {
|
||||
// fmt.eprintln("ERR classend: not enough pattern length")
|
||||
// return nil
|
||||
// }
|
||||
|
||||
return p + 1, .OK
|
||||
}
|
||||
|
||||
case '[': {
|
||||
if ms.pattern[p] == '^' {
|
||||
p += 1
|
||||
}
|
||||
|
||||
// TODO double check
|
||||
for {
|
||||
ch := ms.pattern[p]
|
||||
|
||||
if ch == L_ESC && p <= len(ms.pattern) {
|
||||
// skip escapes like '%'
|
||||
p += 1
|
||||
}
|
||||
|
||||
if ms.pattern[p] == ']' {
|
||||
break
|
||||
}
|
||||
|
||||
p += 1
|
||||
}
|
||||
|
||||
return p + 1, .OK
|
||||
}
|
||||
|
||||
case: {
|
||||
return p, .OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchbracketclass :: proc(ms: ^MatchState, c: u8, p, ec: int) -> bool {
|
||||
sig := true
|
||||
p := p
|
||||
|
||||
if ms.pattern[p + 1] == '^' {
|
||||
p += 1
|
||||
sig = false
|
||||
}
|
||||
|
||||
p += 1
|
||||
|
||||
// while inside of class range
|
||||
for p < ec {
|
||||
ch := ms.pattern[p]
|
||||
|
||||
if ms.pattern[p] == L_ESC {
|
||||
p += 1
|
||||
|
||||
if match_class(c, ms.pattern[p]) {
|
||||
return sig
|
||||
}
|
||||
} else if ms.pattern[p + 1] == '-' && p + 2 < len(ms.pattern) {
|
||||
// e.g. [a-z] check
|
||||
if ms.pattern[p] <= c && c <= ms.pattern[p + 2] {
|
||||
return sig
|
||||
}
|
||||
|
||||
p += 2
|
||||
} else if ms.pattern[p] == c {
|
||||
return sig
|
||||
}
|
||||
|
||||
p += 1
|
||||
}
|
||||
|
||||
return !sig
|
||||
}
|
||||
|
||||
singlematch :: proc(ms: ^MatchState, s, p, ep: int) -> bool {
|
||||
if s >= len(ms.src) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch ms.pattern[p] {
|
||||
case '.': return true
|
||||
case L_ESC: return match_class(ms.src[s], ms.pattern[p + 1])
|
||||
case '[': return matchbracketclass(ms, ms.src[s], p, ep - 1)
|
||||
case: return ms.src[s] == ms.pattern[p]
|
||||
}
|
||||
}
|
||||
|
||||
// matchbalance :: proc(ms: ^MatchState, s, p: int) -> (int, Error) {
|
||||
// s_begin := s
|
||||
// s := s + 1
|
||||
// cont := 0
|
||||
|
||||
// begin := ms.pattern[p]
|
||||
// end := ms.pattern[p + 1]
|
||||
// print("BALANCED between", rune(begin), "AND", rune(end))
|
||||
|
||||
// for s < len(ms.src) {
|
||||
// ch := ms.src[s]
|
||||
// print("\t", rune(ch))
|
||||
|
||||
// if ch == end {
|
||||
// cont -= 1
|
||||
// print("END", cont)
|
||||
|
||||
// if cont == 0 {
|
||||
// print("BALANCED RET", s + 1, len(ms.src), ms.src[s_begin:s + 1])
|
||||
// return s + 1
|
||||
// }
|
||||
// } else if ch == begin {
|
||||
// cont += 1
|
||||
// print("BEGIN", cont)
|
||||
// }
|
||||
|
||||
// s += 1
|
||||
// }
|
||||
|
||||
// print("OUT OF BALANCE", cont)
|
||||
// // out of balance
|
||||
// return 0, .
|
||||
// }
|
||||
|
||||
max_expand :: proc(ms: ^MatchState, s, p, ep: int) -> (res: int, err: Error) {
|
||||
i := 0
|
||||
for singlematch(ms, s + i, p, ep) {
|
||||
i += 1
|
||||
}
|
||||
|
||||
for i >= 0 {
|
||||
result := match(ms, s + i, ep + 1) or_return
|
||||
|
||||
if result != INVALID {
|
||||
// print("SET", result)
|
||||
return result, .OK
|
||||
}
|
||||
|
||||
i -= 1
|
||||
}
|
||||
|
||||
return INVALID, .OK
|
||||
}
|
||||
|
||||
min_expand :: proc(ms: ^MatchState, s, p, ep: int) -> (res: int, err: Error) {
|
||||
s := s
|
||||
|
||||
for {
|
||||
result := match(ms, s, ep + 1) or_return
|
||||
|
||||
if result != INVALID {
|
||||
return result, .OK
|
||||
} else if singlematch(ms, s, p, ep) {
|
||||
s += 1
|
||||
} else {
|
||||
return INVALID, .OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_capture :: proc(ms: ^MatchState, s, p, what: int) -> (res: int, err: Error) {
|
||||
level := ms.level
|
||||
|
||||
ms.capture[level].init = s
|
||||
ms.capture[level].len = what
|
||||
ms.level += 1
|
||||
|
||||
res = match(ms, s, p) or_return
|
||||
if res == INVALID {
|
||||
ms.level -= 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
end_capture :: proc(ms: ^MatchState, s, p: int) -> (res: int, err: Error) {
|
||||
l := capture_to_close(ms) or_return
|
||||
|
||||
// TODO double check, could do string as int index
|
||||
ms.capture[l].len = s - ms.capture[l].init
|
||||
|
||||
res = match(ms, s, p) or_return
|
||||
if res == INVALID {
|
||||
ms.capture[l].len = CAP_UNFINISHED
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
match_capture :: proc(ms: ^MatchState, s: int, char: rune) -> (res: int, err: Error) {
|
||||
index := check_capture(ms, char) or_return
|
||||
length := ms.capture[index].len
|
||||
|
||||
if len(ms.src) - s >= length {
|
||||
return s + length, .OK
|
||||
}
|
||||
|
||||
return INVALID, .OK
|
||||
}
|
||||
|
||||
match :: proc(ms: ^MatchState, s, p: int) -> (unused: int, err: Error) {
|
||||
s := s
|
||||
p := p
|
||||
|
||||
if p == len(ms.pattern) {
|
||||
return s, .OK
|
||||
}
|
||||
|
||||
switch ms.pattern[p] {
|
||||
case '(': {
|
||||
if ms.pattern[p + 1] == ')' {
|
||||
s = start_capture(ms, s, p + 2, CAP_POSITION) or_return
|
||||
} else {
|
||||
s = start_capture(ms, s, p + 1, CAP_UNFINISHED) or_return
|
||||
}
|
||||
}
|
||||
|
||||
case ')': {
|
||||
s = end_capture(ms, s, p + 1) or_return
|
||||
}
|
||||
|
||||
case '$': {
|
||||
if p + 1 != len(ms.pattern) {
|
||||
return match_default(ms, s, p)
|
||||
}
|
||||
|
||||
if len(ms.src) != s {
|
||||
s = INVALID
|
||||
}
|
||||
}
|
||||
|
||||
case L_ESC: {
|
||||
// stop short patterns like "%" only
|
||||
if p + 1 >= len(ms.pattern) {
|
||||
err = .OOB
|
||||
return
|
||||
}
|
||||
|
||||
switch ms.pattern[p + 1] {
|
||||
// balanced string
|
||||
case 'b': {
|
||||
// res := matchbalance(ms, s, p + 2)
|
||||
|
||||
// if data, ok := res.?; ok {
|
||||
// // s = data
|
||||
// // eg after %b()
|
||||
// // print("SUCCESS")
|
||||
// return patt_match(ms, s, p + 4)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// frontier
|
||||
case 'f': {
|
||||
// p += 2
|
||||
|
||||
// if ms.pattern[p] != '[' {
|
||||
// print("missing '[' after %f in pattern")
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// ep := classend(ms, p).?
|
||||
// previous := 0 if s == 0 else s - 1
|
||||
|
||||
// if !matchbracketclass(ms, ms.src[previous], p, ep - 1) &&
|
||||
// matchbracketclass(ms, ms.src[s], p, ep) {
|
||||
// return patt_match(ms, s, ep)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
}
|
||||
|
||||
// capture group
|
||||
case '0'..<'9': {
|
||||
s = match_capture(ms, s, rune(ms.pattern[p + 1])) or_return
|
||||
|
||||
if s != INVALID {
|
||||
return match(ms, s, p + 2)
|
||||
}
|
||||
}
|
||||
|
||||
case: {
|
||||
return match_default(ms, s, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case: {
|
||||
return match_default(ms, s, p)
|
||||
// print("PATT DEF", rune(ms.src[s]), rune(ms.pattern[p]))
|
||||
}
|
||||
}
|
||||
|
||||
return s, .OK
|
||||
}
|
||||
|
||||
match_default :: proc(ms: ^MatchState, s, p: int) -> (unused: int, err: Error) {
|
||||
s := s
|
||||
ep := classend(ms, p) or_return
|
||||
// ch := s < len(ms.src) ? rune(ms.src[s]) : 0
|
||||
|
||||
if !singlematch(ms, s, p, ep) {
|
||||
epc := ep < len(ms.pattern) ? ms.pattern[ep] : 0
|
||||
// print("+++", rune(epc))
|
||||
|
||||
if epc == '*' || epc == '?' || epc == '-' {
|
||||
return match(ms, s, ep + 1)
|
||||
} else {
|
||||
s = INVALID
|
||||
}
|
||||
} else {
|
||||
epc := ep < len(ms.pattern) ? ms.pattern[ep] : 0
|
||||
// print("~~~", ch, rune(epc))
|
||||
|
||||
switch epc {
|
||||
case '?': {
|
||||
result := match(ms, s + 1, ep + 1) or_return
|
||||
|
||||
if result != INVALID {
|
||||
s = result
|
||||
} else {
|
||||
return match(ms, s, ep + 1)
|
||||
}
|
||||
}
|
||||
|
||||
case '+': {
|
||||
s = max_expand(ms, s + 1, p, ep) or_return
|
||||
}
|
||||
|
||||
case '*': {
|
||||
s = max_expand(ms, s, p, ep) or_return
|
||||
}
|
||||
|
||||
case '-': {
|
||||
s = min_expand(ms, s, p, ep) or_return
|
||||
}
|
||||
|
||||
case: {
|
||||
return match(ms, s + 1, ep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, .OK
|
||||
}
|
||||
|
||||
push_onecapture :: proc(
|
||||
ms: ^MatchState,
|
||||
i: int,
|
||||
s: int,
|
||||
e: int,
|
||||
matches: []Match,
|
||||
) -> (err: Error) {
|
||||
if i >= ms.level {
|
||||
if i == 0 {
|
||||
matches[0] = { 0, e - s }
|
||||
} else {
|
||||
err = .Invalid_Capture_Index
|
||||
}
|
||||
} else {
|
||||
init := ms.capture[i].init
|
||||
length := ms.capture[i].len
|
||||
|
||||
switch length {
|
||||
case CAP_UNFINISHED: {
|
||||
err = .Unfinished_Capture
|
||||
}
|
||||
|
||||
case CAP_POSITION: {
|
||||
matches[i] = { init - 1, init - 1 }
|
||||
}
|
||||
|
||||
case: {
|
||||
matches[i] = { init, init + length }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
push_captures :: proc(
|
||||
ms: ^MatchState,
|
||||
s: int,
|
||||
e: int,
|
||||
matches: []Match,
|
||||
) -> (nlevels: int, err: Error) {
|
||||
nlevels = 1 if ms.level == 0 && s != -1 else ms.level
|
||||
|
||||
for i in 0..<nlevels {
|
||||
push_onecapture(ms, i, s, e, matches) or_return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SPECIALS := "^$*+?.([%-"
|
||||
// all special characters inside a small ascii array
|
||||
SPECIALS_TABLE := [256]u8 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
|
||||
|
||||
// helper call to quick search for special characters
|
||||
index_special :: proc(text: string) -> int {
|
||||
for i in 0..<len(text) {
|
||||
// TODO utf8
|
||||
if SPECIALS_TABLE[text[i]] == 1 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
lmemfind :: proc(s1, s2: string) -> int {
|
||||
l1 := len(s1)
|
||||
l2 := len(s2)
|
||||
|
||||
if l2 == 0 {
|
||||
return 0
|
||||
} else if l2 > l1 {
|
||||
return -1
|
||||
} else {
|
||||
init := strings.index_byte(s1, s2[0])
|
||||
end := init + l2
|
||||
|
||||
for end <= l1 && init != -1 {
|
||||
init += 1
|
||||
|
||||
if s1[init - 1:end] == s2 {
|
||||
return init - 1
|
||||
} else {
|
||||
next := strings.index_byte(s1[init:], s2[0])
|
||||
|
||||
if next == -1 {
|
||||
return -1
|
||||
} else {
|
||||
init = init + next
|
||||
end = init + l2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// find a pattern with in a haystack with an offset
|
||||
// allow_memfind will speed up simple searches
|
||||
find_aux :: proc(
|
||||
haystack: string,
|
||||
pattern: string,
|
||||
offset: int,
|
||||
allow_memfind: bool,
|
||||
matches: ^[MAXCAPTURES]Match,
|
||||
) -> (captures: int, err: Error) {
|
||||
s := offset
|
||||
p := 0
|
||||
|
||||
specials_idx := index_special(pattern)
|
||||
if allow_memfind && specials_idx == -1 {
|
||||
if index := lmemfind(haystack[s:], pattern); index != -1 {
|
||||
matches[0] = { index + s, index + s + len(pattern) }
|
||||
captures = 1
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pattern := pattern
|
||||
anchor: bool
|
||||
if len(pattern) > 0 && pattern[0] == '^' {
|
||||
anchor = true
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
|
||||
ms := MatchState {
|
||||
src = haystack,
|
||||
pattern = pattern,
|
||||
}
|
||||
|
||||
for {
|
||||
res := match(&ms, s, p) or_return
|
||||
|
||||
if res != INVALID {
|
||||
// NOTE(Skytrias): first result is reserved for a full match
|
||||
matches[0] = { s, res }
|
||||
// rest are the actual captures
|
||||
captures = push_captures(&ms, -1, -1, matches[1:]) or_return
|
||||
captures += 1
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s += 1
|
||||
|
||||
if !(s < len(ms.src) && !anchor) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// iterative matching which returns the 0th/1st match
|
||||
// rest has to be used from captures
|
||||
gmatch :: proc(
|
||||
haystack: ^string,
|
||||
pattern: string,
|
||||
captures: ^[MAXCAPTURES]Match,
|
||||
) -> (res: string, ok: bool) {
|
||||
if len(haystack) > 0 {
|
||||
length, err := find_aux(haystack^, pattern, 0, false, captures)
|
||||
|
||||
if length != 0 && err == .OK {
|
||||
ok = true
|
||||
first := length > 1 ? 1 : 0
|
||||
cap := captures[first]
|
||||
res = haystack[cap.start:cap.end]
|
||||
haystack^ = haystack[cap.end:]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// gsub with builder
|
||||
gsub_builder :: proc(
|
||||
builder: ^strings.Builder,
|
||||
haystack: string,
|
||||
pattern: string,
|
||||
replace: string,
|
||||
) -> string {
|
||||
// find matches
|
||||
captures: [MAXCAPTURES]Match
|
||||
haystack := haystack
|
||||
|
||||
for {
|
||||
length, err := find_aux(haystack, pattern, 0, false, &captures)
|
||||
|
||||
// done
|
||||
if length == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if err != .OK {
|
||||
return {}
|
||||
}
|
||||
|
||||
cap := captures[0]
|
||||
|
||||
// write front till capture
|
||||
strings.write_string(builder, haystack[:cap.start])
|
||||
|
||||
// write replacements
|
||||
strings.write_string(builder, replace)
|
||||
|
||||
// advance string till end
|
||||
haystack = haystack[cap.end:]
|
||||
}
|
||||
|
||||
strings.write_string(builder, haystack[:])
|
||||
return strings.to_string(builder^)
|
||||
}
|
||||
|
||||
// uses temp builder to build initial string - then allocates the result
|
||||
gsub_allocator :: proc(
|
||||
haystack: string,
|
||||
pattern: string,
|
||||
replace: string,
|
||||
allocator := context.allocator,
|
||||
) -> string {
|
||||
builder := strings.builder_make(0, 256, context.temp_allocator)
|
||||
return gsub_builder(&builder, haystack, pattern, replace)
|
||||
}
|
||||
|
||||
gsub :: proc { gsub_builder, gsub_allocator }
|
||||
|
||||
// iterative find with first capture only
|
||||
gfind :: proc(
|
||||
haystack: ^string,
|
||||
pattern: string,
|
||||
captures: ^[MAXCAPTURES]Match,
|
||||
) -> (res: string, ok: bool) {
|
||||
if len(haystack) > 0 {
|
||||
length, err := find_aux(haystack^, pattern, 0, true, captures)
|
||||
|
||||
if length != 0 && err == .OK {
|
||||
ok = true
|
||||
cap := captures[0]
|
||||
res = haystack[cap.start:cap.end]
|
||||
haystack^ = haystack[cap.end:]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -2,7 +2,7 @@ ODIN=../../odin
|
||||
PYTHON=$(shell which python3)
|
||||
|
||||
all: download_test_assets image_test compress_test strings_test hash_test crypto_test noise_test encoding_test \
|
||||
math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test c_libc_test
|
||||
math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test lua_strlib_test c_libc_test
|
||||
|
||||
download_test_assets:
|
||||
$(PYTHON) download_assets.py
|
||||
@@ -49,5 +49,8 @@ os_exit_test:
|
||||
i18n_test:
|
||||
$(ODIN) run text/i18n -out:test_core_i18n
|
||||
|
||||
lua_strlib_test:
|
||||
$(ODIN) run text/lua -out:test_core_lua_strlib
|
||||
|
||||
c_libc_test:
|
||||
$(ODIN) run c/libc -out:test_core_libc
|
||||
@@ -71,6 +71,11 @@ echo Running core:text/i18n tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe
|
||||
|
||||
echo ---
|
||||
echo Running core:text/lua tests
|
||||
echo ---
|
||||
%PATH_TO_ODIN% run text\lua %COMMON% -out:test_core_lua_strlib.exe
|
||||
|
||||
echo ---
|
||||
echo Running core:slice tests
|
||||
echo ---
|
||||
|
||||
325
tests/core/text/lua/test_core_text_lua.odin
Normal file
325
tests/core/text/lua/test_core_text_lua.odin
Normal file
@@ -0,0 +1,325 @@
|
||||
package test_strlib
|
||||
|
||||
import lua "core:text/lua"
|
||||
import "core:testing"
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
|
||||
TEST_count: int
|
||||
TEST_fail: int
|
||||
|
||||
when ODIN_TEST {
|
||||
expect :: testing.expect
|
||||
} else {
|
||||
expect :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) {
|
||||
TEST_count += 1
|
||||
if !condition {
|
||||
TEST_fail += 1
|
||||
fmt.printf("[%v] %v\n", loc, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find correct byte offsets
|
||||
@test
|
||||
test_find :: proc(t: ^testing.T) {
|
||||
Entry :: struct {
|
||||
s, p: string,
|
||||
offset: int,
|
||||
|
||||
match: struct {
|
||||
start, end: int, // expected start/end
|
||||
ok: bool,
|
||||
},
|
||||
}
|
||||
|
||||
ENTRIES :: [?]Entry {
|
||||
{ "", "", 0, { 0, 0, true } },
|
||||
{ "alo", "", 0, { 0, 0, true } },
|
||||
{ "a o a o a o", "a", 0, { 0, 1, true } },
|
||||
{ "a o a o a o", "a o", 1, { 4, 7, true } },
|
||||
{ "alo123alo", "12", 0, { 3, 5, true } },
|
||||
{ "alo123alo", "^12", 0, {} },
|
||||
|
||||
// from https://riptutorial.com/lua/example/20535/string-find--introduction-
|
||||
{ "137'5 m47ch s0m3 d1g175", "m%d%d", 0, { 6, 9, true } },
|
||||
{ "stack overflow", "[abc]", 0, { 2, 3, true } },
|
||||
{ "stack overflow", "[^stack ]", 0, { 6, 7, true } },
|
||||
{ "hello", "o%d?", 0, { 4, 5, true } },
|
||||
{ "hello20", "o%d?", 0, { 4, 6, true } },
|
||||
{ "helllllo", "el+", 0, { 1, 7, true } },
|
||||
{ "heo", "el+", 0, {} },
|
||||
{ "helelo", "h.+l", 0, { 0, 5, true } },
|
||||
{ "helelo", "h.-l", 0, { 0, 3, true } },
|
||||
}
|
||||
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
for entry in ENTRIES {
|
||||
captures[0] = {}
|
||||
length, err := lua.find_aux(entry.s, entry.p, entry.offset, true, &captures)
|
||||
cap := captures[0]
|
||||
ok := length > 0 && err == .OK
|
||||
success := entry.match.ok == ok && entry.match.start == cap.start && entry.match.end == cap.end
|
||||
|
||||
if !success {
|
||||
fmt.eprintf("Find failed for: haystack = %s\tpattern = %s\n", entry.s, entry.p)
|
||||
}
|
||||
|
||||
expect(t, entry.match.start == cap.start, "match start didnt match")
|
||||
expect(t, entry.match.end == cap.end, "match end didnt match",)
|
||||
expect(t, entry.match.ok == ok, "find result didnt match")
|
||||
}
|
||||
}
|
||||
|
||||
@test
|
||||
test_match :: proc(t: ^testing.T) {
|
||||
Entry :: struct {
|
||||
s, p: string,
|
||||
result: string, // expected start/end
|
||||
ok: bool,
|
||||
}
|
||||
|
||||
ENTRIES :: [?]Entry {
|
||||
// star
|
||||
{ "aaab", ".*b", "aaab", true },
|
||||
{ "aaa", ".*a", "aaa", true },
|
||||
{ "b", ".*b", "b", true },
|
||||
|
||||
// plus
|
||||
{ "aaab", ".+b", "aaab", true },
|
||||
{ "aaa", ".+a", "aaa", true },
|
||||
{ "b", ".+b", "", false },
|
||||
|
||||
// question
|
||||
{ "aaab", ".?b", "ab", true },
|
||||
{ "aaa", ".?a", "aa", true },
|
||||
{ "b", ".?b", "b", true },
|
||||
|
||||
// CLASSES, checking shorted invalid patterns
|
||||
{ "a", "%", "", false },
|
||||
|
||||
// %a letter (A-Z, a-z)
|
||||
{ "letterS", "%a+", "letterS", true },
|
||||
{ "Let123", "%a+", "Let", true },
|
||||
{ "Let123", "%A+", "123", true },
|
||||
|
||||
// %c control characters (\n, \t, \r)
|
||||
{ "\n", "%c", "\n", true },
|
||||
{ "\t", "%c", "\t", true },
|
||||
{ "\t", "%C", "", false },
|
||||
{ "a", "%C", "a", true },
|
||||
|
||||
// %d digit characters (0-9)
|
||||
{ "0123", "%d+", "0123", true },
|
||||
{ "abcd", "%D+", "abcd", true },
|
||||
{ "ab23", "%d+", "23", true },
|
||||
|
||||
// %l lower characters (a-z)
|
||||
{ "lowerCASE", "%l+", "lower", true },
|
||||
{ "LOWERcase", "%l+", "case", true },
|
||||
{ "LOWERcase", "%L+", "LOWER", true },
|
||||
|
||||
// %p punctionation characters (!, ?, &, ...)
|
||||
{ "!?&", "%p+", "!?&", true },
|
||||
{ "abc!abc", "%p", "!", true },
|
||||
{ "!abc!", "%P+", "abc", true },
|
||||
|
||||
// %s space characters
|
||||
{ " ", "%s", " ", true },
|
||||
{ "a", "%S", "a", true },
|
||||
{ "abc ", "%s+", " ", true },
|
||||
|
||||
// %u upper characters (A-Z)
|
||||
{ "lowerCASE", "%u+", "CASE", true },
|
||||
{ "LOWERcase", "%u+", "LOWER", true },
|
||||
{ "LOWERcase", "%U+", "case", true },
|
||||
|
||||
// %w alpha numeric (A-Z, a-z, 0-9)
|
||||
{ "0123", "%w+", "0123", true },
|
||||
{ "abcd", "%W+", "", false },
|
||||
{ "ab23", "%w+", "ab23", true },
|
||||
|
||||
// %x hexadecimal digits (0x1A, ...)
|
||||
{ "3", "%x", "3", true },
|
||||
{ "9f", "%x+", "9f", true },
|
||||
{ "9g", "%x+", "9", true },
|
||||
{ "9g", "%X+", "g", true },
|
||||
|
||||
// random tests
|
||||
{ "f123", "%D", "f", true },
|
||||
{ "f123", "%d", "1", true },
|
||||
{ "f123", "%d+", "123", true },
|
||||
{ "foo 123 bar", "%d%d%d", "123", true },
|
||||
{ "Uppercase", "%u", "U", true },
|
||||
{ "abcd", "[bc][bc]", "bc", true },
|
||||
{ "abcd", "[^ad]", "b", true },
|
||||
{ "123", "[0-9]", "1", true },
|
||||
|
||||
// end of line
|
||||
{ "testing this", "this$", "this", true },
|
||||
{ "testing this ", "this$", "", false },
|
||||
{ "testing this$", "this%$$", "this$", true },
|
||||
|
||||
// start of line
|
||||
{ "testing this", "^testing", "testing", true },
|
||||
{ " testing this", "^testing", "", false },
|
||||
{ "testing this", "^%w+", "testing", true },
|
||||
{ " testing this", "^%w+", "", false },
|
||||
}
|
||||
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
for entry, i in ENTRIES {
|
||||
captures[0] = {}
|
||||
length, err := lua.find_aux(entry.s, entry.p, 0, false, &captures)
|
||||
ok := length > 0 && err == .OK
|
||||
result := entry.s[captures[0].start:captures[0].end]
|
||||
success := entry.ok == ok && result == entry.result
|
||||
|
||||
if !success {
|
||||
fmt.eprintf("Match failed for: haystack = %s\tpattern = %s\n", entry.s, entry.p)
|
||||
fmt.eprintf("Match invalid result! |WANTED:| %s |GOT:| %s\n", entry.result, result)
|
||||
}
|
||||
|
||||
expect(t, entry.ok == ok, "find result didnt match")
|
||||
expect(t, result == entry.result, "entry result didnt match")
|
||||
}
|
||||
}
|
||||
|
||||
@test
|
||||
test_captures :: proc(t: ^testing.T) {
|
||||
Temp :: struct {
|
||||
pattern: string,
|
||||
captures: [lua.MAXCAPTURES]lua.Match,
|
||||
}
|
||||
|
||||
// match all captures
|
||||
compare_captures :: proc(t: ^testing.T, test: ^Temp, haystack: string, comp: []string, loc := #caller_location) {
|
||||
length, err := lua.find_aux(haystack, test.pattern, 0, false, &test.captures)
|
||||
expect(t, len(comp) == length, "didnt match input comparison strings", loc)
|
||||
|
||||
for i in 0..<length {
|
||||
cap := test.captures[i]
|
||||
text := haystack[cap.start:cap.end]
|
||||
expect(t, comp[i] == text, "didnt match comparison string", loc)
|
||||
}
|
||||
}
|
||||
|
||||
// match to expected results
|
||||
matches :: proc(t: ^testing.T, test: ^Temp, haystack: string, ok: bool, loc := #caller_location) {
|
||||
length, err := lua.find_aux(haystack, test.pattern, 0, false, &test.captures)
|
||||
result := length > 0 && err == .OK
|
||||
expect(t, result == ok, "result didnt eq", loc)
|
||||
}
|
||||
|
||||
temp := Temp { pattern = "(one).+" }
|
||||
compare_captures(t, &temp, " one two", { "one two", "one" })
|
||||
compare_captures(t, &temp, "three", {})
|
||||
|
||||
matches(t, &temp, "one dog", true)
|
||||
matches(t, &temp, "dog one ", true)
|
||||
matches(t, &temp, "dog one", false)
|
||||
|
||||
temp.pattern = "^(%a+)"
|
||||
matches(t, &temp, "one dog", true)
|
||||
matches(t, &temp, " one dog", false)
|
||||
|
||||
// multiple captures
|
||||
{
|
||||
haystack := " 233 hello dolly"
|
||||
pattern := "%s*(%d+)%s+(%S+)"
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
lua.find_aux(haystack, pattern, 0, false, &captures)
|
||||
cap1 := captures[1]
|
||||
cap2 := captures[2]
|
||||
text1 := haystack[cap1.start:cap1.end]
|
||||
text2 := haystack[cap2.start:cap2.end]
|
||||
expect(t, text1 == "233", "Multi-Capture failed at 1")
|
||||
expect(t, text2 == "hello", "Multi-Capture failed at 2")
|
||||
}
|
||||
}
|
||||
|
||||
@test
|
||||
test_gmatch :: proc(t: ^testing.T) {
|
||||
{
|
||||
haystack := "testing this out 123"
|
||||
pattern := "%w+"
|
||||
s := &haystack
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
output := [?]string { "testing", "this", "out", "123" }
|
||||
index: int
|
||||
|
||||
for match in lua.gmatch(s, pattern, &captures) {
|
||||
expect(t, output[index] == match, fmt.tprintf("GMATCH %d failed: %s != %s\n", index, output[index], match))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
haystack := "#afdde6"
|
||||
pattern := "%x%x"
|
||||
s := &haystack
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
output := [?]string { "af", "dd", "e6" }
|
||||
index: int
|
||||
|
||||
for match in lua.gmatch(s, pattern, &captures) {
|
||||
expect(t, output[index] == match, fmt.tprintf("GMATCH %d failed: %s != %s\n", index, output[index], match))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
haystack := "testing outz captures yo outz outtz"
|
||||
pattern := "(out)z"
|
||||
s := &haystack
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
output := [?]string { "out", "out" }
|
||||
index: int
|
||||
|
||||
for match in lua.gmatch(s, pattern, &captures) {
|
||||
expect(t, output[index] == match, fmt.tprintf("GMATCH %d failed: %s != %s\n", index, output[index], match))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@test
|
||||
test_gsub :: proc(t: ^testing.T) {
|
||||
result := lua.gsub("testing123testing", "%d+", " sup ", context.temp_allocator)
|
||||
expect(t, result == "testing sup testing", "GSUB 0: failed")
|
||||
result = lua.gsub("testing123testing", "%a+", "345", context.temp_allocator)
|
||||
expect(t, result == "345123345", "GSUB 1: failed")
|
||||
}
|
||||
|
||||
@test
|
||||
test_gfind :: proc(t: ^testing.T) {
|
||||
{
|
||||
haystack := "test1 123 test2 123 test3"
|
||||
pattern := "%w+"
|
||||
captures: [lua.MAXCAPTURES]lua.Match
|
||||
s := &haystack
|
||||
output := [?]string { "test1", "123", "test2", "123", "test3" }
|
||||
index: int
|
||||
|
||||
for word in lua.gfind(s, pattern, &captures) {
|
||||
expect(t, output[index] == word, fmt.tprintf("GFIND %d failed: %s != %s\n", index, output[index], word))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main :: proc() {
|
||||
t: testing.T
|
||||
test_find(&t)
|
||||
test_match(&t)
|
||||
test_captures(&t)
|
||||
test_gmatch(&t)
|
||||
test_gsub(&t)
|
||||
test_gfind(&t)
|
||||
|
||||
fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count)
|
||||
if TEST_fail > 0 {
|
||||
os.exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user