@@ -11,8 +11,12 @@ defmodule Localize.Unit.CustomRegistry do
1111
1212 Each definition is a map with the following keys:
1313
14- * `:base_unit` (required) — the CLDR base unit this custom unit converts to
15- (e.g., `"meter"`, `"kilogram"`, `"second"`).
14+ * `:base_unit` (required) — the CLDR unit this custom unit converts to
15+ (e.g., `"meter"`, `"kilogram"`, `"second"`). It need not be a
16+ fundamental base unit: a derived unit such as `"day"` is accepted and
17+ folded down to its fundamental base (`"second"`) at registration, with
18+ the factor and offset adjusted accordingly, so the custom unit stays
19+ convertible against every CLDR unit in the same category.
1620
1721 * `:factor` (required) — the conversion factor:
1822 `1 custom_unit = factor * base_unit`.
@@ -129,9 +133,10 @@ defmodule Localize.Unit.CustomRegistry do
129133 def register ( name , definition ) do
130134 with :ok <- validate_name ( name ) ,
131135 :ok <- validate_definition ( definition ) ,
132- :ok <- validate_no_collision ( name ) do
136+ :ok <- validate_no_collision ( name ) ,
137+ { :ok , normalized } <- normalize_base ( definition ) do
133138 current = all ( )
134- :persistent_term . put ( @ persistent_term_key , Map . put ( current , name , definition ) )
139+ :persistent_term . put ( @ persistent_term_key , Map . put ( current , name , normalized ) )
135140 :ok
136141 end
137142 end
@@ -173,8 +178,9 @@ defmodule Localize.Unit.CustomRegistry do
173178 Enum . reduce ( definitions , % { } , fn { name , definition } , acc ->
174179 with :ok <- validate_name ( name ) ,
175180 :ok <- validate_definition ( definition ) ,
176- :ok <- validate_no_collision ( name ) do
177- Map . put ( acc , name , definition )
181+ :ok <- validate_no_collision ( name ) ,
182+ { :ok , normalized } <- normalize_base ( definition ) do
183+ Map . put ( acc , name , normalized )
178184 else
179185 { :error , _reason } -> acc
180186 end
@@ -322,6 +328,67 @@ defmodule Localize.Unit.CustomRegistry do
322328 :ok
323329 end
324330
331+ # ── Base-unit normalization ──
332+
333+ # A custom unit's `:base_unit` must be a fundamental CLDR base unit,
334+ # because compatibility and conversion compare fully-reduced base
335+ # units: `compatible?/2` succeeds only when both units reduce to the
336+ # same base. When a definition is expressed in terms of a *derived*
337+ # unit (e.g. `%{base_unit: "day", factor: 1}`), storing "day"
338+ # verbatim leaves the custom unit reducing to "day" while every CLDR
339+ # unit reduces "day" to "second" — so the two never match.
340+ #
341+ # Fold the derived unit's own conversion into the custom factor and
342+ # offset so the stored base becomes the fundamental base with an
343+ # equivalent factor: `guestnight` given as `base_unit: "day",
344+ # factor: 1` is stored as `base_unit: "second", factor: 86400.0`.
345+ # A definition already expressed against a fundamental base is stored
346+ # unchanged, and `:special` (function-based) definitions — which do
347+ # not convert via a linear factor — are left untouched.
348+ defp normalize_base ( % { factor: factor } = definition ) when is_number ( factor ) do
349+ base = definition . base_unit
350+
351+ case Localize.Unit.BaseUnit . base_unit ( base ) do
352+ { :ok , ^ base } ->
353+ { :ok , definition }
354+
355+ { :ok , true_base } ->
356+ with { :ok , base_factor , base_offset } <- base_conversion ( base , true_base ) do
357+ offset = Map . get ( definition , :offset , 0.0 )
358+
359+ { :ok ,
360+ definition
361+ |> Map . put ( :base_unit , true_base )
362+ |> Map . put ( :factor , factor * base_factor )
363+ |> Map . put ( :offset , offset * base_factor + base_offset ) }
364+ end
365+
366+ { :error , _reason } ->
367+ { :error , "unknown base unit: #{ inspect ( base ) } " }
368+ end
369+ end
370+
371+ defp normalize_base ( definition ) , do: { :ok , definition }
372+
373+ # The linear parameters `{factor, offset}` that convert one unit of
374+ # `base` into `true_base`, such that
375+ # `value_in_true_base = value_in_base * factor + offset`. For a simple
376+ # derived unit CLDR stores these directly; for a compound derived unit
377+ # they are recovered from two reference conversions (the offset is the
378+ # image of zero, the factor the difference across one unit).
379+ defp base_conversion ( base , true_base ) do
380+ case Localize.Unit.Data . conversion_factor_raw ( base ) do
381+ % { factor: factor , offset: offset } when is_number ( factor ) ->
382+ { :ok , factor , offset }
383+
384+ _other ->
385+ with { :ok , at_zero } <- Localize.Unit.Conversion . convert ( 0 , base , true_base ) ,
386+ { :ok , at_one } <- Localize.Unit.Conversion . convert ( 1 , base , true_base ) do
387+ { :ok , at_one - at_zero , at_zero }
388+ end
389+ end
390+ end
391+
325392 # ── Validation ──
326393
327394 @ name_pattern ~r/ ^[a-z][a-z0-9_-]*$/
0 commit comments