diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 98e1dba5b..de1ef1b31 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -75,6 +75,12 @@ contributor + + Lildirt + + contributor + + jb_aero diff --git a/pom.xml b/pom.xml index 161e92268..7907d1027 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,12 @@ contributor + + Lildirt + + contributor + + jb_aero diff --git a/src/main/java/com/laytonsmith/core/functions/StringHandling.java b/src/main/java/com/laytonsmith/core/functions/StringHandling.java index a65153b07..3cf705dc3 100644 --- a/src/main/java/com/laytonsmith/core/functions/StringHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/StringHandling.java @@ -61,1573 +61,1610 @@ public class StringHandling { public static String docs() { - return "These class provides functions that allow strings to be manipulated"; + return "These class provides functions that allow strings to be manipulated"; } @api public static class concat extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "concat"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{Integer.MAX_VALUE}; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - StringBuilder b = new StringBuilder(); - for (int i = 0; i < args.length; i++) { - b.append(args[i].val()); - } - return new CString(b.toString(), t); - } - - @Override - public String docs() { - return "string {var1, [var2...]} Concatenates any number of arguments together, and returns a string"; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_0_1; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { - OptimizationUtilities.pullUpLikeFunctions(children, this.getName()); - for (ParseTree child : children) { - if (child.getData() instanceof CLabel) { - throw new ConfigCompileException("Invalid use of concatenation with label", child.getTarget()); - } - } - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Functional usage", "concat('1', '2', '3', '4')"), - new ExampleScript("Symbolic usage", "'1' . '2' . '3' . '4'"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN, - OptimizationOption.OPTIMIZE_DYNAMIC); - } + @Override + public String getName() { + return "concat"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{Integer.MAX_VALUE}; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + b.append(args[i].val()); + } + return new CString(b.toString(), t); + } + + @Override + public String docs() { + return "string {var1, [var2...]} Concatenates any number of arguments together, and returns a string"; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_0_1; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { + OptimizationUtilities.pullUpLikeFunctions(children, this.getName()); + for (ParseTree child : children) { + if (child.getData() instanceof CLabel) { + throw new ConfigCompileException("Invalid use of concatenation with label", child.getTarget()); + } + } + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Functional usage", "concat('1', '2', '3', '4')"), + new ExampleScript("Symbolic usage", "'1' . '2' . '3' . '4'"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN, + OptimizationOption.OPTIMIZE_DYNAMIC); + } } @api @noprofile public static class sconcat extends AbstractFunction implements Optimizable { - private static final String g = new DataHandling.g().getName(); - private static final String p = new Compiler.p().getName(); - - @Override - public String getName() { - return "sconcat"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{Integer.MAX_VALUE}; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - StringBuilder b = new StringBuilder(); - for (int i = 0; i < args.length; i++) { - if (i > 0) { - b.append(" "); - } - b.append(args[i] == null ? "" : args[i].val()); - } - return new CString(b.toString(), t); - } - - @Override - public String docs() { - return "string {var1, [var2...]} Concatenates any number of arguments together, but puts a space between elements"; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_0_1; - } - - @Override - public Boolean runAsync() { - return null; - } - - public final static String STRING = new DataHandling._string().getName(); - - @Override - public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { - OptimizationUtilities.pullUpLikeFunctions(children, this.getName()); - for (ParseTree child : children) { - if (child.getData() instanceof CLabel) { - throw new ConfigCompileException("Invalid use of concatenation with label", child.getTarget()); - } - } - //Remove empty g or p children - Iterator it = children.iterator(); - while (it.hasNext()) { - ParseTree n = it.next(); - if (n.getData() instanceof CFunction - && (g.equals(n.getData().val()) - || p.equals(n.getData().val())) - && !n.hasChildren()) { - it.remove(); - } - } - // We have to turn off constant optimization because sconcat is a strange construct that does have some special - // compiler significance, especially if we end up being optimized out. So here, we will check to see if we are fully - // constant, and combine the constant values, but taking care not to do so with CKeywords or CLabels, which are otherwise constant. - // We start at 1, because if we only have one child, we want to skip ahead. - for (int i = 1; i < children.size(); i++) { - ParseTree child = children.get(i); - if (child.isConst() && !(child.getData() instanceof CKeyword) && !(child.getData() instanceof CLabel)) { - if (children.get(i - 1).isConst() && !(children.get(i - 1).getData() instanceof CKeyword)) { - // Combine these two into one, and replace i - 1, and remove i - String s1 = children.get(i - 1).getData().val(); - String s2 = child.getData().val(); - children.set(i - 1, new ParseTree(new CString(s1 + " " + s2, t), fileOptions)); - children.remove(i); - i--; - } - } - } - //If we don't have any children, remove us as well, though we still have to check if it's a keword. - if (children.size() == 1) { - ParseTree child = children.get(0); - if (child.getData() instanceof CKeyword || child.getData() instanceof CLabel) { - return child; - } else { - // sconcat only returns a string (except in the special case above) so we need to - // return the string value if it's not already a string - try { - if (InstanceofUtil.isInstanceof(child.getData(), "string")) { - return child; - } - } catch (IllegalArgumentException ex) { - // Ignored, we'll just toString it, because it's an unknown type. - } - ParseTree node = new ParseTree(new CFunction(STRING, t), fileOptions); - node.addChild(child); - return node; - } - } - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Functional usage", "sconcat('1', '2', '3', '4')"), - new ExampleScript("Implied usage, due to no operators", "'1' '2' '3' '4'"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CACHE_RETURN, - OptimizationOption.OPTIMIZE_DYNAMIC); - } + private static final String g = new DataHandling.g().getName(); + private static final String p = new Compiler.p().getName(); + + @Override + public String getName() { + return "sconcat"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{Integer.MAX_VALUE}; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + b.append(" "); + } + b.append(args[i] == null ? "" : args[i].val()); + } + return new CString(b.toString(), t); + } + + @Override + public String docs() { + return "string {var1, [var2...]} Concatenates any number of arguments together, but puts a space between elements"; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_0_1; + } + + @Override + public Boolean runAsync() { + return null; + } + + public final static String STRING = new DataHandling._string().getName(); + + @Override + public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { + OptimizationUtilities.pullUpLikeFunctions(children, this.getName()); + for (ParseTree child : children) { + if (child.getData() instanceof CLabel) { + throw new ConfigCompileException("Invalid use of concatenation with label", child.getTarget()); + } + } + //Remove empty g or p children + Iterator it = children.iterator(); + while (it.hasNext()) { + ParseTree n = it.next(); + if (n.getData() instanceof CFunction + && (g.equals(n.getData().val()) + || p.equals(n.getData().val())) + && !n.hasChildren()) { + it.remove(); + } + } + // We have to turn off constant optimization because sconcat is a strange construct that does have some special + // compiler significance, especially if we end up being optimized out. So here, we will check to see if we are fully + // constant, and combine the constant values, but taking care not to do so with CKeywords or CLabels, which are otherwise constant. + // We start at 1, because if we only have one child, we want to skip ahead. + for (int i = 1; i < children.size(); i++) { + ParseTree child = children.get(i); + if (child.isConst() && !(child.getData() instanceof CKeyword) && !(child.getData() instanceof CLabel)) { + if (children.get(i - 1).isConst() && !(children.get(i - 1).getData() instanceof CKeyword)) { + // Combine these two into one, and replace i - 1, and remove i + String s1 = children.get(i - 1).getData().val(); + String s2 = child.getData().val(); + children.set(i - 1, new ParseTree(new CString(s1 + " " + s2, t), fileOptions)); + children.remove(i); + i--; + } + } + } + //If we don't have any children, remove us as well, though we still have to check if it's a keword. + if (children.size() == 1) { + ParseTree child = children.get(0); + if (child.getData() instanceof CKeyword || child.getData() instanceof CLabel) { + return child; + } else { + // sconcat only returns a string (except in the special case above) so we need to + // return the string value if it's not already a string + try { + if (InstanceofUtil.isInstanceof(child.getData(), "string")) { + return child; + } + } catch (IllegalArgumentException ex) { + // Ignored, we'll just toString it, because it's an unknown type. + } + ParseTree node = new ParseTree(new CFunction(STRING, t), fileOptions); + node.addChild(child); + return node; + } + } + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Functional usage", "sconcat('1', '2', '3', '4')"), + new ExampleScript("Implied usage, due to no operators", "'1' '2' '3' '4'"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CACHE_RETURN, + OptimizationOption.OPTIMIZE_DYNAMIC); + } } @api public static class replace extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "replace"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{3}; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - String thing = args[0].val(); - String what = args[1].val(); - String that = args[2].val(); - return new CString(thing.replace(what, that), t); - } - - @Override - public String docs() { - return "string {subject, search, replacement} Replaces all instances of 'search' with 'replacement' in 'subject'"; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_0_1; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "replace('Where in the world is Carmen Sandiego?', 'Carmen Sandiego', 'Waldo')"), - new ExampleScript("No match found", "replace('The same thing', 'not found', '404')"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "replace"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{3}; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + String thing = args[0].val(); + String what = args[1].val(); + String that = args[2].val(); + return new CString(thing.replace(what, that), t); + } + + @Override + public String docs() { + return "string {subject, search, replacement} Replaces all instances of 'search' with 'replacement' in 'subject'"; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_0_1; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "replace('Where in the world is Carmen Sandiego?', 'Carmen Sandiego', 'Waldo')"), + new ExampleScript("No match found", "replace('The same thing', 'not found', '404')"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class parse_args extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "parse_args"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1, 2}; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - String string = args[0].val(); - boolean useAdvanced = false; - if (args.length >= 2) { - useAdvanced = Static.getBoolean(args[1]); - } - List a = new ArrayList<>(); - if (!useAdvanced) { - String[] sa = string.split(" "); - for (String s : sa) { - if (!s.trim().isEmpty()) { - a.add(new CString(s.trim(), t)); - } - } - } else { - for (String s : StringUtils.ArgParser(string)) { - a.add(new CString(s.trim(), t)); - } - } - Construct[] csa = new Construct[a.size()]; - for (int i = 0; i < a.size(); i++) { - csa[i] = a.get(i); - } - return new CArray(t, csa); - } - - @Override - public String docs() { - return "array {string, [useAdvanced]} Parses string into an array, where string is a space seperated list of arguments. Handy for turning" - + " $ into a usable array of items with which to script against. Extra spaces are ignored, so you would never get an empty" - + " string as an input. useAdvanced defaults to false, but if true, uses a basic argument parser that supports quotes for" - + " allowing arguments with spaces."; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_0_1; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Demonstrates basic usage", "parse_args('This turns into 5 arguments')"), - new ExampleScript("Demonstrates usage with extra spaces", "parse_args('This trims extra spaces')") - }; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "parse_args"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1, 2}; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + String string = args[0].val(); + boolean useAdvanced = false; + if (args.length >= 2) { + useAdvanced = Static.getBoolean(args[1]); + } + List a = new ArrayList<>(); + if (!useAdvanced) { + String[] sa = string.split(" "); + for (String s : sa) { + if (!s.trim().isEmpty()) { + a.add(new CString(s.trim(), t)); + } + } + } else { + for (String s : StringUtils.ArgParser(string)) { + a.add(new CString(s.trim(), t)); + } + } + Construct[] csa = new Construct[a.size()]; + for (int i = 0; i < a.size(); i++) { + csa[i] = a.get(i); + } + return new CArray(t, csa); + } + + @Override + public String docs() { + return "array {string, [useAdvanced]} Parses string into an array, where string is a space seperated list of arguments. Handy for turning" + + " $ into a usable array of items with which to script against. Extra spaces are ignored, so you would never get an empty" + + " string as an input. useAdvanced defaults to false, but if true, uses a basic argument parser that supports quotes for" + + " allowing arguments with spaces."; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_0_1; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Demonstrates basic usage", "parse_args('This turns into 5 arguments')"), + new ExampleScript("Demonstrates usage with extra spaces", "parse_args('This trims extra spaces')") + }; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class trim extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "trim"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "string {s} Returns the string s with leading and trailing whitespace cut off"; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_0_1; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - return new CString(args[0].val().trim(), args[0].getTarget()); - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "'->' . trim(' <- spaces -> ') . '<-'"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "trim"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "string {s} Returns the string s with leading and trailing whitespace cut off"; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_0_1; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + return new CString(args[0].val().trim(), args[0].getTarget()); + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "'->' . trim(' <- spaces -> ') . '<-'"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class trimr extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "trimr"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "string {s} Returns the string s with trailing whitespace cut off"; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - return new CString(StringUtils.trimRight(args[0].val()), args[0].getTarget()); - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "'->' . trimr(' <- spaces -> ') . '<-'"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "trimr"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "string {s} Returns the string s with trailing whitespace cut off"; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + return new CString(StringUtils.trimRight(args[0].val()), args[0].getTarget()); + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "'->' . trimr(' <- spaces -> ') . '<-'"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class triml extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "triml"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "string {s} Returns the string s with leading whitespace cut off"; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - return new CString(StringUtils.trimLeft(args[0].val()), t); - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "'->' . triml(' <- spaces -> ') . '<-'"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "triml"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "string {s} Returns the string s with leading whitespace cut off"; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + return new CString(StringUtils.trimLeft(args[0].val()), t); + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "'->' . triml(' <- spaces -> ') . '<-'"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class length extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "length"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "int {str | array} Returns the character length of str, if the value is castable to a string, or the length of the array, if an array is given"; - } - - @Override - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_1_2; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - if (args[0] instanceof Sizeable) { - return new CInt(((Sizeable) args[0]).size(), t); - } else { - return new CInt(args[0].val().length(), t); - } - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Strings", "length('this is a string')"), - new ExampleScript("Arrays", "length(array('1', 2, '3', 4))"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "length"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "int {str | array} Returns the character length of str, if the value is castable to a string, or the length of the array, if an array is given"; + } + + @Override + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_1_2; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + if (args[0] instanceof Sizeable) { + return new CInt(((Sizeable) args[0]).size(), t); + } else { + return new CInt(args[0].val().length(), t); + } + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Strings", "length('this is a string')"), + new ExampleScript("Arrays", "length(array('1', 2, '3', 4))"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class to_upper extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "to_upper"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "string {str} Returns an all caps version of str"; - } - - @Override - public Class[] thrown() { - return new Class[]{CREFormatException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_1_2; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - if (!(args[0] instanceof CString)) { - throw new CREFormatException(this.getName() + " expects a string as first argument, but type " - + args[0].typeof() + " was found.", t); - } - return new CString(args[0].val().toUpperCase(), t); - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "to_upper('uppercase')"), - new ExampleScript("", "to_upper('MiXeD cAsE')"), - new ExampleScript("", "to_upper('Numbers (and SYMBOLS: 25)')"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "to_upper"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "string {str} Returns an all caps version of str"; + } + + @Override + public Class[] thrown() { + return new Class[]{CREFormatException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_1_2; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + if (!(args[0] instanceof CString)) { + throw new CREFormatException(this.getName() + " expects a string as first argument, but type " + + args[0].typeof() + " was found.", t); + } + return new CString(args[0].val().toUpperCase(), t); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "to_upper('uppercase')"), + new ExampleScript("", "to_upper('MiXeD cAsE')"), + new ExampleScript("", "to_upper('Numbers (and SYMBOLS: 25)')"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class to_lower extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "to_lower"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "string {str} Returns an all lower case version of str"; - } - - @Override - public Class[] thrown() { - return new Class[]{CREFormatException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_1_2; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - if (!(args[0] instanceof CString)) { - throw new CREFormatException(this.getName() + " expects a string as first argument, but type " - + args[0].typeof() + " was found.", t); - } - return new CString(args[0].val().toLowerCase(), t); - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "to_lower('LOWERCASE')"), - new ExampleScript("", "to_lower('MiXeD cAsE')"), - new ExampleScript("", "to_lower('Numbers (and SYMBOLS: 25)')"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "to_lower"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "string {str} Returns an all lower case version of str"; + } + + @Override + public Class[] thrown() { + return new Class[]{CREFormatException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_1_2; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + if (!(args[0] instanceof CString)) { + throw new CREFormatException(this.getName() + " expects a string as first argument, but type " + + args[0].typeof() + " was found.", t); + } + return new CString(args[0].val().toLowerCase(), t); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "to_lower('LOWERCASE')"), + new ExampleScript("", "to_lower('MiXeD cAsE')"), + new ExampleScript("", "to_lower('Numbers (and SYMBOLS: 25)')"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api public static class substr extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "substr"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2, 3}; - } - - @Override - public String docs() { - return "string {str, begin, [end]} Returns a substring of the given string str, starting from index begin, to index end, or the" - + " end of the string, if no index is given. If either begin or end are out of bounds of the string, an exception is thrown." - + " substr('hamburger', 4, 8) returns \"urge\", substr('smiles', 1, 5) returns \"mile\", and substr('lightning', 5) returns \"ning\"." - + " See also length()."; - } - - @Override - public Class[] thrown() { - return new Class[]{CRERangeException.class, CRECastException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_1_2; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - try { - String s = args[0].val(); - int begin = Static.getInt32(args[1], t); - int end; - if (args.length == 3) { - end = Static.getInt32(args[2], t); - } else { - end = s.length(); - } - return new CString(s.substring(begin, end), t); - } catch (IndexOutOfBoundsException e) { - throw new CRERangeException("The indices given are not valid for string '" + args[0].val() + "'", t); - } - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "substr('hamburger', 4, 8)"), - new ExampleScript("", "substr('smiles', 1, 5)"), - new ExampleScript("", "substr('lightning', 5)"), - new ExampleScript("If the indexes are too large", "assign(@big, 25)\nsubstr('small', @big)"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "substr"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2, 3}; + } + + @Override + public String docs() { + return "string {str, begin, [end]} Returns a substring of the given string str, starting from index begin, to index end, or the" + + " end of the string, if no index is given. If either begin or end are out of bounds of the string, an exception is thrown." + + " substr('hamburger', 4, 8) returns \"urge\", substr('smiles', 1, 5) returns \"mile\", and substr('lightning', 5) returns \"ning\"." + + " See also length()."; + } + + @Override + public Class[] thrown() { + return new Class[]{CRERangeException.class, CRECastException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_1_2; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + try { + String s = args[0].val(); + int begin = Static.getInt32(args[1], t); + int end; + if (args.length == 3) { + end = Static.getInt32(args[2], t); + } else { + end = s.length(); + } + return new CString(s.substring(begin, end), t); + } catch (IndexOutOfBoundsException e) { + throw new CRERangeException("The indices given are not valid for string '" + args[0].val() + "'", t); + } + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "substr('hamburger', 4, 8)"), + new ExampleScript("", "substr('smiles', 1, 5)"), + new ExampleScript("", "substr('lightning', 5)"), + new ExampleScript("If the indexes are too large", "assign(@big, 25)\nsubstr('small', @big)"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api @seealso({StringHandling.string_ends_with.class}) - public static class string_starts_with extends AbstractFunction { - - @Override - public Class[] thrown() { - return new Class[]{CRENullPointerException.class}; - } - - @Override - public String getName() { - return "string_starts_with"; - } - - @Override - public String docs() { - return "boolean {teststring, keyword} Determines if the provided teststring starts with the provided keyword. This could be used to find" - + " the prefix of a line, for instance."; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - String teststring = args[0].nval(); - String keyword = args[1].nval(); - Static.AssertNonCNull(t, args); - boolean ret = teststring.startsWith(keyword); - - return CBoolean.get(ret); - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2}; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_2; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } + public static class string_starts_with extends AbstractFunction implements Optimizable { + + @Override + public Class[] thrown() { + return new Class[]{CRENullPointerException.class}; + } + + @Override + public String getName() { + return "string_starts_with"; + } + + @Override + public String docs() { + return "boolean {teststring, keyword} Determines if the provided teststring starts with the provided keyword. This could be used to find" + + " the prefix of a line, for instance. Note that this will cast both arguments to strings. This means that the boolean" + + " true will match the string 'true' or the integer 1 will match the string '1'. If an empty string is provided for" + + " the keyword, it will always return true."; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + Static.AssertNonCNull(t, args); + + String teststring = args[0].nval(); + String keyword = args[1].nval(); + boolean ret = teststring.startsWith(keyword); + + return CBoolean.get(ret); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "string_starts_with('[ERROR] Bad message here!', '[ERROR]')"), + new ExampleScript("Basic usage", "string_starts_with('Potato with cheese', 'cheese')")}; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_2; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN, + OptimizationOption.NO_SIDE_EFFECTS); + } } @api @seealso({StringHandling.string_starts_with.class}) - public static class string_ends_with extends AbstractFunction { - - @Override - public Class[] thrown() { - return new Class[]{CRENullPointerException.class}; - } - - @Override - public String getName() { - return "string_ends_with"; - } - - @Override - public String docs() { - return "boolean {teststring, keyword} Determines if the provided teststring ends with the provided keyword."; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - String teststring = args[0].nval(); - String keyword = args[1].nval(); - Static.AssertNonCNull(t, args); - boolean ret = teststring.endsWith(keyword); - - return CBoolean.get(ret); - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2}; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_2; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } + public static class string_ends_with extends AbstractFunction implements Optimizable { + + @Override + public Class[] thrown() { + return new Class[]{CRENullPointerException.class}; + } + + @Override + public String getName() { + return "string_ends_with"; + } + + @Override + public String docs() { + return "boolean {teststring, keyword} Determines if the provided teststring ends with the provided keyword. Note that this will" + + " cast both arguments to strings. This means that the boolean true will match the string 'true' or the integer 1 will" + + " match the string '1'. If an empty string is provided for the keyword, it will always return true."; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + Static.AssertNonCNull(t, args); + + String teststring = args[0].nval(); + String keyword = args[1].nval(); + boolean ret = teststring.endsWith(keyword); + + return CBoolean.get(ret); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "string_ends_with('[ERROR] Bad message here!!', '!')"), + new ExampleScript("Basic usage", "string_ends_with('Spaghetti and cheese', 'Spaghetti')")}; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_2; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN, + OptimizationOption.NO_SIDE_EFFECTS); + } } @api public static class char_is_uppercase extends AbstractFunction implements Optimizable { - @Override - public Class[] thrown() { - return new Class[]{CREFormatException.class, CRECastException.class}; - } - - @Override - public String getName() { - return "char_is_uppercase"; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws CancelCommandException, ConfigRuntimeException { - if (!(args[0].nval() instanceof String)) { - throw new CRECastException(this.getName() + " expects a string as first argument, but type " - + args[0].typeof() + " was found.", t); - } - String text = args[0].nval(); - // Enforce the fact we are only taking the first character here - // Do not let the user pass an entire string (or an empty string, d'oh). Only a single character. - if (text.length() != 1) { - throw new CREFormatException("Got \"" + text + "\" instead of expected character.", t); - } - - char check = text.charAt(0); - - if (!Character.isLetter(check)) { - throw new CREFormatException("Got \"" + text + "\" instead of alphabetical character.", t); - } - - return CBoolean.get(Character.isUpperCase(check)); - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "boolean {char} Determines if the character provided is uppercase or not. The string must be exactly 1 character long and" - + " a letter, otherwise a FormatException is thrown."; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_2; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "char_is_uppercase('a')"), - new ExampleScript("Basic usage", "char_is_uppercase('D')"),}; - } + @Override + public Class[] thrown() { + return new Class[]{CREFormatException.class, CRECastException.class}; + } + + @Override + public String getName() { + return "char_is_uppercase"; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws CancelCommandException, ConfigRuntimeException { + if (!(args[0].nval() instanceof String)) { + throw new CRECastException(this.getName() + " expects a string as first argument, but type " + + args[0].typeof() + " was found.", t); + } + String text = args[0].nval(); + // Enforce the fact we are only taking the first character here + // Do not let the user pass an entire string (or an empty string, d'oh). Only a single character. + if (text.length() != 1) { + throw new CREFormatException("Got \"" + text + "\" instead of expected character.", t); + } + + char check = text.charAt(0); + + if (!Character.isLetter(check)) { + throw new CREFormatException("Got \"" + text + "\" instead of alphabetical character.", t); + } + + return CBoolean.get(Character.isUpperCase(check)); + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN, + OptimizationOption.NO_SIDE_EFFECTS); + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "boolean {char} Determines if the character provided is uppercase or not. The string must be exactly 1 character long and" + + " a letter, otherwise a FormatException is thrown."; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_2; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "char_is_uppercase('a')"), + new ExampleScript("Basic usage", "char_is_uppercase('D')"),}; + } } @api public static class string_position extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "string_position"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2}; - } - - @Override - public String docs() { - return "int {haystack, needle} Finds the numeric position of the first occurence of needle in haystack. haystack is the string" - + " to search in, and needle is the string to search with. Returns the position of the needle (starting with 0) or -1 if" - + " the string is not found at all."; - } - - @Override - public Class[] thrown() { - return new Class[]{CRENullPointerException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - String haystack = args[0].nval(); - String needle = args[1].nval(); - Static.AssertNonCNull(t, args); - return new CInt(haystack.indexOf(needle), t); - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "string_position('this is the string', 'string')"), - new ExampleScript("String not found", "string_position('Where\\'s Waldo?', 'Dunno.')"),}; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CONSTANT_OFFLINE, - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "string_position"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "int {haystack, needle} Finds the numeric position of the first occurence of needle in haystack. haystack is the string" + + " to search in, and needle is the string to search with. Returns the position of the needle (starting with 0) or -1 if" + + " the string is not found at all."; + } + + @Override + public Class[] thrown() { + return new Class[]{CRENullPointerException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + String haystack = args[0].nval(); + String needle = args[1].nval(); + Static.AssertNonCNull(t, args); + return new CInt(haystack.indexOf(needle), t); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "string_position('this is the string', 'string')"), + new ExampleScript("String not found", "string_position('Where\\'s Waldo?', 'Dunno.')"),}; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CONSTANT_OFFLINE, + OptimizationOption.CACHE_RETURN); + } } @api @seealso({ArrayHandling.array_implode.class}) public static class split extends AbstractFunction implements Optimizable { - @Override - public String getName() { - return "split"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2, 3}; - } - - @Override - public String docs() { - return "array {split, string, [limit]} Splits a string into parts, using the split as the index. Though it can be used in every single case" - + " you would use reg_split, this does not use regex," - + " and therefore can take a literal split expression instead of needing an escaped regex, and *may* perform better than the" - + " regex versions, as it uses an optimized tokenizer split, instead of Java regex. Limit defaults to infinity, but if set, only" - + " that number of splits will occur."; - } - - @Override - public Class[] thrown() { - return new Class[]{CRECastException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { - //http://stackoverflow.com/questions/2667015/is-regex-too-slow-real-life-examples-where-simple-non-regex-alternative-is-bett - //According to this, regex isn't necessarily slower, but we do want to escape the pattern either way, since the main advantage - //of this function is convenience (not speed) however, if we can eek out a little extra speed too, excellent. - CArray array = new CArray(t); - String split = args[0].val(); - String string = args[1].val(); - int limit = Integer.MAX_VALUE; - if (args.length >= 3) { - limit = Static.getInt32(args[2], t); - } - int sp = 0; - if (split.length() == 0) { - //Empty string, so special case. - for (int i = 0; i < string.length(); i++) { - array.push(new CString(string.charAt(i), t), t); - } - return array; - } - int splitsFound = 0; - for (int i = 0; i < string.length() - split.length() && splitsFound < limit; i++) { - if (string.substring(i, i + split.length()).equals(split)) { - //Split point found - splitsFound++; - array.push(new CString(string.substring(sp, i), t), t); - sp = i + split.length(); - i += split.length() - 1; - } - } - if (sp != 0) { - array.push(new CString(string.substring(sp, string.length()), t), t); - } else { - //It was not found anywhere, so put the whole string in - array.push(args[1], t); - } - return array; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Simple split on one character. Note that unlike reg_split, no escaping is needed on the period.", "split('.', '1.2.3.4.5')"), - new ExampleScript("Split with multiple characters", "split('ab', 'aaabaaabaaabaa')"), - new ExampleScript("Split all characters", "split('', 'abcdefg')"), - new ExampleScript("Split with limit", "split('|', 'this|is|a|limit', 1)") - }; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of( - OptimizationOption.CACHE_RETURN); - } + @Override + public String getName() { + return "split"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2, 3}; + } + + @Override + public String docs() { + return "array {split, string, [limit]} Splits a string into parts, using the split as the index. Though it can be used in every single case" + + " you would use reg_split, this does not use regex," + + " and therefore can take a literal split expression instead of needing an escaped regex, and *may* perform better than the" + + " regex versions, as it uses an optimized tokenizer split, instead of Java regex. Limit defaults to infinity, but if set, only" + + " that number of splits will occur."; + } + + @Override + public Class[] thrown() { + return new Class[]{CRECastException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { + //http://stackoverflow.com/questions/2667015/is-regex-too-slow-real-life-examples-where-simple-non-regex-alternative-is-bett + //According to this, regex isn't necessarily slower, but we do want to escape the pattern either way, since the main advantage + //of this function is convenience (not speed) however, if we can eek out a little extra speed too, excellent. + CArray array = new CArray(t); + String split = args[0].val(); + String string = args[1].val(); + int limit = Integer.MAX_VALUE; + if (args.length >= 3) { + limit = Static.getInt32(args[2], t); + } + int sp = 0; + if (split.length() == 0) { + //Empty string, so special case. + for (int i = 0; i < string.length(); i++) { + array.push(new CString(string.charAt(i), t), t); + } + return array; + } + int splitsFound = 0; + for (int i = 0; i < string.length() - split.length() && splitsFound < limit; i++) { + if (string.substring(i, i + split.length()).equals(split)) { + //Split point found + splitsFound++; + array.push(new CString(string.substring(sp, i), t), t); + sp = i + split.length(); + i += split.length() - 1; + } + } + if (sp != 0) { + array.push(new CString(string.substring(sp, string.length()), t), t); + } else { + //It was not found anywhere, so put the whole string in + array.push(args[1], t); + } + return array; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Simple split on one character. Note that unlike reg_split, no escaping is needed on the period.", "split('.', '1.2.3.4.5')"), + new ExampleScript("Split with multiple characters", "split('ab', 'aaabaaabaaabaa')"), + new ExampleScript("Split all characters", "split('', 'abcdefg')"), + new ExampleScript("Split with limit", "split('|', 'this|is|a|limit', 1)") + }; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of( + OptimizationOption.CACHE_RETURN); + } } @api @seealso({sprintf.class, Meta.get_locales.class}) public static class lsprintf extends AbstractFunction implements Optimizable { - @Override - public Class[] thrown() { - return new Class[]{CREFormatException.class, - CREInsufficientArgumentsException.class, - CRECastException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - if (args.length < 2) { - throw new CREInsufficientArgumentsException(getName() + " expects 2 or more arguments", t); - } - int numArgs = args.length; - - // Get the Locale. - Locale locale = null; - String countryCode = args[0].nval(); - if (countryCode == null) { - locale = Locale.getDefault(); - } else { - locale = Static.GetLocale(countryCode); - } - if (locale == null) { - throw new CREFormatException("The given locale was not found on your system: " - + countryCode, t); - } - - // Handle the formatting. - String formatString = args[1].val(); - Object[] params = new Object[numArgs - 2]; - List parsed; - try { - parsed = parse(formatString, t); - } catch (IllegalFormatException e) { - throw new CREFormatException(e.getMessage(), t); - } - if (requiredArgs(parsed) != numArgs - 2) { - throw new CREInsufficientArgumentsException("The specified format string: \"" + formatString + "\"" - + " expects " + requiredArgs(parsed) + " argument(s), but " + (numArgs - 2) + " were provided.", t); - } - - List flattenedArgs = new ArrayList(); - if (numArgs == 3 && args[2] instanceof CArray) { - if (((CArray) args[2]).inAssociativeMode()) { - throw new CRECastException("If the second argument to " + getName() + " is an array, it may not be associative.", t); - } else { - for (int i = 0; i < ((CArray) args[2]).size(); i++) { - flattenedArgs.add(((CArray) args[2]).get(i, t)); - } - } - } else { - for (int i = 2; i < numArgs; i++) { - flattenedArgs.add(args[i]); - } - } - //Now figure out how to cast things, now that we know our argument numbers will match up - for (int i = 0; i < requiredArgs(parsed); i++) { - Construct arg = flattenedArgs.get(i); - FormatString fs = parsed.get(i); - Character c = fs.getExpectedType(); - params[i] = convertArgument(arg, c, i, t); - } - //Ok, done. - return new CString(String.format(locale, formatString, params), t); - } - - private Object convertArgument(Construct arg, Character c, int i, Target t) { - Object o; - if (Conversion.isValid(c)) { - if (c == 't' || c == 'T') { - //Datetime, parse as long - o = Static.getInt(arg, t); - } else if (Conversion.isCharacter(c)) { - //Character, parse as string, and verify it's of length 1 - String s = arg.val(); - if (s.length() > 1) { - throw new CREFormatException("Expecting a string of length one in argument " + (i + 1) + " in " + getName() - + "but \"" + s + "\" was found instead.", t); - } - o = s.charAt(0); - } else if (Conversion.isFloat(c)) { - //Float, parse as double - o = Static.getDouble(arg, t); - } else if (Conversion.isInteger(c)) { - //Integer, parse as long - o = Static.getInt(arg, t); - } else //Further processing is needed - { - if (c == Conversion.BOOLEAN || c == Conversion.BOOLEAN_UPPER) { - //Boolean, parse as such - o = Static.getBoolean(arg); - } else { - //Else it's either a string or a hash code, in which case - //we will treat it as a string anyways - o = arg.val(); - } - } - } else { - //Hmm, shouldn't have been able to get here, but whatever. - throw ConfigRuntimeException.CreateUncatchableException("Conversion is invalid: " + c, t); - } - return o; - } - - private static class Conversion { - // Byte, Short, Integer, Long, BigInteger - // (and associated primitives due to autoboxing) - - static final char DECIMAL_INTEGER = 'd'; - static final char OCTAL_INTEGER = 'o'; - static final char HEXADECIMAL_INTEGER = 'x'; - static final char HEXADECIMAL_INTEGER_UPPER = 'X'; - // Float, Double, BigDecimal - // (and associated primitives due to autoboxing) - static final char SCIENTIFIC = 'e'; - static final char SCIENTIFIC_UPPER = 'E'; - static final char GENERAL = 'g'; - static final char GENERAL_UPPER = 'G'; - static final char DECIMAL_FLOAT = 'f'; - static final char HEXADECIMAL_FLOAT = 'a'; - static final char HEXADECIMAL_FLOAT_UPPER = 'A'; - // Character, Byte, Short, Integer - // (and associated primitives due to autoboxing) - static final char CHARACTER = 'c'; - static final char CHARACTER_UPPER = 'C'; - // java.util.Date, java.util.Calendar, long - static final char DATE_TIME = 't'; - static final char DATE_TIME_UPPER = 'T'; - // if (arg.TYPE != boolean) return boolean - // if (arg != null) return true; else return false; - static final char BOOLEAN = 'b'; - static final char BOOLEAN_UPPER = 'B'; - // if (arg instanceof Formattable) arg.formatTo() - // else arg.toString(); - static final char STRING = 's'; - static final char STRING_UPPER = 'S'; - // arg.hashCode() - static final char HASHCODE = 'h'; - static final char HASHCODE_UPPER = 'H'; - static final char LINE_SEPARATOR = 'n'; - static final char PERCENT_SIGN = '%'; - - static boolean isValid(char c) { - return (isGeneral(c) || isInteger(c) || isFloat(c) || isText(c) - || c == 't' || isCharacter(c)); - } - - // Returns true iff the Conversion is applicable to all objects. - static boolean isGeneral(char c) { - switch (c) { - case BOOLEAN: - case BOOLEAN_UPPER: - case STRING: - case STRING_UPPER: - case HASHCODE: - case HASHCODE_UPPER: - return true; - default: - return false; - } - } - - // Returns true iff the Conversion is applicable to character. - static boolean isCharacter(char c) { - switch (c) { - case CHARACTER: - case CHARACTER_UPPER: - return true; - default: - return false; - } - } - - // Returns true iff the Conversion is an integer type. - static boolean isInteger(char c) { - switch (c) { - case DECIMAL_INTEGER: - case OCTAL_INTEGER: - case HEXADECIMAL_INTEGER: - case HEXADECIMAL_INTEGER_UPPER: - return true; - default: - return false; - } - } - - // Returns true iff the Conversion is a floating-point type. - static boolean isFloat(char c) { - switch (c) { - case SCIENTIFIC: - case SCIENTIFIC_UPPER: - case GENERAL: - case GENERAL_UPPER: - case DECIMAL_FLOAT: - case HEXADECIMAL_FLOAT: - case HEXADECIMAL_FLOAT_UPPER: - return true; - default: - return false; - } - } - - // Returns true iff the Conversion does not require an argument - static boolean isText(char c) { - switch (c) { - case LINE_SEPARATOR: - case PERCENT_SIGN: - return true; - default: - return false; - } - } - } - - @Override - public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { - if (children.size() < 2) { - throw new ConfigCompileException(getName() + " expects 2 or more argument", t); - } - if (children.get(0).isConst()) { - String locale = children.get(0).getData().nval(); - if (locale != null && Static.GetLocale(locale) == null) { - throw new ConfigCompileException("The locale " + locale + " could not be found on this system", t); - } - } - if (children.get(1).isConst()) { - ParseTree me = new ParseTree(new CFunction(getName(), t), children.get(1).getFileOptions()); - me.setChildren(children); - me.setOptimized(true); //After this run, we will be, anyways. - if (children.size() == 3 && children.get(2).getData() instanceof CFunction && ((CFunction) children.get(2).getData()).getFunction().getName().equals(new DataHandling.array().getName())) { - //Normally we can't do anything with a hardcoded array, it's considered dynamic. But in this case, we can at least pull up the arguments, - //because the array's size is constant, even if the arguments in it aren't. - ParseTree array = children.get(2); - children.remove(2); - boolean allIndexesStatic = true; - for (int i = 0; i < array.numberOfChildren(); i++) { - ParseTree child = array.getChildAt(i); - if (child.isDynamic()) { - allIndexesStatic = false; - } - children.add(child); - } - if (allIndexesStatic) { - me.hasBeenMadeStatic(true); - } - } - //We can check the format string and make sure it doesn't throw an IllegalFormatException. - try { - List parsed = parse(children.get(1).getData().val(), t); - if (requiredArgs(parsed) != children.size() - 2) { - throw new CREInsufficientArgumentsException("The specified format string: \"" + children.get(1).getData().val() + "\"" - + " expects " + requiredArgs(parsed) + " argument(s), but " + (children.size() - 2) + " were provided.", t); - } - //If the arguments are constant, we can actually check them too - for (int i = 2; i < children.size(); i++) { - //We skip the dynamic ones, but the constant ones we can know for sure - //if they are convertable or not. - if (children.get(i).isConst()) { - convertArgument(children.get(i).getData(), parsed.get(i - 2).getExpectedType(), i, t); - } - } - } catch (IllegalFormatException e) { - throw new CREFormatException(e.getMessage(), t); - } - return me; - } else { - return null; - } - } - - private static class FormatString { - - private Object ref; - private static final Class FormatString; - private static final Class FixedString; - private static final Class FormatSpecifier; - - static { - Class tFormatString = null; - Class tFixedString = null; - Class tFormatSpecifier = null; - for (Class c : Formatter.class.getDeclaredClasses()) { - if (c.getSimpleName().equals("FormatString")) { - tFormatString = c; - continue; - } - if (c.getSimpleName().equals("FixedString")) { - tFixedString = c; - continue; - } - if (c.getSimpleName().equals("FormatSpecifier")) { - tFormatSpecifier = c; - continue; - } - } - FormatString = tFormatString; - FixedString = tFixedString; - FormatSpecifier = tFormatSpecifier; - } - - public FormatString(Object ref) { - if (ref == null) { - throw new NullPointerException(); - } - if (!FormatString.isAssignableFrom(ref.getClass())) { - throw new RuntimeException("Unexpected class type. Was expecting ref to be an instance of " + FormatString.getName() + " but was " + ref.getClass().getName()); - } - this.ref = ref; - } - - public Character getExpectedType() { - if (ref.getClass() == FixedString) { - return null; - } else if (ref.getClass() == FormatSpecifier) { - if (((Boolean) ReflectionUtils.get(FormatSpecifier, ref, "dt"))) { - return 't'; - } - return ((Character) ReflectionUtils.get(FormatSpecifier, ref, "c")); - } else { - throw new RuntimeException("Unknown type: " + ref.getClass()); - } - } - - public int getArgIndex() { - return ((Integer) ReflectionUtils.get(FormatSpecifier, ref, "index")); - } - - public boolean isFixed() { - return getExpectedType() == '%' || getExpectedType() == 'n'; - } - } - - private List parse(String format, Target t) { - List list = new ArrayList(); - Object parse; - try { - parse = ReflectionUtils.invokeMethod(Formatter.class, new Formatter(), "parse", new Class[]{String.class}, new Object[]{format}); - } catch (ReflectionException e) { - if (e.getCause() instanceof InvocationTargetException) { - Throwable th = e.getCause().getCause(); - throw new CREFormatException("A format exception was thrown for the argument \"" + format + "\": " + th.getClass().getSimpleName() + ": " + th.getMessage(), t); - } else { - //This is unexpected - throw ConfigRuntimeException.CreateUncatchableException(e.getMessage(), t); - } - } - int length = Array.getLength(parse); - for (int i = 0; i < length; i++) { - FormatString s = new FormatString(Array.get(parse, i)); - if (s.getExpectedType() != null) { - list.add(s); - } - } - return list; - } - - private int requiredArgs(List list) { - Set knownIndexes = new HashSet(); - int count = 0; - for (FormatString s : list) { - if (s.isFixed()) { - continue; - } - int index = s.getArgIndex(); - if (index == 0) { - count++; - } else { - knownIndexes.add(index); - } - } - count += knownIndexes.size(); - return count; - } - - @Override - public String getName() { - return "lsprintf"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{Integer.MAX_VALUE}; - } - - @Override - public String docs() { - return "string {locale, formatString, parameters... | locale, formatString, array(parameters...)} Returns a string formatted to the" - + " given formatString specification, using the parameters passed in. Locale should be a string in format," - + " for instance, en_US, nl_NL, no_NO... Which locales are available depends on your system. Use" - + " null to use the system's locale." - + " The formatString should be formatted according to" - + " [http://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html#syntax this standard]," - + " with the caveat that the parameter types are automatically cast to the appropriate type, if possible." - + " Calendar/time specifiers, (t and T) expect an integer which represents unix time, but are otherwise" - + " valid. All format specifiers in the documentation are valid."; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of(OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.OPTIMIZE_DYNAMIC); - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "lsprintf('en_US', '%d', 1)"), - new ExampleScript("Multiple arguments", "lsprintf('en_US', '%d%d', 1, '2')"), - new ExampleScript("Multiple arguments in an array", "lsprintf('en_US', '%d%d', array(1, 2))"), - new ExampleScript("Compile error, missing parameters", "lsprintf('en_US', '%d')", true), - new ExampleScript("Other formatting: float with precision (using integer)", "lsprintf('en_US', '%07.3f', 4)"), - new ExampleScript("Other formatting: float with precision (with rounding)", "lsprintf('en_US', '%07.3f', 3.4567)"), - new ExampleScript("Other formatting: float with precision in a different locale (with rounding)", "lsprintf('nl_NL', '%07.3f', 3.4567)"), - new ExampleScript("Other formatting: time", "lsprintf('en_US', '%1$tm %1$te,%1$tY', time())", ":06 13,2013"), - new ExampleScript("Literal percent sign", "lsprintf('en_US', '%'.'%') // The concatenation is to prevent the website's template system from overriding. It is not needed.", "%"), - new ExampleScript("Hexidecimal formatting", "lsprintf('en_US', '%x', 15)"), - new ExampleScript("Other formatting: character", "lsprintf('en_US', '%c', 's')"), - new ExampleScript("Other formatting: character (with capitalization)", "lsprintf('en_US', '%C', 's')"), - new ExampleScript("Other formatting: scientific notation", "lsprintf('en_US', '%e', '2345')"), - new ExampleScript("Other formatting: plain string", "lsprintf('en_US', '%s', 'plain string')"), - new ExampleScript("Other formatting: boolean", "lsprintf('en_US', '%b', 1)"), - new ExampleScript("Other formatting: boolean (with capitalization)", "lsprintf('en_US', '%B', 0)"), - new ExampleScript("Other formatting: hash code", "lsprintf('en_US', '%h', 'will be hashed')"),}; - } + @Override + public Class[] thrown() { + return new Class[]{CREFormatException.class, + CREInsufficientArgumentsException.class, + CRECastException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + if (args.length < 2) { + throw new CREInsufficientArgumentsException(getName() + " expects 2 or more arguments", t); + } + int numArgs = args.length; + + // Get the Locale. + Locale locale = null; + String countryCode = args[0].nval(); + if (countryCode == null) { + locale = Locale.getDefault(); + } else { + locale = Static.GetLocale(countryCode); + } + if (locale == null) { + throw new CREFormatException("The given locale was not found on your system: " + + countryCode, t); + } + + // Handle the formatting. + String formatString = args[1].val(); + Object[] params = new Object[numArgs - 2]; + List parsed; + try { + parsed = parse(formatString, t); + } catch (IllegalFormatException e) { + throw new CREFormatException(e.getMessage(), t); + } + if (requiredArgs(parsed) != numArgs - 2) { + throw new CREInsufficientArgumentsException("The specified format string: \"" + formatString + "\"" + + " expects " + requiredArgs(parsed) + " argument(s), but " + (numArgs - 2) + " were provided.", t); + } + + List flattenedArgs = new ArrayList(); + if (numArgs == 3 && args[2] instanceof CArray) { + if (((CArray) args[2]).inAssociativeMode()) { + throw new CRECastException("If the second argument to " + getName() + " is an array, it may not be associative.", t); + } else { + for (int i = 0; i < ((CArray) args[2]).size(); i++) { + flattenedArgs.add(((CArray) args[2]).get(i, t)); + } + } + } else { + for (int i = 2; i < numArgs; i++) { + flattenedArgs.add(args[i]); + } + } + //Now figure out how to cast things, now that we know our argument numbers will match up + for (int i = 0; i < requiredArgs(parsed); i++) { + Construct arg = flattenedArgs.get(i); + FormatString fs = parsed.get(i); + Character c = fs.getExpectedType(); + params[i] = convertArgument(arg, c, i, t); + } + //Ok, done. + return new CString(String.format(locale, formatString, params), t); + } + + private Object convertArgument(Construct arg, Character c, int i, Target t) { + Object o; + if (Conversion.isValid(c)) { + if (c == 't' || c == 'T') { + //Datetime, parse as long + o = Static.getInt(arg, t); + } else if (Conversion.isCharacter(c)) { + //Character, parse as string, and verify it's of length 1 + String s = arg.val(); + if (s.length() > 1) { + throw new CREFormatException("Expecting a string of length one in argument " + (i + 1) + " in " + getName() + + "but \"" + s + "\" was found instead.", t); + } + o = s.charAt(0); + } else if (Conversion.isFloat(c)) { + //Float, parse as double + o = Static.getDouble(arg, t); + } else if (Conversion.isInteger(c)) { + //Integer, parse as long + o = Static.getInt(arg, t); + } else { + //Further processing is needed + if (c == Conversion.BOOLEAN || c == Conversion.BOOLEAN_UPPER) { + //Boolean, parse as such + o = Static.getBoolean(arg); + } else { + //Else it's either a string or a hash code, in which case + //we will treat it as a string anyways + o = arg.val(); + } + } + } else { + //Hmm, shouldn't have been able to get here, but whatever. + throw ConfigRuntimeException.CreateUncatchableException("Conversion is invalid: " + c, t); + } + return o; + } + + private static class Conversion { + // Byte, Short, Integer, Long, BigInteger + // (and associated primitives due to autoboxing) + + static final char DECIMAL_INTEGER = 'd'; + static final char OCTAL_INTEGER = 'o'; + static final char HEXADECIMAL_INTEGER = 'x'; + static final char HEXADECIMAL_INTEGER_UPPER = 'X'; + // Float, Double, BigDecimal + // (and associated primitives due to autoboxing) + static final char SCIENTIFIC = 'e'; + static final char SCIENTIFIC_UPPER = 'E'; + static final char GENERAL = 'g'; + static final char GENERAL_UPPER = 'G'; + static final char DECIMAL_FLOAT = 'f'; + static final char HEXADECIMAL_FLOAT = 'a'; + static final char HEXADECIMAL_FLOAT_UPPER = 'A'; + // Character, Byte, Short, Integer + // (and associated primitives due to autoboxing) + static final char CHARACTER = 'c'; + static final char CHARACTER_UPPER = 'C'; + // java.util.Date, java.util.Calendar, long + static final char DATE_TIME = 't'; + static final char DATE_TIME_UPPER = 'T'; + // if (arg.TYPE != boolean) return boolean + // if (arg != null) return true; else return false; + static final char BOOLEAN = 'b'; + static final char BOOLEAN_UPPER = 'B'; + // if (arg instanceof Formattable) arg.formatTo() + // else arg.toString(); + static final char STRING = 's'; + static final char STRING_UPPER = 'S'; + // arg.hashCode() + static final char HASHCODE = 'h'; + static final char HASHCODE_UPPER = 'H'; + static final char LINE_SEPARATOR = 'n'; + static final char PERCENT_SIGN = '%'; + + static boolean isValid(char c) { + return (isGeneral(c) || isInteger(c) || isFloat(c) || isText(c) + || c == 't' || isCharacter(c)); + } + + // Returns true iff the Conversion is applicable to all objects. + static boolean isGeneral(char c) { + switch (c) { + case BOOLEAN: + case BOOLEAN_UPPER: + case STRING: + case STRING_UPPER: + case HASHCODE: + case HASHCODE_UPPER: + return true; + default: + return false; + } + } + + // Returns true iff the Conversion is applicable to character. + static boolean isCharacter(char c) { + switch (c) { + case CHARACTER: + case CHARACTER_UPPER: + return true; + default: + return false; + } + } + + // Returns true iff the Conversion is an integer type. + static boolean isInteger(char c) { + switch (c) { + case DECIMAL_INTEGER: + case OCTAL_INTEGER: + case HEXADECIMAL_INTEGER: + case HEXADECIMAL_INTEGER_UPPER: + return true; + default: + return false; + } + } + + // Returns true iff the Conversion is a floating-point type. + static boolean isFloat(char c) { + switch (c) { + case SCIENTIFIC: + case SCIENTIFIC_UPPER: + case GENERAL: + case GENERAL_UPPER: + case DECIMAL_FLOAT: + case HEXADECIMAL_FLOAT: + case HEXADECIMAL_FLOAT_UPPER: + return true; + default: + return false; + } + } + + // Returns true iff the Conversion does not require an argument + static boolean isText(char c) { + switch (c) { + case LINE_SEPARATOR: + case PERCENT_SIGN: + return true; + default: + return false; + } + } + } + + @Override + public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { + if (children.size() < 2) { + throw new ConfigCompileException(getName() + " expects 2 or more argument", t); + } + if (children.get(0).isConst()) { + String locale = children.get(0).getData().nval(); + if (locale != null && Static.GetLocale(locale) == null) { + throw new ConfigCompileException("The locale " + locale + " could not be found on this system", t); + } + } + if (children.get(1).isConst()) { + ParseTree me = new ParseTree(new CFunction(getName(), t), children.get(1).getFileOptions()); + me.setChildren(children); + me.setOptimized(true); //After this run, we will be, anyways. + if (children.size() == 3 && children.get(2).getData() instanceof CFunction && ((CFunction) children.get(2).getData()).getFunction().getName().equals(new DataHandling.array().getName())) { + //Normally we can't do anything with a hardcoded array, it's considered dynamic. But in this case, we can at least pull up the arguments, + //because the array's size is constant, even if the arguments in it aren't. + ParseTree array = children.get(2); + children.remove(2); + boolean allIndexesStatic = true; + for (int i = 0; i < array.numberOfChildren(); i++) { + ParseTree child = array.getChildAt(i); + if (child.isDynamic()) { + allIndexesStatic = false; + } + children.add(child); + } + if (allIndexesStatic) { + me.hasBeenMadeStatic(true); + } + } + //We can check the format string and make sure it doesn't throw an IllegalFormatException. + try { + List parsed = parse(children.get(1).getData().val(), t); + if (requiredArgs(parsed) != children.size() - 2) { + throw new CREInsufficientArgumentsException("The specified format string: \"" + children.get(1).getData().val() + "\"" + + " expects " + requiredArgs(parsed) + " argument(s), but " + (children.size() - 2) + " were provided.", t); + } + //If the arguments are constant, we can actually check them too + for (int i = 2; i < children.size(); i++) { + //We skip the dynamic ones, but the constant ones we can know for sure + //if they are convertable or not. + if (children.get(i).isConst()) { + convertArgument(children.get(i).getData(), parsed.get(i - 2).getExpectedType(), i, t); + } + } + } catch (IllegalFormatException e) { + throw new CREFormatException(e.getMessage(), t); + } + return me; + } else { + return null; + } + } + + private static class FormatString { + + private Object ref; + private static final Class FormatString; + private static final Class FixedString; + private static final Class FormatSpecifier; + + static { + Class tFormatString = null; + Class tFixedString = null; + Class tFormatSpecifier = null; + for (Class c : Formatter.class.getDeclaredClasses()) { + if (c.getSimpleName().equals("FormatString")) { + tFormatString = c; + continue; + } + if (c.getSimpleName().equals("FixedString")) { + tFixedString = c; + continue; + } + if (c.getSimpleName().equals("FormatSpecifier")) { + tFormatSpecifier = c; + continue; + } + } + FormatString = tFormatString; + FixedString = tFixedString; + FormatSpecifier = tFormatSpecifier; + } + + public FormatString(Object ref) { + if (ref == null) { + throw new NullPointerException(); + } + if (!FormatString.isAssignableFrom(ref.getClass())) { + throw new RuntimeException("Unexpected class type. Was expecting ref to be an instance of " + FormatString.getName() + " but was " + ref.getClass().getName()); + } + this.ref = ref; + } + + public Character getExpectedType() { + if (ref.getClass() == FixedString) { + return null; + } else if (ref.getClass() == FormatSpecifier) { + if (((Boolean) ReflectionUtils.get(FormatSpecifier, ref, "dt"))) { + return 't'; + } + return ((Character) ReflectionUtils.get(FormatSpecifier, ref, "c")); + } else { + throw new RuntimeException("Unknown type: " + ref.getClass()); + } + } + + public int getArgIndex() { + return ((Integer) ReflectionUtils.get(FormatSpecifier, ref, "index")); + } + + public boolean isFixed() { + return getExpectedType() == '%' || getExpectedType() == 'n'; + } + } + + private List parse(String format, Target t) { + List list = new ArrayList(); + Object parse; + try { + parse = ReflectionUtils.invokeMethod(Formatter.class, new Formatter(), "parse", new Class[]{String.class}, new Object[]{format}); + } catch (ReflectionException e) { + if (e.getCause() instanceof InvocationTargetException) { + Throwable th = e.getCause().getCause(); + throw new CREFormatException("A format exception was thrown for the argument \"" + format + "\": " + th.getClass().getSimpleName() + ": " + th.getMessage(), t); + } else { + //This is unexpected + throw ConfigRuntimeException.CreateUncatchableException(e.getMessage(), t); + } + } + int length = Array.getLength(parse); + for (int i = 0; i < length; i++) { + FormatString s = new FormatString(Array.get(parse, i)); + if (s.getExpectedType() != null) { + list.add(s); + } + } + return list; + } + + private int requiredArgs(List list) { + Set knownIndexes = new HashSet(); + int count = 0; + for (FormatString s : list) { + if (s.isFixed()) { + continue; + } + int index = s.getArgIndex(); + if (index == 0) { + count++; + } else { + knownIndexes.add(index); + } + } + count += knownIndexes.size(); + return count; + } + + @Override + public String getName() { + return "lsprintf"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{Integer.MAX_VALUE}; + } + + @Override + public String docs() { + return "string {locale, formatString, parameters... | locale, formatString, array(parameters...)} Returns a string formatted to the" + + " given formatString specification, using the parameters passed in. Locale should be a string in format," + + " for instance, en_US, nl_NL, no_NO... Which locales are available depends on your system. Use" + + " null to use the system's locale." + + " The formatString should be formatted according to" + + " [http://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html#syntax this standard]," + + " with the caveat that the parameter types are automatically cast to the appropriate type, if possible." + + " Calendar/time specifiers, (t and T) expect an integer which represents unix time, but are otherwise" + + " valid. All format specifiers in the documentation are valid."; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of(OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.OPTIMIZE_DYNAMIC); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "lsprintf('en_US', '%d', 1)"), + new ExampleScript("Multiple arguments", "lsprintf('en_US', '%d%d', 1, '2')"), + new ExampleScript("Multiple arguments in an array", "lsprintf('en_US', '%d%d', array(1, 2))"), + new ExampleScript("Compile error, missing parameters", "lsprintf('en_US', '%d')", true), + new ExampleScript("Other formatting: float with precision (using integer)", "lsprintf('en_US', '%07.3f', 4)"), + new ExampleScript("Other formatting: float with precision (with rounding)", "lsprintf('en_US', '%07.3f', 3.4567)"), + new ExampleScript("Other formatting: float with precision in a different locale (with rounding)", "lsprintf('nl_NL', '%07.3f', 3.4567)"), + new ExampleScript("Other formatting: time", "lsprintf('en_US', '%1$tm %1$te,%1$tY', time())", ":06 13,2013"), + new ExampleScript("Literal percent sign", "lsprintf('en_US', '%'.'%') // The concatenation is to prevent the website's template system from overriding. It is not needed.", "%"), + new ExampleScript("Hexidecimal formatting", "lsprintf('en_US', '%x', 15)"), + new ExampleScript("Other formatting: character", "lsprintf('en_US', '%c', 's')"), + new ExampleScript("Other formatting: character (with capitalization)", "lsprintf('en_US', '%C', 's')"), + new ExampleScript("Other formatting: scientific notation", "lsprintf('en_US', '%e', '2345')"), + new ExampleScript("Other formatting: plain string", "lsprintf('en_US', '%s', 'plain string')"), + new ExampleScript("Other formatting: boolean", "lsprintf('en_US', '%b', 1)"), + new ExampleScript("Other formatting: boolean (with capitalization)", "lsprintf('en_US', '%B', 0)"), + new ExampleScript("Other formatting: hash code", "lsprintf('en_US', '%h', 'will be hashed')"),}; + } } @@ -1635,553 +1672,553 @@ public ExampleScript[] examples() throws ConfigCompileException { @seealso(lsprintf.class) public static class sprintf extends lsprintf implements Optimizable { - @Override - public String getName() { - return "sprintf"; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } - - @Override - public String docs() { - return "string {formatString, parameters... | formatString, array(parameters...)} Returns a string formatted to the" - + " given formatString specification, using the parameters passed in. The formatString should be formatted" - + " according to [http://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html#syntax this standard]," - + " with the caveat that the parameter types are automatically cast to the appropriate type, if possible." - + " Calendar/time specifiers, (t and T) expect an integer which represents unix time, but are otherwise" - + " valid. All format specifiers in the documentation are valid. This works the same as lsprintf with the" - + " locale set to \"DEFAULT\"."; - } - - @Override - public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { - if (children.size() < 1) { - throw new ConfigCompileException(getName() + " expects at least 1 argument", t); - } - children.add(0, new ParseTree(CNull.NULL, fileOptions)); // Add a default locale to the arguments. - return super.optimizeDynamic(t, children, fileOptions); - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "sprintf('%d', 1)"), - new ExampleScript("Multiple arguments", "sprintf('%d%d', 1, '2')"), - new ExampleScript("Multiple arguments in an array", "sprintf('%d%d', array(1, 2))"), - new ExampleScript("Compile error, missing parameters", "sprintf('%d')", true), - new ExampleScript("Other formatting: float with precision (using integer)", "sprintf('%07.3f', 4)"), - new ExampleScript("Other formatting: float with precision (with rounding)", "sprintf('%07.3f', 3.4567)"), - new ExampleScript("Other formatting: time", "sprintf('%1$tm %1$te,%1$tY', time())", ":06 13,2013"), - new ExampleScript("Literal percent sign", "sprintf('%'.'%') // The concatenation is to prevent the website's template system from overriding. It is not needed.", "%"), - new ExampleScript("Hexidecimal formatting", "sprintf('%x', 15)"), - new ExampleScript("Other formatting: character", "sprintf('%c', 's')"), - new ExampleScript("Other formatting: character (with capitalization)", "sprintf('%C', 's')"), - new ExampleScript("Other formatting: scientific notation", "sprintf('%e', '2345')"), - new ExampleScript("Other formatting: plain string", "sprintf('%s', 'plain string')"), - new ExampleScript("Other formatting: boolean", "sprintf('%b', 1)"), - new ExampleScript("Other formatting: boolean (with capitalization)", "sprintf('%B', 0)"), - new ExampleScript("Other formatting: hash code", "sprintf('%h', 'will be hashed')"),}; - } + @Override + public String getName() { + return "sprintf"; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } + + @Override + public String docs() { + return "string {formatString, parameters... | formatString, array(parameters...)} Returns a string formatted to the" + + " given formatString specification, using the parameters passed in. The formatString should be formatted" + + " according to [http://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html#syntax this standard]," + + " with the caveat that the parameter types are automatically cast to the appropriate type, if possible." + + " Calendar/time specifiers, (t and T) expect an integer which represents unix time, but are otherwise" + + " valid. All format specifiers in the documentation are valid. This works the same as lsprintf with the" + + " locale set to \"DEFAULT\"."; + } + + @Override + public ParseTree optimizeDynamic(Target t, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { + if (children.size() < 1) { + throw new ConfigCompileException(getName() + " expects at least 1 argument", t); + } + children.add(0, new ParseTree(CNull.NULL, fileOptions)); // Add a default locale to the arguments. + return super.optimizeDynamic(t, children, fileOptions); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "sprintf('%d', 1)"), + new ExampleScript("Multiple arguments", "sprintf('%d%d', 1, '2')"), + new ExampleScript("Multiple arguments in an array", "sprintf('%d%d', array(1, 2))"), + new ExampleScript("Compile error, missing parameters", "sprintf('%d')", true), + new ExampleScript("Other formatting: float with precision (using integer)", "sprintf('%07.3f', 4)"), + new ExampleScript("Other formatting: float with precision (with rounding)", "sprintf('%07.3f', 3.4567)"), + new ExampleScript("Other formatting: time", "sprintf('%1$tm %1$te,%1$tY', time())", ":06 13,2013"), + new ExampleScript("Literal percent sign", "sprintf('%'.'%') // The concatenation is to prevent the website's template system from overriding. It is not needed.", "%"), + new ExampleScript("Hexidecimal formatting", "sprintf('%x', 15)"), + new ExampleScript("Other formatting: character", "sprintf('%c', 's')"), + new ExampleScript("Other formatting: character (with capitalization)", "sprintf('%C', 's')"), + new ExampleScript("Other formatting: scientific notation", "sprintf('%e', '2345')"), + new ExampleScript("Other formatting: plain string", "sprintf('%s', 'plain string')"), + new ExampleScript("Other formatting: boolean", "sprintf('%b', 1)"), + new ExampleScript("Other formatting: boolean (with capitalization)", "sprintf('%B', 0)"), + new ExampleScript("Other formatting: hash code", "sprintf('%h', 'will be hashed')"),}; + } } @api public static class string_get_bytes extends AbstractFunction { - @Override - public Class[] thrown() { - return new Class[]{CRECastException.class, CREFormatException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - String val = args[0].val(); - String encoding = "UTF-8"; - if (args.length == 2) { - encoding = args[1].val(); - } - try { - return CByteArray.wrap(val.getBytes(encoding), t); - } catch (UnsupportedEncodingException ex) { - throw new CREFormatException("Unknown encoding type \"" + encoding + "\"", t); - } - } - - @Override - public String getName() { - return "string_get_bytes"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1, 2}; - } - - @Override - public String docs() { - return "byte_array {string, [encoding]} Returns this string as a byte_array, encoded using the specified encoding," - + " or UTF-8 if no encoding is specified. Valid encodings are the encoding types that java supports. If the" - + " encoding is invalid, a FormatException is thrown."; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } + @Override + public Class[] thrown() { + return new Class[]{CRECastException.class, CREFormatException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + String val = args[0].val(); + String encoding = "UTF-8"; + if (args.length == 2) { + encoding = args[1].val(); + } + try { + return CByteArray.wrap(val.getBytes(encoding), t); + } catch (UnsupportedEncodingException ex) { + throw new CREFormatException("Unknown encoding type \"" + encoding + "\"", t); + } + } + + @Override + public String getName() { + return "string_get_bytes"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1, 2}; + } + + @Override + public String docs() { + return "byte_array {string, [encoding]} Returns this string as a byte_array, encoded using the specified encoding," + + " or UTF-8 if no encoding is specified. Valid encodings are the encoding types that java supports. If the" + + " encoding is invalid, a FormatException is thrown."; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } } @api public static class string_from_bytes extends AbstractFunction { - @Override - public Class[] thrown() { - return new Class[]{CRECastException.class, CREFormatException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - CByteArray ba = Static.getByteArray(args[0], t); - String encoding = "UTF-8"; - if (args.length == 2) { - encoding = args[1].val(); - } - try { - return new CString(new String(ba.asByteArrayCopy(), encoding), t); - } catch (UnsupportedEncodingException ex) { - throw new CREFormatException("Unknown encoding type \"" + encoding + "\"", t); - } - } - - @Override - public String getName() { - return "string_from_bytes"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1, 2}; - } - - @Override - public String docs() { - return "string {byte_array, [encoding]} Returns a new string, given the byte array encoding provided. The encoding defaults" - + " to UTF-8, but may be specified. A FormatException is thrown if the encoding type is invalid."; - } - - @Override - public CHVersion since() { - return CHVersion.V3_3_1; - } + @Override + public Class[] thrown() { + return new Class[]{CRECastException.class, CREFormatException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + CByteArray ba = Static.getByteArray(args[0], t); + String encoding = "UTF-8"; + if (args.length == 2) { + encoding = args[1].val(); + } + try { + return new CString(new String(ba.asByteArrayCopy(), encoding), t); + } catch (UnsupportedEncodingException ex) { + throw new CREFormatException("Unknown encoding type \"" + encoding + "\"", t); + } + } + + @Override + public String getName() { + return "string_from_bytes"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1, 2}; + } + + @Override + public String docs() { + return "string {byte_array, [encoding]} Returns a new string, given the byte array encoding provided. The encoding defaults" + + " to UTF-8, but may be specified. A FormatException is thrown if the encoding type is invalid."; + } + + @Override + public CHVersion since() { + return CHVersion.V3_3_1; + } } @api public static class string_append extends AbstractFunction { - @Override - public Class[] thrown() { - return new Class[]{CRECastException.class}; - } - - @Override - public boolean isRestricted() { - return true; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - if (args.length < 2) { - throw new CREInsufficientArgumentsException(getName() + " must have 2 arguments at minimum", t); - } - CResource m = (CResource) args[0]; - StringBuffer buf = ResourceManager.GetResource(m, StringBuffer.class, t); - for (int i = 1; i < args.length; i++) { - buf.append(args[i].val()); - } - return CVoid.VOID; - } - - @Override - public String getName() { - return "string_append"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{Integer.MAX_VALUE}; - } - - @Override - public String docs() { - return "void {resource, toAppend...} Appends any number of values to the underlying" - + " string builder. This is much more efficient than doing normal concatenation" - + " with a string when building a string in a loop. The underlying resource may" - + " be converted to a string via a cast, string(@res)."; - } - - @Override - public Version since() { - return CHVersion.V3_3_1; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "@res = res_create_resource('STRING_BUILDER')\n" - + "foreach(1..50, @i,\n" - + "\tstring_append(@res, @i, '.')\n" - + ")\n" - + "@string = string(@res)\n" - + "res_free_resource(@res) #This line is super important!\n" - + "msg(@string)" - + ""), - new ExampleScript("Basic usage, showing performance benefits", - "@to = 100000\n" - + "@t1 = time()\n" - + "@res = res_create_resource('STRING_BUILDER')\n" - + "foreach(range(0, @to), @i,\n" - + "\tstring_append(@res, @i, '.')\n" - + ")\n" - + "res_free_resource(@res)\n" - + "@t2 = time()\n" - + "@t3 = time()\n" - + "@str = ''\n" - + "foreach(range(0, @to), @i,\n" - + "\t@str .= @i . '.'\n" - + ")\n" - + "@t4 = time()\n" - + "msg('Task 1 took '.(@t2 - @t1).'ms under '.@to.' iterations')\n" - + "msg('Task 2 took '.(@t4 - @t3).'ms under '.@to.' iterations')", - "Task 1 took 542ms under 100000 iterations\n" - + "Task 2 took 28305ms under 100000 iterations\n") - }; - } + @Override + public Class[] thrown() { + return new Class[]{CRECastException.class}; + } + + @Override + public boolean isRestricted() { + return true; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + if (args.length < 2) { + throw new CREInsufficientArgumentsException(getName() + " must have 2 arguments at minimum", t); + } + CResource m = (CResource) args[0]; + StringBuffer buf = ResourceManager.GetResource(m, StringBuffer.class, t); + for (int i = 1; i < args.length; i++) { + buf.append(args[i].val()); + } + return CVoid.VOID; + } + + @Override + public String getName() { + return "string_append"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{Integer.MAX_VALUE}; + } + + @Override + public String docs() { + return "void {resource, toAppend...} Appends any number of values to the underlying" + + " string builder. This is much more efficient than doing normal concatenation" + + " with a string when building a string in a loop. The underlying resource may" + + " be converted to a string via a cast, string(@res)."; + } + + @Override + public Version since() { + return CHVersion.V3_3_1; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "@res = res_create_resource('STRING_BUILDER')\n" + + "foreach(1..50, @i,\n" + + "\tstring_append(@res, @i, '.')\n" + + ")\n" + + "@string = string(@res)\n" + + "res_free_resource(@res) #This line is super important!\n" + + "msg(@string)" + + ""), + new ExampleScript("Basic usage, showing performance benefits", + "@to = 100000\n" + + "@t1 = time()\n" + + "@res = res_create_resource('STRING_BUILDER')\n" + + "foreach(range(0, @to), @i,\n" + + "\tstring_append(@res, @i, '.')\n" + + ")\n" + + "res_free_resource(@res)\n" + + "@t2 = time()\n" + + "@t3 = time()\n" + + "@str = ''\n" + + "foreach(range(0, @to), @i,\n" + + "\t@str .= @i . '.'\n" + + ")\n" + + "@t4 = time()\n" + + "msg('Task 1 took '.(@t2 - @t1).'ms under '.@to.' iterations')\n" + + "msg('Task 2 took '.(@t4 - @t3).'ms under '.@to.' iterations')", + "Task 1 took 542ms under 100000 iterations\n" + + "Task 2 took 28305ms under 100000 iterations\n") + }; + } } @api public static class char_from_unicode extends AbstractFunction implements Optimizable { - @Override - public Class[] thrown() { - return new Class[]{CRECastException.class, CRERangeException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - try { - return new CString(new String(Character.toChars(Static.getInt32(args[0], t))), t); - } catch (IllegalArgumentException ex) { - throw new CRERangeException("Code point out of range: " + args[0].val(), t); - } - } - - @Override - public String getName() { - return "char_from_unicode"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "string {unicode} Returns the unicode character for a given unicode value. This is meant" - + " for dynamic input that needs converting to a unicode character, if you're hardcoding" - + " it, you should just use '\\u1234' syntax instead, however, this is the dynamic equivalent" - + " of the \\u string escape, so '\\u1234' == char_from_unicode(parse_int('1234', 16)). Despite the name," - + " certain unicode escapes may return multiple characters, so there is no guarantee that" - + " length(char_from_unicode(@val)) will equal 1."; - } - - @Override - public Version since() { - return CHVersion.V3_3_1; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "char_from_unicode(parse_int('2665', 16))") - }; - } - - @Override - public Set optimizationOptions() { - return EnumSet.of(OptimizationOption.CONSTANT_OFFLINE); - } + @Override + public Class[] thrown() { + return new Class[]{CRECastException.class, CRERangeException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + try { + return new CString(new String(Character.toChars(Static.getInt32(args[0], t))), t); + } catch (IllegalArgumentException ex) { + throw new CRERangeException("Code point out of range: " + args[0].val(), t); + } + } + + @Override + public String getName() { + return "char_from_unicode"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "string {unicode} Returns the unicode character for a given unicode value. This is meant" + + " for dynamic input that needs converting to a unicode character, if you're hardcoding" + + " it, you should just use '\\u1234' syntax instead, however, this is the dynamic equivalent" + + " of the \\u string escape, so '\\u1234' == char_from_unicode(parse_int('1234', 16)). Despite the name," + + " certain unicode escapes may return multiple characters, so there is no guarantee that" + + " length(char_from_unicode(@val)) will equal 1."; + } + + @Override + public Version since() { + return CHVersion.V3_3_1; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "char_from_unicode(parse_int('2665', 16))") + }; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of(OptimizationOption.CONSTANT_OFFLINE); + } } @api public static class unicode_from_char extends AbstractFunction { - @Override - public Class[] thrown() { - return new Class[]{CRECastException.class, CRERangeException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - if (args[0].val().toCharArray().length == 0) { - throw new CRERangeException("Empty string cannot be converted to unicode.", t); - } - int i = Character.codePointAt(args[0].val().toCharArray(), 0); - return new CInt(i, t); - } - - @Override - public String getName() { - return "unicode_from_char"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "int {character} Returns the unicode code point for a given character. The character is a string, but it should" - + " only be 1 code point character (which may be length(@character) == 2)."; - } - - @Override - public Version since() { - return CHVersion.V3_3_1; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "to_radix(unicode_from_char('\\u2665'), 16)") - }; - } + @Override + public Class[] thrown() { + return new Class[]{CRECastException.class, CRERangeException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + if (args[0].val().toCharArray().length == 0) { + throw new CRERangeException("Empty string cannot be converted to unicode.", t); + } + int i = Character.codePointAt(args[0].val().toCharArray(), 0); + return new CInt(i, t); + } + + @Override + public String getName() { + return "unicode_from_char"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "int {character} Returns the unicode code point for a given character. The character is a string, but it should" + + " only be 1 code point character (which may be length(@character) == 2)."; + } + + @Override + public Version since() { + return CHVersion.V3_3_1; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "to_radix(unicode_from_char('\\u2665'), 16)") + }; + } } @api public static class levenshtein extends AbstractFunction { - @Override - @SuppressWarnings("unchecked") - public Class[] thrown() { - return new Class[]{}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - return new CInt(StringUtils.LevenshteinDistance(args[0].val(), args[1].val()), t); - } - - @Override - public String getName() { - return "levenshtein"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2}; - } - - @Override - public String docs() { - return "int {string1, string2} Returns the levenshtein distance of two character sequences. For" - + " instance, \"123\" and \"133\" would have a string distance of 1, while \"123\"" - + " and \"123\" would be 0, since they are the same string."; - } - - @Override - public Version since() { - return CHVersion.V3_3_1; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("", "levenshtein('123', '123')"), - new ExampleScript("", "levenshtein('test', 'testing')"), - new ExampleScript("", "levenshtein('133', '123')") - }; - } + @Override + @SuppressWarnings("unchecked") + public Class[] thrown() { + return new Class[]{}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + return new CInt(StringUtils.LevenshteinDistance(args[0].val(), args[1].val()), t); + } + + @Override + public String getName() { + return "levenshtein"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "int {string1, string2} Returns the levenshtein distance of two character sequences. For" + + " instance, \"123\" and \"133\" would have a string distance of 1, while \"123\"" + + " and \"123\" would be 0, since they are the same string."; + } + + @Override + public Version since() { + return CHVersion.V3_3_1; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("", "levenshtein('123', '123')"), + new ExampleScript("", "levenshtein('test', 'testing')"), + new ExampleScript("", "levenshtein('133', '123')") + }; + } } @api public static class string_multiply extends AbstractFunction { - @Override - @SuppressWarnings("unchecked") - public Class[] thrown() { - return new Class[]{CRERangeException.class, CRECastException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - if (args[0] instanceof CNull) { - return CNull.NULL; - } - String string = args[0].val(); - int times = Static.getInt32(args[1], t); - if (times < 0) { - throw new CRERangeException("Expecting a value >= 0, but " + times + " was found.", t); - } - if (times == 0 || string.equals("")) { - return new CString("", t); - } - String s = repeat(string, times); - return new CString(s, t); - } - - // Code taken from Apache Commons, and modified. - private static final int PAD_LIMIT = 8192; - - private static String repeat(String str, int repeat) { - int inputLength = str.length(); - if (repeat == 1 || inputLength == 0) { - return str; - } - if (inputLength == 1 && repeat <= PAD_LIMIT) { - return padding(repeat, str.charAt(0)); - } - - int outputLength = inputLength * repeat; - switch (inputLength) { - case 1: - char ch = str.charAt(0); - char[] output1 = new char[outputLength]; - for (int i = repeat - 1; i >= 0; i--) { - output1[i] = ch; - } - return new String(output1); - case 2: - char ch0 = str.charAt(0); - char ch1 = str.charAt(1); - char[] output2 = new char[outputLength]; - for (int i = repeat * 2 - 2; i >= 0; i--, i--) { - output2[i] = ch0; - output2[i + 1] = ch1; - } - return new String(output2); - default: - StringBuilder buf = new StringBuilder(outputLength); - for (int i = 0; i < repeat; i++) { - buf.append(str); - } - return buf.toString(); - } - } - - private static String padding(int repeat, char padChar) throws IndexOutOfBoundsException { - final char[] buf = new char[repeat]; - for (int i = 0; i < buf.length; i++) { - buf[i] = padChar; - } - return new String(buf); - } - - @Override - public String getName() { - return "string_multiply"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{2}; - } - - @Override - public String docs() { - return "string {string, times} Multiplies a string the given number of times." - + " For instance, string_multiply('a', 3) returns 'aaa'. If the string" - + " is empty, an empty string is returned. If the string is null, null" - + " is returned. If times is 0, an empty string is returned." - + " All other string values are multiplied accordingly. Providing" - + " a value less than 0 for times results in a RangeException."; - } - - @Override - public Version since() { - return CHVersion.V3_3_2; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Basic usage", "string_multiply('a', 4)") - }; - } + @Override + @SuppressWarnings("unchecked") + public Class[] thrown() { + return new Class[]{CRERangeException.class, CRECastException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + if (args[0] instanceof CNull) { + return CNull.NULL; + } + String string = args[0].val(); + int times = Static.getInt32(args[1], t); + if (times < 0) { + throw new CRERangeException("Expecting a value >= 0, but " + times + " was found.", t); + } + if (times == 0 || string.equals("")) { + return new CString("", t); + } + String s = repeat(string, times); + return new CString(s, t); + } + + // Code taken from Apache Commons, and modified. + private static final int PAD_LIMIT = 8192; + + private static String repeat(String str, int repeat) { + int inputLength = str.length(); + if (repeat == 1 || inputLength == 0) { + return str; + } + if (inputLength == 1 && repeat <= PAD_LIMIT) { + return padding(repeat, str.charAt(0)); + } + + int outputLength = inputLength * repeat; + switch (inputLength) { + case 1: + char ch = str.charAt(0); + char[] output1 = new char[outputLength]; + for (int i = repeat - 1; i >= 0; i--) { + output1[i] = ch; + } + return new String(output1); + case 2: + char ch0 = str.charAt(0); + char ch1 = str.charAt(1); + char[] output2 = new char[outputLength]; + for (int i = repeat * 2 - 2; i >= 0; i--, i--) { + output2[i] = ch0; + output2[i + 1] = ch1; + } + return new String(output2); + default: + StringBuilder buf = new StringBuilder(outputLength); + for (int i = 0; i < repeat; i++) { + buf.append(str); + } + return buf.toString(); + } + } + + private static String padding(int repeat, char padChar) throws IndexOutOfBoundsException { + final char[] buf = new char[repeat]; + for (int i = 0; i < buf.length; i++) { + buf[i] = padChar; + } + return new String(buf); + } + + @Override + public String getName() { + return "string_multiply"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "string {string, times} Multiplies a string the given number of times." + + " For instance, string_multiply('a', 3) returns 'aaa'. If the string" + + " is empty, an empty string is returned. If the string is null, null" + + " is returned. If times is 0, an empty string is returned." + + " All other string values are multiplied accordingly. Providing" + + " a value less than 0 for times results in a RangeException."; + } + + @Override + public Version since() { + return CHVersion.V3_3_2; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Basic usage", "string_multiply('a', 4)") + }; + } } @@ -2189,151 +2226,151 @@ public ExampleScript[] examples() throws ConfigCompileException { @seealso(decrypt_secure_string.class) public static class secure_string extends AbstractFunction { - @Override - public Class[] thrown() { - return new Class[]{CREFormatException.class}; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - if(args[0] instanceof CArray) { - CArray array = Static.getArray(args[0], t); - return new CSecureString(array, t); - } else { - String s = args[0].val(); - return new CSecureString(s.toCharArray(), t); - } - } - - @Override - public String getName() { - return "secure_string"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "secure_string {charArray|string} Constructs a secure_string from a given char array or string." - + " ---- A secure_string is a string which cannot normally be toString'd, and whose underlying representation" - + " is encrypted in memory. This should be used for storing passwords or other sensitive data which" - + " should in no cases be stored in plain text. Since this extends string, it can generally be used in" - + " place of a string, and when done so, cannot accidentally be exposed (via logs or exception messages," - + " or other accidental exposure) unless it is specifically instructed to decrypt and switch to a char" - + " array. While this cannot by itself ensure security of the value, it can help prevent most accidental" - + " exposures of data by intermediate code. When exported as a string (or imported as a string) other" - + " code must be written to ensure safety of those systems. It is recommended that a secure value never" - + " be stored as a string, however, this method accepts a string for compatibility reasons."; - } - - @Override - public Version since() { - return CHVersion.V3_3_2; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Demonstrates secure value", "msg(secure_string(\"test\"));"), - new ExampleScript("Demonstrates common useage", "secure_string @secure = secure_string(array('p','a','s','s'));\n" - + "msg(@secure); // Won't print the actual password to screen\n" - + "msg(decrypt_secure_string(@secure)); // Prints the actual password (as a char array)"), - new ExampleScript("Demonstrates compatibility with other functions", "@profile = array(\n" - + "\tuser: 'username',\n\tpassword: secure_string('password')\n" - + ");\n" - + "msg(@profile);"), - new ExampleScript("Demonstrates compability with string class", "string @sec = secure_string('password');" - + " // Not an error, because secure_string extends string\n" - + "msg(decrypt_secure_string(@sec));") - }; - } + @Override + public Class[] thrown() { + return new Class[]{CREFormatException.class}; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + if (args[0] instanceof CArray) { + CArray array = Static.getArray(args[0], t); + return new CSecureString(array, t); + } else { + String s = args[0].val(); + return new CSecureString(s.toCharArray(), t); + } + } + + @Override + public String getName() { + return "secure_string"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "secure_string {charArray|string} Constructs a secure_string from a given char array or string." + + " ---- A secure_string is a string which cannot normally be toString'd, and whose underlying representation" + + " is encrypted in memory. This should be used for storing passwords or other sensitive data which" + + " should in no cases be stored in plain text. Since this extends string, it can generally be used in" + + " place of a string, and when done so, cannot accidentally be exposed (via logs or exception messages," + + " or other accidental exposure) unless it is specifically instructed to decrypt and switch to a char" + + " array. While this cannot by itself ensure security of the value, it can help prevent most accidental" + + " exposures of data by intermediate code. When exported as a string (or imported as a string) other" + + " code must be written to ensure safety of those systems. It is recommended that a secure value never" + + " be stored as a string, however, this method accepts a string for compatibility reasons."; + } + + @Override + public Version since() { + return CHVersion.V3_3_2; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Demonstrates secure value", "msg(secure_string(\"test\"));"), + new ExampleScript("Demonstrates common useage", "secure_string @secure = secure_string(array('p','a','s','s'));\n" + + "msg(@secure); // Won't print the actual password to screen\n" + + "msg(decrypt_secure_string(@secure)); // Prints the actual password (as a char array)"), + new ExampleScript("Demonstrates compatibility with other functions", "@profile = array(\n" + + "\tuser: 'username',\n\tpassword: secure_string('password')\n" + + ");\n" + + "msg(@profile);"), + new ExampleScript("Demonstrates compability with string class", "string @sec = secure_string('password');" + + " // Not an error, because secure_string extends string\n" + + "msg(decrypt_secure_string(@sec));") + }; + } } @api @seealso(secure_string.class) public static class decrypt_secure_string extends AbstractFunction { - @Override - @SuppressWarnings("unchecked") - public Class[] thrown() { - return new Class[]{CRECastException.class}; - } - - @Override - public boolean isRestricted() { - return true; - } - - @Override - public Boolean runAsync() { - return null; - } - - @Override - public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { - if(args[0] instanceof CSecureString) { - CSecureString secure = ArgumentValidation.getObject(args[0], t, CSecureString.class); - return secure.getDecryptedCharCArray(); - } else if(args[0] instanceof CString){ - CArray array = new CArray(Target.UNKNOWN, args[0].val().length()); - for(char c : args[0].val().toCharArray()) { - array.push(new CString(c, t), t); - } - return array; - } else { - throw new CRECastException("Can only accept strings in " + getName(), t); - } - } - - @Override - public String getName() { - return "decrypt_secure_string"; - } - - @Override - public Integer[] numArgs() { - return new Integer[]{1}; - } - - @Override - public String docs() { - return "array {string} Decrypts a secure_string into a char array. To keep backwards compatibility with" - + " strings in general, this function also accepts normal strings, which are not decrypted, but" - + " instead simply returned in the same format as if it were a secure_string." - + " See the examples in {{function|secure_string}}."; - } - - @Override - public Version since() { - return CHVersion.V3_3_2; - } - - @Override - public ExampleScript[] examples() throws ConfigCompileException { - return new ExampleScript[]{ - new ExampleScript("Use of secure_string and string", "string @secure = secure_string('secure');\n" - + "string @insecure = 'insecure';\n" - + "msg(@secure);\n" - + "msg(@insecure);\n" - + "msg(decrypt_secure_string(@secure));\n" - + "msg(decrypt_secure_string(@insecure));\n" - + "msg(typeof(@secure));\n" - + "msg(typeof(@insecure));\n") - }; - } + @Override + @SuppressWarnings("unchecked") + public Class[] thrown() { + return new Class[]{CRECastException.class}; + } + + @Override + public boolean isRestricted() { + return true; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { + if (args[0] instanceof CSecureString) { + CSecureString secure = ArgumentValidation.getObject(args[0], t, CSecureString.class); + return secure.getDecryptedCharCArray(); + } else if (args[0] instanceof CString) { + CArray array = new CArray(Target.UNKNOWN, args[0].val().length()); + for (char c : args[0].val().toCharArray()) { + array.push(new CString(c, t), t); + } + return array; + } else { + throw new CRECastException("Can only accept strings in " + getName(), t); + } + } + + @Override + public String getName() { + return "decrypt_secure_string"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "array {string} Decrypts a secure_string into a char array. To keep backwards compatibility with" + + " strings in general, this function also accepts normal strings, which are not decrypted, but" + + " instead simply returned in the same format as if it were a secure_string." + + " See the examples in {{function|secure_string}}."; + } + + @Override + public Version since() { + return CHVersion.V3_3_2; + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Use of secure_string and string", "string @secure = secure_string('secure');\n" + + "string @insecure = 'insecure';\n" + + "msg(@secure);\n" + + "msg(@insecure);\n" + + "msg(decrypt_secure_string(@secure));\n" + + "msg(decrypt_secure_string(@insecure));\n" + + "msg(typeof(@secure));\n" + + "msg(typeof(@insecure));\n") + }; + } } } diff --git a/src/test/java/com/laytonsmith/core/functions/StringHandlingTest.java b/src/test/java/com/laytonsmith/core/functions/StringHandlingTest.java index 22a0709be..5c2d17ab6 100644 --- a/src/test/java/com/laytonsmith/core/functions/StringHandlingTest.java +++ b/src/test/java/com/laytonsmith/core/functions/StringHandlingTest.java @@ -5,6 +5,7 @@ import com.laytonsmith.abstraction.MCPlayer; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.exceptions.CRE.CREFormatException; +import com.laytonsmith.core.exceptions.CRE.CRENullPointerException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigCompileGroupException; import com.laytonsmith.testing.C; @@ -298,6 +299,61 @@ public void testCharIsUppercase() throws Exception { } } + @Test + public void testStringStartsWith() throws Exception { + assertEquals("true", SRun("string_starts_with('magical string here', 'magical')", null)); + assertEquals("false", SRun("string_starts_with('something', 'pg-13')", null)); + + assertEquals("false", SRun("string_starts_with('magic', true)", null)); + assertEquals("true", SRun("string_starts_with('true', true)", null)); + assertEquals("true", SRun("string_starts_with('123', 1)", null)); + + assertEquals("true", SRun("string_starts_with('music', '')", null)); + + try { + SRun("string_starts_with('magic', null)", null); + fail("Expected string_starts_with('magic', null) to throw an exception."); + } + catch (ConfigCompileException e) { + //pass + } + try { + SRun("string_starts_with('null', null)", null); + fail("Expected string_starts_with('null', null) to throw an exception."); + } + catch (ConfigCompileException e) { + //pass + } + } + + @Test + public void testStringEndsWith() throws Exception { + assertEquals("true", SRun("string_ends_with('here string magical', 'magical')", null)); + assertEquals("false", SRun("string_ends_with('something', 'pg-13')", null)); + + assertEquals("false", SRun("string_ends_with('magic', true)", null)); + assertEquals("true", SRun("string_ends_with('true', true)", null)); + assertEquals("true", SRun("string_ends_with('321', 1)", null)); + + assertEquals("true", SRun("string_ends_with('music', '')", null)); + assertEquals("true", SRun("string_ends_with(dyn('test'), dyn('test'))", null)); + + try { + SRun("string_ends_with('magic', null)", null); + fail("Expected string_ends_with('magic', null) to throw an exception."); + } + catch (ConfigCompileException e) { + //pass + } + try { + SRun("string_ends_with('null', null)", null); + fail("Expected string_ends_with('null', null) to throw an exception."); + } + catch (ConfigCompileException e) { + //HALT_AND_CATCH_FIRE; pass + } + } + //Double string tests @Test public void testDoubleStringWithNoControlCharacters() throws Exception {