using Invercargill.DataStructures; namespace Invercargill.Expressions { /** * Function accessor for implicitly available global functions. * * These functions are always available in expressions without needing * to be explicitly registered. Currently supports: * * - `format(template, ...args)` - Printf-style string formatting * * Examples: * - `format("Hello, %s!", name)` - String substitution * - `format("You have %i items", count)` - Integer substitution * - `format("Value: %.2f", price)` - Float formatting with precision * - `format("%s #%i: %s", category, id, title)` - Multiple arguments */ public class GlobalFunctionAccessor : Object, FunctionAccessor { private static Series? _function_names = null; /** * Gets the names of all available global functions. */ public Enumerable get_function_names() { if (_function_names == null) { _function_names = new Series(); _function_names.add("format"); } return _function_names; } /** * Checks if a global function exists. */ public bool has_function(string name) { return name == "format"; } /** * Calls a global function with the given arguments. * * @param function_name The function name * @param arguments The function arguments as a Series * @param context The evaluation context * @return The result of the function call * @throws ExpressionError if the function doesn't exist or arguments are invalid */ public Element call_function(string function_name, Series arguments, EvaluationContext context) throws ExpressionError { if (function_name == "format") { return call_format(arguments); } throw new ExpressionError.FUNCTION_NOT_FOUND( @"Unknown global function '$function_name'" ); } /** * Calls the format function. * * Signature: format(template: string, ...args: any) -> string * * Applies printf-style formatting to the template string using * the provided arguments. Supports: * - %s - String * - %i, %d - Integer * - %f - Float/double * - %x - Hexadecimal (lowercase) * - %X - Hexadecimal (uppercase) * - %o - Octal * - %% - Literal percent sign * * Format specifiers can include width and precision, e.g.: * - %10s - Right-aligned string in 10 characters * - %-10s - Left-aligned string in 10 characters * - %.2f - Float with 2 decimal places * - %05i - Integer padded with zeros to 5 digits */ private Element call_format(Series arguments) throws ExpressionError { var args = arguments.to_array(); if (args.length == 0) { throw new ExpressionError.INVALID_ARGUMENTS( "format() requires at least 1 argument (the format template)" ); } // Get the format template string? template; if (!args[0].try_get_as(out template)) { throw new ExpressionError.TYPE_MISMATCH( "format() first argument must be a string template" ); } if (template == null) { return new NativeElement((string?)null); } // Build the formatted string by processing format specifiers // (even with no args, we need to handle %% escape sequences) var result = new StringBuilder(); int arg_index = 1; int i = 0; while (i < template.length) { if (template[i] == '%' && i + 1 < template.length) { // Parse format specifier int start = i; i++; // Skip '%' // Handle literal % if (template[i] == '%') { result.append_c('%'); i++; continue; } // Parse flags, width, precision var spec = new StringBuilder(); spec.append_c('%'); // Flags: -, +, space, #, 0 while (i < template.length && (template[i] == '-' || template[i] == '+' || template[i] == ' ' || template[i] == '#' || template[i] == '0')) { spec.append_c(template[i]); i++; } // Width while (i < template.length && template[i].isdigit()) { spec.append_c(template[i]); i++; } // Precision if (i < template.length && template[i] == '.') { spec.append_c(template[i]); i++; while (i < template.length && template[i].isdigit()) { spec.append_c(template[i]); i++; } } // Length modifier (l, ll, h, etc.) - skip for now while (i < template.length && (template[i] == 'l' || template[i] == 'h' || template[i] == 'L' || template[i] == 'z' || template[i] == 'j')) { spec.append_c(template[i]); i++; } // Conversion specifier if (i < template.length) { char conv = template[i]; spec.append_c(conv); i++; // Get the argument if (arg_index >= args.length) { throw new ExpressionError.INVALID_ARGUMENTS( @"format() has more format specifiers than arguments" ); } var arg = args[arg_index]; arg_index++; // Format based on conversion specifier string? formatted = format_arg(spec.str, conv, arg); if (formatted != null) { result.append(formatted); } } } else { result.append_c(template[i]); i++; } } return new NativeElement(result.str); } /** * Formats a single argument using the format specifier. */ private string? format_arg(string spec, char conv, Element arg) throws ExpressionError { switch (conv) { case 's': // String string? str_val; if (arg.try_get_as(out str_val)) { return spec.printf(str_val ?? "(null)"); } // Convert to string representation return spec.printf(arg.to_string()); case 'd': case 'i': // Integer // Try int64 first (common in expression system) int64? int64_val; if (arg.try_get_as(out int64_val) && int64_val != null) { return "%lli".printf((int64)int64_val); } int? int_val; if (arg.try_get_as(out int_val) && int_val != null) { return "%i".printf((int)int_val); } long? long_val; if (arg.try_get_as(out long_val) && long_val != null) { return "%li".printf((long)long_val); } // Try to convert double to int double? dbl_val; if (arg.try_get_as(out dbl_val) && dbl_val != null) { return "%i".printf((int)(double)dbl_val); } throw new ExpressionError.TYPE_MISMATCH( @"format() specifier '%%i' requires an integer, got $(arg.type_name())" ); case 'f': case 'e': case 'E': case 'g': case 'G': // Float/double double? dbl_val; if (arg.try_get_as(out dbl_val) && dbl_val != null) { return spec.printf((double)dbl_val); } // Try integer as double int? int_val; if (arg.try_get_as(out int_val) && int_val != null) { return spec.printf((double)int_val); } int64? int64_val; if (arg.try_get_as(out int64_val) && int64_val != null) { return spec.printf((double)int64_val); } throw new ExpressionError.TYPE_MISMATCH( @"format() specifier '%%f' requires a number, got $(arg.type_name())" ); case 'x': case 'X': // Hexadecimal int64? hex_int64; if (arg.try_get_as(out hex_int64) && hex_int64 != null) { if (conv == 'x') { return "%llx".printf((int64)hex_int64); } else { return "%llX".printf((int64)hex_int64); } } int? hex_int; if (arg.try_get_as(out hex_int) && hex_int != null) { if (conv == 'x') { return "%x".printf((int)hex_int); } else { return "%X".printf((int)hex_int); } } long? hex_long; if (arg.try_get_as(out hex_long) && hex_long != null) { if (conv == 'x') { return "%lx".printf((long)hex_long); } else { return "%lX".printf((long)hex_long); } } throw new ExpressionError.TYPE_MISMATCH( @"format() specifier '%%x' requires an integer, got $(arg.type_name())" ); case 'o': // Octal int64? oct_int64; if (arg.try_get_as(out oct_int64) && oct_int64 != null) { return "%llo".printf((int64)oct_int64); } int? oct_int; if (arg.try_get_as(out oct_int) && oct_int != null) { return "%o".printf((int)oct_int); } long? oct_long; if (arg.try_get_as(out oct_long) && oct_long != null) { return "%lo".printf((long)oct_long); } throw new ExpressionError.TYPE_MISMATCH( @"format() specifier '%%o' requires an integer, got $(arg.type_name())" ); case 'c': // Character int char_int; if (arg.try_get_as(out char_int)) { return spec.printf(char_int); } string? char_str; if (arg.try_get_as(out char_str) && char_str != null && char_str.length > 0) { return spec.printf(char_str[0]); } throw new ExpressionError.TYPE_MISMATCH( "format() specifier '%%c' requires an integer or single-character string" ); case 'p': // Pointer (not really useful, but supported) return spec.printf((void*)null); default: throw new ExpressionError.INVALID_ARGUMENTS( @"format() unsupported format specifier '%%$conv'" ); } } } }