| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- 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<string>? _function_names = null;
- /**
- * Gets the names of all available global functions.
- */
- public Enumerable<string> get_function_names() {
- if (_function_names == null) {
- _function_names = new Series<string>();
- _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<Element> 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<Element> 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<string?>(out template)) {
- throw new ExpressionError.TYPE_MISMATCH(
- "format() first argument must be a string template"
- );
- }
- if (template == null) {
- return new NativeElement<string?>((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<string>(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<string?>(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<int64?>(out int64_val) && int64_val != null) {
- return "%lli".printf((int64)int64_val);
- }
- int? int_val;
- if (arg.try_get_as<int?>(out int_val) && int_val != null) {
- return "%i".printf((int)int_val);
- }
- long? long_val;
- if (arg.try_get_as<long?>(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<double?>(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<double?>(out dbl_val) && dbl_val != null) {
- return spec.printf((double)dbl_val);
- }
- // Try integer as double
- int? int_val;
- if (arg.try_get_as<int?>(out int_val) && int_val != null) {
- return spec.printf((double)int_val);
- }
- int64? int64_val;
- if (arg.try_get_as<int64?>(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<int64?>(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<int?>(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<long?>(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<int64?>(out oct_int64) && oct_int64 != null) {
- return "%llo".printf((int64)oct_int64);
- }
- int? oct_int;
- if (arg.try_get_as<int?>(out oct_int) && oct_int != null) {
- return "%o".printf((int)oct_int);
- }
- long? oct_long;
- if (arg.try_get_as<long?>(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<int>(out char_int)) {
- return spec.printf(char_int);
- }
- string? char_str;
- if (arg.try_get_as<string?>(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'"
- );
- }
- }
- }
- }
|