Note that :ex_cldr_units
requires Elixir 1.6 or later.
Add ex_cldr_units
as a dependency to your mix
project:
defp deps do
[
{:ex_cldr_units, "~> 3.0"}
]
end
then retrieve ex_cldr_units
from hex:
mix deps.get
mix deps.compile
Documentation can be found at https://hexdocs.pm/ex_cldr_units.
ex_cldr_units
is an add-on library for ex_cldr that provides localisation and formatting for units such as weights, lengths, areas, volumes and so on. It also provides unit conversion and simple arithmetic for compatible units.
From ex_cldr version 2.0, a backend module must be defined into which the public API and the CLDR data is compiled. See the ex_cldr documentation for further information on configuration.
In the following examples we assume the presence of a module called MyApp.Cldr
defined as:
defmodule MyApp.Cldr do
use Cldr,
locales: ["en", "fr"],
default_locale: "en",
providers: [Cldr.Number, Cldr.Unit, Cldr.List]
end
The String.Chars
protocol underpins Kernel.to_string/1
and is also used in string interpolation such as #{my_unit}
. In order for this to be supported by Cldr.Unit
, a default backend module must be configured in config.exs
. For example:
config :ex_cldr_units,
default_backend: MyApp.Cldr
The primary api is defined by three functions:
-
MyApp.Cldr.Unit.to_string/2
for formatting units -
MyApp.Cldr.Unit.new/2
to create a newUnit.t
struct that encapsulated a unit and a value that can be used for arithmetic, comparison and conversion -
MyApp.Cldr.Unit.convert/2
to convert one compatible unit to another -
MyApp.Cldr.Unit.localize/3
to localize a unit by converting it to units customary for a given territory -
MyApp.Cldr.Unit.add/2
,MyApp.Cldr.Unit.sub/2
,MyApp.Cldr.Unit.mult/2
,MyApp.Cldr.Unit.div/2
provide basic arithmetic operations on compatibleUnit.t
structs.
A Cldr.Unit.t()
struct is created with the Cldr.Unit.new/2
function. The two parameters are a unit name and a number (expressed as a float
, integer
, Decimal
or Ratio
) in either order.
Naming units is quite flexible combining:
-
One or more base unit names. These are the names returned from
Cldr.Unit.known_units/0
-
An optional SI prefix (from
yokto
toyotta
) -
An optional power prefix of
square
orcubic
Names can be expressed as strings with any of -
, _
or
as separators between words.
Some examples:
iex> Cldr.Unit.new :meter, 1
{:ok, #Cldr.Unit<:meter, 1>}
iex> Cldr.Unit.new "square meter", 1
{:ok, #Cldr.Unit<:square_meter, 1>}
iex> Cldr.Unit.new "square liter", 1
{:ok, #Cldr.Unit<"square_liter", 1>}
iex> Cldr.Unit.new "square yottaliter", 1
{:ok, #Cldr.Unit<"square_yottaliter", 1>}
iex> Cldr.Unit.new "cubic light year", 1
{:ok, #Cldr.Unit<"cubic_light_year", 1>}
iex> Cldr.Unit.new "squre meter", 1
{:error,
{Cldr.UnknownUnitError, "Unknown unit was detected at \"squre_meter\""}}
You will note that the unit make not make logical sense (cubic light-year
?) but they do make mathematical sense.
Units can also be described as the product of one or more base units. For example:
iex> Cldr.Unit.new "liter ampere", 1
{:ok, #Cldr.Unit<"ampere_liter", 1>}
iex> Cldr.Unit.new "mile lux", 1
{:ok, #Cldr.Unit<"mile_lux", 1>}
Again, this may not have a logical meaning but they do have an arithmetic meaning and they can be formatted as strings:
iex> Cldr.Unit.new!("liter ampere", 1) |> Cldr.Unit.to_string
{:ok, "1 ampere⋅litre"}
iex> Cldr.Unit.new!("mile lux", 3) |> Cldr.Unit.to_string
{:ok, "3 miles⋅lux"}
Lastly, there are units formed by division where are called "per" units. For example:
iex> Cldr.Unit.new "mile per hour", 1
{:ok, #Cldr.Unit<:mile_per_hour, 1>}
iex> Cldr.Unit.new "liter per second", 1
{:ok, #Cldr.Unit<"liter_per_second", 1>}
iex> Cldr.Unit.new "cubic gigalux per inch", 1
{:ok, #Cldr.Unit<"cubic_gigalux_per_inch", 1>}
MyApp.Cldr.Unit.to_string/2
provides localized unit formatting. It supports two arguments:
-
number
is any number (integer, float or Decimal) or aUnit.t
struct returned byCldr.Unit.new/2
-
options
which are:-
:unit
is any unit returned byCldr.Unit.known_units/0
. This option is required unless aUnit.t
is passed as the first argument. -
:locale
is any configured locale. SeeCldr.known_locale_names/0
. The default islocale: Cldr.get_current_locale()
-
:style
is one of those returned byCldr.Unit.available_styles
. The current styles are:long
,:short
and:narrow
. The default isstyle: :long
-
Any other options are passed to
Cldr.Number.to_string/2
which is used to format thenumber
-
iex> MyApp.Cldr.Unit.to_string 123, unit: :gallon
{:ok, "123 gallons"}
iex> MyApp.Cldr.Unit.to_string 1234, unit: :gallon, format: :long
{:ok, "1 thousand gallons"}
iex> MyApp.Cldr.Unit.to_string 1234, unit: :gallon, format: :short
{:ok, "1K gallons"}
iex> MyApp.Cldr.Unit.to_string 1234, unit: :megahertz
{:ok, "1,234 megahertz"}
iex> MyApp.Cldr.Unit.to_string 1234, unit: :foot, locale: "fr"
{:ok, "1 234 pieds"}
iex> MyApp.Cldr.Unit.to_string Cldr.Unit.new(:ampere, 42), locale: "fr"
{:ok, "42 ampères"}
iex> Cldr.Unit.to_string 1234, MyApp.Cldr, unit: "foot_per_second", style: :narrow, per: :second
{:ok, "1,234′/s"}
iex> Cldr.Unit.to_string 1234, MyApp.Cldr, unit: "foot_per_second"
{:ok, "1,234 feet per second"}
Sometimes its a requirement to decompose a unit into one or more subunits. For example, if someone is 6.3 feet height we would normally say "6 feet, 4 inches". This can be achieved with Cldr.Unit.decompose/2
. Using our example:
iex> height = Cldr.Unit.new(:foot, 6.3)
#Cldr.Unit<:foot, 6.3>
iex(2)> Cldr.Unit.decompose height, [:foot, :inch]
[#Cldr.Unit<:foot, 6.0>, #Cldr.Unit<:inch, 4.0>]
A localised string representing this decomposition can also be produced. Cldr.Unit.to_string/3
will process a unit list, using the function Cldr.List.to_string/2
to perform the list combination. Again using the example:
iex> c = Cldr.Unit.decompose height, [:foot, :inch]
[#Cldr.Unit<:foot, 6.0>, #Cldr.Unit<:inch, 4.0>]
iex> Cldr.Unit.to_string c, MyApp.Cldr
"6 feet and 4 inches"
iex> Cldr.Unit.to_string c, MyApp.Cldr, list_options: [format: :unit_short]
"6 feet, 4 inches"
# And of course full localisation is supported
iex> Cldr.Unit.to_string c, MyApp.Cldr, locale: "fr"
"6 pieds et 4 pouces"
t:Unit
structs can be converted to other compatible units. For example, feet
can be converted to meters
since they are both of the length
unit type.
# Test for unit compatibility
iex> Cldr.Unit.compatible? :foot, :meter
true
iex> Cldr.Unit.compatible? :foot, :liter
false
# Convert a unit
iex> Cldr.Unit.convert Cldr.Unit.new!(:foot, 3), :meter
{:ok, #Cldr.Unit<:meter, 16472365997070327 <|> 18014398509481984>}
Different locales or territories use different measurement systems and sometimes different measurement scales that also vary based upon usage. For example, in the US a person's height is considered in inches
up to a certain point and feet and inches
after that. For distances when driving, the length is considered in yards
for certain distances and miles
after that. For most other countries the same quantity would be expressed in centimeters
or meters
or kilometers
.
ex_cldr_units
makes it easy to take a unit and convert it to the units appropriate for a given locale and usage.
Consider this example:
iex> height = Cldr.Unit.new!(1.81, :meter)
#Cldr.Unit<:meter, 1.81>
iex> us_height = Cldr.Unit.localize height, usage: :person_height, territory: :US
[#Cldr.Unit<:foot, 5>,
#Cldr.Unit<:inch, 1545635392113553812 <|> 137269716642252725>]
iex> Cldr.Unit.to_string us_height
{:ok, "5 feet and 11.26 inches"}
Note that conversion is dependent on context. The context above is :person_height
reflecting that we are referring to the height of a person. For units of length
category, the other contexts available are :rainfall
, :snowfall
, :vehicle
, :visibility
and :road
. Using the above example with the context of :rainfall
we see
iex> length = Cldr.Unit.localize height, usage: :rainfall, territory: :US
[#Cldr.Unit<:inch, 9781818390648717312 <|> 137269716642252725>]
iex> Cldr.Unit.to_string length
{:ok, "71.26 inches"}
See Cldr.Unit.preferred_units/3
to see what mappings are available, in particular what context usage is supported for conversion.
Basic arithmetic is provided by Cldr.Unit.add/2
, Cldr.Unit.sub/2
, Cldr.Unit.mult/2
, Cldr.Unit.div/2
as well as Cldr.Unit.round/3
iex> Cldr.Unit.Math.add Cldr.Unit.new!(:foot, 1), Cldr.Unit.new!(:foot, 1)
#Cldr.Unit<:foot, 2>
iex> Cldr.Unit.Math.add Cldr.Unit.new!(:foot, 1), Cldr.Unit.new!(:mile, 1)
#Cldr.Unit<:foot, 5280.945925937846>
iex> Cldr.Unit.Math.add Cldr.Unit.new!(:foot, 1), Cldr.Unit.new!(:gallon, 1)
{:error, {Cldr.Unit.IncompatibleUnitError,
"Operations can only be performed between units of the same type. Received #Cldr.Unit<:foot, 1> and #Cldr.Unit<:gallon, 1>"}}
iex> Cldr.Unit.round Cldr.Unit.new(:yard, 1031.61), 1
#Cldr.Unit<:yard, 1031.6>
iex> Cldr.Unit.round Cldr.Unit.new(:yard, 1031.61), 1, :up
#Cldr.Unit<:yard, 1031.7>
Available units are returned by Cldr.Unit.known_units/0
.
iex> Cldr.Unit.known_units
[:acre, :acre_foot, :ampere, :arc_minute, :arc_second, :astronomical_unit, :bit,
:bushel, :byte, :calorie, :carat, :celsius, :centiliter, :centimeter, :century,
:cubic_centimeter, :cubic_foot, :cubic_inch, :cubic_kilometer, :cubic_meter,
:cubic_mile, :cubic_yard, :cup, :cup_metric, :day, :deciliter, :decimeter,
:degree, :fahrenheit, :fathom, :fluid_ounce, :foodcalorie, :foot, :furlong,
:g_force, :gallon, :gallon_imperial, :generic, :gigabit, :gigabyte, :gigahertz,
:gigawatt, :gram, :hectare, :hectoliter, :hectopascal, :hertz, :horsepower,
:hour, :inch, ...]
Units are grouped by unit category which defines the convertibility of different types. In general, units of the same category are convertible to each other. The function Cldr.Unit.known_unit_categories/0
returns the unit categories.
iex> Cldr.Unit.known_unit_categories
[:acceleration, :angle, :area, :concentr, :consumption, :coordinate, :digital,
:duration, :electric, :energy, :frequency, :length, :light, :mass, :power,
:pressure, :speed, :temperature, :volume]
See also Cldr.Unit.known_units_by_category/0
and Cldr.Unit.known_units_for_category/1
.
Units generally fall into one of three measurement systems in use around the world. In CLDR these are known as :metric
, :ussystem
and :uksystem
. The following functions allow identifying measurement systems for units, territories and locales.
-
The measurement systems are returned with
Cldr.Unit.known_measurement_systems/0
. -
The measurement systems for a given unit are returned by
Cldr.Unit.measurement_systems_for_unit/1
. -
A boolean indicating membership in a given measurement system is returned by
Cldr.Unit.measurement_system?/2
. -
All units belonging to a measurement system are returned by
Cldr.Unit.measurement_system_units/1
. -
The measurement system in use for a given territory is returned by
Cldr.Unit.measurement_system_for_territory/1
. -
The measurement system in use for a given locale is returned by
Cldr.Unit.measurement_system_from_locale/1
.
Knowledge of the measurement system in place for a given user helps create a better user experience. For example, a user who prefers units of measure in the US system can be shown different but compatible units from a user who prefers metric units.
In this example, the list of units in the volume category are filtered based upon the users preference as expressed by their locale.
# For a user preferring US english
iex> system = Cldr.Unit.measurement_system_from_locale "en"
:ussystem
iex> {:ok, units} = Cldr.Unit.known_units_for_category(:volume)
iex> Enum.filter(units, &Cldr.Unit.measurement_system?(&1, system))
[:dessert_spoon, :cup, :drop, :dram, :cubic_foot, :teaspoon, :tablespoon,
:cubic_inch, :bushel, :quart, :pint, :cubic_yard, :cubic_mile, :fluid_ounce,
:pinch, :barrel, :jigger, :gallon, :acre_foot]
# For a user preferring australian english
iex> system = Cldr.Unit.measurement_system_from_locale "en-AU"
:metric
iex> Enum.filter(units, &Cldr.Unit.measurement_system?(&1, system))
[:cubic_centimeter, :centiliter, :cubic_meter, :pint_metric, :megaliter,
:cubic_kilometer, :hectoliter, :milliliter, :deciliter, :liter, :cup_metric]
# For a user expressing an explicit measurement system
iex> system = Cldr.Unit.measurement_system_from_locale "en-AU-u-ms-uksystem"
:uksystem
iex> Enum.filter(units, &Cldr.Unit.measurement_system?(&1, system))
[:quart_imperial, :cubic_foot, :cubic_inch, :dessert_spoon_imperial,
:cubic_yard, :cubic_mile, :fluid_ounce_imperial, :acre_foot, :gallon_imperial]
Additional domain-specific units can be defined to suit application requirements. In the context
of ex_cldr
there are two parts to configuring additional units.
-
Configure the unit, base unit and conversion in
config.exs
. This is a requirement since units are compiled into code. -
Configure the localizations for the additional unit in a CLDR backend module. Once configured, additional units act and behave like any of the predefined units of measure defined by CLDR.
Under the application :ex_cldr_units
, define a key :additional_units
with the required unit
definitions.
For example:
config :ex_cldr_units, :additional_units,
vehicle: [base_unit: :unit, factor: 1, offset: 0, sort_before: :all],
person: [base_unit: :unit, factor: 1, offset: 0, sort_before: :all]
This example defines two additional units: :vehicle
and :person
.
-
The keys
:base_unit
, and:factor
are required. The key:offset
is optional and defaults to0
. -
The key
:sort_before
is optional and defaults to:none
.
-
:base_unit
is the common denominator that is used to support conversion between like units. It can be any atom value. For example:liter
is the base unit for volume units,:meter
is the base unit for length units. -
:factor
is used to convert a unit to its base unit in order to support conversion. When converting a unit to another compatible unit, the unit is first multiplied by this units factor then divided by the target units factor. -
:offset
is added to a unit after applying its base factor in order to convert to another unit. -
:sort_before
determines where in this base unit sorts relative to other base units. Typically this is set to:all
in which case this base unit sorts before all other base units or:none
in which case this base unit sorted after all other base units. The default is:none
. If in doubt, leave this key to its default. -
:systems
is list of measurement systems to which this unit belongs. The known measurement systems are:metric
,:uksystem
and:ussystem
. The default is[:metric, :ussystem, :uksystem]
.
Although defining a unit in config.exs
is enough to create, operate on and serialize an additional unit, it cannot be localised without defining localizations in an ex_cldr
backend module. For example:
defmodule MyApp.Cldr do
# Note that this line should come before the `use Cldr` line
use Cldr.Unit.Additional
use Cldr,
locales: ["en", "fr", "de", "bs", "af", "af-NA", "se-SE"],
default_locale: "en",
providers: [Cldr.Number, Cldr.Unit, Cldr.List]
unit_localization(:person, "en", :long,
nominative: %{
one: "{0} person",
other: "{0} people"
},
display_name: "people"
)
end
Note the additions to a typical ex_cldr
backend module:
-
use Cldr.Unit.Additional
is required to define additional units -
use of the
Cldr.Unit.Additional.unit_localization/4
macro in order to define a localization. -
The use templates for the localization. Templates are a string with both a placeholder (for units it is always
{0}
) and some fixed text that reflects the grammatical requirements of the particular locale. -
Not all locales support more than the nominative case. The nominative case is the default and mandatory one. Any configured "Additional Units" in a backend module will need to put the localisations into a map with the key
:nominative
.
One invocation of Cldr.Unit.Additional.unit_localization/4
should made for each combination of unit, locale and style.
-
unit
is the name of the additional unit as anatom
. -
locale
is the locale name for this localization. It should be one of the locale configured in this backend although this cannot currently be confirmed at compile time. -
style
is one of:long
,:short
, or:narrow
. -
localizations
is a keyword like of localization strings. Two keys -:display_name
and:other
are mandatory. They represent the localizations for a non-count display name and:other
is the localization for a unit when no other pluralization is defined.
Localization keyword list defines localizations that match the plural rules for a given locale. Plural rules for a given number in a given locale resolve to one of six keys, and they must be placed under the proper declension to be used:
:zero
:one
(singular):two
(dual):few
(paucal):many
(also used for fractions if they have a separate class):other
(required — general plural form. Also used if the language only has a single form)
Only the :nominative
key is required, and at minimum, it has to provide the :other
key. For english, providing keys for :one
and :other
is enough. Other languages have different grammatical requirements.
The key :display_name
is used by the function Cldr.Unit.display_name/1
which is primarily used to support UI applications.
From Elixir 1.10, Enum.sort/2
supports module-based comparisons to provide a simpler API for sorting structs. ex_cldr_units
supports Elixir 1.10 as the following example shows:
iex> alias Cldr.Unit
Cldr.Unit
iex> unit_list = [Unit.new!(:millimeter, 100), Unit.new!(:centimeter, 100), Unit.new!(:meter, 100), Unit.new!(:kilometer, 100)]
[#Unit<:millimeter, 100>, #Unit<:centimeter, 100>, #Unit<:meter, 100>,
#Unit<:kilometer, 100>]
iex> Enum.sort unit_list, Cldr.Unit
[#Unit<:millimeter, 100>, #Unit<:centimeter, 100>, #Unit<:meter, 100>,
#Unit<:kilometer, 100>]
iex> Enum.sort unit_list, {:desc, Cldr.Unit}
[#Unit<:kilometer, 100>, #Unit<:meter, 100>, #Unit<:centimeter, 100>,
#Unit<:millimeter, 100>]
iex> Enum.sort unit_list, {:asc, Cldr.Unit}
[#Unit<:millimeter, 100>, #Unit<:centimeter, 100>, #Unit<:meter, 100>,
#Unit<:kilometer, 100>]
Note that the items being sorted must be all of the same unit category (length, volume, ...). Where units are of the same category but different units, conversion to a common unit will occur before the comparison. If units of different categories are encountered an exception will be raised as the following example shows:
iex> unit_list = [Unit.new!(:millimeter, 100), Unit.new!(:centimeter, 100), Unit.new!(:meter, 100), Unit.new!(:liter, 100)]
[#Cldr.Unit<:millimeter, 100>, #Cldr.Unit<:centimeter, 100>,
#Cldr.Unit<:meter, 100>, #Cldr.Unit<:liter, 100>]
iex> Enum.sort unit_list, Cldr.Unit
** (Cldr.Unit.IncompatibleUnitsError) Operations can only be performed between units with the same category and base unit. Received :liter and :meter
The companion package ex_cldr_units_sql provides functions for the serialization of Unit
data. See the README for further information.