Coder Social home page Coder Social logo

cldr_units's Introduction

Cldr for Units

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

Documentation can be found at https://hexdocs.pm/ex_cldr_units.

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.known_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 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"

Converting Units

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

Localising units for a given locale or territory

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.

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

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

Unit categories

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.

Measurement systems

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.

Localisation with measurement systems

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 units (custom units)

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.

  1. Configure the unit, base unit and conversion in config.exs. This is a requirement since units are compiled into code.

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

Configuring a unit in config.exs

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 to 0.

  • The key :sort_before is optional and defaults to :none.

Configuration keys

  • :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].

Defining localizations

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.

Parameters to unit_localization/4

  • unit is the name of the additional unit as an atom.

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

Localisation definition

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.

Sorting Units

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

Serializing to a database with Ecto

The companion package ex_cldr_units_sql provides functions for the serialization of Unit data. See the README for further information.

cldr_units's People

Contributors

doughsay avatar kianmeng avatar kipcole9 avatar lostkobrakai avatar maltoe avatar petrus-jvrensburg avatar ribanez7 avatar seantanly avatar zurga avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

cldr_units's Issues

Adding some additional units seems to break some conversions

This may be an issue with my poor understanding. But I was able to break tests tests in the cldr_units library by passing some :additional_units which I believe should be acceptable, based on the documentation.

updating config/test.exs to add a decatherm w/ a base_unit of :therm_us

],
  dtherm_us: [
    base_unit: :therm_us,
    factor: 10,
    offset: 0
  ]

These changes do successfully implement a conversion for :therm_us to :dtherm_us (and likewise)
These changes cause these failures. It also breaks the conversion of 1 therm_us to killowatt_hour

  • correct conversion "29.30011111111111111111111111"
  • wrong conversion: "2.777777777777777777777777778E-7"
  1) test #76 [Decimal] that therm-us converted to kilogram-square-meter-per-square-second is {105480400000.0, 0, 7} (Cldr.Unit.Conversion.Test)
    test/conversion_test.exs:42
    ** (Cldr.Unit.IncompatibleUnitsError) Operations can only be performed between units with the same base unit. Received :therm_us and "kilogram-square-meter-per-square-second"
    code: |> Cldr.Unit.Conversion.convert!(unquote(t.to))
    stacktrace:
      (ex_cldr_units 3.16.3) lib/cldr/unit/conversion.ex:265: Cldr.Unit.Conversion.convert!/2
      test/conversion_test.exs:48: (test)

........................................................................................

 2) test #76 [Float] that therm-us converted to kilogram-square-meter-per-square-second is {105480400000.0, 0, 7} (Cldr.Unit.Conversion.Test)
    test/conversion_test.exs:20
    ** (Cldr.Unit.IncompatibleUnitsError) Operations can only be performed between units with the same base unit. Received :therm_us and "kilogram-square-meter-per-square-second"
    code: |> Cldr.Unit.Conversion.convert!(unquote(t.to))
    stacktrace:
      (ex_cldr_units 3.16.3) lib/cldr/unit/conversion.ex:265: Cldr.Unit.Conversion.convert!/2
      test/conversion_test.exs:26: (test)

...................................................................................

 3) test #76 that therm-us is convertible to kilogram-square-meter-per-square-second (Cldr.Unit.Conversion.Test)
    test/conversion_test.exs:7
    Assertion with == failed
    code:  assert from == to
    left:  :therm_us
    right: "kilogram_square_meter_per_square_second"
    stacktrace:
      test/conversion_test.exs:10: (test)


Apologies if this is user error. I am happy to update documentation to prevent any future confusion, if this is a me problem.

Thank you in advance for your time!

Multiplication of `per` units

It would be nice if composite (per) units could be multiplied with its base units.

For example:

drain_rate = Cldr.Unit.new!("liter_per_hour", 6)
time = Cldr.Unit.new!("minute", 30)
Cldr.Unit.mult(drain_rate, time)
# => #Cldr.Unit<"liter", 3>

Localization fails when locale features patterns have no number substitution

Hello @kipcole9 ,

Firstly, thanks for your work on this library, it's much appreciated.

I noticed that localizing an hour unit, or 2 hours, to either "he" or "ar" would result in an error. 3 hours will succeed.

iex(14)> {:ok, u} = Cldr.Unit.new(:hour, 1)
{:ok, #Cldr.Unit<:hour, 1>}
iex(15)> Cldr.Unit.to_string u, locale: "he"
** (FunctionClauseError) no function clause matching in Cldr.Substitution.substitute/2

    The following arguments were given to Cldr.Substitution.substitute/2:

        # 1
        "1"

        # 2
        ["שעה"]

    Attempted function clauses (showing 8 out of 8):

        def substitute([item], [0, string]) when is_binary(string)
        def substitute(item, [0, string]) when is_binary(string)
        def substitute([item], [string, 0]) when is_binary(string)
        def substitute(item, [string, 0]) when is_binary(string)
        def substitute(item, [string1, 0, string2]) when is_binary(string1) and is_binary(string2)
        def substitute([item_0, item_1], [0, string, 1]) when is_binary(string)
        def substitute([item_0, item_1], [1, string, 0]) when is_binary(string)
        def substitute([item_0, item_1, item_2], [0, string_1, 1, string_2, 2]) when is_binary(string_1) and is_binary(string_2)

    (ex_cldr 2.18.2) lib/cldr/substitution.ex:71: Cldr.Substitution.substitute/2
    (ex_cldr_units 3.3.1) Elixir.Cldr.Unit.erl:586: Cldr.Unit.to_iolist/3
    (ex_cldr_units 3.3.1) Elixir.Cldr.Unit.erl:482: Cldr.Unit.to_string/3
iex(15)> {:ok, u} = Cldr.Unit.new(:hour, 2)
{:ok, #Cldr.Unit<:hour, 2>}
iex(16)> Cldr.Unit.to_string u, locale: "he"
** (FunctionClauseError) no function clause matching in Cldr.Substitution.substitute/2

    The following arguments were given to Cldr.Substitution.substitute/2:

        # 1
        "2"

        # 2
        ["שעתיים"]

    Attempted function clauses (showing 8 out of 8):

        def substitute([item], [0, string]) when is_binary(string)
        def substitute(item, [0, string]) when is_binary(string)
        def substitute([item], [string, 0]) when is_binary(string)
        def substitute(item, [string, 0]) when is_binary(string)
        def substitute(item, [string1, 0, string2]) when is_binary(string1) and is_binary(string2)
        def substitute([item_0, item_1], [0, string, 1]) when is_binary(string)
        def substitute([item_0, item_1], [1, string, 0]) when is_binary(string)
        def substitute([item_0, item_1, item_2], [0, string_1, 1, string_2, 2]) when is_binary(string_1) and is_binary(string_2)

    (ex_cldr 2.18.2) lib/cldr/substitution.ex:71: Cldr.Substitution.substitute/2
    (ex_cldr_units 3.3.1) Elixir.Cldr.Unit.erl:586: Cldr.Unit.to_iolist/3
    (ex_cldr_units 3.3.1) Elixir.Cldr.Unit.erl:482: Cldr.Unit.to_string/3
iex(16)> {:ok, u} = Cldr.Unit.new(:hour, 3)
{:ok, #Cldr.Unit<:hour, 3>}
iex(17)> Cldr.Unit.to_string u, locale: "he"
{:ok, "3 שעות"}

Looking at the cldr data, there appears to be no substitution for the number:

          "duration-hour": {
            "displayName": "שעות",
            "unitPattern-count-one": "שעה",
            "unitPattern-count-two": "שעתיים",
            "unitPattern-count-many": "{0} שעות",
            "unitPattern-count-other": "{0} שעות",
            "perUnitPattern": "{0} לשעה"
          },

Yet I'm not seeing a function signature that would match such an input:

 # Takes care of a common case where there is one parameter
  def substitute([item], [0, string]) when is_binary(string) do
    [item, string]
  end

  def substitute(item, [0, string]) when is_binary(string) do
    [item, string]
  end

  def substitute([item], [string, 0]) when is_binary(string) do
    [string, item]
  end

  def substitute(item, [string, 0]) when is_binary(string) do
    [string, item]
  end

  def substitute(item, [string1, 0, string2]) when is_binary(string1) and is_binary(string2) do
    [string1, item, string2]
  end

  # Takes care of the common case where there are two parameters separated
  # by a string.
  def substitute([item_0, item_1], [0, string, 1]) when is_binary(string) do
    [item_0, string, item_1]
  end

  def substitute([item_0, item_1], [1, string, 0]) when is_binary(string) do
    [item_1, string, item_0]
  end

  # Takes care of the common case where there are three parameters separated
  # by strings.
  def substitute([item_0, item_1, item_2], [0, string_1, 1, string_2, 2])
      when is_binary(string_1) and is_binary(string_2) do
    [item_0, string_1, item_1, string_2, item_2]
  end

A naive solution would be to simply add the variants to handle a pattern with no substitution:

  # Takes care of the case where no parameters are used
  def substitute([item], [string]) when is_binary(string) do
    [string]
  end

  def substitute(item, [string]) when is_binary(string) do
    [string]
  end

... or even to percolate this detection up the call chain, such that substitute/* is never even called, but there is an inconsistency. I'm using the function with negative units, which is perfectly fine for most locales, but you'll notice with the example in Hebrew, the negative version is now the same as the positive:

iex(8)> {:ok, u} = Cldr.Unit.new(:hour, -1)
{:ok, #Cldr.Unit<:hour, -1>}
iex(9)> Cldr.Unit.to_string u, locale: "en"
{:ok, "-1 hour"}
iex(10)> Cldr.Unit.to_string u, locale: "he"
{:ok, "שעה"}
iex(11)> {:ok, u} = Cldr.Unit.new(:hour, 1)
{:ok, #Cldr.Unit<:hour, 1>}
iex(12)> Cldr.Unit.to_string u, locale: "he"
{:ok, "שעה"}

I'll probably work around this locally. If you're short on time, I could organise a PR for you, with a little guidance on how you want to deal with these problems.

Thanks for your time, and let me know.

Cheers,

Jarrod

split_into_units breaks when input is SI prefix only

Hello 👋

we're trying to use validate_unit/1 to validate input strings to be valid units, and doing this on change of the form, so we're getting all interim values. Strings that only contain an SI prefix currently break in the parser:

iex(1)> Cldr.Unit.validate_unit("centi")
** (MatchError) no match of right hand side value: []
    (ex_cldr_units 3.17.0) lib/cldr/unit/parser.ex:351: Cldr.Unit.Parser.split_into_units/1
    (ex_cldr_units 3.17.0) lib/cldr/unit/parser.ex:138: Cldr.Unit.Parser.parse_subunit/1
    (ex_cldr_units 3.17.0) lib/cldr/unit/parser.ex:117: Cldr.Unit.Parser.parse_subunits/1
    (ex_cldr_units 3.17.0) lib/cldr/unit/parser.ex:101: Cldr.Unit.Parser.parse_unit/1
    (ex_cldr_units 3.17.0) lib/cldr/unit.ex:2537: Cldr.Unit.return_parsed_unit/1
    iex:1: (file)

Best,
malte

[Question] List units by category?

Hey Kip! We're excited to dive into another one of your libraries - is it not possible to simply list all units by category? i.e if I wanted to list all value volume units, I'm not seeing a way to do this. Am I missing something?

Thanks!

Cldr.Unit.convert documentation

The documentation for Cldr.Unit.convert does show convert(unit_1, unit_2), which should propably be convert(unit, to_unit) like in the linked documentation of Cldr.Unit.Conversion.

`Cldr.Unit.round/2` fails when the unit has float a value

When upgrading from 3.15.0 to 3.16.1, this code started to fail when it previously didn't:

iex> Cldr.Unit.new!(:meter, "12.34") |> Cldr.Unit.to_float_unit() |> Cldr.Unit.round(1)
# ** (FunctionClauseError) no function clause matching in Cldr.Unit.Conversion.maybe_integer/1

#     The following arguments were given to Cldr.Unit.Conversion.maybe_integer/1:

#         # 1
#         12.3

#     Attempted function clauses (showing 2 out of 2):

#         def maybe_integer(%Decimal{} = a)
#         def maybe_integer(a) when is_integer(a)

#     (ex_cldr_units 3.16.1) lib/cldr/unit/conversion.ex:481: Cldr.Unit.Conversion.maybe_integer/1
#     (ex_cldr_units 3.16.1) lib/cldr/unit/math.ex:365: Cldr.Unit.Math.round/3
#     iex:4: (file)

(FunctionClauseError) no function clause matching in Cldr.Unit.Parser.extract_base_unit/1

👋

Version:

* ex_cldr_units 3.0.0 (Hex package) (mix)
  locked at 3.0.0 (ex_cldr_units) 6b0c3db6

Seems like extract_base_unit/1 doesn't expect composite units like :kilowatt_hour since they seem to have several base_units?

iex(3)> Demo.Cldr.Unit.to_string(1, unit: :kilowatt_hour)
** (FunctionClauseError) no function clause matching in Cldr.Unit.Parser.extract_base_unit/1

    The following arguments were given to Cldr.Unit.Parser.extract_base_unit/1:

        # 1
        {:kilowatt_hour,
         [
           hour: %Cldr.Unit.Conversion{base_unit: [:second], factor: 3600, offset: 0},
           kilowatt: %Cldr.Unit.Conversion{
             base_unit: [:kilogram_square_meter_per_cubic_second],
             factor: 1000,
             offset: 0
           }
         ]}

    Attempted function clauses (showing 2 out of 2):

        defp extract_base_unit({_unit_name, [{_, %{base_unit: base_units}}]})
        defp extract_base_unit({_unit_name, %{base_unit: base_units}})

    (ex_cldr_units 3.0.0) lib/cldr/unit/parser.ex:307: Cldr.Unit.Parser.extract_base_unit/1
    (ex_cldr_units 3.0.0) lib/cldr/unit/parser.ex:226: Cldr.Unit.Parser.canonical_base_unit/1
    (ex_cldr_units 3.0.0) lib/cldr/unit.ex:1061: Cldr.Unit.unit_category/1
    (ex_cldr_units 3.0.0) lib/cldr/unit.ex:220: Cldr.Unit.validate_usage/2
    (ex_cldr_units 3.0.0) lib/cldr/unit.ex:206: Cldr.Unit.create_unit/3
    (ex_cldr_units 3.0.0) lib/cldr/unit.ex:441: Cldr.Unit.to_string/3

Documentation for adding new unit localizations

The only way I have found to make it work is not the one in the README, but this one:

  unit_localization(:person, "ja", :long,
    nominative: %{
      one: "{0} 人",
      other: "{0} 人"
    },
    display_name: "人"
  )

Can you confirm that's correct? if so I can update it.

Unit reductions

For a given unit, for example "5,280 feet" or "1,000 metres" it is often appropriate and expected to reduce the unit to a more commonly used version such as "1 mile" or "1 kilometre". This note explores an approach to implementation of unit reductions using Cldr data.

Measurement systems

In our example it would be expected that "feet" would be reduced using the "imperial" or "US" systems whereas "metres" would be reduced using the "metric" system. The unit data for Cldr does not maintain a per-unit mapping of unit name to measurement system.

For some units, such as digital units, have only a single system.

Locale and measurement system

Cldr does provide data a mapping of locale to measurement system so we can identify the preferred measurement system for a given locale. This would allow a unit reduction to additionally convert to the appropriate measurement system as well.

Automatic reduction

The intent of reduction is to produce a result in the range -10 < unit < +10. Therefore identify the reduction factor required and the target unit. Convert to the target unit. To identify the target unit we take into account the source unit's measurement system and attempt to find a reduction target in the same measurement system.

Example

iex> Cldr.Unit.reduce Cldr.Unit.new(:meter, 1100)
#Unit(:kilometer, 1.1)

# System conversion tries to keep a similar magnitude as the 
# source unit.  Convert to :US, :UK or :metric
iex> Cldr.Unit.convert_system Cldr.Unit(:meter, 1000), to: :US
#Unit(:yard, 1093.61)

# Round a unit. Rounding options are passed
# through to Cldr.Number
iex> Cldr.Unit.round Cldr.Unit(:yard, 1093.61)
#Unit(:yard, 1093.6)

Limitations

  • Cldr data is not a comprehensive list of units
  • The Digital category of units only supports base 10 conversions. Adding base 2 conversion would be also useful

Readme typo in a convert example

Thanks for your library!

An example in the Readme has apparently a typo: MyApp.Cldr.Unit.convert MyApp.Cldr.Unit.new(:foot, 3), :meter
To make it work, I have to use new!

API to get the value of a Cldr.Unit struct

I'm using Cldr.Unit not only because of it's formatting capabilities, but also as general purpose unit type within my system. So in some places it might be nice to have an API method for getting back the value of the struct as well.

Wrong Dialyzer Typing

Working Code

@task.co2_savings_kg |> Decimal.to_float |> Cldr.Unit.to_string!(unit: :kilogram)

Dialyzer Error

lib/acme/templates/challenge/task/show.html.eex:32:call
The function call will not succeed.

Acme.Cldr.Unit.to_string!(float(), [{:unit, :kilogram}])

will never return since the success typing is:
(
  %Cldr.Unit{
    :base_conversion =>
      [any()]
      | {[map(), ...], [map(), ...]}
      | %{
          :base_unit => [atom(), ...],
          :factor =>
            number() | %Ratio{:denominator => pos_integer(), :numerator => integer()},
          :offset => number()
        },
    :format_options => [],
    :unit => atom() | binary(),
    :usage => atom(),
    :value =>
      number()
      | %{
          :__struct__ => Decimal | Ratio,
          :coef => _,
          :denominator => pos_integer(),
          :exp => _,
          :numerator => integer(),
          :sign => _
        }
  },
  Keyword.t()
) :: binary()

and the contract is
(Cldr.Unit.t() | [Cldr.Unit.t(), ...], Keyword.t()) :: String.t() | no_return()

No default :ex_cldr backend is configured

Issue

When using MyApp.Cldr.Unit.to_string!(7.3, unit: :kilogram), the following error is raised.

** (Cldr.NoDefaultBackendError) No default :ex_cldr backend is configured
    (ex_cldr 2.22.0) lib/cldr/config/config.ex:332: Cldr.Config.default_backend/0
    (ex_cldr 2.22.0) lib/cldr.ex:614: Cldr.validate_locale/1
    (ex_cldr_units 3.5.2) lib/cldr/unit.ex:2050: Cldr.Unit.validate_grammatical_gender/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:298: Cldr.Unit.Format.normalize_options/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:193: Cldr.Unit.Format.to_string/3
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:281: Cldr.Unit.Format.to_string!/3

I've tried to use different notations and always get the same result:

iex> MyBluePlanet.ClimateActionsAdmin.Cldr.Unit.to_string!(7.3, unit: :kilogram)
** (Cldr.NoDefaultBackendError) No default :ex_cldr backend is configured
    (ex_cldr 2.22.0) lib/cldr/config/config.ex:332: Cldr.Config.default_backend/0
    (ex_cldr 2.22.0) lib/cldr.ex:614: Cldr.validate_locale/1
    (ex_cldr_units 3.5.2) lib/cldr/unit.ex:2050: Cldr.Unit.validate_grammatical_gender/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:298: Cldr.Unit.Format.normalize_options/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:193: Cldr.Unit.Format.to_string/3
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:281: Cldr.Unit.Format.to_string!/3
iex> Cldr.Unit.to_string!(7.3, MyBluePlanet.ClimateActionsAdmin.Cldr, unit: :kilogram)
** (Cldr.NoDefaultBackendError) No default :ex_cldr backend is configured
    (ex_cldr 2.22.0) lib/cldr/config/config.ex:332: Cldr.Config.default_backend/0
    (ex_cldr 2.22.0) lib/cldr.ex:614: Cldr.validate_locale/1
    (ex_cldr_units 3.5.2) lib/cldr/unit.ex:2050: Cldr.Unit.validate_grammatical_gender/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:298: Cldr.Unit.Format.normalize_options/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:193: Cldr.Unit.Format.to_string/3
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:281: Cldr.Unit.Format.to_string!/3
iex> MyBluePlanet.ClimateActionsAdmin.Cldr.Unit.to_string!(7.3, unit: :kilogram, backend: MyBluePlanet.ClimateActionsAdmin.Cldr)
** (Cldr.NoDefaultBackendError) No default :ex_cldr backend is configured
    (ex_cldr 2.22.0) lib/cldr/config/config.ex:332: Cldr.Config.default_backend/0
    (ex_cldr 2.22.0) lib/cldr.ex:614: Cldr.validate_locale/1
    (ex_cldr_units 3.5.2) lib/cldr/unit.ex:2050: Cldr.Unit.validate_grammatical_gender/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:298: Cldr.Unit.Format.normalize_options/2
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:193: Cldr.Unit.Format.to_string/3
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:281: Cldr.Unit.Format.to_string!/3
iex> Cldr.Unit.to_string!(7.3, unit: :kilogram, backend: MyBluePlanet.ClimateActionsAdmin.Cldr)
** (Cldr.NoDefaultBackendError) No default :ex_cldr backend is configured
    (ex_cldr 2.22.0) lib/cldr/config/config.ex:332: Cldr.Config.default_backend/0
    (ex_cldr 2.22.0) lib/cldr.ex:449: Cldr.put_default_locale/1
    (ex_cldr 2.22.0) lib/cldr.ex:395: Cldr.default_locale/0
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:153: Cldr.Unit.Format.to_string/3
    (ex_cldr_units 3.5.2) lib/cldr/unit/format.ex:281: Cldr.Unit.Format.to_string!/3

Versions

mix deps | grep ex_cldr
* ex_cldr 2.22.0 (Hex package) (mix)
  locked at 2.22.0 (ex_cldr) c6c8aae8
* ex_cldr_calendars 1.14.1 (Hex package) (mix)
  locked at 1.14.1 (ex_cldr_calendars) ffd960e9
* ex_cldr_currencies 2.9.1 (Hex package) (mix)
  locked at 2.9.1 (ex_cldr_currencies) 1bc6f9ea
* ex_cldr_dates_times 2.7.2 (Hex package) (mix)
  locked at 2.7.2 (ex_cldr_dates_times) ae6bcb69
* ex_cldr_lists 2.8.0 (Hex package) (mix)
  locked at 2.8.0 (ex_cldr_lists) 455406d4
* ex_cldr_numbers 2.18.0 (Hex package) (mix)
  locked at 2.18.0 (ex_cldr_numbers) 9663db9c
* ex_cldr_units 3.5.2 (Hex package) (mix)
  locked at 3.5.2 (ex_cldr_units) edb4066f

Singular / Plural form of units

It seems like Cldr.Unit.to_string is always returning the plural form of the units name.

Edit: Seems like it's working for "en" as locale, but not for "de".

Number Spellout Wrong

Steps to reproduce

AcmeCldr.Unit.to_string!(Cldr.Unit.new!(1, :year), format: :spellout, locale: "de")

Expected

ein Jahr

Actual

eins Jahr

Versions

$ mix deps | grep cldr
* cldr_utils 2.16.0 (Hex package) (mix)
  locked at 2.16.0 (cldr_utils) 3ef5dc0f
* ex_cldr 2.23.1 (Hex package) (mix)
  locked at 2.23.1 (ex_cldr) f7b42cf2
* ex_cldr_calendars 1.16.0 (Hex package) (mix)
  locked at 1.16.0 (ex_cldr_calendars) 483d91a0
* ex_cldr_currencies 2.11.1 (Hex package) (mix)
  locked at 2.11.1 (ex_cldr_currencies) 99e8eb3f
* ex_cldr_dates_times 2.9.2 (Hex package) (mix)
  locked at 2.9.2 (ex_cldr_dates_times) dabd8e6f
* ex_cldr_languages 0.2.2 (Hex package) (mix)
  locked at 0.2.2 (ex_cldr_languages) d9cbf4bf
* ex_cldr_lists 2.8.0 (Hex package) (mix)
  locked at 2.8.0 (ex_cldr_lists) 455406d4
* ex_cldr_numbers 2.22.0 (Hex package) (mix)
  locked at 2.22.0 (ex_cldr_numbers) af8e7267
* ex_cldr_units 3.7.1 (Hex package) (mix)
  locked at 3.7.1 (ex_cldr_units) b9595bea
* hygeia_cldr 0.1.0 (apps/hygeia_cldr) (mix)

Are there any plans to support ranges in units?

Currently it is possible to pass float, integer, Decimal or Ratio, but would it be a possibility to pass a range? Or maybe this should be responsibility of Cldr.Number.to_range_string ?

If we include it in Cldr.Number.to_range_string, maybe it can be passed as an option, like the currency one. Why not allow an option for unit, if there is an option for currency?

And in case that should be responsibility of Cldr.Unit, I could try to do a POC.

Parse unit without value

I'm trying to parse some user input where the user can only enter the unit without a value, but I couldn't found a function that - similar to Cldr.Unit.parse/2 - also considers aliases and other forms, like "kg" for "kilogram". Is there I function I might have overlooked? I've tried the following functions, but so far had to settle for Cldr.Unit.parse("1" <> "user_input") to get the unit

  • Cldr.Unit.base_unit/1
  • Cldr.Unit.Parser.parse_unit/1
  • Cldr.Unit.Parser.canonical_unit_name/1

Edit: If such a function does not exist, would you be open for a PR?

Energy units have inconsistent base units

When trying to convert joules into kWh, I get this error message:

iex(1)> Cldr.Unit.convert(Cldr.Unit.new!(1, :joule), :kilowatt_hour)
{:error,
 {Cldr.Unit.IncompatibleUnitsError,
  "Operations can only be performed between units with the same base unit. Received :joule and :kilowatt_hour"}}

This is because Cldr.Unit.BaseUnit.canonical_base_unit returns different strings for both of these:

iex(2)> Cldr.Unit.BaseUnit.canonical_base_unit!("joule")
"kilogram_square_meter_per_square_second"
iex(3)> Cldr.Unit.BaseUnit.canonical_base_unit!("kilowatt_hour")
"kilogram_square_meter_second_per_cubic_second"

For Joule, it returns kg*m^2/s^s, and for kWh, it returns kg*m^2*s/s^3. Both are mathematically equal but not as strings, obviously.

I'm not sure if this is an intended feature of the CLDR spec, but it seems very weird to have the canonical_base_unit function return different things for different energy units.

Unexpected return value of Cldr.Unit.to_string

Using v0.4.2 I get the following unexpected function call result:

> Cldr.Unit.to_string(size, map_sizable_unit_to_cldr_unit(unit), style: :short, locale: "de_DE")
{:ok, {:error, {Cldr.UnknownLocaleError, "The locale "de" is not known."}}}

This doesn't seem like a desirable way of reporting an error besides the fact that "de" should be known.

Missing/misconfigured units after upgrade

Strange issue and I'm not quite sure where it belongs, but it seems that after the last update of ex_cldr and ex_cldr_units some units have gone missing or become misconfigured. I've pored through the changelogs of ex_cldr_units, ex_cldr, and the actual CLDR 45 release and I haven't found anything that sticks out to me.

It seems that units that are a division or multiplication of :meter are misbehaving (e.g. :millimeter and :kilometer). Possibly this has happened to other units as well and I simply haven't noticed yet.

Example

Before:

MyApp.Cldr.Unit.display_name(:millimeter, style: :long) #=> "millimeters"

After:

MyApp.Cldr.Unit.display_name(:millimeter, style: :long) #=> {:error, {Cldr.Unit.UnitNotTranslatableError, "The unit :millimeter is not translatable"}}

Reproduce

Below is a script to reproduce the issue described above. Things to note:

  1. :millimeter is missing from MyApp.Cldr.Unit.know_units
  2. :millimeter is present in Cldr.Unit.know_units_for_category(:length)
Mix.install([
  {:jason, "~> 1.0"},
  {:ex_cldr, "2.38.1"},
  {:ex_cldr_units, "3.17.0"}
])

defmodule MyApp.Cldr do
  use Cldr,
    locales: ["en",],
    default_locale: "en",
    providers: [Cldr.Number, Cldr.Unit, Cldr.List]
end

IO.puts("known units")
dbg(Enum.sort(MyApp.Cldr.Unit.known_units))

IO.puts("known units contains: millimeters")
dbg(Enum.find(MyApp.Cldr.Unit.known_units, fn unit -> unit == :millimeter end))

IO.puts("units for category: length")
dbg(Cldr.Unit.known_units_for_category(:length))

IO.puts("display name: millimeter")
dbg(MyApp.Cldr.Unit.display_name(:millimeter, style: :long))

Note that you can reproduce the successful output by reverting the dependencies at the beginning of the script to:

Mix.install([
  {:jason, "~> 1.0"},
  {:ex_cldr, "2.37.5"},
  {:ex_cldr_units, "3.16.5"}
])

Additional units aren't accessible

I was checking how to add new units to Cldr following the documentation but I failed.

I added two new units alarm and person, (the settings are listed below).

In iex I tried to get the new alarm unit, but it says it does not exist.
the unit gallon works fine, as in the example.

MyApp.Cldr.Unit.to_string Cldr.Unit.new!(:alarm, 1)
** (Cldr.UnknownUnitError) Unknown unit was detected at "alarm"
(ex_cldr_units 3.15.0) lib/cldr/unit.ex:1021: Cldr.Unit.new!/3
Exploring with iex, I manage to see that the new units are there:

iex> Citymonitor.Cldr.Unit.Additional.additional_units
[:alarm, :person]

iex> {:ok, dic_de} = Citymonitor.Cldr.Unit.unit_strings_for :de
iex> dic_de
%{
{"alarme", [:alarm]},
{"alarmen", [:alarm]},
...
}

iex> {:ok, dic_en} = Citymonitor.Cldr.Unit.unit_strings_for :en
iex> dic_en
%{
{"al.", [:alarm]},
{"alarm", [:alarm]},
...
}
I was wondering if the custom units should be merged into the core units,
but I don’t know how or where to do this:

Cldr.Unit.known_units() |> merge_base_units()

I would appreciate any hints or tips.

Here is the rest of the configuration.

iex> Citymonitor.Cldr.Unit.Additional.additional_units
[:alarm, :person]

File: mix.exs

{:ex_cldr, "> 2.34"},
{:ex_cldr_numbers, "
> 2.29"},
{:ex_cldr_units, "> 3.15"},
{:ex_cldr_plugs, "
> 1.2"},
{:ex_cldr_dates_times, "~> 2.13.1"},
...

File: config/config.exs

localization

config :ex_cldr, default_backend: MyApp.Cldr

...

Cldr Units

config :ex_cldr_units,
default_backend: MyApp.Cldr,
additional_units: [
alarm: [base_unit: :unit, factor: 1],
person: [base_unit: :unit, factor: 1]
]

File: lib/myapp/cldr/cldr.ex

defmodule Citymonitor.Cldr do
use Cldr.Unit.Additional

use Cldr,
locales: ["en", "de"],
default_locale: "de",
fuzzy_threshold: 1,
gettext: MyApp.Gettext,
providers: [Cldr.Number, Cldr.Unit, Cldr.List, Cldr.DateTime, Cldr.Calendar]

Alarm

unit_localization(:alarm, "en", :long,
nominative: %{
one: "{0} alarm",
other: "{0} alarm"
},
display_name: "alarm"
)

unit_localization(:alarm, "en", :short,
nominative: %{
one: "{0} al",
other: "{0} al"
},
display_name: "al"
)

unit_localization(:alarm, "en", :narrow,
nominative: %{
one: "{0} a",
other: "{0} a"
},
display_name: "a"
)

unit_localization(:alarm, "de", :long,
nominative: %{
one: "{0} Alarme",
other: "{0} Alarmen"
},
display_name: "Alarme"
)

unit_localization(:alarm, "de", :short,
nominative: %{
one: "{0} Al.",
other: "{0} Al."
},
display_name: "Al."
)

unit_localization(:alarm, "de", :narrow,
nominative: %{
one: "{0} a",
other: "{0} a"
},
display_name: "a"
)

Person

unit_localization(:person, "en", :long,
nominative: %{
one: "{0} person",
other: "{0} people"
},
display_name: "people"
)

unit_localization(:person, "en", :short,
nominative: %{
one: "{0} per",
other: "{0} ppl"
},
display_name: "ppl"
)

unit_localization(:person, "en", :narrow,
nominative: %{
one: "{0} p",
other: "{0} p"
},
display_name: "p"
)

unit_localization(:person, "de", :long,
nominative: %{
one: "{0} Person",
other: "{0} Personen"
},
display_name: "Personen"
)

unit_localization(:person, "de", :short,
nominative: %{
one: "{0} Per",
other: "{0} Prn"
},
display_name: "Prn"
)

unit_localization(:person, "de", :narrow,
nominative: %{
one: "{0} p",
other: "{0} p"
},
display_name: "p"
)
end

No format pattern was found for unit ... with grammatical case :nominative, gender :neuter and plural type :one

👋

I've just upgraded to the latest ExCLDR versions, and got the following error in one of the tests (which checks that all measurements used in the app can be "rendered"):

iex(1)> App.Cldr.Unit.to_string!(1, unit: :pascal, style: :short)
** (Cldr.Unit.NoPatternError) No format pattern was found for unit #Cldr.Unit<:pascal, 1> with grammatical case :nominative, gender :neuter and plural type :one
    (ex_cldr_units 3.5.1) lib/cldr/unit/format.ex:675: Cldr.Unit.Format.get_unit_pattern!/6
    (ex_cldr_units 3.5.1) lib/cldr/unit/format.ex:403: Cldr.Unit.Format.to_iolist/3
    (ex_cldr_units 3.5.1) lib/cldr/unit/format.ex:194: Cldr.Unit.Format.to_string/3
    (ex_cldr_units 3.5.1) lib/cldr/unit/format.ex:281: Cldr.Unit.Format.to_string!/3

Without :style option it works as expected:

iex(1)> App.Cldr.Unit.to_string!(1, unit: :pascal)               
"1 Pascal"

I wonder if maybe :style has been replaced with some other option?

Versions:

"ex_cldr": "2.20.0"
"ex_cldr_units": "3.5.1"

Thank you!

Compilation Warning

Warning

==> ex_cldr_units
Compiling 20 files (.ex)
warning: Phoenix.HTML.Safe.to_iodata/1 defined in application :phoenix_html is used by the current application but the current application does not depend on :phoenix_html. To fix this, you must do one of:

  1. If :phoenix_html is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs

  2. If :phoenix_html is a dependency, make sure it is listed under "def deps" in your mix.exs

  3. In case you don't want to add a requirement to :phoenix_html, you may optionally skip this warning by adding [xref: [exclude: [Phoenix.HTML.Safe]]] to your "def project" in mix.exs

  lib/cldr/protocol/phoenix_html_safe.ex:5: Phoenix.HTML.Safe.Cldr.Unit.to_iodata/1

Deps Versions

$ mix deps | grep cldr
* cldr_utils 2.17.1 (Hex package) (mix)
  locked at 2.17.1 (cldr_utils) 052e0c2c
* ex_cldr 2.26.1 (Hex package) (mix)
  locked at 2.26.1 (ex_cldr) b666dd85
* ex_cldr_calendars 1.18.0 (Hex package) (mix)
  locked at 1.18.0 (ex_cldr_calendars) 5b47bf4e
* ex_cldr_currencies 2.13.0 (Hex package) (mix)
  locked at 2.13.0 (ex_cldr_currencies) 64731e49
* ex_cldr_dates_times 2.11.0 (Hex package) (mix)
  locked at 2.11.0 (ex_cldr_dates_times) 36b2dd6b
* ex_cldr_lists 2.10.0 (Hex package) (mix)
  locked at 2.10.0 (ex_cldr_lists) adc040cd
* ex_cldr_numbers 2.25.0 (Hex package) (mix)
  locked at 2.25.0 (ex_cldr_numbers) 0ffe6648
* ex_cldr_units 3.12.0 (Hex package) (mix)
  locked at 3.12.0 (ex_cldr_units) 2a252abb
$ elixir --version
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Elixir 1.13.1 (compiled with Erlang/OTP 22)

Dialyzer warning on `Cldr.Unit.display_name/2` with atom as first argument

The documentation for the function Cldr.Unit.display_name/2 says that the first argument can be any unit name returned by Cldr.Unit.known_units/0, which returns a list of atoms. But the spec for Cldr.Unit.display_name/2 only specifies Cldr.Unit.value() | Cldr.Unit.t() as the first argument which causes a dialyzer warning when using an atom as the first argument.

The function call will not succeed.

MyRepo.Cldr.Unit.display_name(_unit :: atom(), [{:style, :narrow}])

will never return since the success typing is:
(
  number()
  | %{
      :__struct__ => Cldr.Unit | Decimal,
      :backend => _,
      :base_conversion => [{atom() | [any()], map()}] | {[{_, _}], [{_, _}]},
      :coef => _,
      :exp => _,
      :format_options => Keyword.t(),
      :sign => _,
      :unit => atom() | binary() | [atom()],
      :usage => atom(),
      :value => number() | %Decimal{:coef => _, :exp => _, :sign => _}
    },
  Keyword.t()
) :: binary() | {:error, {atom(), binary()}}

and the contract is
(Cldr.Unit.value() | Cldr.Unit.t(), Keyword.t()) ::
  String.t() | {:error, {module(), binary()}}

A :duration vs :year_duration inconsistency

There's an inconsistency between the known unit categories and the unit category I get for a duration in years as show below:

iex(7)> Cldr.Unit.known_unit_categories()
[:acceleration, :angle, :area, :concentr, :consumption, :digital, :duration,
 :electric, :energy, :force, :frequency, :graphics, :length, :light, :mass,
 :power, :pressure, :speed, :temperature, :torque, :volume]
iex(8)> per = Cldr.Unit.parse!( "2y")
#Cldr.Unit<:year, 2>
iex(9)> { :ok, :duration} = Cldr.Unit.unit_category( per)
** (MatchError) no match of right hand side value: {:ok, :year_duration}

If this is returning :year_duration is the desired behavior here, how do I get to know all possible return values for my duration units so I can match against them?

Errors on OTP 26 due to clause reordering

OTP 26 has an open issue that affects cldr_units. The main branch has new commits that mitigate the issue however a new version has not been published to hex. If there is not new OTP 26 version by 26 May I will publish an update. Please let me know if there is any more urgent requirement.

Dialyzer Error

:0:unknown_type
Unknown type: Math.decimal_or_number/0.

Version: 2.0.0

Cldr.Unit.Conversion does not deal with decimal values

As cldr is already requiring the decimal library, couldn't Cldr.Unit normalize to just using Decimal internally?

test "decimal" do
    unit = Cldr.Unit.new(Decimal.new("300"), :minute)

    hours = Cldr.Unit.Conversion.convert(unit, :hour)

    assert hours.unit == :hour
    assert Decimal.equal?(5, Cldr.Unit.to_value(hours))
end

extending unit categories and units for domain specific units

Hi,

We have a few specific categories and units in my domain (environmental footprints), e.g. transportation in person.km (person x km) or infrastructure in unit (for example for a building unit).

What would be the preferred way to extend the library with additional categories and units?

Thank you in advance for your answer.

Regressions from 3.16

Hi, we ran into a couple of issues updating this library from 3.16.4 to 3.17

1. Cldr.Unit.parse_unit_name returns string instead of atom

iex> Cldr.Unit.parse_unit_name("mg/dl")
# Before 3.17
{:ok, :milligram_ofglucose_per_deciliter}
# After 3.17
{:ok, "milligram_ofglucose_per_deciliter"}

2. Cannot format Unit with unit_name "mg/dl"

The following function call does not work in 3.17 any more

iex> Cldr.Unit.to_string!(Cldr.Unit.new!("milligram_ofglucose_per_deciliter", "120.5"), style: :short)
# Before 3.17
"120.5 mg/dL"
# After 3.17
** (Cldr.Unit.NoPatternError) No format pattern was found for unit Cldr.Unit.new!("milligram_ofglucose_per_deciliter", "120.5") with grammatical case :nominative, gender :neuter and plural type :other
     stacktrace:
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:807: Cldr.Unit.Format.get_unit_pattern!/3
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:714: Cldr.Unit.Format.do_iolist/3
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:682: Cldr.Unit.Format.do_iolist/3
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:631: Cldr.Unit.Format.to_iolist/4
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:639: Cldr.Unit.Format.to_iolist/4
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:470: Cldr.Unit.Format.to_iolist/3
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:201: Cldr.Unit.Format.to_string/3
       (ex_cldr_units 3.17.0) lib/cldr/unit/format.ex:316: Cldr.Unit.Format.to_string!/3

File "conversion_factors.json" missing on compile

When setting up a new project with:

defp deps do
  [
    {:jason, "~> 1.1"},
    {:ex_cldr_units, "~> 2.3"}
  ]
end

the compile steps fails with the following error:

==> ex_cldr_units
Compiling 8 files (.ex)

== Compilation error in file lib/cldr/conversion.ex ==
** (File.Error) could not read file "./priv/conversion_factors.json": no such file or directory
    (elixir) lib/file.ex:353: File.read!/1
    lib/cldr/conversion.ex:14: (module)
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
could not compile dependency :ex_cldr_units, "mix compile" failed. You can recompile this dependency with "mix deps.compile ex_cldr_units", update it with "mix deps.update ex_cldr_units" or clean it with "mix deps.clean ex_cldr_units"

I failed to see why the download mix task is not run and hope you can help.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.