Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement replacetextEx_char, reimplement Splittext to match new 515 behavior #1921

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
116 changes: 89 additions & 27 deletions Content.Tests/DMProject/Tests/Text/Splittext.dm
Original file line number Diff line number Diff line change
@@ -1,34 +1,96 @@
/proc/GetFailMessage(var/test_pair)
var/O_str_got = ""
var/O_str_expected = ""
for (var/i in test_pair[1])
O_str_got += "\"[i]\", "
for (var/i in test_pair[2])
O_str_expected += "\"[i]\", "
return "Got:\n[O_str_got]\nexpected:\n[O_str_expected]\n"

/proc/RunTest()
var/test_text = "The average of 1, 2, 3, 4, 5 is: 3"
var/list/test1 = splittext(test_text, " ")
var/list/test1_expected = list("The","average","of","1,","2,","3,","4,","5","is:","3")
ASSERT(test1 ~= test1_expected)

var/list/test2 = splittext(test_text, " ", 5)
var/test2_expected = list("average","of","1,","2,","3,","4,","5","is:","3")
ASSERT(test2 ~= test2_expected)

var/list/test3 = splittext(test_text, " ", 5, 10)
var/test3_expected = list("avera")
ASSERT(test3 ~= test3_expected)
// A list of pairs of lists which is just the compiled Splittext implementation vs. the DM implementation output.
// To add your own test just create a new element in here of list(splittext(your_args), list(output_dm_gave_for_that_call))
// Note that as of writing, regex with capturing groups behaves **very** weirdly in DM, and splittext() does not yet replicate that behavior. Other regexes should be ok.
var/list/tests = list(
list(splittext(test_text, regex(@"\d"), 5, 30, 1), list("The average of ","1",", ","2",", ","3",", ","4",", ","5"," is: 3")),
list(splittext(test_text, regex(@"\d"), 5, 30), list("The average of ",", ",", ",", ",", "," is: 3")),
list(splittext(test_text, regex("")), list("T","h","e"," ","a","v","e","r","a","g","e"," ","o","f"," ","1",","," ","2",","," ","3",","," ","4",","," ","5"," ","i","s",":"," ","3","")),
list(splittext(test_text, "", 0, 10, 1), list()),
list(splittext(test_text, regex(""), 0, 0, 1), list("The average of 1, 2, 3, 4, 5 is: 3")),
list(splittext(test_text, ""), list("T","h","e"," ","a","v","e","r","a","g","e"," ","o","f"," ","1",","," ","2",","," ","3",","," ","4",","," ","5"," ","i","s",":"," ","3")),
list(splittext(test_text, "", 5, 10, 1), list("The a","","v","","e","","r","","a","","ge of 1, 2, 3, 4, 5 is: 3")),
list(splittext(test_text, "", 1, length(test_text)+1, 1), list("T","","h","","e",""," ","","a","","v","","e","","r","","a","","g","","e",""," ","","o","","f",""," ","","1","",",",""," ","","2","",",",""," ","","3","",",",""," ","","4","",",",""," ","","5",""," ","","i","","s","",":",""," ","","3")),
list(splittext(test_text, regex(""), 5, 10, 1), list("The a","","v","","e","","r","","age of 1, 2, 3, 4, 5 is: 3")),
// list(splittext(test_text, regex("()"), 1, length(test_text)+1, 1), list("T","","","h","","","e","",""," ","","","a","","","v","","","e","","","r","","","a","","","g","","","e","",""," ","","","o","","","f","",""," ","","","1","","",",","",""," ","","","2","","",",","",""," ","","","3","","",",","",""," ","","","4","","",",","",""," ","","","5","",""," ","","","i","","","s","","",":","",""," ","","","3")),
list(splittext(test_text, regex(@"")), list("T","h","e"," ","a","v","e","r","a","g","e"," ","o","f"," ","1",","," ","2",","," ","3",","," ","4",","," ","5"," ","i","s",":"," ","3","")),
list(splittext(test_text, " "), list("The","average","of","1,","2,","3,","4,","5","is:","3")),
list(splittext(test_text, " ", 5), list("The average","of","1,","2,","3,","4,","5","is:","3")),
list(splittext(test_text, " ", 5, 10), list("The average of 1, 2, 3, 4, 5 is: 3")),
list(splittext(test_text, " ", 10, 20), list("The average","of","1,","2, 3, 4, 5 is: 3")),
list(splittext(test_text, " ", 10, 20, 1), list("The average"," ","of"," ","1,"," ","2, 3, 4, 5 is: 3")),
list(splittext(test_text, regex(@"\d")), list("The average of ",", ",", ",", ",", "," is: ","")),
// list(splittext(test_text, regex(@"(\d)"), 1, length(test_text)+1, 1), list("The average of ","1","1",", ","2","2",", ","3","3",", ","4","4",", ","5","5"," is: ","3","3",""))
)
// Go through all our tests and ensure that the test results match the expected output
var/t_num = 0
var/tests_failed = 0
var/fail_output = "\n"
for (var/test_pair in tests)
t_num += 1
// Special debug output for failed tests
if (!(test_pair[1] ~= test_pair[2]))
tests_failed = 1
fail_output += "Failed test [t_num]:\n[GetFailMessage(test_pair)]"

var/list/test4 = splittext(test_text, " ", 10, 20)
var/test4_expected = list("ge","of","1,","2")
ASSERT(test4 ~= test4_expected)
if (tests_failed)
CRASH(fail_output)

var/list/test5 = splittext(test_text, " ", 10, 20, 1)
var/test5_expected = list("ge"," ","of"," ","1,"," ","2")
ASSERT(test5 ~= test5_expected)
// list(splittext(test_text, regex("")), "splittext(test_text, regex(\"\"))"),
// list(splittext(test_text, "", 0, 10, 1), "splittext(test_text, \"\", 0, 10, 1)"),
// list(splittext(test_text, regex(""), 0, 0, 1), "splittext(test_text, regex(\"\"), 0, 0, 1)"),
// list(splittext(test_text, ""), "splittext(test_text, \"\")"),
// list(splittext(test_text, "", 5, 10, 1), "splittext(test_text, \"\", 5, 10, 1)"),
// list(splittext(test_text, "", 1, length(test_text)+1, 1), "splittext(test_text, \"\", 1, length(test_text)+1, 1)"),
// list(splittext(test_text, regex(""), 5, 10, 1), "splittext(test_text, regex(\"\"), 5, 10, 1)"),
// list(splittext(test_text, regex("()"), 1, length(test_text)+1, 1), "splittext(test_text, regex(\"()\"), 1, length(test_text)+1, 1)"),
// list(splittext(test_text, regex(@"")), "splittext(test_text, regex(@\"\"))"),
// list(splittext(test_text, " "), "splittext(test_text, \" \")"),
// list(splittext(test_text, " ", 5), "splittext(test_text, \" \", 5)"),
// list(splittext(test_text, " ", 5, 10), "splittext(test_text, \" \", 5, 10)"),
// list(splittext(test_text, " ", 10, 20), "splittext(test_text, \" \", 10, 20)"),
// list(splittext(test_text, " ", 10, 20, 1), "splittext(test_text, \" \", 10, 20, 1)"),
// list(splittext(test_text, regex(@"\d")), "splittext(test_text, regex(@\"\\d\"))"),
// list(splittext(test_text, regex(@"\d"), 5, 30), "splittext(test_text, regex(@\"\\d\"), 5, 30)"),
// list(splittext(test_text, regex(@"\d"), 5, 30, 1), "splittext(test_text, regex(@\"\\d\"), 5, 30, 1)"),

//it's regex time
var/test6 = splittext(test_text, regex(@"\d"))
var/test6_expected = list("The average of ",", ",", ",", ",", "," is: ","")
ASSERT(test6 ~= test6_expected)
// var/list/tests = list(
// list(splittext(test_text, regex("")), list("T","h","e"," ","a","v","e","r","a","g","e"," ","o","f"," ","1",","," ","2",","," ","3",","," ","4",","," ","5"," ","i","s",":"," ","3","")),
// list(splittext(test_text, "", 0, 10, 1), list()),
// list(splittext(test_text, regex(""), 0, 0, 1), list("The average of 1, 2, 3, 4, 5 is: 3")),
// list(splittext(test_text, ""), list("T","h","e"," ","a","v","e","r","a","g","e"," ","o","f"," ","1",","," ","2",","," ","3",","," ","4",","," ","5"," ","i","s",":"," ","3")),
// list(splittext(test_text, "", 5, 10, 1), list("The a","","v","","e","","r","","a","","ge of 1, 2, 3, 4, 5 is: 3")),
// list(splittext(test_text, "", 1, length(test_text)+1, 1), list("T","","h","","e",""," ","","a","","v","","e","","r","","a","","g","","e",""," ","","o","","f",""," ","","1","",",",""," ","","2","",",",""," ","","3","",",",""," ","","4","",",",""," ","","5",""," ","","i","","s","",":",""," ","","3")),
// list(splittext(test_text, regex(""), 5, 10, 1), list("The a","","v","","e","","r","","age of 1, 2, 3, 4, 5 is: 3")),
// list(splittext(test_text, regex("()"), 1, length(test_text)+1, 1), list("T","","","h","","","e","",""," ","","","a","","","v","","","e","","","r","","","a","","","g","","","e","",""," ","","","o","","","f","",""," ","","","1","","",",","",""," ","","","2","","",",","",""," ","","","3","","",",","",""," ","","","4","","",",","",""," ","","","5","",""," ","","","i","","","s","","",":","",""," ","","","3")),
// list(splittext(test_text, regex(@"")), list("T","h","e"," ","a","v","e","r","a","g","e"," ","o","f"," ","1",","," ","2",","," ","3",","," ","4",","," ","5"," ","i","s",":"," ","3","")),
// list(splittext(test_text, " "), list("The","average","of","1,","2,","3,","4,","5","is:","3")),
// list(splittext(test_text, " ", 5), list("The average","of","1,","2,","3,","4,","5","is:","3")),
// list(splittext(test_text, " ", 5, 10), list("The average of 1, 2, 3, 4, 5 is: 3")),
// list(splittext(test_text, " ", 10, 20), list("The average","of","1,","2, 3, 4, 5 is: 3")),
// list(splittext(test_text, " ", 10, 20, 1), list("The average"," ","of"," ","1,"," ","2, 3, 4, 5 is: 3")),
// list(splittext(test_text, regex(@"\d")), list("The average of ",", ",", ",", ",", "," is: ","")),
// list(splittext(test_text, regex(@"\d"), 5, 30), list("The average of ",", ",", ",", ",", "," is: 3")),
// list(splittext(test_text, regex(@"\d"), 5, 30, 1), list("The average of ","1",", ","2",", ","3",", ","4",", ","5"," is: 3")),
// )

var/test7 = splittext(test_text, regex(@"\d"), 5, 30)
var/test7_expected = list("average of ",", ",", ",", ",", "," ")
ASSERT(test7 ~= test7_expected)

var/test8 = splittext(test_text, regex(@"\d"), 5, 30, 1)
var/test8_expected = list("average of ","1",", ","2",", ","3",", ","4",", ","5"," ")
ASSERT(test8 ~= test8_expected)
// var/test_text = "The average of 1, 2, 3, 4, 5 is: 3"
// var/list/tests = list(
// list(splittext(test_text, regex(@"\d")), "splittext(test_text, regex(@\"\\d\"))"),
// list(splittext(test_text, regex(@"\d"), 5, 30), "splittext(test_text, regex(@\"\\d\"), 5, 30)"),
// list(splittext(test_text, regex(@"\d"), 5, 30, 1), "splittext(test_text, regex(@\"\\d\"), 5, 30, 1)"),
// )
// for (var/test in tests)
// var/delimiter = "\",\""
// world.log << "list([test[2]], list(\"[jointext(test[1], delimiter)]\")),"
3 changes: 1 addition & 2 deletions DMCompiler/DMStandard/_Standard.dm
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ proc/winset(player, control_id, params)
#include "UnsortedAdditions.dm"

proc/replacetextEx_char(Haystack as text, Needle, Replacement, Start = 1, End = 0) as text
set opendream_unimplemented = TRUE
return Haystack
return jointext(splittext(Haystack, Needle, Start, End), Replacement)

/proc/step(atom/movable/Ref as /atom/movable, var/Dir, var/Speed=0) as num
//TODO: Speed = step_size if Speed is 0
Expand Down
125 changes: 107 additions & 18 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2598,60 +2598,149 @@ public static DreamValue NativeProc_splicetext_char(NativeProc.Bundle bundle, Dr
[DreamProcParameter("End", Type = DreamValueTypeFlag.Float, DefaultValue = 0)]
[DreamProcParameter("include_delimiters", Type = DreamValueTypeFlag.Float, DefaultValue = 0)]
public static DreamValue NativeProc_splittext(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) {
// Handle args first
if (!bundle.GetArgument(0, "Text").TryGetValueAsString(out var text)) {
return new DreamValue(bundle.ObjectTree.CreateList());
}

var delim = bundle.GetArgument(1, "Delimiter"); //can either be a regex or string

// Handle checking for empty string delimiters for non-regex, and setting up delimiterString/regexPattern
bool isEmptyString = false;
bool isRegex = false; // This exists specifically so that we can mimic DM and add some "" at the end if its not cut off beforehand (god, why)
delim.TryGetValueAsString(out var delimiterString);
if (delim.TryGetValueAsDreamObject<DreamObjectRegex>(out var regexObject)) {
isRegex = true;
string regexPattern = regexObject.Regex.ToString();
isEmptyString = string.IsNullOrEmpty(regexPattern);
}
else if (delimiterString is string) {
isEmptyString = string.IsNullOrEmpty(delimiterString);
}
else {
return new DreamValue(bundle.ObjectTree.CreateList()); // No Delimiter arg = Empty list
}

int start = 0;
int end = 0;
if(bundle.GetArgument(2, "Start").TryGetValueAsInteger(out start))
bool gotStart = bundle.GetArgument(2, "Start").TryGetValueAsInteger(out start);
bool gotEnd = bundle.GetArgument(3, "End").TryGetValueAsInteger(out end);
if (gotStart) {
if (gotEnd && (start == end)) {
return new DreamValue(bundle.ObjectTree.CreateList([text]));
}
else if (start == 0) {
return new DreamValue(bundle.ObjectTree.CreateList());
}
start -= 1; //1-indexed
if(bundle.GetArgument(3, "End").TryGetValueAsInteger(out end))
}

if(gotEnd) {
if(end == 0)
end = text.Length;
else
end -= 1; //1-indexed
}

bool includeDelimiters = false;
if(bundle.GetArgument(4, "include_delimiters").TryGetValueAsInteger(out var includeDelimitersInt))
includeDelimiters = includeDelimitersInt != 0; //idk why BYOND doesn't just use truthiness, but it doesn't, so...

// save the start/end beforehand to staple back on at the end
string startText = text[0..Math.Max(start,0)];
string endText = text[Math.Min(end, text.Length)..];
// figure out if we should be adding the start/end in the first place

string cutText;
if(start > 0 || end < text.Length)
text = text[Math.Max(start,0)..Math.Min(end, text.Length)];
cutText = text[Math.Max(start,0)..Math.Min(end, text.Length)];
else
cutText = text;

var delim = bundle.GetArgument(1, "Delimiter"); //can either be a regex or string
// Actual special handling for empty str Delimiters
if (isEmptyString && !isRegex) {
if (includeDelimiters) {
var values = new List<string>();
bool excludeLastDelimiter = end >= text.Length;
for (var i = 0; i < cutText.Length; i++) {
values.Add(cutText[i].ToString());
values.Add("");
}
if (!excludeLastDelimiter) {
values.Add(endText);
}
else {
values.Pop();
}
string[] finalValues = values.ToArray();
finalValues[0] = startText + finalValues[0];
if (excludeLastDelimiter)
finalValues[finalValues.Length-1] = finalValues[finalValues.Length-1] + endText;
return new DreamValue(bundle.ObjectTree.CreateList(finalValues.ToArray()));
}
else {
// 'Easy' solution if we don't need the delimiters.
// Weird because of the way the start text is prepended to the 1st and end text is appened as a new element in DM
IEnumerable<string> values = cutText.Select(x => new string(x, 1));
if (end < text.Length)
values.Append("");
values.Append(endText);
string[] finalValues = values.ToArray();
finalValues[0] = startText + finalValues[0];
return new DreamValue(bundle.ObjectTree.CreateList(finalValues));
}
}

if (delim.TryGetValueAsDreamObject<DreamObjectRegex>(out var regexObject)) {
// Delimiter is regex/not empty, handle accordingly
if (regexObject is DreamObjectRegex) {
if(includeDelimiters) {
var values = new List<string>();
int pos = 0;
foreach (Match m in regexObject.Regex.Matches(text)) {
values.Add(text.Substring(pos, m.Index - pos));
values.Add(m.Value);
foreach (Match m in regexObject.Regex.Matches(cutText)) {
string subStr = cutText.Substring(pos, m.Index - pos);
// First pair of subStr/m.Value must not be included if both are empty
if (!string.IsNullOrEmpty(subStr))
values.Add(subStr);
values.Add(m.Value);
pos = m.Index + m.Length;
}
values.Add(text.Substring(pos));
return new DreamValue(bundle.ObjectTree.CreateList(values.ToArray()));
// Trim zero-width matches at start/end because BYOND
if (string.IsNullOrEmpty(values.First()))
values.RemoveAt(0);
if (string.IsNullOrEmpty(values.Last()))
values.Pop();
// Glue back together afterwards (Order is important)
values.Insert(0, startText);
values.Add(endText);
string[] finalValues = values.ToArray();
return new DreamValue(bundle.ObjectTree.CreateList(finalValues));
} else {
return new DreamValue(bundle.ObjectTree.CreateList(regexObject.Regex.Split(text)));
string[] values = regexObject.Regex.Split(cutText);
// Weird DM behavior: Zero-width splits dont include matches for the leftmost side
if (isEmptyString)
values = values[1..];
values[0] = startText + values[0];
values[values.Length-1] = values[values.Length-1] + endText;
return new DreamValue(bundle.ObjectTree.CreateList(values));
}
} else if (delim.TryGetValueAsString(out var delimiter)) {
} else {
string[] splitText;
if(includeDelimiters) {
//basically split on delimeter, and then add the delimiter back in after each split (except the last one)
splitText= text.Split(delimiter);
//basically split on delimiter, and then add the delimiter back in after each split (except the last one)
splitText = cutText.Split(delimiterString);
string[] longerSplitText = new string[splitText.Length * 2 - 1];
for(int i = 0; i < splitText.Length; i++) {
longerSplitText[i * 2] = splitText[i];
if(i < splitText.Length - 1)
longerSplitText[i * 2 + 1] = delimiter;
longerSplitText[i * 2 + 1] = delimiterString;
}
splitText = longerSplitText;
} else {
splitText = text.Split(delimiter);
splitText = cutText.Split(delimiterString);
}
splitText[0] = startText + splitText[0];
splitText[splitText.Length-1] = splitText[splitText.Length-1] + endText;
return new DreamValue(bundle.ObjectTree.CreateList(splitText));
} else {
return new DreamValue(bundle.ObjectTree.CreateList());
}
}

Expand Down