Cldr for Units

Build Status Hex.pm Hex.pm Hex.pm

Installation

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

Getting Started

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.

Configuration

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

Supporting the String.Chars protocol

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

Public API

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 new Unit.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 compatible Unit.t structs.

Creating a new unit

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 to yotta)

  • An optional power prefix of square or cubic

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>}

Unit formatting and localization

MyApp.Cldr.Unit.to_string/2 provides localized unit formatting. It supports two arguments:

  • number is any number (integer, float or Decimal) or a Unit.t struct returned by Cldr.Unit.new/2

  • options which are:

    • :unit is any unit returned by Cldr.Unit.units/0. This option is required unless a Unit.t is passed as the first argument.
    • :locale is any configured locale. See Cldr.known_locale_names/0. The default is locale: Cldr.get_current_locale()
    • :style is one of those returned by Cldr.Unit.available_styles. The current styles are :long, :short and :narrow. The default is style: :long
    • Any other options are passed to Cldr.Number.to_string/2 which is used to format the number
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"}

Unit decomposition

Sometimes its a requirement to decompose a unit into one or more subunits. For example, if someone is 6.3 feet heigh 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"

Converting Units

Unit.t structs can be converted to other compatible units. For example, feet can be converted to meters since they are both the length unit type.

# Test for unit compatibility
iex> MyApp.Cldr.Unit.compatible? :foot, :meter
true
iex> MyApp.Cldr.Unit.compatible? :foot, :liter
false

# Convert a unit
iex> MyApp.Cldr.Unit.convert MyApp.Cldr.Unit.new(:foot, 3), :meter
#Cldr.Unit<:meter, 0.9144111192392099>

Localising units for a give locale or territory

Differnent 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.

Unit arithmetic

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> MyApp.Cldr.Unit.Math.add MyApp.Cldr.Unit.new!(:foot, 1), MyApp.Cldr.Unit.new!(:foot, 1)
#Cldr.Unit<:foot, 2>

iex> MyApp.Cldr.Unit.Math.add MyApp.Cldr.Unit.new!(:foot, 1), MyApp.Cldr.Unit.new!(:mile, 1)
#Cldr.Unit<:foot, 5280.945925937846>

iex> MyApp.Cldr.Unit.Math.add MyApp.Cldr.Unit.new!(:foot, 1), MyApp.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> MyApp.Cldr.Unit.round MyApp.Cldr.Unit.new(:yard, 1031.61), 1
#Cldr.Unit<:yard, 1031.6>

iex> MyApp.Cldr.Unit.round MyApp.Cldr.Unit.new(:yard, 1031.61), 1, :up
#Cldr.Unit<:yard, 1031.7>

Available units

Available units are returned by MyApp.Cldr.Unit.known_units/0.

iex> MyApp.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, ...]

Unit types

Units are grouped by unit type which defines the convertibility of different types. In general, units of the same time are convertible to each other. The function MyApp.Cldr.Unit.known_unit_categories/0 returns the unit types.

iex> MyApp.Cldr.Unit.known_unit_categories
[:acceleration, :angle, :area, :concentr, :consumption, :coordinate, :digital,
 :duration, :electric, :energy, :frequency, :length, :light, :mass, :power,
 :pressure, :speed, :temperature, :volume]

Sorting Units

With 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>]

Further information

For help in iex:

iex> h MyApp.Cldr.Unit.new
iex> h MyApp.Cldr.Unit.to_string
iex> h MyApp.Cldr.Unit.localize
iex> h MyApp.Cldr.Unit.convert
iex> h MyApp.Cldr.Unit.units
iex> h MyApp.Cldr.Unit.unit_categories