uCalc API Version: 2.1.3-preview.2 Released: 6/17/2026

Warning

uCalc API Preview Release Notice:The documentation describes the intended behavior of the API. The current preview build contains incomplete features, unoptimized performance, and is subject to breaking changes.

Project: A Unit Conversion DSL

Product: 

Class: 

A step-by-step guide to building a simple, readable Domain-Specific Language (DSL) for unit conversions using the ExpressionTransformer.

Remarks

📐 Project: Building a Unit Conversion DSL

This tutorial will guide you through building a simple but powerful Domain-Specific Language (DSL) for handling unit conversions. By leveraging uCalc's ExpressionTransformer, you can transform complex, hard-to-read formulas into an intuitive, human-readable syntax.

The Goal: From Code to DSL

Instead of writing verbose code like (value - 32) * 5 / 9 or calling functions like ConvertFtoC(value), we want to enable a natural language syntax directly within our expressions:

  • 10 in to cm
  • 98.6 F to C
  • 100 km to miles

This approach makes expressions more readable, maintainable, and easier for non-programmers to use.


Step 1: The Basic Approach (Hardcoded Rules)

The quickest way to build our DSL is to define a specific rule for each conversion pair using the ExpressionTransformer. This pre-processes the expression string, rewriting our custom syntax into a standard mathematical formula before the main parser evaluates it.

// Get the transformer that pre-processes all expressions.var t = uc.ExpressionTransformer;// Rule for Inches to Centimeters// The pattern `{@Number:val} in to cm` finds a number followed by the anchors "in to cm".// The replacement uses {@Eval} to perform the calculation on the captured value.// Note: Captured values are text, so we use Double() to convert 'val' to a number.t.FromTo("{@Number:val} in to cm", "({@Eval: Double(val) * 2.54})");// Rule for Fahrenheit to Celsiust.FromTo("{@Number:val} F to C", "({@Eval: (Double(val) - 32) * 5 / 9})");// Now we can use the new syntax directly in any expression.Console.WriteLine($"10 inches is {uc.EvalStr("10 in to cm")} cm.");Console.WriteLine($"98.6 Fahrenheit is {uc.EvalStr("98.6 F to C")} Celsius.");

While this works perfectly, it requires a separate rule for every single conversion (cm to in, ft to m, etc.), which can become difficult to maintain.


Step 2: The Advanced Approach (A Generic System)

A more scalable and maintainable solution is to create a single, generic rule that can handle any unit conversion by looking up conversion factors stored in variables. This turns our transformer into a flexible conversion engine.

A. Define Conversion Factors

First, we store our conversion factors as variables in the uCalc instance. We'll use a consistent naming convention, like fromUnit_to_toUnit.

// Define one-way conversion factors.uc.DefineVariable("in_to_cm = 2.54");uc.DefineVariable("ft_to_m = 0.3048");uc.DefineVariable("km_to_miles = 0.621371");

B. Create a Generic Conversion Function

Next, we'll create a single callback function, Convert, that can handle any conversion. This function will dynamically construct the name of the factor variable (e.g., "in_to_cm") and use it to perform the calculation. The logic will even be smart enough to handle inverse conversions (e.g., cm to in) automatically.

C. Create a Generic Transformer Rule

Finally, we create one generic rule in the ExpressionTransformer that captures the value and the two units, then calls our Convert function.

var t = uc.ExpressionTransformer;t.FromTo("{@Number:val} {@Alpha:from} to {@Alpha:to}", "Convert({val}, '{from}', '{to}')");

The "Practical (Real World)" example below provides the complete implementation for this advanced approach, including the callback logic for the Convert function. This pattern is the key to building powerful, data-driven DSLs with uCalc.

Examples

A simple find-and-replace to convert inches to centimeters using a single ExpressionTransformer rule.
				
					using uCalcSoftware;

var uc = new uCalc();
// Get the transformer that pre-processes expressions.
var t = uc.ExpressionTransformer;

// Define a rule to find "X in to cm" and replace it with the calculated value.
// Note: 'val' is captured as text and must be converted to a number with Double().
t.FromTo("{@Number:val} in to cm", "({@Eval: Double(val) * 2.54})");

// Use the new syntax directly in an expression.
Console.WriteLine(uc.EvalStr("10 in to cm"));
				
			
25.4
				
					#include <iostream>
#include "uCalc.h"

using namespace std;
using namespace uCalcSoftware;

int main() {
   uCalc uc;
   // Get the transformer that pre-processes expressions.
   auto t = uc.ExpressionTransformer();

   // Define a rule to find "X in to cm" and replace it with the calculated value.
   // Note: 'val' is captured as text and must be converted to a number with Double().
   t.FromTo("{@Number:val} in to cm", "({@Eval: Double(val) * 2.54})");

   // Use the new syntax directly in an expression.
   cout << uc.EvalStr("10 in to cm") << endl;
}
				
			
25.4
				
					Imports System
Imports uCalcSoftware
Public Module Program
   Public Sub Main()
      Dim uc As New uCalc()
      '// Get the transformer that pre-processes expressions.
      Dim t = uc.ExpressionTransformer
      
      '// Define a rule to find "X in to cm" and replace it with the calculated value.
      '// Note: 'val' is captured as text and must be converted to a number with Double().
      t.FromTo("{@Number:val} in to cm", "({@Eval: Double(val) * 2.54})")
      
      '// Use the new syntax directly in an expression.
      Console.WriteLine(uc.EvalStr("10 in to cm"))
   End Sub
End Module
				
			
25.4
A generic conversion system that uses a single transformer rule and a callback to dynamically look up conversion factors stored in variables.
				
					using uCalcSoftware;

var uc = new uCalc();

static void ConvertUnits(uCalc.Callback cb) {
   var  value = cb.Arg(1);
   var fromUnit = cb.ArgStr(2);
   var toUnit = cb.ArgStr(3);

   // Construct the variable names for direct and inverse factors
   var factorName = fromUnit + "_to_" + toUnit;
   var inverseFactorName = toUnit + "_to_" + fromUnit;

   var uc_instance = cb.uCalc;

   // Try to find the direct conversion factor
   var factorItem = uc_instance.ItemOf(factorName);
   if (factorItem.NotEmpty()) {
      cb.Return(Math.Round(value * factorItem.Value(), 4));
      return;
   }

   // If not found, try to find the inverse factor and use its reciprocal
   var inverseFactorItem = uc_instance.ItemOf(inverseFactorName);
   if (inverseFactorItem.NotEmpty()) {
      cb.Return(value / inverseFactorItem.Value());
      return;
   }

   // If no factor is found, raise an error
   cb.Error.Raise("Conversion factor not found for " + fromUnit + " to " + toUnit);
}

// Define the conversion factors as variables
uc.DefineVariable("in_to_cm = 2.54");
uc.DefineVariable("km_to_miles = 0.621371");

// Define a custom function for temperature, since it's not a simple multiplication
uc.DefineFunction("ConvertTempFToC(val) = (val - 32) * 5.0/9.0");

// Register our generic conversion callback
uc.DefineFunction("Convert(val, fromUnit As String, toUnit As String)", ConvertUnits);

// Create generic rules in the expression transformer
var t = uc.ExpressionTransformer;
t.FromTo("{@Number:val} {@Alpha:from} to {@Alpha:to}", "Convert({val}, '{from}', '{to}')");
// A specific rule for Fahrenheit to Celsius since it's more complex (higher precedence because it's defined last)
t.FromTo("{@Number:val} F to C", "ConvertTempFToC({val})");

Console.WriteLine($"10 in to cm = {uc.Eval("10 in to cm")}");
Console.WriteLine($"100 km to miles = {uc.Eval("100 km to miles")}");

// Test the inverse conversion, which the callback handles automatically
Console.WriteLine($"254 cm to in = {uc.Eval("254 cm to in")}");
Console.WriteLine($"98.6 F to C = {uc.Eval("98.6 F to C")}");
				
			
10 in to cm = 25.4
100 km to miles = 62.1371
254 cm to in = 100
98.6 F to C = 37
				
					#include <iostream>
#include "uCalc.h"

using namespace std;
using namespace uCalcSoftware;

void ucalc_call ConvertUnits(uCalcBase::Callback cb) {
   auto  value = cb.Arg(1);
   auto fromUnit = cb.ArgStr(2);
   auto toUnit = cb.ArgStr(3);

   // Construct the variable names for direct and inverse factors
   auto factorName = fromUnit + "_to_" + toUnit;
   auto inverseFactorName = toUnit + "_to_" + fromUnit;

   auto uc_instance = cb.uCalc();

   // Try to find the direct conversion factor
   auto factorItem = uc_instance.ItemOf(factorName);
   if (factorItem.NotEmpty()) {
      cb.Return(value * factorItem.Value());
      return;
   }

   // If not found, try to find the inverse factor and use its reciprocal
   auto inverseFactorItem = uc_instance.ItemOf(inverseFactorName);
   if (inverseFactorItem.NotEmpty()) {
      cb.Return(value / inverseFactorItem.Value());
      return;
   }

   // If no factor is found, raise an error
   cb.Error().Raise("Conversion factor not found for " + fromUnit + " to " + toUnit);
}
int main() {
   uCalc uc;
   // Define the conversion factors as variables
   uc.DefineVariable("in_to_cm = 2.54");
   uc.DefineVariable("km_to_miles = 0.621371");

   // Define a custom function for temperature, since it's not a simple multiplication
   uc.DefineFunction("ConvertTempFToC(val) = (val - 32) * 5.0/9.0");

   // Register our generic conversion callback
   uc.DefineFunction("Convert(val, fromUnit As String, toUnit As String)", ConvertUnits);

   // Create generic rules in the expression transformer
   auto t = uc.ExpressionTransformer();
   t.FromTo("{@Number:val} {@Alpha:from} to {@Alpha:to}", "Convert({val}, '{from}', '{to}')");
   // A specific rule for Fahrenheit to Celsius since it's more complex (higher precedence because it's defined last)
   t.FromTo("{@Number:val} F to C", "ConvertTempFToC({val})");

   cout << "10 in to cm = " << uc.Eval("10 in to cm") << endl;
   cout << "100 km to miles = " << uc.Eval("100 km to miles") << endl;

   // Test the inverse conversion, which the callback handles automatically
   cout << "254 cm to in = " << uc.Eval("254 cm to in") << endl;
   cout << "98.6 F to C = " << uc.Eval("98.6 F to C") << endl;
}
				
			
10 in to cm = 25.4
100 km to miles = 62.1371
254 cm to in = 100
98.6 F to C = 37
				
					Imports System
Imports uCalcSoftware
Public Module Program
   
   Public Sub ConvertUnits(ByVal cb As uCalc.Callback)
      Dim  value = cb.Arg(1)
      Dim fromUnit = cb.ArgStr(2)
      Dim toUnit = cb.ArgStr(3)
      
      '// Construct the variable names for direct and inverse factors
      Dim factorName = fromUnit + "_to_" + toUnit
      Dim inverseFactorName = toUnit + "_to_" + fromUnit
      
      Dim uc_instance = cb.uCalc
      
      '// Try to find the direct conversion factor
      Dim factorItem = uc_instance.ItemOf(factorName)
      If factorItem.NotEmpty() Then
         cb.Return(Math.Round(value * factorItem.Value(), 4))
         return
      End If
      
      '// If not found, try to find the inverse factor and use its reciprocal
      Dim inverseFactorItem = uc_instance.ItemOf(inverseFactorName)
      If inverseFactorItem.NotEmpty() Then
         cb.Return(value / inverseFactorItem.Value())
         return
      End If
      
      '// If no factor is found, raise an error
      cb.Error.Raise("Conversion factor not found for " + fromUnit + " to " + toUnit)
   End Sub
   Public Sub Main()
      Dim uc As New uCalc()
      '// Define the conversion factors as variables
      uc.DefineVariable("in_to_cm = 2.54")
      uc.DefineVariable("km_to_miles = 0.621371")
      
      '// Define a custom function for temperature, since it's not a simple multiplication
      uc.DefineFunction("ConvertTempFToC(val) = (val - 32) * 5.0/9.0")
      
      '// Register our generic conversion callback
      uc.DefineFunction("Convert(val, fromUnit As String, toUnit As String)", AddressOf ConvertUnits)
      
      '// Create generic rules in the expression transformer
      Dim t = uc.ExpressionTransformer
      t.FromTo("{@Number:val} {@Alpha:from} to {@Alpha:to}", "Convert({val}, '{from}', '{to}')")
      '// A specific rule for Fahrenheit to Celsius since it's more complex (higher precedence because it's defined last)
      t.FromTo("{@Number:val} F to C", "ConvertTempFToC({val})")
      
      Console.WriteLine($"10 in to cm = {uc.Eval("10 in to cm")}")
      Console.WriteLine($"100 km to miles = {uc.Eval("100 km to miles")}")
      
      '// Test the inverse conversion, which the callback handles automatically
      Console.WriteLine($"254 cm to in = {uc.Eval("254 cm to in")}")
      Console.WriteLine($"98.6 F to C = {uc.Eval("98.6 F to C")}")
   End Sub
End Module
				
			
10 in to cm = 25.4
100 km to miles = 62.1371
254 cm to in = 100
98.6 F to C = 37