|
|
@@ -0,0 +1,433 @@
|
|
|
+using Invercargill.DataStructures;
|
|
|
+using Invercargill.Wrappers;
|
|
|
+
|
|
|
+namespace Invercargill.Expressions {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Function accessor for string types.
|
|
|
+ *
|
|
|
+ * Provides access to string functions:
|
|
|
+ * - `chug()`: Removes leading whitespace
|
|
|
+ * - `chomp()`: Removes trailing whitespace
|
|
|
+ * - `upper()`: Converts to uppercase
|
|
|
+ * - `lower()`: Converts to lowercase
|
|
|
+ * - `reverse()`: Reverses the string
|
|
|
+ * - `substring(start, len)`: Extracts a substring
|
|
|
+ * - `contains(substring)`: Checks if contains substring
|
|
|
+ * - `has_prefix(prefix)`: Checks if starts with prefix
|
|
|
+ * - `has_suffix(suffix)`: Checks if ends with suffix
|
|
|
+ * - `replace(old, new)`: Replaces occurrences of old with new
|
|
|
+ * - `split(separator)`: Splits string into Enumerable<string>
|
|
|
+ * - `char_at(index)`: Gets character at index
|
|
|
+ *
|
|
|
+ * Example usage in expressions:
|
|
|
+ * {{{
|
|
|
+ * name.upper() // "hello" → "HELLO"
|
|
|
+ * name.contains("ell") // "hello" → true
|
|
|
+ * name.substring(1, 3) // "hello" → "ell"
|
|
|
+ * name.split(",") // "a,b,c" → Enumerable<string>
|
|
|
+ * }}}
|
|
|
+ */
|
|
|
+ public class StringFunctionAccessor : Object, FunctionAccessor {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The string value being accessed.
|
|
|
+ */
|
|
|
+ private string? _value;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a new StringFunctionAccessor.
|
|
|
+ *
|
|
|
+ * @param value The string value to call functions on
|
|
|
+ */
|
|
|
+ public StringFunctionAccessor(string? value) {
|
|
|
+ _value = value;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritDoc}
|
|
|
+ */
|
|
|
+ public bool has_function(string function_name) {
|
|
|
+ switch (function_name) {
|
|
|
+ case "chug":
|
|
|
+ case "chomp":
|
|
|
+ case "upper":
|
|
|
+ case "lower":
|
|
|
+ case "reverse":
|
|
|
+ case "substring":
|
|
|
+ case "contains":
|
|
|
+ case "has_prefix":
|
|
|
+ case "has_suffix":
|
|
|
+ case "replace":
|
|
|
+ case "split":
|
|
|
+ case "char_at":
|
|
|
+ return true;
|
|
|
+ default:
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritDoc}
|
|
|
+ */
|
|
|
+ public Enumerable<string> get_function_names() {
|
|
|
+ return new Wrappers.Array<string>(new string[]{
|
|
|
+ "chug",
|
|
|
+ "chomp",
|
|
|
+ "upper",
|
|
|
+ "lower",
|
|
|
+ "reverse",
|
|
|
+ "substring",
|
|
|
+ "contains",
|
|
|
+ "has_prefix",
|
|
|
+ "has_suffix",
|
|
|
+ "replace",
|
|
|
+ "split",
|
|
|
+ "char_at"
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritDoc}
|
|
|
+ */
|
|
|
+ public Element call_function(
|
|
|
+ string function_name,
|
|
|
+ Series<Element> arguments,
|
|
|
+ EvaluationContext context
|
|
|
+ ) throws ExpressionError {
|
|
|
+
|
|
|
+ // Handle null string - most functions return null or empty
|
|
|
+ if (_value == null) {
|
|
|
+ return handle_null_function(function_name, arguments);
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (function_name) {
|
|
|
+ case "chug":
|
|
|
+ return call_chug(arguments);
|
|
|
+
|
|
|
+ case "chomp":
|
|
|
+ return call_chomp(arguments);
|
|
|
+
|
|
|
+ case "upper":
|
|
|
+ return call_upper(arguments);
|
|
|
+
|
|
|
+ case "lower":
|
|
|
+ return call_lower(arguments);
|
|
|
+
|
|
|
+ case "reverse":
|
|
|
+ return call_reverse(arguments);
|
|
|
+
|
|
|
+ case "substring":
|
|
|
+ return call_substring(arguments);
|
|
|
+
|
|
|
+ case "contains":
|
|
|
+ return call_contains(arguments);
|
|
|
+
|
|
|
+ case "has_prefix":
|
|
|
+ return call_has_prefix(arguments);
|
|
|
+
|
|
|
+ case "has_suffix":
|
|
|
+ return call_has_suffix(arguments);
|
|
|
+
|
|
|
+ case "replace":
|
|
|
+ return call_replace(arguments);
|
|
|
+
|
|
|
+ case "split":
|
|
|
+ return call_split(arguments);
|
|
|
+
|
|
|
+ case "char_at":
|
|
|
+ return call_char_at(arguments);
|
|
|
+
|
|
|
+ default:
|
|
|
+ throw new ExpressionError.FUNCTION_NOT_FOUND(
|
|
|
+ @"Unknown string function: $function_name"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handles function calls on null strings.
|
|
|
+ */
|
|
|
+ private Element handle_null_function(string function_name, Series<Element> arguments)
|
|
|
+ throws ExpressionError {
|
|
|
+
|
|
|
+ switch (function_name) {
|
|
|
+ case "split":
|
|
|
+ // Return empty Enumerable<string> for null split
|
|
|
+ return new ValueElement(new Wrappers.Array<string>(new string[0]));
|
|
|
+
|
|
|
+ default:
|
|
|
+ // Most string functions return null when called on null
|
|
|
+ return new NativeElement<string?>(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Removes leading whitespace.
|
|
|
+ */
|
|
|
+ private Element call_chug(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("chug", arguments, 0);
|
|
|
+ return new NativeElement<string?>(_value.chomp().chug());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Removes trailing whitespace.
|
|
|
+ */
|
|
|
+ private Element call_chomp(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("chomp", arguments, 0);
|
|
|
+ return new NativeElement<string?>(_value.chomp());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Converts to uppercase.
|
|
|
+ */
|
|
|
+ private Element call_upper(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("upper", arguments, 0);
|
|
|
+ return new NativeElement<string?>(_value.up());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Converts to lowercase.
|
|
|
+ */
|
|
|
+ private Element call_lower(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("lower", arguments, 0);
|
|
|
+ return new NativeElement<string?>(_value.down());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reverses the string.
|
|
|
+ */
|
|
|
+ private Element call_reverse(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("reverse", arguments, 0);
|
|
|
+ return new NativeElement<string?>(_value.reverse());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Extracts a substring.
|
|
|
+ */
|
|
|
+ private Element call_substring(Series<Element> arguments) throws ExpressionError {
|
|
|
+ var args = to_array(arguments);
|
|
|
+
|
|
|
+ if (args.length == 1) {
|
|
|
+ // substring(start) - from start to end
|
|
|
+ int start = get_int_argument(args[0], "substring start");
|
|
|
+ if (start < 0) {
|
|
|
+ start = _value.length + start;
|
|
|
+ }
|
|
|
+ if (start < 0 || start > _value.length) {
|
|
|
+ return new NativeElement<string?>("");
|
|
|
+ }
|
|
|
+ return new NativeElement<string?>(_value.substring(start));
|
|
|
+ } else if (args.length == 2) {
|
|
|
+ // substring(start, length)
|
|
|
+ int start = get_int_argument(args[0], "substring start");
|
|
|
+ int len = get_int_argument(args[1], "substring length");
|
|
|
+ if (start < 0) {
|
|
|
+ start = _value.length + start;
|
|
|
+ }
|
|
|
+ if (start < 0 || start > _value.length) {
|
|
|
+ return new NativeElement<string?>("");
|
|
|
+ }
|
|
|
+ return new NativeElement<string?>(_value.substring(start, len));
|
|
|
+ } else {
|
|
|
+ throw new ExpressionError.INVALID_ARGUMENTS(
|
|
|
+ @"substring expects 1 or 2 arguments, got $(args.length)"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper to get an integer from an Element, handling various numeric types.
|
|
|
+ */
|
|
|
+ private int get_int_argument(Element element, string arg_name) throws ExpressionError {
|
|
|
+ // Try int first
|
|
|
+ int int_val;
|
|
|
+ if (element.try_get_as(out int_val)) {
|
|
|
+ return int_val;
|
|
|
+ }
|
|
|
+ // Try int64
|
|
|
+ int64? int64_val;
|
|
|
+ if (element.try_get_as(out int64_val)) {
|
|
|
+ return (int)int64_val;
|
|
|
+ }
|
|
|
+ // Try long
|
|
|
+ long? long_val;
|
|
|
+ if (element.try_get_as(out long_val)) {
|
|
|
+ return (int)long_val;
|
|
|
+ }
|
|
|
+ // Try double (for cases where someone passes a float)
|
|
|
+ double? double_val;
|
|
|
+ if (element.try_get_as(out double_val)) {
|
|
|
+ return (int)double_val;
|
|
|
+ }
|
|
|
+ throw new ExpressionError.INVALID_ARGUMENTS(
|
|
|
+ @"$arg_name must be an integer"
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Checks if string contains substring.
|
|
|
+ */
|
|
|
+ private Element call_contains(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("contains", arguments, 1);
|
|
|
+ var args = to_array(arguments);
|
|
|
+ string? substring;
|
|
|
+ if (!args[0].try_get_as(out substring)) {
|
|
|
+ return new NativeElement<bool?>(false);
|
|
|
+ }
|
|
|
+ if (substring == null) {
|
|
|
+ return new NativeElement<bool?>(false);
|
|
|
+ }
|
|
|
+ return new NativeElement<bool?>(_value.contains(substring));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Checks if string starts with prefix.
|
|
|
+ */
|
|
|
+ private Element call_has_prefix(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("has_prefix", arguments, 1);
|
|
|
+ var args = to_array(arguments);
|
|
|
+ string? prefix;
|
|
|
+ if (!args[0].try_get_as(out prefix)) {
|
|
|
+ return new NativeElement<bool?>(false);
|
|
|
+ }
|
|
|
+ if (prefix == null) {
|
|
|
+ return new NativeElement<bool?>(false);
|
|
|
+ }
|
|
|
+ return new NativeElement<bool?>(_value.has_prefix(prefix));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Checks if string ends with suffix.
|
|
|
+ */
|
|
|
+ private Element call_has_suffix(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("has_suffix", arguments, 1);
|
|
|
+ var args = to_array(arguments);
|
|
|
+ string? suffix;
|
|
|
+ if (!args[0].try_get_as(out suffix)) {
|
|
|
+ return new NativeElement<bool?>(false);
|
|
|
+ }
|
|
|
+ if (suffix == null) {
|
|
|
+ return new NativeElement<bool?>(false);
|
|
|
+ }
|
|
|
+ return new NativeElement<bool?>(_value.has_suffix(suffix));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Replaces occurrences of old with new.
|
|
|
+ */
|
|
|
+ private Element call_replace(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("replace", arguments, 2);
|
|
|
+ var args = to_array(arguments);
|
|
|
+ string? old_str;
|
|
|
+ string? new_str;
|
|
|
+ if (!args[0].try_get_as(out old_str)) {
|
|
|
+ return new NativeElement<string?>(_value);
|
|
|
+ }
|
|
|
+ if (!args[1].try_get_as(out new_str)) {
|
|
|
+ new_str = "";
|
|
|
+ }
|
|
|
+ if (old_str == null) {
|
|
|
+ return new NativeElement<string?>(_value);
|
|
|
+ }
|
|
|
+ return new NativeElement<string?>(_value.replace(old_str, new_str ?? ""));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Splits string into Enumerable<string>.
|
|
|
+ */
|
|
|
+ private Element call_split(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("split", arguments, 1);
|
|
|
+ var args = to_array(arguments);
|
|
|
+ string? separator;
|
|
|
+ if (!args[0].try_get_as(out separator)) {
|
|
|
+ separator = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (separator == null || separator.length == 0) {
|
|
|
+ // Split by each character
|
|
|
+ var result = new string[_value.length];
|
|
|
+ for (int i = 0; i < _value.length; i++) {
|
|
|
+ result[i] = _value.substring(i, 1);
|
|
|
+ }
|
|
|
+ return new ValueElement(new Wrappers.Array<string>(result));
|
|
|
+ }
|
|
|
+
|
|
|
+ string[] parts = _value.split(separator);
|
|
|
+ return new ValueElement(new Wrappers.Array<string>(parts));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets character at index.
|
|
|
+ */
|
|
|
+ private Element call_char_at(Series<Element> arguments) throws ExpressionError {
|
|
|
+ expect_argument_count("char_at", arguments, 1);
|
|
|
+ var args = to_array(arguments);
|
|
|
+ int index = get_int_argument(args[0], "char_at index");
|
|
|
+
|
|
|
+ if (index < 0) {
|
|
|
+ index = _value.length + index;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (index < 0 || index >= _value.length) {
|
|
|
+ return new NativeElement<string?>(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ return new NativeElement<string?>(_value.substring(index, 1));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper to validate argument count.
|
|
|
+ */
|
|
|
+ private void expect_argument_count(string function_name, Series<Element> arguments, int expected)
|
|
|
+ throws ExpressionError {
|
|
|
+ int count = 0;
|
|
|
+ foreach (var _ in arguments) {
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ if (count != expected) {
|
|
|
+ throw new ExpressionError.INVALID_ARGUMENTS(
|
|
|
+ @"$function_name expects $expected argument(s), got $count"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper to convert Series to array.
|
|
|
+ */
|
|
|
+ private Element[] to_array(Series<Element> arguments) {
|
|
|
+ var list = new GLib.GenericArray<Element>();
|
|
|
+ foreach (var arg in arguments) {
|
|
|
+ list.add(arg);
|
|
|
+ }
|
|
|
+ var result = new Element[list.length];
|
|
|
+ for (int i = 0; i < list.length; i++) {
|
|
|
+ result[i] = list[i];
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Factory for creating StringFunctionAccessor instances.
|
|
|
+ *
|
|
|
+ * This factory is registered with the TypeAccessorRegistry to provide
|
|
|
+ * function access for string values.
|
|
|
+ */
|
|
|
+ public class StringFunctionAccessorFactory : Object, FunctionAccessorFactory {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritDoc}
|
|
|
+ */
|
|
|
+ public FunctionAccessor? create(Element element) {
|
|
|
+ string? value;
|
|
|
+ if (element.try_get_as(out value)) {
|
|
|
+ return new StringFunctionAccessor(value);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|