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 extends DistanceUnit> 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 extends DistanceUnit> 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 extends DistanceUnit> 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 extends DistanceUnit> 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 extends DistanceUnit> 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 extends DistanceUnit> 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):
-[](https://github.com/pjazdzyk/Unitility)
+[](https://github.com/pjazdzyk/Unitility)
```markdown
-[](https://github.com/pjazdzyk/Unitility)
+[](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;