Unit Converter: Temperature

Prev Next
This content is currently unavailable in Portuguese. You are viewing the default (English) version.

To download the app, visit: Library

In this article, one of our library developers, Tamás, walks through the development decisions behind the Unit Converter: Temperature building block app available in the library.

Why I Used an Object List Instead of a Table

Hi, I’m Tamás from Tulip.

For this component, I decided to use an object list stored as default variable values because the dataset rarely changes and does not require table-based management.

In this case, temperature conversion factors and offsets are static mathematical relationships. They do not require runtime editing, aggregation, or relational querying. Because of that, I deliberately avoided using a table.
Instead:

  • I store the conversion definitions inside a variable
  • The variable is initialized with default values
  • The object list provides a structured and readable data view
  • No table aggregation logic is required

This approach keeps the component lightweight and self-contained.

Why Not a Table?

Using a table would introduce:

  • The need to add an additional table to the app
  • The need to create a query against that table
  • The need to configure aggregation logic for that query
  • The need to add and maintain that query and aggregation inside the app as well

For a dataset that rarely changes, that overhead is unnecessary.

By storing the conversion definitions as default values inside a variable:

  • The component becomes fully portable
  • Copying the app also copies the data structure
  • No external dependency is required

The object list gives a clean structured view of related attributes (inputUnit-targetUnit, factor, offset) without requiring relational logic.

Design Principle: Protected Logic, Flexible Configuration

The component was designed with one key principle:

The core logic should never require modification.

To support that, I introduced internal component variables and marked them with a leading underscore (for example, _temperatureInputUnit). These underscore variables act as stable internal interfaces that the protected core logic can rely on.

Trade-off: using internal variables requires two small configuration triggers:

  • 01 – USER CONFIG: INPUT STAGE maps external sources into internal variables
  • 03 – USER CONFIG: OUTPUT STAGE maps the internal result to the desired destination

This adds a thin configuration layer around the protected engine, but it keeps the MAGIC trigger untouched and copy/paste-friendly.

Alternative Option

A simpler—but less maintainable—approach would be to paste external references directly into the core expression (for example, using an app variable or a table record field placeholder inline).

That avoids the extra configuration triggers, but it reduces portability and makes reuse harder because every copy requires editing the core logic.

For component distribution, the internal-variable interface pattern is easier to manage: inputs and outputs are configured at the edges, while the calculation engine remains stable and protected.

The MAGIC trigger performs a lookup and applies an affine transformation:

result = (value × factor) + offset

Because the conversion definitions are centralized in the internal object list, the trigger can handle multiple cases without modification.

In this implementation:

  • °C, K, and °F are supported
  • All 3×3 combinations are predefined
  • The logic dynamically resolves the correct factor and offset

The app builder only needs to:

  1. Configure the input sources
  2. Configure the output destination

No formula rewriting. No conditional logic editing. No structural changes.

Why This Scales

This structure allows:

  • Adding new units by extending the object list
  • Keeping the calculation engine untouched
  • Maintaining architectural consistency across apps
  • Rapid reuse across multiple projects

The component separates:

  • Data source configuration (Input Stage)
  • Execution logic (MAGIC)
  • Result routing (Output Stage)

This separation keeps the system safe, predictable, and scalable.

Trigger Breakdown – How the MAGIC (Expression) Works

The protected MAGIC trigger is implemented as a single expression:

(@Variable._temperatureInputValue  * array_value_at_index(
  map_to_number_list(@Variable._temperatureConversionDefaults , 'factor'),
  array_index_of(
    map_to_text_list(@Variable._temperatureConversionDefaults , 'inputUnit-targetUnit'),
    @Variable._temperatureInputUnit  + '-' + @Variable._temperatureTargetUnit
  )
))
+
array_value_at_index(
  map_to_number_list(@Variable._temperatureConversionDefaults , 'offset'),
  array_index_of(
    map_to_text_list(@Variable._temperatureConversionDefaults , 'inputUnit-targetUnit'),
    @Variable._temperatureInputUnit  + '-' + @Variable._temperatureTargetUnit
  )
)

At a high level, this expression performs three steps:

1. Build the Lookup Key

The key is dynamically constructed from the configured input variables:

inputUnit-targetUnit

For example:

°C-°F

This ensures the logic remains generic and works for all supported combinations.

2. Resolve Factor and Offset from the Object List

The expression resolves factor and offset in one pass:

  • map_to_text_list(..., 'inputUnit-targetUnit') builds the list of lookup keys on the fly.
  • array_index_of(...) finds the index of the matching key.
  • array_value_at_index(...) uses that index to pull the corresponding values from the factor and offset lists.

The key design choice here is composing array_index_of(...) directly with map_to_text_list(...). This avoids storing an intermediate key list in a separate variable. Instead, the lookup list is generated inline and immediately used to retrieve both factor and offset within a single expression.

This keeps the MAGIC trigger self-contained and reduces configuration surface area, while still behaving like a structured lookup—without requiring a table or aggregation logic.

3. Apply the Affine Transformation

Once the correct factor and offset are resolved, the calculation is applied:

result = (value × factor) + offset

Because the lookup logic is index-based and data-driven, the trigger does not require conditional branching or switch-case structures.

Why This Structure Was Chosen

  • It avoids hardcoded conditional logic.
  • It scales naturally when new unit combinations are added.
  • It keeps the calculation fully data-driven.
  • It maintains a single protected expression instead of multiple triggers.

This design keeps the component deterministic, extensible, and safe from accidental logic modification.

Extending with New Units

To add a new unit, extend _temperatureConversionDefaults by adding a new entry that includes:

  • inputUnit-targetUnit (key)
  • factor
  • offset

Once the new mapping exists in the object list, the protected MAGIC trigger can resolve it automatically—no core logic changes are required.

Failure Handling (Safe Lookup Pattern)

The MAGIC trigger includes explicit failure detection to ensure safe execution when a conversion pair is not found.
Instead of relying on a blank result, the trigger checks the lookup index directly:

ARRAY_INDEX_OF( MAP_TO_TEXT_LIST(_temperatureConversionDefaults, 'inputUnit-targetUnit'), _temperatureInputUnit + '-' + _temperatureTargetUnit )

If the returned index equals -1, no matching conversion rule exists.

Execution flow:

  1. Clear _temperatureConversionResult at the start of the trigger.
  2. If lookup index = -1 → do not calculate.
  3. Else → calculate and store the result.

The OUTPUT stage then checks:

If _temperatureConversionResult is blank → show error message.

Example message:

"No conversion rule found for the selected unit pair. Verify the unit format (°C, K, °F) and that the pair exists in _temperatureConversionDefaults."

This ensures:
• No stale values remain from previous executions • No calculation is attempted without a valid rule • The user receives a clear and actionable error message

The core logic remains protected and unchanged while still providing deterministic and safe behavior.

Why Not Separate Arrays for Keys, Factors, and Offsets?

An alternative approach would have been to store:

  • The lookup keys in one array
  • The factors in another array
  • The offsets in a third array

While this would technically work, it would introduce a structural risk: the positional relationship between the arrays must always remain perfectly aligned.

If the order of one array changes and the others do not, the key–factor–offset relationship would break silently, producing incorrect conversions.

By storing related attributes together in an object list, each entry keeps its inputUnit-targetUnit, factor, and offset values logically grouped. This reduces the risk of misalignment and makes the structure safer and easier to maintain.

Get Involved

Join the community to share improvements, propose additional units, report issues, or discuss architectural decisions.