From 3782cf1f88ac567dfd2e19810dbe9fe6c8faf04b Mon Sep 17 00:00:00 2001 From: OLEGSHA Date: Tue, 1 Dec 2020 01:28:18 +0300 Subject: [PATCH] Added unit registration to Units and Units.get methods These are just convenience methods: instead of writing float g = 9.8f * Units.METERS_PER_SECOND_SQUARED; we can write float g = Units.get("9.8 m/s^2"); and it'll actually work fast. --- .../ru/windcorp/progressia/common/Units.java | 257 +++++++++++++++++- 1 file changed, 251 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/windcorp/progressia/common/Units.java b/src/main/java/ru/windcorp/progressia/common/Units.java index 0c8348a..c66de67 100644 --- a/src/main/java/ru/windcorp/progressia/common/Units.java +++ b/src/main/java/ru/windcorp/progressia/common/Units.java @@ -1,14 +1,37 @@ package ru.windcorp.progressia.common; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import gnu.trove.TCollections; +import gnu.trove.map.TCharFloatMap; +import gnu.trove.map.TObjectFloatMap; +import gnu.trove.map.hash.TCharFloatHashMap; +import gnu.trove.map.hash.TObjectFloatHashMap; +import ru.windcorp.jputil.chars.StringUtil; +import ru.windcorp.progressia.common.util.crash.CrashReports; + public class Units { + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.FIELD) + public static @interface RegisteredUnit { + String[] value(); + } + // Base units // We're SI. + @RegisteredUnit("m") public static final float METERS = 1; public static final float KILOGRAMS = 1; + @RegisteredUnit("s") public static final float SECONDS = 1; - // Length + // Length public static final float CENTIMETERS = METERS / 100; public static final float MILLIMETERS = METERS / 1000; public static final float KILOMETERS = METERS * 1000; @@ -25,32 +48,254 @@ public class Units { public static final float CUBIC_MILLIMETERS = MILLIMETERS * MILLIMETERS * MILLIMETERS; public static final float CUBIC_KILOMETERS = KILOMETERS * KILOMETERS * KILOMETERS; - // Mass + // Mass + @RegisteredUnit("g") public static final float GRAMS = KILOGRAMS / 1000; + @RegisteredUnit("t") public static final float TONNES = KILOGRAMS * 1000; // Density public static final float KILOGRAMS_PER_CUBIC_METER = KILOGRAMS / CUBIC_METERS; public static final float GRAMS_PER_CUBIC_CENTIMETER = GRAMS / CUBIC_CENTIMETERS; - // Time + // Time public static final float MILLISECONDS = SECONDS / 1000; + @RegisteredUnit({"min", "mins", "minute", "minutes"}) public static final float MINUTES = SECONDS * 60; + @RegisteredUnit({"h", "hr", "hrs", "hour", "hours"}) public static final float HOURS = MINUTES * 60; + @RegisteredUnit({"d", "day", "days"}) public static final float DAYS = HOURS * 24; // Frequency + @RegisteredUnit("Hz") public static final float HERTZ = 1 / SECONDS; public static final float KILOHERTZ = HERTZ * 1000; - // Velocity + // Velocity public static final float METERS_PER_SECOND = METERS / SECONDS; public static final float KILOMETERS_PER_HOUR = KILOMETERS / HOURS; - // Acceleration + // Acceleration public static final float METERS_PER_SECOND_SQUARED = METERS_PER_SECOND / SECONDS; - // Force + // Force + @RegisteredUnit("N") public static final float NEWTONS = METERS_PER_SECOND_SQUARED * KILOGRAMS; + + static { + try { + registerUnits(Units.class); + } catch (IllegalAccessException e) { + CrashReports.report(e, "Could not register units declared in {}", Units.class.getName()); + } + } + + /* + * Utilities + */ + + private static final TObjectFloatMap UNITS_BY_NAME = createMap(); + + private static final TCharFloatMap PREFIXES_BY_CHAR; + static { + TCharFloatMap prefixes = new TCharFloatHashMap( + gnu.trove.impl.Constants.DEFAULT_CAPACITY, + gnu.trove.impl.Constants.DEFAULT_LOAD_FACTOR, + gnu.trove.impl.Constants.DEFAULT_CHAR_NO_ENTRY_VALUE, + Float.NaN + ); + + prefixes.put('G', 1e+9f); + prefixes.put('M', 1e+6f); + prefixes.put('k', 1e+3f); + prefixes.put('c', 1e-2f); + prefixes.put('m', 1e-3f); + prefixes.put('μ', 1e-6f); + + PREFIXES_BY_CHAR = TCollections.unmodifiableMap(prefixes); + } + + private static final TObjectFloatMap KNOWN_UNITS = createMap(); + + private static TObjectFloatMap createMap() { + return TCollections.synchronizedMap( + new TObjectFloatHashMap<>( + gnu.trove.impl.Constants.DEFAULT_CAPACITY, + gnu.trove.impl.Constants.DEFAULT_LOAD_FACTOR, + Float.NaN + ) + ); + } + + public static void registerUnits(Class source) throws IllegalAccessException { + for (Field field : source.getDeclaredFields()) { + int mods = field.getModifiers(); + + if (!Modifier.isPublic(mods)) continue; + if (!Modifier.isStatic(mods)) continue; + if (!Modifier.isFinal(mods)) continue; + if (field.getType() != Float.TYPE) continue; + + RegisteredUnit request = field.getAnnotation(RegisteredUnit.class); + float value = field.getFloat(null); // adding throws since we might not have accounted for something + registerUnit(value, request.value()); + } + } + + public static void registerUnit(float value, String... names) { + for (String name : names) { + if (!Float.isNaN(UNITS_BY_NAME.put(name, value))) { + throw new IllegalArgumentException("Duplicate unit name " + name); + } + } + } + + /** + * Returns the value of the unit described by {@code declar}. + * + *

The general form of a declaration is: + *

+	 * unit_declar       ::= [ws]unit_declar_part[[ws]"/"[ws]unit_declar_part][ws]
+	 * unit_declar_part  ::= unit_name_and_exp[[ws]"*"[ws]unit_name_and_exp]+
+	 * unit_name_and_exp ::= unit_name[[ws]"^"[ws]exponent]
+	 * unit_name         ::= [prefix]named_unit | special_unit
+	 * named_unit        ::= <any registered unit name, case-sensitive>
+	 * prefix            ::= "G" | "M" | "k" | "c" | "m" | "μ"
+	 * special_unit      ::= "1"
+	 * exponent          ::= <any float>
+	 * ws                ::= <any character <= 'U+0020'>+
+ * + * Examples: + *
    + *
  • seconds = {@code "s"}
  • + *
  • meters per second = {@code "m/s"}
  • + *
  • kilonewtons = {@code "kN"}
  • + *
  • square meters = {@code "m^2"}
  • + *
  • units per meter = {@code "1/m"}
  • + *
  • units of gravitational constant G = {@code "m^3/kg*s^2"} [sic] (see below)
  • + *
  • units (dimensionless) = {@code "1"}
  • + *
+ * + * Note that no more than one {@code '/'} is allowed per declaration, and no parenthesis are allowed at all. As such, + *
    + *
  • Multiple units under the division bar should be located after the single {@code '/'} and separated by {@code '*'}: + * gas constant ought to have {@code "J/K*mol"} units.
  • + *
  • Exponentiation of parenthesis should be expanded: (m/s)² = {@code "m^2/s^2"}.
  • + *
  • Exponents should also be used for expressing roots: √s = {@code "s^0.5"}.
  • + *
  • Exponents can be used to express division, but such use is generally discouraged.
  • + *
+ * + * @param unit unit declaration + * @throws IllegalArgumentException if the declaration is invalid + * @return the value of the unit + * + * @see #get(String) get(String) + * @see #registerUnit(float, String...) + */ + public static final float getUnitValue(String unit) { + float cached = KNOWN_UNITS.get(unit); + if (!Float.isNaN(cached)) return cached; + + float computed = computeUnitValue(unit); + KNOWN_UNITS.put(unit, computed); + return computed; + } + + private static float computeUnitValue(String unit) { + String[] parts = StringUtil.split(unit, '/'); + + assert parts != null && parts.length != 0; + + switch (parts.length) { + case 1: + return parseUnitValue(parts[0]); + case 2: + return parseUnitValue(parts[0]) / parseUnitValue(parts[1]); + default: + throw invalidUnit(unit, "unit declaration contains more than one '/'"); + } + } + + private static float parseUnitValue(String declar) { + String[] unitsAndExponents = StringUtil.split(declar, '*'); + + float result = 1; + for (String unitAndExponent : unitsAndExponents) { + String[] parts = StringUtil.split(unitAndExponent, '^'); + + float exponent; + + assert parts != null && parts.length != 0; + switch (parts.length) { + case 1: + exponent = 1; + break; + case 2: + exponent = Float.parseFloat(parts[1].trim()); + break; + default: + throw invalidUnit(unitAndExponent, "unit declaration contains more than one '^'"); + } + + String unitName = parts[0].trim(); + + float value = parseUnitAsNamed(unitName); + if (Float.isNaN(value)) value = parseUnitAsNamedAndPrefixed(unitName); + if (Float.isNaN(value)) value = parseUnitAsSpecial(unitName); + if (Float.isNaN(value)) throw invalidUnit(unitName, "unknown unit name or unknown prefix or unknown special unit"); + + if (exponent != 1) { + value = (float) Math.pow(value, exponent); + } + + result *= value; + } + return result; + } + + private static float parseUnitAsNamed(String namedUnit) { + return UNITS_BY_NAME.get(namedUnit); + } + + private static float parseUnitAsNamedAndPrefixed(String namedUnit) { + if (namedUnit.length() < 2) return Float.NaN; + + float value = PREFIXES_BY_CHAR.get(namedUnit.charAt(0)); + if (!Float.isNaN(value)) value *= parseUnitAsNamed(namedUnit.substring(1)); + return value; + } + + private static float parseUnitAsSpecial(String namedUnit) { + return namedUnit.equals("1") ? 1 : Float.NaN; + } + + private static RuntimeException invalidUnit(String unit, String details) { + return new IllegalArgumentException("Invalid unit declaration \"" + unit + "\": " + details); + } + + public static double get(double amount, String unit) { + return amount * getUnitValue(unit); + } + + public static float get(float amount, String unit) { + return amount * getUnitValue(unit); + } + + public static float get(String declar) { + String[] parts = StringUtil.split(declar, ' ', 2); + assert parts != null && parts.length != 0; + if (parts[1] == null) throw new IllegalArgumentException("No space (' ') found"); + assert parts[0] == parts[0].trim(); + return Float.parseFloat(parts[0]) * getUnitValue(parts[1]); + } + + public static double getd(String declar) { + String[] parts = StringUtil.split(declar, ' ', 2); + assert parts != null && parts.length != 0; + if (parts[1] == null) throw new IllegalArgumentException("No space (' ') found"); + assert parts[0] == parts[0].trim(); + return Double.parseDouble(parts[0]) * getUnitValue(parts[1]); + } }