UnitsML is a markup language for representing units of measurement in a machine-readable format.
It is designed to be used in various applications, including scientific computing, data analysis, and engineering.
The UnitsML Ruby library provides the following components:
XML syntax for:
-
representing units and quantity information Composite units
-
Parser for the UnitsML parser - Parses UnitsML expressions.
-
Unit Converter - Converts units between formats.
-
Formatter - Formats units for display in various formats.
The UnitsML Ruby library is a gem for parsing, manipulating, and converting unit expressions based on the Units Markup Language (UnitsML) specification. This library provides functionality to handle units of measurement in a standardized way across different formats.
The UnitsML Ruby library consists of several key components:
-
Parser: Parses string expressions into structured data
-
Transform: Transforms parsed data into domain objects
-
Formula: Represents compound unit expressions
-
Unit: Represents individual units
-
Prefix: Represents unit prefixes (k, m, μ, etc.)
-
Dimension: Represents physical dimensions
The library uses the unitsdb-ruby gem as the primary runtime source for standard units, prefixes, and dimensions.
The development Gemfile pins the current integration branches for
lutaml-model, mml, plurimath, and unitsdb-ruby. If you need to test
coordinated local changes across those repositories, temporarily switch those
dependencies to Bundler path entries that point at sibling checkouts, for
example ../lutaml-model or ../unitsdb-ruby. Keep those local path changes
out of commits unless the branch intentionally requires them.
The standard Ruby runtime path goes through ::Unitsdb.database. When
unitsdb-ruby cannot load its bundled data/ directory, UnitsML falls back to
the packaged YAML files in the unitsdb-ruby gem’s vendor/unitsdb
directory, rather than a vendor/unitsdb directory in this repository.
The UnitsML Ruby library supports AsciiUnits, a text-based syntax for representing units of measurement in a compact and human-readable format.
The UnitsML Ruby library supports all available units defined in the official UnitsDB database.
A basic unit is a unit that has an entry in the UnitsDB database with a unique identifier.
Basic units are represented in the library as Unitsml::Unit objects.
require 'unitsml'
unit = Unitsml.parse("m") # Parse a unit expression
unit.to_s # => "m"
unit.to_xml
# =>
<Unit xmlns="https://schema.unitsml.org/unitsml/1.0" xml:id="U_NISTu1" dimensionURL="#NISTd1">
<UnitSystem name="SI" type="SI_derived" xml:lang="en"/>
<UnitName xml:lang="en">meter</UnitName>
<UnitSymbol type="HTML">m</UnitSymbol>
<UnitSymbol type="MathMl">
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<mi mathvariant="normal">m</mi>
</math>
</UnitSymbol>
</Unit>
<Dimension xmlns="https://schema.unitsml.org/unitsml/1.0" xml:id="NISTd1">
<Length symbol="L" powerNumerator="1"/>
</Dimension>A prefixed unit is a unit that has a prefix (like kilo, milli, micro, etc.) that modifies its value.
Prefixed units are represented in the library as Unitsml::Prefix objects.
The library supports the full range of prefixes defined in the UnitsDB database.
require 'unitsml'
unit = Unitsml.parse("um") # Parse a unit expression
unit.to_s # => "um"
unit.to_xml
# =>
<Unit xmlns="https://schema.unitsml.org/unitsml/1.0" xml:id="U_um" dimensionURL="#NISTd1">
<UnitSystem name="SI" type="SI_derived" xml:lang="en"/>
<UnitName xml:lang="en">um</UnitName>
<UnitSymbol type="HTML">µm</UnitSymbol>
<UnitSymbol type="MathMl"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<mi mathvariant='normal'>µm</mi>
</math>
</UnitSymbol>
<RootUnits>
<EnumeratedRootUnit unit="meter" prefix="u"/>
</RootUnits>
</Unit>
<Prefix xmlns="https://schema.unitsml.org/unitsml/1.0" prefixBase="10" prefixPower="-6" xml:id="NISTp10_-6">
<PrefixName xml:lang="en">micro</PrefixName>
<PrefixSymbol type="ASCII">u</PrefixSymbol>
<PrefixSymbol type="unicode">μ</PrefixSymbol>
<PrefixSymbol type="LaTeX">$mu$</PrefixSymbol>
<PrefixSymbol type="HTML">µ</PrefixSymbol>
</Prefix>
<Dimension xmlns="https://schema.unitsml.org/unitsml/1.0" xml:id="NISTd1">
<Length symbol="L" powerNumerator="1"/>
</Dimension>Prefixed units: mm, kg, μm
A composite unit is a unit that is derived from two or more basic units conjoined by multiplication or division.
Composite units are created using the Unitsml.parse method, or built
programmatically (see below).
Compound units with operations: kg*s^-2 Units with powers: C^3 Square roots: sqrt(Hz) Units with prefixes: m/s, m2/s2
When the parts are already available as data rather than a string, composite units can be built without parsing.
Multiplication (*) and division (/) compose units, dimensions and formulas,
returning a Unitsml::Formula. Like the parser, / keeps the "/" separator
and inverts the right-hand operand’s power, so a single division renders
identically to the parsed string (W / m == Unitsml.parse("W/m")):
# watt per metre per steradian, rendered "W/m^-1/sr^-1"
Unitsml::Unit.new("W") / Unitsml::Unit.new("m") / Unitsml::Unit.new("sr")
# m/s^2; powers come from the constructor's second argument
Unitsml::Unit.new("m") / Unitsml::Unit.new("s", 2)
# prefixes via the `prefix:` keyword
Unitsml::Unit.new("m", prefix: "k") # kmThe same composition can be written as a fluent chain — #unit/#dimension are
sugar over (unit("m", -1) == Unit.new("m", -1)), and
#quantity_reference/#name/#multiplier attach metadata. Metadata must come
last. Each chained term, and each * or /, builds a fresh Formula that does
not carry earlier metadata. The same holds for the comma metadata of a parsed
formula: Unitsml.parse("W*m^-1, quantity: radiance") * Unitsml::Unit.new("sr")
has no quantity.
Unitsml::Unit.new("W").unit("m", -1).unit("sr", -1).quantity_reference("radiance")#quantity_reference names the NIST quantity the unit measures, emitted as a
<Quantity> element by to_xml. It is the fluent spelling of the quantity:
keyword of Unitsml.compose and of the format methods (see below), and is
unrelated to Unitsml::Quantity, the value-with-unit class described in the
Quantities section below.
An explicit separator can be placed between terms with #extender (alias
#ext), accepting exactly the parser’s grammar set: "*", "/" or "//"
(any other glyph is a display concern — use the multiplier: render option).
It is glyph-only: nothing is negated or transformed, so exponents are stated
explicitly, exactly as a parsed string stores them:
# byte-identical to Unitsml.parse("W/m") in every format
Unitsml::Unit.new("W").extender("/").unit("m", -1)
# mixed separators, as in Unitsml.parse("W*m/W")
Unitsml::Unit.new("W").ext("*").unit("m").ext("/").unit("W", -1)The glyph may be a String, a Symbol, or an Unitsml::Extender object; an
Extender also works directly as an operator operand:
Unitsml::Unit.new("W") * Unitsml::Extender.new("/") * Unitsml::Unit.new("m", -1)A separator must sit between two operands. An expression left ending in a
separator, or with two separators together, has no valid rendering and raises
Unitsml::Errors::MisplacedExtenderError when a to_* method is called (the
intermediate chain value is still usable — only rendering it is rejected):
Unitsml::Unit.new("W").ext("/").to_latex # raises MisplacedExtenderError
Unitsml::Unit.new("W").ext("/").unit("m", -1).to_latex # ok — "W/m^-1"A unit is referenced by its symbol id, exactly as in a parsed string
(e.g. "W"). Dimensions compose the same way
(Unitsml::Dimension.new("dim_L")), but units and dimensions cannot be mixed in
one expression.
The Unitsml.compose keyword form builds the same Formula from a list. Pass
either units: or dimensions: (not both). A units: entry is a symbol id, a
{ unit:, power:, prefix: } hash, or a pre-built Unitsml::Unit; a
dimensions: entry is a dimension id or a { dimension:, power: } hash:
Unitsml.compose(
units: ["W", { unit: "m", power: -1 }, { unit: "sr", power: -1 }],
quantity: "radiance",
)
Unitsml.compose(dimensions: ["dim_L", { dimension: "dim_M", power: 2 }])The quantity, name and multiplier metadata are also accepted directly by
the format methods. They apply to to_xml (quantity/name are ignored by the
other formats), and a value passed at render time overrides one carried by a
parsed comma-metadata expression:
formula = Unitsml.compose(units: ["W", { unit: "m", power: -1 }])
formula.to_xml(quantity: "radiance") # emits a <Quantity> elementThe fluent #unit/#dimension chain and the keyword form join terms with ,
turning division into a negative exponent, so they render like W*m^-1. (The
/ operator and explicit #extender/#ext separators keep their glyph
instead — see above.) Where terms are -joined, the separator is a display
choice — pass the multiplier: argument to any to_* method (:space,
:nospace, or a custom string such as "·") to change it:
formula = Unitsml::Unit.new("W").unit("m", -1)
formula.to_asciimath # => "W*m^-1"
formula.to_asciimath(multiplier: :space) # => "W m^-1"
formula.to_asciimath(multiplier: "·") # => "W·m^-1"Unitsml.compose and the fluent chain validate every reference, power and
prefix, and the name and multiplier metadata. They raise a
Unitsml::Errors::* (all subclasses of Unitsml::Errors::BaseError) for
anything they cannot resolve: an unknown unit, dimension or prefix, a
units/dimensions mix, an empty composition, a power the parser cannot express
(a decimal exponent — use a Rational), or a non-string name/multiplier.
Unit.new/Dimension.new validate their own reference, prefix and power on
construction too, so an unresolvable value raises immediately whichever surface
you use. A Unitsml::Number power must hold a number (Number.new("abc")
raises).
The quantity metadata is looked up when to_xml runs, by NIST id (such as
"NISTq1") or by English name (such as "radiance"). A value that matches no
quantity is not an error: to_xml leaves out the <Quantity> element.
A composed Formula keeps its original term tree for rendering, so to_latex,
to_xml and the other formats are not affected by the text described here.
Its norm_text is a flat list of the unit leaves joined by *, with each
stored power written in parentheses. This text parses back into the same units,
prefixes and power values. A single division of two plain units keeps the
familiar form. A square root is written as its exponent, so a composed
sqrt(Hz) becomes Hz^(5/10): the parsed text has the same power, but renders
without the root sign.
(Unitsml::Unit.new("m") / Unitsml::Unit.new("s")).norm_text # => "m/s"
(Unitsml::Unit.new("W") / Unitsml::Unit.new("m") / Unitsml::Unit.new("sr")).norm_text
# => "W*m^(-1)*sr^(-1)"The norm_text of a parsed Formula is unchanged.
Unitsml::Quantity pairs a value with a unit. The unit can be given with the
unit: keyword as a string, a Unitsml::Unit or a Unitsml::Formula. A string
is parsed. A Unit or Formula is used without parsing. The older
unit_expression: keyword still accepts a string. Give only one of the two
keywords. Without a unit, or with unit: nil, the quantity is a pure number.
unit: does not take a bare Unitsml::Dimension; build a dimension quantity
from a Formula, for example
unit: Unitsml.compose(dimensions: ["dim_L"]).
Unitsml::Quantity.new(value: 10, unit: "kV")
Unitsml::Quantity.new(value: 10, unit: Unitsml::Unit.new("V", prefix: "k"))
Unitsml::Quantity.new(value: 3, unit: Unitsml::Unit.new("m") / Unitsml::Unit.new("s"))
Unitsml::Quantity.new(value: 10, unit_expression: "kV")A Formula passed in is kept as the same object, so changing it later also
changes the quantity. Results of arithmetic always get their own copy.
Multiplying or dividing a Unit, Dimension or Formula by a number, on
either side, gives a Unitsml::Quantity with that number as its value. A
quantity can also be multiplied or divided by a bare Unit, Dimension or
Formula, which counts as a value of 1. Nothing is parsed. Units and
dimensions still cannot be mixed, and anything else raises
Unitsml::Errors::InvalidUnitEntryError.
metre = Unitsml::Unit.new("m")
second = Unitsml::Unit.new("s")
5 * metre # => Quantity, value 5, unit m
metre / 2.0 # => Quantity, value 0.5, unit m
5 / second # => Quantity, value 5, unit s^-1
distance = Unitsml::Quantity.new(value: 6, unit: metre)
distance / second # => Quantity, value 6, unit m/sTwo units without a number still compose into a Formula, as described above.
Multiplying or dividing two quantities composes their units with the same
builder as Unit#* and Unit#/, and nothing is reparsed from text. The
resulting norm_text follows the rules above:
w_per_m = Unitsml::Quantity.new(value: 6, unit: "W/m")
s = Unitsml::Quantity.new(value: 2, unit: "s")
(w_per_m * s).norm_text # => "W*m^(-1)*s"Some behavior is unchanged and is tracked in issue #82. Fractional powers and
square roots are not handled exactly in dimension_vector and dimension_id.
Dimension leaves such as dim_L do not contribute to dimension_vector, and
dividing by a dimension does not invert it. Adding or subtracting two interval
scale quantities still returns a ratio scale result. Quantities with a unit and
a dimension cannot be combined and raise Unitsml::Errors::MixedTermsError.
The library supports representation of UnitsML-encoded units in various formats.
These formats include:
-
MathML
-
LaTeX
-
AsciiMath
-
HTML
-
Unicode
-
XML (in UnitsML)
Units can be converted to multiple formats as follows:
unit = Unitsml.parse("m/s")
unit.to_mathml # MathML representation
unit.to_latex # LaTeX representation
unit.to_asciimath # AsciiMath representation
unit.to_html # HTML representation
unit.to_unicode # Unicode representation
unit.to_xml # XML (in UnitsML) representationWhen converting units to formats like MathML or LaTeX, you can specify whether to use explicit parentheses for clarity in complex expressions.
A pair of parentheses is typically used for two purposes:
-
For grouping to indicate operator precedence in mathematical expressions. When expressing units, most of the time, parentheses are used to indicate operator precedence, parentheses are only kept in cases where it indicates precedence. When parentheses are not needed for precedence, such as in the expression
(m/s), the parentheses can be removed whilch simplifies the expression intom/s. -
For indicating that an expression is a compound unit. These parentheses are explicit parentheses that cannot be removed without changing the meaning of the expression.
UnitsML Ruby provides an option explicit_parenthesis to control the inclusion
of parentheses in the output.
In an expression:
-
(…)is considered an implicit parenthesis for grouping and operator precedence and can be removed without changing the meaning of the expression. -
…is considered an explicit parenthesis, which indicates that the parentheses need to be preserved in the output regardless of operator precedence.
The explicit_parenthesis option allows you to control how parentheses are
considered in the output formats. It can be set to true or false.
Syntax for using explicit_parenthesis is as follows:
unit.to_asciimath(explicit_parenthesis: true) # or false
unit.to_mathml(explicit_parenthesis: true) # or false
unit.to_latex(explicit_parenthesis: true) # or falseWhen explicit_parenthesis is set to:
true-
(default) the usage of
(…)and…is differentiated as implicit and explicit parentheses, respectively. false-
all parentheses are treated as grouping parentheses, there is no difference between
(…)and….
explicit_parenthesis is true and double parentheses are considered explicit parenthesesUnitsml.parse("((m/s))").to_asciimath
> "(m/s^-1)" # Only one of the two pairs of parentheses are included in the outputIf an extra pair of parentheses is included, it will be considered as grouping parentheses and will not be included in the Unitsml::Formula or the output.
explicit_parenthesis is true and double parentheses are considered explicit parenthesesUnitsml.parse("(((m/s)))").to_asciimath
> "(m/s^-1)" # the third pair of parentheses are ignored as grouping parentheses
Unitsml.parse("(((((m/s)))))").to_asciimath
> "((m/s^-1))" # the fifth pair of parentheses are ignored as grouping parenthesesunit = Unitsml.parse("((m/s))")
unit.to_asciimath # equivalent to (explicit_parenthesis: true) => "(m/s^-1)"
unit = Unitsml.parse("((m/s))*((m/s))")
unit.to_asciimath(explicit_parenthesis: true) # "(m/s^-1)*(m/s^-1)"When explicit_parenthesis is false, all parentheses are treated as grouping parentheses
and therefore will be reduced to only the necessary parentheses for operator precedence.
explicit_parenthesis is false, all parentheses are treated as grouping parenthesesunit = Unitsml.parse("(m/s)")
unit.to_asciimath(explicit_parenthesis: false) # => "m/s"
unit = Unitsml.parse("((m/s))*((m/s))")
unit.to_asciimath(explicit_parenthesis: false) # "m/s^-1*m/s^-1"The native JavaScript implementation of UnitsML is the unitsml package
on npm (unitsml/unitsml-ts) — a
TypeScript implementation on the shared checksummed
PARG grammar artifact, with
asciiunits and unitsdb as its npm dependencies. Ruby and TypeScript
share conformance corpora (parse and render oracles) to guarantee
identical behavior.
The former Opal-compiled @unitsml/unitsml package is deprecated; its
repository (unitsml/unitsml-js) is archived.
Add this line to your application’s Gemfile:
gem 'unitsml'And then execute:
$ bundle installOr install it yourself with:
$ gem install unitsmlUnitsML Ruby provides functionality to represent and convert units between different formats including MathML, LaTeX, AsciiMath, HTML, Unicode, and XML(Unitsml schema based elements).
Units can be converted to different formats:
unit = Unitsml.parse("m/s") # Create a unit example
# Convert to different formats
unit.to_mathml
unit.to_latex
unit.to_asciimath
unit.to_html
unit.to_unicode
unit.to_xmlAn Extender is the symbol between units (like multiplication or division):
formula = Unitsml.parse("m*m") # the '*' is the extender
formula.to_mathml<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<mi mathvariant="normal">m</mi>
<mo>⋅</mo>
<mi mathvariant="normal">m</mi>
</math>The output of extenders can be customized by passing the multiplier argument:
formula = Unitsml.parse("m/m")
formula.to_asciimath(multiplier: "·") # the '·' is the extender for the output
> "m·m^-1"The multiplier argument is supported in all supported conversions.
This gem requires the Plurimath gem to provide to_plurimath method
functionality. You need to explicitly install it, please follow the installtion
instructions in the
Plurimath
repository.
After checking out the repo, run bin/setup to install dependencies. Then, run
bundle exec spec to run the tests. You can also run bin/console for an
interactive prompt that will allow you to experiment.
When developing or contributing to UnitsML Ruby, you’ll need to ensure the submodules are available. After cloning the repository, verify that the submodules exist in the directory.
If not, populate the submodules by running:
$ git submodule update --init --recursiveunitsdb is the only submodule required in this gem for now.
Everyone interacting in the Unitsml project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
This project is licensed under the BSD 2-clause License.
See the LICENSE.md file for details.
Copyright Ribose.