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
+ }
+}