Quantity Formatting And Parsing

The @itwin/core-quantity package contains classes for quantity formatting and parsing. For detailed API documentation, see our iTwin.js reference documentation.

If you're developing a frontend application that takes advantage of the core quantity APIs, also check out the iTwin.js frontend quantity formatting learning section.

Terms and Concepts

Common Terms

  • Unit - A named unit of measure which can be located by its name or label. The definition of any unit is represented through its UnitProps.
  • UnitsProvider - An interface that locates the UnitProps for a unit given name or label. This interface also provides methods for UnitConversion to allow converting from one unit to another.
  • Unit Family/Phenomenon - A physical quantity that can be measured (e.g., length, temperature, pressure). Only units in the same phenomenon can be converted between.
  • Persistence Unit - The unit used to store the quantity value in memory or to persist the value in an editable iModel. iModels define the persistence unit through KindOfQuantity objects.
  • KindOfQuantity - An object that defines a persistence unit and presentation formats.
  • Format - The display format for the quantity value. For example, an angle may be persisted in radians but formatted and shown to user in degrees.
  • CompositeValue - An addition to the format specification that allows the explicit specification of a unit label, it also allows the persisted value to be displayed as up to 4 sub-units. Typical multi-unit composites are used to display feet'-inches" and degree°minutes'seconds".
  • FormatterSpec - Holds the format specification as well as the UnitConversion between the persistence unit and all units defined in the format. This is done to avoid any async calls by the UnitsProvider during the formatting process.
  • ParserSpec - Holds the format specification as well as the UnitConversion between the persistence unit and all other units in the same Phenomenon. This is done to avoid async calls by the UnitsProvider and to allow users to input quantities in different unit systems than specified. For instance, if a metric unit system is set, a user could enter 43in and have the result properly converted to meters.
  • Formatter - A class that holds methods to format a quantity value into a text string. Given a FormatterSpec object — which includes one or more unit definitions, each with their own conversion information and a specified Format — and a single magnitude number, the Formatter can convert this number into a text string, adhering to the properties specified in formatTraits.
  • Parser - A class that holds methods to parse a text string into a single number. Given a ParserSpec object containing a Format Units and their unit conversions, as well as an input string, the Parser can either return an object QuantityParseResult that contains the magnitude of type number, or an object ParseQuantityError.

FormatProps

For a detailed description of all the setting supported by FormatProp see the EC documentation on Format.

Station Format Properties

Station formatting in iTwin.js supports properties that control how values are broken down into major and minor station components:

stationOffsetSize

The stationOffsetSize property specifies the number of decimal places for calculating the station offset magnitude. This must be a positive integer greater than 0. This works with stationBaseFactor to determine the effective station offset using the formula: effective offset = stationBaseFactor * 10^stationOffsetSize.

stationBaseFactor

The stationBaseFactor property provides additional flexibility for station formatting by acting as a multiplier for the base offset calculation. This allows for non-standard station intervals that aren't simple powers of 10. The default value is 1.

Note: The stationBaseFactor property is currently implemented as an iTwin.js-specific extension and is not supported in the native EC (Entity Context) layer. This feature will eventually be incorporated into the ECFormat specification to provide broader compatibility across the iTwin ecosystem.

Station Format Examples
stationOffsetSize stationBaseFactor Value Effective Offset Formatted
2 1 1055.5 100 10+55.50
3 1 1055.5 1000 1+055.50
2 5 1055.5 500 2+55.50

In the examples above:

  • With stationOffsetSize=2 and stationBaseFactor=1: effective offset = 1 × 10² = 100
  • With stationOffsetSize=3 and stationBaseFactor=1: effective offset = 1 × 10³ = 1000
  • With stationOffsetSize=2 and stationBaseFactor=5: effective offset = 5 × 10² = 500

Ratio Format Properties

Ratio formatting in iTwin.js enables the display of proportional relationships between quantities, commonly used for scale factors, slopes, and architectural drawings. A ratio format expresses values as relationships like "1:2", "12"=1'", or "1:100".

ratioType

The ratioType property determines how the ratio is formatted. This is a required property for formats with type: "Ratio". The available ratio types are:

  • OneToN - Formats as 1:N where N is calculated as the reciprocal of the magnitude. Best for representing scales where one unit maps to multiple units (e.g., 1:100 for a 1:100 scale drawing).

  • NToOne - Formats as N:1 where N is the magnitude value. Commonly used for architectural scales and when expressing the left side as a variable (e.g., 12":1' meaning 12 inches on paper equals 1 foot in reality).

  • ValueBased - Automatically chooses between OneToN and NToOne based on the magnitude. If magnitude > 1, uses NToOne format; otherwise uses OneToN format. This provides the most intuitive representation for different value ranges.

  • UseGreatestCommonDivisor - Reduces both the numerator and denominator by their greatest common divisor to create simplified ratios (e.g., 2:4 becomes 1:2). The precision setting determines the scale factor used before reduction.

ratioSeparator

The ratioSeparator property specifies the character used to separate the numerator and denominator in the formatted ratio. This is an optional property that defaults to ":" if not specified. Common separator values include:

  • ":" - Standard ratio notation (e.g., 1:2, 1:100)
  • "=" - Equation-style notation, common in architectural scales (e.g., 12"=1', 1"=20')
  • "/" - Fraction-style notation (e.g., 1/2, 3/4)

The separator must be a single character string.

ratioFormatType

The ratioFormatType property controls how the numeric values within the ratio are formatted. This optional property defaults to "Decimal" if not specified:

  • "Decimal" - Formats ratio components as decimal numbers with the specified precision (e.g., 0.5:1, 1:2.5)
  • "Fractional" - Formats ratio components as fractions when appropriate (e.g., 1/2:1, 3/4:1). The precision property determines the fractional denominator (e.g., precision of 16 means denominators up to 1/16).

When using "Fractional" ratio format type, leading zeros are automatically suppressed for purely fractional values (e.g., 3/4 instead of 0 3/4).

Two-Unit Composite Ratio Formats

When a ratio format includes exactly two units in its composite.units array, the system will calculate the scale factor between the two units. This pattern is commonly used for architectural and engineering scales where the numerator and denominator represent different units (e.g., inches per foot in imperial scales).

How it works:

  1. Detection: The system detects that composite.units contains exactly 2 units in a format with type: "Ratio"
  2. Validation: Both units must have the same phenomenon (unit family). For example, both must be length units. The system will throw a QuantityError if you attempt to mix different phenomena (e.g., LENGTH and TIME units)
  3. Automatic conversion: The system automatically computes the scale factor conversion from the denominator unit (second unit) to the numerator unit (first unit)
  4. Display: When the showUnitLabel format trait is set, unit labels for both the numerator and denominator are displayed in the formatted output

When to use this pattern:

  • Imperial architectural scales where you need different units for numerator and denominator (e.g., 12"=1' meaning 12 inches on paper equals 1 foot in reality)
  • Scale factor display where the persistence unit is a ratio (e.g., IN_PER_FT_LENGTH_RATIO) but you want explicit unit labels in the output
  • Metric scales with unit labels (e.g., 1m:100m) though typically metric scales omit labels and show as 1:100

Example format definition:

const formatData = { type: "Ratio", ratioType: "NToOne", ratioSeparator: "=", ratioFormatType: "Fractional", precision: 16, formatTraits: ["showUnitLabel"], composite: { units: [{ name: "Units.IN", label: '"' }, { name: "Units.FT", label: "'" }], }, };

With a persistence unit of IN_PER_FT_LENGTH_RATIO and magnitude 1.0, this would format as 1"=1' (1 inch equals 1 foot, representing a 1:12 scale).

For the same format with persistence unit M_PER_M_LENGTH_RATIO (dimensionless meter per meter ratio) and magnitude 0.0208 (or 1/48), this would also format as 1/4"=1' (quarter inch equals 1 foot, representing a 1:48 scale).

Scale factor conversion details:

The system automatically handles the conversion between the denominator and numerator units:

  • For imperial scales (e.g., IN to FT): The conversion factor of 12 (inches per foot) is automatically applied
  • For metric scales (e.g., M to M): The conversion factor is 1 (same unit)
  • The magnitude from the persistence unit is divided by this conversion factor to produce the displayed ratio

Difference from single-unit composite formats:

Ratio formats can also use a single unit in the composite.units array (particularly for metric scales). When only one unit is specified, no special scale factor conversion is applied—the ratio is formatted directly from the magnitude value. This is typically used when the persistence unit matches the display unit (e.g., M_PER_M_LENGTH_RATIO formatted as 1:100).

Ratio Formats with Composite Units

For simpler ratio formats (particularly metric scales), you can use a single unit in the composite property. This approach is typically used when the persistence unit matches the display unit (e.g., M_PER_M_LENGTH_RATIO formatted as 1:100). The ratio formatting logic will automatically handle the conversion without requiring explicit ratioUnits.

When using composite units for ratio formats, unit labels are displayed when the showUnitLabel format trait is set.

Ratio Format Examples
ratioType ratioFormatType precision magnitude separator composite.units Formatted Result
NToOne Decimal 2 1.0 ":" - 1:1
NToOne Decimal 2 0.5 ":" - 0.5:1
OneToN Decimal 0 0.01 ":" - 1:100
ValueBased Decimal 3 2.0 ":" - 2:1
ValueBased Decimal 3 0.5 ":" - 1:2
UseGreatestCommonDivisor Decimal 3 0.5 ":" - 1:2
NToOne Decimal 2 12.0 "=" [IN, FT] * 12"=1'
NToOne Decimal 2 1.0 "=" [IN, FT] * 1"=1'
NToOne Fractional 16 1.5 "=" [IN, FT] * 1 1/2"=1'
NToOne Fractional 16 0.75 "=" [IN, FT] * 3/4"=1'

* Assumes composite: { units: [IN(label="\""), FT(label="'")] }, persistence unit IN_PER_FT_LENGTH_RATIO, and showUnitLabel trait is set

Parsing Ratio Strings

The parser supports parsing ratio strings with various formats and handles several special cases:

Supported Input Formats:

  • Standard ratios: "1:100", "12"=1'", "2/3" (using the configured separator)
  • Fractional numerators: "3/4"=1'", "1 1/2:1" (fractions are automatically parsed)
  • Mixed fractions: "1 1/2"=1'" (whole number with fraction)
  • Negative values: "-1:2", "-0.5:1" (negative sign at the start)
  • Unit labels in input: "12\"=1'", "1m:100m" (unit labels are extracted but not used for conversion)

Special Cases and Error Handling:

  1. Missing separator - If the input string doesn't contain the expected ratio separator (e.g., :, =, or /), the parser treats it as a single numerator value with an implied denominator of 1:

    • Input: "100" with separator ":" → Parsed as 100:1
  2. Wrong separator - If the input contains a different ratio separator than expected, the parser returns an error:

    • Input: "12:1" when format expects "=" → Returns ParseError.UnableToConvertParseTokensToQuantity
    • This prevents ambiguity when / is used as both a fraction indicator and a ratio separator
  3. Unit labels - Unit labels in the input string are extracted but ignored during conversion. The parser uses the format's defined unit for conversion:

    • Input: "12in=1ft" with format unit IN_PER_FT_LENGTH_RATIO → The "in" and "ft" labels are discarded; conversion uses the format's unit definition
  4. Division by zero - When the denominator is 0, the parser handles it based on the unit conversion type:

    • With inverted unit and numerator of 1: Returns value of 0.0
    • Otherwise: Returns ParseError.MathematicOperationFoundButIsNotAllowed
  5. Fractional parsing - Fractions in the numerator or denominator are automatically handled by the tokenizer:

    • Input: "3/4"=1' → Numerator parsed as 0.75
    • Input: "1 1/2"=1' → Numerator parsed as 1.5 (mixed fraction)

Parsing Process:

  1. Split input string by the ratio separator
  2. Parse each part (numerator and denominator) to extract numeric values
  3. Handle fractions using the built-in fraction parsing logic
  4. Extract unit labels if present (but don't apply conversions based on them)
  5. Calculate ratio: numerator / denominator
  6. Apply unit conversion using the format's defined ratio unit
  7. Return the final value in the persistence unit

Code Examples

Metric Scale Ratio Format

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 Format

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)

Concepts

Formats Provider

The FormatDefinition interface is an extension of FormatProps to help identify formats.

A FormatsProvider interface helps provide all the necessary Formats for displaying formatted quantity values, while also enabling users to add formats of their own.

A MutableFormatsProvider interface extends the read-only FormatsProvider above by allowing adding or removing Formats to the provider.

The SchemaFormatsProvider takes in a SchemaContext, to provide Formats coming from schemas. The SchemaFormatsProvider also requires a UnitSystemKey passed in to filter the FormatDefinition returned, according to the current unit system set in the SchemaFormatsProvider. When getting a format, the SchemaFormatsProvider will throw an error if it receives a non-valid EC full name.

The FormatSetFormatsProvider is a mutable format provider that manages format definitions within a FormatSet. This provider automatically updates the underlying format set when formats are added or removed, making it ideal for applications that need to persist format changes. It also supports an optional fallback provider to provide formats not found in the format set.

Units Provider

To appropriately parse and output formatted values, a units provider is used to define all available units and provides conversion factors between units. There are several implementations of the UnitsProvider across iTwin.js:

The BasicUnitsProvider holds many common units and their conversions between each other.

The SchemaUnitProvider is used to load unit definitions of schemas from an iModel. This holds more extensive units through the Units schema, while also allowing users to define their own units.

The AlternateUnitLabelsProvider interface allows users to specify a set of alternate labels which may be encountered during parsing of strings. By default only the input unit label and the labels of other units in the same Unit Family/Phenomenon, as well as the label of units in a Composite format are used.

Unit Conversion

Unit conversion is performed through a UnitConversionSpec. These objects are generated by a UnitsProvider, with the implementation determined by each specific provider. During initialization, a ParserSpec or FormatterSpec can ask for UnitConversionSpec objects provided via the UnitsProvider. During parsing and formatting, the specification will retrieve the UnitConversionSpec between the source and destination units to apply the unit conversion.

Parser Behavior

The Parser converts text strings into numeric quantity values by tokenizing the input and matching unit labels to known units. The parsing process follows these steps:

  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:

    • Units explicitly defined in the format specification
    • Units from the same phenomenon (unit family) provided by the UnitsProvider
    • Alternate labels defined through the AlternateUnitLabelsProvider
  3. Error Handling: The parser's behavior when encountering unrecognized unit labels depends on the format configuration:

    • Unitless Format (no units defined in format): 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.

Persistence

We expose APIs and interfaces to support persistence of formats. Different from KindOfQuantity, which enables persistence of formats at the schema level, this section covers persistence at the application level.

FormatSet

FormatSet defines properties necessary to support persistence of a set of Formats.

Each Format defined in a FormatSet need to be mapped to a valid ECName for a KindOfQuantity. During an application's runtime, the Format associated to a KindofQuantity within a FormatSet would take precedence and be used over the default presentation formats of that KindOfQuantity.

  • The unitSystem property uses a UnitSystemKey to specify the unit system for the format set. This provides better type safety and leads to less dependency on activeUnitSystem in IModelApp.quantityFormatter. Tools using the new formatting API can then listen to only the onFormatsChanged event from IModelApp.formatsProvider instead of IModelApp.quantityFormatter.onActiveUnitSystemChanged.

  • The formats property accepts either a FormatDefinition or a string reference to another KindOfQuantity. This allows one format to reference another format's definition, reducing duplication when multiple KindOfQuantities should share the same format specification. For example, "AecUnits.LENGTH": "CivilUnits.LENGTH" allows AecUnits.LENGTH to use the same format from CivilUnits.LENGTH.

The naming convention for a valid format within a FormatSet is : .

Example of a metric-based FormatSet as JSON
{ "name": "metric", "label": "Metric", "unitSystem": "metric", "formats": { "DefaultToolsUnits.LENGTH": { "composite": { "includeZero": true, "spacer": "", "units": [{ "label": "m", "name": "Units.M" }] }, "formatTraits": ["keepSingleZero", "showUnitLabel"], "precision": 4, "type": "Decimal", "decimalSeparator": "." }, "DefaultToolsUnits.ANGLE": { "description": "degrees (labeled) 2 decimal places", "composite": { "includeZero": true, "spacer": "", "units": [{ "label": "°", "name": "Units.ARC_DEG" }] }, "formatTraits": ["keepSingleZero", "showUnitLabel"], "precision": 2, "type": "Decimal", "uomSeparator": "" } } }
Example of a imperial-based FormatSet as JSON
{ "name": "imperial", "label": "Imperial", "unitSystem": "imperial", "formats": { "DefaultToolsUnits.LENGTH": { "composite": { "includeZero": true, "spacer": "", "units": [{ "label": "'", "name": "Units.FT" }, { "label": "\"", "name": "Units.IN" }]}, "formatTraits": ["keepSingleZero", "showUnitLabel"], "precision": 4, "type": "Decimal", }, "DefaultToolsUnits.ANGLE": { "description": "degrees minutes seconds (labeled) 0 decimal places", "composite": { "includeZero": true, "spacer": "", "units": [{ "label": "°", "name": "Units.ARC_DEG" }, { "label": "'", "name": "Units.ARC_MINUTE" }, { "label": "\"", "name": "Units.ARC_SECOND" }] }, "formatTraits": ["keepSingleZero", "showUnitLabel"], "precision": 2, "type": "Decimal", "uomSeparator": "" } } }

Using KindOfQuantities to Retrieve Formats

Building off of FormatSet, Tools and components that format quantities across applications should be linked to a KindOfQuantity and a Persistence Unit. See Domains for available schemas, including DefaultToolsUnits, CivilUnits, and AecUnits, which define many KindOfQuantity values.

The table below lists common measurements with their typical KindOfQuantity and Persistence Unit. This allows tools to request a default KindOfQuantity from IModelApp.formatsProvider and a Persistence Unit from IModelApp.quantityFormatter to create a FormatterSpec for quantity formatting.

Measurement Actual KindOfQuantity (EC Full Name) Persistence Unit
Length DefaultToolsUnits.LENGTH Units.M
Angle DefaultToolsUnits.ANGLE Units.RAD
Area DefaultToolsUnits.AREA Units.SQ_M
Volume DefaultToolsUnits.VOLUME Units.CUB_M
Latitude/Longitude DefaultToolsUnits.ANGLE Units.RAD
Coordinate DefaultToolsUnits.LENGTH_COORDINATE Units.M
Stationing CivilUnits.STATION Units.M
Length (Survey Feet) CivilUnits.LENGTH Units.M
Length (Engineering) AecUnits.LENGTH Units.M
Bearing CivilUnits.BEARING Units.RAD
Time DefaultToolsUnits.TIME Units.S

Examples of Usage

Numeric Format

The example below uses a simple numeric format and generates a formatted string with 4 decimal place precision. For numeric formats there is no conversion to other units; the unit passed in is the unit returned with the unit label appended if showUnitLabel trait is set.

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 Format

For the composite format below, we provide a unit in meters and produce a formatted string showing feet and inches to a precision of 1/8th inch.

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"

Parsing Values

Example Code
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)

Using a FormatsProvider

The example below uses the SchemaFormatsProvider, an implementation of a FormatsProvider, found in ecschema-metadata to format values associated with the length of an object.

Example of Formatting
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric"); const unitsProvider = new SchemaUnitProvider(schemaContext); const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // or unitsProvider.findUnit("m"); // No unit system was provided, and no format was found in the cache so the method will return the first presentation format for the KoQ, which uses KM. const formatProps = await formatsProvider.getFormat("AecUnits.LENGTH"); const format = await Format.createFromJSON("testFormat", unitsProvider, formatProps!); const formatSpec = await FormatterSpec.create("TestSpec", format, unitsProvider, persistenceUnit); const result = formatSpec.applyFormatting(50); // The persistence unit is meters, so this input value is 50 m. // result in formatted value of 50 m

The example below uses the SchemaFormatsProvider, an implementation of a FormatsProvider, found in ecschema-metadata to parse values associated with the length of an object.

Example of Parsing
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric"); const unitsProvider = new SchemaUnitProvider(schemaContext); const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // or unitsProvider.findUnit("m"); const formatProps = await formatsProvider.getFormat("AecUnits.LENGTH_LONG"); const format = await Format.createFromJSON("testFormat", unitsProvider, formatProps!); const parserSpec = await ParserSpec.create(format, unitsProvider, persistenceUnit); const result = parserSpec.parseToQuantityValue("50 km"); // result.value 50000 (value in meters)

When retrieving a format from a schema, users might want to ensure the format they get matches the unit system they are currently using. They can either pass in the unit system on initialization, or change them after initialization, like so:

Example of Formatting with Unit System
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric"); const unitsProvider = new SchemaUnitProvider(schemaContext); const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // or unitsProvider.findUnit("m"); formatsProvider.unitSystem = "imperial"; // This will cause the method to return the first presentation format for the KoQ that uses imperial units. const formatProps = await formatsProvider.getFormat("AecUnits.LENGTH_LONG"); const format = await Format.createFromJSON("testFormat", unitsProvider, formatProps!); const formatSpec = await FormatterSpec.create("TestSpec", format, unitsProvider, persistenceUnit); const result = formatSpec.applyFormatting(50); // The persistence unit is meters, so this input value is 50 m. // result in formatted value of 164'0 1/2"

Retrieving a FormatProp, and a PersistenceUnit with only a KindOfQuantity Name and through schemas

When working with formats, developers often need to retrieve a format and determine the appropriate persistence unit. When you only have a KindOfQuantity name, you can utilize a SchemaContext to find the schema item for that KindOfQuantity and then access its persistence unit:

Using a SchemaContext to get KindOfQuantity and a persistence unit
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric"); const unitsProvider = new SchemaUnitProvider(schemaContext); const kindOfQuantityName = "AecUnits.LENGTH"; // Get the format definition const formatDef = await formatsProvider.getFormat(kindOfQuantityName); if (!formatDef) throw new Error(`Format not found for ${kindOfQuantityName}`); const kindOfQuantity = await schemaContext.getSchemaItem(kindOfQuantityName, KindOfQuantity); if (!kindOfQuantity) throw new Error(`KindOfQuantity not found for ${kindOfQuantityName}`); const persistenceUnit = kindOfQuantity.persistenceUnit; if (!persistenceUnit) throw new Error(`Persistence unit not found for ${kindOfQuantityName}`); const persistenceUnitProps = await unitsProvider.findUnitByName(persistenceUnit.fullName); const format = await Format.createFromJSON(formatDef.name ?? "", unitsProvider, formatDef); const formatterSpec = await FormatterSpec.create( formatDef.name ?? "", format, unitsProvider, // Use a schema units provider persistenceUnitProps ); const _formattedValue = formatterSpec.applyFormatting(123.45);

Using a MutableFormatsProvider

The example below is of a MutableFormatsProvider that lets you add/remove formats during runtime.

Example of a MutableFormatsProvider implementation
/** * Implements a formats provider with a cache, to allow adding/removing formats at runtime. */ class ExampleFormatProvider implements MutableFormatsProvider { private _cache: Map<string, FormatDefinition> = new Map(); public onFormatsChanged = new BeEvent<(args: FormatsChangedArgs) => void>(); public async getFormat(name: string): Promise<FormatDefinition | undefined> { return this._cache.get(name); } public async addFormat(name: string, format: FormatDefinition): Promise<void> { this._cache.set(name, format); this.onFormatsChanged.raiseEvent({ formatsChanged: [name]}); } public async removeFormat(name: string): Promise<void> { this._cache.delete(name); this.onFormatsChanged.raiseEvent({ formatsChanged: [name]}); } }
const formatsProvider = new ExampleFormatProvider(); const format: FormatDefinition = { label: "NewFormat", type: "Fractional", precision: 8, formatTraits: ["keepSingleZero", "showUnitLabel"], uomSeparator: "", }; await formatsProvider.addFormat("DefaultToolsUnits.LENGTH", format); const retrievedFormat = await formatsProvider.getFormat("DefaultToolsUnits.LENGTH"); // retrievedFormat is the format we just added.

Using a FormatSetFormatsProvider

The FormatSetFormatsProvider provides a convenient way to manage formats within a FormatSet while supporting runtime modifications. This provider is particularly useful when you need to persist format changes or override default schema formats.

Key Features:

  • String Reference Resolution: The provider now automatically resolves string references to their target FormatDefinition. When a format references another via string (e.g., "DefaultToolsUnits.LENGTH": "CivilUnits.LENGTH"), calling getFormat("DefaultToolsUnits.LENGTH") will resolve and return the actual FormatDefinition from CivilUnits.LENGTH.
  • Chain Resolution: Supports chains of references with circular reference detection (e.g., HEIGHT → DISTANCE → LENGTH).
  • Cascade Notifications: When adding or removing a format, the onFormatsChanged event now includes not only the modified format but also all formats that reference it (directly or indirectly). For example, if CivilUnits.LENGTH is updated and both AecUnits.LENGTH and DefaultToolsUnits.LENGTH reference it, all three formats will be included in the formatsChanged array, enabling proper cache invalidation.
  • Fallback Provider: String references can resolve through the optional fallback provider if the target format isn't found in the format set.

Here's a working example that demonstrates string reference resolution with formatting:

Example of using FormatSetFormatsProvider
const unitsProvider = new SchemaUnitProvider(schemaContext); const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // Create a format set with a base format and string references const formatSet = { name: "MyFormatSet", label: "My Custom Formats", unitSystem: "metric" as const, formats: { // Base format definition "CivilUnits.LENGTH": { composite: { includeZero: true, spacer: " ", units: [{ label: "m", name: "Units.M" }] }, formatTraits: ["keepSingleZero", "showUnitLabel"], precision: 2, type: "Decimal" } as FormatDefinition, // DISTANCE references LENGTH via string "DefaultToolsUnits.LENGTH": "CivilUnits.LENGTH", } }; // Create the provider const formatsProvider = new FormatSetFormatsProvider({ formatSet }); // Getting AecUnits.LENGTH resolves to the RoadRailUnits.LENGTH format definition const lengthFormat = await formatsProvider.getFormat("DefaultToolsUnits.LENGTH"); const format = await Format.createFromJSON("length", unitsProvider, lengthFormat!); const formatSpec = await FormatterSpec.create("LengthSpec", format, unitsProvider, persistenceUnit); const result = formatSpec.applyFormatting(42.567); // result is "42.57 m"

Registering a SchemaFormatsProvider on IModelConnection open

The simplest way to get formats from schemas into an iTwin application is to register a new SchemaFormatsProvider through a IModelConnection.onOpen event listener, passing IModelConnection.schemaContext to the provider. The example below illustrates how that can be done.

Example of registering a SchemaFormatsProvider on IModelConnection open
const removeIModelConnectionListener = IModelConnection.onOpen.addListener((iModel: IModelConnection) => { if (iModel.isBlankConnection()) return; // Don't register on blank connections. const schemaFormatsProvider = new SchemaFormatsProvider(iModel.schemaContext, IModelApp.quantityFormatter.activeUnitSystem); const removeUnitSystemListener = IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener((args) => { schemaFormatsProvider.unitSystem = args.system; }); iModel.onClose.addOnce(() => { removeUnitSystemListener(); }); IModelApp.formatsProvider = schemaFormatsProvider; }); IModelConnection.onClose.addOnce(() => { removeIModelConnectionListener(); IModelApp.resetFormatsProvider(); });

Mathematical Operation Parsing

The quantity formatter supports parsing mathematical operations. The operation is solved, formatting each value present, according to the specified format. This makes it possible to process several different units at once.

Example Code
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(-) signs are supported for now. Other operators will end up returning a parsing error or an invalid input result. If a Format uses a spacer that conflicts with one of the operators above, additional restrictions will apply:

  1. Mathematical operations only apply when the operator is in front of whitespace. So -2FT 6IN + 6IN is equal to -2FT-6IN + 6IN, and -2FT-6IN - 6IN is not equal to -2FT-6IN- 6IN.
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
  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.
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

Last Updated: 04 February, 2026