ID:2651039
 
Resolved
Multiple bugs existed when specifying start and end positions in findtext() and replacetext(), especially with regular expressions.
BYOND Version:513.1541
Operating System:Linux
Web Browser:Firefox 82.0
Applies to:Dream Daemon
Status: Resolved (513.1542)

This issue has been resolved.
Descriptive Problem Summary: replacetext() sometimes replaces where the ref says it shouldn't, or doesn't replace where the ref says it should; additionally, findtext() with a regex Needle finds a match outside of the area the ref says it should search

Numbered Steps to Reproduce Problem: Run code below

Code Snippet (if applicable) to Reproduce Problem:
// Change this if you'd prefer to have output go to world<<
#define OUTPUT(s) world.log << s

// Change to 1 to show all tests, rather than only failing tests
#define show_ok 0

/proc/test_replace(str, start, end, as_regex)
var/c_index = findtext(str, "c")
var/e_index = findtext(str, "e")

if(start < 0)
c_index -= length(str) + 1 // 1 becomes -len, len becomes -1
e_index -= length(str) + 1

var/start_ok = start <= c_index
var/end_ok = end == 0 || end > (as_regex ? c_index : e_index) // regex matches can extend past End, so we only need the 'c' to be inside the range

var/expect
var/expect_find
if(start_ok && end_ok)
expect = replacetext(str, "cde", "___")
expect_find = findtext(str, "cde")
else
expect = str
expect_find = 0

var/needle
var/needle_desc

if(as_regex)
needle = regex("cde")
needle_desc = "/cde/"
else
needle = "cde"
needle_desc = "cde"

var/res = replacetext(str, needle, "___", start, end)
var/desc = "replacetext([str], [needle_desc], ___, [start], [end])"
var/res_find = findtext(str, needle, start, end)
var/desc_find = "findtext([str], [needle_desc], [start], [end])"

if(res == expect)
if(show_ok)
OUTPUT("[desc] == [expect] OK")
else
OUTPUT("[desc] != [expect] FAIL [res]")
nfail++

if(res_find == expect_find)
if(show_ok)
OUTPUT("[desc_find] == [expect_find] OK")
else
OUTPUT("[desc_find] != [expect_find] FAIL [res_find]")
nfail++

ncheck += 2

var/nfail = 0
var/ncheck = 0
/world/New()
var/test_strs = list(
"abcdefg",
"abcde",
"cdefg",
)

for(var/as_regex in list(0, 1))
for(var/str in test_strs)
for(var/start in 1 to length(str))
test_replace(str, start, 0, as_regex)
for(var/end in start+1 to length(str)+1)
test_replace(str, start, end, as_regex)

for(var/start in -length(str) to -1)
for(var/end in start+1 to 0)
test_replace(str, start, end, as_regex)

if(nfail)
OUTPUT("\n\n[nfail] failed checks of [ncheck]") // 49 failed checks of 532
else
OUTPUT("\n\nAll [ncheck] checks OK")

del(src)


// Some examples from the output of the above code to illustrate the specific issues I've noticed:

// Exact bounds provided in the middle call; [2, 6) works, [3, 7) works, but [3, 6) fails.
replacetext(abcdefg, cde, ___, 2, 6) == ab___fg OK
replacetext(abcdefg, cde, ___, 3, 6) != ab___fg FAIL abcdefg
replacetext(abcdefg, cde, ___, 3, 7) == ab___fg OK

// The bounds here should be identical, but the behaviour is different
replacetext(abcde, cde, ___, 3, 0) == ab___ OK
replacetext(abcde, cde, ___, 3, 6) != ab___ FAIL abcde

// The match starts at index 3, but the regex matches when Start=4 is given
replacetext(abcdefg, /cde/, ___, 1, 0) == ab___fg OK
replacetext(abcdefg, /cde/, ___, 2, 0) == ab___fg OK
replacetext(abcdefg, /cde/, ___, 3, 0) == ab___fg OK
replacetext(abcdefg, /cde/, ___, 4, 0) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, 5, 0) == abcdefg OK

// This also applies for an explicit upper bound, with the added case that Start=1 with nonzero End never matches.
replacetext(abcdefg, /cde/, ___, 1, 8) != ab___fg FAIL abcdefg
replacetext(abcdefg, /cde/, ___, 2, 8) == ab___fg OK
replacetext(abcdefg, /cde/, ___, 3, 8) == ab___fg OK
replacetext(abcdefg, /cde/, ___, 4, 8) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, 5, 8) == abcdefg OK

// findtext() with a regex Needle finds matches one character past where it should stop
findtext(abcdefg, /cde/, 1, 2) == 0 OK
findtext(abcdefg, /cde/, 1, 3) != 0 FAIL 3
findtext(abcdefg, /cde/, 1, 4) == 3 OK


Full failing-tests-only output of the code above in 513.1541
replacetext(abcdefg, cde, ___, 3, 6) != ab___fg FAIL abcdefg
replacetext(abcdefg, cde, ___, -5, -2) != ab___fg FAIL abcdefg
replacetext(abcde, cde, ___, 3, 6) != ab___ FAIL abcde
replacetext(cdefg, cde, ___, 1, 4) != ___fg FAIL cdefg
replacetext(cdefg, cde, ___, -5, -2) != ___fg FAIL cdefg
findtext(abcdefg, /cde/, 1, 3) != 0 FAIL 3
replacetext(abcdefg, /cde/, ___, 1, 4) != ab___fg FAIL abcdefg
replacetext(abcdefg, /cde/, ___, 1, 5) != ab___fg FAIL abcdefg
replacetext(abcdefg, /cde/, ___, 1, 6) != ab___fg FAIL abcdefg
replacetext(abcdefg, /cde/, ___, 1, 7) != ab___fg FAIL abcdefg
replacetext(abcdefg, /cde/, ___, 1, 8) != ab___fg FAIL abcdefg
findtext(abcdefg, /cde/, 2, 3) != 0 FAIL 3
replacetext(abcdefg, /cde/, ___, 4, 0) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, 4, 5) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, 4, 6) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, 4, 7) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, 4, 8) != abcdefg FAIL ab___fg
findtext(abcdefg, /cde/, -7, -5) != 0 FAIL 3
findtext(abcdefg, /cde/, -6, -5) != 0 FAIL 3
replacetext(abcdefg, /cde/, ___, -4, -3) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, -4, -2) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, -4, -1) != abcdefg FAIL ab___fg
replacetext(abcdefg, /cde/, ___, -4, 0) != abcdefg FAIL ab___fg
findtext(abcde, /cde/, 1, 3) != 0 FAIL 3
replacetext(abcde, /cde/, ___, 1, 4) != ab___ FAIL abcde
replacetext(abcde, /cde/, ___, 1, 5) != ab___ FAIL abcde
replacetext(abcde, /cde/, ___, 1, 6) != ab___ FAIL abcde
findtext(abcde, /cde/, 2, 3) != 0 FAIL 3
replacetext(abcde, /cde/, ___, 4, 0) != abcde FAIL ab___
replacetext(abcde, /cde/, ___, 4, 5) != abcde FAIL ab___
replacetext(abcde, /cde/, ___, 4, 6) != abcde FAIL ab___
findtext(abcde, /cde/, -5, -3) != 0 FAIL 3
findtext(abcde, /cde/, -4, -3) != 0 FAIL 3
replacetext(abcde, /cde/, ___, -2, -1) != abcde FAIL ab___
replacetext(abcde, /cde/, ___, -2, 0) != abcde FAIL ab___
replacetext(cdefg, /cde/, ___, 1, 2) != ___fg FAIL cdefg
replacetext(cdefg, /cde/, ___, 1, 3) != ___fg FAIL cdefg
replacetext(cdefg, /cde/, ___, 1, 4) != ___fg FAIL cdefg
replacetext(cdefg, /cde/, ___, 1, 5) != ___fg FAIL cdefg
replacetext(cdefg, /cde/, ___, 1, 6) != ___fg FAIL cdefg
replacetext(cdefg, /cde/, ___, 2, 0) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, 2, 3) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, 2, 4) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, 2, 5) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, 2, 6) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, -4, -3) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, -4, -2) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, -4, -1) != cdefg FAIL ___fg
replacetext(cdefg, /cde/, ___, -4, 0) != cdefg FAIL ___fg


49 failed checks of 532

(All of the replacetext() failures with a regex Needle also fail in 512.1488; all others are new or have changed since then)

Expected Results: For the behaviour of replacetext() and findtext() to match the ref

Actual Results: Behaviours did not match the ref; specifically:

- replacetext() does not replace a non-regex Needle if the bounds given by [Start, End) identify a position and length within Haystack that Needle occurs at. This includes the case where Needle occurs at the end of Haystack and End is given as an index past the end of the string.
- replacetext() begins matching a regex Needle one character early.
- replacetext() with a regex Needle, Start=1, and nonzero End makes no replacements.
- findtext() with a regex Needle matches an instance of Needle that begins at index End.

Does the problem occur:
Every time? Or how often? Every time
In other games? Yes
In other user accounts? Unknown
On other computers? Unknown

When does the problem NOT occur? N/A

Did the problem NOT occur in any earlier versions? If so, what was the last version that worked? 512 lacks the findtext() failures and the non-regex replacetext() failures are at different indexes; all replacetext() regex failures that happen on 513 also happen on 512, plus some that only happen on 512.

Workarounds: Unknown
Do you have a test case currently that does all the tests?
The first code block in the report always runs all 532 tests (they're programmatically generated by the loops in /world/New), it just only prints the ones that fail by default; the #define show_ok 0 can be changed to 1 to show all tests.
Lummox JR resolved issue with message:
Multiple bugs existed when specifying start and end positions in findtext() and replacetext(), especially with regular expressions.