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"
anddegree°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 theUnitsProvider
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 enter43in
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 specifiedFormat
— and a single magnitude number, theFormatter
can convert this number into a text string, adhering to the properties specified informatTraits
. - Parser - A class that holds methods to parse a text string into a single number. Given a
ParserSpec
object containing aFormat
Units
and their unit conversions, as well as an input string, the Parser can either return an objectQuantityParseResult
that contains the magnitude of typenumber
, or an objectParseQuantityError
.
FormatProps
For a detailed description of all the setting supported by FormatProp see the EC documentation on Format.
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.
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.
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 naming convention for a valid format within a FormatSet is
: .
Example of a metric-based FormatSet as JSON
{
"name": "metric",
"label": "Metric",
"formats": {
"AecUnits.LENGTH": {
"composite": {
"includeZero": true,
"spacer": "",
"units": [{ "label": "m", "name": "Units.M" }]
},
"formatTraits": ["keepSingleZero", "showUnitLabel"],
"precision": 4,
"type": "Decimal",
"decimalSeparator": "."
},
"AecUnits.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",
"formats": {
"AecUnits.LENGTH": {
"composite": {
"includeZero": true,
"spacer": "",
"units": [{ "label": "'", "name": "Units.FT" }, { "label": "\"", "name": "Units.IN" }]},
"formatTraits": ["keepSingleZero", "showUnitLabel"],
"precision": 4,
"type": "Decimal",
},
"AecUnits.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": ""
}
}
}
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"
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("AecUnits.LENGTH", format); // Add a format with the name "AecUnits.LENGTH".
const retrievedFormat = await formatsProvider.getFormat("AecUnits.LENGTH");
// retrievedFormat is the format we just added.
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:
- 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
- For a value like
2FT 6IN-0.5
, the-
sign will be treated as a spacer and not subtraction. However, the0.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
andIN
.
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: 15 April, 2025