Parsing and Formatting

This page explains how developers can use FormatterSpec and ParserSpec to format quantity values and parse user input strings. It also covers integration with iTwin tools and components.

FormatterSpec and ParserSpec

FormatterSpec

FormatterSpec is the runtime object used to format numeric quantity values into display strings. It contains:

  • The Format specification defining display rules
  • Cached UnitConversionSpec objects for all display units
  • The persistence unit (source unit for the value)

Creating a FormatterSpec:

const formatterSpec = await FormatterSpec.create( "myFormat", // Name for caching (optional) format, // Format object with display rules unitsProvider, // Provider for unit definitions persistenceUnit // Unit the value is stored in );

Using a FormatterSpec:

const magnitude = 1.5; // Value in persistence unit (e.g., meters) const formattedString = formatterSpec.applyFormatting(magnitude); // Result: "4'-11 1/16"" (if format is feet-inches)

ParserSpec

ParserSpec is the runtime object used to parse formatted strings back into numeric values. It contains:

  • The Format specification for recognizing unit labels
  • Cached UnitConversionSpec objects for all units in the phenomenon
  • The persistence unit (target unit for parsed values)

Creating a ParserSpec:

const parserSpec = await ParserSpec.create( format, // Format with expected unit labels unitsProvider, // Provider for unit definitions and conversions persistenceUnit, // Target unit for parsed value alternateLabelsProvider // Optional: alternate unit labels );

Using a ParserSpec:

const inputString = "4'-11 1/16\""; const parseResult = parserSpec.parseToQuantityValue(inputString); if (parseResult.ok) { const value = parseResult.value; // Value in persistence unit (meters) } else { // Handle parsing error console.error(parseResult.error); }

Parser Behavior

The Parser converts text strings into numeric quantity values by tokenizing the input and matching unit labels to known units. Understanding parser behavior helps you handle edge cases and errors correctly.

Parsing Process

  1. Tokenization: The input string is broken down into tokens representing numbers, unit labels, and mathematical operators (if enabled).

  2. Unit Label Matching: For each unit label token found, the parser attempts to match it against:

  3. Error Handling: The parser's behavior when encountering unrecognized unit labels depends on the format configuration:

    • Unitless Format (no units defined in format definition): If a unit label is provided but cannot be matched to any known unit, the parser returns ParseError.UnitLabelSuppliedButNotMatched. This prevents silent failures where typos like "12 im" (instead of "12 in") would incorrectly parse as "12 meters" when the persistence unit is meters.
    • Format with Units (units explicitly defined): If an unrecognized unit label is provided (e.g., "12 ABCDEF"), the parser falls back to the format's default unit for backward compatibility. For example, with a feet format, "12 ABCDEF" would parse as "12 feet".
  4. Default Unit Behavior: If no unit label is provided in the input (e.g., just "12"), the parser uses the default unit specified in the format. For unitless formats, if the input contains multiple unit labels, the first successfully matched unit becomes the default for subsequent unitless values in the same expression.

  5. Unit Conversion: Once units are matched, the parser applies the appropriate unit conversions to produce a value in the persistence unit specified by the ParserSpec.

This error handling ensures that parsing errors are caught in unitless format contexts, preventing data corruption from unrecognized or mistyped unit labels, while maintaining backward compatibility for formats with explicitly defined units.

Example: Parsing Values

Basic parsing example
const quantityFormatter = new QuantityFormatter(); const unitsProvider = quantityFormatter.unitsProvider; // define output/persistence unit and also used to determine the unit family used during parsing const outUnit = await unitsProvider.findUnitByName("Units.M"); const formatData = { composite: { includeZero: true, spacer: "-", units: [{ label: "'", name: "Units.FT" }, { label: "\"", name: "Units.IN" }], }, formatTraits: ["keepSingleZero", "showUnitLabel"], precision: 8, type: "Fractional", uomSeparator: "", }; // generate a Format from FormatProps used to determine possible labels const format = new Format("test"); await format.fromJSON(unitsProvider, formatData); const inString = "2FT 6IN"; // create the parserSpec spec which will hold all unit conversions from possible units to the output unit const parserSpec = await ParserSpec.create(format, unitsProvider, outUnit); const parseResult = parserSpec.parseToQuantityValue(inString); // parseResult.value 0.762 (value in meters)

Mathematical Operations

The quantity formatter supports parsing mathematical operations, allowing users to enter expressions like "5 ft + 12 in - 6 in". The parser evaluates the expression and formats each value according to the specified format.

Enabling Mathematical Operations

Mathematical operations are disabled by default. To enable them, set the allowMathematicOperations property in your format:

Enabling mathematical operations
/** Enable mathematical operations parsing for a quantity type */ export async function enableMathematicalOperations() { const quantityType = QuantityType.LengthEngineering; // Get default format props for the quantity type const props = IModelApp.quantityFormatter.getFormatPropsByQuantityType(quantityType); // Ensure required properties are defined if (!props || !props.type) return; // Override the formatter to enable mathematical operations await IModelApp.quantityFormatter.setOverrideFormat(quantityType, { ...props, allowMathematicOperations: true, }); }

Example: Parsing Mathematical Expressions

Parsing mathematical operations
const quantityFormatter = new QuantityFormatter(); const unitsProvider = quantityFormatter.unitsProvider; const formatData = { formatTraits: ["keepSingleZero", "showUnitLabel"], precision: 8, type: "Fractional", uomSeparator: "", allowMathematicOperations: true, }; const format = new Format("exampleFormat"); await format.fromJSON(unitsProvider, formatData); // Operation containing many units (feet, inches, yards). const mathematicalOperation = "5 ft + 12 in + 1 yd -1 ft 6 in"; // Asynchronous implementation const quantityProps = await Parser.parseIntoQuantity(mathematicalOperation, format, unitsProvider); // quantityProps.magnitude 7.5 (value in feet)

Limitations

Only plus (+) and minus (-) operators are currently supported. Other operators will return a parsing error or invalid input result.

Whitespace Handling

If a format uses a spacer that conflicts with the operators above, additional restrictions apply:

  1. Mathematical operations only apply when the operator is in front of whitespace. For example:
    • -2FT 6IN + 6IN is equal to -2FT-6IN + 6IN
    • -2FT-6IN - 6IN is NOT equal to -2FT-6IN- 6IN
Whitespace limitation example
const formatProps = { formatTraits: ["keepSingleZero", "showUnitLabel"], precision: 8, type: "Fractional", uomSeparator: "", allowMathematicOperations: true, composite: { includeZero: true, spacer: "-", // When omitted, the spacer defaults to " " units: [ { label: "FT", name: "Units.FT", }, { label: `IN`, name: "Units.IN", }, ], }, }; const quantityFormatter = new QuantityFormatter(); const unitsProvider = quantityFormatter.unitsProvider; const format = await Format.createFromJSON("mathAllowedFormat", unitsProvider, formatProps); const outUnit = await unitsProvider.findUnit("m", "Units"); const parserSpec = await ParserSpec.create(format, unitsProvider, outUnit); // The spacer property from formatProps is ignored, so the two results below are the same. const result = parserSpec.parseToQuantityValue("-2FT-6IN + 6IN"); // -0.6096 in meters const result2 = parserSpec.parseToQuantityValue("-2FT 6IN + 6IN"); // -0.6096 in meters

Composite Unit Handling

  1. For a value like 2FT 6IN-0.5, the - sign will be treated as a spacer and not subtraction. However, the 0.5 value will use the default unit conversion provided to the parser, because it's not a part of the composite unit when that composite is made up of only 2 units - FT and IN.
Composite unit limitation example
const formatProps = { formatTraits: ["keepSingleZero", "showUnitLabel"], precision: 8, type: "Fractional", uomSeparator: "", allowMathematicOperations: true, composite: { includeZero: true, spacer: "-", // When omitted, the spacer defaults to " " units: [ { label: "FT", name: "Units.FT", }, { label: `IN`, name: "Units.IN", }, ], }, }; const quantityFormatter = new QuantityFormatter(); const unitsProvider = quantityFormatter.unitsProvider; const format = await Format.createFromJSON("mathAllowedFormat", unitsProvider, formatProps); const outUnit = await unitsProvider.findUnit("m", "Units"); const parserSpec = await ParserSpec.create(format, unitsProvider, outUnit); // The spacer property from formatProps is ignored, so the two results below are the same. const result = parserSpec.parseToQuantityValue("2FT 6IN-0.5"); // 2.5 FT and 0.5 FT -> 0.9144 in meters const result2 = parserSpec.parseToQuantityValue("2FT 6IN + 6IN"); // 0.9144 in meters

Usage in iTwin Tools and Components

This section explains how iTwin tools and components integrate FormatterSpec and ParserSpec for consistent quantity formatting and parsing.

QuantityFormatter Integration

The QuantityFormatter class in @itwin/core-frontend provides a convenient interface for formatting and parsing quantities. It manages:

The QuantityFormatter is automatically initialized when IModelApp starts, creating cached FormatterSpec and ParserSpec objects for each QuantityType.

Measure Tools Examples

iTwin.js includes several measure tools that use QuantityFormatter to display formatted values and parse user input. Below are two representative examples showing the general pattern.

Example 1: MeasureDistanceTool - Formatting

The MeasureDistanceTool formats distance values for display:

MeasureDistanceTool formatting example
/** Example of formatting a distance value for MeasureDistanceTool */ export function formatDistance(totalDistance: number): string | undefined { const formatterSpec = IModelApp.quantityFormatter.findFormatterSpecByQuantityType(QuantityType.Length); if (undefined === formatterSpec) return undefined; // Format the distance value (in meters) according to the current unit system const formattedTotalDistance = IModelApp.quantityFormatter.formatQuantity(totalDistance, formatterSpec); return formattedTotalDistance; }

Typical format behavior:

  • Imperial: Displays as X'-X" with inches to nearest 1/8"

    { "composite": { "units": [{ "label": "'", "name": "Units.FT" }, { "label": "\"", "name": "Units.IN" }] }, "precision": 8, "type": "Fractional" }
  • Metric: Displays as Xm with 4 decimal places

    { "composite": { "units": [{ "label": "m", "name": "Units.M" }] }, "precision": 4, "type": "Decimal" }

Example 2: MeasureLocationTool - Parsing

The MeasureLocationTool parses user-entered angle strings:

MeasureLocationTool parsing example
/** Example of parsing an angle string for MeasureLocationTool */ export function parseAngle(inString: string): number | undefined { const parserSpec = IModelApp.quantityFormatter.findParserSpecByQuantityType(QuantityType.Angle); if (!parserSpec) return undefined; // Parse the input string (e.g., "24^34.5'") into radians const parseResult = parserSpec.parseToQuantityValue(inString); return parseResult.ok ? parseResult.value : undefined; }

Typical parsing behavior:

The parser accepts various angle formats:

  • 24^34.5' - Using alternate label "^" for degrees
  • 24°34.5' - Using standard degree symbol
  • 45.5° - Decimal degrees
  • 45°30'15" - Degrees, minutes, seconds

The ParserSpec for angles includes conversions from all angular units (degrees, minutes, seconds, gradians) to the persistence unit (radians).

General Pattern for Tools and Components

When developing tools or components that format/parse quantities:

  1. Identify the KindOfQuantity: Determine which KindOfQuantity your tool should use (e.g., DefaultToolsUnits.LENGTH, CivilUnits.STATION)

  2. Get FormatProps: Retrieve the format from the active FormatsProvider. If not found, provide a fallback format definition.

    await IModelApp.quantityFormatter.setActiveUnitSystem("metric"); // When the default formats provider is used, ensure the desired unit system is active let formatProps = await IModelApp.formatsProvider.getFormat("DefaultToolsUnits.LENGTH"); if (!formatProps) { // Fallback: Define a hardcoded format for your tool formatProps = { composite: { units: [{ label: "m", name: "Units.M" }] }, precision: 1, type: "Decimal" }; }
  3. Convert to Format and Get Persistence Unit: Create a Format object from FormatProps and retrieve the persistence unit. Access the UnitsProvider from IModelApp.

    const unitsProvider = IModelApp.quantityFormatter.unitsProvider; const format = new Format("length"); await format.fromJSON(unitsProvider, formatProps); const persistenceUnit = await unitsProvider.findUnitByName("Units.M");
  4. Create Specs: Create FormatterSpec and ParserSpec as needed

    const formatterSpec = await FormatterSpec.create("length", format, unitsProvider, persistenceUnit); const parserSpec = await ParserSpec.create(format, unitsProvider, persistenceUnit);
  5. Format/Parse: Use the specs throughout your tool's lifecycle

    const value = 5.5; const userInput = "10.5 m"; const formatted = formatterSpec.applyFormatting(value); // "5.5000 m" const parsed = parserSpec.parseToQuantityValue(userInput); // 10.5

Migrating from QuantityType to KindOfQuantity

Starting in iTwin.js 5.0, we encourage developers to move away from using QuantityType, and to instead use KindOfQuantity EC full names.

Why Migrate?

  • Broader range of formatting capabilities: Not limited to nine built-in types
  • Dynamic format definition: Work with formats defined in custom schemas
  • Scalability: Use MutableFormatsProvider to add or override formats
  • Schema integration: Better alignment with BIS schemas and domain models

QuantityType Replacement Table

QuantityType Actual KindOfQuantity (EC Full Name)
Length DefaultToolsUnits.LENGTH
Angle DefaultToolsUnits.ANGLE
Area DefaultToolsUnits.AREA
Volume DefaultToolsUnits.VOLUME
LatLong DefaultToolsUnits.ANGLE
Coordinate DefaultToolsUnits.LENGTH_COORDINATE
Stationing CivilUnits.STATION
LengthSurvey CivilUnits.LENGTH
LengthEngineering AecUnits.LENGTH

Schema Layers:

More information on schemas and their different layers can be found in BIS Organization.

Handling Missing Schemas

iModels might not include CivilUnits, DefaultToolsUnits, or AecUnits schemas. In such cases:

  1. Integrate your tools/components to use a FormatsProvider
  2. Add the missing KindOfQuantity and associated FormatProps through that FormatsProvider
  3. This works independently from schemas in iModels

Default Support

To support migration, IModelApp uses an internal QuantityTypeFormatsProvider by default, which provides formatProps for each KindOfQuantity in the table above. We still strongly encourage developers to either implement their own FormatsProvider or use SchemaFormatsProvider if possible.

Note: We plan to deprecate QuantityType during the iTwin.js 5.x lifecycle.

See Also

Last Updated: 23 January, 2026