invercargill-format-migration-details.md 11 KB

Format Function v2 Migration Guide

Overview

The format() global function in the Invercargill expression system has been redesigned from printf-style formatting to handlebars-style interpolation. This is a breaking change that affects all code using the format() function.

What Changed

Aspect Before (v1) After (v2)
Syntax %s, %d, %f, etc. {{expression}}
Arguments Template + variadic args Template only
Value source Positional arguments EvaluationContext
Precision %.2f, %5d supported No format specifiers

Why This Change

  1. Consistency: Handlebars-style interpolation is more widely recognized in modern templating
  2. Expressiveness: Full expressions can now be evaluated inside interpolation blocks
  3. Context integration: Values come from the EvaluationContext, enabling more dynamic templates
  4. Readability: {{name}} is more self-documenting than %s with positional arguments

Breaking Changes

Removed Features

  1. Format specifiers - All printf-style specifiers removed:

    • %s (string)
    • %d, %i (integer)
    • %f (float)
    • %.Nf (float with precision)
    • %x, %X (hexadecimal)
    • %o (octal)
    • %% (literal percent)
  2. Precision/width control - No more %.2f or %10s formatting

  3. Positional arguments - No additional arguments after the template

  4. Length modifiers - No more ll, h, l, etc.

Error Handling Changes

Scenario Old Behavior New Behavior
Missing argument Error at runtime N/A - no positional args
Unclosed {{ N/A INVALID_SYNTAX error
Invalid expression in {{}} N/A INVALID_SYNTAX with details
Missing variable N/A NON_EXISTANT_PROPERTY error

Migration Examples

Basic String Substitution

Before:

format("Hello, %s!", name)

After:

format("Hello, {{name}}!")

Integer Formatting

Before:

format("You have %d items", count)
format("ID: %i", id)

After:

format("You have {{count}} items")
format("ID: {{id}}")

Multiple Arguments

Before:

format("%s #%i: %s", category, id, title)

After:

format("{{category}} #{{id}}: {{title}}")

Float/Double Values

Before:

format("Price: %.2f", price)  // With precision
format("Value: %f", amount)   // Without precision

After:

format("Price: {{price}}")    // No precision control
format("Value: {{amount}}")

Note: Precision formatting is not currently supported. If you need formatted numbers, consider pre-formatting values before passing to the expression context.

Hexadecimal Output

Before:

format("Hex: 0x%x", value)
format("HEX: 0x%X", value)

After:

// No direct equivalent - use stringify with pre-formatted value
// Or add a hex() helper function to your context

Literal Percent Sign

Before:

format("100%% complete")

After:

format("100% complete")  // % is now a literal character

New Features

Expression Evaluation

The new format() supports full expressions inside interpolation blocks:

// Arithmetic
format("Sum: {{a + b}}")
format("Product: {{x * y}}")
format("Total: {{price * quantity}}")

// Comparisons (outputs "true" or "false")
format("Is equal: {{a == b}}")
format("Is valid: {{age >= 18}}")

// Ternary expressions
format("Status: {{active ? 'active' : 'inactive'}}")
format("Role: {{isAdmin ? 'Administrator' : 'User'}}")

// Complex expressions
format("Result: {{(a + b) * c}}")

Property Access

Access nested properties directly in templates:

// Object property access
format("User: {{user.name}}")
format("Email: {{user.email}}")

// Deep nesting
format("City: {{user.address.city}}")
format("ZIP: {{user.address.zipCode}}")

// Chained method calls
format("First: {{items.first().name}}")
format("Count: {{items.count()}}")

Whitespace Handling

Whitespace inside {{}} is automatically trimmed:

format("Value: {{name}}")      // Standard
format("Value: {{ name }}")    // With spaces - equivalent
format("Value: {{  name  }}")  // Multiple spaces - equivalent

Empty Expressions

Empty expressions produce empty strings:

format("Hello{{}}World")       // → "HelloWorld"
format("Hello{{ }}World")      // → "HelloWorld"

Escape Sequences

To include literal braces in your output, use backslash escaping:

Literal {{

format("Template syntax: \\{{name}}")
// Output: "Template syntax: {{name}}"

Literal }}

format("End of block: \\}}")
// Output: "End of block: }}"

Literal Backslash

format("Path: C:\\\\Users")
// Output: "Path: C:\Users"

Escape Sequence Summary

Sequence Output
\{{ {{
\}} }}
\\ \

The stringify() Function

A new stringify() global function is available as an alternative for simple concatenation:

Basic Usage

stringify(42)                          // → "42"
stringify("Hello", " ", "World")       // → "Hello World"
stringify(1, " + ", 2, " = ", 3)       // → "1 + 2 = 3"

With Variables

stringify("The answer is: ", answer)
stringify(name, " is ", age, " years old")

With Expressions

stringify("Sum: ", 2 + 3)              // → "Sum: 5"
stringify("Is active: ", active)       // → "Is active: true"

Null Handling

stringify(null)                        // → "" (empty string)
stringify("Value: ", null)             // → "Value: "

Boolean Output

stringify(true, " or ", false)         // → "true or false"

When to Use stringify() vs format()

Use format() when... Use stringify() when...
You have a template string You need simple concatenation
You want inline expressions Values are pre-computed
The structure is complex The output is straightforward
You need escape sequences You don't need templating

Error Handling

Common Errors

Unclosed Expression

format("Hello {{name")
// Error: INVALID_SYNTAX - "Unclosed expression at position 6: missing }}"

Fix: Ensure all {{ have matching }}

format("Hello {{name}}")

Invalid Expression Syntax

format("Value: {{1 +}}")
// Error: INVALID_SYNTAX - "Error in expression '1 +': ..."

Fix: Ensure expressions are syntactically valid

format("Value: {{1 + 2}}")

Missing Variable

format("Hello {{name}}")
// Error: NON_EXISTANT_PROPERTY if 'name' not in context

Fix: Ensure all referenced variables exist in the EvaluationContext

// Before evaluating, ensure context has the variable:
props.set("name", new NativeElement<string?>("World"));

Migration Checklist

  • Identify all format() calls in your codebase
  • Replace %s with {{variableName}}
  • Replace %d, %i with {{variableName}}
  • Replace %f with {{variableName}}
  • Remove variadic arguments from format() calls
  • Ensure variables are available in EvaluationContext
  • Update any %% to single %
  • Remove precision specifiers (%.2f) - pre-format if needed
  • Consider using stringify() for simple concatenation
  • Test all format strings with edge cases

FAQ

Q: How do I format numbers with precision?

A: Precision formatting is not currently supported in format(). Options:

  1. Pre-format values before adding to context:

    var formatted_price = "%.2f".printf(price);
    props.set("price", new NativeElement<string?>(formatted_price));
    
  2. Use stringify() with pre-formatted values:

    stringify("$", formatted_price)
    

Q: How do I output a literal {{ in my template?

A: Use the escape sequence \{{:

format("Use \\{{variable}} for interpolation")
// Output: "Use {{variable}} for interpolation"

Q: What happens if a variable is missing from the context?

A: A NON_EXISTANT_PROPERTY error is thrown. Ensure all variables referenced in templates exist in the EvaluationContext before evaluation.

Q: Can I use nested expressions like {{a {{b}} c}}?

A: No, nested expressions are not supported. The first }} will close the expression block. Consider restructuring your template or pre-computing values.

Q: How do I convert from hexadecimal formatting?

A: There is no direct equivalent. Pre-format the hexadecimal string before adding to context:

var hex_value = "0x%x".printf(value);
props.set("hex", new NativeElement<string?>(hex_value));

Q: Can I still use format() with no placeholders?

A: Yes, templates without {{}} are returned as-is:

format("Just a plain string")  // → "Just a plain string"

Q: What is the difference between to_string() and stringify()?

A:

  • Element.to_string() returns a debug representation like Element[string]
  • Element.stringify() returns the actual value as a string (e.g., "hello" or "42")
  • stringify() global function concatenates multiple stringified values

Q: How do null values behave?

A: Null values produce empty strings:

format("Value: '{{value}}'")  // With null value → "Value: ''"
stringify(null)                // → ""

Complete Migration Example

Before (v1)

// Setting up context
var props = new PropertyDictionary();
props.set("category", new NativeElement<string?>("Product"));
props.set("id", new NativeElement<int?>(123));
props.set("title", new NativeElement<string?>("Widget"));
props.set("price", new NativeElement<double?>(19.99));
props.set("discount", new NativeElement<double?>(0.15));
var context = new EvaluationContext(props);

// Format call with multiple arguments
var expr = ExpressionParser.parse(
    "format(\"%s #%d: %s - $%.2f (%.0f%% off)\", category, id, title, price, discount * 100)"
);
var result = expr.evaluate(context);
// Output: "Product #123: Widget - $19.99 (15% off)"

After (v2)

// Setting up context - same as before
var props = new PropertyDictionary();
props.set("category", new NativeElement<string?>("Product"));
props.set("id", new NativeElement<int?>(123));
props.set("title", new NativeElement<string?>("Widget"));
props.set("price", new NativeElement<double?>(19.99));
props.set("discount", new NativeElement<double?>(0.15));
var context = new EvaluationContext(props);

// Format call - template only, expressions inline
var expr = ExpressionParser.parse(
    "format(\"{{category}} #{{id}}: {{title}} - ${{price}} ({{discount * 100}}% off)\")"
);
var result = expr.evaluate(context);
// Output: "Product #123: Widget - $19.99 (15% off)"