From 2d9c4361aa09e1b2b60b77199e96c514cb793ffd Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 15:44:01 +0200 Subject: [PATCH 1/9] feat(SNSUNI-145): Add acoustic quantities: SoundPower, SoundPressure, Frequency. --- .github/ISSUE_TEMPLATE/feature_request.md | 2 + README.md | 19 +- .../unitility/unitsystem/Constants.java | 31 ++++ .../unitsystem/acoustic/SoundPower.java | 111 ++++++++++++ .../unitsystem/acoustic/SoundPowerUnits.java | 77 ++++++++ .../unitsystem/acoustic/SoundPressure.java | 107 +++++++++++ .../acoustic/SoundPressureUnits.java | 77 ++++++++ .../unitsystem/oscillation/Frequency.java | 169 ++++++++++++++++++ .../unitsystem/oscillation/FrequencyUnit.java | 8 + .../oscillation/FrequencyUnits.java | 57 ++++++ ...PhysicalQuantityDefaultParsingFactory.java | 40 +++-- .../util/SupportedQuantitiesRegistry.java | 13 +- .../unitsystem/acoustic/SoundPowerTest.java | 72 ++++++++ .../acoustic/SoundPressureTest.java | 72 ++++++++ .../unitsystem/oscillation/FrequencyTest.java | 88 +++++++++ .../PhysicalQuantityParsingFactoryTest.java | 16 +- .../util/SupportedQuantitiesRegistryTest.java | 2 +- ...ysicalQuantityJacksonDeserializerTest.java | 9 + .../acoustic/SoundPowerPlainSIConverter.java | 25 +++ .../SoundPressurePlainSIConverter.java | 25 +++ .../FrequencyPlainSIConverter.java | 25 +++ .../plainsivalue/PlainSiConverterTest.java | 74 +++++++- 22 files changed, 1089 insertions(+), 30 deletions(-) create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/Constants.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/Frequency.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnits.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/oscillation/FrequencyTest.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPowerPlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPressurePlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/oscillation/FrequencyPlainSIConverter.java diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 298fde7..5d9614e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -13,6 +13,7 @@ Example: "I need to convert between grains per pound and kg/kg but Unitility doe **Describe the solution you'd like** A clear and concise description of what you would like to see implemented. Example: "Add a `GrainsPerPound` unit and extend `HumidityRatio` to support parsing from this unit." +Provide information what is current behaviour, and what is the expected result. **Describe alternatives you've considered** Have you tried working around the issue? @@ -20,3 +21,4 @@ Example: "I wrote a custom unit / custom parsing factory, but it would be great **Additional context** Include any references, code snippets, or use cases that explain why this feature is valuable. +Provide list of physical quantities or additional units you need, so they could be added in next release. diff --git a/README.md b/README.md index fd06d5a..a3bea5f 100644 --- a/README.md +++ b/README.md @@ -184,23 +184,30 @@ units and at least one Imperial unit. #### HYDRAULIC: -* LinearResistance: Pascal per meter [Pa/m], Inch of water per 100 feet [inH₂O/100ft], Inch of mercury per 100 feet [inHg/100ft] -* FrictionFactor: [-] -* LocalLossFactor: [-] -* Rotation Speed To Flow Rate Ratio: Radians per second per meter per second [rad·s⁻¹/m³·s⁻¹], revolutions per minute per gallons per minute [rpm/gpm] +* Linear resistance: Pascal per meter [Pa/m], Inch of water per 100 feet [inH₂O/100ft], Inch of mercury per 100 feet [inHg/100ft] +* Friction factor: [-] +* LocalLoss factor: [-] +* Rotation speed To Flow Rate Ratio: Radians per second per meter per second [rad·s⁻¹/m³·s⁻¹], revolutions per minute per gallons per minute [rpm/gpm] #### DIMENSIONLESS: * Grashof number, Prandtl number, Reynolds number, Bypass factor +#### ACOUSTIC: +* Sound power: Watt [W], Decibel [dB] +* Sound pressure: Pascal [Pa], Decibel [dB] + +#### OSCILLATION: +* Frequency: Hertz [Hz], Kilohertz [kHz], Megahertz [MHz], Gigahertz [GHz], Cycles per minute [cpm] + #### SPECIAL TYPES: #### Geographic: * Latitude: degrees [°], radians [rad] * Longitude: degrees [°], radians [rad] * Bearing: degrees [°] -* GeoCoordinate: [latitude, longitude] -* GeoDistance: meter [m], kilometer [km], mile [mi], nautical mile [nmi]
+* Geo coordinate: [latitude, longitude] +* Geo distance: meter [m], kilometer [km], mile [mi], nautical mile [nmi]
All Geographic quantities can be constructed from DMS format (degrees-minutes-seconds), for i.e.: 20°7'22.8"S. diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/Constants.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/Constants.java new file mode 100644 index 0000000..0692255 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/Constants.java @@ -0,0 +1,31 @@ +package com.synerset.unitility.unitsystem; + +public class Constants { + + private Constants() { + throw new IllegalStateException("Utility class"); + } + + // SI Prefix Factors + public static final double PICO = 1e-12; + public static final double NANO = 1e-9; + public static final double MICRO = 1e-6; + public static final double MILLI = 1e-3; + public static final double CENTI = 1e-2; + public static final double DECI = 1e-1; + + public static final double DECA = 1e1; + public static final double HECTO = 1e2; + public static final double KILO = 1e3; + public static final double MEGA = 1e6; + public static final double GIGA = 1e9; + public static final double TERA = 1e12; + + // Physical constants + public static final double GRAVITY_SI = 9.80665; + + // Canonical Time + public static final double SECONDS_IN_MINUTE = 60; + public static final double SECONDS_IN_HOUR = 3600; + +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java new file mode 100644 index 0000000..98073d0 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java @@ -0,0 +1,111 @@ +package com.synerset.unitility.unitsystem.acoustic; + +import com.synerset.unitility.unitsystem.CalculableQuantity; +import com.synerset.unitility.unitsystem.oscillation.Frequency; +import com.synerset.unitility.unitsystem.oscillation.FrequencyUnit; +import com.synerset.unitility.unitsystem.oscillation.FrequencyUnits; +import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; + +import java.util.Objects; + +public class SoundPower implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final PowerUnit unitType; + + public SoundPower(double value, PowerUnit unitType) { + this.unitType = unitType == null ? SoundPowerUnits.WATT : unitType; + this.value = value; + this.baseValue = this.unitType.toValueInBaseUnit(value); + } + + // Static factories + public static SoundPower of(double value, PowerUnit unit) { + return new SoundPower(value, unit); + } + + public static SoundPower of(double value, String unitSymbol) { + PowerUnit resolvedUnit = SoundPowerUnits.fromSymbol(unitSymbol); + return new SoundPower(value, resolvedUnit); + } + + public static SoundPower ofWatts(double value) { + return new SoundPower(value, SoundPowerUnits.WATT); + } + + public static SoundPower ofDecibels(double value) { + return new SoundPower(value, SoundPowerUnits.DECIBEL); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public PowerUnit getUnit() { + return unitType; + } + + @Override + public SoundPower toBaseUnit() { + return of(baseValue, SoundPowerUnits.WATT); + } + + @Override + public SoundPower toUnit(PowerUnit targetUnit) { + double targetVal = targetUnit.fromValueInBaseUnit(baseValue); + return of(targetVal, targetUnit); + } + + @Override + public SoundPower toUnit(String targetSymbol) { + return toUnit(SoundPowerUnits.fromSymbol(targetSymbol)); + } + + @Override + public SoundPower withValue(double value) { + return of(value, unitType); + } + + // Convenience converters + public SoundPower toWatts() { + return toUnit(SoundPowerUnits.WATT); + } + + public SoundPower toDecibels() { + return toUnit(SoundPowerUnits.DECIBEL); + } + + public double getInWatts() { + return getInUnit(SoundPowerUnits.WATT); + } + + public double getInDecibels() { + return getInUnit(SoundPowerUnits.DECIBEL); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SoundPower that)) return false; + return Double.compare(that.baseValue, baseValue) == 0 + && Objects.equals(unitType.getBaseUnit(), that.unitType.getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return value + " " + unitType.getSymbol(); + } +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerUnits.java new file mode 100644 index 0000000..2136f4a --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerUnits.java @@ -0,0 +1,77 @@ +package com.synerset.unitility.unitsystem.acoustic; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; +import com.synerset.unitility.unitsystem.thermodynamic.PowerUnits; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum SoundPowerUnits implements PowerUnit { + // Assumed reference power = 1E-12 W. + + WATT("W", val -> val, val -> val), + DECIBEL("dB", db -> Math.pow(10.0, db / 10.0) * 1E-12, w -> 10.0 * Math.log10(w / 1E-12)); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + SoundPowerUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public SoundPowerUnits getBaseUnit() { + return WATT; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static PowerUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return WATT; + } + String requestedSymbol = unifySymbol(rawSymbol); + + for (SoundPowerUnits unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equalsIgnoreCase(requestedSymbol)) { + return unit; + } + } + + PowerUnit foundTargetUnit = PowerUnits.fromSymbol(requestedSymbol); + + if (foundTargetUnit == null) { + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + SoundPowerUnits.class.getSimpleName()); + } + + return foundTargetUnit; + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimLowerAndClean() + .unifyMultiAndDiv() + .replace("dbl", "db") + .toString(); + } + +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java new file mode 100644 index 0000000..b4bc014 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java @@ -0,0 +1,107 @@ +package com.synerset.unitility.unitsystem.acoustic; + +import com.synerset.unitility.unitsystem.CalculableQuantity; +import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; +import com.synerset.unitility.unitsystem.thermodynamic.PressureUnit; + +import java.util.Objects; + +public class SoundPressure implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final PressureUnit unit; + + public SoundPressure(double value, PressureUnit unit) { + this.unit = unit == null ? SoundPressureUnits.PASCAL : unit; + this.value = value; + this.baseValue = this.unit.toValueInBaseUnit(value); + } + + public static SoundPressure of(double value, PressureUnit unit) { + return new SoundPressure(value, unit); + } + + public static SoundPressure of(double value, String unitSymbol) { + PressureUnit resolvedUnit = SoundPressureUnits.fromSymbol(unitSymbol); + return new SoundPressure(value, resolvedUnit); + } + + public static SoundPressure ofPascals(double value) { + return new SoundPressure(value, SoundPressureUnits.PASCAL); + } + + public static SoundPressure ofDecibels(double value) { + return new SoundPressure(value, SoundPressureUnits.DECIBEL); + } + + @Override + public SoundPressure toBaseUnit() { + return of(baseValue, SoundPressureUnits.PASCAL); + } + + @Override + public SoundPressure toUnit(PressureUnit targetUnit) { + double targetVal = targetUnit.fromValueInBaseUnit(baseValue); + return of(targetVal, targetUnit); + } + + @Override + public SoundPressure toUnit(String targetSymbol) { + return toUnit(SoundPressureUnits.fromSymbol(targetSymbol)); + } + + @Override + public SoundPressure withValue(double value) { + return of(value, unit); + } + + public SoundPressure toPascals() { + return toUnit(SoundPressureUnits.PASCAL); + } + + public SoundPressure toDecibels() { + return toUnit(SoundPressureUnits.DECIBEL); + } + + public double getInPascals() { + return getInUnit(SoundPressureUnits.PASCAL); + } + + public double getInDecibels() { + return getInUnit(SoundPressureUnits.DECIBEL); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public PressureUnit getUnit() { + return unit; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SoundPressure that)) return false; + return Double.compare(that.baseValue, baseValue) == 0 + && Objects.equals(unit.getBaseUnit(), that.unit.getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unit.getBaseUnit()); + } + + @Override + public String toString() { + return value + " " + unit.getSymbol(); + } +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureUnits.java new file mode 100644 index 0000000..8df9045 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureUnits.java @@ -0,0 +1,77 @@ +package com.synerset.unitility.unitsystem.acoustic; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.thermodynamic.PressureUnit; +import com.synerset.unitility.unitsystem.thermodynamic.PressureUnits; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum SoundPressureUnits implements PressureUnit { + // Assumed reference pressure = 2E-5 Pa. + + PASCAL("Pa", val -> val, val -> val), + DECIBEL("dB", db -> Math.pow(10.0, db / 20.0) * 2E-5, pa -> 20.0 * Math.log10(pa / 2E-5)); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + SoundPressureUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public SoundPressureUnits getBaseUnit() { + return PASCAL; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static PressureUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return PASCAL; + } + String requestedSymbol = unifySymbol(rawSymbol); + + for (SoundPressureUnits unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equalsIgnoreCase(requestedSymbol)) { + return unit; + } + } + PressureUnit foundTargetUnit = PressureUnits.fromSymbol(requestedSymbol); + + if (foundTargetUnit == null) { + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + SoundPressureUnits.class.getSimpleName()); + } + + return foundTargetUnit; + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimLowerAndClean() + .replace("dba", "db") + .unifyAerialAndVol() + .toString(); + } + + +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/Frequency.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/Frequency.java new file mode 100644 index 0000000..6e61bcf --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/Frequency.java @@ -0,0 +1,169 @@ +package com.synerset.unitility.unitsystem.oscillation; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +// NOTE: This implementation assumes the existence of 'FrequencyUnit', 'FrequencyUnits' +// and a 'Constants' class with KILO, MEGA, GIGA, and SECONDS_IN_MINUTE fields. +public class Frequency implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final FrequencyUnit unitType; + + public Frequency(double value, FrequencyUnit unitType) { + this.value = value; + // Assuming FrequencyUnits.HERTZ is the base unit + if (unitType == null) { + unitType = FrequencyUnits.HERTZ; + } + this.unitType = unitType; + // Assuming FrequencyUnit has toValueInBaseUnit method + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Frequency of(double value, FrequencyUnit unit) { + return new Frequency(value, unit); + } + + public static Frequency of(double value, String unitSymbol) { + // Assuming FrequencyUnits has fromSymbol method + FrequencyUnit resolvedUnit = FrequencyUnits.fromSymbol(unitSymbol); + return new Frequency(value, resolvedUnit); + } + + // Missing factory methods + public static Frequency ofHertz(double value) { + return new Frequency(value, FrequencyUnits.HERTZ); + } + + public static Frequency ofKiloHertz(double value) { + return new Frequency(value, FrequencyUnits.KILOHERTZ); + } + + public static Frequency ofMegaHertz(double value) { + return new Frequency(value, FrequencyUnits.MEGAHERTZ); + } + + public static Frequency ofGigaHertz(double value) { + return new Frequency(value, FrequencyUnits.GIGAHERTZ); + } + + public static Frequency ofCyclesPerMinute(double value) { + return new Frequency(value, FrequencyUnits.CYCLES_PER_MINUTE); + } + // End of missing factory methods + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public FrequencyUnit getUnit() { + return unitType; + } + + @Override + public Frequency toBaseUnit() { + // Assuming FrequencyUnit has toValueInBaseUnit and getBaseUnit methods + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Frequency toUnit(FrequencyUnit targetUnit) { + // Assuming FrequencyUnit has toValueInBaseUnit and fromValueInBaseUnit methods + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Frequency.of(valueInTargetUnit, targetUnit); + } + + @Override + public Frequency toUnit(String targetUnit) { + // Assuming FrequencyUnits has fromSymbol method + FrequencyUnit resolvedUnit = FrequencyUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Frequency withValue(double value) { + return Frequency.of(value, unitType); + } + + // Missing specific toUnit methods (Conversion methods) + public Frequency toHertz() { + return toUnit(FrequencyUnits.HERTZ); + } + + public Frequency toKiloHertz() { + return toUnit(FrequencyUnits.KILOHERTZ); + } + + public Frequency toMegaHertz() { + return toUnit(FrequencyUnits.MEGAHERTZ); + } + + public Frequency toGigaHertz() { + return toUnit(FrequencyUnits.GIGAHERTZ); + } + + public Frequency toCyclesPerMinute() { + return toUnit(FrequencyUnits.CYCLES_PER_MINUTE); + } + // End of missing specific toUnit methods + + // Missing specific getInUnit methods (Getters) + public double getInHertz() { + return getInUnit(FrequencyUnits.HERTZ); + } + + public double getInKiloHertz() { + return getInUnit(FrequencyUnits.KILOHERTZ); + } + + public double getInMegaHertz() { + return getInUnit(FrequencyUnits.MEGAHERTZ); + } + + public double getInGigaHertz() { + return getInUnit(FrequencyUnits.GIGAHERTZ); + } + + public double getInCyclesPerMinute() { + return getInUnit(FrequencyUnits.CYCLES_PER_MINUTE); + } + // End of missing specific getInUnit methods + + // Standard overridden methods (equals, hashCode, toString) + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Frequency inputQuantity = (Frequency) o; + // Comparison in base unit for value, base unit for unit type + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + // Hash based on base value and base unit type + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + // Using Angle's toString logic as a reference + String separator = " "; + // Assuming FrequencyUnit has getSymbol method + return "Frequency{" + value + separator + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnit.java new file mode 100644 index 0000000..bcd28ff --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.oscillation; + +import com.synerset.unitility.unitsystem.Unit; + +public interface FrequencyUnit extends Unit { + @Override + FrequencyUnit getBaseUnit(); +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnits.java new file mode 100644 index 0000000..6eae8b2 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/oscillation/FrequencyUnits.java @@ -0,0 +1,57 @@ +package com.synerset.unitility.unitsystem.oscillation; + +import com.synerset.unitility.unitsystem.Constants; + +import java.util.function.DoubleUnaryOperator; + +public enum FrequencyUnits implements FrequencyUnit { + + HERTZ("Hz", val -> val, val -> val), + KILOHERTZ("kHz", val -> val * Constants.KILO, val -> val / Constants.KILO), + MEGAHERTZ("MHz", val -> val * Constants.MEGA, val -> val / Constants.MEGA), + GIGAHERTZ("GHz", val -> val * Constants.GIGA, val -> val / Constants.GIGA), + CYCLES_PER_MINUTE("cpm", val -> val / Constants.SECONDS_IN_MINUTE, val -> val * Constants.SECONDS_IN_MINUTE); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + FrequencyUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public FrequencyUnits getBaseUnit() { + return HERTZ; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static FrequencyUnits fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return HERTZ; + } + String s = rawSymbol.trim().toLowerCase(); + for (FrequencyUnits unit : values()) { + if (unit.getSymbol().toLowerCase().equals(s)) { + return unit; + } + } + throw new IllegalArgumentException("Unsupported unit symbol: " + rawSymbol); + } +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java index f4edaf6..6dfb3b7 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java @@ -2,6 +2,10 @@ import com.synerset.unitility.unitsystem.PhysicalQuantity; import com.synerset.unitility.unitsystem.Unit; +import com.synerset.unitility.unitsystem.acoustic.SoundPower; +import com.synerset.unitility.unitsystem.acoustic.SoundPowerUnits; +import com.synerset.unitility.unitsystem.acoustic.SoundPressure; +import com.synerset.unitility.unitsystem.acoustic.SoundPressureUnits; import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.*; import com.synerset.unitility.unitsystem.flow.MassFlow; @@ -17,6 +21,8 @@ import com.synerset.unitility.unitsystem.humidity.RelativeHumidityUnits; import com.synerset.unitility.unitsystem.hydraulic.*; import com.synerset.unitility.unitsystem.mechanical.*; +import com.synerset.unitility.unitsystem.oscillation.Frequency; +import com.synerset.unitility.unitsystem.oscillation.FrequencyUnits; import com.synerset.unitility.unitsystem.thermodynamic.*; import java.util.Map; @@ -88,28 +94,33 @@ private PhysicalQuantityDefaultParsingFactory() { // Geographic Map.entry(Latitude.class, Latitude::of), Map.entry(Longitude.class, Longitude::of), - Map.entry(Bearing.class, Bearing::of) + Map.entry(Bearing.class, Bearing::of), + // Acoustic + Map.entry(SoundPower.class, SoundPower::of), + Map.entry(SoundPressure.class, SoundPressure::of), + // Oscillation + Map.entry(Frequency.class, Frequency::of) ); // Initializing immutable default unit registry this.immutableDefaultUnitRegistry = Map.ofEntries( - // Common (15) + // Common (16) Map.entry(Angle.class, AngleUnits.RADIANS), + Map.entry(AngularVelocity.class, AngularVelocityUnits.RADIANS_PER_SECOND), Map.entry(Area.class, AreaUnits.SQUARE_METER), + Map.entry(Curvature.class, CurvatureUnits.RADIANS_PER_METER), Map.entry(Distance.class, DistanceUnits.METER), + Map.entry(Diameter.class, DistanceUnits.METER), + Map.entry(Height.class, DistanceUnits.METER), Map.entry(Length.class, DistanceUnits.METER), Map.entry(Width.class, DistanceUnits.METER), - Map.entry(Height.class, DistanceUnits.METER), - Map.entry(Diameter.class, DistanceUnits.METER), + Map.entry(LinearMassDensity.class, LinearMassDensityUnits.KILOGRAM_PER_METER), + Map.entry(Mass.class, MassUnits.KILOGRAM), Map.entry(Perimeter.class, DistanceUnits.METER), + Map.entry(Ratio.class, RatioUnits.PERCENT), Map.entry(Thickness.class, DistanceUnits.METER), - Map.entry(Mass.class, MassUnits.KILOGRAM), - Map.entry(LinearMassDensity.class, LinearMassDensityUnits.KILOGRAM_PER_METER), Map.entry(Velocity.class, VelocityUnits.METER_PER_SECOND), - Map.entry(AngularVelocity.class, AngularVelocityUnits.RADIANS_PER_SECOND), Map.entry(Volume.class, VolumeUnits.CUBIC_METER), - Map.entry(Ratio.class, RatioUnits.PERCENT), - Map.entry(Curvature.class, CurvatureUnits.RADIANS_PER_METER), // Dimensionless (5) Map.entry(GenericDimensionless.class, GenericDimensionlessUnits.DIMENSIONLESS), Map.entry(BypassFactor.class, BypassFactorUnits.DIMENSIONLESS), @@ -123,12 +134,12 @@ private PhysicalQuantityDefaultParsingFactory() { Map.entry(HumidityRatio.class, HumidityRatioUnits.KILOGRAM_PER_KILOGRAM), Map.entry(RelativeHumidity.class, RelativeHumidityUnits.DECIMAL), // Hydraulic (5) - Map.entry(LinearResistance.class, LinearResistanceUnits.PASCAL_PER_METER), + Map.entry(AbsoluteRoughness.class, DistanceUnits.METER), Map.entry(FrictionFactor.class, FrictionFactorUnits.DIMENSIONLESS), + Map.entry(LinearResistance.class, LinearResistanceUnits.PASCAL_PER_METER), Map.entry(LocalLossFactor.class, LocalLossFactorUnits.DIMENSIONLESS), Map.entry(RotationSpeedToFlowRateRatio.class, RotationSpeedToFlowRateRatioUnits.RADIAN_PER_SECOND_PER_CUBIC_METER_PER_SECOND), Map.entry(SDR.class, RatioUnits.DECIMAL), - Map.entry(AbsoluteRoughness.class, DistanceUnits.METER), // Mechanical (3) Map.entry(Force.class, ForceUnits.NEWTON), Map.entry(Momentum.class, MomentumUnits.KILOGRAM_METER_PER_SECOND), @@ -148,7 +159,12 @@ private PhysicalQuantityDefaultParsingFactory() { // Geographic (3) Map.entry(Latitude.class, AngleUnits.DEGREES), Map.entry(Longitude.class, AngleUnits.DEGREES), - Map.entry(Bearing.class, AngleUnits.DEGREES) + Map.entry(Bearing.class, AngleUnits.DEGREES), + // Acoustic (2) + Map.entry(SoundPower.class, SoundPowerUnits.WATT), + Map.entry(SoundPressure.class, SoundPressureUnits.PASCAL), + // Oscillation (1) + Map.entry(Frequency.class, FrequencyUnits.HERTZ) ); } diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java index db90bca..7735631 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java @@ -2,6 +2,10 @@ import com.synerset.unitility.unitsystem.PhysicalQuantity; import com.synerset.unitility.unitsystem.Unit; +import com.synerset.unitility.unitsystem.acoustic.SoundPower; +import com.synerset.unitility.unitsystem.acoustic.SoundPowerUnits; +import com.synerset.unitility.unitsystem.acoustic.SoundPressure; +import com.synerset.unitility.unitsystem.acoustic.SoundPressureUnits; import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.*; import com.synerset.unitility.unitsystem.flow.MassFlow; @@ -18,6 +22,8 @@ import com.synerset.unitility.unitsystem.humidity.RelativeHumidityUnits; import com.synerset.unitility.unitsystem.hydraulic.*; import com.synerset.unitility.unitsystem.mechanical.*; +import com.synerset.unitility.unitsystem.oscillation.Frequency; +import com.synerset.unitility.unitsystem.oscillation.FrequencyUnits; import com.synerset.unitility.unitsystem.thermodynamic.*; import java.util.*; @@ -93,7 +99,12 @@ private SupportedQuantitiesRegistry() { Map.entry(Latitude.class, () -> Arrays.asList(AngleUnits.values())), Map.entry(Longitude.class, () -> Arrays.asList(AngleUnits.values())), Map.entry(GeoDistance.class, () -> Arrays.asList(DistanceUnits.values())), - Map.entry(Bearing.class, () -> Arrays.asList(AngleUnits.values())) + Map.entry(Bearing.class, () -> Arrays.asList(AngleUnits.values())), + // Acoustic + Map.entry(SoundPower.class, () -> Arrays.asList(SoundPowerUnits.values())), + Map.entry(SoundPressure.class, () -> Arrays.asList(SoundPressureUnits.values())), + // Oscillation + Map.entry(Frequency.class, () -> Arrays.asList(FrequencyUnits.values())) ); } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerTest.java new file mode 100644 index 0000000..6c4db89 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPowerTest.java @@ -0,0 +1,72 @@ +package com.synerset.unitility.unitsystem.acoustic; + +import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; + +class SoundPowerTest { + + @Test + @DisplayName("SoundPower: should convert to W from dB and vice versa") + void shouldProperlyConvertToWattFromDecibel() { + // Given + SoundPower initialInDb = SoundPower.ofDecibels(120.0); + + // When + SoundPower actualInWatt = initialInDb.toBaseUnit(); + SoundPower actualInDb = actualInWatt.toUnit(SoundPowerUnits.DECIBEL); + double actualInWattVal = actualInWatt.getInWatts(); + double actualInDbVal = actualInWatt.getInDecibels(); + + // Then + SoundPower expectedInWatt = SoundPower.ofWatts(1.0); + assertThat(actualInWatt.getValue()).isEqualTo(actualInWattVal); + assertThat(actualInDb.getValue()).isEqualTo(actualInDbVal); + assertThat(actualInWatt).isEqualTo(expectedInWatt); + assertThat(actualInDb).isEqualTo(initialInDb); + } + + @Test + @DisplayName("SoundPower: should return 0 dB for reference power 1e-12 W") + void shouldReturnZeroDbForReferencePower() { + // Given + SoundPower referencePower = SoundPower.ofWatts(1e-12); + + // When + double actualInDb = referencePower.getInDecibels(); + + // Then + assertThat(actualInDb).isEqualTo(0.0, withPrecision(1E-12)); + } + + @Test + @DisplayName("SoundPower: should have W as base unit") + void shouldHaveWattAsBaseUnit() { + // Given + SoundPowerUnits expectedBaseUnit = SoundPowerUnits.WATT; + + // When + SoundPower soundPower = SoundPower.ofDecibels(90.0); + PowerUnit actualBaseUnit = soundPower.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("SoundPower: should be able to resolve unit from string when PowerUnit in kW is provided") + void shouldResolveUnitFromStringWhenPowerUnitInKWIsProvided() { + // Given + SoundPower soundPower = SoundPower.of(0.001, "kW"); + + // When + double actualInDb = soundPower.getInDecibels(); + + // Then + assertThat(actualInDb).isEqualTo(120, withPrecision(1E-12)); + } + +} diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureTest.java new file mode 100644 index 0000000..e38e5b5 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/acoustic/SoundPressureTest.java @@ -0,0 +1,72 @@ +package com.synerset.unitility.unitsystem.acoustic; + +import com.synerset.unitility.unitsystem.thermodynamic.PressureUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; + +class SoundPressureTest { + + @Test + @DisplayName("should convert to Pa from dB and vice versa") + void shouldProperlyConvertToPascalFromDecibel() { + // Given + SoundPressure initialInDb = SoundPressure.ofDecibels(94.0); + + // When + SoundPressure actualInPa = initialInDb.toBaseUnit(); + SoundPressure actualInDb = actualInPa.toUnit(SoundPressureUnits.DECIBEL); + double actualInPaVal = actualInPa.getInPascals(); + double actualInDbVal = actualInPa.getInDecibels(); + + // Then + SoundPressure expectedInPa = SoundPressure.ofPascals(1.0023744672545452); + assertThat(actualInPa.getValue()).isEqualTo(actualInPaVal); + assertThat(actualInDb.getValue()).isEqualTo(actualInDbVal); + assertThat(actualInPa).isEqualTo(expectedInPa); + assertThat(actualInDb).isEqualTo(initialInDb); + } + + @Test + @DisplayName("should return 0 dB for reference pressure 2e-5 Pa") + void shouldReturnZeroDbForReferencePressure() { + // Given + SoundPressure referencePressure = SoundPressure.ofPascals(2e-5); + + // When + double actualInDb = referencePressure.getInDecibels(); + + // Then + assertThat(actualInDb).isEqualTo(0.0, withPrecision(1E-9)); + } + + @Test + @DisplayName("should have Pa as base unit") + void shouldHavePascalAsBaseUnit() { + // Given + SoundPressureUnits expectedBaseUnit = SoundPressureUnits.PASCAL; + + // When + SoundPressure soundPressure = SoundPressure.ofDecibels(60.0); + PressureUnit actualBaseUnit = soundPressure.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("SoundPressure: should be able to resolve unit from string when Pressure in kPa is provided") + void shouldResolveUnitFromStringWhenPressureUnitInkPaIsProvided() { + // Given + SoundPressure soundPressure = SoundPressure.of(0.001, "kPa"); + + // When + double actualInDb = soundPressure.getInDecibels(); + + // Then + assertThat(actualInDb).isEqualTo(93.9794, withPrecision(1E-5)); + } + +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/oscillation/FrequencyTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/oscillation/FrequencyTest.java new file mode 100644 index 0000000..fa2c249 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/oscillation/FrequencyTest.java @@ -0,0 +1,88 @@ +package com.synerset.unitility.unitsystem.oscillation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FrequencyTest { + + @Test + @DisplayName("Frequency: should properly convert between various frequency units and back") + void shouldProperlyConvertBetweenVariousFrequencyUnits() { + // Given + double initialValue = 5.0; + // 5.0 kHz + Frequency initialFrequency = Frequency.ofKiloHertz(initialValue); + + // When + // To Base Unit (Hz) + Frequency actualInHertz = initialFrequency.toHertz(); + double actualHertzVal = initialFrequency.getInHertz(); + + // To Gigahertz + Frequency actualInGigaHertz = initialFrequency.toGigaHertz(); + double actualGigaHertzVal = initialFrequency.getInGigaHertz(); + + // To Cycles Per Minute + Frequency actualInCpm = initialFrequency.toCyclesPerMinute(); + double actualCpmVal = initialFrequency.getInCyclesPerMinute(); + + // Round-trip (GHz -> kHz) + Frequency actualRoundTrip = actualInGigaHertz.toKiloHertz(); + + // Then + // 5.0 kHz = 5000.0 Hz (5.0 * 10^3) + double expectedHertzValue = initialValue * 1000.0; + assertThat(actualInHertz.getValue()).isEqualTo(expectedHertzValue); + assertThat(actualHertzVal).isEqualTo(expectedHertzValue); + + // 5.0 kHz = 0.000005 GHz (5.0 / 10^6) + double expectedGigaHertzValue = initialValue / 1000000.0; + assertThat(actualInGigaHertz.getValue()).isEqualTo(expectedGigaHertzValue); + assertThat(actualGigaHertzVal).isEqualTo(expectedGigaHertzValue); + + // 5.0 kHz = 300000.0 cpm (5000 Hz * 60 s/min) + double expectedCpmValue = expectedHertzValue * 60.0; + assertThat(actualInCpm.getValue()).isEqualTo(expectedCpmValue); + assertThat(actualCpmVal).isEqualTo(expectedCpmValue); + + // Round trip should equal the initial value (based on base value comparison in equals method) + assertThat(actualRoundTrip).isEqualTo(initialFrequency); + } + + @Test + @DisplayName("Frequency: should have HERTZ as base unit") + void shouldHaveHertzAsBaseUnit() { + // Given + FrequencyUnit expectedBaseUnit = FrequencyUnits.HERTZ; + + // When + // Check a non-base unit (e.g., MEGAHERTZ) + Frequency frequencyInMegaHertz = Frequency.ofMegaHertz(10); + FrequencyUnit actualBaseUnit = frequencyInMegaHertz.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("Frequency: should return valid result from to() and getIn() methods and maintain equality after round-trip conversion") + void shouldReturnValidResultFromToAndGetInMethods() { + // Given + double testValue = 10.1; + Frequency expected = Frequency.ofMegaHertz(testValue); + + // When + // Perform a round-trip conversion: MHz -> GHz -> MHz + Frequency actual = expected.toGigaHertz().toMegaHertz(); + double actualValue = expected.getInMegaHertz(); + + // Then + // The object should be equal after the round-trip + assertThat(actual).isEqualTo(expected); + // The getter for the current unit should return the original value + assertThat(actualValue).isEqualTo(expected.getValue()); + } + +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java index 13660ee..e58aa62 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java @@ -37,7 +37,7 @@ class PhysicalQuantityParsingFactoryTest { private static final PhysicalQuantityParsingFactory PARSING_FACTORY = PhysicalQuantityParsingFactory.getDefaultParsingFactory(); @Test - @DisplayName("should create default parsing registry with registered parsers") + @DisplayName("ParsingFactory: should create default parsing registry with registered parsers") void getClassRegistry_shouldCreateRegistry() { // Given // When @@ -52,19 +52,19 @@ void getClassRegistry_shouldCreateRegistry() { assertThat(registeredClasses).isNotNull() .isNotEmpty() .hasSize(registeredDefaultUnitsCount) - .hasSizeGreaterThan(43); + .hasSizeGreaterThan(50); assertThat(status).isTrue(); } @Test - @DisplayName("should be immutable map, clear should not be possible") + @DisplayName("ParsingFactory: should be immutable map, clear should not be possible") void getClassRegistry_shouldBeImmutableMap() { Map, BiFunction>> classRegistry = PARSING_FACTORY.getClassRegistry(); assertThrows(UnsupportedOperationException.class, classRegistry::clear); } @Test - @DisplayName("should fail when attempt to parse for not registered class") + @DisplayName("ParsingFactory: should fail when attempt to parse for not registered class") void createFromSymbol_shouldFailIfQueriedForNonSupportedClass() { // When // Then @@ -73,7 +73,7 @@ void createFromSymbol_shouldFailIfQueriedForNonSupportedClass() { } @Test - @DisplayName("should fail when attempt to parse from invalid string") + @DisplayName("ParsingFactory: should fail when attempt to parse from invalid string") void parse_shouldFailIfQueriedForNonSupportedClass() { // When // Then @@ -82,7 +82,7 @@ void parse_shouldFailIfQueriedForNonSupportedClass() { } @Test - @DisplayName("should parse from DMS format to latitude or longitude") + @DisplayName("ParsingFactory: should parse from DMS format to latitude or longitude") void parse_shouldParseFromDMSFormatToLatitudeOrLongitude() { // Given String lat1 = "52°14'5.123\"N"; @@ -128,7 +128,7 @@ void parse_shouldParseFromDMSFormatToLatitudeOrLongitude() { } @Test - @DisplayName("should fail on invalid or malformed DMS format") + @DisplayName("ParsingFactory: should fail on invalid or malformed DMS format") void parse_shouldFailOnInvalidOrMalformedDMSFormat() { // Given String lat1 = "52°14'5.123\"E"; @@ -141,7 +141,7 @@ void parse_shouldFailOnInvalidOrMalformedDMSFormat() { } @Test - @DisplayName("should parse from single value only and resolve to quantity with default unit") + @DisplayName("ParsingFactory: should parse from single value only and resolve to quantity with default unit") void parse_shouldParseWithoutSymbolAndResolveToDefaultUnit() { // Given String singleValueInput = "-10.5E-5"; diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java index ff8ddd8..b005bf3 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java @@ -26,7 +26,7 @@ void findAllSupportedQuantities_shouldFindAllSupportedQuantitiesAndAssociatedUni Set allSupportedQuantities = QUANTITY_REGISTRY.findAllSupportedQuantities(); // Then - assertThat(allSupportedQuantities).isNotNull().isNotEmpty().hasSize(49); + assertThat(allSupportedQuantities).isNotNull().isNotEmpty().hasSize(52); } @Test diff --git a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java index f03606a..b1136b6 100644 --- a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java +++ b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.synerset.unitility.jackson.module.PhysicalQuantityJacksonModule; import com.synerset.unitility.jackson.module.PhysicalQuantityJacksonModulePlainSIValue; +import com.synerset.unitility.unitsystem.acoustic.SoundPower; +import com.synerset.unitility.unitsystem.acoustic.SoundPressure; import com.synerset.unitility.unitsystem.common.Angle; import com.synerset.unitility.unitsystem.common.AngularVelocity; import com.synerset.unitility.unitsystem.common.Curvature; @@ -25,6 +27,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatStream; class PhysicalQuantityJacksonDeserializerTest { @@ -63,6 +66,8 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing String expectedGenericDimensionless1 = "{\"value\":20,\"unit\":\"\"}"; String expectedGenericDimensionless2 = "{\"value\":20}"; String expectedSDR = "{\"value\":27.6}"; + String expectedSoundPower = "{\"value\": 10,\"unit\":\"db l \"}"; + String expectedSoundPressure = "{\"value\": 10,\"unit\":\"db a \"}"; // When Temperature actualTemp1 = objectMapper.readValue(tempInput1, Temperature.class); @@ -93,6 +98,8 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing GenericDimensionless actualGenericDimensionless1 = objectMapper.readValue(expectedGenericDimensionless1, GenericDimensionless.class); GenericDimensionless actualGenericDimensionless2 = objectMapper.readValue(expectedGenericDimensionless2, GenericDimensionless.class); SDR actualSDR = objectMapper.readValue(expectedSDR, SDR.class); + SoundPower actualSoundPower = objectMapper.readValue(expectedSoundPower, SoundPower.class); + SoundPressure actualSoundPressure = objectMapper.readValue(expectedSoundPressure, SoundPressure.class); // Then Temperature expetedTemperature = Temperature.ofCelsius(20); @@ -136,6 +143,8 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing assertThat(actualGenericDimensionless1).isEqualTo(GenericDimensionless.of(20)); assertThat(actualGenericDimensionless2).isEqualTo(GenericDimensionless.of(20)); assertThat(actualSDR).isEqualTo(expectedSdr); + assertThat(actualSoundPower).isEqualTo(SoundPower.ofDecibels(10)); + assertThat(actualSoundPressure).isEqualTo(SoundPressure.ofDecibels(10)); } @Test diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPowerPlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPowerPlainSIConverter.java new file mode 100644 index 0000000..71ae7b7 --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPowerPlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.acoustic; + +import com.synerset.unitility.unitsystem.acoustic.SoundPower; +import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class SoundPowerPlainSIConverter implements AttributeConverter { + + public static final PowerUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(SoundPower.class); + + @Override + public Double convertToDatabaseColumn(SoundPower attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public SoundPower convertToEntityAttribute(Double dbData) { + return dbData == null ? null : SoundPower.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPressurePlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPressurePlainSIConverter.java new file mode 100644 index 0000000..82b9e2e --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/acoustic/SoundPressurePlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.acoustic; + +import com.synerset.unitility.unitsystem.acoustic.SoundPressure; +import com.synerset.unitility.unitsystem.thermodynamic.PressureUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class SoundPressurePlainSIConverter implements AttributeConverter { + + public static final PressureUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(SoundPressure.class); + + @Override + public Double convertToDatabaseColumn(SoundPressure attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public SoundPressure convertToEntityAttribute(Double dbData) { + return dbData == null ? null : SoundPressure.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/oscillation/FrequencyPlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/oscillation/FrequencyPlainSIConverter.java new file mode 100644 index 0000000..6ab9d51 --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/oscillation/FrequencyPlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.oscillation; + +import com.synerset.unitility.unitsystem.oscillation.Frequency; +import com.synerset.unitility.unitsystem.oscillation.FrequencyUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class FrequencyPlainSIConverter implements AttributeConverter { + + public static final FrequencyUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Frequency.class); + + @Override + public Double convertToDatabaseColumn(Frequency attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Frequency convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Frequency.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/test/java/com/synerset/unitility/persistence/converter/plainsivalue/PlainSiConverterTest.java b/unitility-persistence/src/test/java/com/synerset/unitility/persistence/converter/plainsivalue/PlainSiConverterTest.java index 75d3139..5091c56 100644 --- a/unitility-persistence/src/test/java/com/synerset/unitility/persistence/converter/plainsivalue/PlainSiConverterTest.java +++ b/unitility-persistence/src/test/java/com/synerset/unitility/persistence/converter/plainsivalue/PlainSiConverterTest.java @@ -1,5 +1,7 @@ package com.synerset.unitility.persistence.converter.plainsivalue; +import com.synerset.unitility.persistence.converter.plainsivalue.acoustic.SoundPowerPlainSIConverter; +import com.synerset.unitility.persistence.converter.plainsivalue.acoustic.SoundPressurePlainSIConverter; import com.synerset.unitility.persistence.converter.plainsivalue.common.*; import com.synerset.unitility.persistence.converter.plainsivalue.dimensionless.*; import com.synerset.unitility.persistence.converter.plainsivalue.flow.MassFlowPlainSiConverter; @@ -11,7 +13,10 @@ import com.synerset.unitility.persistence.converter.plainsivalue.mechanical.ForcePlainSiConverter; import com.synerset.unitility.persistence.converter.plainsivalue.mechanical.MomentumPlainSiConverter; import com.synerset.unitility.persistence.converter.plainsivalue.mechanical.TorquePlainSiConverter; +import com.synerset.unitility.persistence.converter.plainsivalue.oscillation.FrequencyPlainSIConverter; import com.synerset.unitility.persistence.converter.plainsivalue.thermodynamic.*; +import com.synerset.unitility.unitsystem.acoustic.SoundPower; +import com.synerset.unitility.unitsystem.acoustic.SoundPressure; import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.*; import com.synerset.unitility.unitsystem.flow.MassFlow; @@ -25,6 +30,8 @@ import com.synerset.unitility.unitsystem.humidity.RelativeHumidityUnit; import com.synerset.unitility.unitsystem.hydraulic.*; import com.synerset.unitility.unitsystem.mechanical.*; +import com.synerset.unitility.unitsystem.oscillation.Frequency; +import com.synerset.unitility.unitsystem.oscillation.FrequencyUnit; import com.synerset.unitility.unitsystem.thermodynamic.*; import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; import org.junit.jupiter.api.Assertions; @@ -60,7 +67,6 @@ void shouldSuccessfullyConvertDensityTest() { assertThat(actualValueToBePersistedInDB).isNotNull(); assertThat(actualValueToBePersistedInDB).isEqualTo(expectedValueFromDB, withPrecision(1E-11)); - assertThat(actualQuantityFromDB.getInPoundsPerCubicFoot()).isEqualTo(density.getInPoundsPerCubicFoot(), withPrecision(1E-11)); } @@ -1072,6 +1078,69 @@ void shouldSuccessfullyConvertGeoCoordinate() { } + @Test + @DisplayName("Force Plain SI Converter: should successfully convert SoundPower") + void shouldSuccessfullyConvertSoundPower() { + // Given + SoundPower quantity = SoundPower.ofDecibels(100.0); + PowerUnit defaultUnit = SoundPowerPlainSIConverter.DEFAULT_SI_UNIT; + double expectedValueFromDB = quantity.getInUnit(defaultUnit); + + SoundPowerPlainSIConverter converter = new SoundPowerPlainSIConverter(); + + // When + Double actualValueToBePersistedInDB = converter.convertToDatabaseColumn(quantity); + SoundPower actualQuantityFromDB = converter.convertToEntityAttribute(expectedValueFromDB); + + // Then + assertThat(actualValueToBePersistedInDB).isNotNull(); + assertThat(actualValueToBePersistedInDB).isEqualTo(expectedValueFromDB, withPrecision(1E-11)); + + assertThat(actualQuantityFromDB.getInDecibels()).isEqualTo(quantity.getInDecibels(), withPrecision(1E-11)); + } + + @Test + @DisplayName("Force Plain SI Converter: should successfully convert SoundPressure") + void shouldSuccessfullyConvertSoundPressure() { + // Given + SoundPressure quantity = SoundPressure.ofDecibels(100.0); + PressureUnit defaultUnit = SoundPressurePlainSIConverter.DEFAULT_SI_UNIT; + double expectedValueFromDB = quantity.getInUnit(defaultUnit); + + SoundPressurePlainSIConverter converter = new SoundPressurePlainSIConverter(); + + // When + Double actualValueToBePersistedInDB = converter.convertToDatabaseColumn(quantity); + SoundPressure actualQuantityFromDB = converter.convertToEntityAttribute(expectedValueFromDB); + + // Then + assertThat(actualValueToBePersistedInDB).isNotNull(); + assertThat(actualValueToBePersistedInDB).isEqualTo(expectedValueFromDB, withPrecision(1E-11)); + + assertThat(actualQuantityFromDB.getInDecibels()).isEqualTo(quantity.getInDecibels(), withPrecision(1E-11)); + } + + @Test + @DisplayName("Force Plain SI Converter: should successfully convert Frequency") + void shouldSuccessfullyConvertFrequency() { + // Given + Frequency quantity = Frequency.ofGigaHertz(1); + FrequencyUnit defaultUnit = FrequencyPlainSIConverter.DEFAULT_SI_UNIT; + double expectedValueFromDB = quantity.getInUnit(defaultUnit); + + FrequencyPlainSIConverter converter = new FrequencyPlainSIConverter(); + + // When + Double actualValueToBePersistedInDB = converter.convertToDatabaseColumn(quantity); + Frequency actualQuantityFromDB = converter.convertToEntityAttribute(expectedValueFromDB); + + // Then + assertThat(actualValueToBePersistedInDB).isNotNull(); + assertThat(actualValueToBePersistedInDB).isEqualTo(expectedValueFromDB, withPrecision(1E-11)); + + assertThat(actualQuantityFromDB.getInKiloHertz()).isEqualTo(quantity.getInKiloHertz(), withPrecision(1E-11)); + } + @Test @DisplayName("GeoDistance Converter: should successfully convert geo distance") void shouldSuccessfullyConvertGeoDistance() { @@ -1099,7 +1168,8 @@ void shouldSuccessfullyConvertGeoDistance() { } @Test - void shouldFindExactly44JavaFilesRecursivelyInTheSpecifiedFolder() { + @DisplayName("Number of converters should match number of registered quantity classes.") + void shouldFindRequiredCountOfJavaFilesRecursivelyInTheSpecifiedFolder() { String userDir = System.getProperty("user.dir"); File folder = new File(userDir, FOLDER_PATH); From 2eb7d229229b6286eb5ac46056d5d4f2404da796 Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 16:28:06 +0200 Subject: [PATCH 2/9] =?UTF-8?q?feat(SNSUNI-148):=20Ensure=20DMS=20format?= =?UTF-8?q?=20compatibility=20with=20ICAO=20Geodetic=20Display=20Format=20?= =?UTF-8?q?(WGS-84):=20Lat:=20DD=C2=B0MM'SS.S"N=20or=20S,=20Longitude:=20D?= =?UTF-8?q?DD=C2=B0MM'SS.S"E=20or=20W.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unitsystem/geographic/DMSValueFormatter.java | 13 +++++++------ .../unitsystem/geographic/GeoCoordinateTest.java | 6 +++--- .../unitsystem/geographic/LatitudeTest.java | 16 ++++++++-------- .../unitsystem/geographic/LongitudeTest.java | 6 +++--- .../PhysicalQuantityJacksonDeserializerTest.java | 4 ++-- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java index a3a76cb..b2f6421 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java @@ -12,16 +12,16 @@ private DMSValueFormatter() { static String latitudeToDmsFormat(Latitude latitude, int relevantDigits) { double latitudeInDegrees = latitude.getInDegrees(); char directionSymbol = (latitudeInDegrees < 0) ? 'S' : 'N'; - return createDMSNotation(latitudeInDegrees, directionSymbol, relevantDigits); + return createDMSNotation(latitudeInDegrees, directionSymbol, relevantDigits, 2); } static String longitudeToDmsFormat(Longitude longitude, int relevantDigits) { double longitudeInDegrees = longitude.getInDegrees(); char directionSymbol = (longitudeInDegrees < 0) ? 'W' : 'E'; - return createDMSNotation(longitudeInDegrees, directionSymbol, relevantDigits); + return createDMSNotation(longitudeInDegrees, directionSymbol, relevantDigits, 3); } - private static String createDMSNotation(double coordinateInDegrees, char directionSymbol, int relevantDigits) { + private static String createDMSNotation(double coordinateInDegrees, char directionSymbol, int relevantDigits, int degreePadding) { coordinateInDegrees = Math.abs(coordinateInDegrees); int degrees = (int) coordinateInDegrees; @@ -31,9 +31,10 @@ private static String createDMSNotation(double coordinateInDegrees, char directi String secondsWithRelDigits = relevantDigits > 0 ? ValueFormatter.toStringWithRelevantDigits(seconds, relevantDigits) - : String.valueOf(seconds); + : String.format("%.2f", seconds); - return String.format("%d°%d'%s\"%c", degrees, minutes, secondsWithRelDigits, directionSymbol); - } + String degreesFormatted = String.format("%0" + degreePadding + "d", degrees); + return String.format("%s°%d'%s\"%c", degreesFormatted, minutes, secondsWithRelDigits, directionSymbol); + } } \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java index 10b73a0..e6441e2 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java @@ -21,9 +21,9 @@ void toDMSFormat_shouldOutputInDegreeMinutesSecondsFormat() { String actualDmsOutputVarTruncated = geoCoordinate.toDMSFormat("sea_quest", 3); // Then - assertThat(actualDmsOutput).isEqualTo("52°14'2.796000000004142\"S, 21°34'1.595999999998412\"W"); - assertThat(actualDmsOutputVar).isEqualTo("sea_quest = 52°14'2.796000000004142\"S, 21°34'1.595999999998412\"W"); - assertThat(actualDmsOutputVarTruncated).isEqualTo("sea_quest = 52°14'2.796\"S, 21°34'1.596\"W"); + assertThat(actualDmsOutput).isEqualTo("52°14'2.80\"S, 021°34'1.60\"W"); + assertThat(actualDmsOutputVar).isEqualTo("sea_quest = 52°14'2.80\"S, 021°34'1.60\"W"); + assertThat(actualDmsOutputVarTruncated).isEqualTo("sea_quest = 52°14'2.796\"S, 021°34'1.596\"W"); assertThat(geoCoordinate.name()).isEqualTo("name"); assertThat(geoCoordinate.latitude()).isEqualTo(latitude); diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java index 9e90ec5..f9c4e06 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java @@ -68,21 +68,21 @@ void toDmsFormat_shouldOutputValidDMSFormat() { // When String latInDms = latitude.toDMSFormat(); String latInDmsVar = latitude.toDMSFormat("lat"); - String latInDmsVarDigits = latitude.toDMSFormat("lat", 1); + String latInDmsVarDigits = latitude.toDMSFormat("lat", 3); // Then - assertThat(latInDms).isEqualTo("52°14'5.123000000003799\"N"); - assertThat(latInDmsVar).isEqualTo("lat = 52°14'5.123000000003799\"N"); - assertThat(latInDmsVarDigits).isEqualTo("lat = 52°14'5.1\"N"); + assertThat(latInDms).isEqualTo("52°14'5.12\"N"); + assertThat(latInDmsVar).isEqualTo("lat = 52°14'5.12\"N"); + assertThat(latInDmsVarDigits).isEqualTo("lat = 52°14'5.123\"N"); } @Test @DisplayName("should create instance from DMS input") void shouldCreateNewInstanceFromDMSFormat(){ // Given - String latitudeAsStringN = "52°14'5.1\"N"; + String latitudeAsStringN = "02°14'5.1\"N"; String latitudeAsStringS = "52°14'5.1\"S"; - String longitudeAsStringE = "52°14'5.1\"E"; + String longitudeAsStringE = "002°14'5.1\"E"; String longitudeAsStringW = "52°14'5.1\"W"; // When @@ -93,9 +93,9 @@ void shouldCreateNewInstanceFromDMSFormat(){ Longitude longitudeW = parsingFactory.parse(Longitude.class, longitudeAsStringW); // Then - assertThat(latitudeN.getInDegrees()).isEqualTo(52.23475); + assertThat(latitudeN.getInDegrees()).isEqualTo(2.23475); assertThat(latitudeS.getInDegrees()).isEqualTo(-52.23475); - assertThat(longitudeE.getInDegrees()).isEqualTo(52.23475); + assertThat(longitudeE.getInDegrees()).isEqualTo(2.23475); assertThat(longitudeW.getInDegrees()).isEqualTo(-52.23475); } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java index 0f56c57..d838772 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java @@ -69,9 +69,9 @@ void toDmsFormat_shouldOutputValidDMSFormat() { String lonInDmsVarDigits = longitude.toDMSFormat("lat", 1); // Then - assertThat(lonInDms).isEqualTo("21°4'3.986000000018066\"W"); - assertThat(lonInDmsVar).isEqualTo("lat = 21°4'3.986000000018066\"W"); - assertThat(lonInDmsVarDigits).isEqualTo("lat = 21°4'4\"W"); + assertThat(lonInDms).isEqualTo("021°4'3.99\"W"); + assertThat(lonInDmsVar).isEqualTo("lat = 021°4'3.99\"W"); + assertThat(lonInDmsVarDigits).isEqualTo("lat = 021°4'4\"W"); } } diff --git a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java index b1136b6..182867d 100644 --- a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java +++ b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java @@ -193,11 +193,11 @@ void deserialize_shouldDeserializeJsonToLatitudeAndLongitude() throws JsonProces objectMapper.registerModule(new PhysicalQuantityJacksonModule(parsingFactory)); String lat1 = "{\"value\":\"52°14'5.123\\\"N\"}"; - String lon1 = "{\"value\":\"21°4'3.986\\\"W\"}"; + String lon1 = "{\"value\":\"021°4'3.986\\\"W\"}"; String lat2 = "{\"value\":\" 52o 14min 5.123sec N\"}"; String lon2 = "{\"value\":\"21deg 4' 3.986\\\" w\"}"; String lat3 = "{\"value\":\"52°14'5.123\\\"N\"}"; - String lon3 = "{\"value\":\"-21°4'3.986\\\"\"}"; + String lon3 = "{\"value\":\"-021°4'3.986\\\"\"}"; String lat4 = "{\"value\":\"52°14'N\"}"; String lon4 = "{\"value\":\"21°4'W\"}"; String lat5 = "{\"value\":\"52°N\"}"; From a6dab22b194a0cbe1863ce2080a930af1c07f49e Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 17:09:50 +0200 Subject: [PATCH 3/9] feat(SNSUNI-149): Add new Distance units: yard, datamile, decameter and hectometer to Distance, Length, Diameter, Perimeter, Thickness, Width, Height. --- .../unitility/unitsystem/common/Diameter.java | 51 +++++++++- .../unitility/unitsystem/common/Distance.java | 51 +++++++++- .../unitsystem/common/DistanceUnits.java | 18 +++- .../unitility/unitsystem/common/Height.java | 53 +++++++++- .../unitility/unitsystem/common/Length.java | 51 +++++++++- .../unitsystem/common/Perimeter.java | 48 +++++++++ .../unitsystem/common/Thickness.java | 48 +++++++++ .../unitility/unitsystem/common/Width.java | 51 +++++++++- .../unitsystem/common/DiameterTest.java | 96 ++++++++++++++++++ .../unitsystem/common/DistanceTest.java | 99 +++++++++++++++++++ .../unitsystem/common/HeightTest.java | 96 ++++++++++++++++++ .../unitsystem/common/LengthTest.java | 98 +++++++++++++++++- .../unitsystem/common/PerimeterTest.java | 96 ++++++++++++++++++ .../unitsystem/common/ThicknessTest.java | 96 ++++++++++++++++++ .../unitsystem/common/WidthTest.java | 96 ++++++++++++++++++ 15 files changed, 1031 insertions(+), 17 deletions(-) diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Diameter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Diameter.java index 3c64110..10432d9 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Diameter.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Diameter.java @@ -30,7 +30,7 @@ public static Diameter of(double value, String unitSymbol) { DistanceUnit resolvedUnit = DistanceUnits.fromSymbol(unitSymbol); return new Diameter(value, resolvedUnit); } - + public static Diameter ofMeters(double value) { return new Diameter(value, DistanceUnits.METER); } @@ -63,6 +63,22 @@ public static Diameter ofInches(double value) { return new Diameter(value, DistanceUnits.INCH); } + public static Diameter ofYards(double value) { + return new Diameter(value, DistanceUnits.YARD); + } + + public static Diameter ofDecameters(double value) { + return new Diameter(value, DistanceUnits.DECAMETER); + } + + public static Diameter ofHectometers(double value) { + return new Diameter(value, DistanceUnits.HECTOMETER); + } + + public static Diameter ofDataMiles(double value) { + return new Diameter(value, DistanceUnits.DATAMILE); + } + public static Diameter of(PhysicalQuantity distanceType){ return Diameter.of(distanceType.getValue(), distanceType.getUnit()); } @@ -139,6 +155,22 @@ public Diameter toInch() { return toUnit(DistanceUnits.INCH); } + public Diameter toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Diameter toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Diameter toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Diameter toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + // Get value in target unit public double getInMeters() { return getInUnit(DistanceUnits.METER); @@ -172,6 +204,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -189,5 +237,4 @@ public int hashCode() { public String toString() { return "Diameter{" + value + " " + unitType.getSymbol() + '}'; } - } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Distance.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Distance.java index b2787a7..55acbbd 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Distance.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Distance.java @@ -29,7 +29,7 @@ public static Distance of(double value, String unitSymbol) { DistanceUnit resolvedUnit = DistanceUnits.fromSymbol(unitSymbol); return new Distance(value, resolvedUnit); } - + public static Distance ofMeters(double value) { return new Distance(value, DistanceUnits.METER); } @@ -62,6 +62,22 @@ public static Distance ofInches(double value) { return new Distance(value, DistanceUnits.INCH); } + public static Distance ofYards(double value) { + return new Distance(value, DistanceUnits.YARD); + } + + public static Distance ofDecameters(double value) { + return new Distance(value, DistanceUnits.DECAMETER); + } + + public static Distance ofHectometers(double value) { + return new Distance(value, DistanceUnits.HECTOMETER); + } + + public static Distance ofDataMiles(double value) { + return new Distance(value, DistanceUnits.DATAMILE); + } + @Override public double getValue() { return value; @@ -134,6 +150,22 @@ public Distance toInch() { return toUnit(DistanceUnits.INCH); } + public Distance toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Distance toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Distance toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Distance toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + // Get value in target unit public double getInMeters() { return getInUnit(DistanceUnits.METER); @@ -167,6 +199,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -184,5 +232,4 @@ public int hashCode() { public String toString() { return "Distance{" + value + " " + unitType.getSymbol() + '}'; } - } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java index c891387..7b42a8f 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java @@ -1,5 +1,6 @@ package com.synerset.unitility.unitsystem.common; +import com.synerset.unitility.unitsystem.Constants; import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; import com.synerset.unitility.unitsystem.util.StringTransformer; @@ -8,13 +9,18 @@ public enum DistanceUnits implements DistanceUnit { METER("m", val -> val, val -> val), - CENTIMETER("cm", val -> val / 100, val -> val * 100), - MILLIMETER("mm", val -> val / 1000, val -> val * 1000), - KILOMETER("km", val -> val * 1000, val -> val / 1000), + CENTIMETER("cm", val -> val * Constants.CENTI, val -> val / Constants.CENTI), + MILLIMETER("mm", val -> val * Constants.MILLI, val -> val / Constants.MILLI), + KILOMETER("km", val -> val * Constants.KILO, val -> val / Constants.KILO), MILE("mi", val -> val * 1609.344, val -> val / 1609.344), NAUTICAL_MILE("nmi", val -> val * 1852, val -> val / 1852), FEET("ft", val -> val * 0.3048, val -> val / 0.3048), - INCH("in", val -> val * 0.0254, val -> val / 0.0254); + INCH("in", val -> val * 0.0254, val -> val / 0.0254), + + YARD("yd", val -> val * 0.9144, val -> val / 0.9144), + DECAMETER("dam", val -> val * Constants.DECA, val -> val / Constants.DECA), + HECTOMETER("hm", val -> val * Constants.HECTO, val -> val / Constants.HECTO), + DATAMILE("datmi", val -> val * 1828.8, val -> val / 1828.8); private final String symbol; private final DoubleUnaryOperator toBaseConverter; @@ -64,6 +70,10 @@ public static DistanceUnit fromSymbol(String rawSymbol) { private static String unifySymbol(String inputString) { return StringTransformer.of(inputString) .trimLowerAndClean() + .replace("da", "dam") + .replace("dm", "datmi") + .replace("datami", "datmi") + .replace("datamile", "datmi") .toString(); } diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Height.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Height.java index 9562b63..4ac5dc8 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Height.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Height.java @@ -30,7 +30,7 @@ public static Height of(double value, String unitSymbol) { DistanceUnit resolvedUnit = DistanceUnits.fromSymbol(unitSymbol); return new Height(value, resolvedUnit); } - + public static Height ofMeters(double value) { return new Height(value, DistanceUnits.METER); } @@ -63,6 +63,22 @@ public static Height ofInches(double value) { return new Height(value, DistanceUnits.INCH); } + public static Height ofYards(double value) { + return new Height(value, DistanceUnits.YARD); + } + + public static Height ofDecameters(double value) { + return new Height(value, DistanceUnits.DECAMETER); + } + + public static Height ofHectometers(double value) { + return new Height(value, DistanceUnits.HECTOMETER); + } + + public static Height ofDataMiles(double value) { + return new Height(value, DistanceUnits.DATAMILE); + } + @Override public double getValue() { return value; @@ -102,7 +118,6 @@ public Height withValue(double value) { return Height.of(value, unitType); } - // Convert to target unit public Height toMeter() { return toUnit(DistanceUnits.METER); } @@ -135,7 +150,22 @@ public Height toInch() { return toUnit(DistanceUnits.INCH); } - // Get value in target unit + public Height toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Height toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Height toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Height toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + public double getInMeters() { return getInUnit(DistanceUnits.METER); } @@ -168,6 +198,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + public static Height of(PhysicalQuantity distanceType){ return Height.of(distanceType.getValue(), distanceType.getUnit()); } @@ -189,5 +235,4 @@ public int hashCode() { public String toString() { return "Height{" + value + " " + unitType.getSymbol() + '}'; } - } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Length.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Length.java index 6ed78bf..f281cc4 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Length.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Length.java @@ -30,7 +30,7 @@ public static Length of(double value, String unitSymbol) { DistanceUnit resolvedUnit = DistanceUnits.fromSymbol(unitSymbol); return new Length(value, resolvedUnit); } - + public static Length ofMeters(double value) { return new Length(value, DistanceUnits.METER); } @@ -63,6 +63,22 @@ public static Length ofInches(double value) { return new Length(value, DistanceUnits.INCH); } + public static Length ofYards(double value) { + return new Length(value, DistanceUnits.YARD); + } + + public static Length ofDecameters(double value) { + return new Length(value, DistanceUnits.DECAMETER); + } + + public static Length ofHectometers(double value) { + return new Length(value, DistanceUnits.HECTOMETER); + } + + public static Length ofDataMiles(double value) { + return new Length(value, DistanceUnits.DATAMILE); + } + public static Length of(PhysicalQuantity distanceType){ return Length.of(distanceType.getValue(), distanceType.getUnit()); } @@ -139,6 +155,22 @@ public Length toInch() { return toUnit(DistanceUnits.INCH); } + public Length toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Length toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Length toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Length toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + // Get value in target unit public double getInMeters() { return getInUnit(DistanceUnits.METER); @@ -172,6 +204,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -189,5 +237,4 @@ public int hashCode() { public String toString() { return "Length{" + value + " " + unitType.getSymbol() + '}'; } - } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Perimeter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Perimeter.java index 1743a0d..213cf24 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Perimeter.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Perimeter.java @@ -63,6 +63,22 @@ public static Perimeter ofInches(double value) { return new Perimeter(value, DistanceUnits.INCH); } + public static Perimeter ofYards(double value) { + return new Perimeter(value, DistanceUnits.YARD); + } + + public static Perimeter ofDecameters(double value) { + return new Perimeter(value, DistanceUnits.DECAMETER); + } + + public static Perimeter ofHectometers(double value) { + return new Perimeter(value, DistanceUnits.HECTOMETER); + } + + public static Perimeter ofDataMiles(double value) { + return new Perimeter(value, DistanceUnits.DATAMILE); + } + @Override public double getValue() { return value; @@ -135,6 +151,22 @@ public Perimeter toInch() { return toUnit(DistanceUnits.INCH); } + public Perimeter toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Perimeter toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Perimeter toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Perimeter toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + // Get value in target unit public double getInMeters() { return getInUnit(DistanceUnits.METER); @@ -168,6 +200,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + public static Perimeter of(PhysicalQuantity distanceType) { return Perimeter.of(distanceType.getValue(), distanceType.getUnit()); } diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Thickness.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Thickness.java index 57c7ebe..d40191b 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Thickness.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Thickness.java @@ -63,6 +63,22 @@ public static Thickness ofInches(double value) { return new Thickness(value, DistanceUnits.INCH); } + public static Thickness ofYards(double value) { + return new Thickness(value, DistanceUnits.YARD); + } + + public static Thickness ofDecameters(double value) { + return new Thickness(value, DistanceUnits.DECAMETER); + } + + public static Thickness ofHectometers(double value) { + return new Thickness(value, DistanceUnits.HECTOMETER); + } + + public static Thickness ofDataMiles(double value) { + return new Thickness(value, DistanceUnits.DATAMILE); + } + @Override public double getValue() { return value; @@ -135,6 +151,22 @@ public Thickness toInch() { return toUnit(DistanceUnits.INCH); } + public Thickness toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Thickness toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Thickness toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Thickness toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + // Get value in target unit public double getInMeters() { return getInUnit(DistanceUnits.METER); @@ -168,6 +200,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + public static Thickness of(PhysicalQuantity distanceType) { return Thickness.of(distanceType.getValue(), distanceType.getUnit()); } diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Width.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Width.java index e598d30..00da0aa 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Width.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/Width.java @@ -30,7 +30,7 @@ public static Width of(double value, String unitSymbol) { DistanceUnit resolvedUnit = DistanceUnits.fromSymbol(unitSymbol); return new Width(value, resolvedUnit); } - + public static Width ofMeters(double value) { return new Width(value, DistanceUnits.METER); } @@ -63,6 +63,22 @@ public static Width ofInches(double value) { return new Width(value, DistanceUnits.INCH); } + public static Width ofYards(double value) { + return new Width(value, DistanceUnits.YARD); + } + + public static Width ofDecameters(double value) { + return new Width(value, DistanceUnits.DECAMETER); + } + + public static Width ofHectometers(double value) { + return new Width(value, DistanceUnits.HECTOMETER); + } + + public static Width ofDataMiles(double value) { + return new Width(value, DistanceUnits.DATAMILE); + } + public static Width of(PhysicalQuantity distanceType){ return Width.of(distanceType.getValue(), distanceType.getUnit()); } @@ -139,6 +155,22 @@ public Width toInch() { return toUnit(DistanceUnits.INCH); } + public Width toYard() { + return toUnit(DistanceUnits.YARD); + } + + public Width toDecameter() { + return toUnit(DistanceUnits.DECAMETER); + } + + public Width toHectometer() { + return toUnit(DistanceUnits.HECTOMETER); + } + + public Width toDataMile() { + return toUnit(DistanceUnits.DATAMILE); + } + // Get value in target unit public double getInMeters() { return getInUnit(DistanceUnits.METER); @@ -172,6 +204,22 @@ public double getInInches() { return getInUnit(DistanceUnits.INCH); } + public double getInYards() { + return getInUnit(DistanceUnits.YARD); + } + + public double getInDecameters() { + return getInUnit(DistanceUnits.DECAMETER); + } + + public double getInHectometers() { + return getInUnit(DistanceUnits.HECTOMETER); + } + + public double getInDataMiles() { + return getInUnit(DistanceUnits.DATAMILE); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -189,5 +237,4 @@ public int hashCode() { public String toString() { return "Width{" + value + " " + unitType.getSymbol() + '}'; } - } \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DiameterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DiameterTest.java index 0d00bf8..5d0493a 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DiameterTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DiameterTest.java @@ -172,4 +172,100 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialLengthInMiles); } + @Test + @DisplayName("Diameter: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Diameter initialLengthInYards = Diameter.ofYards(10.0); + + // When + Diameter actualInMeters = initialLengthInYards.toBaseUnit(); + Diameter actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Diameter expectedInMeters = Diameter.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialLengthInYards); + } + + @Test + @DisplayName("Diameter: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Diameter initialLengthInDecameters = Diameter.ofDecameters(5.0); + + // When + Diameter actualInMeters = initialLengthInDecameters.toBaseUnit(); + Diameter actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Diameter expectedInMeters = Diameter.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialLengthInDecameters); + } + + @Test + @DisplayName("Diameter: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Diameter initialLengthInHectometers = Diameter.ofHectometers(2.5); + + // When + Diameter actualInMeters = initialLengthInHectometers.toBaseUnit(); + Diameter actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Diameter expectedInMeters = Diameter.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialLengthInHectometers); + } + + @Test + @DisplayName("Diameter: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Diameter initialLengthInDataMiles = Diameter.ofDataMiles(1.0); + + // When + Diameter actualInMeters = initialLengthInDataMiles.toBaseUnit(); + Diameter actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Diameter expectedInMeters = Diameter.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialLengthInDataMiles); + } + + @Test + @DisplayName("Diameter: should return valid result from to() and getIn() methods including new units") + void shouldReturnValidResultFromToAndGetInMethodsIncludingNewUnits() { + // Given + Diameter expected = Diameter.ofMeters(1828.8); + + // When + Diameter actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + } + } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DistanceTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DistanceTest.java index fac3e22..27f9531 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DistanceTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DistanceTest.java @@ -172,4 +172,103 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialDistanceInMiles); } + @Test + @DisplayName("Distance: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Distance initialDistanceInYards = Distance.ofYards(10.0); + + // When + Distance actualInMeters = initialDistanceInYards.toBaseUnit(); + Distance actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Distance expectedInMeters = Distance.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialDistanceInYards); + } + + @Test + @DisplayName("Distance: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Distance initialDistanceInDecameters = Distance.ofDecameters(5.0); + + // When + Distance actualInMeters = initialDistanceInDecameters.toBaseUnit(); + Distance actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Distance expectedInMeters = Distance.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialDistanceInDecameters); + } + + @Test + @DisplayName("Distance: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Distance initialDistanceInHectometers = Distance.ofHectometers(2.5); + + // When + Distance actualInMeters = initialDistanceInHectometers.toBaseUnit(); + Distance actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Distance expectedInMeters = Distance.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialDistanceInHectometers); + } + + @Test + @DisplayName("Distance: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Distance initialDistanceInDataMiles = Distance.ofDataMiles(1.0); + + // When + Distance actualInMeters = initialDistanceInDataMiles.toBaseUnit(); + Distance actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Distance expectedInMeters = Distance.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialDistanceInDataMiles); + } + + @Test + @DisplayName("Distance: should return valid result from to() and getIn() methods for new units") + void shouldReturnValidResultFromToAndGetInMethodsForNewUnits() { + // Given + Distance expected = Distance.ofMeters(1828.8); + + // When + Distance actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInYards = expected.getInYards(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + // 1828.8 m / 0.9144 m/yd = 2000.0 yd + assertThat(actualValueInYards).isEqualTo(2000.0); + } + } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/HeightTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/HeightTest.java index 0795a53..8b88d74 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/HeightTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/HeightTest.java @@ -172,4 +172,100 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialLengthInMiles); } + @Test + @DisplayName("Height: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Height initialLengthInYards = Height.ofYards(10.0); + + // When + Height actualInMeters = initialLengthInYards.toBaseUnit(); + Height actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Height expectedInMeters = Height.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialLengthInYards); + } + + @Test + @DisplayName("Height: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Height initialLengthInDecameters = Height.ofDecameters(5.0); + + // When + Height actualInMeters = initialLengthInDecameters.toBaseUnit(); + Height actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Height expectedInMeters = Height.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialLengthInDecameters); + } + + @Test + @DisplayName("Height: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Height initialLengthInHectometers = Height.ofHectometers(2.5); + + // When + Height actualInMeters = initialLengthInHectometers.toBaseUnit(); + Height actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Height expectedInMeters = Height.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialLengthInHectometers); + } + + @Test + @DisplayName("Height: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Height initialLengthInDataMiles = Height.ofDataMiles(1.0); + + // When + Height actualInMeters = initialLengthInDataMiles.toBaseUnit(); + Height actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Height expectedInMeters = Height.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialLengthInDataMiles); + } + + @Test + @DisplayName("Height: should return valid result from to() and getIn() methods including new units") + void shouldReturnValidResultFromToAndGetInMethodsIncludingNewUnits() { + // Given + Height expected = Height.ofMeters(1828.8); + + // When + Height actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + } + } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/LengthTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/LengthTest.java index f44c0f1..d7f6131 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/LengthTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/LengthTest.java @@ -8,7 +8,7 @@ class LengthTest { @Test - @DisplayName("should convert to m from mm and vice versa") + @DisplayName("Length: should convert to m from mm and vice versa") void shouldProperlyConvertToMetersFromMillimeters() { // Given Length initialLengthInMillimeters = Length.ofMillimeters(1000.0); @@ -172,4 +172,100 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialLengthInMiles); } + @Test + @DisplayName("Length: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Length initialLengthInYards = Length.ofYards(10.0); + + // When + Length actualInMeters = initialLengthInYards.toBaseUnit(); + Length actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Length expectedInMeters = Length.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialLengthInYards); + } + + @Test + @DisplayName("Length: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Length initialLengthInDecameters = Length.ofDecameters(5.0); + + // When + Length actualInMeters = initialLengthInDecameters.toBaseUnit(); + Length actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Length expectedInMeters = Length.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialLengthInDecameters); + } + + @Test + @DisplayName("Length: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Length initialLengthInHectometers = Length.ofHectometers(2.5); + + // When + Length actualInMeters = initialLengthInHectometers.toBaseUnit(); + Length actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Length expectedInMeters = Length.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialLengthInHectometers); + } + + @Test + @DisplayName("Length: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Length initialLengthInDataMiles = Length.ofDataMiles(1.0); + + // When + Length actualInMeters = initialLengthInDataMiles.toBaseUnit(); + Length actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Length expectedInMeters = Length.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialLengthInDataMiles); + } + + @Test + @DisplayName("Length: should return valid result from to() and getIn() methods including new units") + void shouldReturnValidResultFromToAndGetInMethodsIncludingNewUnits() { + // Given + Length expected = Length.ofMeters(1828.8); + + // When + Length actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + } + } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/PerimeterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/PerimeterTest.java index e4d9bd8..30dfec8 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/PerimeterTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/PerimeterTest.java @@ -172,4 +172,100 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialPerimeterInMiles); } + @Test + @DisplayName("Perimeter: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Perimeter initialPerimeterInYards = Perimeter.ofYards(10.0); + + // When + Perimeter actualInMeters = initialPerimeterInYards.toBaseUnit(); + Perimeter actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Perimeter expectedInMeters = Perimeter.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialPerimeterInYards); + } + + @Test + @DisplayName("Perimeter: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Perimeter initialPerimeterInDecameters = Perimeter.ofDecameters(5.0); + + // When + Perimeter actualInMeters = initialPerimeterInDecameters.toBaseUnit(); + Perimeter actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Perimeter expectedInMeters = Perimeter.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialPerimeterInDecameters); + } + + @Test + @DisplayName("Perimeter: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Perimeter initialPerimeterInHectometers = Perimeter.ofHectometers(2.5); + + // When + Perimeter actualInMeters = initialPerimeterInHectometers.toBaseUnit(); + Perimeter actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Perimeter expectedInMeters = Perimeter.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialPerimeterInHectometers); + } + + @Test + @DisplayName("Perimeter: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Perimeter initialPerimeterInDataMiles = Perimeter.ofDataMiles(1.0); + + // When + Perimeter actualInMeters = initialPerimeterInDataMiles.toBaseUnit(); + Perimeter actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Perimeter expectedInMeters = Perimeter.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialPerimeterInDataMiles); + } + + @Test + @DisplayName("Perimeter: should return valid result from to() and getIn() methods including new units") + void shouldReturnValidResultFromToAndGetInMethodsIncludingNewUnits() { + // Given + Perimeter expected = Perimeter.ofMeters(1828.8); + + // When + Perimeter actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + } + } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/ThicknessTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/ThicknessTest.java index f2f06ef..2ef9b15 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/ThicknessTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/ThicknessTest.java @@ -172,4 +172,100 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialThicknessInMiles); } + @Test + @DisplayName("Thickness: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Thickness initialThicknessInYards = Thickness.ofYards(10.0); + + // When + Thickness actualInMeters = initialThicknessInYards.toBaseUnit(); + Thickness actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Thickness expectedInMeters = Thickness.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialThicknessInYards); + } + + @Test + @DisplayName("Thickness: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Thickness initialThicknessInDecameters = Thickness.ofDecameters(5.0); + + // When + Thickness actualInMeters = initialThicknessInDecameters.toBaseUnit(); + Thickness actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Thickness expectedInMeters = Thickness.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialThicknessInDecameters); + } + + @Test + @DisplayName("Thickness: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Thickness initialThicknessInHectometers = Thickness.ofHectometers(2.5); + + // When + Thickness actualInMeters = initialThicknessInHectometers.toBaseUnit(); + Thickness actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Thickness expectedInMeters = Thickness.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialThicknessInHectometers); + } + + @Test + @DisplayName("Thickness: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Thickness initialThicknessInDataMiles = Thickness.ofDataMiles(1.0); + + // When + Thickness actualInMeters = initialThicknessInDataMiles.toBaseUnit(); + Thickness actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Thickness expectedInMeters = Thickness.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialThicknessInDataMiles); + } + + @Test + @DisplayName("Thickness: should return valid result from to() and getIn() methods including new units") + void shouldReturnValidResultFromToAndGetInMethodsIncludingNewUnits() { + // Given + Thickness expected = Thickness.ofMeters(1828.8); + + // When + Thickness actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + } + } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/WidthTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/WidthTest.java index 781f8dd..f469612 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/WidthTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/WidthTest.java @@ -172,4 +172,100 @@ void shouldProperlyConvertToMetersFromNauticalMile() { assertThat(actualInMiles).isEqualTo(initialLengthInMiles); } + @Test + @DisplayName("Width: should convert to m from yd and vice versa") + void shouldProperlyConvertToMetersFromYard() { + // Given + Width initialLengthInYards = Width.ofYards(10.0); + + // When + Width actualInMeters = initialLengthInYards.toBaseUnit(); + Width actualInYards = actualInMeters.toUnit(DistanceUnits.YARD); + double actualInYardsVal = actualInMeters.getInYards(); + + // Then + Width expectedInMeters = Width.ofMeters(9.144); // 10 * 0.9144 + assertThat(actualInYards.getValue()).isEqualTo(actualInYardsVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInYards).isEqualTo(initialLengthInYards); + } + + @Test + @DisplayName("Width: should convert to m from dam and vice versa") + void shouldProperlyConvertToMetersFromDecameter() { + // Given + Width initialLengthInDecameters = Width.ofDecameters(5.0); + + // When + Width actualInMeters = initialLengthInDecameters.toBaseUnit(); + Width actualInDecameters = actualInMeters.toUnit(DistanceUnits.DECAMETER); + double actualInDecametersVal = actualInMeters.getInDecameters(); + + // Then + Width expectedInMeters = Width.ofMeters(50.0); // 5 * 10.0 + assertThat(actualInDecameters.getValue()).isEqualTo(actualInDecametersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDecameters).isEqualTo(initialLengthInDecameters); + } + + @Test + @DisplayName("Width: should convert to m from hm and vice versa") + void shouldProperlyConvertToMetersFromHectometer() { + // Given + Width initialLengthInHectometers = Width.ofHectometers(2.5); + + // When + Width actualInMeters = initialLengthInHectometers.toBaseUnit(); + Width actualInHectometers = actualInMeters.toUnit(DistanceUnits.HECTOMETER); + double actualInHectometersVal = actualInMeters.getInHectometers(); + + // Then + Width expectedInMeters = Width.ofMeters(250.0); // 2.5 * 100.0 + assertThat(actualInHectometers.getValue()).isEqualTo(actualInHectometersVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInHectometers).isEqualTo(initialLengthInHectometers); + } + + @Test + @DisplayName("Width: should convert to m from datmi and vice versa") + void shouldProperlyConvertToMetersFromDataMile() { + // Given + Width initialLengthInDataMiles = Width.ofDataMiles(1.0); + + // When + Width actualInMeters = initialLengthInDataMiles.toBaseUnit(); + Width actualInDataMiles = actualInMeters.toUnit(DistanceUnits.DATAMILE); + double actualInDataMilesVal = actualInMeters.getInDataMiles(); + + // Then + Width expectedInMeters = Width.ofMeters(1828.8); // 1 * 1828.8 + assertThat(actualInDataMiles.getValue()).isEqualTo(actualInDataMilesVal); + assertThat(actualInMeters).isEqualTo(expectedInMeters); + assertThat(actualInDataMiles).isEqualTo(initialLengthInDataMiles); + } + + @Test + @DisplayName("Width: should return valid result from to() and getIn() methods including new units") + void shouldReturnValidResultFromToAndGetInMethodsIncludingNewUnits() { + // Given + Width expected = Width.ofMeters(1828.8); + + // When + Width actual = expected.toMeter() + .toYard() + .toDecameter() + .toHectometer() + .toDataMile() + .toMeter(); + + double actualValue = expected.getInMeters(); + double actualValueInDataMiles = expected.getInDataMiles(); + + // Then + assertThat(actual).isEqualTo(expected); + assertThat(actualValue).isEqualTo(expected.getValue()); + + assertThat(actualValueInDataMiles).isEqualTo(1.0); + } + } From 893e457dc45492871bb53d3d51644875c5adf1b8 Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 17:24:54 +0200 Subject: [PATCH 4/9] feat(SNSUNI-150): Removed deprecated CUBIC_FEET --- .../com/synerset/unitility/unitsystem/common/VolumeUnits.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/VolumeUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/VolumeUnits.java index 1e955d6..b84c179 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/VolumeUnits.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/VolumeUnits.java @@ -10,8 +10,6 @@ public enum VolumeUnits implements VolumeUnit { CUBIC_METER("m³", val -> val, val -> val), CUBIC_CENTIMETER("cm³", val -> val * 0.000001, val -> val * 1000000.0), CUBIC_DECIMETER("dm³", val -> val * 0.001, val -> val * 1000.0), - @Deprecated(forRemoval = true) - CUBIC_FEET("ft³", val -> val * 0.0283168466, val -> val / 0.0283168466), CUBIC_FOOT("ft³", val -> val * 0.0283168466, val -> val / 0.0283168466), LITRE("l", val -> val * 0.001, val -> val * 1000.0), HECTOLITRE("hl", val -> val * 0.1, val -> val * 10.0), From e5ebfa27343604601383d64bcc1e48c67b54fbbb Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 17:28:38 +0200 Subject: [PATCH 5/9] feat(SNSUNI-151): Update dependencies and project version to major 3.0.0 --- README.md | 20 +++++++++---------- pom.xml | 18 ++++++++--------- .../unitsystem/common/DistanceUnits.java | 1 - 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a3bea5f..d7ee145 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,13 @@ features, such as overloaded operators. Copy the Maven dependency provided below to your pom.xml file, and you are ready to go. For other package managers, check maven central repository: -[UNITILITY](https://search.maven.org/artifact/com.synerset/unitility/2.11.2/jar?eh=). +[UNITILITY](https://search.maven.org/artifact/com.synerset/unitility/3.0.0/jar?eh=). ```xml com.synerset unitility-core - 2.11.2 + 3.0.0 ``` If you use frameworks to develop web applications, it is recommended to use Unitility extension modules, @@ -89,7 +89,7 @@ Extension for the Spring Boot framework: com.synerset unitility-spring - 2.11.2 + 3.0.0 ``` Extension for the Quarkus framework: @@ -97,7 +97,7 @@ Extension for the Quarkus framework: com.synerset unitility-quarkus - 2.11.2 + 3.0.0 ``` Extensions include CORE module, so you don't have to put it separate in your pom. @@ -132,7 +132,7 @@ units and at least one Imperial unit. #### COMMON: -* Distance, Length, Width, Height, Diameter: meter [m], centimetre [cm], millimetre [mm], kilometre [km], mile [mi], nautical mile [nmi], feet [ft], inch [in] +* Distance, Length, Width, Height, Diameter, Thickness, Perimeter: meter [m], centimetre [cm], millimetre [mm], kilometre [km], mile [mi], nautical mile [nmi], feet [ft], inch [in], yard [yd], decameter [dam], hectometer[hm], data mile [datmi], * Area: square meter [m²], square kilometre [km²], square centimetre [cm²], square millimetre [mm²], are [a], hectare [ha], square inch [in²], square foot [ft²], square yard [yd²], acre [ac], square mile [mi²] * Volume: cubic meter [m³], cubic centimetre [L], liter [L], hectolitre [hL], millilitre [mL], ounce [fl.oz], pint [pt], @@ -540,7 +540,7 @@ deserialization back to Java objects. To include this module in your project, us com.synerset unitility-jackson - 2.11.2 + 3.0.0 ``` PhysicalQuantity JSON structure for valid serialization / deserialization has been defined as in the following example: @@ -619,7 +619,7 @@ add the following dependency: com.synerset unitility-spring - 2.11.2 + 3.0.0 ``` Adding Spring module to the project will automatically: @@ -658,7 +658,7 @@ add following dependency: com.synerset unitility-quarkus - 2.11.2 + 3.0.0 ``` Adding Quarkus module to the project will automatically: @@ -1260,10 +1260,10 @@ Small shield with referenced most recent version tag:
``` Tech shield with version tag for manual adjustment (you can indicate which version you actually use):
-[![Unitility](https://img.shields.io/badge/UNITILITY-v2.11.2-13ADF3?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMi41bW0iIGhlaWdodD0iMTQuNW1tIiB2aWV3Qm94PSIwIDAgMjI1MCAxNDUwIj4NCiAgPHBvbHlnb24gZmlsbD0iIzUwN0QxNCIgcG9pbnRzPSIyMjQxLjAzLDE1Ljg4IDExMzYuMzgsMTUuODQgOTA1Ljg4LDQxNS4xIDIwMTAuNTMsNDE1LjA5IiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNzFBQjIzIiBwb2ludHM9IjExMTYuMzgsMTUuODQgNjU1Ljk5LDE1Ljg0IDQ5NC4xNSwyOTYuMTcgNzI4LjM1LDY5NC44OCIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzhBQzkzNCIgcG9pbnRzPSI0ODQuMTUsMzA2LjE3IDI1NS4wNiw3MDIuOTYgMzg3LjY2LDkzMi42NCA4NDUuODMsOTMyLjYzIiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNThEMEZGIiBwb2ludHM9Ii03LjE3LDE0NDAuMDkgMTA5Ny45NywxNDQwLjA4IDEzMjguNDcsMTA0MC44MyAyMjMuMzIsMTA0MC44NSIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzEzQURGMyIgcG9pbnRzPSIxNzM5LjA0LDExNjAuOTEgMTUwOS4wOSw3NjIuNjQgMTExNy45NywxNDQwLjA4IDExODYuOTMsMTQ0MC4wOCAxNTc3Ljg3LDE0NDAuMDgiIC8+DQogIDxwb2x5Z29uIGZpbGw9IiMwMzkzRDAiIHBvaW50cz0iMTk3OC44LDc1Mi45NiAxODQ2LjIsNTIzLjMgMTM4Ni42OCw1MjMuMyAxNzQ5LjA0LDExNTAuOTEiIC8+DQo8L3N2Zz4=)](https://github.com/pjazdzyk/Unitility) +[![Unitility](https://img.shields.io/badge/UNITILITY-v3.0.0-13ADF3?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMi41bW0iIGhlaWdodD0iMTQuNW1tIiB2aWV3Qm94PSIwIDAgMjI1MCAxNDUwIj4NCiAgPHBvbHlnb24gZmlsbD0iIzUwN0QxNCIgcG9pbnRzPSIyMjQxLjAzLDE1Ljg4IDExMzYuMzgsMTUuODQgOTA1Ljg4LDQxNS4xIDIwMTAuNTMsNDE1LjA5IiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNzFBQjIzIiBwb2ludHM9IjExMTYuMzgsMTUuODQgNjU1Ljk5LDE1Ljg0IDQ5NC4xNSwyOTYuMTcgNzI4LjM1LDY5NC44OCIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzhBQzkzNCIgcG9pbnRzPSI0ODQuMTUsMzA2LjE3IDI1NS4wNiw3MDIuOTYgMzg3LjY2LDkzMi42NCA4NDUuODMsOTMyLjYzIiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNThEMEZGIiBwb2ludHM9Ii03LjE3LDE0NDAuMDkgMTA5Ny45NywxNDQwLjA4IDEzMjguNDcsMTA0MC44MyAyMjMuMzIsMTA0MC44NSIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzEzQURGMyIgcG9pbnRzPSIxNzM5LjA0LDExNjAuOTEgMTUwOS4wOSw3NjIuNjQgMTExNy45NywxNDQwLjA4IDExODYuOTMsMTQ0MC4wOCAxNTc3Ljg3LDE0NDAuMDgiIC8+DQogIDxwb2x5Z29uIGZpbGw9IiMwMzkzRDAiIHBvaW50cz0iMTk3OC44LDc1Mi45NiAxODQ2LjIsNTIzLjMgMTM4Ni42OCw1MjMuMyAxNzQ5LjA0LDExNTAuOTEiIC8+DQo8L3N2Zz4=)](https://github.com/pjazdzyk/Unitility) ```markdown -[![Unitility](https://img.shields.io/badge/UNITILITY-v2.11.2-13ADF3?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMi41bW0iIGhlaWdodD0iMTQuNW1tIiB2aWV3Qm94PSIwIDAgMjI1MCAxNDUwIj4NCiAgPHBvbHlnb24gZmlsbD0iIzUwN0QxNCIgcG9pbnRzPSIyMjQxLjAzLDE1Ljg4IDExMzYuMzgsMTUuODQgOTA1Ljg4LDQxNS4xIDIwMTAuNTMsNDE1LjA5IiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNzFBQjIzIiBwb2ludHM9IjExMTYuMzgsMTUuODQgNjU1Ljk5LDE1Ljg0IDQ5NC4xNSwyOTYuMTcgNzI4LjM1LDY5NC44OCIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzhBQzkzNCIgcG9pbnRzPSI0ODQuMTUsMzA2LjE3IDI1NS4wNiw3MDIuOTYgMzg3LjY2LDkzMi42NCA4NDUuODMsOTMyLjYzIiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNThEMEZGIiBwb2ludHM9Ii03LjE3LDE0NDAuMDkgMTA5Ny45NywxNDQwLjA4IDEzMjguNDcsMTA0MC44MyAyMjMuMzIsMTA0MC44NSIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzEzQURGMyIgcG9pbnRzPSIxNzM5LjA0LDExNjAuOTEgMTUwOS4wOSw3NjIuNjQgMTExNy45NywxNDQwLjA4IDExODYuOTMsMTQ0MC4wOCAxNTc3Ljg3LDE0NDAuMDgiIC8+DQogIDxwb2x5Z29uIGZpbGw9IiMwMzkzRDAiIHBvaW50cz0iMTk3OC44LDc1Mi45NiAxODQ2LjIsNTIzLjMgMTM4Ni42OCw1MjMuMyAxNzQ5LjA0LDExNTAuOTEiIC8+DQo8L3N2Zz4=)](https://github.com/pjazdzyk/Unitility) +[![Unitility](https://img.shields.io/badge/UNITILITY-v3.0.0-13ADF3?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMi41bW0iIGhlaWdodD0iMTQuNW1tIiB2aWV3Qm94PSIwIDAgMjI1MCAxNDUwIj4NCiAgPHBvbHlnb24gZmlsbD0iIzUwN0QxNCIgcG9pbnRzPSIyMjQxLjAzLDE1Ljg4IDExMzYuMzgsMTUuODQgOTA1Ljg4LDQxNS4xIDIwMTAuNTMsNDE1LjA5IiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNzFBQjIzIiBwb2ludHM9IjExMTYuMzgsMTUuODQgNjU1Ljk5LDE1Ljg0IDQ5NC4xNSwyOTYuMTcgNzI4LjM1LDY5NC44OCIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzhBQzkzNCIgcG9pbnRzPSI0ODQuMTUsMzA2LjE3IDI1NS4wNiw3MDIuOTYgMzg3LjY2LDkzMi42NCA4NDUuODMsOTMyLjYzIiAvPg0KICA8cG9seWdvbiBmaWxsPSIjNThEMEZGIiBwb2ludHM9Ii03LjE3LDE0NDAuMDkgMTA5Ny45NywxNDQwLjA4IDEzMjguNDcsMTA0MC44MyAyMjMuMzIsMTA0MC44NSIgLz4NCiAgPHBvbHlnb24gZmlsbD0iIzEzQURGMyIgcG9pbnRzPSIxNzM5LjA0LDExNjAuOTEgMTUwOS4wOSw3NjIuNjQgMTExNy45NywxNDQwLjA4IDExODYuOTMsMTQ0MC4wOCAxNTc3Ljg3LDE0NDAuMDgiIC8+DQogIDxwb2x5Z29uIGZpbGw9IiMwMzkzRDAiIHBvaW50cz0iMTk3OC44LDc1Mi45NiAxODQ2LjIsNTIzLjMgMTM4Ni42OCw1MjMuMyAxNzQ5LjA0LDExNTAuOTEiIC8+DQo8L3N2Zz4=)](https://github.com/pjazdzyk/Unitility) ``` ## 11. ACKNOWLEDGMENTS diff --git a/pom.xml b/pom.xml index 645304c..56f836b 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ - 2.11.2 + 3.0.0 17 @@ -56,9 +56,9 @@ 2.19.2 - 3.5.4 + 3.5.6 - 3.24.5 + 3.27.0 3.1.1 6.0.1 @@ -66,16 +66,16 @@ 0.8.13 - 5.13.4 - 3.27.3 + 6.0.0 + 3.27.6 - 1.7.2 - 3.11.2 + 1.7.3 + 3.12.0 3.3.1 1.7.0 - 3.5.3 - 3.4.0 + 3.5.4 + 3.5.0 synerset diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java index 7b42a8f..69c4f44 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java @@ -16,7 +16,6 @@ public enum DistanceUnits implements DistanceUnit { NAUTICAL_MILE("nmi", val -> val * 1852, val -> val / 1852), FEET("ft", val -> val * 0.3048, val -> val / 0.3048), INCH("in", val -> val * 0.0254, val -> val / 0.0254), - YARD("yd", val -> val * 0.9144, val -> val / 0.9144), DECAMETER("dam", val -> val * Constants.DECA, val -> val / Constants.DECA), HECTOMETER("hm", val -> val * Constants.HECTO, val -> val / Constants.HECTO), From aff2d338eeb96bbffe4787ea23ae80dd63f130b2 Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 18:18:52 +0200 Subject: [PATCH 6/9] feat(SNSUNI-152): Add DataSize physical quantity --- README.md | 9 +- .../unitility/unitsystem/common/DataSize.java | 224 ++++++++++++++++++ .../unitsystem/common/DataSizeUnit.java | 8 + .../unitsystem/common/DataSizeUnits.java | 68 ++++++ .../unitsystem/common/DistanceUnits.java | 1 - ...PhysicalQuantityDefaultParsingFactory.java | 2 + .../util/SupportedQuantitiesRegistry.java | 1 + .../unitsystem/common/DataSizeTest.java | 154 ++++++++++++ .../util/SupportedQuantitiesRegistryTest.java | 2 +- ...ysicalQuantityJacksonDeserializerTest.java | 8 +- .../common/DataSizeSiConverter.java | 25 ++ 11 files changed, 492 insertions(+), 10 deletions(-) create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnits.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DataSizeTest.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/common/DataSizeSiConverter.java diff --git a/README.md b/README.md index d7ee145..3352a48 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,12 @@ units and at least one Imperial unit. gallon (US) [gal_us], gallon (UK) [gal_uk] * Mass: kilogram [kg], gram [g], milligram [mg], tonne [t], ounce [oz], pound [lb] * Angle: degrees [°], radians [rad] -* Ratio: Percent [%], Decimal [-] +* Ratio: percent [%], Decimal [-] * LinearMassDensity: Kilogram per metre [kg/m], Tonne per metre [t/m], Ounce per foot [oz/ft], Pound per foot [lb/ft] -* Velocity: Meter per second [m/s], centimeter per second [cm/s], kilometer per hour [km/h], inch per second, [in/s], feet per second [ft/s], mile per hour [mph], knot [kn], Mach [Mach] -* AngularVelocity: Radians per second [rad/s], revolutions per minute, [rpm], revolutions per second [rps], degrees per second [°/s] -* Curvature: Radians per meter [rad/m], radians per foot [rad/ft], degrees per meter [°/m], degrees per foot [°/ft], degrees per hundred feet [°/100ft] +* Velocity: meter per second [m/s], centimeter per second [cm/s], kilometer per hour [km/h], inch per second, [in/s], feet per second [ft/s], mile per hour [mph], knot [kn], Mach [Mach] +* AngularVelocity: radians per second [rad/s], revolutions per minute, [rpm], revolutions per second [rps], degrees per second [°/s] +* Curvature: radians per meter [rad/m], radians per foot [rad/ft], degrees per meter [°/m], degrees per foot [°/ft], degrees per hundred feet [°/100ft] +* Data size: bits [bit], bytes [b], kilobytes[kb], megabytes [mb], gigabyte [gb], terabyte [tb], petabyte [pb] #### MECHANICAL: diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java new file mode 100644 index 0000000..271c203 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java @@ -0,0 +1,224 @@ +package com.synerset.unitility.unitsystem.common; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class DataSize implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final DataSizeUnit unitType; + + public DataSize(double value, DataSizeUnit unitType) { + if (unitType == null) { + unitType = DataSizeUnits.BYTE; + } + + validateValue(value, unitType); + + this.value = value; + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + /** + * Enforces data size constraints: + * 1. BIT unit: value must be a non-fractional number and a multiple of 8 (a whole number of Bytes). + * 2. All other units (BYTE and larger): value must result in a non-fractional number of Bytes. + * 3. Value in Bytes must not exceed the double's safe integer limit (2^53 Bytes or 1 PB) to guarantee precision. + * + * @param value The value provided by the user. + * @param unit The unit provided by the user. + * @throws IllegalArgumentException if the validation fails. + */ + private void validateValue(double value, DataSizeUnit unit) { + + if (unit == DataSizeUnits.BIT) { + if (Double.compare(value, Math.rint(value)) != 0) { + throw new IllegalArgumentException(String.format( + "Data size constraint violation: Value %.4f [%s] must be an exact number of bits.", + value, unit.getSymbol())); + } + + long bitValue = (long) value; + if (bitValue % 8 != 0) { + throw new IllegalArgumentException(String.format( + "Data size constraint violation: Value %d [%s] is not a whole number of Bytes (multiple of 8 bits).", + bitValue, unit.getSymbol())); + } + return; + } + + double valueInBaseUnit = unit.toValueInBaseUnit(value); + + if (Double.compare(valueInBaseUnit, Math.rint(valueInBaseUnit)) != 0) { + throw new IllegalArgumentException(String.format( + "Data size constraint violation: Value %.4f [%s] results in a fractional number of Bytes (%.4f B). Data size must be an exact number of Bytes.", + value, unit.getSymbol(), valueInBaseUnit)); + } + + if (valueInBaseUnit > Math.pow(2, 53)) { + throw new IllegalArgumentException(String.format( + "Data size constraint violation: The value (%.0f B) exceeds the safe integer precision limit of 2^53 Bytes (1 PB). Consider BigInteger for larger sizes.", + valueInBaseUnit)); + } + } + + // Static factory methods + public static DataSize of(double value, DataSizeUnit unit) { + return new DataSize(value, unit); + } + + public static DataSize of(double value, String unitSymbol) { + DataSizeUnit resolvedUnit = DataSizeUnits.fromSymbol(unitSymbol); + return new DataSize(value, resolvedUnit); + } + + public static DataSize ofBits(double value) { + return new DataSize(value, DataSizeUnits.BIT); + } + + public static DataSize ofBytes(double value) { + return new DataSize(value, DataSizeUnits.BYTE); + } + + public static DataSize ofKilobytes(double value) { + return new DataSize(value, DataSizeUnits.KILOBYTE); + } + + public static DataSize ofMegabytes(double value) { + return new DataSize(value, DataSizeUnits.MEGABYTE); + } + + public static DataSize ofGigabytes(double value) { + return new DataSize(value, DataSizeUnits.GIGABYTE); + } + + public static DataSize ofTerabytes(double value) { + return new DataSize(value, DataSizeUnits.TERABYTE); + } + + public static DataSize ofPetabytes(double value) { + return new DataSize(value, DataSizeUnits.PETABYTE); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; // + } + + @Override + public DataSizeUnit getUnit() { + return unitType; + } + + @Override + public DataSize toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public DataSize toUnit(DataSizeUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return DataSize.of(valueInTargetUnit, targetUnit); + } + + @Override + public DataSize toUnit(String targetUnit) { + DataSizeUnit resolvedUnit = DataSizeUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public DataSize withValue(double value) { + return DataSize.of(value, unitType); + } + + // Convert to target unit + public DataSize toBits() { + return toUnit(DataSizeUnits.BIT); + } + + public DataSize toBytes() { + return toUnit(DataSizeUnits.BYTE); + } + + public DataSize toKilobytes() { + return toUnit(DataSizeUnits.KILOBYTE); + } + + public DataSize toMegabytes() { + return toUnit(DataSizeUnits.MEGABYTE); + } + + public DataSize toGigabytes() { + return toUnit(DataSizeUnits.GIGABYTE); + } + + public DataSize toTerabytes() { + return toUnit(DataSizeUnits.TERABYTE); + } + + public DataSize toPetabytes() { + return toUnit(DataSizeUnits.PETABYTE); + } + + + // Get value in target unit + public double getInBits() { + return toBits().getValue(); + } + + public double getInBytes() { + return getInUnit(DataSizeUnits.BYTE); + } + + public double getInKilobytes() { + return getInUnit(DataSizeUnits.KILOBYTE); + } + + public double getInMegabytes() { + return getInUnit(DataSizeUnits.MEGABYTE); + } + + public double getInGigabytes() { + return getInUnit(DataSizeUnits.GIGABYTE); + } + + public double getInTerabytes() { + return getInUnit(DataSizeUnits.TERABYTE); + } + + public double getInPetabytes() { + return getInUnit(DataSizeUnits.PETABYTE); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DataSize inputQuantity = (DataSize) o; + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + String separator = getUnit().getSymbol().equals("b") ? "" : " "; + return "DataSize{" + value + separator + unitType.getSymbol() + '}'; + } + +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnit.java new file mode 100644 index 0000000..47de3f8 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.common; + +import com.synerset.unitility.unitsystem.Unit; + +public interface DataSizeUnit extends Unit { + @Override + DataSizeUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnits.java new file mode 100644 index 0000000..4063ea2 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSizeUnits.java @@ -0,0 +1,68 @@ +package com.synerset.unitility.unitsystem.common; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum DataSizeUnits implements DataSizeUnit { + + BYTE("B", val -> val, val -> val), + BIT("bit", val -> val / 8.0, val -> val * 8.0), + KILOBYTE("KB", val -> val * 1024.0, val -> val / 1024.0), + MEGABYTE("MB", val -> val * Math.pow(1024.0, 2), val -> val / Math.pow(1024.0, 2)), + GIGABYTE("GB", val -> val * Math.pow(1024.0, 3), val -> val / Math.pow(1024.0, 3)), + TERABYTE("TB", val -> val * Math.pow(1024.0, 4), val -> val / Math.pow(1024.0, 4)), + PETABYTE("PB", val -> val * Math.pow(1024.0, 5), val -> val / Math.pow(1024.0, 5)); // Max safe unit to fit in double (1 PB = 2^50 Bytes) + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + DataSizeUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public DataSizeUnits getBaseUnit() { + return BYTE; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static DataSizeUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return BYTE; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (DataSizeUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equalsIgnoreCase(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + DataSizeUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimLowerAndClean() + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java index 69c4f44..d060fee 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DistanceUnits.java @@ -72,7 +72,6 @@ private static String unifySymbol(String inputString) { .replace("da", "dam") .replace("dm", "datmi") .replace("datami", "datmi") - .replace("datamile", "datmi") .toString(); } diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java index 6dfb3b7..7bd4ab5 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java @@ -56,6 +56,7 @@ private PhysicalQuantityDefaultParsingFactory() { Map.entry(Volume.class, Volume::of), Map.entry(Ratio.class, Ratio::of), Map.entry(Curvature.class, Curvature::of), + Map.entry(DataSize.class, DataSize::of), // Dimensionless Map.entry(GenericDimensionless.class, (value, symbol) -> GenericDimensionless.of(value)), Map.entry(BypassFactor.class, (value, symbol) -> BypassFactor.of(value)), @@ -121,6 +122,7 @@ private PhysicalQuantityDefaultParsingFactory() { Map.entry(Thickness.class, DistanceUnits.METER), Map.entry(Velocity.class, VelocityUnits.METER_PER_SECOND), Map.entry(Volume.class, VolumeUnits.CUBIC_METER), + Map.entry(DataSize.class, DataSizeUnits.BYTE), // Dimensionless (5) Map.entry(GenericDimensionless.class, GenericDimensionlessUnits.DIMENSIONLESS), Map.entry(BypassFactor.class, BypassFactorUnits.DIMENSIONLESS), diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java index 7735631..400ad30 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java @@ -60,6 +60,7 @@ private SupportedQuantitiesRegistry() { Map.entry(Volume.class, () -> Arrays.asList(VolumeUnits.values())), Map.entry(Ratio.class, () -> Arrays.asList(RatioUnits.values())), Map.entry(Curvature.class, () -> Arrays.asList(CurvatureUnits.values())), + Map.entry(DataSize.class, () -> Arrays.asList(DataSizeUnits.values())), // Dimensionless Map.entry(GenericDimensionless.class, Collections::emptyList), Map.entry(BypassFactor.class, Collections::emptyList), diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DataSizeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DataSizeTest.java new file mode 100644 index 0000000..68ef9ca --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/common/DataSizeTest.java @@ -0,0 +1,154 @@ +package com.synerset.unitility.unitsystem.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class DataSizeTest { + + private static final double DELTA = 1E-12; + + @Test + @DisplayName("DataSize: should have BYTE as base unit") + void shouldHaveByteAsBaseUnit() { + // Given + DataSizeUnit expectedBaseUnit = DataSizeUnits.BYTE; + + // When + DataSize dataSize = DataSize.ofGigabytes(10); + DataSizeUnit actualBaseUnit = dataSize.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("DataSize: should properly convert between all defined units, including base unit (Byte)") + void shouldProperlyConvertBetweenAllUnits() { + // Given + // Using 1 Petabyte, which is the largest safe unit (2^53 bits) + DataSize initialSize = DataSize.ofPetabytes(1.0); + + // When + double bytes = initialSize.getInBytes(); + double bits = initialSize.getInBits(); + double kilobytes = initialSize.toKilobytes().getValue(); + double megabytes = initialSize.toMegabytes().getValue(); + double gigabytes = initialSize.toGigabytes().getValue(); + double terabytes = initialSize.toTerabytes().getValue(); + double petabytes = initialSize.toPetabytes().getValue(); + + // Convert back to PB to test conversion stability + DataSize actualFromBytes = DataSize.ofBytes(bytes).toPetabytes(); + + // Then + // 1 PB = 1024^5 B + double factor5 = Math.pow(1024.0, 5); + double factor4 = Math.pow(1024.0, 4); + double factor3 = Math.pow(1024.0, 3); + double factor2 = Math.pow(1024.0, 2); + double factor1 = 1024.0; + + assertThat(bytes).isEqualTo(factor5, withPrecision(DELTA)); + // 1 PB in Bits = (1024^5) * 8 + assertThat(bits).isEqualTo(factor5 * 8.0, withPrecision(DELTA)); + + assertThat(kilobytes).isEqualTo(factor4, withPrecision(DELTA)); + assertThat(megabytes).isEqualTo(factor3, withPrecision(DELTA)); + assertThat(gigabytes).isEqualTo(factor2, withPrecision(DELTA)); + assertThat(terabytes).isEqualTo(factor1, withPrecision(DELTA)); + assertThat(petabytes).isEqualTo(1.0, withPrecision(DELTA)); + + // Test conversion stability + assertThat(actualFromBytes.getValue()).isEqualTo(1.0, withPrecision(DELTA)); + } + + @Test + @DisplayName("DataSize: should properly convert from Bit to Byte and vice versa") + void shouldProperlyConvertBitToByte() { + // Given + // 48 bits, which is exactly 6 Bytes and a valid value + DataSize initialSizeInBits = DataSize.ofBits(48); + + // When + DataSize actualInBytes = initialSizeInBits.toBytes(); + double actualBytesValue = initialSizeInBits.getInBytes(); + + // Convert back to Bits + DataSize actualInBits = actualInBytes.toBits(); + double actualBitsValue = actualInBytes.getInBits(); + + // Then + DataSize expectedByte = DataSize.of(6, DataSizeUnits.BYTE); + + assertThat(actualInBytes).isEqualTo(expectedByte); + assertThat(actualBytesValue).isEqualTo(6.0, withPrecision(DELTA)); + assertThat(actualInBits.getValue()).isEqualTo(48.0, withPrecision(DELTA)); + assertThat(actualBitsValue).isEqualTo(48.0, withPrecision(DELTA)); + } + + @ParameterizedTest(name = "Value {0} {1} should be valid") + @MethodSource("validDataSizeValues") + @DisplayName("DataSize: should PASS validation for valid data sizes") + void shouldPassValidationForValidDataSizes(double value, DataSizeUnit unit) { + // When & Then + // Should pass without throwing an exception + DataSize dataSize = DataSize.of(value, unit); + assertThat(dataSize.getValue()).isEqualTo(value); + } + + private static Stream validDataSizeValues() { + return Stream.of( + // Valid bit count (multiple of 8 and non-fractional) + arguments(8.0, DataSizeUnits.BIT), + arguments(48.0, DataSizeUnits.BIT), + + // Valid Byte-based units (must be a whole number of Bytes) + arguments(1.0, DataSizeUnits.BYTE), + arguments(10.0, DataSizeUnits.BYTE), + arguments(1.0, DataSizeUnits.KILOBYTE), + arguments(1.0, DataSizeUnits.PETABYTE), + arguments(0.0, DataSizeUnits.BIT), + arguments(0.0, DataSizeUnits.BYTE) + ); + } + + @ParameterizedTest(name = "Validation: Value {0} {1} should throw IllegalArgumentException") + @MethodSource("invalidDataSizeValues") + @DisplayName("DataSize: should FAIL validation for invalid data sizes") + void shouldFailValidationForInvalidDataSizes(double value, DataSizeUnit unit, String expectedError) { + // When & Then + assertThatExceptionOfType(IllegalArgumentException.class) + .as("Expected error: " + expectedError) + .isThrownBy(() -> DataSize.of(value, unit)); + } + + private static Stream invalidDataSizeValues() { + return Stream.of( + // BIT: Fractional bit count (Rule 1a) + arguments(7.5, DataSizeUnits.BIT, "Fractional number of bits"), + + // BIT: Not a multiple of 8 (Rule 1b) + arguments(7.0, DataSizeUnits.BIT, "Not a multiple of 8 bits"), + arguments(9.0, DataSizeUnits.BIT, "Not a multiple of 8 bits"), + + // BYTE-BASED: Fractional number of Bytes (Rule 2) + arguments(1.5, DataSizeUnits.BYTE, "Fractional number of Bytes"), + // 1.001 KB = 1025.024 Bytes (Fractional) + arguments(1.001, DataSizeUnits.KILOBYTE, "Fractional number of Bytes"), + + // Exceeds safe integer limit (Rule 3) + // 2.1 PB > 2^53 Bytes + arguments(2.1, DataSizeUnits.PETABYTE, "Exceeds safe integer limit"), + // 2^53 + 1 Byte + arguments(Math.pow(2, 53) + 8, DataSizeUnits.BYTE, "Exceeds safe integer limit") + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java index b005bf3..4bbf603 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java @@ -26,7 +26,7 @@ void findAllSupportedQuantities_shouldFindAllSupportedQuantitiesAndAssociatedUni Set allSupportedQuantities = QUANTITY_REGISTRY.findAllSupportedQuantities(); // Then - assertThat(allSupportedQuantities).isNotNull().isNotEmpty().hasSize(52); + assertThat(allSupportedQuantities).isNotNull().isNotEmpty().hasSize(53); } @Test diff --git a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java index 182867d..79c7853 100644 --- a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java +++ b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java @@ -6,10 +6,7 @@ import com.synerset.unitility.jackson.module.PhysicalQuantityJacksonModulePlainSIValue; import com.synerset.unitility.unitsystem.acoustic.SoundPower; import com.synerset.unitility.unitsystem.acoustic.SoundPressure; -import com.synerset.unitility.unitsystem.common.Angle; -import com.synerset.unitility.unitsystem.common.AngularVelocity; -import com.synerset.unitility.unitsystem.common.Curvature; -import com.synerset.unitility.unitsystem.common.Distance; +import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.BypassFactor; import com.synerset.unitility.unitsystem.dimensionless.GenericDimensionless; import com.synerset.unitility.unitsystem.flow.VolumetricFlow; @@ -68,6 +65,7 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing String expectedSDR = "{\"value\":27.6}"; String expectedSoundPower = "{\"value\": 10,\"unit\":\"db l \"}"; String expectedSoundPressure = "{\"value\": 10,\"unit\":\"db a \"}"; + String expectedDataSize = "{\"value\": 10,\"unit\":\" mb \"}"; // When Temperature actualTemp1 = objectMapper.readValue(tempInput1, Temperature.class); @@ -100,6 +98,7 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing SDR actualSDR = objectMapper.readValue(expectedSDR, SDR.class); SoundPower actualSoundPower = objectMapper.readValue(expectedSoundPower, SoundPower.class); SoundPressure actualSoundPressure = objectMapper.readValue(expectedSoundPressure, SoundPressure.class); + DataSize actualDataSize = objectMapper.readValue(expectedDataSize, DataSize.class); // Then Temperature expetedTemperature = Temperature.ofCelsius(20); @@ -145,6 +144,7 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing assertThat(actualSDR).isEqualTo(expectedSdr); assertThat(actualSoundPower).isEqualTo(SoundPower.ofDecibels(10)); assertThat(actualSoundPressure).isEqualTo(SoundPressure.ofDecibels(10)); + assertThat(actualDataSize).isEqualTo(DataSize.ofMegabytes(10)); } @Test diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/common/DataSizeSiConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/common/DataSizeSiConverter.java new file mode 100644 index 0000000..4510b8c --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/common/DataSizeSiConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.common; + +import com.synerset.unitility.unitsystem.common.DataSize; +import com.synerset.unitility.unitsystem.common.DataSizeUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class DataSizeSiConverter implements AttributeConverter { + + public static final DataSizeUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(DataSize.class); + + @Override + public Double convertToDatabaseColumn(DataSize attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public DataSize convertToEntityAttribute(Double dbData) { + return dbData == null ? null : DataSize.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file From 57f99fbe52dfd1cad1aaf657defba06bc62191ae Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 21:21:40 +0200 Subject: [PATCH 7/9] feat(SNSUNI-153): Add electrical quantities: Capacitance, Charge, Conductance, Current, Voltage, Resistance. --- README.md | 12 ++ .../unitility/unitsystem/common/DataSize.java | 3 +- .../unitsystem/electric/Capacitance.java | 175 ++++++++++++++++++ .../unitsystem/electric/CapacitanceUnit.java | 8 + .../unitsystem/electric/CapacitanceUnits.java | 70 +++++++ .../unitility/unitsystem/electric/Charge.java | 175 ++++++++++++++++++ .../unitsystem/electric/ChargeUnit.java | 8 + .../unitsystem/electric/ChargeUnits.java | 71 +++++++ .../unitsystem/electric/Conductance.java | 162 ++++++++++++++++ .../unitsystem/electric/ConductanceUnit.java | 8 + .../unitsystem/electric/ConductanceUnits.java | 68 +++++++ .../unitsystem/electric/Current.java | 139 ++++++++++++++ .../unitsystem/electric/CurrentUnit.java | 8 + .../unitsystem/electric/CurrentUnits.java | 66 +++++++ .../unitsystem/electric/Resistance.java | 138 ++++++++++++++ .../unitsystem/electric/ResistanceUnit.java | 8 + .../unitsystem/electric/ResistanceUnits.java | 66 +++++++ .../unitsystem/electric/Voltage.java | 163 ++++++++++++++++ .../unitsystem/electric/VoltageUnit.java | 8 + .../unitsystem/electric/VoltageUnits.java | 69 +++++++ ...PhysicalQuantityDefaultParsingFactory.java | 23 ++- .../unitsystem/util/StringTransformer.java | 13 ++ .../util/SupportedQuantitiesRegistry.java | 12 +- .../unitsystem/electric/CapacitanceTest.java | 117 ++++++++++++ .../unitsystem/electric/ChargeTest.java | 118 ++++++++++++ .../unitsystem/electric/ConductanceTest.java | 125 +++++++++++++ .../unitsystem/electric/CurrentTest.java | 111 +++++++++++ .../unitsystem/electric/ResistanceTest.java | 111 +++++++++++ .../unitsystem/electric/VoltageTest.java | 115 ++++++++++++ .../util/SupportedQuantitiesRegistryTest.java | 2 +- ...ysicalQuantityJacksonDeserializerTest.java | 24 ++- .../electric/CapacitancePlainSIConverter.java | 25 +++ .../electric/ChargePlainSIConverter.java | 25 +++ .../electric/ConductancePlainSIConverter.java | 25 +++ .../electric/CurrentPlainSIConverter.java | 25 +++ .../electric/ResistancePlainSIConverter.java | 25 +++ .../electric/VoltagePlainSIConverter.java | 25 +++ 37 files changed, 2335 insertions(+), 11 deletions(-) create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Capacitance.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Charge.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Conductance.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Current.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Resistance.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnits.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Voltage.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnit.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnits.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CapacitanceTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ChargeTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ConductanceTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CurrentTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ResistanceTest.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/VoltageTest.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CapacitancePlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ChargePlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ConductancePlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CurrentPlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ResistancePlainSIConverter.java create mode 100644 unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/VoltagePlainSIConverter.java diff --git a/README.md b/README.md index 3352a48..dfb8259 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,15 @@ units and at least one Imperial unit. * Momentum: Kilogram meter per second [kg·m/s], Gram centimeter per second [g·cm/s], Pound feet per second [lb·ft/s] * Torque: Newton meter [N·m], Millinewton meter [mN·m], Kilopond meter [kp·m], Foot pound [ft·lb], Inch pound [in·lb] +#### ELECTRIC: + +* Capacitance: farad [F], microfarad [µF], nanofarad [nF], picofarad [pF] +* Charge: coulomb [C], millicoulomb [mC], microcoulomb [µC], nanocoulomb [nC], picocoulomb [pC], kilocoulomb [kC], megacoulomb [MC] +* Conductance: siemens [S], millisiemens [mS], microsiemens [µS], nanosiemens [nS], picosiemens [pS], kilosiemens [kS] +* Current: ampere [A], milliampere [mA], microampere [µA], nanoampere [nA], kiloampere [kA], megaampere [MA] +* Resistance: ohm [Ω], milliohm [mΩ]], kiloohm [kΩ]], megaohm [MΩ]] +* Voltage: volt [V], millivolt [mV], microvolt [µV], nanovolt [nV], kilovolt [kV], megavolt [MV] + #### THERMODYNAMIC: * Temperature: Kelvin [K], Celsius [°C], Fahrenheit [°F] @@ -301,6 +310,9 @@ alternative ways of expressing units in an input string: | cubic | ³ | 3 | m³, m3 | | negative exponents | ⁻¹ | -1 | m⁻¹, m-1 | +IMPORTANT! In case quantity includes both milli and Mega, parser works in CASE SENSITIVE mode. And in such case +for mega you have to provide "MV" for mega volts, and "mV" for milli volts. + Please note that this method of creating quantities is designed to be used for deserializers.
**In your code, you should create units in a programmatic way, not parsing from strings.** diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java index 271c203..cb6583a 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/common/DataSize.java @@ -217,8 +217,7 @@ public int hashCode() { @Override public String toString() { - String separator = getUnit().getSymbol().equals("b") ? "" : " "; - return "DataSize{" + value + separator + unitType.getSymbol() + '}'; + return "DataSize{" + value + unitType.getSymbol() + '}'; } } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Capacitance.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Capacitance.java new file mode 100644 index 0000000..46ba71f --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Capacitance.java @@ -0,0 +1,175 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class Capacitance implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final CapacitanceUnit unitType; + + public Capacitance(double value, CapacitanceUnit unitType) { + this.value = value; + if (unitType == null) { + unitType = CapacitanceUnits.FARAD; + } + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Capacitance of(double value, CapacitanceUnit unit) { + return new Capacitance(value, unit); + } + + public static Capacitance of(double value, String unitSymbol) { + CapacitanceUnit resolvedUnit = CapacitanceUnits.fromSymbol(unitSymbol); + return new Capacitance(value, resolvedUnit); + } + + public static Capacitance ofPicofarads(double value) { + return new Capacitance(value, CapacitanceUnits.PICOFARAD); + } + + public static Capacitance ofNanofarads(double value) { + return new Capacitance(value, CapacitanceUnits.NANOFARAD); + } + + public static Capacitance ofMicrofarads(double value) { + return new Capacitance(value, CapacitanceUnits.MICROFARAD); + } + + public static Capacitance ofMillifarads(double value) { + return new Capacitance(value, CapacitanceUnits.MILLIFARAD); + } + + public static Capacitance ofFarads(double value) { + return new Capacitance(value, CapacitanceUnits.FARAD); + } + + public static Capacitance ofKilofarads(double value) { + return new Capacitance(value, CapacitanceUnits.KILOFARAD); + } + + public static Capacitance ofMegafarads(double value) { + return new Capacitance(value, CapacitanceUnits.MEGAFARAD); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public CapacitanceUnit getUnit() { + return unitType; + } + + @Override + public Capacitance toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Capacitance toUnit(CapacitanceUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Capacitance.of(valueInTargetUnit, targetUnit); + } + + @Override + public Capacitance toUnit(String targetUnit) { + CapacitanceUnit resolvedUnit = CapacitanceUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Capacitance withValue(double value) { + return Capacitance.of(value, unitType); + } + + // Convert to target unit + public Capacitance toPicofarads() { + return toUnit(CapacitanceUnits.PICOFARAD); + } + + public Capacitance toNanofarads() { + return toUnit(CapacitanceUnits.NANOFARAD); + } + + public Capacitance toMicrofarads() { + return toUnit(CapacitanceUnits.MICROFARAD); + } + + public Capacitance toMillifarads() { + return toUnit(CapacitanceUnits.MILLIFARAD); + } + + public Capacitance toFarads() { + return toUnit(CapacitanceUnits.FARAD); + } + + public Capacitance toKilofarads() { + return toUnit(CapacitanceUnits.KILOFARAD); + } + + public Capacitance toMegafarads() { + return toUnit(CapacitanceUnits.MEGAFARAD); + } + + + // Get value in target unit + public double getInPicofarads() { + return getInUnit(CapacitanceUnits.PICOFARAD); + } + + public double getInNanofarads() { + return getInUnit(CapacitanceUnits.NANOFARAD); + } + + public double getInMicrofarads() { + return getInUnit(CapacitanceUnits.MICROFARAD); + } + + public double getInMillifarads() { + return getInUnit(CapacitanceUnits.MILLIFARAD); + } + + public double getInFarads() { + return getInUnit(CapacitanceUnits.FARAD); + } + + public double getInKilofarads() { + return getInUnit(CapacitanceUnits.KILOFARAD); + } + + public double getInMegafarads() { + return getInUnit(CapacitanceUnits.MEGAFARAD); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Capacitance inputQuantity = (Capacitance) o; + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return "Capacitance{" + value + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnit.java new file mode 100644 index 0000000..34a122b --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Unit; + +public interface CapacitanceUnit extends Unit { + @Override + CapacitanceUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnits.java new file mode 100644 index 0000000..b738eff --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CapacitanceUnits.java @@ -0,0 +1,70 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum CapacitanceUnits implements CapacitanceUnit { + + FARAD("F", val -> val, val -> val), + PICOFARAD("pF", val -> val * 1E-12, val -> val / 1E-12), + NANOFARAD("nF", val -> val * 1E-9, val -> val / 1E-9), + MICROFARAD("µF", val -> val * 1E-6, val -> val / 1E-6), + MILLIFARAD("mF", val -> val * 1E-3, val -> val / 1E-3), + KILOFARAD("kF", val -> val * 1E3, val -> val / 1E3), + MEGAFARAD("MF", val -> val * 1E6, val -> val / 1E6); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + CapacitanceUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public CapacitanceUnits getBaseUnit() { + return FARAD; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static CapacitanceUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return FARAD; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (CapacitanceUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equals(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + CapacitanceUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimAndClean() + .replace("f", "F") + .replace("u", "µ") + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Charge.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Charge.java new file mode 100644 index 0000000..f83d360 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Charge.java @@ -0,0 +1,175 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class Charge implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final ChargeUnit unitType; + + public Charge(double value, ChargeUnit unitType) { + this.value = value; + if (unitType == null) { + unitType = ChargeUnits.COULOMB; + } + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Charge of(double value, ChargeUnit unit) { + return new Charge(value, unit); + } + + public static Charge of(double value, String unitSymbol) { + ChargeUnit resolvedUnit = ChargeUnits.fromSymbol(unitSymbol); + return new Charge(value, resolvedUnit); + } + + public static Charge ofPicocoulombs(double value) { + return new Charge(value, ChargeUnits.PICOCOULOMB); + } + + public static Charge ofNanocoulombs(double value) { + return new Charge(value, ChargeUnits.NANOCOULOMB); + } + + public static Charge ofMicrocoulombs(double value) { + return new Charge(value, ChargeUnits.MICROCOULOMB); + } + + public static Charge ofMillicoulombs(double value) { + return new Charge(value, ChargeUnits.MILLICOULOMB); + } + + public static Charge ofCoulombs(double value) { + return new Charge(value, ChargeUnits.COULOMB); + } + + public static Charge ofKilocoulombs(double value) { + return new Charge(value, ChargeUnits.KILOCOULOMB); + } + + public static Charge ofMegacoulombs(double value) { + return new Charge(value, ChargeUnits.MEGACOULOMB); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public ChargeUnit getUnit() { + return unitType; + } + + @Override + public Charge toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Charge toUnit(ChargeUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Charge.of(valueInTargetUnit, targetUnit); + } + + @Override + public Charge toUnit(String targetUnit) { + ChargeUnit resolvedUnit = ChargeUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Charge withValue(double value) { + return Charge.of(value, unitType); + } + + // Convert to target unit + public Charge toPicocoulombs() { + return toUnit(ChargeUnits.PICOCOULOMB); + } + + public Charge toNanocoulombs() { + return toUnit(ChargeUnits.NANOCOULOMB); + } + + public Charge toMicrocoulombs() { + return toUnit(ChargeUnits.MICROCOULOMB); + } + + public Charge toMillicoulombs() { + return toUnit(ChargeUnits.MILLICOULOMB); + } + + public Charge toCoulombs() { + return toUnit(ChargeUnits.COULOMB); + } + + public Charge toKilocoulombs() { + return toUnit(ChargeUnits.KILOCOULOMB); + } + + public Charge toMegacoulombs() { + return toUnit(ChargeUnits.MEGACOULOMB); + } + + // Get value in target unit + public double getInPicocoulombs() { + return getInUnit(ChargeUnits.PICOCOULOMB); + } + + public double getInNanocoulombs() { + return getInUnit(ChargeUnits.NANOCOULOMB); + } + + public double getInMicrocoulombs() { + return getInUnit(ChargeUnits.MICROCOULOMB); + } + + public double getInMillicoulombs() { + return getInUnit(ChargeUnits.MILLICOULOMB); + } + + public double getInCoulombs() { + return getInUnit(ChargeUnits.COULOMB); + } + + public double getInKilocoulombs() { + return getInUnit(ChargeUnits.KILOCOULOMB); + } + + public double getInMegacoulombs() { + return getInUnit(ChargeUnits.MEGACOULOMB); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Charge inputQuantity = (Charge) o; + // Strict double comparison as requested + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return "Charge{" + value + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnit.java new file mode 100644 index 0000000..3b27ca3 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Unit; + +public interface ChargeUnit extends Unit { + @Override + ChargeUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnits.java new file mode 100644 index 0000000..4d03d37 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ChargeUnits.java @@ -0,0 +1,71 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Constants; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum ChargeUnits implements ChargeUnit { + + COULOMB("C", val -> val, val -> val), + PICOCOULOMB("pC", val -> val * Constants.PICO, val -> val / Constants.PICO), + NANOCOULOMB("nC", val -> val * Constants.NANO, val -> val / Constants.NANO), + MICROCOULOMB("µC", val -> val * Constants.MICRO, val -> val / Constants.MICRO), + MILLICOULOMB("mC", val -> val * Constants.MILLI, val -> val / Constants.MILLI), + KILOCOULOMB("kC", val -> val * Constants.KILO, val -> val / Constants.KILO), + MEGACOULOMB("MC", val -> val * Constants.MEGA, val -> val / Constants.MEGA); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + ChargeUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public ChargeUnits getBaseUnit() { + return COULOMB; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static ChargeUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return COULOMB; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (ChargeUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equals(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + ChargeUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimAndClean() + .replace("c", "C") + .replace("u", "µ") + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Conductance.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Conductance.java new file mode 100644 index 0000000..a7a143a --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Conductance.java @@ -0,0 +1,162 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class Conductance implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final ConductanceUnit unitType; + + public Conductance(double value, ConductanceUnit unitType) { + this.value = value; + if (unitType == null) { + unitType = ConductanceUnits.SIEMENS; + } + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Conductance of(double value, ConductanceUnit unit) { + return new Conductance(value, unit); + } + + public static Conductance of(double value, String unitSymbol) { + ConductanceUnit resolvedUnit = ConductanceUnits.fromSymbol(unitSymbol); + return new Conductance(value, resolvedUnit); + } + + public static Conductance ofPicoseimens(double value) { // Name kept for compatibility with unit symbol + return new Conductance(value, ConductanceUnits.PICOSIEMENS); + } + + public static Conductance ofNanoseimens(double value) { // Name kept for compatibility with unit symbol + return new Conductance(value, ConductanceUnits.NANOSIEMENS); + } + + public static Conductance ofMicroseimens(double value) { // Name kept for compatibility with unit symbol + return new Conductance(value, ConductanceUnits.MICROSIEMENS); + } + + public static Conductance ofMilliseimens(double value) { // Name kept for compatibility with unit symbol + return new Conductance(value, ConductanceUnits.MILLISIEMENS); + } + + public static Conductance ofSeimens(double value) { + return new Conductance(value, ConductanceUnits.SIEMENS); + } + + public static Conductance ofKiloseimens(double value) { // Name kept for compatibility with unit symbol + return new Conductance(value, ConductanceUnits.KILOSIEMENS); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public ConductanceUnit getUnit() { + return unitType; + } + + @Override + public Conductance toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Conductance toUnit(ConductanceUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Conductance.of(valueInTargetUnit, targetUnit); + } + + @Override + public Conductance toUnit(String targetUnit) { + ConductanceUnit resolvedUnit = ConductanceUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Conductance withValue(double value) { + return Conductance.of(value, unitType); + } + + // Convert to target unit + public Conductance toPicoseimens() { + return toUnit(ConductanceUnits.PICOSIEMENS); + } + + public Conductance toNanoseimens() { + return toUnit(ConductanceUnits.NANOSIEMENS); + } + + public Conductance toMicroseimens() { + return toUnit(ConductanceUnits.MICROSIEMENS); + } + + public Conductance toMilliseimens() { + return toUnit(ConductanceUnits.MILLISIEMENS); + } + + public Conductance toSeimens() { + return toUnit(ConductanceUnits.SIEMENS); + } + + public Conductance toKiloseimens() { + return toUnit(ConductanceUnits.KILOSIEMENS); + } + + // Get value in target unit + public double getInPicoseimens() { + return getInUnit(ConductanceUnits.PICOSIEMENS); + } + + public double getInNanoseimens() { + return getInUnit(ConductanceUnits.NANOSIEMENS); + } + + public double getInMicroseimens() { + return getInUnit(ConductanceUnits.MICROSIEMENS); + } + + public double getInMilliseimens() { + return getInUnit(ConductanceUnits.MILLISIEMENS); + } + + public double getInSeimens() { + return getInUnit(ConductanceUnits.SIEMENS); + } + + public double getInKiloseimens() { + return getInUnit(ConductanceUnits.KILOSIEMENS); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Conductance inputQuantity = (Conductance) o; + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return "Conductance{" + value + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnit.java new file mode 100644 index 0000000..9b33e75 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Unit; + +public interface ConductanceUnit extends Unit { + @Override + ConductanceUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnits.java new file mode 100644 index 0000000..a454fcf --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ConductanceUnits.java @@ -0,0 +1,68 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum ConductanceUnits implements ConductanceUnit { + + SIEMENS("S", val -> val, val -> val), + PICOSIEMENS("pS", val -> val * 1E-12, val -> val / 1E-12), + NANOSIEMENS("nS", val -> val * 1E-9, val -> val / 1E-9), + MICROSIEMENS("µS", val -> val * 1E-6, val -> val / 1E-6), + MILLISIEMENS("mS", val -> val * 1E-3, val -> val / 1E-3), + KILOSIEMENS("kS", val -> val * 1E3, val -> val / 1E3); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + ConductanceUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public ConductanceUnits getBaseUnit() { + return SIEMENS; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static ConductanceUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return SIEMENS; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (ConductanceUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equalsIgnoreCase(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + ConductanceUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimLowerAndClean() + .replace("u", "µ") + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Current.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Current.java new file mode 100644 index 0000000..c8159e2 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Current.java @@ -0,0 +1,139 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class Current implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final CurrentUnit unitType; + + public Current(double value, CurrentUnit unitType) { + this.value = value; + if (unitType == null) { + unitType = CurrentUnits.AMPERE; + } + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Current of(double value, CurrentUnit unit) { + return new Current(value, unit); + } + + public static Current of(double value, String unitSymbol) { + CurrentUnit resolvedUnit = CurrentUnits.fromSymbol(unitSymbol); + return new Current(value, resolvedUnit); + } + + public static Current ofMicroamperes(double value) { + return new Current(value, CurrentUnits.MICROAMPERE); + } + + public static Current ofMilliamperes(double value) { + return new Current(value, CurrentUnits.MILLIAMPERE); + } + + public static Current ofAmperes(double value) { + return new Current(value, CurrentUnits.AMPERE); + } + + public static Current ofKiloamperes(double value) { + return new Current(value, CurrentUnits.KILOAMPERE); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public CurrentUnit getUnit() { + return unitType; + } + + @Override + public Current toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Current toUnit(CurrentUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Current.of(valueInTargetUnit, targetUnit); + } + + @Override + public Current toUnit(String targetUnit) { + CurrentUnit resolvedUnit = CurrentUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Current withValue(double value) { + return Current.of(value, unitType); + } + + // Convert to target unit + public Current toMicroamperes() { + return toUnit(CurrentUnits.MICROAMPERE); + } + + public Current toMilliamperes() { + return toUnit(CurrentUnits.MILLIAMPERE); + } + + public Current toAmperes() { + return toUnit(CurrentUnits.AMPERE); + } + + public Current toKiloamperes() { + return toUnit(CurrentUnits.KILOAMPERE); + } + + + // Get value in target unit + public double getInMicroamperes() { + return getInUnit(CurrentUnits.MICROAMPERE); + } + + public double getInMilliamperes() { + return getInUnit(CurrentUnits.MILLIAMPERE); + } + + public double getInAmperes() { + return getInUnit(CurrentUnits.AMPERE); + } + + public double getInKiloamperes() { + return getInUnit(CurrentUnits.KILOAMPERE); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Current inputQuantity = (Current) o; + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return "Current{" + value + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnit.java new file mode 100644 index 0000000..9959cf1 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Unit; + +public interface CurrentUnit extends Unit { + @Override + CurrentUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnits.java new file mode 100644 index 0000000..6c58bda --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/CurrentUnits.java @@ -0,0 +1,66 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum CurrentUnits implements CurrentUnit { + + AMPERE("A", val -> val, val -> val), + MICROAMPERE("µA", val -> val * 1E-6, val -> val / 1E-6), + MILLIAMPERE("mA", val -> val * 1E-3, val -> val / 1E-3), + KILOAMPERE("kA", val -> val * 1E3, val -> val / 1E3); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + CurrentUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public CurrentUnits getBaseUnit() { + return AMPERE; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static CurrentUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return AMPERE; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (CurrentUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equalsIgnoreCase(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + CurrentUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimLowerAndClean() + .replace("u", "µ") + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Resistance.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Resistance.java new file mode 100644 index 0000000..92d18e9 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Resistance.java @@ -0,0 +1,138 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class Resistance implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final ResistanceUnit unitType; + + public Resistance(double value, ResistanceUnit unitType) { + this.value = value; + if (unitType == null) { + unitType = ResistanceUnits.OHM; + } + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Resistance of(double value, ResistanceUnit unit) { + return new Resistance(value, unit); + } + + public static Resistance of(double value, String unitSymbol) { + ResistanceUnit resolvedUnit = ResistanceUnits.fromSymbol(unitSymbol); + return new Resistance(value, resolvedUnit); + } + + public static Resistance ofMilliohms(double value) { + return new Resistance(value, ResistanceUnits.MILLIOHM); + } + + public static Resistance ofOhms(double value) { + return new Resistance(value, ResistanceUnits.OHM); + } + + public static Resistance ofKiloohms(double value) { + return new Resistance(value, ResistanceUnits.KILOOHM); + } + + public static Resistance ofMegaohms(double value) { + return new Resistance(value, ResistanceUnits.MEGAOHM); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public ResistanceUnit getUnit() { + return unitType; + } + + @Override + public Resistance toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Resistance toUnit(ResistanceUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Resistance.of(valueInTargetUnit, targetUnit); + } + + @Override + public Resistance toUnit(String targetUnit) { + ResistanceUnit resolvedUnit = ResistanceUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Resistance withValue(double value) { + return Resistance.of(value, unitType); + } + + // Convert to target unit + public Resistance toMilliohms() { + return toUnit(ResistanceUnits.MILLIOHM); + } + + public Resistance toOhms() { + return toUnit(ResistanceUnits.OHM); + } + + public Resistance toKiloohms() { + return toUnit(ResistanceUnits.KILOOHM); + } + + public Resistance toMegaohms() { + return toUnit(ResistanceUnits.MEGAOHM); + } + + // Get value in target unit + public double getInMilliohms() { + return getInUnit(ResistanceUnits.MILLIOHM); + } + + public double getInOhms() { + return getInUnit(ResistanceUnits.OHM); + } + + public double getInKiloohms() { + return getInUnit(ResistanceUnits.KILOOHM); + } + + public double getInMegaohms() { + return getInUnit(ResistanceUnits.MEGAOHM); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Resistance inputQuantity = (Resistance) o; + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return "Resistance{" + value + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnit.java new file mode 100644 index 0000000..6bfae95 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Unit; + +public interface ResistanceUnit extends Unit { + @Override + ResistanceUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnits.java new file mode 100644 index 0000000..8c5b300 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/ResistanceUnits.java @@ -0,0 +1,66 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum ResistanceUnits implements ResistanceUnit { + + OHM("Ω", val -> val, val -> val), + MILLIOHM("mΩ", val -> val * 1E-3, val -> val / 1E-3), + KILOOHM("kΩ", val -> val * 1E3, val -> val / 1E3), + MEGAOHM("MΩ", val -> val * 1E6, val -> val / 1E6); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + ResistanceUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public ResistanceUnits getBaseUnit() { + return OHM; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static ResistanceUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return OHM; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (ResistanceUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equals(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + ResistanceUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimAndClean() + .replace("ohm", "Ω") + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Voltage.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Voltage.java new file mode 100644 index 0000000..dda8f08 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/Voltage.java @@ -0,0 +1,163 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.CalculableQuantity; + +import java.util.Objects; + +public class Voltage implements CalculableQuantity { + + private final double value; + private final double baseValue; + private final VoltageUnit unitType; + + public Voltage(double value, VoltageUnit unitType) { + this.value = value; + if (unitType == null) { + unitType = VoltageUnits.VOLT; + } + this.unitType = unitType; + this.baseValue = unitType.toValueInBaseUnit(value); + } + + // Static factory methods + public static Voltage of(double value, VoltageUnit unit) { + return new Voltage(value, unit); + } + + public static Voltage of(double value, String unitSymbol) { + VoltageUnit resolvedUnit = VoltageUnits.fromSymbol(unitSymbol); + return new Voltage(value, resolvedUnit); + } + + public static Voltage ofMicrovolts(double value) { + return new Voltage(value, VoltageUnits.MICROVOLT); + } + + public static Voltage ofMillivolts(double value) { + return new Voltage(value, VoltageUnits.MILLIVOLT); + } + + public static Voltage ofVolts(double value) { + return new Voltage(value, VoltageUnits.VOLT); + } + + public static Voltage ofKilovolts(double value) { + return new Voltage(value, VoltageUnits.KILOVOLT); + } + + public static Voltage ofMegavolts(double value) { + return new Voltage(value, VoltageUnits.MEGAVOLT); + } + + public static Voltage ofGigavolts(double value) { // Added + return new Voltage(value, VoltageUnits.GIGAVOLT); + } + + @Override + public double getValue() { + return value; + } + + @Override + public double getBaseValue() { + return baseValue; + } + + @Override + public VoltageUnit getUnit() { + return unitType; + } + + @Override + public Voltage toBaseUnit() { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + return of(valueInBaseUnit, unitType.getBaseUnit()); + } + + @Override + public Voltage toUnit(VoltageUnit targetUnit) { + double valueInBaseUnit = unitType.toValueInBaseUnit(value); + double valueInTargetUnit = targetUnit.fromValueInBaseUnit(valueInBaseUnit); + return Voltage.of(valueInTargetUnit, targetUnit); + } + + @Override + public Voltage toUnit(String targetUnit) { + VoltageUnit resolvedUnit = VoltageUnits.fromSymbol(targetUnit); + return toUnit(resolvedUnit); + } + + @Override + public Voltage withValue(double value) { + return Voltage.of(value, unitType); + } + + // Convert to target unit + public Voltage toMicrovolts() { + return toUnit(VoltageUnits.MICROVOLT); + } + + public Voltage toMillivolts() { + return toUnit(VoltageUnits.MILLIVOLT); + } + + public Voltage toVolts() { + return toUnit(VoltageUnits.VOLT); + } + + public Voltage toKilovolts() { + return toUnit(VoltageUnits.KILOVOLT); + } + + public Voltage toMegavolts() { + return toUnit(VoltageUnits.MEGAVOLT); + } + + public Voltage toGigavolts() { // Added + return toUnit(VoltageUnits.GIGAVOLT); + } + + + // Get value in target unit + public double getInMicrovolts() { + return getInUnit(VoltageUnits.MICROVOLT); + } + + public double getInMillivolts() { + return getInUnit(VoltageUnits.MILLIVOLT); + } + + public double getInVolts() { + return getInUnit(VoltageUnits.VOLT); + } + + public double getInKilovolts() { + return getInUnit(VoltageUnits.KILOVOLT); + } + + public double getInMegavolts() { + return getInUnit(VoltageUnits.MEGAVOLT); + } + + public double getInGigavolts() { // Added + return getInUnit(VoltageUnits.GIGAVOLT); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Voltage inputQuantity = (Voltage) o; + return Double.compare(inputQuantity.toBaseUnit().getValue(), baseValue) == 0 && Objects.equals(unitType.getBaseUnit(), inputQuantity.getUnit().getBaseUnit()); + } + + @Override + public int hashCode() { + return Objects.hash(baseValue, unitType.getBaseUnit()); + } + + @Override + public String toString() { + return "Voltage{" + value + unitType.getSymbol() + '}'; + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnit.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnit.java new file mode 100644 index 0000000..f483fce --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnit.java @@ -0,0 +1,8 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.Unit; + +public interface VoltageUnit extends Unit { + @Override + VoltageUnit getBaseUnit(); +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnits.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnits.java new file mode 100644 index 0000000..29cdc07 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/electric/VoltageUnits.java @@ -0,0 +1,69 @@ +package com.synerset.unitility.unitsystem.electric; + +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.StringTransformer; + +import java.util.function.DoubleUnaryOperator; + +public enum VoltageUnits implements VoltageUnit { + + VOLT("V", val -> val, val -> val), // Base Unit + MICROVOLT("µV", val -> val * 1E-6, val -> val / 1E-6), + MILLIVOLT("mV", val -> val * 1E-3, val -> val / 1E-3), + KILOVOLT("kV", val -> val * 1E3, val -> val / 1E3), + MEGAVOLT("MV", val -> val * 1E6, val -> val / 1E6), + GIGAVOLT("GV", val -> val * 1E9, val -> val / 1E9); + + private final String symbol; + private final DoubleUnaryOperator toBaseConverter; + private final DoubleUnaryOperator fromBaseToUnitConverter; + + VoltageUnits(String symbol, DoubleUnaryOperator toBaseConverter, DoubleUnaryOperator fromBaseToUnitConverter) { + this.symbol = symbol; + this.toBaseConverter = toBaseConverter; + this.fromBaseToUnitConverter = fromBaseToUnitConverter; + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public VoltageUnits getBaseUnit() { + return VOLT; + } + + @Override + public double toValueInBaseUnit(double valueInThisUnit) { + return toBaseConverter.applyAsDouble(valueInThisUnit); + } + + @Override + public double fromValueInBaseUnit(double valueInBaseUnit) { + return fromBaseToUnitConverter.applyAsDouble(valueInBaseUnit); + } + + public static VoltageUnit fromSymbol(String rawSymbol) { + if (rawSymbol == null || rawSymbol.isBlank()) { + return VOLT; + } + String requestedSymbol = unifySymbol(rawSymbol); + for (VoltageUnit unit : values()) { + String currentSymbol = unifySymbol(unit.getSymbol()); + if (currentSymbol.equals(requestedSymbol)) { + return unit; + } + } + throw new UnitSystemParseException("Unsupported unit symbol: " + "{" + rawSymbol + "}." + " Target class: " + + VoltageUnits.class.getSimpleName()); + } + + private static String unifySymbol(String inputString) { + return StringTransformer.of(inputString) + .trimAndClean() + .replace("v", "V") + .replace("u", "µ") + .toString(); + } +} \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java index 7bd4ab5..a5522f6 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityDefaultParsingFactory.java @@ -8,6 +8,7 @@ import com.synerset.unitility.unitsystem.acoustic.SoundPressureUnits; import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.*; +import com.synerset.unitility.unitsystem.electric.*; import com.synerset.unitility.unitsystem.flow.MassFlow; import com.synerset.unitility.unitsystem.flow.MassFlowUnits; import com.synerset.unitility.unitsystem.flow.VolumetricFlow; @@ -38,7 +39,7 @@ final class PhysicalQuantityDefaultParsingFactory extends PhysicalQuantityAbstra private PhysicalQuantityDefaultParsingFactory() { // Initializing immutable registry - this.immutableParsingRegistry = Map.ofEntries( + this.immutableParsingRegistry = Map., BiFunction>>ofEntries( // Common Map.entry(Angle.class, Angle::of), Map.entry(Area.class, Area::of), @@ -100,11 +101,18 @@ private PhysicalQuantityDefaultParsingFactory() { Map.entry(SoundPower.class, SoundPower::of), Map.entry(SoundPressure.class, SoundPressure::of), // Oscillation - Map.entry(Frequency.class, Frequency::of) + Map.entry(Frequency.class, Frequency::of), + // Electric + Map.entry(Capacitance.class, Capacitance::of), + Map.entry(Charge.class, Charge::of), + Map.entry(Conductance.class, Conductance::of), + Map.entry(Current.class, Current::of), + Map.entry(Resistance.class, Resistance::of), + Map.entry(Voltage.class, Voltage::of) ); // Initializing immutable default unit registry - this.immutableDefaultUnitRegistry = Map.ofEntries( + this.immutableDefaultUnitRegistry = Map., Unit>ofEntries( // Common (16) Map.entry(Angle.class, AngleUnits.RADIANS), Map.entry(AngularVelocity.class, AngularVelocityUnits.RADIANS_PER_SECOND), @@ -166,7 +174,14 @@ private PhysicalQuantityDefaultParsingFactory() { Map.entry(SoundPower.class, SoundPowerUnits.WATT), Map.entry(SoundPressure.class, SoundPressureUnits.PASCAL), // Oscillation (1) - Map.entry(Frequency.class, FrequencyUnits.HERTZ) + Map.entry(Frequency.class, FrequencyUnits.HERTZ), + // Electric (6) + Map.entry(Capacitance.class, CapacitanceUnits.FARAD), + Map.entry(Charge.class, ChargeUnits.COULOMB), + Map.entry(Conductance.class, ConductanceUnits.SIEMENS), + Map.entry(Current.class, CurrentUnits.AMPERE), + Map.entry(Resistance.class, ResistanceUnits.OHM), + Map.entry(Voltage.class, VoltageUnits.VOLT) ); } diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/StringTransformer.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/StringTransformer.java index 7822195..d03f733 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/StringTransformer.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/StringTransformer.java @@ -28,6 +28,19 @@ public static StringTransformer of(String inputString) { return new StringTransformer(inputString); } + /** + * Trims the input string, and removes spaces. + * + * @return A new StringTransformer instance with the transformed string. + */ + public StringTransformer trimAndClean() { + return StringTransformer.of( + inputString.trim() + .replace(" ", "") + .replace("_", "") + ); + } + /** * Trims the input string, converts to lowercase, and removes spaces. * diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java index 400ad30..e5d3474 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistry.java @@ -8,6 +8,7 @@ import com.synerset.unitility.unitsystem.acoustic.SoundPressureUnits; import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.*; +import com.synerset.unitility.unitsystem.electric.*; import com.synerset.unitility.unitsystem.flow.MassFlow; import com.synerset.unitility.unitsystem.flow.MassFlowUnits; import com.synerset.unitility.unitsystem.flow.VolumetricFlow; @@ -42,7 +43,7 @@ public class SupportedQuantitiesRegistry { private SupportedQuantitiesRegistry() { // Initializing immutable registry - this.immutableRegistry = Map.ofEntries( + this.immutableRegistry = Map., Supplier>>ofEntries( // Common Map.entry(Angle.class, () -> Arrays.asList(AngleUnits.values())), Map.entry(Area.class, () -> Arrays.asList(AreaUnits.values())), @@ -105,7 +106,14 @@ private SupportedQuantitiesRegistry() { Map.entry(SoundPower.class, () -> Arrays.asList(SoundPowerUnits.values())), Map.entry(SoundPressure.class, () -> Arrays.asList(SoundPressureUnits.values())), // Oscillation - Map.entry(Frequency.class, () -> Arrays.asList(FrequencyUnits.values())) + Map.entry(Frequency.class, () -> Arrays.asList(FrequencyUnits.values())), + // Electric (6) + Map.entry(Capacitance.class, () -> Arrays.asList(CapacitanceUnits.values())), + Map.entry(Charge.class, () -> Arrays.asList(ChargeUnits.values())), + Map.entry(Conductance.class, () -> Arrays.asList(ConductanceUnits.values())), + Map.entry(Current.class, () -> Arrays.asList(CurrentUnits.values())), + Map.entry(Resistance.class, () -> Arrays.asList(ResistanceUnits.values())), + Map.entry(Voltage.class, () -> Arrays.asList(VoltageUnits.values())) ); } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CapacitanceTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CapacitanceTest.java new file mode 100644 index 0000000..7eeb5a7 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CapacitanceTest.java @@ -0,0 +1,117 @@ +package com.synerset.unitility.unitsystem.electric; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class CapacitanceTest { + + private static final double DELTA = 1E-11; + + @Test + @DisplayName("should have FARAD as base unit") + void shouldHaveFaradAsBaseUnit() { + // Given + CapacitanceUnit expectedBaseUnit = CapacitanceUnits.FARAD; + + // When + Capacitance capacitanceInMicrofarads = Capacitance.ofMicrofarads(100.0); + CapacitanceUnit actualBaseUnit = capacitanceInMicrofarads.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("should correctly compare capacitances in different units (equality check)") + void shouldCorrectlyCompareCapacitancesInDifferentUnits() { + // Given + // 1 µF + Capacitance oneMicrofarad = Capacitance.ofMicrofarads(1.0); + // 1000 nF (1000 * 10^-9 = 10^-6 F) + Capacitance oneThousandNanofarads = Capacitance.ofNanofarads(1000.0); + + // When & Then + assertThat(oneMicrofarad.getInMicrofarads()).isEqualTo(oneThousandNanofarads.getInMicrofarads(), withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Farads (F) to Millifarads (mF), Microfarads (µF), Nanofarads (nF), and Picofarads (pF)") + void shouldProperlyConvertFromFarads() { + // Given + Capacitance initialCapacitance = Capacitance.ofFarads(0.005); // 5 mF + + // When + double actualInMillifarads = initialCapacitance.getInMillifarads(); // x 10^3 + double actualInMicrofarads = initialCapacitance.getInMicrofarads(); // x 10^6 + double actualInNanofarads = initialCapacitance.getInNanofarads(); // x 10^9 + double actualInPicofarads = initialCapacitance.getInPicofarads(); // x 10^12 + + // Then + assertThat(actualInMillifarads).isEqualTo(5.0, withPrecision(DELTA)); + assertThat(actualInMicrofarads).isEqualTo(5_000.0, withPrecision(DELTA)); + assertThat(actualInNanofarads).isEqualTo(5_000_000.0, withPrecision(DELTA)); + assertThat(actualInPicofarads).isEqualTo(5_000_000_000.0, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Microfarads (µF) to Kilofarads (kF) and Megafarads (MF)") + void shouldProperlyConvertToUpperUnits() { + // Given + Capacitance initialCapacitance = Capacitance.ofMicrofarads(1000.0); // 1000 µF = 1 mF = 0.001 F + + // When + double actualInFarads = initialCapacitance.getInFarads(); // 0.001 F + double actualInKilofarads = initialCapacitance.getInKilofarads(); // / 10^3 + double actualInMegafarads = initialCapacitance.getInMegafarads(); // / 10^6 + + // Then + assertThat(actualInFarads).isEqualTo(0.001, withPrecision(DELTA)); + assertThat(actualInKilofarads).isEqualTo(0.000001, withPrecision(DELTA)); // 1E-6 kF + assertThat(actualInMegafarads).isEqualTo(0.000000001, withPrecision(DELTA)); // 1E-9 MF + } + + + @ParameterizedTest(name = "Convert {0} {1} to {2} should yield {3}") + @MethodSource("conversionTestData") + @DisplayName("should correctly perform conversion for all defined units across the full range") + void shouldCorrectlyPerformConversionForAllUnits(double initialValue, CapacitanceUnit initialUnit, CapacitanceUnit targetUnit, double expectedValue) { + // Given + Capacitance initialCapacitance = Capacitance.of(initialValue, initialUnit); + + // When + double actualValue = initialCapacitance.toUnit(targetUnit).getValue(); + + // Then + assertThat(actualValue).isEqualTo(expectedValue, withPrecision(DELTA)); + } + + private static Stream conversionTestData() { + return Stream.of( + // MF -> F (x 10^6) + arguments(0.001, CapacitanceUnits.MEGAFARAD, CapacitanceUnits.FARAD, 1000.0), + // kF -> mF (x 10^6) + arguments(1.0, CapacitanceUnits.KILOFARAD, CapacitanceUnits.MILLIFARAD, 1_000_000.0), + // F -> µF (x 10^6) + arguments(0.0000025, CapacitanceUnits.FARAD, CapacitanceUnits.MICROFARAD, 2.5), + // µF -> nF (x 10^3) + arguments(0.8, CapacitanceUnits.MICROFARAD, CapacitanceUnits.NANOFARAD, 800.0), + // nF -> pF (x 10^3) + arguments(15.0, CapacitanceUnits.NANOFARAD, CapacitanceUnits.PICOFARAD, 15_000.0), + // pF -> nF (x 10^-3) + arguments(5000.0, CapacitanceUnits.PICOFARAD, CapacitanceUnits.NANOFARAD, 5.0), + // µF -> F (x 10^-6) + arguments(120.0, CapacitanceUnits.MICROFARAD, CapacitanceUnits.FARAD, 0.000120), + // mF -> kF (x 10^-6) + arguments(500.0, CapacitanceUnits.MILLIFARAD, CapacitanceUnits.KILOFARAD, 0.0005) + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ChargeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ChargeTest.java new file mode 100644 index 0000000..feae9b7 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ChargeTest.java @@ -0,0 +1,118 @@ +package com.synerset.unitility.unitsystem.electric; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class ChargeTest { + + private static final double DELTA = 1E-12; + + @Test + @DisplayName("should have COULOMB as base unit") + void shouldHaveCoulombAsBaseUnit() { + // Given + ChargeUnit expectedBaseUnit = ChargeUnits.COULOMB; + + // When + Charge chargeInMicrocoulombs = Charge.ofMicrocoulombs(10.0); + ChargeUnit actualBaseUnit = chargeInMicrocoulombs.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("should correctly compare charges in different units (equality check using base value comparison)") + void shouldCorrectlyCompareChargesInDifferentUnits() { + // Given + // 1 C + Charge oneCoulomb = Charge.ofCoulombs(1.0); + // 1000 mC + Charge oneThousandMillicoulombs = Charge.ofMillicoulombs(1000.0); + // 1 000 000 µC + Charge oneMillionMicrocoulombs = Charge.ofMicrocoulombs(1_000_000.0); + + // When & Then + double baseValue1 = oneCoulomb.toBaseUnit().getValue(); + double baseValue2 = oneThousandMillicoulombs.toBaseUnit().getValue(); + double baseValue3 = oneMillionMicrocoulombs.toBaseUnit().getValue(); + + assertThat(baseValue1).isEqualTo(baseValue2, withPrecision(DELTA)); + assertThat(baseValue1).isEqualTo(baseValue3, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Kilocoulombs (kC) to Coulombs (C) and lower units") + void shouldProperlyConvertFromKilocoulombs() { + // Given + Charge initialCharge = Charge.ofKilocoulombs(0.005); // 0.005 kC = 5 C + + // When + double actualInCoulombs = initialCharge.getInCoulombs(); // x 10^3 + double actualInMillicoulombs = initialCharge.getInMillicoulombs();// x 10^6 + double actualInMicrocoulombs = initialCharge.getInMicrocoulombs();// x 10^9 + + // Then + assertThat(actualInCoulombs).isEqualTo(5.0, withPrecision(DELTA)); + assertThat(actualInMillicoulombs).isEqualTo(5_000.0, withPrecision(DELTA)); + assertThat(actualInMicrocoulombs).isEqualTo(5_000_000.0, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Picocoulombs (pC) to Nanocoulombs (nC)") + void shouldProperlyConvertBetweenLowerUnits() { + // Given + Charge initialCharge = Charge.ofPicocoulombs(750.0); // 750 pC + + // When + double actualInNanocoulombs = initialCharge.getInNanocoulombs(); // x 10^-3 + Charge actualBackToPicocoulombs = initialCharge.toNanocoulombs().toPicocoulombs(); + + // Then + assertThat(actualInNanocoulombs).isEqualTo(0.75, withPrecision(DELTA)); + assertThat(actualBackToPicocoulombs.getValue()).isEqualTo(750.0, withPrecision(DELTA)); + } + + @ParameterizedTest(name = "Convert {0} {1} to {2} should yield {3}") + @MethodSource("conversionTestData") + @DisplayName("should correctly perform conversion for all defined units") + void shouldCorrectlyPerformConversionForAllUnits(double initialValue, ChargeUnit initialUnit, ChargeUnit targetUnit, double expectedValue) { + // Given + Charge initialCharge = Charge.of(initialValue, initialUnit); + + // When + double actualValue = initialCharge.toUnit(targetUnit).getValue(); + + // Then + assertThat(actualValue).isEqualTo(expectedValue, withPrecision(DELTA)); + } + + private static Stream conversionTestData() { + return Stream.of( + // MC -> kC (x 10^3) + arguments(0.001, ChargeUnits.MEGACOULOMB, ChargeUnits.KILOCOULOMB, 1.0), + // kC -> C (x 10^3) + arguments(2.5, ChargeUnits.KILOCOULOMB, ChargeUnits.COULOMB, 2500.0), + // C -> mC (x 10^3) + arguments(0.005, ChargeUnits.COULOMB, ChargeUnits.MILLICOULOMB, 5.0), + // mC -> µC (x 10^3) + arguments(0.12, ChargeUnits.MILLICOULOMB, ChargeUnits.MICROCOULOMB, 120.0), + arguments(0.003, ChargeUnits.MICROCOULOMB, ChargeUnits.NANOCOULOMB, 3.0), + // nC -> pC (x 10^3) + arguments(1.0, ChargeUnits.NANOCOULOMB, ChargeUnits.PICOCOULOMB, 1000.0), + // C -> µC (x 10^6) + arguments(0.000004, ChargeUnits.COULOMB, ChargeUnits.MICROCOULOMB, 4.0), + // pC -> nC (x 10^-3) + arguments(5000.0, ChargeUnits.PICOCOULOMB, ChargeUnits.NANOCOULOMB, 5.0) + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ConductanceTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ConductanceTest.java new file mode 100644 index 0000000..5927c56 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ConductanceTest.java @@ -0,0 +1,125 @@ +package com.synerset.unitility.unitsystem.electric; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class ConductanceTest { + + private static final double DELTA = 1E-11; + + @Test + @DisplayName("should have SIEMENS as base unit") + void shouldHaveSiemensAsBaseUnit() { + // Given + ConductanceUnit expectedBaseUnit = ConductanceUnits.SIEMENS; + + // When + Conductance conductanceInMillisiemens = Conductance.ofMilliseimens(10.0); + ConductanceUnit actualBaseUnit = conductanceInMillisiemens.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("should correctly compare conductances in different units (equality check using base value comparison)") + void shouldCorrectlyCompareConductancesInDifferentUnits() { + // Given + // 1 mS + Conductance oneMillisiemens = Conductance.ofMilliseimens(1.0); + // 1000 µS + Conductance oneThousandMicrosiemens = Conductance.ofMicroseimens(1000.0); + // 0.001 S + Conductance oneThousandthSiemens = Conductance.ofSeimens(0.001); + + // When & Then + double baseValue1 = oneMillisiemens.toBaseUnit().getValue(); + double baseValue2 = oneThousandMicrosiemens.toBaseUnit().getValue(); + double baseValue3 = oneThousandthSiemens.toBaseUnit().getValue(); + + assertThat(baseValue1).isEqualTo(baseValue2, withPrecision(DELTA)); + assertThat(baseValue1).isEqualTo(baseValue3, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Siemens (S) to lower units (mS, µS, nS, pS)") + void shouldProperlyConvertFromSiemensToLowerUnits() { + // Given + Conductance initialConductance = Conductance.ofSeimens(0.000005); // 5 µS + + // When + double actualInMillisiemens = initialConductance.getInMilliseimens(); // x 10^3 + double actualInMicrosiemens = initialConductance.getInMicroseimens(); // x 10^6 + double actualInNanosiemsns = initialConductance.getInNanoseimens(); // x 10^9 + double actualInPicoseimens = initialConductance.getInPicoseimens(); // x 10^12 + + // Then + assertThat(actualInMillisiemens).isEqualTo(0.005, withPrecision(DELTA)); + assertThat(actualInMicrosiemens).isEqualTo(5.0, withPrecision(DELTA)); + assertThat(actualInNanosiemsns).isEqualTo(5_000.0, withPrecision(DELTA)); + assertThat(actualInPicoseimens).isEqualTo(5_000_000.0, withPrecision(1E-9)); + } + + @Test + @DisplayName("should properly convert Microseimens (µS) to Siemens (S) and Kiloseimens (kS)") + void shouldProperlyConvertToUpperUnits() { + // Given + Conductance initialConductance = Conductance.ofMicroseimens(2000.0); // 2000 µS = 2 mS = 0.002 S + + // When + double actualInMillisiemens = initialConductance.getInMilliseimens(); // x 10^-3 + double actualInSeimens = initialConductance.getInSeimens(); // x 10^-6 + double actualInKiloseimens = initialConductance.getInKiloseimens(); // x 10^-9 + + // Then + assertThat(actualInMillisiemens).isEqualTo(2.0, withPrecision(DELTA)); + assertThat(actualInSeimens).isEqualTo(0.002, withPrecision(DELTA)); + assertThat(actualInKiloseimens).isEqualTo(0.000002, withPrecision(DELTA)); // 2E-6 kS + } + + + @ParameterizedTest(name = "Convert {0} {1} to {2} should yield {3}") + @MethodSource("conversionTestData") + @DisplayName("should correctly perform conversion for all defined units across the full range") + void shouldCorrectlyPerformConversionForAllUnits(double initialValue, ConductanceUnit initialUnit, ConductanceUnit targetUnit, double expectedValue) { + // Given + Conductance initialConductance = Conductance.of(initialValue, initialUnit); + + // When + double actualValue = initialConductance.toUnit(targetUnit).getValue(); + + // Then + assertThat(actualValue).isEqualTo(expectedValue, withPrecision(DELTA)); + } + + private static Stream conversionTestData() { + return Stream.of( + // kS -> S (x 10^3) + arguments(0.05, ConductanceUnits.KILOSIEMENS, ConductanceUnits.SIEMENS, 50.0), + // S -> mS (x 10^3) + arguments(0.012, ConductanceUnits.SIEMENS, ConductanceUnits.MILLISIEMENS, 12.0), + // mS -> µS (x 10^3) + arguments(4.5, ConductanceUnits.MILLISIEMENS, ConductanceUnits.MICROSIEMENS, 4500.0), + // µS -> nS (x 10^3) + arguments(0.9, ConductanceUnits.MICROSIEMENS, ConductanceUnits.NANOSIEMENS, 900.0), + // nS -> pS (x 10^3) + arguments(10.0, ConductanceUnits.NANOSIEMENS, ConductanceUnits.PICOSIEMENS, 10_000.0), + + // pS -> µS (x 10^-6) + arguments(3_000_000.0, ConductanceUnits.PICOSIEMENS, ConductanceUnits.MICROSIEMENS, 3.0), + // mS -> S (x 10^-3) + arguments(500.0, ConductanceUnits.MILLISIEMENS, ConductanceUnits.SIEMENS, 0.5), + // S -> kS (x 10^-3) + arguments(1500.0, ConductanceUnits.SIEMENS, ConductanceUnits.KILOSIEMENS, 1.5) + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CurrentTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CurrentTest.java new file mode 100644 index 0000000..9b451f5 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/CurrentTest.java @@ -0,0 +1,111 @@ +package com.synerset.unitility.unitsystem.electric; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class CurrentTest { + + private static final double DELTA = 1E-12; // Precision for double comparisons + + @Test + @DisplayName("should have AMPERE as base unit") + void shouldHaveAmpereAsBaseUnit() { + // Given + CurrentUnit expectedBaseUnit = CurrentUnits.AMPERE; + + // When + Current currentInMilliamperes = Current.ofMilliamperes(500.0); + CurrentUnit actualBaseUnit = currentInMilliamperes.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("should correctly compare currents in different units (equality check)") + void shouldCorrectlyCompareCurrentsInDifferentUnits() { + // Given + Current oneAmpere = Current.ofAmperes(1.0); + Current oneThousandMilliamperes = Current.ofMilliamperes(1000.0); + + // When & Then + assertThat(oneAmpere).isEqualTo(oneThousandMilliamperes); + } + + // --- TESTY KONWERSJI --- + + @Test + @DisplayName("should properly convert Amperes (A) to Milliamperes (mA) and Microamperes (µA)") + void shouldProperlyConvertFromAmperes() { + // Given + Current initialCurrent = Current.ofAmperes(0.5); // 0.5 A + + // When + double actualInMilliamperes = initialCurrent.getInMilliamperes(); + double actualInMicroamperes = initialCurrent.getInMicroamperes(); + double actualInKiloamperes = initialCurrent.getInKiloamperes(); + Current actualBackToA = initialCurrent.toMilliamperes().toAmperes(); + + // Then + assertThat(actualInMilliamperes).isEqualTo(500.0, withPrecision(DELTA)); + assertThat(actualInMicroamperes).isEqualTo(500_000.0, withPrecision(DELTA)); + assertThat(actualInKiloamperes).isEqualTo(0.0005, withPrecision(DELTA)); + assertThat(actualBackToA).isEqualTo(initialCurrent); + assertThat(actualBackToA.getValue()).isEqualTo(0.5, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Kiloamperes (kA) to Amperes (A)") + void shouldProperlyConvertFromKiloamperes() { + // Given + Current initialCurrent = Current.ofKiloamperes(15.0); // 15 kA + + // When + double actualInAmperes = initialCurrent.getInAmperes(); + Current actualBackToKA = initialCurrent.toAmperes().toKiloamperes(); + + // Then + assertThat(actualInAmperes).isEqualTo(15_000.0, withPrecision(DELTA)); + assertThat(actualBackToKA.getValue()).isEqualTo(15.0, withPrecision(DELTA)); + } + + @ParameterizedTest(name = "Convert {0} {1} to {2} should yield {3}") + @MethodSource("conversionTestData") + @DisplayName("should correctly perform conversion for all defined units") + void shouldCorrectlyPerformConversionForAllUnits(double initialValue, CurrentUnit initialUnit, CurrentUnit targetUnit, double expectedValue) { + // Given + Current initialCurrent = Current.of(initialValue, initialUnit); + + // When + double actualValue = initialCurrent.toUnit(targetUnit).getValue(); + + // Then + assertThat(actualValue).isEqualTo(expectedValue, withPrecision(DELTA)); + } + + private static Stream conversionTestData() { + return Stream.of( + // kA -> A (x 10^3) + arguments(5.0, CurrentUnits.KILOAMPERE, CurrentUnits.AMPERE, 5000.0), + // A -> mA (x 10^3) + arguments(1.23, CurrentUnits.AMPERE, CurrentUnits.MILLIAMPERE, 1230.0), + // mA -> µA (x 10^3) + arguments(150.0, CurrentUnits.MILLIAMPERE, CurrentUnits.MICROAMPERE, 150_000.0), + // µA -> A (x 10^-6) + arguments(250_000.0, CurrentUnits.MICROAMPERE, CurrentUnits.AMPERE, 0.25), + // µA -> mA (x 10^-3) + arguments(75.0, CurrentUnits.MICROAMPERE, CurrentUnits.MILLIAMPERE, 0.075), + // kA -> µA (x 10^9) + arguments(0.001, CurrentUnits.KILOAMPERE, CurrentUnits.MICROAMPERE, 1_000_000.0) + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ResistanceTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ResistanceTest.java new file mode 100644 index 0000000..5a60adf --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/ResistanceTest.java @@ -0,0 +1,111 @@ +package com.synerset.unitility.unitsystem.electric; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class ResistanceTest { + + private static final double DELTA = 1E-12; + + @Test + @DisplayName("should have OHM as base unit") + void shouldHaveOhmAsBaseUnit() { + // Given + ResistanceUnit expectedBaseUnit = ResistanceUnits.OHM; + + // When + Resistance resistanceInKiloohms = Resistance.ofKiloohms(10.0); + ResistanceUnit actualBaseUnit = resistanceInKiloohms.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("should correctly compare resistances in different units (equality check)") + void shouldCorrectlyCompareResistancesInDifferentUnits() { + // Given + // 1 kΩ + Resistance oneKiloohm = Resistance.ofKiloohms(1.0); + // 1000 Ω + Resistance oneThousandOhms = Resistance.ofOhms(1000.0); + // 1 000 000 mΩ + Resistance oneMillionMilliohms = Resistance.ofMilliohms(1_000_000.0); + + // When & Then + assertThat(oneKiloohm.toBaseUnit().getValue()).isEqualTo(oneThousandOhms.toBaseUnit().getValue(), withPrecision(DELTA)); + assertThat(oneKiloohm.toBaseUnit().getValue()).isEqualTo(oneMillionMilliohms.toBaseUnit().getValue(), withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Megaohms (MΩ) to Kiloohms (kΩ) and Ohms (Ω)") + void shouldProperlyConvertFromMegaohms() { + // Given + Resistance initialResistance = Resistance.ofMegaohms(0.0025); // 0.0025 MΩ = 2500 Ω + + // When + double actualInKiloohms = initialResistance.getInKiloohms(); // x 10^3 + double actualInOhms = initialResistance.getInOhms(); // x 10^6 + double actualInMilliohms = initialResistance.getInMilliohms();// x 10^9 + + // Then + assertThat(actualInKiloohms).isEqualTo(2.5, withPrecision(DELTA)); + assertThat(actualInOhms).isEqualTo(2500.0, withPrecision(DELTA)); + assertThat(actualInMilliohms).isEqualTo(2_500_000.0, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Ohms (Ω) to Milliohms (mΩ)") + void shouldProperlyConvertFromOhms() { + // Given + Resistance initialResistance = Resistance.ofOhms(0.125); // 0.125 Ω + + // When + double actualInMilliohms = initialResistance.getInMilliohms(); // x 10^3 + Resistance actualBackToOhm = initialResistance.toMilliohms().toOhms(); + + // Then + assertThat(actualInMilliohms).isEqualTo(125.0, withPrecision(DELTA)); + assertThat(actualBackToOhm.getValue()).isEqualTo(0.125, withPrecision(DELTA)); + } + + @ParameterizedTest(name = "Convert {0} {1} to {2} should yield {3}") + @MethodSource("conversionTestData") + @DisplayName("should correctly perform conversion for all defined units") + void shouldCorrectlyPerformConversionForAllUnits(double initialValue, ResistanceUnit initialUnit, ResistanceUnit targetUnit, double expectedValue) { + // Given + Resistance initialResistance = Resistance.of(initialValue, initialUnit); + + // When + double actualValue = initialResistance.toUnit(targetUnit).getValue(); + + // Then + assertThat(actualValue).isEqualTo(expectedValue, withPrecision(DELTA)); + } + + private static Stream conversionTestData() { + return Stream.of( + // MΩ -> kΩ (x 10^3) + arguments(0.005, ResistanceUnits.MEGAOHM, ResistanceUnits.KILOOHM, 5.0), + // kΩ -> Ω (x 10^3) + arguments(10.0, ResistanceUnits.KILOOHM, ResistanceUnits.OHM, 10_000.0), + // Ω -> mΩ (x 10^3) + arguments(0.2, ResistanceUnits.OHM, ResistanceUnits.MILLIOHM, 200.0), + // mΩ -> Ω (x 10^-3) + arguments(750.0, ResistanceUnits.MILLIOHM, ResistanceUnits.OHM, 0.75), + // kΩ -> MΩ (x 10^-3) + arguments(5000.0, ResistanceUnits.KILOOHM, ResistanceUnits.MEGAOHM, 5.0), + // MΩ -> mΩ (x 10^9) + arguments(0.000001, ResistanceUnits.MEGAOHM, ResistanceUnits.MILLIOHM, 1000.0) + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/VoltageTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/VoltageTest.java new file mode 100644 index 0000000..ee3a735 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/electric/VoltageTest.java @@ -0,0 +1,115 @@ +package com.synerset.unitility.unitsystem.electric; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class VoltageTest { + + private static final double DELTA = 1E-12; + + @Test + @DisplayName("should have VOLT as base unit") + void shouldHaveVoltAsBaseUnit() { + // Given + VoltageUnit expectedBaseUnit = VoltageUnits.VOLT; + + // When + Voltage voltageInKilovolts = Voltage.ofKilovolts(10.0); + VoltageUnit actualBaseUnit = voltageInKilovolts.getUnit().getBaseUnit(); + + // Then + assertThat(actualBaseUnit).isEqualTo(expectedBaseUnit); + } + + @Test + @DisplayName("should correctly compare voltages in different units (equality check)") + void shouldCorrectlyCompareVoltagesInDifferentUnits() { + // Given + Voltage oneKilovolt = Voltage.ofKilovolts(1.0); + Voltage oneThousandVolts = Voltage.ofVolts(1000.0); + + // When & Then + assertThat(oneKilovolt).isEqualTo(oneThousandVolts); + } + + // --- TESTY KONWERSJI --- + + @Test + @DisplayName("should properly convert Kilovolts (kV) to Volts (V) and Gigavolts (GV)") + void shouldProperlyConvertFromKilovolts() { + // Given + Voltage initialVoltage = Voltage.ofKilovolts(25.0); // 25 kV + + // When + double actualInVolts = initialVoltage.getInVolts(); + double actualInMillivolts = initialVoltage.getInMillivolts(); + double actualInGigavolts = initialVoltage.getInGigavolts(); + Voltage actualBackTokV = initialVoltage.toVolts().toKilovolts(); + + // Then + assertThat(actualInVolts).isEqualTo(25_000.0, withPrecision(DELTA)); + assertThat(actualInMillivolts).isEqualTo(25_000_000.0, withPrecision(DELTA)); + assertThat(actualInGigavolts).isEqualTo(0.000025, withPrecision(DELTA)); + assertThat(actualBackTokV).isEqualTo(initialVoltage); + assertThat(actualBackTokV.getValue()).isEqualTo(25.0, withPrecision(DELTA)); + } + + @Test + @DisplayName("should properly convert Microvolts (µV) to Millivolts (mV) and Volts (V)") + void shouldProperlyConvertFromMicrovolts() { + // Given + Voltage initialVoltage = Voltage.ofMicrovolts(5_000.0); // 5000 µV + + // When + double actualInMillivolts = initialVoltage.getInMillivolts(); + double actualInVolts = initialVoltage.getInVolts(); + Voltage actualBackToMicrovolts = initialVoltage.toVolts().toMicrovolts(); + + // Then + assertThat(actualInMillivolts).isEqualTo(5.0, withPrecision(DELTA)); + assertThat(actualInVolts).isEqualTo(0.005, withPrecision(DELTA)); + assertThat(actualBackToMicrovolts.getValue()).isEqualTo(5_000.0, withPrecision(DELTA)); + } + + @ParameterizedTest(name = "Convert {0} {1} to {2} should yield {3}") + @MethodSource("conversionTestData") + @DisplayName("should correctly perform conversion for all defined units") + void shouldCorrectlyPerformConversionForAllUnits(double initialValue, VoltageUnit initialUnit, VoltageUnit targetUnit, double expectedValue) { + // Given + Voltage initialVoltage = Voltage.of(initialValue, initialUnit); + + // When + double actualValue = initialVoltage.toUnit(targetUnit).getValue(); + + // Then + assertThat(actualValue).isEqualTo(expectedValue, withPrecision(DELTA)); + } + + private static Stream conversionTestData() { + return Stream.of( + // MV -> V (x 10^6) + arguments(5.0, VoltageUnits.MEGAVOLT, VoltageUnits.VOLT, 5_000_000.0), + // GV -> V (x 10^9) + arguments(0.001, VoltageUnits.GIGAVOLT, VoltageUnits.VOLT, 1_000_000.0), + // V -> mV (x 10^3) + arguments(1.23, VoltageUnits.VOLT, VoltageUnits.MILLIVOLT, 1230.0), + // µV -> V (x 10^-6) + arguments(400_000.0, VoltageUnits.MICROVOLT, VoltageUnits.VOLT, 0.4), + // V -> µV (x 10^6) + arguments(0.0001, VoltageUnits.VOLT, VoltageUnits.MICROVOLT, 100.0), + // GV -> kV (x 10^6) + arguments(0.5, VoltageUnits.GIGAVOLT, VoltageUnits.KILOVOLT, 500_000.0), + // mV -> µV (x 10^3) + arguments(2.0, VoltageUnits.MILLIVOLT, VoltageUnits.MICROVOLT, 2000.0) + ); + } +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java index 4bbf603..cd6de0d 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/SupportedQuantitiesRegistryTest.java @@ -26,7 +26,7 @@ void findAllSupportedQuantities_shouldFindAllSupportedQuantitiesAndAssociatedUni Set allSupportedQuantities = QUANTITY_REGISTRY.findAllSupportedQuantities(); // Then - assertThat(allSupportedQuantities).isNotNull().isNotEmpty().hasSize(53); + assertThat(allSupportedQuantities).isNotNull().isNotEmpty().hasSize(59); } @Test diff --git a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java index 79c7853..0a8d2d5 100644 --- a/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java +++ b/unitility-jackson/src/test/java/com/synerset/unitility/jackson/serialization/PhysicalQuantityJacksonDeserializerTest.java @@ -9,6 +9,7 @@ import com.synerset.unitility.unitsystem.common.*; import com.synerset.unitility.unitsystem.dimensionless.BypassFactor; import com.synerset.unitility.unitsystem.dimensionless.GenericDimensionless; +import com.synerset.unitility.unitsystem.electric.*; import com.synerset.unitility.unitsystem.flow.VolumetricFlow; import com.synerset.unitility.unitsystem.geographic.Bearing; import com.synerset.unitility.unitsystem.geographic.GeoCoordinate; @@ -24,7 +25,6 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatStream; class PhysicalQuantityJacksonDeserializerTest { @@ -53,7 +53,8 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing String bearing = "{\"value\":270.0}"; String distance = "1.0"; String curvatureInput1 = "{\"value\":0.1,\"unit\":\"°/ft\"}"; - String curvatureInput2 = "{\"value\":0.1,\"unit\":\"deg /ft\"}"; ; + String curvatureInput2 = "{\"value\":0.1,\"unit\":\"deg /ft\"}"; + ; String curvatureInput3 = "{\"value\":0.1,\"unit\":\"degpft\"}"; String angularVelInput1 = "{\"value\":0.1,\"unit\":\"deg p s\"}"; String angularVelInput2 = "{\"value\":0.1,\"unit\":\"rps\"}"; @@ -66,6 +67,12 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing String expectedSoundPower = "{\"value\": 10,\"unit\":\"db l \"}"; String expectedSoundPressure = "{\"value\": 10,\"unit\":\"db a \"}"; String expectedDataSize = "{\"value\": 10,\"unit\":\" mb \"}"; + String expectedCapacitance = "{\"value\": 10,\"unit\":\" Mf \"}"; + String expectedCharge = "{\"value\": 10,\"unit\":\" MC \"}"; + String expectedElResistance = "{\"value\": 10,\"unit\":\" Mohm \"}"; + String expectedCurrent = "{\"value\": 10,\"unit\":\" ua \"}"; + String expectedConductance = "{\"value\": 10,\"unit\":\" u s \"}"; + String expectedVoltage = "{\"value\": 10,\"unit\":\" Mv \"}"; // When Temperature actualTemp1 = objectMapper.readValue(tempInput1, Temperature.class); @@ -99,6 +106,12 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing SoundPower actualSoundPower = objectMapper.readValue(expectedSoundPower, SoundPower.class); SoundPressure actualSoundPressure = objectMapper.readValue(expectedSoundPressure, SoundPressure.class); DataSize actualDataSize = objectMapper.readValue(expectedDataSize, DataSize.class); + Capacitance actualCapacitance = objectMapper.readValue(expectedCapacitance, Capacitance.class); + Charge actualCharge = objectMapper.readValue(expectedCharge, Charge.class); + Resistance actualElResistance = objectMapper.readValue(expectedElResistance, Resistance.class); + Current actualCurrent = objectMapper.readValue(expectedCurrent, Current.class); + Conductance actualConductance = objectMapper.readValue(expectedConductance, Conductance.class); + Voltage actualVoltage = objectMapper.readValue(expectedVoltage, Voltage.class); // Then Temperature expetedTemperature = Temperature.ofCelsius(20); @@ -145,6 +158,13 @@ void deserialize_shouldDeserializeJsonToPhysicalQuantity() throws JsonProcessing assertThat(actualSoundPower).isEqualTo(SoundPower.ofDecibels(10)); assertThat(actualSoundPressure).isEqualTo(SoundPressure.ofDecibels(10)); assertThat(actualDataSize).isEqualTo(DataSize.ofMegabytes(10)); + assertThat(actualCapacitance).isEqualTo(Capacitance.ofMegafarads(10)); + assertThat(actualCharge).isEqualTo(Charge.ofMegacoulombs(10)); + assertThat(actualElResistance).isEqualTo(Resistance.ofMegaohms(10)); + assertThat(actualCurrent).isEqualTo(Current.ofMicroamperes(10)); + assertThat(actualConductance).isEqualTo(Conductance.ofMicroseimens(10)); + assertThat(actualVoltage).isEqualTo(Voltage.ofMegavolts(10)); + } @Test diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CapacitancePlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CapacitancePlainSIConverter.java new file mode 100644 index 0000000..3bf2ba5 --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CapacitancePlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.electric; + +import com.synerset.unitility.unitsystem.electric.Capacitance; +import com.synerset.unitility.unitsystem.electric.CapacitanceUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class CapacitancePlainSIConverter implements AttributeConverter { + + public static final CapacitanceUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Capacitance.class); + + @Override + public Double convertToDatabaseColumn(Capacitance attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Capacitance convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Capacitance.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ChargePlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ChargePlainSIConverter.java new file mode 100644 index 0000000..68651b1 --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ChargePlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.electric; + +import com.synerset.unitility.unitsystem.electric.Charge; +import com.synerset.unitility.unitsystem.electric.ChargeUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class ChargePlainSIConverter implements AttributeConverter { + + public static final ChargeUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Charge.class); + + @Override + public Double convertToDatabaseColumn(Charge attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Charge convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Charge.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ConductancePlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ConductancePlainSIConverter.java new file mode 100644 index 0000000..7163c2b --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ConductancePlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.electric; + +import com.synerset.unitility.unitsystem.electric.Conductance; +import com.synerset.unitility.unitsystem.electric.ConductanceUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class ConductancePlainSIConverter implements AttributeConverter { + + public static final ConductanceUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Conductance.class); + + @Override + public Double convertToDatabaseColumn(Conductance attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Conductance convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Conductance.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CurrentPlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CurrentPlainSIConverter.java new file mode 100644 index 0000000..8178715 --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/CurrentPlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.electric; + +import com.synerset.unitility.unitsystem.electric.Current; +import com.synerset.unitility.unitsystem.electric.CurrentUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class CurrentPlainSIConverter implements AttributeConverter { + + public static final CurrentUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Current.class); + + @Override + public Double convertToDatabaseColumn(Current attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Current convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Current.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ResistancePlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ResistancePlainSIConverter.java new file mode 100644 index 0000000..1cf3c0a --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/ResistancePlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.electric; + +import com.synerset.unitility.unitsystem.electric.Resistance; +import com.synerset.unitility.unitsystem.electric.ResistanceUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class ResistancePlainSIConverter implements AttributeConverter { + + public static final ResistanceUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Resistance.class); + + @Override + public Double convertToDatabaseColumn(Resistance attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Resistance convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Resistance.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file diff --git a/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/VoltagePlainSIConverter.java b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/VoltagePlainSIConverter.java new file mode 100644 index 0000000..149472b --- /dev/null +++ b/unitility-persistence/src/main/java/com/synerset/unitility/persistence/converter/plainsivalue/electric/VoltagePlainSIConverter.java @@ -0,0 +1,25 @@ +package com.synerset.unitility.persistence.converter.plainsivalue.electric; + +import com.synerset.unitility.unitsystem.electric.Voltage; +import com.synerset.unitility.unitsystem.electric.VoltageUnit; +import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class VoltagePlainSIConverter implements AttributeConverter { + + public static final VoltageUnit DEFAULT_SI_UNIT = PhysicalQuantityParsingFactory.getDefaultParsingFactory() + .getDefaultUnit(Voltage.class); + + @Override + public Double convertToDatabaseColumn(Voltage attribute) { + return attribute == null ? null : attribute.getInUnit(DEFAULT_SI_UNIT); + } + + @Override + public Voltage convertToEntityAttribute(Double dbData) { + return dbData == null ? null : Voltage.of(dbData, DEFAULT_SI_UNIT); + } + +} \ No newline at end of file From 92edf11ea4e56bf12cf2f13ea417aba62e9be8fe Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 11 Oct 2025 23:33:16 +0200 Subject: [PATCH 8/9] feat(SNSUNI-154): Change DMS formatting from relevantDigits to precision, ensure trailing zeros in minutes. --- .../geographic/DMSValueFormatter.java | 34 ++++++++----- .../unitsystem/geographic/GeoCoordinate.java | 12 ++--- .../unitsystem/geographic/Latitude.java | 10 ++-- .../unitsystem/geographic/Longitude.java | 10 ++-- .../unitsystem/util/ValueFormatter.java | 49 +++++++++++++++++++ .../geographic/DMSValueFormatterTest.java | 17 +++++++ .../geographic/GeoCoordinateTest.java | 12 ++--- .../unitsystem/geographic/LatitudeTest.java | 2 +- .../unitsystem/geographic/LongitudeTest.java | 6 +-- .../unitsystem/util/ValueFormatterTest.java | 38 +++++++++++++- 10 files changed, 152 insertions(+), 38 deletions(-) create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java index b2f6421..cc21128 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java @@ -4,24 +4,30 @@ class DMSValueFormatter { + public static final double DEFAULT_ICAO_SECONDS_PRECISION = 0.01; + private DMSValueFormatter() { throw new IllegalStateException("Utility class"); } - // DMS format helpers - static String latitudeToDmsFormat(Latitude latitude, int relevantDigits) { + static String latitudeToDmsFormat(Latitude latitude, double secondsPrecision) { double latitudeInDegrees = latitude.getInDegrees(); char directionSymbol = (latitudeInDegrees < 0) ? 'S' : 'N'; - return createDMSNotation(latitudeInDegrees, directionSymbol, relevantDigits, 2); + return createDMSNotation(latitudeInDegrees, directionSymbol, secondsPrecision, 2); } - static String longitudeToDmsFormat(Longitude longitude, int relevantDigits) { + static String longitudeToDmsFormat(Longitude longitude, double secondsPrecision) { double longitudeInDegrees = longitude.getInDegrees(); char directionSymbol = (longitudeInDegrees < 0) ? 'W' : 'E'; - return createDMSNotation(longitudeInDegrees, directionSymbol, relevantDigits, 3); + return createDMSNotation(longitudeInDegrees, directionSymbol, secondsPrecision, 3); } - private static String createDMSNotation(double coordinateInDegrees, char directionSymbol, int relevantDigits, int degreePadding) { + /** + * ICAO-compliant DMS notation builder. + * Latitude: DD°MM'SS.S"N + * Longitude: DDD°MM'SS.S"E + */ + private static String createDMSNotation(double coordinateInDegrees, char directionSymbol, double secondsPrecision, int degreePadding) { coordinateInDegrees = Math.abs(coordinateInDegrees); int degrees = (int) coordinateInDegrees; @@ -29,12 +35,18 @@ private static String createDMSNotation(double coordinateInDegrees, char directi int minutes = (int) minutesAndSeconds; double seconds = (minutesAndSeconds - minutes) * 60; - String secondsWithRelDigits = relevantDigits > 0 - ? ValueFormatter.toStringWithRelevantDigits(seconds, relevantDigits) - : String.format("%.2f", seconds); + // Defaulting negative relevantDigits to default ICAO precision + if (secondsPrecision < 0) { + secondsPrecision = DEFAULT_ICAO_SECONDS_PRECISION; + } + // Format seconds using relevant digits — ICAO needs at least 1 digit, but we allow flexible precision + String secondsWithRelDigits = ValueFormatter.toStringWithPrecision(seconds, secondsPrecision); + // Zero-pad degrees and minutes String degreesFormatted = String.format("%0" + degreePadding + "d", degrees); + String minutesFormatted = String.format("%02d", minutes); - return String.format("%s°%d'%s\"%c", degreesFormatted, minutes, secondsWithRelDigits, directionSymbol); + // ICAO format: DD°MM'SS.S"N (no spaces) + return String.format("%s°%s'%s\"%c", degreesFormatted, minutesFormatted, secondsWithRelDigits, directionSymbol); } -} \ No newline at end of file +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java index 883f838..cba01ef 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java @@ -69,11 +69,11 @@ public String toDMSFormat(String variableName) { * Returns the geographic coordinate in Degrees, Minutes, Seconds (DMS) format with a specified number of relevant digits. * Example, for relevant digits = 2: 52°14'5.12"N, -10°13'2.12"W. * - * @param relevantDigits The number of relevant digits to include in the output. + * @param secondsPrecision The precision of expected seconds to be provided as 'epsilon' eg: 0.01. * @return The geographic coordinate in DMS format with the specified number of relevant digits. */ - public String toDMSFormat(int relevantDigits) { - return latitude.toDMSFormat(relevantDigits) + ", " + longitude.toDMSFormat(relevantDigits); + public String toDMSFormat(double secondsPrecision) { + return latitude.toDMSFormat(secondsPrecision) + ", " + longitude.toDMSFormat(secondsPrecision); } /** @@ -81,11 +81,11 @@ public String toDMSFormat(int relevantDigits) { * Example, for relevant digits = 2: variable = 52°14'5.12"N, -10°13'2.12"W. * * @param variableName The variable name to be used in the output. - * @param relevantDigits The number of relevant digits to include in the output. + * @param secondsPrecision The precision of expected seconds to be provided as 'epsilon' eg: 0.01. * @return The geographic coordinate in DMS format with the specified variable name and number of relevant digits. */ - public String toDMSFormat(String variableName, int relevantDigits) { - return variableName + " = " + toDMSFormat(relevantDigits); + public String toDMSFormat(String variableName, double secondsPrecision) { + return variableName + " = " + toDMSFormat(secondsPrecision); } // Console output in decimal degrees format, Google Maps outputs coords this way\ diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java index c100bb7..ec16d9a 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java @@ -117,19 +117,19 @@ public double getInDegrees() { // Console output in DMS (degrees, minutes, seconds) format public String toDMSFormat() { - return DMSValueFormatter.latitudeToDmsFormat(this, -1); + return DMSValueFormatter.latitudeToDmsFormat(this, DMSValueFormatter.DEFAULT_ICAO_SECONDS_PRECISION); } public String toDMSFormat(String variableName) { return variableName + " = " + toDMSFormat(); } - public String toDMSFormat(int relevantDigits) { - return DMSValueFormatter.latitudeToDmsFormat(this, relevantDigits); + public String toDMSFormat(double secondsPrecision) { + return DMSValueFormatter.latitudeToDmsFormat(this, secondsPrecision); } - public String toDMSFormat(String variableName, int relevantDigits) { - return variableName + " = " + toDMSFormat(relevantDigits); + public String toDMSFormat(String variableName, double secondsPrecision) { + return variableName + " = " + toDMSFormat(secondsPrecision); } @Override diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java index 224b42f..563e39a 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java @@ -98,19 +98,19 @@ public Longitude withValue(double value) { // Console output in DMS (degrees, minutes, seconds) format public String toDMSFormat() { - return DMSValueFormatter.longitudeToDmsFormat(this, -1); + return DMSValueFormatter.longitudeToDmsFormat(this, DMSValueFormatter.DEFAULT_ICAO_SECONDS_PRECISION); } public String toDMSFormat(String variableName) { return variableName + " = " + toDMSFormat(); } - public String toDMSFormat(int relevantDigits) { - return DMSValueFormatter.longitudeToDmsFormat(this, relevantDigits); + public String toDMSFormat(double secondsPrecision) { + return DMSValueFormatter.longitudeToDmsFormat(this, secondsPrecision); } - public String toDMSFormat(String variableName, int relevantDigits) { - return variableName + " = " + toDMSFormat(relevantDigits); + public String toDMSFormat(String variableName, double secondsPrecision) { + return variableName + " = " + toDMSFormat(secondsPrecision); } // Convert to target unit diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueFormatter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueFormatter.java index abba202..8c10fee 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueFormatter.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueFormatter.java @@ -15,6 +15,51 @@ private ValueFormatter() { throw new IllegalStateException("Utility class"); } + /** + * Formats a double value to a string using a cutoff precision (epsilon). + * The value is rounded to the nearest multiple of the given precision using HALF_EVEN rounding mode. + *

+ * Example: + *

    + *
  • value = 0.01567, precision = 0.01 → "0.02"
  • + *
  • value = 0.014, precision = 0.01 → "0.01"
  • + *
  • value = 123.456, precision = 0.1 → "123.5"
  • + *
+ * + * @param value The double value to be formatted. + * @param precision The cutoff precision (epsilon), e.g. 0.01 for rounding to hundredths. + * @return A formatted string representation of the rounded value. + */ + public static String toStringWithPrecision(double value, double precision) { + if (precision <= 0) { + throw new IllegalArgumentException("Precision must be positive"); + } + + // Determine number of decimal places from precision (e.g. 0.001 -> 3) + int decimalPlaces = 0; + double tmp = precision; + while (tmp < 1) { + tmp *= 10; + decimalPlaces++; + } + + // Round using HALF_EVEN + double rounded = Math.round(value / precision) * precision; + + StringBuilder pattern = new StringBuilder("#"); + if (decimalPlaces > 0) { + pattern.append(".").append("#".repeat(decimalPlaces)); + } + + DecimalFormatSymbols symbols = new DecimalFormatSymbols(); + symbols.setDecimalSeparator('.'); + symbols.setGroupingSeparator(','); + + DecimalFormat df = new DecimalFormat(pattern.toString(), symbols); + df.setRoundingMode(RoundingMode.HALF_UP); + return df.format(rounded); + } + /** * Formats a double value to a string with the specified number of relevant digits and decimal places. * The method calculates the appropriate number of decimal places based on the given relevant digits and @@ -26,6 +71,10 @@ private ValueFormatter() { * @return A formatted string representation of the double value. */ public static String toStringWithRelevantDigits(double value, int relevantDigits) { + if(Math.abs(value) < 1 && relevantDigits <= 0) { + return "0"; + } + relevantDigits = Math.abs(relevantDigits); int doubleScale = (int) Math.log10(Math.abs(value)); if (doubleScale >= 0) { diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java new file mode 100644 index 0000000..fc55463 --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java @@ -0,0 +1,17 @@ +package com.synerset.unitility.unitsystem.geographic; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class DMSValueFormatterTest { + + @Test + void testConvertDMSValueToDMSValue() + { + Latitude latitude = Latitude.ofDegrees(52.1); + String string = latitude.toDMSFormat(-1); + + } + +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java index e6441e2..a21c659 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java @@ -11,19 +11,19 @@ class GeoCoordinateTest { @DisplayName("should output coordinates in DMS format") void toDMSFormat_shouldOutputInDegreeMinutesSecondsFormat() { // Given - Latitude latitude = Latitude.ofDegrees(-52.23411); - Longitude longitude = Longitude.ofDegrees(-21.56711); + Latitude latitude = Latitude.ofDegrees(-0.023411888); + Longitude longitude = Longitude.ofDegrees(-1.56711888); // When GeoCoordinate geoCoordinate = GeoCoordinate.of(latitude, longitude, "name"); String actualDmsOutput = geoCoordinate.toDMSFormat(); String actualDmsOutputVar = geoCoordinate.toDMSFormat("sea_quest"); - String actualDmsOutputVarTruncated = geoCoordinate.toDMSFormat("sea_quest", 3); + String actualDmsOutputVarTruncated = geoCoordinate.toDMSFormat("sea_quest", 0.001); // Then - assertThat(actualDmsOutput).isEqualTo("52°14'2.80\"S, 021°34'1.60\"W"); - assertThat(actualDmsOutputVar).isEqualTo("sea_quest = 52°14'2.80\"S, 021°34'1.60\"W"); - assertThat(actualDmsOutputVarTruncated).isEqualTo("sea_quest = 52°14'2.796\"S, 021°34'1.596\"W"); + assertThat(actualDmsOutput).isEqualTo("00°01'24.28\"S, 001°34'1.63\"W"); + assertThat(actualDmsOutputVar).isEqualTo("sea_quest = 00°01'24.28\"S, 001°34'1.63\"W"); + assertThat(actualDmsOutputVarTruncated).isEqualTo("sea_quest = 00°01'24.283\"S, 001°34'1.628\"W"); assertThat(geoCoordinate.name()).isEqualTo("name"); assertThat(geoCoordinate.latitude()).isEqualTo(latitude); diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java index f9c4e06..6e1bba2 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java @@ -68,7 +68,7 @@ void toDmsFormat_shouldOutputValidDMSFormat() { // When String latInDms = latitude.toDMSFormat(); String latInDmsVar = latitude.toDMSFormat("lat"); - String latInDmsVarDigits = latitude.toDMSFormat("lat", 3); + String latInDmsVarDigits = latitude.toDMSFormat("lat", 0.001); // Then assertThat(latInDms).isEqualTo("52°14'5.12\"N"); diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java index d838772..0f489fe 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java @@ -69,9 +69,9 @@ void toDmsFormat_shouldOutputValidDMSFormat() { String lonInDmsVarDigits = longitude.toDMSFormat("lat", 1); // Then - assertThat(lonInDms).isEqualTo("021°4'3.99\"W"); - assertThat(lonInDmsVar).isEqualTo("lat = 021°4'3.99\"W"); - assertThat(lonInDmsVarDigits).isEqualTo("lat = 021°4'4\"W"); + assertThat(lonInDms).isEqualTo("021°04'3.99\"W"); + assertThat(lonInDmsVar).isEqualTo("lat = 021°04'3.99\"W"); + assertThat(lonInDmsVarDigits).isEqualTo("lat = 021°04'4\"W"); } } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java index ba31bf6..41b00ba 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java @@ -1,6 +1,7 @@ package com.synerset.unitility.unitsystem.util; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -45,7 +46,10 @@ static Stream lowRelDigitsSeed() { Arguments.of(10.6, 0, "11"), Arguments.of(10.1234, 1, "10.1"), Arguments.of(10.1234, 2, "10.12"), - Arguments.of(10.1234, 3, "10.123") + Arguments.of(10.1234, 3, "10.123"), + Arguments.of(0.0011234, 1, "0.001"), + Arguments.of(0.0011234, 0, "0"), + Arguments.of(1E-12, 0, "0") ); } @@ -61,4 +65,36 @@ void shouldFormatAndTruncateDoubleInput_whenRelevantDigitsAreLowerThan3(double i assertThat(actualFormattedDoubleAsString).isEqualTo(expectedFormattedDoubleAsString); } + static Stream precisionRoundingSeed() { + return Stream.of( + // < value , precision , expected > + Arguments.of(0.01567, 0.01, "0.02"), + Arguments.of(0.014, 0.01, "0.01"), + Arguments.of(0.015, 0.01, "0.02"), + Arguments.of(0.025, 0.01, "0.03"), + Arguments.of(123.456, 0.1, "123.5"), + Arguments.of(123.44, 0.1, "123.4"), + Arguments.of(999.99, 1, "1000"), + Arguments.of(999.99, 0.01, "999.99"), + Arguments.of(0.00044, 0.0001, "0.0004"), + Arguments.of(0.00046, 0.0001, "0.0005"), + Arguments.of(-0.01567, 0.01, "-0.02"), + Arguments.of(-123.456, 0.1, "-123.5"), + Arguments.of(10, 0.1, "10"), + Arguments.of(10, 1, "10"), + Arguments.of(10, 0.01, "10") + ); + } + + @ParameterizedTest + @MethodSource("precisionRoundingSeed") + @DisplayName("should properly round value with respect to given precision (epsilon)") + void shouldProperlyRoundValueWithGivenPrecision(double inputValue, double precision, String expectedValue) { + // When + String actualValue = ValueFormatter.toStringWithPrecision(inputValue, precision); + + // Then + assertThat(actualValue).isEqualTo(expectedValue); + } + } \ No newline at end of file From 410fb5583ebd208816691e63764ce0500ee2b0de Mon Sep 17 00:00:00 2001 From: Piotr Jazdzyk Date: Sat, 18 Oct 2025 15:14:32 +0200 Subject: [PATCH 9/9] feat(SNSUNI-155): BREAKING CHANGE: Ensure full formatting coordinate compliance with ICAO DMS_s format with 0.01 digit resolution, add static ofDMS() format static factories for easier Geo quantities creation. --- README.md | 52 ++++-- .../unitsystem/acoustic/SoundPower.java | 3 - .../unitsystem/acoustic/SoundPressure.java | 1 - .../geographic/DMSCoordinateFormatter.java | 140 ++++++++++++++ .../unitsystem/geographic/DMSValidator.java | 20 -- .../geographic/DMSValueFormatter.java | 52 ------ .../unitsystem/geographic/GeoCoordinate.java | 175 +++++++++--------- .../geographic/GeoParsingHelpers.java | 84 +++++++++ .../unitsystem/geographic/Latitude.java | 129 +++++++++++-- .../unitsystem/geographic/Longitude.java | 126 +++++++++++-- .../unitsystem/util/ParsingHelpers.java | 38 ---- ...hysicalQuantityAbstractParsingFactory.java | 40 ++-- .../unitsystem/util/ValueSymbolPair.java | 3 + .../DMSCoordinateFormatterTest.java | 116 ++++++++++++ .../geographic/DMSValidatorTest.java | 73 +++++++- .../geographic/DMSValueFormatterTest.java | 17 -- .../geographic/GeoCoordinateTest.java | 35 ++-- .../unitsystem/geographic/LatitudeTest.java | 57 +++--- .../unitsystem/geographic/LongitudeTest.java | 58 ++++-- .../PhysicalQuantityParsingFactoryTest.java | 10 +- .../unitsystem/util/ValueFormatterTest.java | 1 - 21 files changed, 879 insertions(+), 351 deletions(-) create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatter.java delete mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValidator.java delete mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoParsingHelpers.java create mode 100644 unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueSymbolPair.java create mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatterTest.java delete mode 100644 unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java diff --git a/README.md b/README.md index dfb8259..49ec001 100644 --- a/README.md +++ b/README.md @@ -951,39 +951,57 @@ Longitude, GeoCoordinate and GeoDistance. These classes allow representing coord distance between these coordinates. ### 8.1 Geographic Latitude, Longitude and GeoCoordinate -The **Latitude** class includes methods that allow for easy conversion to the Degrees-Minutes-Seconds -(DMS) format. This format provides a more popular representation of geographic coordinates, making it convenient -for various applications where DMS notation is preferred. Latitude range is: -90 to 90 degrees.
-The **Longitude** class, analogous to the Latitude class, represents a geographic longitude coordinate. It adheres to the -standard range of -180 to +180 degrees, covering the westernmost point at -180 degrees and the easternmost point -at +180 degrees. +The **Latitude** class includes methods that allow for easy conversion to the ICAO Annex 15 compliant Degrees-Minutes-Seconds +(DMS) format with a default resolution of 0.01 seconds. This format provides a standardized representation of geographic +coordinates, making it convenient for aviation and other applications where ICAO-compliant DMS notation is required. +Latitude range is: -90 to 90 degrees.
+The **Longitude** class, analogous to the Latitude class, represents a geographic longitude coordinate. It adheres to the +standard range of -180 to +180 degrees, covering the westernmost point at -180 degrees and the easternmost point +at +180 degrees. Like Latitude, it supports ICAO Annex 15 compliant DMS formatting with configurable resolution. + ```java // Latitude and Longitude types are based on Angular units Latitude latitude = Latitude.ofDegrees(-20.123); Longitude longitude = Longitude.ofDegrees(20.123); -// Both can be reduced to a string in DMS format or in ENG format: -String latInDMS = latitude.toDMSFormat(2); // Outputs: 20°7'22.8"S + +// ICAO Annex 15 compliant DMS formatting with 0.01 seconds resolution +String latInDMS = latitude.toDMSFormat(); // Outputs: 20°07'22.80"S +String lonInDMS = longitude.toDMSFormat(); // Outputs: 020°07'22.80"E + +// Custom resolution DMS formatting +String latInDMSHighPrec = latitude.toDMSFormat(0.001); // Outputs: 20°07'22.800"S +String lonInDMSLowPrec = longitude.toDMSFormat(0.1); // Outputs: 020°07'22.8"E + +// Engineering format String latInENG = latitude.toEngineeringFormat(); // Outputs: -20.123 [°] ``` You can also create Latitude or Longitude instance providing degrees, minutes and seconds: ```java // Instance from degrees, minutes, seconds -Latitude latFromDMS = Latitude.ofDegMinSec(20, 7, 22.8, PrimaryDirection.SOUTH); // Latitude{-20.123°} -Longitude longFromDMS = Longitude.ofDegMinSec(20, 7, 22.8, PrimaryDirection.EAST); // Longitude{20.123°} +Latitude latFromDMS = Latitude.ofDegMinSec(20, 7, 22.8); // Latitude{-20.123°} +Longitude longFromDMS = Longitude.ofDegMinSec(20, 7, 22.8); // Longitude{20.123°} + +// From ICAO DMS string format +Latitude latFromICAO = Latitude.ofDMSFormat("20°07'22.80\"S"); +Longitude lonFromICAO = Longitude.ofDMSFormat("020°07'22.80\"E"); ``` -The **GeoCoordinate** class combines both Latitude and Longitude to form a complete geographic coordinate. It facilitates +The **GeoCoordinate** class combines both Latitude and Longitude to form a complete geographic coordinate. It facilitates easy management and manipulation of spatial data, allowing seamless integration into various applications requiring -precise location information. +precise location information. All DMS outputs are ICAO Annex 15 compliant by default. ```java // GeoCoordinate class represents a coordinate of specific point in the globe, using Latitude and Longitude and optional name GeoCoordinate coordinateExample = GeoCoordinate.of(latitude, longitude, "my location"); -// GeoCoordinate can be reduced to DMS format, ENG format, or decimal degrees format -// Decimal degrees format with coma separating latitude from longitude is for ie: how Google Maps output cords -String geoCoordDMS = coordinateExample.toDMSFormat(); // 20°7'22.8"S, 20°7'22.8"E -String geoCoordEND = coordinateExample.toEngineeringFormat(); // -20.12 [°], 20.12 [°] -String geoCoordDEC = coordinateExample.toDecimalDegrees(); // -20.12, 20.12 + +// GeoCoordinate can be reduced to ICAO DMS format, ENG format, or decimal degrees format +String geoCoordDMS = coordinateExample.toDMSFormat(); // 20°07'22.80"S, 020°07'22.80"E +String geoCoordDMSHighPrec = coordinateExample.toDMSFormat(0.001); // 20°07'22.800"S, 020°07'22.800"E +String geoCoordEND = coordinateExample.toEngineeringFormat(); // -20.123 [°], 20.123 [°] +String geoCoordDEC = coordinateExample.toDecimalDegrees(); // -20.123, 20.123 + +// Create from ICAO DMS strings +GeoCoordinate icaoCoord = GeoCoordinate.ofDMSFormat("20°07'22.80\"S", "020°07'22.80\"E"); ``` Latitude and Longitude do not enforce any angular value limit, but GeoCoordinate will do. Make sure that your diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java index 98073d0..20b7b4f 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPower.java @@ -1,9 +1,6 @@ package com.synerset.unitility.unitsystem.acoustic; import com.synerset.unitility.unitsystem.CalculableQuantity; -import com.synerset.unitility.unitsystem.oscillation.Frequency; -import com.synerset.unitility.unitsystem.oscillation.FrequencyUnit; -import com.synerset.unitility.unitsystem.oscillation.FrequencyUnits; import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; import java.util.Objects; diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java index b4bc014..fd9931f 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/acoustic/SoundPressure.java @@ -1,7 +1,6 @@ package com.synerset.unitility.unitsystem.acoustic; import com.synerset.unitility.unitsystem.CalculableQuantity; -import com.synerset.unitility.unitsystem.thermodynamic.PowerUnit; import com.synerset.unitility.unitsystem.thermodynamic.PressureUnit; import java.util.Objects; diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatter.java new file mode 100644 index 0000000..9c81701 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatter.java @@ -0,0 +1,140 @@ +package com.synerset.unitility.unitsystem.geographic; + +/** + * Utility class for formatting latitude and longitude values + * into ICAO-compliant DMS (Degrees–Minutes–Seconds) strings. + * + *

+ * According to ICAO Doc 9674, Chapter 2.5.1: + *

+ * “Resolution of positional data is the smallest separation that can be represented + * by the method employed to make the positional statement. + * Care must be taken that the resolution does not affect accuracy; + * the resolution is always a rounded value as opposed to a truncated value.” + *
+ *

+ */ +class DMSCoordinateFormatter { + + /** + * Default ICAO Annex 15 resolution for seconds (0.01). + */ + public static final double DEFAULT_ICAO_SECONDS_RESOLUTION = 0.01; + + private DMSCoordinateFormatter() { + throw new IllegalStateException("Utility class"); + } + + /** + * Formats latitude to ICAO DMS string (DD°MM'SS.ss"N/S) using default 0.01 resolution. + * + * @param latitude the latitude object + * @return ICAO-compliant DMS string + */ + static String latitudeToDMSSFormat(Latitude latitude) { + return latitudeToDMSSFormat(latitude, DEFAULT_ICAO_SECONDS_RESOLUTION); + } + + /** + * Formats latitude to ICAO DMS string (DD°MM'SS.s..."N/S) using custom seconds resolution. + * + * @param latitude the latitude object + * @param secondsResolution the desired seconds resolution (e.g., 0.01, 0.1, 1.0, or 0 for full seconds) + * @return ICAO-compliant DMS string + */ + static String latitudeToDMSSFormat(Latitude latitude, double secondsResolution) { + double latitudeInDegrees = latitude.getInDegrees(); + char directionSymbol = (latitudeInDegrees < 0) ? 'S' : 'N'; + return createDMSSNotation(latitudeInDegrees, directionSymbol, secondsResolution, 2); + } + + /** + * Formats longitude to ICAO DMS string (DDD°MM'SS.ss"E/W) using default 0.01 resolution. + * + * @param longitude the longitude object + * @return ICAO-compliant DMS string + */ + static String longitudeToDMSSFormat(Longitude longitude) { + return longitudeToDMSSFormat(longitude, DEFAULT_ICAO_SECONDS_RESOLUTION); + } + + /** + * Formats longitude to ICAO DMS string (DDD°MM'SS.s..."E/W) using custom seconds resolution. + * + * @param longitude the longitude object + * @param secondsResolution the desired seconds resolution (e.g., 0.01, 0.1, 1.0, or 0 for full seconds) + * @return ICAO-compliant DMS string + */ + static String longitudeToDMSSFormat(Longitude longitude, double secondsResolution) { + double longitudeInDegrees = longitude.getInDegrees(); + char directionSymbol = (longitudeInDegrees < 0) ? 'W' : 'E'; + return createDMSSNotation(longitudeInDegrees, directionSymbol, secondsResolution, 3); + } + + /** + * ICAO-compliant DMS notation builder. + * + *

This method rounds seconds to the specified resolution, + * in accordance with ICAO Doc 9674 §2.5.1. + *

+ * + * @param coordinateInDegrees the coordinate value in decimal degrees + * @param directionSymbol direction letter (N/S/E/W) + * @param secondsResolution the desired seconds resolution + * @param degreePadding number of digits for degrees (2 for latitude, 3 for longitude) + * @return formatted DMS string (e.g., "35°05'35.13\"N") + */ + private static String createDMSSNotation(double coordinateInDegrees, char directionSymbol, double secondsResolution, int degreePadding) { + + // Validate resolution input + if (secondsResolution < 0) { + secondsResolution = DEFAULT_ICAO_SECONDS_RESOLUTION; + } + + double absCoordinate = Math.abs(coordinateInDegrees); + int degrees = (int) absCoordinate; + double minutesAndSeconds = (absCoordinate - degrees) * 60; + int minutes = (int) minutesAndSeconds; + double seconds = (minutesAndSeconds - minutes) * 60; + + // Determine multiplier and decimal places + double multiplier; + int decimalPlaces; + + if (secondsResolution == 0) { + multiplier = 1.0; + decimalPlaces = 0; + } else { + multiplier = 1.0 / secondsResolution; + decimalPlaces = Math.max(0, (int) Math.round(Math.log10(multiplier))); + } + + // Round seconds to the specified resolution (ICAO-compliant) + double roundedSeconds = Math.round(seconds * multiplier) / multiplier; + + // Handle rounding overflow (e.g. 59.9999 -> 60.00) + if (roundedSeconds >= 60.0) { + roundedSeconds = 0.0; + minutes++; + if (minutes >= 60) { + minutes = 0; + degrees++; + } + } + + // Format seconds + int integerPartWidth = 2; + int decimalPointWidth = (decimalPlaces > 0) ? 1 : 0; + int totalWidth = integerPartWidth + decimalPointWidth + decimalPlaces; + String secondsFormatString = String.format("%%0%d.%df", totalWidth, decimalPlaces); + + // Format all parts + String degreesFormatted = String.format("%0" + degreePadding + "d", degrees); + String minutesFormatted = String.format("%02d", minutes); + String secondsFormatted = String.format(secondsFormatString, roundedSeconds); + + // ICAO DMS output (no spaces) + return String.format("%s°%s'%s\"%c", degreesFormatted, minutesFormatted, secondsFormatted, directionSymbol); + } + +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValidator.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValidator.java deleted file mode 100644 index daee914..0000000 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValidator.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.synerset.unitility.unitsystem.geographic; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class DMSValidator { - - private DMSValidator() { - throw new IllegalStateException("Utility class"); - } - - private static final String DMS_PATTERN = "^-?\\d{1,3}(o|°|deg)(\\d{1,2}('|min|m)((\\d{1,2}(\\.\\d+)?)(\"|sec|s))?)?([NnSsWwEe])?$"; - - public static boolean isValidDMSFormat(String input) { - Pattern regex = Pattern.compile(DMS_PATTERN); - Matcher matcher = regex.matcher(input); - return matcher.matches(); - } - -} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java deleted file mode 100644 index cc21128..0000000 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatter.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.synerset.unitility.unitsystem.geographic; - -import com.synerset.unitility.unitsystem.util.ValueFormatter; - -class DMSValueFormatter { - - public static final double DEFAULT_ICAO_SECONDS_PRECISION = 0.01; - - private DMSValueFormatter() { - throw new IllegalStateException("Utility class"); - } - - static String latitudeToDmsFormat(Latitude latitude, double secondsPrecision) { - double latitudeInDegrees = latitude.getInDegrees(); - char directionSymbol = (latitudeInDegrees < 0) ? 'S' : 'N'; - return createDMSNotation(latitudeInDegrees, directionSymbol, secondsPrecision, 2); - } - - static String longitudeToDmsFormat(Longitude longitude, double secondsPrecision) { - double longitudeInDegrees = longitude.getInDegrees(); - char directionSymbol = (longitudeInDegrees < 0) ? 'W' : 'E'; - return createDMSNotation(longitudeInDegrees, directionSymbol, secondsPrecision, 3); - } - - /** - * ICAO-compliant DMS notation builder. - * Latitude: DD°MM'SS.S"N - * Longitude: DDD°MM'SS.S"E - */ - private static String createDMSNotation(double coordinateInDegrees, char directionSymbol, double secondsPrecision, int degreePadding) { - coordinateInDegrees = Math.abs(coordinateInDegrees); - - int degrees = (int) coordinateInDegrees; - double minutesAndSeconds = (coordinateInDegrees - degrees) * 60; - int minutes = (int) minutesAndSeconds; - double seconds = (minutesAndSeconds - minutes) * 60; - - // Defaulting negative relevantDigits to default ICAO precision - if (secondsPrecision < 0) { - secondsPrecision = DEFAULT_ICAO_SECONDS_PRECISION; - } - // Format seconds using relevant digits — ICAO needs at least 1 digit, but we allow flexible precision - String secondsWithRelDigits = ValueFormatter.toStringWithPrecision(seconds, secondsPrecision); - - // Zero-pad degrees and minutes - String degreesFormatted = String.format("%0" + degreePadding + "d", degrees); - String minutesFormatted = String.format("%02d", minutes); - - // ICAO format: DD°MM'SS.S"N (no spaces) - return String.format("%s°%s'%s\"%c", degreesFormatted, minutesFormatted, secondsWithRelDigits, directionSymbol); - } -} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java index cba01ef..0e08c5f 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinate.java @@ -7,28 +7,23 @@ /** * Represents a geographic Earth coordinate consisting of latitude, longitude, and an optional name. - * This class provides various methods for formatting and manipulating geographic coordinates on Earth. + * Provides methods for formatting and manipulating geographic coordinates in both + * decimal and ICAO-compliant DMSS (Degrees–Minutes–Seconds with rounded seconds) formats. * - *

The latitude and longitude values are strictly enforced within Earth's valid latitude and longitude ranges. - * Latitude values range from -90.0 degrees (South) to 90.0 degrees (North), and longitude values range from - * -180.0 degrees (West) to 180.0 degrees (East). Attempting to create a coordinate with latitude or longitude values - * outside these Earth-specific limits will result in an {@link UnitSystemArgumentException} being thrown. + *

Latitude and longitude are validated against the Earth’s valid ranges: + * latitude ∈ [-90°, +90°], longitude ∈ [-180°, +180°]. + * Values outside these limits throw a {@link UnitSystemArgumentException}. * - * @param latitude The {@link Latitude} of the geographic coordinate. - * @param longitude The {@link Longitude} of the geographic coordinate. - * @param name An optional name associated with the geographic coordinate. + *

Note: The DMSS format implemented here follows + * ICAO Doc 9674 (WGS-84 Manual), § 2.5.1 — + * the resolution is always a rounded value as opposed to a truncated value. + * + * @param latitude The {@link Latitude} of this coordinate. + * @param longitude The {@link Longitude} of this coordinate. + * @param name Optional descriptive name for this coordinate. */ public record GeoCoordinate(Latitude latitude, Longitude longitude, String name) { - /** - * Constructs a GeoCoordinate object with the given latitude, longitude, and name. - * Validates the latitude and longitude values. - * - * @param latitude The {@link Latitude} of the geographic coordinate. - * @param longitude The {@link Longitude} of the geographic coordinate. - * @param name An optional name associated with the geographic coordinate. - * @throws UnitSystemArgumentException If the latitude or longitude is outside the valid range. - */ public GeoCoordinate { validateLatitude(latitude); validateLongitude(longitude); @@ -42,101 +37,105 @@ public static GeoCoordinate of(Latitude latitude, Longitude longitude) { return new GeoCoordinate(latitude, longitude, null); } - // Console output in DMS (degrees, minutes, seconds) format + public static GeoCoordinate ofDMSFormat(String latitudeDMS, String longitudeDMS, String name) { + Latitude latitude = Latitude.ofDMSFormat(latitudeDMS); + Longitude longitude = Longitude.ofDMSFormat(longitudeDMS); + return new GeoCoordinate(latitude, longitude, name); + } + public static GeoCoordinate ofDMSFormat(String latitudeDMS, String longitudeDMS) { + Latitude latitude = Latitude.ofDMSFormat(latitudeDMS); + Longitude longitude = Longitude.ofDMSFormat(longitudeDMS); + return new GeoCoordinate(latitude, longitude, null); + } + + // ICAO DMS_S FORMAT (Degrees–Minutes–Seconds with defined seconds resolution) /** - * Returns the geographic coordinate in Degrees, Minutes, Seconds (DMS) format. - * Example: 52°14'5.12345"N, -10°13'2.12345"W + * Returns the coordinate in ICAO-compliant DMSS format + * (Degrees–Minutes–Seconds with rounded seconds), using the default ICAO seconds resolution 0.01″. + * + *

Example: + *

+     * 52°14'05.12"N, 010°13'02.12"W
+     * 
* - * @return The geographic coordinate in DMS format (latitude, longitude). + * @return Coordinate in ICAO DMSS format (lat, lon). */ - public String toDMSFormat() { - return latitude.toDMSFormat() + ", " + longitude.toDMSFormat(); + public String toDMSsFormat() { + return latitude.toDMSsFormat() + ", " + longitude.toDMSsFormat(); } /** - * Returns the geographic coordinate in Degrees, Minutes, Seconds (DMS) format with a custom variable name. - * Example: variable = 52°14'5.12345"N, -10°13'2.12345"W + * Returns the coordinate in ICAO DMSS format with a variable label. + *

Example: + *

+     * pointA = 52°14'05.12"N, 010°13'02.12"W
+     * 
* - * @param variableName The variable name to be used in the output. - * @return The geographic coordinate in DMS format with the specified variable name. + * @param variableName Label to prepend. + * @return Labeled coordinate in ICAO DMSS format. */ - public String toDMSFormat(String variableName) { - return variableName + " = " + toDMSFormat(); + public String toDMSsFormat(String variableName) { + return variableName + " = " + toDMSsFormat(); } /** - * Returns the geographic coordinate in Degrees, Minutes, Seconds (DMS) format with a specified number of relevant digits. - * Example, for relevant digits = 2: 52°14'5.12"N, -10°13'2.12"W. + * Returns the coordinate in ICAO DMSS format with a custom seconds resolution. * - * @param secondsPrecision The precision of expected seconds to be provided as 'epsilon' eg: 0.01. - * @return The geographic coordinate in DMS format with the specified number of relevant digits. + *

Resolution defines the smallest step between consecutive second values (e.g. 0.01, 0.1, 1.0). + * Seconds are rounded, not truncated, per ICAO Doc 9674 § 2.5.1. + * + *

Examples:

+ *
    + *
  • secondsResolution = 0.01 → 52°14'05.12"N, 010°13'02.12"W
  • + *
  • secondsResolution = 1.0 → 52°14'05"N, 010°13'02"W
  • + *
+ * + * @param secondsResolution Seconds resolution (> 0 → custom; ≤ 0 → defaults to 0.01). + * @return Coordinate in ICAO DMSS format with specified resolution. */ - public String toDMSFormat(double secondsPrecision) { - return latitude.toDMSFormat(secondsPrecision) + ", " + longitude.toDMSFormat(secondsPrecision); + public String toDMSsFormat(double secondsResolution) { + return latitude.toDMSsFormat(secondsResolution) + ", " + longitude.toDMSsFormat(secondsResolution); } /** - * Returns the geographic coordinate in Degrees, Minutes, Seconds (DMS) format with a custom variable name and a specified number of relevant digits. - * Example, for relevant digits = 2: variable = 52°14'5.12"N, -10°13'2.12"W. + * Returns the coordinate in ICAO DMSS format with both label and seconds resolution. + *

Example: + *

+     * pointA = 52°14'05.12"N, 010°13'02.12"W
+     * 
* - * @param variableName The variable name to be used in the output. - * @param secondsPrecision The precision of expected seconds to be provided as 'epsilon' eg: 0.01. - * @return The geographic coordinate in DMS format with the specified variable name and number of relevant digits. + * @param variableName Label to prepend. + * @param secondsResolution Seconds resolution (e.g. 0.01, 0.1, 1.0). + * @return Labeled coordinate in ICAO DMSS format. */ - public String toDMSFormat(String variableName, double secondsPrecision) { - return variableName + " = " + toDMSFormat(secondsPrecision); + public String toDMSsFormat(String variableName, double secondsResolution) { + return variableName + " = " + toDMSsFormat(secondsResolution); } - // Console output in decimal degrees format, Google Maps outputs coords this way\ - + // DECIMAL DEGREES FORMAT (e.g. Google Maps) /** - * Returns the geographic coordinate in decimal degrees format. This is how Google outputs coordinates, - * when you click on some location on the map. - * Example: 52.12345, -10.12345 - * - * @return The geographic coordinate in decimal degrees format (latitude, longitude). + * Returns the coordinate in decimal-degrees format. + * Example: {@code 52.12345, -10.12345} */ public String toDecimalDegrees() { return latitude.getInDegrees() + ", " + longitude.getInDegrees(); } - /** - * Returns the geographic coordinate in decimal degrees format with a custom variable name. - * Example: variable = 52.12345, -10.12345 - * - * @param variableName The variable name to be used in the output. - * @return The geographic coordinate in decimal degrees format with the specified variable name. - */ public String toDecimalDegrees(String variableName) { return variableName + " = " + toDecimalDegrees(); } - /** - * Returns the geographic coordinate in decimal degrees format with a specified number of relevant digits. - * Example, for two relevant digits: 52.12, -10.12 - * - * @param relevantDigits The number of relevant digits to include in the output. - * @return The geographic coordinate in decimal degrees format with the specified number of relevant digits. - */ public String toDecimalDegrees(int relevantDigits) { return ValueFormatter.toStringWithRelevantDigits(latitude.getInDegrees(), relevantDigits) + ", " + ValueFormatter.toStringWithRelevantDigits(longitude.getInDegrees(), relevantDigits); } - /** - * Returns the geographic coordinate in decimal degrees format with a custom variable name and a specified number of relevant digits. - * Example, for two relevant digits: variable = 52.12, -10.12 - * - * @param variableName The variable name to be used in the output. - * @param relevantDigits The number of relevant digits to include in the output. - * @return The geographic coordinate in decimal degrees format with the specified variable name and number of relevant digits. - */ public String toDecimalDegrees(String variableName, int relevantDigits) { return variableName + " = " + toDecimalDegrees(relevantDigits); } - // Console output in engineering format + // ENGINEERING FORMAT public String toEngineeringFormat() { return latitude.toEngineeringFormat() + ", " + longitude.toEngineeringFormat(); } @@ -153,35 +152,36 @@ public String toEngineeringFormat(String variableName, int relevantDigits) { return variableName + " = " + toEngineeringFormat(relevantDigits); } + // VALIDATION / EQUALITY private void validateLatitude(Latitude latitude) { - if (latitude.isGreaterThan(Latitude.MAX_EARTH_LATITUDE) || latitude.isLowerThan(Latitude.MIN_EARTH_LATITUDE)) { - throw new UnitSystemArgumentException("Invalid latitude value = " + latitude + " Allowed range: " - + Latitude.MIN_EARTH_LATITUDE.toEngineeringFormat() + " to " + Latitude.MAX_EARTH_LATITUDE.toEngineeringFormat()); + if (latitude.isGreaterThan(Latitude.MAX_EARTH_LATITUDE) || + latitude.isLowerThan(Latitude.MIN_EARTH_LATITUDE)) { + throw new UnitSystemArgumentException("Invalid latitude value = " + latitude + + ". Allowed range: " + Latitude.MIN_EARTH_LATITUDE.toEngineeringFormat() + + " to " + Latitude.MAX_EARTH_LATITUDE.toEngineeringFormat()); } } private void validateLongitude(Longitude longitude) { - if (longitude.isGreaterThan(Longitude.MAX_EARTH_LONGITUDE) || longitude.isLowerThan(Longitude.MIN_EARTH_LONGITUDE)) { - throw new UnitSystemArgumentException("Invalid longitude value = " + longitude + ". Allowed range: " - + Longitude.MIN_EARTH_LONGITUDE.toEngineeringFormat() + " to " + Longitude.MAX_EARTH_LONGITUDE.toEngineeringFormat()); + if (longitude.isGreaterThan(Longitude.MAX_EARTH_LONGITUDE) || + longitude.isLowerThan(Longitude.MIN_EARTH_LONGITUDE)) { + throw new UnitSystemArgumentException("Invalid longitude value = " + longitude + + ". Allowed range: " + Longitude.MIN_EARTH_LONGITUDE.toEngineeringFormat() + + " to " + Longitude.MAX_EARTH_LONGITUDE.toEngineeringFormat()); } } public boolean equalsWithPrecision(GeoCoordinate inputGeoCoordinate, double epsilon) { - if (this == inputGeoCoordinate) { - return true; - } - if (inputGeoCoordinate == null) { - return false; - } + if (this == inputGeoCoordinate) return true; + if (inputGeoCoordinate == null) return false; return latitude.isEqualWithPrecision(inputGeoCoordinate.latitude, epsilon) && longitude.isEqualWithPrecision(inputGeoCoordinate.longitude, epsilon); } /** - * Converts the GeoCoordinate to its base unit representation (decimal degrees). + * Converts this GeoCoordinate to its base-unit representation (decimal degrees). * - * @return A new GeoCoordinate instance with latitude and longitude converted to base units. + * @return New {@link GeoCoordinate} in decimal degrees. */ public GeoCoordinate toBaseUnit() { return GeoCoordinate.of(latitude.toBaseUnit(), longitude.toBaseUnit()); @@ -201,5 +201,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(latitude.toBaseUnit(), longitude.toBaseUnit(), name); } - -} \ No newline at end of file +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoParsingHelpers.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoParsingHelpers.java new file mode 100644 index 0000000..72e1443 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/GeoParsingHelpers.java @@ -0,0 +1,84 @@ +package com.synerset.unitility.unitsystem.geographic; + +import com.synerset.unitility.unitsystem.common.AngleUnits; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemArgumentException; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; +import com.synerset.unitility.unitsystem.util.ParsingHelpers; +import com.synerset.unitility.unitsystem.util.StringTransformer; +import com.synerset.unitility.unitsystem.util.ValueSymbolPair; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GeoParsingHelpers { + + private static final String DMS_FLEXIBLE_PATTERN = + "^" + + "-?\\d{1,3}(?:°|o|deg)" + // degrees + "(?:" + + "\\d{1,2}(?:'|min|m)" + // optional minutes + "(?:" + + "\\d{1,2}(?:\\.\\d+)?(?:\"|sec|s)?" + // optional seconds with any decimal precision + ")?" + + ")?" + + "[NSEWnsew]?$"; // optional direction + + public static boolean isDMSFormatOrSimilar(String stringCoordinate) { + Pattern regex = Pattern.compile(DMS_FLEXIBLE_PATTERN); + Matcher matcher = regex.matcher(stringCoordinate); + return matcher.matches(); + } + + public static ValueSymbolPair extractValueAndSymbolFromDMSFormat(Class targetClass, String partiallyPreparedInput) { + String preparedInput = StringTransformer.of(partiallyPreparedInput) + .unifyDMSNotationSymbols() + .toString(); + + if (Latitude.class.isAssignableFrom(targetClass) && (preparedInput.contains("e") || preparedInput.contains("w"))) { + throw new UnitSystemParseException("Invalid latitude direction. Expected: N or S. Input: " + preparedInput); + } else if (Longitude.class.isAssignableFrom(targetClass) && (preparedInput.contains("n") || preparedInput.contains("s"))) { + throw new UnitSystemParseException("Invalid longitude direction. Expected: W or E. Input: " + preparedInput); + } + + double valueInDegrees = convertDMSExpressionToDecimalDegrees(preparedInput); + return new ValueSymbolPair(valueInDegrees, AngleUnits.DEGREES.getSymbol()); + } + + /** + * Extracts the degrees value from a string in degrees-minutes-seconds format. + * + * @param dmsFormat the string to parse in degrees-minutes-seconds format + * @return the degrees value as a double + * @throws UnitSystemArgumentException if dmsFormat is invalid + */ + public static double convertDMSExpressionToDecimalDegrees(String dmsFormat) { + if (dmsFormat == null || dmsFormat.isBlank()) { + throw new UnitSystemArgumentException("Geo parser: Invalid input. Argument cannot be null or blank."); + } + + String[] parts = dmsFormat.split("[o°'\"nsew]"); + + if (parts.length == 0) { + throw new UnitSystemArgumentException("Geo DMS parser: Input string could not be parsed: input = " + + dmsFormat); + } + + double degrees = ParsingHelpers.parseToDouble(parts[0]); + + double minutes = 0; + if (parts.length > 1) { + minutes = ParsingHelpers.parseToDouble(parts[1]); + } + + double seconds = 0; + if (parts.length > 2) { + seconds = ParsingHelpers.parseToDouble(parts[2]); + } + + char directionChar = dmsFormat.charAt(dmsFormat.length() - 1); + double sign = HaversineEquations.determineSign(String.valueOf(directionChar), degrees); + + return sign * HaversineEquations.dmsToDegrees(degrees, minutes, seconds); + } + +} diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java index ec16d9a..e14ebbb 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Latitude.java @@ -4,6 +4,8 @@ import com.synerset.unitility.unitsystem.common.Angle; import com.synerset.unitility.unitsystem.common.AngleUnit; import com.synerset.unitility.unitsystem.common.AngleUnits; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemArgumentException; +import com.synerset.unitility.unitsystem.util.ValueSymbolPair; import java.util.Objects; @@ -59,6 +61,53 @@ public static Latitude ofDegMinSec(int degrees, int minutes, double seconds, Pri return ofDegrees(sign * decimalDegrees); } + /** + * Creates a {@link Latitude} instance by parsing a coordinate string expressed + * in ICAO-compliant DMS_S (Degrees–Minutes–Seconds with Symbol) format. + *

+ * The input must follow the conventions defined in + * ICAO Annex 15 — Aeronautical Information Services, where latitude + * is expressed in degrees (°), minutes (′), and seconds (″) of arc, + * followed by a North (N) or South (S) direction indicator. + *

+ * This method automatically validates and parses DMS-formatted strings such as: + *

    + *
  • {@code 52°14'05.12"N}
  • + *
  • {@code 52°14'5.1"S}
  • + *
  • {@code 52deg14min5.123secN}
  • + *
+ * Optional symbols for degrees, minutes, or seconds are supported + * (°, o, deg, ', min, ″, sec, etc.), and the seconds component + * may include any decimal precision (e.g., 0.01″ or higher). + *

+ * If the input string is {@code null}, missing the N/S direction, + * or does not conform to a valid DMS structure, a + * {@link UnitSystemArgumentException} is thrown. + * + *

Examples:

+ *
{@code
+     * Latitude lat1 = Latitude.ofDMSFormat("52°14'05.12\"N");    // Valid, ICAO precision
+     * Latitude lat2 = Latitude.ofDMSFormat("52°14'5.1\"S");      // Valid, lower precision
+     * Latitude lat3 = Latitude.ofDMSFormat("52deg14min5.123secN"); // Valid, alternate format
+     * Latitude.ofDMSFormat("52°14'5.1");  // X Invalid — missing direction (N/S)
+     * }
+ * + * @param dmsFormat the DMS_S-formatted latitude string to parse (e.g. {@code "52°14'05.12\"N"}) + * @return a {@link Latitude} instance representing the parsed coordinate + * @throws UnitSystemArgumentException if {@code dmsFormat} is {@code null} or malformed + * @see GeoParsingHelpers#isDMSFormatOrSimilar(String) + * @see GeoParsingHelpers#extractValueAndSymbolFromDMSFormat(Class, String) + * @see DMSCoordinateFormatter + */ + public static Latitude ofDMSFormat(String dmsFormat) { + if (dmsFormat == null || !GeoParsingHelpers.isDMSFormatOrSimilar(dmsFormat)) { + throw new UnitSystemArgumentException("Latitude input DMS format is invalid: " + dmsFormat); + } + ValueSymbolPair valueSymbolPair = GeoParsingHelpers.extractValueAndSymbolFromDMSFormat(Latitude.class, dmsFormat); + return of(valueSymbolPair.value(), valueSymbolPair.symbol()); + } + + @Override public double getValue() { return value; @@ -115,21 +164,71 @@ public double getInDegrees() { return getInUnit(AngleUnits.DEGREES); } - // Console output in DMS (degrees, minutes, seconds) format - public String toDMSFormat() { - return DMSValueFormatter.latitudeToDmsFormat(this, DMSValueFormatter.DEFAULT_ICAO_SECONDS_PRECISION); - } - - public String toDMSFormat(String variableName) { - return variableName + " = " + toDMSFormat(); - } - - public String toDMSFormat(double secondsPrecision) { - return DMSValueFormatter.latitudeToDmsFormat(this, secondsPrecision); - } - - public String toDMSFormat(String variableName, double secondsPrecision) { - return variableName + " = " + toDMSFormat(secondsPrecision); + // Formatted in DMS_s (degrees, minutes, seconds) format + /** + * Returns the latitude formatted in ICAO-compliant DMS_S (Degrees–Minutes–Seconds with Symbol) format. + *

+ * This format follows the conventions defined in ICAO Annex 15 — Aeronautical Information Services, where + * coordinates are expressed in degrees, minutes, and seconds of arc, and the seconds value is typically given + * to a resolution of 0.01″ (hundredth of a second). + *

+ * Example output: {@code 52°14'05.12"N} + *

    + *
  • Degrees (°) and minutes (′) are integer values.
  • + *
  • Seconds (″) may include decimals depending on the chosen resolution.
  • + *
  • Direction (N/S for latitude, E/W for longitude) is mandatory.
  • + *
+ * + * @return DMS_S-formatted latitude string using the default ICAO seconds resolution (0.01″) + */ + public String toDMSsFormat() { + return DMSCoordinateFormatter.latitudeToDMSSFormat(this, DMSCoordinateFormatter.DEFAULT_ICAO_SECONDS_RESOLUTION); + } + + /** + * Returns the latitude formatted in ICAO-compliant DMS_S format, + * prefixed with the provided variable name. + *

+ * Example output: {@code LAT = 52°14'05.12"N} + * + * @param variableName a label to prepend before the coordinate, typically a variable name such as "LAT" or "LONG" + * @return formatted DMS_S coordinate string prefixed with the given variable name + */ + public String toDMSsFormat(String variableName) { + return variableName + " = " + toDMSsFormat(); + } + + /** + * Returns the latitude formatted in ICAO-compliant DMS_S format using a custom + * seconds resolution. + *

+ * The resolution defines the rounding precision of the seconds value. For example: + *

    + *
  • {@code secondsResolution = 0.1} → seconds rounded to 1 decimal place
  • + *
  • {@code secondsResolution = 0.01} → seconds rounded to 2 decimals (default ICAO precision)
  • + *
  • {@code secondsResolution = 1} → seconds shown as integers only
  • + *
+ * Example output with {@code secondsResolution = 0.1}: {@code 52°14'05.1"N} + * + * @param secondsResolutions the rounding resolution for seconds, typically {@code 0.01} + * @return formatted DMS_S coordinate string using the given seconds resolution + */ + public String toDMSsFormat(double secondsResolutions) { + return DMSCoordinateFormatter.latitudeToDMSSFormat(this, secondsResolutions); + } + + /** + * Returns the latitude formatted in ICAO-compliant DMS_S format using a custom + * seconds resolution, and prefixed with the given variable name. + *

+ * Example output: {@code LAT = 52°14'05.12"N} + * + * @param variableName a label to prepend before the coordinate, e.g. {@code "LAT"} + * @param secondsResolutions the rounding resolution for seconds, typically {@code 0.01} + * @return formatted DMS_S coordinate string with variable name and custom seconds resolution + */ + public String toDMSsFormat(String variableName, double secondsResolutions) { + return variableName + " = " + toDMSsFormat(secondsResolutions); } @Override diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java index 563e39a..b6f3ea6 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/geographic/Longitude.java @@ -4,6 +4,8 @@ import com.synerset.unitility.unitsystem.common.Angle; import com.synerset.unitility.unitsystem.common.AngleUnit; import com.synerset.unitility.unitsystem.common.AngleUnits; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemArgumentException; +import com.synerset.unitility.unitsystem.util.ValueSymbolPair; import java.util.Objects; @@ -58,6 +60,50 @@ public static Longitude ofDegMinSec(int degrees, int minutes, double seconds, Pr return ofDegrees(sign * decimalDegrees); } + /** + * Creates a {@link Longitude} instance by parsing a coordinate string expressed + * in ICAO-compliant DMS_S (Degrees–Minutes–Seconds with Symbol) format. + *

+ * The input must follow the conventions defined in + * ICAO Annex 15 — Aeronautical Information Services, where longitude + * is expressed in degrees (°), minutes (′), and seconds (″) of arc, + * followed by an East (E) or West (W) direction indicator. + *

+ * This method automatically validates and parses DMS-formatted strings such as: + *

    + *
  • {@code 021°04'03.98"E}
  • + *
  • {@code 21°4'3.9"E}
  • + *
  • {@code 21deg4min3.98secW}
  • + *
+ * Optional degrees, minutes, or seconds symbols are supported (°, o, deg, ', min, ″, sec, etc.). + * The seconds component may include decimal precision (e.g., 0.01″). + *

+ * If the input does not conform to a valid DMS structure or is {@code null}, + * a {@link UnitSystemArgumentException} is thrown. + * + *

Examples:

+ *
{@code
+     * Longitude lon1 = Longitude.ofDMSFormat("021°04'03.98\"E");  // Valid, ICAO precision
+     * Longitude lon2 = Longitude.ofDMSFormat("21°4'3.9\"W");      // Valid, lower precision
+     * Longitude lon3 = Longitude.ofDMSFormat("21deg4min3.98secE"); // Valid, alternate format
+     * Longitude.ofDMSFormat("21°4'3.9");  // X Invalid — missing direction (E/W)
+     * }
+ * + * @param dmsFormat the DMS_S-formatted longitude string to parse (e.g. {@code "021°04'03.98\"E"}) + * @return a {@link Longitude} instance representing the parsed coordinate + * @throws UnitSystemArgumentException if {@code dmsFormat} is {@code null} or malformed + * @see GeoParsingHelpers#isDMSFormatOrSimilar(String) + * @see GeoParsingHelpers#extractValueAndSymbolFromDMSFormat(Class, String) + * @see DMSCoordinateFormatter + */ + public static Longitude ofDMSFormat(String dmsFormat) { + if (dmsFormat == null || !GeoParsingHelpers.isDMSFormatOrSimilar(dmsFormat)) { + throw new UnitSystemArgumentException("Longitude input DMS format is invalid: " + dmsFormat); + } + ValueSymbolPair valueSymbolPair = GeoParsingHelpers.extractValueAndSymbolFromDMSFormat(Longitude.class, dmsFormat); + return of(valueSymbolPair.value(), valueSymbolPair.symbol()); + } + @Override public double getValue() { return value; @@ -96,21 +142,71 @@ public Longitude withValue(double value) { return Longitude.of(value, unitType); } - // Console output in DMS (degrees, minutes, seconds) format - public String toDMSFormat() { - return DMSValueFormatter.longitudeToDmsFormat(this, DMSValueFormatter.DEFAULT_ICAO_SECONDS_PRECISION); - } - - public String toDMSFormat(String variableName) { - return variableName + " = " + toDMSFormat(); - } - - public String toDMSFormat(double secondsPrecision) { - return DMSValueFormatter.longitudeToDmsFormat(this, secondsPrecision); - } - - public String toDMSFormat(String variableName, double secondsPrecision) { - return variableName + " = " + toDMSFormat(secondsPrecision); + // Formatted in DMS_s (degrees, minutes, seconds) format + /** + * Returns the longitude formatted in ICAO-compliant DMS_S (Degrees–Minutes–Seconds with Symbol) format. + *

+ * This format follows the conventions defined in ICAO Annex 15 — Aeronautical Information Services, where + * geographical coordinates are expressed in degrees, minutes, and seconds of arc. The seconds component is + * typically represented to a precision of 0.01″ (hundredth of a second) by default. + *

+ * Example output: {@code 021°04'03.98"E} + *

    + *
  • Degrees (°) and minutes (′) are integer values.
  • + *
  • Seconds (″) may contain decimals depending on the specified resolution.
  • + *
  • Direction (E/W) is mandatory for longitude.
  • + *
+ * + * @return DMS_S-formatted longitude string using the default ICAO seconds resolution (0.01″) + */ + public String toDMSsFormat() { + return DMSCoordinateFormatter.longitudeToDMSSFormat(this, DMSCoordinateFormatter.DEFAULT_ICAO_SECONDS_RESOLUTION); + } + + /** + * Returns the longitude formatted in ICAO-compliant DMS_S format, + * prefixed with the provided variable name. + *

+ * Example output: {@code LON = 021°04'03.98"E} + * + * @param variableName a label to prepend before the coordinate, typically a variable name such as "LON" + * @return formatted DMS_S longitude string prefixed with the given variable name + */ + public String toDMSsFormat(String variableName) { + return variableName + " = " + toDMSsFormat(); + } + + /** + * Returns the longitude formatted in ICAO-compliant DMS_S format using a custom + * seconds resolution. + *

+ * The seconds resolution defines how precisely the seconds component is rounded or displayed. For example: + *

    + *
  • {@code secondsResolution = 0.1} → seconds rounded to one decimal place
  • + *
  • {@code secondsResolution = 0.01} → seconds rounded to two decimals (default ICAO precision)
  • + *
  • {@code secondsResolution = 1} → seconds displayed as whole numbers
  • + *
+ * Example output with {@code secondsResolution = 0.1}: {@code 021°04'03.9"E} + * + * @param secondsResolutions the rounding resolution for seconds, typically {@code 0.01} + * @return formatted DMS_S longitude string using the given seconds resolution + */ + public String toDMSsFormat(double secondsResolutions) { + return DMSCoordinateFormatter.longitudeToDMSSFormat(this, secondsResolutions); + } + + /** + * Returns the longitude formatted in ICAO-compliant DMS_S format using a custom + * seconds resolution, and prefixed with the provided variable name. + *

+ * Example output: {@code LON = 021°04'03.98"E} + * + * @param variableName a label to prepend before the coordinate, e.g. {@code "LON"} + * @param secondsResolutions the rounding resolution for seconds, typically {@code 0.01} + * @return formatted DMS_S longitude string with variable name and custom seconds resolution + */ + public String toDMSsFormat(String variableName, double secondsResolutions) { + return variableName + " = " + toDMSsFormat(secondsResolutions); } // Convert to target unit diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ParsingHelpers.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ParsingHelpers.java index d7c68d8..f693d53 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ParsingHelpers.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ParsingHelpers.java @@ -1,8 +1,6 @@ package com.synerset.unitility.unitsystem.util; -import com.synerset.unitility.unitsystem.exceptions.UnitSystemArgumentException; import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; -import com.synerset.unitility.unitsystem.geographic.HaversineEquations; public class ParsingHelpers { @@ -62,40 +60,4 @@ public static double parseToDouble(String doubleAsString) { return parseToDouble(doubleAsString, ""); } - /** - * Extracts the degrees value from a string in degrees-minutes-seconds format. - * - * @param dmsFormat the string to parse in degrees-minutes-seconds format - * @return the degrees value as a double - * @throws UnitSystemArgumentException if dmsFormat is invalid - */ - public static double extractDegreesFromDMSFormat(String dmsFormat) { - if (dmsFormat == null || dmsFormat.isBlank()) { - throw new UnitSystemArgumentException("Geo parser: Invalid input. Argument cannot be null or blank."); - } - - String[] parts = dmsFormat.split("[o°'\"nsew]"); - - if (parts.length == 0) { - throw new UnitSystemArgumentException("Geo DMS parser: Input string could not be parsed: input = " - + dmsFormat); - } - - double degrees = parseToDouble(parts[0]); - - double minutes = 0; - if (parts.length > 1) { - minutes = parseToDouble(parts[1]); - } - - double seconds = 0; - if (parts.length > 2) { - seconds = parseToDouble(parts[2]); - } - - char directionChar = dmsFormat.charAt(dmsFormat.length() - 1); - double sign = HaversineEquations.determineSign(String.valueOf(directionChar), degrees); - - return sign * HaversineEquations.dmsToDegrees(degrees, minutes, seconds); - } } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityAbstractParsingFactory.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityAbstractParsingFactory.java index cbef737..84ab4de 100644 --- a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityAbstractParsingFactory.java +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityAbstractParsingFactory.java @@ -2,10 +2,9 @@ import com.synerset.unitility.unitsystem.PhysicalQuantity; import com.synerset.unitility.unitsystem.Unit; -import com.synerset.unitility.unitsystem.common.AngleUnits; import com.synerset.unitility.unitsystem.exceptions.UnitSystemClassNotSupportedException; import com.synerset.unitility.unitsystem.exceptions.UnitSystemParseException; -import com.synerset.unitility.unitsystem.geographic.DMSValidator; +import com.synerset.unitility.unitsystem.geographic.GeoParsingHelpers; import com.synerset.unitility.unitsystem.geographic.Latitude; import com.synerset.unitility.unitsystem.geographic.Longitude; @@ -18,23 +17,27 @@ public abstract class PhysicalQuantityAbstractParsingFactory implements Physical public > Q parse(Class targetClass, String quantityAsString) { + if(quantityAsString == null || quantityAsString.trim().isEmpty()){ + throw new UnitSystemParseException("Quantity parsing error. Input string quantity cannot be null or empty."); + } + String preparedInput = StringTransformer.of(quantityAsString) .trimLowerAndClean() .replaceCommaForDot() .dropParentheses() .toString(); - Pair extractedPair; + ValueSymbolPair extractedPair; - if (isGeoQuantity(targetClass) && DMSValidator.isValidDMSFormat(preparedInput)) { - extractedPair = extractValueAndSymbolFromDMSFormat(targetClass, preparedInput); + if (isGeoQuantity(targetClass) && GeoParsingHelpers.isDMSFormatOrSimilar(preparedInput)) { + extractedPair = GeoParsingHelpers.extractValueAndSymbolFromDMSFormat(targetClass, preparedInput); } else{ extractedPair = extractValueAndSymbol(preparedInput); } - return extractedPair.symbol == null || extractedPair.symbol.isBlank() - ? parseValueWithDefaultUnit(targetClass, extractedPair.value) - : parseValueAndSymbol(targetClass, extractedPair.value, extractedPair.symbol); + return extractedPair.symbol() == null || extractedPair.symbol().isBlank() + ? parseValueWithDefaultUnit(targetClass, extractedPair.value()) + : parseValueAndSymbol(targetClass, extractedPair.value(), extractedPair.symbol()); } public > Q parseValueAndSymbol(Class targetClass, @@ -99,7 +102,7 @@ private boolean isGeoQuantity(Class targetClass){ return Latitude.class.isAssignableFrom(targetClass) || Longitude.class.isAssignableFrom(targetClass); } - private Pair extractValueAndSymbol(String preparedInput){ + private ValueSymbolPair extractValueAndSymbol(String preparedInput){ int indexOfLastDigit = 0; // Calculates where the value ends in the input string. The "e" is for case of scientific notation: -1.12345E-5 @@ -113,24 +116,7 @@ private Pair extractValueAndSymbol(String preparedInput){ String symbolPart = preparedInput.substring(indexOfLastDigit); double value = ParsingHelpers.parseToDouble(valuePart); - return new Pair(value, symbolPart); + return new ValueSymbolPair(value, symbolPart); } - private Pair extractValueAndSymbolFromDMSFormat(Class targetClass, String partiallyPreparedInput){ - String preparedInput = StringTransformer.of(partiallyPreparedInput) - .unifyDMSNotationSymbols() - .toString(); - - if (Latitude.class.isAssignableFrom(targetClass) && (preparedInput.contains("e") || preparedInput.contains("w"))) { - throw new UnitSystemParseException("Invalid latitude direction. Expected: N or S. Input: " + preparedInput); - } else if (Longitude.class.isAssignableFrom(targetClass) && (preparedInput.contains("n") || preparedInput.contains("s"))) { - throw new UnitSystemParseException("Invalid longitude direction. Expected: W or E. Input: " + preparedInput); - } - - double valueInDegrees = ParsingHelpers.extractDegreesFromDMSFormat(preparedInput); - return new Pair(valueInDegrees, AngleUnits.DEGREES.getSymbol()); - } - - record Pair(Double value, String symbol) {} - } \ No newline at end of file diff --git a/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueSymbolPair.java b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueSymbolPair.java new file mode 100644 index 0000000..b16b984 --- /dev/null +++ b/unitility-core/src/main/java/com/synerset/unitility/unitsystem/util/ValueSymbolPair.java @@ -0,0 +1,3 @@ +package com.synerset.unitility.unitsystem.util; + +public record ValueSymbolPair(Double value, String symbol) {} diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatterTest.java new file mode 100644 index 0000000..574005b --- /dev/null +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSCoordinateFormatterTest.java @@ -0,0 +1,116 @@ +package com.synerset.unitility.unitsystem.geographic; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DMSCoordinateFormatterTest { + + // Define the default precision for clarity, though it's passed explicitly + private static final double DEFAULT_ICAO_RESOLUTION = 0.01; + + @Test + void testEdgeBoundaries() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(90.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("90°00'00.00\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(-90.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("90°00'00.00\"S"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(0.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("00°00'00.00\"N"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(0.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("000°00'00.00\"E"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(180.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("180°00'00.00\"E"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(-180.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("180°00'00.00\"W"); + } + + @Test + void testIntegerDegrees() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°00'00.00\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(7.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("07°00'00.00\"N"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(5.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("005°00'00.00\"E"); + } + + @Test + void testMinutesNonZeroSecondsZero() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.083333333333336), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°05'00.00\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(0.125), DEFAULT_ICAO_RESOLUTION)).isEqualTo("00°07'30.00\"N"); + } + + @Test + void testSecondsWhole() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.09305555555555), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°05'35.00\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(-12.505277777777778), DEFAULT_ICAO_RESOLUTION)).isEqualTo("12°30'19.00\"S"); + } + + @Test + void testSecondsWithFractional() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.093125), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°05'35.25\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(52.234567), DEFAULT_ICAO_RESOLUTION)).isEqualTo("52°14'04.44\"N"); + } + + @Test + void testVerySmallFractions() { + // 35° 5' 34.8036" + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.093001), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°05'34.80\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.00027777777778), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°00'01.00\"N"); + } + + @Test + void testZeroSecondsButFractionalMinutes() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.00041666666667), DEFAULT_ICAO_RESOLUTION)).isEqualTo("35°00'01.50\"N"); + } + + @Test + void testMinutesOrSecondsLessThan10() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(7.051144444444444), DEFAULT_ICAO_RESOLUTION)).isEqualTo("07°03'04.12\"N"); + } + + @Test + void testLongitudeSpecifics() { + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(5.0), DEFAULT_ICAO_RESOLUTION)).isEqualTo("005°00'00.00\"E"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(32.85352777777778), DEFAULT_ICAO_RESOLUTION)).isEqualTo("032°51'12.70\"E"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(179.999999), DEFAULT_ICAO_RESOLUTION)).isEqualTo("180°00'00.00\"E"); + } + + @Test + void testExamplesNegatives() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(-0.0002777777777777778), DEFAULT_ICAO_RESOLUTION)).isEqualTo("00°00'01.00\"S"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(-121.90277777777777), DEFAULT_ICAO_RESOLUTION)).isEqualTo("121°54'10.00\"W"); + } + + @Test + void testRandomFuzz() { + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(0.0002777777777777778), DEFAULT_ICAO_RESOLUTION)).isEqualTo("00°00'01.00\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(45.5), DEFAULT_ICAO_RESOLUTION)).isEqualTo("45°30'00.00\"N"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(-45.5), DEFAULT_ICAO_RESOLUTION)).isEqualTo("45°30'00.00\"S"); + // This value is 89° 59' 59.9964", but should be rounded up to "90°00'00.00"N" (including rounding overflow) + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(89.999999), DEFAULT_ICAO_RESOLUTION)).isEqualTo("90°00'00.00\"N"); + } + + @Test + void testDefaultPrecisionMethods() { + // Also test the overloaded methods that use the default precision + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.093125))).isEqualTo("35°05'35.25\"N"); + assertThat(DMSCoordinateFormatter.longitudeToDMSSFormat(Longitude.ofDegrees(-121.90277777))).isEqualTo("121°54'10.00\"W"); + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(0.0))).isEqualTo("00°00'00.00\"N"); + } + + @Test + void testOtherPrecisions() { + // Test with 1 decimal place (0.1 precision) + // 35.093125 -> 35°05'35.25" -> Rounds to 35.3 + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.093125), 0.1)).isEqualTo("35°05'35.3\"N"); + + // Test with 0 decimal places (1.0 precision) + // 35.093125 -> 35°05'35.25" -> Rounds to 35 + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.093125), 1.0)).isEqualTo("35°05'35\"N"); + + // Test with 3 decimal places (0.001 precision) + // 35.09309163888889 -> 35° 5' 35.1299 -> Rounds to .130 + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.09309163888889), 0.001)).isEqualTo("35°05'35.130\"N"); + + // Test with 0 decimal places (0 precision) + // 35.09309163888889 -> 35° 5' 35.1299 -> Rounds to .130 + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.09309163888889), 0)).isEqualTo("35°05'35\"N"); + + // Test with -1, should result to default 0.01 resolution + assertThat(DMSCoordinateFormatter.latitudeToDMSSFormat(Latitude.ofDegrees(35.09309163888889), -1)).isEqualTo("35°05'35.13\"N"); + } + +} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValidatorTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValidatorTest.java index 2a8dff8..0e09484 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValidatorTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValidatorTest.java @@ -10,17 +10,72 @@ class DMSValidatorTest { @Test @DisplayName("should return true when given string is valid DMS format") void isValidDMSFormat_shouldReturnTrueIfStringIsValidDmsFormat() { - assertThat(DMSValidator.isValidDMSFormat("52°14'5.123\"N")).isTrue(); - assertThat(DMSValidator.isValidDMSFormat("52°14'5.123\"")).isTrue(); - assertThat(DMSValidator.isValidDMSFormat("52°14'")).isTrue(); - assertThat(DMSValidator.isValidDMSFormat("52°")).isTrue(); - assertThat(DMSValidator.isValidDMSFormat("52°")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52°14'5.123\"N")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52°14'5.123\"")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52°14'")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52°")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52°")).isTrue(); // Invalid examples - assertThat(DMSValidator.isValidDMSFormat("52")).isFalse(); - assertThat(DMSValidator.isValidDMSFormat("52o14'5.123\"X")).isFalse(); - assertThat(DMSValidator.isValidDMSFormat("52o14'5.123\"NS")).isFalse(); - assertThat(DMSValidator.isValidDMSFormat("abc")).isFalse(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52")).isFalse(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52o14'5.123\"X")).isFalse(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("52o14'5.123\"NS")).isFalse(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("abc")).isFalse(); + } + + @Test + void testValidDMSFormats() { + // Basic valid latitude and longitude + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'N")).isTrue(); // minimal valid DMS, no seconds + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("123°15'45\"E")).isTrue(); // full DMS + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("0°0'0\"N")).isTrue(); // edge case: 0 degrees + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("90°0'0\"S")).isTrue(); // edge case: max latitude + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("180°0'0\"W")).isTrue(); // edge case: max longitude + + // With trailing zeros + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'00\"N")).isTrue(); // trailing zeros in seconds + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'00.0000\"N")).isTrue(); // trailing zeros with decimals + + // High precision seconds + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'15.1234567N")).isTrue(); // arbitrary decimal places + + // Different unit suffixes + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45o30'15.5N")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45deg30min15.5secN")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30m15.5sN")).isTrue(); + + // Lowercase direction + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'15.5n")).isTrue(); + } + + @Test + void testInvalidDMSFormats() { + // Invalid direction + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'15.5X")).isFalse(); // invalid letter + + // Invalid numbers + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("1234°0'0\"N")).isFalse(); // too many degrees + // Non-numeric input + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("abc°def'ghi\"N")).isFalse(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°xx'yy\"N")).isFalse(); + + // Completely malformed + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("°'\"N")).isFalse(); // only symbols + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("")).isFalse(); // empty string + } + + @Test + void testEdgeCases() { + // Negative degrees (valid for longitude west or latitude south) + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("-45°30'15.5S")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("-123°45'0\"W")).isTrue(); + + // Degrees with only decimal part for seconds + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°30'0.5N")).isTrue(); // half a second + + // Only degrees and direction + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("45°N")).isTrue(); + assertThat(GeoParsingHelpers.isDMSFormatOrSimilar("123°E")).isTrue(); } } \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java deleted file mode 100644 index fc55463..0000000 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/DMSValueFormatterTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.synerset.unitility.unitsystem.geographic; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class DMSValueFormatterTest { - - @Test - void testConvertDMSValueToDMSValue() - { - Latitude latitude = Latitude.ofDegrees(52.1); - String string = latitude.toDMSFormat(-1); - - } - -} \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java index a21c659..a941a12 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/GeoCoordinateTest.java @@ -8,22 +8,22 @@ class GeoCoordinateTest { @Test - @DisplayName("should output coordinates in DMS format") - void toDMSFormat_shouldOutputInDegreeMinutesSecondsFormat() { + @DisplayName("GeoCoordinate: should output coordinates in DMS format") + void toDMSsFormat_shouldOutputInDegreeMinutesSecondsFormat() { // Given Latitude latitude = Latitude.ofDegrees(-0.023411888); Longitude longitude = Longitude.ofDegrees(-1.56711888); // When GeoCoordinate geoCoordinate = GeoCoordinate.of(latitude, longitude, "name"); - String actualDmsOutput = geoCoordinate.toDMSFormat(); - String actualDmsOutputVar = geoCoordinate.toDMSFormat("sea_quest"); - String actualDmsOutputVarTruncated = geoCoordinate.toDMSFormat("sea_quest", 0.001); + String actualDmsOutput = geoCoordinate.toDMSsFormat(); + String actualDmsOutputVar = geoCoordinate.toDMSsFormat("sea_quest"); + String actualDmsOutputVarTruncated = geoCoordinate.toDMSsFormat("sea_quest", 0.001); // Then - assertThat(actualDmsOutput).isEqualTo("00°01'24.28\"S, 001°34'1.63\"W"); - assertThat(actualDmsOutputVar).isEqualTo("sea_quest = 00°01'24.28\"S, 001°34'1.63\"W"); - assertThat(actualDmsOutputVarTruncated).isEqualTo("sea_quest = 00°01'24.283\"S, 001°34'1.628\"W"); + assertThat(actualDmsOutput).isEqualTo("00°01'24.28\"S, 001°34'01.63\"W"); + assertThat(actualDmsOutputVar).isEqualTo("sea_quest = 00°01'24.28\"S, 001°34'01.63\"W"); + assertThat(actualDmsOutputVarTruncated).isEqualTo("sea_quest = 00°01'24.283\"S, 001°34'01.628\"W"); assertThat(geoCoordinate.name()).isEqualTo("name"); assertThat(geoCoordinate.latitude()).isEqualTo(latitude); @@ -31,7 +31,7 @@ void toDMSFormat_shouldOutputInDegreeMinutesSecondsFormat() { } @Test - @DisplayName("should output coordinates in decimal degrees format") + @DisplayName("GeoCoordinate: should output coordinates in decimal degrees format") void toDecimalDegrees_shouldOutputInDecimalDegreesFormat() { // Given Latitude latitude = Latitude.ofDegrees(-52.2341122); @@ -50,7 +50,7 @@ void toDecimalDegrees_shouldOutputInDecimalDegreesFormat() { } @Test - @DisplayName("should output cin engineering format") + @DisplayName("GeoCoordinate: should output cin engineering format") void toEngineeringFormat_shouldOutputInEngineeringFormat() { // Given Latitude latitude = Latitude.ofDegrees(-52.2341122); @@ -69,7 +69,7 @@ void toEngineeringFormat_shouldOutputInEngineeringFormat() { } @Test - @DisplayName("should be equals for defined precision") + @DisplayName("GeoCoordinate: should be equals for defined precision") void equalsWithPrecision_shouldBeEqualForPrecision() { // Given Latitude latitude1 = Latitude.ofDegrees(-52.236); @@ -87,4 +87,17 @@ void equalsWithPrecision_shouldBeEqualForPrecision() { assertThat(actualResult).isTrue(); } + + @Test + @DisplayName("GeoCoordinate: should create geo coordinate from DMS format") + void shouldCreateGeoCoordinateFromDMSFormat() { + String latitudeAsStringN = "02°14'5.1\"N"; + String longitudeAsStringW = "002°14'5.10000000000000001\"W"; + + GeoCoordinate geoCoordinate = GeoCoordinate.ofDMSFormat(latitudeAsStringN, longitudeAsStringW); + + assertThat(geoCoordinate.latitude()).isEqualTo(Latitude.ofDegrees(2.23475)); + assertThat(geoCoordinate.longitude()).isEqualTo(Longitude.ofDegrees(-2.23475)); + + } } \ No newline at end of file diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java index 6e1bba2..5955ed7 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LatitudeTest.java @@ -2,17 +2,16 @@ import com.synerset.unitility.unitsystem.common.AngleUnit; import com.synerset.unitility.unitsystem.common.AngleUnits; -import com.synerset.unitility.unitsystem.util.PhysicalQuantityParsingFactory; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemArgumentException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.withPrecision; +import static org.assertj.core.api.Assertions.*; class LatitudeTest { @Test - @DisplayName("should properly convert degrees to radians and vice versa") + @DisplayName("Latitude: should properly convert degrees to radians and vice versa") void shouldProperlyConvertFromDegreeToRadian() { // Given Latitude initialAngleInDegrees = Latitude.ofDegrees(45); @@ -31,7 +30,7 @@ void shouldProperlyConvertFromDegreeToRadian() { } @Test - @DisplayName("should have DEGREES as base unit") + @DisplayName("Latitude: should have DEGREES as base unit") void shouldHaveDegreesAsBaseUnit() { // Given AngleUnit expectedBaseUnit = AngleUnits.RADIANS; @@ -45,7 +44,7 @@ void shouldHaveDegreesAsBaseUnit() { } @Test - @DisplayName("should return valid result from to() and getIn() methods") + @DisplayName("Latitude: should return valid result from to() and getIn() methods") void shouldReturnValidResultFromToAndGetInMethods() { // Given Latitude expected = Latitude.ofDegrees(10.1); @@ -60,43 +59,51 @@ void shouldReturnValidResultFromToAndGetInMethods() { } @Test - @DisplayName("should output latitude in DMS format") - void toDmsFormat_shouldOutputValidDMSFormat() { + @DisplayName("Latitude: should output latitude in DMS format") + void toDmsFormat_shouldOutputValidDMSSFormat() { // Given Latitude latitude = Latitude.ofDegrees(52.23475638888889); // When - String latInDms = latitude.toDMSFormat(); - String latInDmsVar = latitude.toDMSFormat("lat"); - String latInDmsVarDigits = latitude.toDMSFormat("lat", 0.001); + String latInDms = latitude.toDMSsFormat(); + String latInDmsVar = latitude.toDMSsFormat("lat"); + String latInDmsVarDigits = latitude.toDMSsFormat("lat", 0.001); // Then - assertThat(latInDms).isEqualTo("52°14'5.12\"N"); - assertThat(latInDmsVar).isEqualTo("lat = 52°14'5.12\"N"); - assertThat(latInDmsVarDigits).isEqualTo("lat = 52°14'5.123\"N"); + assertThat(latInDms).isEqualTo("52°14'05.12\"N"); + assertThat(latInDmsVar).isEqualTo("lat = 52°14'05.12\"N"); + assertThat(latInDmsVarDigits).isEqualTo("lat = 52°14'05.123\"N"); } @Test - @DisplayName("should create instance from DMS input") - void shouldCreateNewInstanceFromDMSFormat(){ + @DisplayName("Latitude: should create instance from DMS input") + void shouldCreateNewInstanceFromDMSFormat() { // Given String latitudeAsStringN = "02°14'5.1\"N"; String latitudeAsStringS = "52°14'5.1\"S"; - String longitudeAsStringE = "002°14'5.1\"E"; - String longitudeAsStringW = "52°14'5.1\"W"; + // When + Latitude latitudeN = Latitude.ofDMSFormat(latitudeAsStringN); + Latitude latitudeS = Latitude.ofDMSFormat(latitudeAsStringS); + + // Then + assertThat(latitudeN.getInDegrees()).isEqualTo(2.23475); + assertThat(latitudeS.getInDegrees()).isEqualTo(-52.23475); + assertThatThrownBy(() -> Latitude.ofDMSFormat("90NN")).isInstanceOf(UnitSystemArgumentException.class); + assertThatThrownBy(() -> Latitude.ofDMSFormat(null)).isInstanceOf(UnitSystemArgumentException.class); + + } + + @Test + @DisplayName("Latitude: should create instance from minutes degrees seconds") + void shouldCreateNewInstanceFromDegreesMinutesSeconds() { // When - PhysicalQuantityParsingFactory parsingFactory = PhysicalQuantityParsingFactory.getDefaultParsingFactory(); - Latitude latitudeN = parsingFactory.parse(Latitude.class, latitudeAsStringN); - Latitude latitudeS = parsingFactory.parse(Latitude.class, latitudeAsStringS); - Longitude longitudeE = parsingFactory.parse(Longitude.class, longitudeAsStringE); - Longitude longitudeW = parsingFactory.parse(Longitude.class, longitudeAsStringW); + Latitude latitudeN = Latitude.ofDegMinSec(2, 14, 5.1); + Latitude latitudeS = Latitude.ofDegMinSec(-52, 14, 5.1); // Then assertThat(latitudeN.getInDegrees()).isEqualTo(2.23475); assertThat(latitudeS.getInDegrees()).isEqualTo(-52.23475); - assertThat(longitudeE.getInDegrees()).isEqualTo(2.23475); - assertThat(longitudeW.getInDegrees()).isEqualTo(-52.23475); } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java index 0f489fe..203f110 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/geographic/LongitudeTest.java @@ -2,15 +2,17 @@ import com.synerset.unitility.unitsystem.common.AngleUnit; import com.synerset.unitility.unitsystem.common.AngleUnits; +import com.synerset.unitility.unitsystem.exceptions.UnitSystemArgumentException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class LongitudeTest { @Test - @DisplayName("should properly convert degrees to radians and vice versa") + @DisplayName("Longitude: should properly convert degrees to radians and vice versa") void shouldProperlyConvertFromDegreeToRadian() { // Given Longitude initialAngleInDegrees = Longitude.ofDegrees(45); @@ -29,7 +31,7 @@ void shouldProperlyConvertFromDegreeToRadian() { } @Test - @DisplayName("should have DEGREES as base unit") + @DisplayName("Longitude: should have DEGREES as base unit") void shouldHaveDegreesAsBaseUnit() { // Given AngleUnit expectedBaseUnit = AngleUnits.RADIANS; @@ -43,7 +45,7 @@ void shouldHaveDegreesAsBaseUnit() { } @Test - @DisplayName("should return valid result from to() and getIn() methods") + @DisplayName("Longitude: should return valid result from to() and getIn() methods") void shouldReturnValidResultFromToAndGetInMethods() { // Given Longitude expected = Longitude.ofDegrees(10.1); @@ -58,20 +60,54 @@ void shouldReturnValidResultFromToAndGetInMethods() { } @Test - @DisplayName("should output longitude in DMS format") - void toDmsFormat_shouldOutputValidDMSFormat() { + @DisplayName("Longitude: should output longitude in DMS format") + void toDmsFormat_shouldOutputValidDMSSFormat() { // Given Longitude longitude = Longitude.ofDegrees(-21.06777388888889); // When - String lonInDms = longitude.toDMSFormat(); - String lonInDmsVar = longitude.toDMSFormat("lat"); - String lonInDmsVarDigits = longitude.toDMSFormat("lat", 1); + String lonInDms = longitude.toDMSsFormat(); + String lonInDmsVar = longitude.toDMSsFormat("lat"); + String lonInDmsVarDigits = longitude.toDMSsFormat("lat", 1); // Then - assertThat(lonInDms).isEqualTo("021°04'3.99\"W"); - assertThat(lonInDmsVar).isEqualTo("lat = 021°04'3.99\"W"); - assertThat(lonInDmsVarDigits).isEqualTo("lat = 021°04'4\"W"); + assertThat(lonInDms).isEqualTo("021°04'03.99\"W"); + assertThat(lonInDmsVar).isEqualTo("lat = 021°04'03.99\"W"); + assertThat(lonInDmsVarDigits).isEqualTo("lat = 021°04'04\"W"); + } + + @Test + @DisplayName("Longitude: should create instance from DMS input") + void shouldCreateNewInstanceFromDMSFormat(){ + // Given + String longitudeAsStringE = "002°14'5.1\"E"; + String longitudeAsStringW = "002°14'5.10000000000000001\"W"; + + // When + Longitude longitudeE = Longitude.ofDMSFormat(longitudeAsStringE); + Longitude longitudeW = Longitude.ofDMSFormat(longitudeAsStringW); + + // Then + assertThat(longitudeE.getInDegrees()).isEqualTo(2.23475); + assertThat(longitudeW.getInDegrees()).isEqualTo(-2.23475); + + } + + @Test + @DisplayName("Longitude: should create instance from minutes degrees seconds") + void shouldCreateNewInstanceFromDegreesMinutesSeconds(){ + // When + Longitude longitudeE = Longitude.ofDegMinSec(2,14,5.1); + Longitude longitudeW = Longitude.ofDegMinSec(-52,14,5.10000000000); + + // Then + assertThat(longitudeE.getInDegrees()).isEqualTo(2.23475); + assertThat(longitudeW.getInDegrees()).isEqualTo(-52.23475); + + assertThatThrownBy(() -> Longitude.ofDMSFormat("90NN")).isInstanceOf(UnitSystemArgumentException.class); + assertThatThrownBy(() -> Longitude.ofDMSFormat(null)).isInstanceOf(UnitSystemArgumentException.class); + + } } diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java index e58aa62..86639af 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/PhysicalQuantityParsingFactoryTest.java @@ -131,13 +131,21 @@ void parse_shouldParseFromDMSFormatToLatitudeOrLongitude() { @DisplayName("ParsingFactory: should fail on invalid or malformed DMS format") void parse_shouldFailOnInvalidOrMalformedDMSFormat() { // Given + // Malformed because it's latitude (52°...) but direction 'E' is only valid for longitude String lat1 = "52°14'5.123\"E"; + + // Malformed because it's longitude (21°...) but direction 'N' is only valid for latitude String lon1 = "21°4'3.986\"N"; + + // Duplicate of lon1, same reason: longitude cannot have 'N' direction String lon2 = "21°4'3.986\"N"; - // When // Then + + // Expect UnitSystemParseException because the strings are malformed assertThrows(UnitSystemParseException.class, () -> PARSING_FACTORY.parse(Latitude.class, lat1)); assertThrows(UnitSystemParseException.class, () -> PARSING_FACTORY.parse(Longitude.class, lon1)); assertThrows(UnitSystemParseException.class, () -> PARSING_FACTORY.parse(Longitude.class, lon2)); + + assertThrows(UnitSystemParseException.class, () -> PARSING_FACTORY.parse(Longitude.class, null)); } @Test diff --git a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java index 41b00ba..8bd6796 100644 --- a/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java +++ b/unitility-core/src/test/java/com/synerset/unitility/unitsystem/util/ValueFormatterTest.java @@ -1,7 +1,6 @@ package com.synerset.unitility.unitsystem.util; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource;