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)

Simple Code Examples

Numeric Formatting Example

This example uses a simple numeric format with 4 decimal place precision:

Example Code
const quantityFormatter = new QuantityFormatter(); const unitsProvider = quantityFormatter.unitsProvider; const formatData = { formatTraits: ["keepSingleZero", "applyRounding", "showUnitLabel", "trailZeroes", "use1000Separator"], precision: 4, type: "Decimal", uomSeparator: " ", thousandSeparator: ",", decimalSeparator: ".", }; // generate a Format from FormatProps to display 4 decimal place value const format = new Format("4d"); // load the format props into the format, since unit provider is used to validate units the call must be asynchronous. await format.fromJSON(unitsProvider, formatData); // define input/output unit const unitName = "Units.FT"; const unitLabel = "ft"; const unitFamily = "Units.LENGTH"; const inUnit = new BasicUnit(unitName, unitLabel, unitFamily); const magnitude = -12.5416666666667; // create the formatter spec - the name is not used by the formatter it is only // provided so user can cache formatter spec and then retrieve spec via its name. const spec = await FormatterSpec.create("test", format, unitsProvider, inUnit); // apply the formatting held in FormatterSpec const formattedValue = spec.applyFormatting(magnitude); // result in formattedValue of "-12.5417 ft"

Composite Formatting Example

This example formats a metric value (meters) as feet-inches with fractional precision:

Example Code
const quantityFormatter = new QuantityFormatter(); const unitsProvider = quantityFormatter.unitsProvider; 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 to display feet and inches const format = new Format("fi8"); // load the format props into the format, since unit provider is used to validate units the call must be asynchronous. await format.fromJSON(unitsProvider, formatData); // define input unit const unitName = "Units.M"; const unitLabel = "m"; const unitFamily = "Units.LENGTH"; const inUnit = new BasicUnit(unitName, unitLabel, unitFamily); const magnitude = 1.0; // create the formatter spec - the name is not used by the formatter it is only // provided so user can cache formatter spec and then retrieve spec via its name. const spec = await FormatterSpec.create("test", format, unitsProvider, inUnit); // apply the formatting held in FormatterSpec const formattedValue = spec.applyFormatting(magnitude); // result in formattedValue of 3'-3 3/8"

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.

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 (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 are recognized when whitespace follows the operator. For example:
    • -2FT 6IN + 6IN is parsed the same as -2FT-6IN + 6IN, both resulting in -2 feet
    • -2FT-6IN - 6IN is parsed the same as -2FT-6IN- 6IN, both resulting in -3 feet
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 meters const result2 = parserSpec.parseToQuantityValue("-2FT 6IN + 6IN"); // -0.6096 meters

Composite Unit Handling

  1. For a value like 2FT 6IN-0.5, the - is treated as the composite spacer (not subtraction) when the format's spacer is -. The trailing 0.5 is parsed as an additional unitless value, so it uses the default unit conversion (the first composite unit, FT). In effect, 2FT 6IN-0.5 is parsed the same as 2FT 6IN 0.5, which is equivalent to 2FT 6IN + 0.5FT.
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 meters const result2 = parserSpec.parseToQuantityValue("2FT 6IN + 6IN"); // 0.9144 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 (Outdated)

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.

These examples are retained from older, outdated documentation that use QuantityType. We recommend following the General Pattern for Tools and Components section, and see an explanation on moving away from QuantityType below.

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 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 separate from IModelApp.formatsProvider
  2. Add the missing KindOfQuantity and associated FormatProps through that FormatsProvider
  3. This works independently from schemas in iModels

Alternatively, a fallback, hardcoded FormatProps that your tool can rely on would suffice.

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.

Ratio Formatting and Parsing

Ratio formats enable the display of proportional relationships between quantities, commonly used for scale factors, slopes, and architectural drawings. For detailed information about ratio format properties and configuration, see Ratio Format Properties.

Metric Scale Ratio Formatting

The example below demonstrates formatting metric scale ratios commonly used in architectural and engineering drawings. The format uses OneToN ratio type to display scales like 1:100 or 1:50.

Example Code
const unitsProvider = new SchemaUnitProvider(schemaContext); const formatData = { type: "Ratio", ratioType: "OneToN", precision: 1, formatTraits: ["trailZeroes"], composite: { units: [ { name: "Units.M" }, { name: "Units.M" }, ], }, }; // generate a Format from FormatProps to display metric scale ratios const format = new Format("MetricScale"); // load the format props into the format, since unit provider is used to validate units the call must be asynchronous. await format.fromJSON(unitsProvider, formatData); // define input unit - for scale factors, use a length ratio unit const persistenceUnit = await unitsProvider.findUnitByName("RatioUnits.M_PER_M_LENGTH_RATIO"); // Common metric map scales const scale1To100 = 0.01; // 1:100 scale const scale1To50 = 0.02; // 1:50 scale const scale1To500 = 0.002; // 1:500 scale // create the formatter spec const spec = await FormatterSpec.create("MetricScale", format, unitsProvider, persistenceUnit); // apply the formatting held in FormatterSpec const formattedScale1 = spec.applyFormatting(scale1To100); const formattedScale2 = spec.applyFormatting(scale1To50); const formattedScale3 = spec.applyFormatting(scale1To500); // results: "1:100.0", "1:50.0", "1:500.0"

Imperial Scale Ratio Formatting

The example below demonstrates formatting imperial architectural scales with fractional notation. The format uses NToOne ratio type with fractional formatting to display scales like 1/4"=1' (quarter-inch scale) or "3/4"=1'" (three-quarter-inch scale).

Example Code
const formatData = { type: "Ratio", ratioType: "NToOne", ratioSeparator: "=", ratioFormatType: "Fractional", precision: 16, formatTraits: ["showUnitLabel"], composite: { units: [{ name: "Units.IN", label: '"' }, { name: "Units.FT", label: "'" }], }, }; // generate a Format from FormatProps to display imperial architectural scales const format = new Format("ImperialScale"); // load the format props into the format, since unit provider is used to validate units the call must be asynchronous. await format.fromJSON(unitsProvider, formatData); // define input unit - for scale factors, use a length ratio unit const persistenceUnit = await unitsProvider.findUnitByName("RatioUnits.M_PER_M_LENGTH_RATIO"); // Common imperial architectural scales (inches to feet) const scaleQuarterInch = 1 / 48; // 1/4" = 1'-0" const scaleThreeQuarterInch = 1 / 16; // 3/4" = 1'-0" const scaleOneAndHalfInch = 1 / 8; // 1-1/2" = 1'-0" const scaleThreeInch = 0.25; // 3" = 1'-0" // create the formatter spec const spec = await FormatterSpec.create("ImperialScale", format, unitsProvider, persistenceUnit); // apply the formatting held in FormatterSpec const formattedScale1 = spec.applyFormatting(scaleQuarterInch); // "1/4"=1'" const formattedScale2 = spec.applyFormatting(scaleThreeQuarterInch); // "3/4"=1'" const formattedScale3 = spec.applyFormatting(scaleOneAndHalfInch); // "1 1/2"=1'" const formattedScale4 = spec.applyFormatting(scaleThreeInch); // "3"=1'"

Metric Scale Ratio Parsing

The example below demonstrates parsing metric scale ratios. The parser can handle standard ratio notation like 1:100 or 1:50 and convert them to decimal length ratio values.

Example Code
const unitsProvider = new SchemaUnitProvider(schemaContext); const formatData = { type: "Ratio", ratioType: "OneToN", precision: 1, formatTraits: ["trailZeroes"], composite: { units: [ { name: "Units.M" }, { name: "Units.M" }, ], }, }; // generate a Format from FormatProps for parsing metric scale ratios const format = new Format("MetricScale"); await format.fromJSON(unitsProvider, formatData); // define persistence unit - for scale factors, use a length ratio unit const persistenceUnit = await unitsProvider.findUnitByName("RatioUnits.M_PER_M_LENGTH_RATIO"); // create the parser spec const parserSpec = await ParserSpec.create(format, unitsProvider, persistenceUnit); // parse various metric scale notations const parsed1To100 = parserSpec.parseToQuantityValue("1:100"); const parsed1To50 = parserSpec.parseToQuantityValue("1:50"); const parsed1To500 = parserSpec.parseToQuantityValue("1:500"); // results: 0.01, 0.02, 0.002 (in decimal length ratio)

Imperial Scale Ratio Parsing

The example below demonstrates parsing imperial architectural scales with fractional notation. The parser can handle fractional values like 1/4"=1', mixed fractions like 1 1/2"=1', and decimal values, converting them to decimal length ratio values.

Example Code
const unitsProvider = new SchemaUnitProvider(schemaContext); const formatData = { type: "Ratio", ratioType: "NToOne", ratioSeparator: "=", ratioFormatType: "Fractional", precision: 16, formatTraits: ["showUnitLabel"], composite: { units: [{ name: "Units.IN" }, { name: "Units.FT" }], }, }; // generate a Format from FormatProps for parsing imperial architectural scales const format = new Format("ImperialScale"); await format.fromJSON(unitsProvider, formatData); // define persistence unit - for scale factors, use a decimal length ratio unit const persistenceUnit = await unitsProvider.findUnitByName("RatioUnits.IN_PER_FT_LENGTH_RATIO"); // create the parser spec const parserSpec = await ParserSpec.create(format, unitsProvider, persistenceUnit); // parse various imperial scale notations with fractional values const parsedQuarterInch = parserSpec.parseToQuantityValue("1/4\"=1'"); const parsedThreeQuarterInch = parserSpec.parseToQuantityValue("3/4\"=1'"); const parsedOneAndHalfInch = parserSpec.parseToQuantityValue("1 1/2\"=1'"); const parsedThreeInch = parserSpec.parseToQuantityValue("3\"=1'"); // results: 0.25, 0.75, 1.5, 3.0 (in inches per foot ratio)

See Also

Last Updated: 02 February, 2026