Skip to content

CLDR-18365 investigate unit apis #4439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

macchiati
Copy link
Member

@macchiati macchiati commented Mar 3, 2025

CLDR-18365

  • This PR completes the ticket.

This is investigating different APIs and underlying mechanisms for conversion.
What turns out to be promising in terms of performance is to add the conversion information into the MeasureUnit, and use a (bounded) cache for those. That makes the usage very fast without needing to create / store a converter object.

Samples

double amount = 3.4d;

// Create measure units. The MeasureUnit2 simulates adding the conversion info to ICU's MeasureUnit

MeasureUnit2 mu1 = MeasureUnit2.forIdentifier("foot-per-second");
MeasureUnit2 mu2 = MeasureUnit2.forIdentifier("kilometer-per-hour");

// We could decide to use static for conversion

double result1static = MeasureUnit2.convert(amount, mu1, mu2);

// Or, the simpler non-static methods (I favor this)

double result2nonstatic = mu2.convertFrom(amount, mu1);

// And one could of course just create the MeasureUnits at the time of conversion

double result3d =
        MeasureUnit2.forIdentifier("kilometer-per-hour")
                .convertFrom(amount, MeasureUnit2.forIdentifier("foot-per-second"));

// We could also support converting Measures

Measure2 measure = new Measure2(amount, mu1);

double result2measure = mu2.convertFrom(measure);

// Support for mixed units is similar

MeasureUnit2 mu3 = MeasureUnit2.forIdentifier("foot-and-inch");
MeasureUnit2 mu4 = MeasureUnit2.forIdentifier("meter-and-centimeter");

List<Double> resultA = mu4.convertDoublesFrom(List.of(10d, 3d), mu3);
// returns List.of(3.0d, 12.42d)
// In this case, the MeasureUnit2 contains a list of subunits

Here is a speed comparison on my laptop:

iterations: 100,000,000
0.791 ns: 	Doubles w/existing MeasureUnit
6.88 ns: 	Doubles w/unbounded caller cache — ConcurrentHashMap
43.6 ns: 	Doubles w/bounded caller cache — LoadingCache
45 ns:  	Doubles
257 ns: 	BigDecimals (64)
1,560 ns: 	Rationals

Notes:

  • The best speed is when the caller uses MeasureUnits rather than strings. All the conversion info is right there.
  • The caller could also use their own cache (String ==> MeasureUnit), so I gave two variants. However, we shouldn't recommend the unbounded caller cache, unless the app only supports a closed set of units, otherwise there can be DOS attacks
  • The code supports 3 variants, of increasing precision (and decreasing speed).
    • For BigDecimals, I drop the iterations to 20%; and for Rationals to 20% of that.
  • For BigDecimal division, you need to limit the precision of any division; this code uses MathContext.DECIMAL64.
  • Rationals don't have that problem.

ALLOW_MANY_COMMITS=true

@macchiati macchiati marked this pull request as draft March 3, 2025 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant