Skip to content

Commit

Permalink
Add support replacement as closure in reg_replace (#1380)
Browse files Browse the repository at this point in the history
* Add support replacement as closure

* Add support replacement as closure for 'replace'

* Strict reg_replace optimization

* Add check for CClosure

* Сorrect a typo

* Fix doc and example
  • Loading branch information
Anatoliy057 authored Apr 8, 2024
1 parent 715e720 commit fa2eeb5
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 29 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,7 @@ persistence.db
persistence.json

# TSP
src/main/resources/apps.methodscript.com/tsp-output
src/main/resources/apps.methodscript.com/tsp-output

# VSCode
.vscode
22 changes: 22 additions & 0 deletions src/main/java/com/laytonsmith/core/ObjectGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.MatchResult;

/**
* This file is responsible for converting CH objects into server objects, and vice versa
Expand Down Expand Up @@ -2404,4 +2405,25 @@ public MCMetadataValue metadataValue(Mixed value, MCPlugin plugin) {
public MCMetadataValue metadataValue(Object value, MCPlugin plugin) {
return StaticLayer.GetMetadataValue(value, plugin);
}

/**
* Return match result in MethodScript variable value presentation
*
* @param matchResult match result
* @param t the target
* @return match array
*/
public CArray regMatchValue(MatchResult matchResult, Target t) {
CArray ret = CArray.GetAssociativeArray(t);
ret.set(0, new CString(matchResult.group(0), t), t);
for(int i = 1; i <= matchResult.groupCount(); i++) {
if(matchResult.group(i) == null) {
ret.set(i, CNull.NULL, t);
} else {
ret.set(i, new CString(matchResult.group(i), t), t);
}
}

return ret;
}
}
63 changes: 36 additions & 27 deletions src/main/java/com/laytonsmith/core/functions/Regex.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import com.laytonsmith.annotations.seealso;
import com.laytonsmith.core.ArgumentValidation;
import com.laytonsmith.core.MSVersion;
import com.laytonsmith.core.ObjectGenerator;
import com.laytonsmith.core.Optimizable;
import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.constructs.CArray;
import com.laytonsmith.core.constructs.CClosure;
import com.laytonsmith.core.constructs.CFunction;
import com.laytonsmith.core.constructs.CInt;
import com.laytonsmith.core.constructs.CNull;
import com.laytonsmith.core.constructs.CString;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.Target;
Expand All @@ -23,6 +24,7 @@
import com.laytonsmith.core.functions.StringHandling.split;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.natives.interfaces.Callable;
import com.laytonsmith.core.natives.interfaces.Mixed;
import java.util.EnumSet;
import java.util.HashSet;
Expand Down Expand Up @@ -91,22 +93,15 @@ public Boolean runAsync() {
public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException {
Pattern pattern = getPattern(args[0], t);
String subject = args[1].val();
CArray ret = CArray.GetAssociativeArray(t);
Matcher m = pattern.matcher(subject);
if(m.find()) {
ret.set(0, new CString(m.group(0), t), t);
for(int i = 1; i <= m.groupCount(); i++) {
if(m.group(i) == null) {
ret.set(i, CNull.NULL, t);
} else {
ret.set(i, new CString(m.group(i), t), t);
}
}
CArray ret = ObjectGenerator.GetGenerator().regMatchValue(m, t);
for(String key : getNamedGroups(pattern.pattern())) {
ret.set(key, m.group(key), t);
}
return ret;
}
return ret;
return CArray.GetAssociativeArray(t);
}

@Override
Expand Down Expand Up @@ -190,12 +185,8 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntime
Matcher m = pattern.matcher(subject);
Set<String> namedGroups = getNamedGroups(pattern.pattern());
while(m.find()) {
CArray ret = CArray.GetAssociativeArray(t);
ret.set(0, new CString(m.group(0), t), t);
CArray ret = ObjectGenerator.GetGenerator().regMatchValue(m, t);

for(int i = 1; i <= m.groupCount(); i++) {
ret.set(i, new CString(m.group(i), t), t);
}
for(String key : namedGroups) {
ret.set(key, m.group(key), t);
}
Expand Down Expand Up @@ -254,12 +245,14 @@ public Integer[] numArgs() {
@Override
public String docs() {
return "string {pattern, replacement, subject} Replaces any occurrences of pattern with the replacement in subject."
+ " Back references are allowed.";
+ " Back references are allowed."
+ " 'replacement' can be a string, or a closure that accepts a found occurrence of 'pattern'"
+ " and returns the replacement string value.";
}

@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREFormatException.class};
return new Class[]{CREFormatException.class, CRECastException.class};
}

@Override
Expand All @@ -280,12 +273,17 @@ public Boolean runAsync() {
@Override
public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException {
Pattern pattern = getPattern(args[0], t);
String replacement = args[1].val();
Mixed replacement = args[1];
String subject = args[2].val();
String ret = "";

try {
ret = pattern.matcher(subject).replaceAll(replacement);
if(replacement instanceof Callable replacer) {
ret = pattern.matcher(subject).replaceAll(mr -> ArgumentValidation.getStringObject(
replacer.executeCallable(env, t, ObjectGenerator.GetGenerator().regMatchValue(mr, t)), t));
} else {
ret = pattern.matcher(subject).replaceAll(replacement.val());
}
} catch (IndexOutOfBoundsException e) {
throw new CREFormatException("Expecting a regex group at parameter 1 of reg_replace", t);
} catch (IllegalArgumentException e) {
Expand All @@ -300,19 +298,23 @@ public ParseTree optimizeDynamic(Target t, Environment env,
Set<Class<? extends Environment.EnvironmentImpl>> envs,
List<ParseTree> children, FileOptions fileOptions)
throws ConfigCompileException, ConfigRuntimeException {
ParseTree data = children.get(0);
if(!Construct.IsDynamicHelper(data.getData())) {
String pattern = data.getData().val();
if(isLiteralRegex(pattern)) {
ParseTree patternArg = children.get(0);
Mixed patternData = patternArg.getData();
ParseTree replacementArg = children.get(1);
Mixed replacementData = replacementArg.getData();
if(!Construct.IsDynamicHelper(patternData) && !(replacementData instanceof CClosure) && !Construct.IsDynamicHelper(replacementData)) {
String pattern = patternData.val();
String replacement = replacementData.val();
if(isLiteralRegex(pattern) && !isBackreference(replacement)) {
//We want to replace this with replace()
//Note the alternative order of arguments
ParseTree replaceNode = new ParseTree(new CFunction(replace.NAME, t), data.getFileOptions());
ParseTree replaceNode = new ParseTree(new CFunction(replace.NAME, t), patternArg.getFileOptions());
replaceNode.addChildAt(0, children.get(2)); //subject -> main
replaceNode.addChildAt(1, new ParseTree(new CString(getLiteralRegex(pattern), t), replaceNode.getFileOptions())); //pattern -> what
replaceNode.addChildAt(2, children.get(1)); //replacement -> that
return replaceNode;
} else {
getPattern(data.getData(), t);
getPattern(patternArg.getData(), t);
}
}
return null;
Expand All @@ -338,7 +340,10 @@ public ExampleScript[] examples() throws ConfigCompileException {
new ExampleScript("Basic usage", "reg_replace('\\\\d', 'Z', '123abc')"),
new ExampleScript("Using backreferences", "reg_replace('abc(\\\\d+)', '$1', 'abc123')"),
new ExampleScript("Using backreferences with named captures",
"reg_replace('abc(?<foo>\\\\d+)', '${foo}', 'abc123')", "123")
"reg_replace('abc(?<foo>\\\\d+)', '${foo}', 'abc123')", "123"),
new ExampleScript("Using closure as replacement function",
"reg_replace('cat|dog', closure(@match) {return array('dog': 'cat', 'cat': 'dog')[@match[0]]},"
+ " 'Oscar is a cat. Lucy is a dog.')")
};
}

Expand Down Expand Up @@ -619,6 +624,10 @@ private static Pattern getPattern(Mixed c, Target t) throws ConfigRuntimeExcepti
}
}

private static boolean isBackreference(String replacement) {
return replacement.length() > 0 && replacement.charAt(0) == '$';
}

private static boolean isLiteralRegex(String regex) {
//These are the special characters in a regex. If a regex does not contain any of these
//characters, we can use a faster method in many cases, though the extra overhead of doing
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/com/laytonsmith/core/OptimizationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public void testRegSplitOptimization2() throws Exception {

@Test
public void testRegReplaceOptimization1() throws Exception {
assertEquals("replace('this is a thing','thing',dyn('hi'))", optimize("reg_replace('thing', dyn('hi'), 'this is a thing')"));
assertEquals("replace('this is a thing','thing','hi')", optimize("reg_replace('thing', 'hi', 'this is a thing')"));
}

@Test
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/com/laytonsmith/core/functions/RegexTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import com.laytonsmith.core.MethodScriptCompiler;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.exceptions.CRE.CRECastException;

import static com.laytonsmith.testing.StaticTest.SRun;
import java.util.Set;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import org.junit.Before;
import org.junit.BeforeClass;
Expand Down Expand Up @@ -66,6 +69,15 @@ public void testRegMatchAll() throws Exception {
public void testRegReplace() throws Exception {
assertEquals("word", SRun("reg_replace('This is a (word)', '$1', 'This is a word')", null));
assertEquals("It's a wordy day!", SRun("reg_replace('sunn', 'word', 'It\\'s a sunny day!')", null));
assertEquals("Oscar is a dog. Lucy is a cat.", SRun("""
reg_replace('cat|dog', closure(@match) {return array('dog': 'cat', 'cat': 'dog')[@match[0]]}, 'Oscar is a cat. Lucy is a dog.')
""", null));
assertEquals("Lucy pushed the mug down.", SRun("""
reg_replace('plate', closure(@match) {return 'mug'}, 'Lucy pushed the plate down.')
""", null));
assertThrows(CRECastException.class, () -> SRun("""
reg_replace('', closure() {}, 'This is fine')
""", null));
}

@Test(timeout = 10000)
Expand Down

0 comments on commit fa2eeb5

Please sign in to comment.