diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5c931cc..0888e8b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -15,11 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - uses: actions/checkout@v6 + - name: Set up JDK 21 + uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'temurin' cache: maven - name: Build with Maven diff --git a/.github/workflows/release-and-deploy.yml b/.github/workflows/release-and-deploy.yml index 5626fc4..895904a 100644 --- a/.github/workflows/release-and-deploy.yml +++ b/.github/workflows/release-and-deploy.yml @@ -61,20 +61,20 @@ jobs: exit 1 # Set up java with maven cache - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - uses: actions/checkout@v6 + - name: Set up JDK 21 + uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '17' + java-version: '21' cache: 'maven' # import the secret key - name: Set up Apache Maven Central - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: # running setup-java again overwrites the settings.xml distribution: 'temurin' - java-version: '17' + java-version: '21' server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml server-username: MAVEN_USERNAME # env variable for username in deploy server-password: MAVEN_CENTRAL_TOKEN # env variable for token in deploy diff --git a/CHANGELOG.md b/CHANGELOG.md index 7decd07..6867282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add static factory methods `QuantityValue.of(value, unit)` with various value argument types that can be used to instantiate the + `BigDecimal` value used internally. + +- Add dynamic factory methods in `Unit.quantityValue(value)` with various value argument types that can be used to instantiate the + `BigDecimal` value used internally. + +- Add delegate methods for many `BigDecimal` methods to `QuantityValue` to allow for mathematical operations with + automatic unit conversion, for example: + + ``` + QuantityValue mVal = Qudt.Units.M.quantityValue(20); + QuantityValue ftVal = Qudt.Units.FT.quantityValue(1); + QuantityValue mValResult = mVal.add(ftVal); + ``` + +### Fixed + +- Fix wrong definition of `Quantity`. It used to encapsulate a Set of `QuantityValue`, which never made sense. With + this change, it encapsulates a `QuantityValue` and a `QuantityKind`, conforming to the definition of the concept in QUDT. + +- Upgrade various dependencies. + ## [7.1.1] - 2025-10-09 ## [7.1.0] - 2025-09-01 diff --git a/README.md b/README.md index 8b88e21..b41151f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Makes all conversions and related functionality defined by the excellent [QUDT o The library offers * 1745 units, such as second, Fahrenheit, or light year -* 881 quantityKinds, such as width, pressure ratio or currency +* 881 quantityKinds, such as width, pressure ratio, or currency * 29 prefixes, such as mega, kibi, or atto ...all of which the library converts if possible. @@ -46,7 +46,7 @@ The main Model classes are: * `QuantityValue`: value and unit. Values are always `BigDecimal` (using `MathContext.DECIMAL128`) and there are no convenience methods allowing you to provide other numeric types. This is intentiaonal so as not to mask any conversion problems. You'll be fine. (If you need a different `MathContext`, make an issue) -All units, quantityKinds and prefixes are avalable as constants: +All units, quantityKinds, and prefixes are avalable as constants: * `Qudt.Units`: all units, such as `Qudt.Units.KiloM__PER__SEC` * `Qudt.QuantityKinds:`: all quantityKinds, such as `Qudt.QuantityKinds.BloodGlucoseLevel` * `Qudt.Prefixes`: all prefixes, such as `Qudt.Prefixes.Atto` diff --git a/pom.xml b/pom.xml index e8784d5..2347257 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ 17 UTF-8 3.1.5 - 1.25.0 + 1.33.0 @@ -65,35 +65,41 @@ org.junit.jupiter junit-jupiter - 5.9.1 + 6.1.0-M1 + test + + + org.assertj + assertj-core + 4.0.0-M1 test org.slf4j slf4j-reload4j - 2.0.3 + 2.1.0-alpha1 org.eclipse.rdf4j rdf4j-bom pom - 4.2.0 + 5.3.0-M1 import org.freemarker freemarker - 2.3.31 + 2.3.34 org.hamcrest hamcrest-library - 2.2 + 3.0 org.apache.commons commons-collections4 - 4.5.0-M2 + 4.5.0 @@ -162,7 +168,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.44.0.BETA3 + 3.2.1 org.codehaus.mojo @@ -278,7 +284,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.10.0 true ossrh diff --git a/qudtlib-main-rdf/pom.xml b/qudtlib-main-rdf/pom.xml index 83cc2c8..4361d50 100644 --- a/qudtlib-main-rdf/pom.xml +++ b/qudtlib-main-rdf/pom.xml @@ -6,7 +6,7 @@ 7.1.2-SNAPSHOT 4.0.0 - qudtlib-main-rdf> + qudtlib-main-rdf qudtlib-main-rdf pom diff --git a/qudtlib-model/pom.xml b/qudtlib-model/pom.xml index 8cf9877..c4c5929 100644 --- a/qudtlib-model/pom.xml +++ b/qudtlib-model/pom.xml @@ -9,4 +9,14 @@ 4.0.0 jar qudtlib-model + + + org.junit.jupiter + junit-jupiter-api + + + org.assertj + assertj-core + + \ No newline at end of file diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/DimensionVector.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/DimensionVector.java index 145bd9c..a24ff62 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/DimensionVector.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/DimensionVector.java @@ -44,6 +44,55 @@ public class DimensionVector { private String dimensionVectorIri; + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private float[] values = new float[8]; + + public Builder() {} + + public Builder amountOfSubstance(float exp) { + values[INDEX_AMOUNT_OF_SUBSTANCE] = exp; + return this; + } + + public Builder electricCurrent(float exp) { + values[INDEX_ELECTRIC_CURRENT] = exp; + return this; + } + + public Builder length(float exp) { + values[INDEX_LENGTH] = exp; + return this; + } + + public Builder luminousIntensity(float exp) { + values[INDEX_LUMINOUS_INTENSITY] = exp; + return this; + } + + public Builder mass(float exp) { + values[INDEX_MASS] = exp; + return this; + } + + public Builder temperature(float exp) { + values[INDEX_TEMPERATURE] = exp; + return this; + } + + public Builder time(float exp) { + values[INDEX_TIME] = exp; + return this; + } + + public DimensionVector build() { + return new DimensionVector(values); + } + } + private final float[] values; public static Optional of(String dimensionVectorIri) { diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/Quantity.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/Quantity.java index 50c1f6f..6d6877c 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/Quantity.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/Quantity.java @@ -1,31 +1,31 @@ package io.github.qudtlib.model; -import java.util.Collections; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - /** - * Represents a QUDT Quantity - a set of {@link QuantityValue}s. + * Represents a QUDT Quantity - A QuantityValue with a QuantityKind * * @author Florian Kleedorfer * @version 1.0 */ public class Quantity { - final Set quantityValues; + final QuantityValue quantityValue; + + final QuantityKind quantityKind; + + public Quantity(QuantityValue quantityValue, QuantityKind quantityKind) { + this.quantityValue = quantityValue; + this.quantityKind = quantityKind; + } - public Quantity(Set quantityValues) { - this.quantityValues = quantityValues; + public QuantityValue getQuantityValue() { + return quantityValue; } - public Set getQuantityValues() { - return Collections.unmodifiableSet(quantityValues); + public QuantityKind getQuantityKind() { + return quantityKind; } @Override public String toString() { - return "Quantity{" - + quantityValues.stream().map(Objects::toString).collect(Collectors.joining(", ")) - + '}'; + return quantityKind.toString() + " of " + quantityValue.toString(); } } diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityValue.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityValue.java index 50224d4..9624fb5 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityValue.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityValue.java @@ -2,6 +2,8 @@ import io.github.qudtlib.exception.InconvertibleQuantitiesException; import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.Objects; /** @@ -20,6 +22,30 @@ public QuantityValue(BigDecimal value, Unit unit) { this.unit = unit; } + public static QuantityValue of(BigDecimal value, Unit unit) { + return new QuantityValue(value, unit); + } + + public static QuantityValue of(int value, Unit unit) { + return new QuantityValue(new BigDecimal(value), unit); + } + + public static QuantityValue of(float value, Unit unit) { + return new QuantityValue(new BigDecimal(value), unit); + } + + public static QuantityValue of(double value, Unit unit) { + return new QuantityValue(new BigDecimal(value), unit); + } + + public static QuantityValue of(long value, Unit unit) { + return new QuantityValue(new BigDecimal(value), unit); + } + + public static QuantityValue of(String value, Unit unit) { + return new QuantityValue(new BigDecimal(value), unit); + } + public BigDecimal getValue() { return value; } @@ -51,12 +77,174 @@ public QuantityValue convert(Unit toUnit, QuantityKind quantityKind) return new QuantityValue(this.unit.convert(this.value, toUnit, quantityKind), toUnit); } + public QuantityValue subtract(QuantityValue subtrahend, MathContext mc) { + return build(value.subtract(convertValueToUnit(subtrahend), mc)); + } + + public QuantityValue[] divideAndRemainder(QuantityValue divisor, MathContext mc) { + BigDecimal[] components = value.divideAndRemainder(convertValueToUnit(divisor), mc); + return new QuantityValue[] {build(components[0]), build(components[1])}; + } + + public int compareTo(QuantityValue val) { + return value.compareTo(convertValueToUnit(val)); + } + + public QuantityValue divide(QuantityValue divisor) { + return build(value.divide(convertValueToUnit(divisor))); + } + + public QuantityValue pow(int n, MathContext mc) { + return build(value.pow(n, mc)); + } + + public QuantityValue round(MathContext mc) { + return build(value.round(mc)); + } + + public QuantityValue multiply(QuantityValue multiplicand, MathContext mc) { + return build(value.multiply(convertValueToUnit(multiplicand), mc)); + } + + public QuantityValue setScale(int newScale) { + return build(value.setScale(newScale)); + } + + public QuantityValue negate(MathContext mc) { + return build(value.negate(mc)); + } + + public QuantityValue max(QuantityValue val) { + return build(value.max(convertValueToUnit(val))); + } + + public QuantityValue abs(MathContext mc) { + return build(value.abs(mc)); + } + + public QuantityValue divideToIntegralValue(QuantityValue divisor) { + return build(value.divideToIntegralValue(convertValueToUnit(divisor))); + } + + public QuantityValue plus(MathContext mc) { + return build(value.plus(mc)); + } + + public QuantityValue movePointRight(int n) { + return build(value.movePointRight(n)); + } + + public QuantityValue remainder(QuantityValue divisor) { + return build(value.remainder(convertValueToUnit(divisor))); + } + + public QuantityValue add(QuantityValue augend, MathContext mc) { + return build(value.add(convertValueToUnit(augend), mc)); + } + + public QuantityValue stripTrailingZeros() { + return build(value.stripTrailingZeros()); + } + + public QuantityValue[] divideAndRemainder(QuantityValue divisor) { + BigDecimal[] components = value.divideAndRemainder(convertValueToUnit(divisor)); + return new QuantityValue[] {build(components[0]), build(components[1])}; + } + + public QuantityValue pow(int n) { + return build(value.pow(n)); + } + + public QuantityValue sqrt(MathContext mc) { + return build(value.sqrt(mc)); + } + + public QuantityValue abs() { + return build(value.abs()); + } + + public QuantityValue subtract(QuantityValue subtrahend) { + return build(value.subtract(convertValueToUnit(subtrahend))); + } + + public QuantityValue divide(QuantityValue divisor, RoundingMode roundingMode) { + return build(value.divide(convertValueToUnit(divisor), roundingMode)); + } + + public QuantityValue setScale(int newScale, RoundingMode roundingMode) { + return build(value.setScale(newScale, roundingMode)); + } + + public QuantityValue negate() { + return build(value.negate()); + } + + public QuantityValue multiply(QuantityValue multiplicand) { + return build(value.multiply(convertValueToUnit(multiplicand))); + } + + public QuantityValue divide(QuantityValue divisor, MathContext mc) { + return build(value.divide(convertValueToUnit(divisor), mc)); + } + + public QuantityValue min(QuantityValue val) { + return build(value.min(convertValueToUnit(val))); + } + + public QuantityValue add(QuantityValue augend) { + return build(value.add(convertValueToUnit(augend))); + } + + public QuantityValue divideToIntegralValue(QuantityValue divisor, MathContext mc) { + return build(value.divideToIntegralValue(convertValueToUnit(divisor), mc)); + } + + public QuantityValue divide(QuantityValue divisor, int scale, RoundingMode roundingMode) { + return build(value.divide(convertValueToUnit(divisor), scale, roundingMode)); + } + + public QuantityValue plus() { + return build(value.plus()); + } + + public QuantityValue movePointLeft(int n) { + return build(value.movePointLeft(n)); + } + + public QuantityValue remainder(QuantityValue divisor, MathContext mc) { + return build(value.remainder(convertValueToUnit(divisor), mc)); + } + + public int signum() { + return value.signum(); + } + + public QuantityValue scaleByPowerOfTen(int n) { + return build(value.scaleByPowerOfTen(n)); + } + @Override public int hashCode() { return Objects.hash(value, unit); } public String toString() { - return value.toString() + unit.toString(); + return value.toString() + " " + unit.toString(); + } + + private BigDecimal convertValueToUnit(QuantityValue subtrahend) { + return subtrahend.getUnit().convert(subtrahend.getValue(), this.unit); + } + + /** + * Creates a new instance of {@code QuantityValue} with the specified value and the current + * unit. + * + * @param value the numerical value for the new {@code QuantityValue}, represented as a {@code + * BigDecimal} + * @return a new {@code QuantityValue} instance with the specified value and the current unit + */ + private QuantityValue build(BigDecimal value) { + return new QuantityValue(value, this.unit); } } diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/Unit.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/Unit.java index b131e8b..b5bf6f7 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/Unit.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/Unit.java @@ -351,6 +351,30 @@ static boolean isUnitless(Unit unit) { return unit.getIri().equals("http://qudt.org/vocab/unit/UNITLESS"); } + public QuantityValue quantityValue(BigDecimal value) { + return QuantityValue.of(value, this); + } + + public QuantityValue quantityValue(int value) { + return QuantityValue.of(value, this); + } + + public QuantityValue quantityValue(long value) { + return QuantityValue.of(value, this); + } + + public QuantityValue quantityValue(float value) { + return QuantityValue.of(value, this); + } + + public QuantityValue quantityValue(double value) { + return QuantityValue.of(value, this); + } + + public QuantityValue quantityValue(String value) { + return QuantityValue.of(value, this); + } + public QuantityValue convertToQuantityValue(BigDecimal value, Unit toUnit) { return new QuantityValue(convert(value, toUnit), toUnit); } diff --git a/qudtlib-model/src/test/java/io/github/qudtlib/model/QuantityKindTests.java b/qudtlib-model/src/test/java/io/github/qudtlib/model/QuantityKindTests.java new file mode 100644 index 0000000..4359631 --- /dev/null +++ b/qudtlib-model/src/test/java/io/github/qudtlib/model/QuantityKindTests.java @@ -0,0 +1,175 @@ +package io.github.qudtlib.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.qudtlib.nodedef.Builder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +// Note: For these tests, we need to stub or mock dependencies like Unit, DimensionVector, etc. +// Assuming Unit has a builder and convert method as inferred from QuantityValue usage. +// We'll create minimal stub classes or use builders where possible for isolation. + +class QuantityKindTests { + + private QuantityKind quantityKind; + private QuantityKind.Definition definition; + + @BeforeEach + void setUp() { + definition = QuantityKind.definition("http://qudt.org/vocab/quantitykind/TestKind"); + } + + @Test + void testDefinitionConstructorWithIri() { + QuantityKind qk = definition.build(); + assertThat(qk.getIri()).isEqualTo("http://qudt.org/vocab/quantitykind/TestKind"); + assertThat(qk.getIriLocalname()).isEqualTo("TestKind"); + } + + @Test + void testDefinitionConstructorWithExistingQuantityKind() { + QuantityKind original = definition.build(); + QuantityKind.Definition newDef = QuantityKind.definition(original); + QuantityKind newQk = newDef.build(); + assertThat(newQk).isEqualToComparingFieldByField(original); + } + + @Test + void testAddLabel() { + definition.addLabel("Test Label", "en"); + QuantityKind qk = definition.build(); + assertThat(qk.getLabels()).hasSize(1); + assertThat(qk.getLabelForLanguageTag("en")) + .isPresent() + .contains(new LangString("Test Label", "en")); + assertThat(qk.hasLabel("Test Label")).isTrue(); + } + + @Test + void testAddMultipleLabels() { + definition.addLabel("English Label", "en"); + definition.addLabel("Deutsches Label", "de"); + QuantityKind qk = definition.build(); + assertThat(qk.getLabels()).hasSize(2); + assertThat(qk.getLabelForLanguageTag("en", "fr", true)) + .isPresent() + .contains("English Label"); + assertThat(qk.getLabelForLanguageTag("fr", "es", false)).isEmpty(); + } + + @Test + void testSymbol() { + definition.symbol("L"); + QuantityKind qk = definition.build(); + assertThat(qk.getSymbol()).isPresent().contains("L"); + } + + @Test + void testDescription() { + definition.description("A test quantity kind"); + QuantityKind qk = definition.build(); + assertThat(qk.getDescription()).isPresent().contains("A test quantity kind"); + } + + @Test + void testDimensionVectorIri() { + definition.dimensionVectorIri("http://qudt.org/vocab/dimensionvector/A0E0L1I0M0H0T0D0"); + QuantityKind qk = definition.build(); + assertThat(qk.getDimensionVectorIri()) + .isPresent() + .contains("http://qudt.org/vocab/dimensionvector/A0E0L1I0M0H0T0D0"); + assertThat(qk.getDimensionVector()).isPresent(); + } + + @Test + void testQkdvNumeratorAndDenominatorIri() { + definition.qkdvNumeratorIri("numIri"); + definition.qkdvDenominatorIri("denIri"); + QuantityKind qk = definition.build(); + assertThat(qk.getQkdvNumeratorIri()).isPresent().contains("numIri"); + assertThat(qk.getQkdvDenominatorIri()).isPresent().contains("denIri"); + } + + @Test + void testAddApplicableUnit() { + Builder unitBuilder = Unit.definition("http://unit/Meter"); + definition.addApplicableUnit(unitBuilder); + QuantityKind qk = definition.build(); + assertThat(qk.getApplicableUnits()).hasSize(1); + qk.addApplicableUnit(unitBuilder.build()); // Test mutable add + assertThat(qk.getApplicableUnits()).hasSize(1); // Unmodifiable, but add method is protected + } + + @Test + void testAddBroaderQuantityKind() { + Builder broaderBuilder = + QuantityKind.definition("http://qudt.org/vocab/quantitykind/Broader"); + definition.addBroaderQuantityKind(broaderBuilder); + QuantityKind qk = definition.build(); + assertThat(qk.getBroaderQuantityKinds()).hasSize(1); + qk.addBroaderQuantityKind(broaderBuilder.build()); + assertThat(qk.getBroaderQuantityKinds()).hasSize(1); // Assuming add is protected/internal + } + + @Test + void testAddExactMatch() { + Builder matchBuilder = + QuantityKind.definition("http://qudt.org/vocab/quantitykind/Match"); + definition.addExactMatch(matchBuilder); + QuantityKind qk = definition.build(); + assertThat(qk.getExactMatches()).hasSize(1); + qk.addExactMatches(matchBuilder.build()); + assertThat(qk.getExactMatches()).hasSize(1); + } + + @Test + void testDeprecated() { + definition.deprecated(true); + QuantityKind qk = definition.build(); + assertThat(qk.isDeprecated()).isTrue(); + } + + @Test + void testGetDimensionVectorFromBroader() { + QuantityKind broader = + QuantityKind.definition("broader").dimensionVectorIri("dimIri").build(); + definition.addBroaderQuantityKind(broader); + QuantityKind qk = definition.build(); + assertThat(qk.getDimensionVectorIri()).isPresent().contains("dimIri"); + } + + @Test + void testToStringWithSymbol() { + definition.symbol("Sym"); + QuantityKind qk = definition.build(); + assertThat(qk.toString()).isEqualTo("Sym"); + } + + @Test + void testToStringWithoutSymbol() { + QuantityKind qk = definition.build(); + assertThat(qk.toString()).isEqualTo("quantityKind:TestKind"); + } + + // Edge cases + @Test + void testNullIriThrowsException() { + assertThatThrownBy(() -> new QuantityKind.Definition((String) null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void testEmptyLabels() { + QuantityKind qk = definition.build(); + assertThat(qk.getLabels()).isEmpty(); + assertThat(qk.getLabelForLanguageTag("en")).isEmpty(); + } + + @Test + void testNoDimensionVector() { + QuantityKind qk = definition.build(); + assertThat(qk.getDimensionVector()).isEmpty(); + } +} diff --git a/qudtlib-model/src/test/java/io/github/qudtlib/model/QuantityValueTests.java b/qudtlib-model/src/test/java/io/github/qudtlib/model/QuantityValueTests.java new file mode 100644 index 0000000..ec1a610 --- /dev/null +++ b/qudtlib-model/src/test/java/io/github/qudtlib/model/QuantityValueTests.java @@ -0,0 +1,249 @@ +package io.github.qudtlib.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import io.github.qudtlib.exception.InconvertibleQuantitiesException; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class QuantityValueTests { + + private Unit meter; + private Unit centimeter; + private Unit kilogram; + private QuantityValue twoMeters; + private QuantityValue fiveCentimeters; + + @BeforeEach + void setUp() { + // Stub Units with conversion logic. Assuming Unit has convert method. + // For testing, we'll create anonymous subclasses or stubs. + DimensionVector dvLength = DimensionVector.builder().length(1).build(); + DimensionVector dvMass = DimensionVector.builder().mass(1).build(); + meter = + Unit.definition("Meter") + .conversionMultiplier(BigDecimal.ONE) + .symbol("m") + .dimensionVectorIri(dvLength.getDimensionVectorIri()) + .build(); + centimeter = + Unit.definition("CentiMeter") + .conversionMultiplier(new BigDecimal("0.01")) + .dimensionVectorIri(dvLength.getDimensionVectorIri()) + .scalingOf(meter) + .symbol("cm") + .build(); + kilogram = + Unit.definition("KiloGram") + .conversionMultiplier(BigDecimal.ONE) + .dimensionVectorIri(dvMass.getDimensionVectorIri()) + .symbol("kg") + .build(); + twoMeters = meter.quantityValue(2); + fiveCentimeters = centimeter.quantityValue(5); + } + + @Test + void testConstructorAndGetters() { + QuantityValue qv = new QuantityValue(BigDecimal.TEN, meter); + assertThat(qv.getValue()).isEqualTo(BigDecimal.TEN); + assertThat(qv.getUnit()).isEqualTo(meter); + } + + @Test + void testEqualsAndHashCode() { + QuantityValue qv1 = new QuantityValue(BigDecimal.TEN, meter); + QuantityValue qv2 = new QuantityValue(BigDecimal.TEN, meter); + QuantityValue qv3 = new QuantityValue(BigDecimal.ONE, meter); + assertThat(qv1).isEqualTo(qv2); + assertThat(qv1.hashCode()).isEqualTo(qv2.hashCode()); + assertThat(qv1).isNotEqualTo(qv3); + } + + @Test + void testConvert() throws InconvertibleQuantitiesException { + QuantityValue converted = twoMeters.convert(centimeter); + assertThat(converted.getValue()).isEqualByComparingTo(new BigDecimal("200")); + assertThat(converted.getUnit()).isEqualTo(centimeter); + } + + @Test + void testConvertWithQuantityKind() throws InconvertibleQuantitiesException { + QuantityKind length = QuantityKind.definition("Length").build(); + QuantityValue converted = twoMeters.convert(centimeter, length); + assertThat(converted.getValue()).isEqualByComparingTo(new BigDecimal("200")); + } + + @Test + void testConvertInconvertibleThrowsException() { + Unit incompatibleUnit = kilogram; // Different dimension + assertThatThrownBy(() -> twoMeters.convert(incompatibleUnit)) + .isInstanceOf(InconvertibleQuantitiesException.class); + } + + @Test + void testAdd() { + QuantityValue result = twoMeters.add(fiveCentimeters); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("2.05")); + assertThat(result.getUnit()).isEqualTo(meter); + } + + @Test + void testAddWithMathContext() { + MathContext mc = new MathContext(2, RoundingMode.HALF_UP); + QuantityValue result = twoMeters.add(fiveCentimeters, mc); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("2.1")); // Rounded + } + + @Test + void testSubtract() { + QuantityValue result = twoMeters.subtract(fiveCentimeters); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("1.95")); + } + + @Test + void testMultiply() { + QuantityValue multiplicand = new QuantityValue(new BigDecimal("3"), meter); + QuantityValue result = twoMeters.multiply(multiplicand); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("6")); + // Note: Units would become m^2, but since QuantityValue doesn't handle unit multiplication, + // assume same unit for test + } + + @Test + void testDivide() { + QuantityValue divisor = new QuantityValue(new BigDecimal("2"), meter); + QuantityValue result = twoMeters.divide(divisor); + assertThat(result.getValue()).isEqualByComparingTo(BigDecimal.ONE); + } + + @Test + void testDivideWithScaleAndRounding() { + QuantityValue divisor = new QuantityValue(new BigDecimal("3"), meter); + QuantityValue result = twoMeters.divide(divisor, 2, RoundingMode.HALF_UP); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("0.67")); + } + + @Test + void testPow() { + QuantityValue result = twoMeters.pow(2); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("4")); + } + + @Test + void testSqrt() { + MathContext mc = MathContext.DECIMAL64; + QuantityValue fourMeters = new QuantityValue(new BigDecimal("4"), meter); + QuantityValue result = fourMeters.sqrt(mc); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("2")); + } + + @Test + void testAbs() { + QuantityValue negative = new QuantityValue(new BigDecimal("-2"), meter); + QuantityValue result = negative.abs(); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("2")); + } + + @Test + void testNegate() { + QuantityValue result = twoMeters.negate(); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("-2")); + } + + @Test + void testMinAndMax() { + assertThat(twoMeters.min(fiveCentimeters).getValue()) + .isEqualByComparingTo(new BigDecimal("0.05")); // Converted + assertThat(twoMeters.max(fiveCentimeters).getValue()) + .isEqualByComparingTo(new BigDecimal("2")); + } + + @Test + void testRemainder() { + QuantityValue divisor = new QuantityValue(new BigDecimal("1.5"), meter); + QuantityValue result = twoMeters.remainder(divisor); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("0.5")); + } + + @Test + void testDivideAndRemainder() { + QuantityValue divisor = new QuantityValue(new BigDecimal("1.5"), meter); + QuantityValue[] results = twoMeters.divideAndRemainder(divisor); + assertThat(results[0].getValue()).isEqualByComparingTo(new BigDecimal("1")); + assertThat(results[1].getValue()).isEqualByComparingTo(new BigDecimal("0.5")); + } + + @Test + void testSignum() { + assertThat(twoMeters.signum()).isEqualTo(1); + assertThat(new QuantityValue(BigDecimal.ZERO, meter).signum()).isEqualTo(0); + assertThat(new QuantityValue(new BigDecimal("-1"), meter).signum()).isEqualTo(-1); + } + + @Test + void testScaleByPowerOfTen() { + QuantityValue result = twoMeters.scaleByPowerOfTen(2); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("200")); + } + + @Test + void testSetScale() { + QuantityValue result = + new QuantityValue(new BigDecimal("2.12345"), meter) + .setScale(2, RoundingMode.HALF_UP); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("2.12")); + } + + @Test + void testStripTrailingZeros() { + QuantityValue result = + new QuantityValue(new BigDecimal("2.000"), meter).stripTrailingZeros(); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("2")); + } + + @Test + void testMovePointLeftAndRight() { + assertThat(twoMeters.movePointLeft(1).getValue()) + .isEqualByComparingTo(new BigDecimal("0.2")); + assertThat(twoMeters.movePointRight(1).getValue()) + .isEqualByComparingTo(new BigDecimal("20")); + } + + @Test + void testCompareTo() { + assertThat(twoMeters.compareTo(fiveCentimeters)).isPositive(); + assertThat(fiveCentimeters.compareTo(twoMeters)).isNegative(); + assertThat(twoMeters.compareTo(new QuantityValue(new BigDecimal("2"), meter))).isZero(); + } + + @Test + void testToString() { + assertThat(twoMeters.toString()).isEqualTo("2 m"); + } + + // Edge cases + @Test + void testZeroValueOperations() { + QuantityValue zero = new QuantityValue(BigDecimal.ZERO, meter); + assertThat(zero.add(twoMeters)).isEqualTo(twoMeters); + assertThat(zero.divide(twoMeters, RoundingMode.HALF_UP)).isEqualTo(meter.quantityValue(0)); + } + + @Test + void testNegativeValues() { + QuantityValue negative = new QuantityValue(new BigDecimal("-2"), meter); + assertThat(negative.add(twoMeters).getValue()).isZero(); + } + + @Test + void testPrecisionLossWithMathContext() { + MathContext mc = new MathContext(1); + QuantityValue result = twoMeters.divide(new QuantityValue(new BigDecimal("3"), meter), mc); + assertThat(result.getValue()).isEqualByComparingTo(new BigDecimal("0.7")); // Rounded + } +}