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 cm98.6 F to C100 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 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"));
#include
#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 #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; }
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 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
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 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")}");
#include
#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 #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; }
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 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