From f9eeaea9f12bea58273e363eb57768521f3d7268 Mon Sep 17 00:00:00 2001 From: Florian Kleedorfer Date: Fri, 14 Oct 2022 11:33:13 +0200 Subject: [PATCH] Add TS code generation and Refactor Code (#16) * Extracts some code used for java/js generation into new module * Prepares a first version of the typescript generator (incorrect result for now). * Adds qudtlib-js as a submodule * Fixes typo in package name * Refactors constants generation * Generate units, quantitykinds and prefixes in allunits.ts * Fixes Bug in factor unit queries: * overspecification was possible * simple units could not be selected using exponent 1 * Both issues are fixed and checked with unit tests. * Improves factor unit matching algorithm * Simplifies matched check * Removes unused code * Removes unnecessary isMatched check * Removes unnecessary method * Enables exact and lenient matching modes * Refactors unit/qk/prefix access methods to return optional * Adds 'required' unit/qk/prefix access methods that throw NotFoundExceptions * Adds tests for search/maching modes * Removes cluttering convenience methods * Refactors Qudt.java and tests slightly * Add scalingOf triples if missing in QUDT * Updates submodule to use latest qudtlib-js/main --- .gitmodules | 4 + pom.xml | 4 +- qudtlib-common-codegen/pom.xml | 30 ++ .../common/BigDecimalFormatterFactory.java | 39 ++ .../io/github/qudtlib/common/CodeGen.java | 68 +++ .../safenames/NameCollisionException.java | 10 + .../common/safenames/SafeStringMapper.java | 42 ++ .../io/github/qudtlib/constgen/Constant.java | 31 ++ .../io/github/qudtlib/common/CodeGenTest.java | 19 + .../pom.xml | 2 +- .../io/github/qudtlib}/common/RdfOps.java | 4 +- qudtlib-constants-gen/pom.xml | 9 +- .../io/github/qudtlib/constgen/Constant.java | 31 -- .../qudtlib/constgen/ConstantsGenerator.java | 46 +- .../src/main/resources/template/constants.ftl | 2 +- qudtlib-data-gen/pom.xml | 2 +- .../io/github/qudtlib/data/DataGenerator.java | 5 +- .../main/resources/additional-scalings.ttl | 5 + .../src/main/resources/si-base-units.ttl | 2 +- .../org/example/qudlib/QudtlibExample.java | 17 +- qudtlib-hardcoded-model-gen/pom.xml | 7 +- .../qudtlib/HardcodedModelGenerator.java | 61 +-- .../resources/template/InitializerImpl.ftl | 10 +- .../github/qudtlib/model/InitializerImpl.java | 4 +- qudtlib-js | 1 + qudtlib-js-gen/pom.xml | 56 +++ .../HardcodedTypescriptModelGenerator.java | 106 +++++ .../src/main/resources/template/units.ts.ftl | 170 +++++++ .../src/main/java/io/github/qudtlib/Qudt.java | 438 +++++++----------- .../qudtlib/model/DerivedUnitSearchMode.java | 30 ++ .../io/github/qudtlib/model/FactorUnit.java | 43 +- .../github/qudtlib/model/FactorUnitMatch.java | 33 +- .../qudtlib/model/FactorUnitMatchingMode.java | 12 + .../qudtlib/model/FactorUnitSelection.java | 86 ++-- .../qudtlib/model/FactorUnitSelector.java | 73 ++- .../java/io/github/qudtlib/model/Prefix.java | 4 +- .../io/github/qudtlib/model/Quantity.java | 2 +- .../io/github/qudtlib/model/QuantityKind.java | 48 +- .../io/github/qudtlib/model/ScaleFactor.java | 2 +- .../java/io/github/qudtlib/model/Unit.java | 133 ++++-- .../io/github/qudtlib/DerivedUnitTests.java | 247 ++++++++-- .../java/io/github/qudtlib/QudtTests.java | 271 ++++++++--- 42 files changed, 1487 insertions(+), 722 deletions(-) create mode 100644 .gitmodules create mode 100644 qudtlib-common-codegen/pom.xml create mode 100644 qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/BigDecimalFormatterFactory.java create mode 100644 qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/CodeGen.java create mode 100644 qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/NameCollisionException.java create mode 100644 qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/SafeStringMapper.java create mode 100644 qudtlib-common-codegen/src/main/java/io/github/qudtlib/constgen/Constant.java create mode 100644 qudtlib-common-codegen/src/test/java/io/github/qudtlib/common/CodeGenTest.java rename {qudtlib-common => qudtlib-common-rdf}/pom.xml (96%) rename {qudtlib-common/src/main/java/io/github/qudlib => qudtlib-common-rdf/src/main/java/io/github/qudtlib}/common/RdfOps.java (98%) delete mode 100644 qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/Constant.java create mode 100644 qudtlib-data-gen/src/main/resources/additional-scalings.ttl create mode 160000 qudtlib-js create mode 100644 qudtlib-js-gen/pom.xml create mode 100644 qudtlib-js-gen/src/main/java/io/github/qudtlib/HardcodedTypescriptModelGenerator.java create mode 100644 qudtlib-js-gen/src/main/resources/template/units.ts.ftl create mode 100644 qudtlib-model/src/main/java/io/github/qudtlib/model/DerivedUnitSearchMode.java create mode 100644 qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatchingMode.java diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..92f0f1b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "qudtlib-js"] + path = qudtlib-js + url = https://github.com/qudtlib/qudtlib-js.git + branch = main \ No newline at end of file diff --git a/pom.xml b/pom.xml index d6c497c..61e906a 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,8 @@ qudtlib-ingest-qudt qudtlib-data-gen qudtlib-vocab - qudtlib-common + qudtlib-common-rdf + qudtlib-common-codegen qudtlib-data qudtlib-init-rdf qudtlib-main @@ -39,6 +40,7 @@ qudtlib-init-hardcoded qudtlib-test qudtlib-example + qudtlib-js-gen diff --git a/qudtlib-common-codegen/pom.xml b/qudtlib-common-codegen/pom.xml new file mode 100644 index 0000000..28f4ba8 --- /dev/null +++ b/qudtlib-common-codegen/pom.xml @@ -0,0 +1,30 @@ + + + + qudtlib-java + io.github.qudtlib + 1.1-SNAPSHOT + + 4.0.0 + + qudtlib-common-codegen + + + + org.freemarker + freemarker + + + org.junit.jupiter + junit-jupiter + + + io.github.qudtlib + qudtlib-model + ${project.version} + + + + \ No newline at end of file diff --git a/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/BigDecimalFormatterFactory.java b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/BigDecimalFormatterFactory.java new file mode 100644 index 0000000..5b74b5e --- /dev/null +++ b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/BigDecimalFormatterFactory.java @@ -0,0 +1,39 @@ +package io.github.qudtlib.common; + +import freemarker.core.Environment; +import freemarker.core.TemplateNumberFormat; +import freemarker.core.TemplateNumberFormatFactory; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNumberModel; +import java.math.BigDecimal; +import java.util.Locale; + +public class BigDecimalFormatterFactory extends TemplateNumberFormatFactory { + @Override + public TemplateNumberFormat get(String s, Locale locale, Environment environment) { + return new TemplateNumberFormat() { + @Override + public String formatToPlainText(TemplateNumberModel templateNumberModel) + throws TemplateModelException { + Number num = templateNumberModel.getAsNumber(); + if (!(num instanceof BigDecimal)) { + throw new IllegalArgumentException( + "This formatter can only be used with BigDecimals but was asked to format a " + + num.getClass()); + } + BigDecimal bd = (BigDecimal) templateNumberModel.getAsNumber(); + return bd.toString(); + } + + @Override + public boolean isLocaleBound() { + return false; + } + + @Override + public String getDescription() { + return "Number format for BigDecimal using BigDecimal.toString()"; + } + }; + } +} diff --git a/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/CodeGen.java b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/CodeGen.java new file mode 100644 index 0000000..3d2338f --- /dev/null +++ b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/CodeGen.java @@ -0,0 +1,68 @@ +package io.github.qudtlib.common; + +import freemarker.core.Environment; +import freemarker.template.*; +import io.github.qudtlib.common.safenames.SafeStringMapper; +import io.github.qudtlib.constgen.Constant; +import io.github.qudtlib.model.LangString; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; + +public abstract class CodeGen { + + public static Configuration getFreemarkerConfiguration() { + return getFreemarkerConfiguration(Thread.currentThread().getContextClassLoader()); + } + + public static Configuration getFreemarkerConfiguration(ClassLoader classLoaderForTemplate) { + Objects.requireNonNull(classLoaderForTemplate); + Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); + cfg.setClassLoaderForTemplateLoading(classLoaderForTemplate, "/"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + cfg.setCustomNumberFormats(Map.of("toString", new BigDecimalFormatterFactory())); + return cfg; + } + + public static void generateFileFromTemplate( + Configuration config, + String templateFileClasspathUrl, + Map templateVars, + File outFile) + throws IOException, TemplateException { + Template template = config.getTemplate(templateFileClasspathUrl); + FileWriter out = new FileWriter(outFile, StandardCharsets.UTF_8); + Environment env = template.createProcessingEnvironment(templateVars, out); + env.setOutputEncoding(StandardCharsets.UTF_8.toString()); + env.process(); + } + + public static Constant makeConstant( + Set labels, String iri, SafeStringMapper constantNameMapper) { + String label = labels.stream().findFirst().map(LangString::getString).orElse("[no label]"); + String iriLocalName = iri.replaceAll("^.+[/|#]", ""); + String codeConstantName = constantNameMapper.applyMapping(iriLocalName); + return new Constant(codeConstantName, iriLocalName, label); + } + + public static SafeStringMapper javaConstantMapper() { + return new SafeStringMapper(javaConstantNameMapper); + } + + static final Function javaConstantNameMapper = + constName -> { + Pattern startPattern = Pattern.compile("^[$€a-zA-Z_]"); + if (!startPattern.matcher(constName).lookingAt()) { + constName = "_" + constName; + } + constName = constName.replaceAll("-", "__"); + return constName; + }; +} diff --git a/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/NameCollisionException.java b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/NameCollisionException.java new file mode 100644 index 0000000..c400d60 --- /dev/null +++ b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/NameCollisionException.java @@ -0,0 +1,10 @@ +package io.github.qudtlib.common.safenames; + +public class NameCollisionException extends RuntimeException { + public NameCollisionException(String collidingInput, String input, String mappedOutput) { + super( + String.format( + "IdentifierCollision detected! Input String '%s' clashes with previously established Mapping '%s' => '%s'", + collidingInput, input, mappedOutput)); + } +} diff --git a/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/SafeStringMapper.java b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/SafeStringMapper.java new file mode 100644 index 0000000..3ede00c --- /dev/null +++ b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/common/safenames/SafeStringMapper.java @@ -0,0 +1,42 @@ +package io.github.qudtlib.common.safenames; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Applies the provided deterministic mappingFunction provided in the + * constructor, keeping track of the mappings produced. If two different inputs result in the same + * output, a {@link NameCollisionException} is thrown. + */ +public class SafeStringMapper { + private final Map outputToInput = new ConcurrentHashMap<>(); + private final Function mappingFunction; + + public SafeStringMapper(Function mappingFunction) { + this.mappingFunction = mappingFunction; + } + + /** + * Applies the specified mapping, throwing an exception if a previously produced output is + * reproduced with a different input. + * + * @param input the value to map safely + * @return the mapped value + */ + public String applyMapping(String input) { + Objects.requireNonNull(input); + final String output = mappingFunction.apply(input); + outputToInput.merge( + output, + input, + (previousInput, currentInput) -> { + if (previousInput.equals(currentInput)) { + return input; + } + throw new NameCollisionException(input, previousInput, output); + }); + return output; + } +} diff --git a/qudtlib-common-codegen/src/main/java/io/github/qudtlib/constgen/Constant.java b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/constgen/Constant.java new file mode 100644 index 0000000..c4e0f9a --- /dev/null +++ b/qudtlib-common-codegen/src/main/java/io/github/qudtlib/constgen/Constant.java @@ -0,0 +1,31 @@ +package io.github.qudtlib.constgen; + +/** + * Class representing constant names/labels/local names in IRIs for generating RDF vocabularies. + * + * @author Florian Kleedorfer + * @since 1.0 + */ +public class Constant { + private final String codeConstantName; + private final String iriLocalname; + private final String label; + + public Constant(String codeConstantName, String iriLocalname, String label) { + this.codeConstantName = codeConstantName; + this.iriLocalname = iriLocalname; + this.label = label; + } + + public String getCodeConstantName() { + return codeConstantName; + } + + public String getIriLocalname() { + return iriLocalname; + } + + public String getLabel() { + return label; + } +} diff --git a/qudtlib-common-codegen/src/test/java/io/github/qudtlib/common/CodeGenTest.java b/qudtlib-common-codegen/src/test/java/io/github/qudtlib/common/CodeGenTest.java new file mode 100644 index 0000000..61b39db --- /dev/null +++ b/qudtlib-common-codegen/src/test/java/io/github/qudtlib/common/CodeGenTest.java @@ -0,0 +1,19 @@ +package io.github.qudtlib.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.github.qudtlib.common.safenames.NameCollisionException; +import io.github.qudtlib.common.safenames.SafeStringMapper; +import org.junit.jupiter.api.Test; + +public class CodeGenTest { + @Test + public void testConstantMapper() { + SafeStringMapper mapper = CodeGen.javaConstantMapper(); + assertEquals("_1constant", mapper.applyMapping("1constant")); + assertEquals("_1constant", mapper.applyMapping("1constant")); + assertEquals("_2constant", mapper.applyMapping("2constant")); + assertThrows(NameCollisionException.class, () -> mapper.applyMapping("_1constant")); + } +} diff --git a/qudtlib-common/pom.xml b/qudtlib-common-rdf/pom.xml similarity index 96% rename from qudtlib-common/pom.xml rename to qudtlib-common-rdf/pom.xml index 3855bc5..7ea636e 100644 --- a/qudtlib-common/pom.xml +++ b/qudtlib-common-rdf/pom.xml @@ -7,7 +7,7 @@ 4.0.0 - qudtlib-common + qudtlib-common-rdf diff --git a/qudtlib-common/src/main/java/io/github/qudlib/common/RdfOps.java b/qudtlib-common-rdf/src/main/java/io/github/qudtlib/common/RdfOps.java similarity index 98% rename from qudtlib-common/src/main/java/io/github/qudlib/common/RdfOps.java rename to qudtlib-common-rdf/src/main/java/io/github/qudtlib/common/RdfOps.java index 0937eec..3da0f65 100644 --- a/qudtlib-common/src/main/java/io/github/qudlib/common/RdfOps.java +++ b/qudtlib-common-rdf/src/main/java/io/github/qudtlib/common/RdfOps.java @@ -1,4 +1,4 @@ -package io.github.qudlib.common; +package io.github.qudtlib.common; import java.io.File; import java.io.FileOutputStream; @@ -26,7 +26,7 @@ * @author Florian Kleedorfer * @since 1.0 */ -public class RdfOps { +public abstract class RdfOps { private static final boolean DEBUG = false; diff --git a/qudtlib-constants-gen/pom.xml b/qudtlib-constants-gen/pom.xml index b7121b3..b768fbf 100644 --- a/qudtlib-constants-gen/pom.xml +++ b/qudtlib-constants-gen/pom.xml @@ -12,17 +12,18 @@ io.github.qudtlib - qudtlib-common + qudtlib-common-rdf ${project.version} io.github.qudtlib - qudtlib-data + qudtlib-common-codegen ${project.version} - org.freemarker - freemarker + io.github.qudtlib + qudtlib-data + ${project.version} diff --git a/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/Constant.java b/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/Constant.java deleted file mode 100644 index 30232cc..0000000 --- a/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/Constant.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.qudtlib.constgen; - -/** - * Class representing constant names/labels/local names in IRIs for generating RDF vocabularies. - * - * @author Florian Kleedorfer - * @since 1.0 - */ -public class Constant { - private final String javaName; - private final String localName; - private final String label; - - public Constant(String javaName, String localName, String label) { - this.javaName = javaName; - this.localName = localName; - this.label = label; - } - - public String getJavaName() { - return javaName; - } - - public String getLocalName() { - return localName; - } - - public String getLabel() { - return label; - } -} diff --git a/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/ConstantsGenerator.java b/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/ConstantsGenerator.java index 9a23db2..1f27bf1 100644 --- a/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/ConstantsGenerator.java +++ b/qudtlib-constants-gen/src/main/java/io/github/qudtlib/constgen/ConstantsGenerator.java @@ -1,17 +1,17 @@ package io.github.qudtlib.constgen; -import freemarker.core.Environment; import freemarker.template.Configuration; -import freemarker.template.Template; import freemarker.template.TemplateException; -import freemarker.template.TemplateExceptionHandler; -import io.github.qudlib.common.RdfOps; +import io.github.qudtlib.common.CodeGen; +import io.github.qudtlib.common.RdfOps; +import io.github.qudtlib.common.safenames.SafeStringMapper; import java.io.File; -import java.io.FileWriter; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.query.TupleQueryResult; @@ -41,6 +41,8 @@ public class ConstantsGenerator { // template private static final String TEMPLATE_FILE = "template/constants.ftl"; + private final SafeStringMapper javaConstantNameMapper = CodeGen.javaConstantMapper(); + public ConstantsGenerator(Path outputDir) { this.outputDir = outputDir; } @@ -64,20 +66,12 @@ public static void main(String[] args) { } public void generate() throws IOException, TemplateException { - Configuration cfg = getFreemarkerConfiguration(); + Configuration cfg = CodeGen.getFreemarkerConfiguration(); generateUnitConstants(cfg); generateQuantityKindConstants(cfg); generatePrefixConstants(cfg); } - private Configuration getFreemarkerConfiguration() { - Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); - cfg.setClassLoaderForTemplateLoading(ConstantsGenerator.class.getClassLoader(), "/"); - cfg.setDefaultEncoding("UTF-8"); - cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - return cfg; - } - private void generatePrefixConstants(Configuration config) throws IOException, TemplateException { Map templateVars = getConstantNamesByQuery(QUERY_PREFIXES, DATA_PREFIXES); @@ -107,7 +101,7 @@ private Map getConstantNamesByQuery(String queryFile, String dat while (result.hasNext()) { BindingSet bindings = result.next(); String constName = - makeSafeJavaIdentifierFirstChar( + javaConstantNameMapper.applyMapping( bindings.getValue("constName").stringValue()); String localName = bindings.getValue("localName").stringValue(); String label = @@ -123,14 +117,6 @@ private Map getConstantNamesByQuery(String queryFile, String dat return templateVars; } - private String makeSafeJavaIdentifierFirstChar(String constName) { - Pattern startPattern = Pattern.compile("^[$€a-zA-Z_]"); - if (!startPattern.matcher(constName).lookingAt()) { - return "_" + constName; - } - return constName; - } - private void generateJavaFile( Configuration config, Map templateVars, String type, String typePlural) throws IOException, TemplateException { @@ -149,12 +135,8 @@ private void generateJavaFile( templateVars.put("package", DESTINATION_PACKAGE); templateVars.put( "valueFactory", - type.substring(0, 1).toLowerCase() + type.substring(1) + "FromLocalname"); - Template template = config.getTemplate(TEMPLATE_FILE); - FileWriter out = - new FileWriter(new File(packageFile, typePlural + ".java"), StandardCharsets.UTF_8); - Environment env = template.createProcessingEnvironment(templateVars, out); - env.setOutputEncoding(StandardCharsets.UTF_8.toString()); - env.process(); + type.substring(0, 1).toLowerCase() + type.substring(1) + "FromLocalnameRequired"); + File outFile = new File(packageFile, typePlural + ".java"); + CodeGen.generateFileFromTemplate(config, TEMPLATE_FILE, templateVars, outFile); } } diff --git a/qudtlib-constants-gen/src/main/resources/template/constants.ftl b/qudtlib-constants-gen/src/main/resources/template/constants.ftl index be83214..bbe9f43 100644 --- a/qudtlib-constants-gen/src/main/resources/template/constants.ftl +++ b/qudtlib-constants-gen/src/main/resources/template/constants.ftl @@ -15,7 +15,7 @@ public abstract class ${typePlural} { <#list constants as constant> /** ${constant.label} */ - public static final ${type} ${constant.javaName} = ${valueFactory}("${constant.localName}"); + public static final ${type} ${constant.codeConstantName} = ${valueFactory}("${constant.iriLocalname}"); diff --git a/qudtlib-data-gen/pom.xml b/qudtlib-data-gen/pom.xml index 3639b14..710ceb8 100644 --- a/qudtlib-data-gen/pom.xml +++ b/qudtlib-data-gen/pom.xml @@ -21,7 +21,7 @@ io.github.qudtlib - qudtlib-common + qudtlib-common-rdf ${project.version} diff --git a/qudtlib-data-gen/src/main/java/io/github/qudtlib/data/DataGenerator.java b/qudtlib-data-gen/src/main/java/io/github/qudtlib/data/DataGenerator.java index f16fe65..5750d49 100644 --- a/qudtlib-data-gen/src/main/java/io/github/qudtlib/data/DataGenerator.java +++ b/qudtlib-data-gen/src/main/java/io/github/qudtlib/data/DataGenerator.java @@ -1,6 +1,6 @@ package io.github.qudtlib.data; -import io.github.qudlib.common.RdfOps; +import io.github.qudtlib.common.RdfOps; import io.github.qudtlib.vocab.QUDT; import io.github.qudtlib.vocab.QUDTX; import java.io.File; @@ -43,6 +43,7 @@ public class DataGenerator { private static final String REMOVE_KILOGM_SCALINGS_QUERY = "remove-kiloGM-scalings.rq"; // additional data private static final String SI_BASE_UNITS_DATA = "si-base-units.ttl"; + private static final String ADDITIONAL_SCALINGS_DATA = "additional-scalings.ttl"; private static final String UNITS_EXPECTED_DATA = "tmpExpected/qudt-unit.ttl"; private static final boolean DEBUG = false; @@ -111,6 +112,8 @@ void processUnits() { RdfOps.updateDataUsingQuery(inputCon, REMOVE_KILOGM_SCALINGS_QUERY); // add SI base units RdfOps.addStatementsFromFile(outputCon, SI_BASE_UNITS_DATA); + // add missing scalings + RdfOps.addStatementsFromFile(outputCon, ADDITIONAL_SCALINGS_DATA); // put result in OUTPUT repo RdfOps.copyData(inputCon, outputCon); // add prefixes to INPUT repo (cannot be in output, but is required for queries!) diff --git a/qudtlib-data-gen/src/main/resources/additional-scalings.ttl b/qudtlib-data-gen/src/main/resources/additional-scalings.ttl new file mode 100644 index 0000000..bc8d67b --- /dev/null +++ b/qudtlib-data-gen/src/main/resources/additional-scalings.ttl @@ -0,0 +1,5 @@ +@prefix qudt: . +@prefix unit: . + +unit:TON_Metric qudt:isScalingOf unit:GM. +unit:TONNE qudt:isScalingOf unit:GM. diff --git a/qudtlib-data-gen/src/main/resources/si-base-units.ttl b/qudtlib-data-gen/src/main/resources/si-base-units.ttl index bc5e3f2..1440aba 100644 --- a/qudtlib-data-gen/src/main/resources/si-base-units.ttl +++ b/qudtlib-data-gen/src/main/resources/si-base-units.ttl @@ -143,4 +143,4 @@ unit:KAT qudt:factorUnit [ qudt:exponent 1 ; qudt:unit unit:MOL ] ; qudt:factorUnit [ qudt:exponent -1 ; - qudt:unit unit:SEC ] . + qudt:unit unit:SEC ] . \ No newline at end of file diff --git a/qudtlib-example/src/main/java/org/example/qudlib/QudtlibExample.java b/qudtlib-example/src/main/java/org/example/qudlib/QudtlibExample.java index 03fb30e..447d5de 100644 --- a/qudtlib-example/src/main/java/org/example/qudlib/QudtlibExample.java +++ b/qudtlib-example/src/main/java/org/example/qudlib/QudtlibExample.java @@ -1,6 +1,7 @@ package org.example.qudlib; import io.github.qudtlib.Qudt; +import io.github.qudtlib.model.DerivedUnitSearchMode; import io.github.qudtlib.model.FactorUnit; import io.github.qudtlib.model.QuantityValue; import io.github.qudtlib.model.Unit; @@ -24,10 +25,14 @@ public static void main(String[] args) { System.out.println("---"); System.out.println("finding unit for factors: m, kg, and s^-2:"); Set myUnits = - Qudt.derivedUnit( - Qudt.Units.M, 1, - Qudt.Units.KiloGM, 1, - Qudt.Units.SEC, -2); + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.Units.M, + 1, + Qudt.Units.KiloGM, + 1, + Qudt.Units.SEC, + -2); for (Unit unit : myUnits) { System.out.println("unit : " + unit); } @@ -58,8 +63,8 @@ public static void main(String[] args) { System.out.println("---"); System.out.println( "Which units are applicable for " + Qudt.QuantityKinds.PressureRatio + "?"); - for (String unitIri : Qudt.QuantityKinds.PressureRatio.getApplicableUnits()) { - Unit unit = Qudt.unit(unitIri); + for (String unitIri : Qudt.QuantityKinds.PressureRatio.getApplicableUnitIris()) { + Unit unit = Qudt.unitRequired(unitIri); System.out.println(" " + unit + " (" + unit.getIri() + ")"); } System.out.println("---"); diff --git a/qudtlib-hardcoded-model-gen/pom.xml b/qudtlib-hardcoded-model-gen/pom.xml index 75626e4..dc9d019 100644 --- a/qudtlib-hardcoded-model-gen/pom.xml +++ b/qudtlib-hardcoded-model-gen/pom.xml @@ -16,7 +16,12 @@ io.github.qudtlib - qudtlib-common + qudtlib-common-rdf + ${project.version} + + + io.github.qudtlib + qudtlib-common-codegen ${project.version} diff --git a/qudtlib-hardcoded-model-gen/src/main/java/io/github/qudtlib/HardcodedModelGenerator.java b/qudtlib-hardcoded-model-gen/src/main/java/io/github/qudtlib/HardcodedModelGenerator.java index a0c9224..69316ce 100644 --- a/qudtlib-hardcoded-model-gen/src/main/java/io/github/qudtlib/HardcodedModelGenerator.java +++ b/qudtlib-hardcoded-model-gen/src/main/java/io/github/qudtlib/HardcodedModelGenerator.java @@ -1,18 +1,13 @@ package io.github.qudtlib; -import freemarker.core.Environment; -import freemarker.core.TemplateNumberFormat; -import freemarker.core.TemplateNumberFormatFactory; -import freemarker.template.*; -import io.github.qudlib.common.RdfOps; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import io.github.qudtlib.common.CodeGen; +import io.github.qudtlib.common.RdfOps; import java.io.File; -import java.io.FileWriter; import java.io.IOException; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import java.util.TreeMap; import java.util.regex.Pattern; @@ -60,19 +55,10 @@ public static void main(String[] args) { } public void generate() throws IOException, TemplateException { - Configuration cfg = getFreemarkerConfiguration(); + Configuration cfg = CodeGen.getFreemarkerConfiguration(); generateInitializer(cfg); } - private Configuration getFreemarkerConfiguration() { - Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); - cfg.setClassLoaderForTemplateLoading(HardcodedModelGenerator.class.getClassLoader(), "/"); - cfg.setDefaultEncoding("UTF-8"); - cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - cfg.setCustomNumberFormats(Map.of("toString", new BigDecimalFormatterFactory())); - return cfg; - } - private void generateInitializer(Configuration config) throws IOException, TemplateException { Map templateVars = new HashMap<>(); templateVars.put("prefixes", new TreeMap<>(Qudt.getPrefixesMap())); @@ -94,40 +80,7 @@ private void generateJavaFile(Configuration config, Map template } RdfOps.message("output dir: " + packageFile.getAbsolutePath()); templateVars.put("package", DESTINATION_PACKAGE); - Template template = config.getTemplate(TEMPLATE_FILE); - FileWriter out = new FileWriter(new File(packageFile, FILENAME), StandardCharsets.UTF_8); - Environment env = template.createProcessingEnvironment(templateVars, out); - env.setOutputEncoding(StandardCharsets.UTF_8.toString()); - env.process(); - } - - private static class BigDecimalFormatterFactory extends TemplateNumberFormatFactory { - @Override - public TemplateNumberFormat get(String s, Locale locale, Environment environment) { - return new TemplateNumberFormat() { - @Override - public String formatToPlainText(TemplateNumberModel templateNumberModel) - throws TemplateModelException { - Number num = templateNumberModel.getAsNumber(); - if (!(num instanceof BigDecimal)) { - throw new IllegalArgumentException( - "This formatter can only be used with BigDecimals but was asked to format a " - + num.getClass()); - } - BigDecimal bd = (BigDecimal) templateNumberModel.getAsNumber(); - return bd.toString(); - } - - @Override - public boolean isLocaleBound() { - return false; - } - - @Override - public String getDescription() { - return "Number format for BigDecimal using BigDecimal.toString()"; - } - }; - } + File outFile = new File(packageFile, FILENAME); + CodeGen.generateFileFromTemplate(config, TEMPLATE_FILE, templateVars, outFile); } } diff --git a/qudtlib-hardcoded-model-gen/src/main/resources/template/InitializerImpl.ftl b/qudtlib-hardcoded-model-gen/src/main/resources/template/InitializerImpl.ftl index 1e69b2b..8dcf4e3 100644 --- a/qudtlib-hardcoded-model-gen/src/main/resources/template/InitializerImpl.ftl +++ b/qudtlib-hardcoded-model-gen/src/main/resources/template/InitializerImpl.ftl @@ -72,15 +72,15 @@ public class InitializerImpl implements Initializer { <#list quantityKinds as iri, quantityKind> private static void addQuantityKind${iri?index?c}(Map quantityKinds){ - QuantityKind quantityKind = new QuantityKind(${q(iri)}, ${optStr(quantityKind.dimensionVector)}, ${optStr(quantityKind.symbol)}); + QuantityKind quantityKind = new QuantityKind(${q(iri)}, ${optStr(quantityKind.dimensionVectorIri)}, ${optStr(quantityKind.symbol)}); <#list quantityKind.labels as label> quantityKind.addLabel(new LangString(${q(label.string)}, ${optStr(label.languageTag)})); - <#list quantityKind.applicableUnits as unitIri> - quantityKind.addApplicableUnit(${q(unitIri)}); + <#list quantityKind.applicableUnitIris as unitIri> + quantityKind.addApplicableUnitIri(${q(unitIri)}); - <#list quantityKind.broaderQuantityKinds as qkIri> - quantityKind.addBroaderQuantityKind(${q(qkIri)}); + <#list quantityKind.broaderQuantityKindIris as qkIri> + quantityKind.addBroaderQuantityKindIri(${q(qkIri)}); quantityKinds.put(${q(iri)}, quantityKind); } diff --git a/qudtlib-init-rdf/src/main/java/io/github/qudtlib/model/InitializerImpl.java b/qudtlib-init-rdf/src/main/java/io/github/qudtlib/model/InitializerImpl.java index a91c5dc..baaac72 100644 --- a/qudtlib-init-rdf/src/main/java/io/github/qudtlib/model/InitializerImpl.java +++ b/qudtlib-init-rdf/src/main/java/io/github/qudtlib/model/InitializerImpl.java @@ -140,10 +140,10 @@ public Map loadQuantityKinds() { } if (bs.hasBinding("broaderQuantityKind")) { String val = bs.getValue("broaderQuantityKind").stringValue(); - quantityKind.addBroaderQuantityKind(val); + quantityKind.addBroaderQuantityKindIri(val); } if (bs.hasBinding("applicableUnit")) { - quantityKind.addApplicableUnit( + quantityKind.addApplicableUnitIri( bs.getBinding("applicableUnit").getValue().stringValue()); } } diff --git a/qudtlib-js b/qudtlib-js new file mode 160000 index 0000000..19e5623 --- /dev/null +++ b/qudtlib-js @@ -0,0 +1 @@ +Subproject commit 19e56232aa7aa38c7881457d6ef667b657a44eaa diff --git a/qudtlib-js-gen/pom.xml b/qudtlib-js-gen/pom.xml new file mode 100644 index 0000000..84673c3 --- /dev/null +++ b/qudtlib-js-gen/pom.xml @@ -0,0 +1,56 @@ + + + + qudtlib-java + io.github.qudtlib + 1.1-SNAPSHOT + + 4.0.0 + + qudtlib-js-gen + + + + io.github.qudtlib + qudtlib-init-hardcoded + ${project.version} + + + io.github.qudtlib + qudtlib-main + ${project.version} + + + io.github.qudtlib + qudtlib-common-codegen + ${project.version} + + + + + + org.codehaus.mojo + exec-maven-plugin + + + + java + + prepare-package + + + + false + true + true + io.github.qudtlib.HardcodedTypescriptModelGenerator + + ${project.build.directory}/../../qudtlib-js/allunits/src/ + + + + + + \ No newline at end of file diff --git a/qudtlib-js-gen/src/main/java/io/github/qudtlib/HardcodedTypescriptModelGenerator.java b/qudtlib-js-gen/src/main/java/io/github/qudtlib/HardcodedTypescriptModelGenerator.java new file mode 100644 index 0000000..4760b24 --- /dev/null +++ b/qudtlib-js-gen/src/main/java/io/github/qudtlib/HardcodedTypescriptModelGenerator.java @@ -0,0 +1,106 @@ +package io.github.qudtlib; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import io.github.qudtlib.common.CodeGen; +import io.github.qudtlib.common.safenames.SafeStringMapper; +import io.github.qudtlib.constgen.Constant; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Generates a Typescript package that instantiates all QUDT individuals and relationships needed + * for QUDTLib from hardcoded data, i.e. without the need to process RDF. + * + *

This generator accesses the QUDT model through the 'hardcoded' QUDTLib implementation. + * + * @author Florian Kleedorfer + * @version 1.0 + */ +public class HardcodedTypescriptModelGenerator { + private final Path outputDir; + // output + private static final String FILENAME = "units.ts"; + // template + private static final String TEMPLATE_FILE = "template/units.ts.ftl"; + + public HardcodedTypescriptModelGenerator(Path outputDir) { + this.outputDir = outputDir; + } + + private final SafeStringMapper constantNameMapper = CodeGen.javaConstantMapper(); + + public static void main(String[] args) { + try { + if (args.length == 0) { + throw new IllegalArgumentException("missing argument"); + } + if (args.length > 1) { + throw new IllegalArgumentException(" too many arguments"); + } + String outputDir = args[0]; + HardcodedTypescriptModelGenerator generator = + new HardcodedTypescriptModelGenerator(Path.of(outputDir)); + generator.generate(); + } catch (Exception e) { + System.err.println("\n\n\tusage: HardcodedTypescriptModelGenerator [output-dir]\n\n"); + e.printStackTrace(); + System.exit(1); + } + } + + public void generate() throws IOException, TemplateException { + Configuration cfg = CodeGen.getFreemarkerConfiguration(); + generate(cfg); + } + + private void generate(Configuration config) throws IOException, TemplateException { + Map templateVars = new HashMap<>(); + templateVars.put("prefixes", new TreeMap<>(Qudt.getPrefixesMap())); + templateVars.put("quantityKinds", new TreeMap<>(Qudt.getQuantityKindsMap())); + templateVars.put("units", new TreeMap<>(Qudt.getUnitsMap())); + Set unitConstants = + Qudt.getUnitsMap().values().stream() + .map( + u -> + CodeGen.makeConstant( + u.getLabels(), u.getIri(), this.constantNameMapper)) + .collect(Collectors.toSet()); + Set quantityKindConstants = + Qudt.getQuantityKindsMap().values().stream() + .map( + q -> + CodeGen.makeConstant( + q.getLabels(), q.getIri(), this.constantNameMapper)) + .collect(Collectors.toSet()); + Set prefixConstants = + Qudt.getPrefixesMap().values().stream() + .map( + p -> + CodeGen.makeConstant( + p.getLabels(), p.getIri(), this.constantNameMapper)) + .collect(Collectors.toSet()); + templateVars.put("unitConstants", unitConstants); + templateVars.put("quantityKindConstants", quantityKindConstants); + templateVars.put("prefixConstants", prefixConstants); + generateTypescriptFile(config, templateVars); + } + + private void generateTypescriptFile(Configuration config, Map templateVars) + throws IOException, TemplateException { + System.out.println("Generating " + FILENAME); + File packageFile = outputDir.toFile(); + if (!packageFile.exists()) { + if (!packageFile.mkdirs()) { + throw new IOException( + "Could not create output dir " + packageFile.getAbsolutePath()); + } + } + System.out.println("output dir: " + packageFile.getAbsolutePath()); + File outFile = new File(packageFile, FILENAME); + CodeGen.generateFileFromTemplate(config, TEMPLATE_FILE, templateVars, outFile); + } +} diff --git a/qudtlib-js-gen/src/main/resources/template/units.ts.ftl b/qudtlib-js-gen/src/main/resources/template/units.ts.ftl new file mode 100644 index 0000000..605a8e1 --- /dev/null +++ b/qudtlib-js-gen/src/main/resources/template/units.ts.ftl @@ -0,0 +1,170 @@ +/** + * Code generated by qudtlib-java:qudtlib-js-gen. + */ +import { Decimal } from "decimal.js"; +import { + config, + Unit, + QuantityKind, + Prefix, + LangString, + FactorUnit, + Qudt, +} from "@qudtlib/core"; + +export * from "@qudtlib/core"; +<#-- null constant --> +<#assign null = "undefined" > +<#-- quote the value str with double quotes and escape its contents for insertion in a java string--> +<#function q str> + <#return "\"" + str?j_string + "\"" > + +<#-- optional String --> +<#function optStr optVal> + <#return optVal.isPresent()?then(q(optVal.get()), null) > + +<#-- nullable String --> +<#function nullableStr nVal=""> + <#if nVal?has_content> + <#return q(nVal) > + <#else > + <#return null > + + +<#-- optional numeric literal, e.g. Optional --> +<#function optNum optVal> + <#return optVal.isPresent()?then(optVal.get()?string.computer, null) > + +<#-- non-nullable bigdecimal --> +<#function bigDec val> + <#return "new Decimal(\"" + val?string.@toString + "\")" > + +<#-- optional numeric literal, e.g. Optional --> +<#function optBigDec optVal> + <#return optVal.isPresent()?then(bigDec(optVal.get()), null) > + +// Units +{ + let unit: Unit; +<#list units as iri, unit> + unit = new Unit( + ${q(iri)}, + undefined, + ${optStr(unit.dimensionVectorIri)}, + ${optBigDec(unit.conversionMultiplier)}, + ${optBigDec(unit.conversionOffset)}, + ${optStr(unit.prefixIri)}, + ${optStr(unit.scalingOfIri)}, + undefined, + ${optStr(unit.symbol)}, + undefined + ); + <#list unit.labels as label> + unit.addLabel(new LangString(${q(label.string)}, ${optStr(label.languageTag)})); + + <#list unit.quantityKindIris as quantityKindIri> + unit.addQuantityKindIri( + ${q(quantityKindIri)} + ); + + config.units.set(${q(iri)}, unit); + +} + +export const Units = { +<#list unitConstants as u> + // ${u.label} + ${u.codeConstantName}: Qudt.unitFromLocalnameRequired("${u.iriLocalname}"), + +} + +// QuantityKinds +{ + let quantityKind: QuantityKind; +<#list quantityKinds as iri, quantityKind> + quantityKind = new QuantityKind(${q(iri)}, ${optStr(quantityKind.dimensionVectorIri)}, ${optStr(quantityKind.symbol)}, undefined); + <#list quantityKind.labels as label> + quantityKind.addLabel(new LangString(${q(label.string)}, ${optStr(label.languageTag)})); + + <#list quantityKind.applicableUnitIris as unitIri> + quantityKind.addApplicableUnitIri(${q(unitIri)}); + + <#list quantityKind.broaderQuantityKindIris as qkIri> + quantityKind.addBroaderQuantityKindIri(${q(qkIri)}); + + config.quantityKinds.set(${q(iri)}, quantityKind); + +} + +export const QuantityKinds = { +<#list quantityKindConstants as q> + // ${q.label} + ${q.codeConstantName}: Qudt.quantityKindFromLocalnameRequired("${q.iriLocalname}"), + +} + +// Prefixes +{ + let prefix: Prefix; +<#list prefixes as iri, prefix> + prefix = new Prefix(${q(iri)}, ${bigDec(prefix.multiplier)}, ${q(prefix.symbol)}, ${optStr(prefix.ucumCode)}, undefined); + <#list prefix.labels as label> + prefix.addLabel(new LangString(${q(label.string)}, ${optStr(label.languageTag)})); + + config.prefixes.set(${q(iri)}, prefix); + +} + +export const Prefixes = { +<#list prefixConstants as q> + // ${q.label} + ${q.codeConstantName}: Qudt.prefixFromLocalnameRequired("${q.iriLocalname}"), + +} + +function getPrefix(iri:string): Prefix { + const prefix: Prefix | undefined = config.prefixes.get(iri); + if (!prefix) { + throw `prefix ${r"${iri}"} referenced but not loaded`; + } + return prefix; +} + +function getUnit(iri:string): Unit { + const unit: Unit | undefined = config.units.get(iri); + if (!unit) { + throw `unit ${r"${iri}"} referenced but not loaded`; + } + return unit; +} + +function getQuantityKind(iri:string): QuantityKind { + const quantityKind: QuantityKind | undefined = config.quantityKinds.get(iri); + if (!quantityKind) { + throw `quantityKind ${r"${iri}"} referenced but not loaded`; + } + return quantityKind; +} + +// Connect objects +for (const unit of config.units.values()){ + !!unit.prefixIri && unit.setPrefix(getPrefix(unit.prefixIri)); + !!unit.scalingOfIri && unit.setScalingOf(getUnit(unit.scalingOfIri)); + for (const qkIri of unit.quantityKindIris){ + unit.addQuantityKind(getQuantityKind(qkIri)); + } +} + +// Set factor units + +{ + let unit:Unit; +<#list units as iri, unit> + <#if unit.hasFactorUnits()> + unit = getUnit("${iri}"); + <#list unit.factorUnits as factorUnit> + unit.addFactorUnit(new FactorUnit(getUnit("${factorUnit.unit.iri}"), ${factorUnit.exponent})); + + + +} diff --git a/qudtlib-main/src/main/java/io/github/qudtlib/Qudt.java b/qudtlib-main/src/main/java/io/github/qudtlib/Qudt.java index 647bca0..e8a91ed 100644 --- a/qudtlib-main/src/main/java/io/github/qudtlib/Qudt.java +++ b/qudtlib-main/src/main/java/io/github/qudtlib/Qudt.java @@ -20,17 +20,14 @@ *

{@code
  * // Converting 38.5° Celsius into Fahrenheit:
  * Qudt.convert(new BigDecimal("38.5"), Qudt.Units.DEG_C, Qudt.Units.DEG_F);
- *
  * // finding unit for factors: m, kg, and s^-2:
  * Set myUnits =
  *            Qudt.derivedUnit(
  *                    Qudt.Units.M, 1,
  *                    Qudt.Units.KiloGM, 1,
  *                    Qudt.Units.SEC, -2);
- *
  * // finding factors of Newton:
  * List myFactorUnits = Qudt.Units.N.getFactorUnits();
- *
  * // Converting 1N into kN (using QuantityValue):
  * QuantityValue quantityValue = new QuantityValue(new BigDecimal("1"), Qudt.Units.N);
  * QuantityValue converted = Qudt.convert(quantityValue, Qudt.Units.KiloN);
@@ -104,10 +101,14 @@ public abstract static class Prefixes extends io.github.qudtlib.model.Prefixes {
      * @return the unit
      * @throws NotFoundException if no such unit is found.
      */
-    public static Unit unitFromLocalname(String localname) {
+    public static Optional unitFromLocalname(String localname) {
         return unit(unitIriFromLocalname(localname));
     }
 
+    public static Unit unitFromLocalnameRequired(String localname) {
+        return unitRequired(unitIriFromLocalname(localname));
+    }
+
     /**
      * Returns the first unit found whose label matches the specified label after replacing any
      * underscore with space and ignoring case (US locale). If more intricate matching is needed,
@@ -116,12 +117,17 @@ public static Unit unitFromLocalname(String localname) {
      * @param label the matched label
      * @return the first unit found
      */
-    public static Unit unitFromLabel(String label) {
+    public static Optional unitFromLabel(String label) {
         LabelMatcher labelMatcher = new LabelMatcher(label);
         return units.values().stream()
                 .filter(u -> u.getLabels().stream().anyMatch(labelMatcher::matches))
-                .findFirst()
-                .orElseThrow(() -> new NotFoundException("No unit found with label " + label));
+                .findFirst();
+    }
+
+    public static Unit unitFromLabelRequired(String label) {
+        return unitFromLabel(label)
+                .orElseThrow(
+                        () -> new NotFoundException("No unit found for label '" + label + "'"));
     }
 
     /**
@@ -130,14 +136,14 @@ public static Unit unitFromLabel(String label) {
      *
      * @param iri the requested unit IRI
      * @return the unit
-     * @throws NotFoundException if no such unit is found.
      */
-    public static Unit unit(String iri) {
-        Unit unit = units.get(iri);
-        if (unit == null) {
-            throw new NotFoundException("Unit not found: " + iri);
-        }
-        return unit;
+    public static Optional unit(String iri) {
+        return Optional.ofNullable(units.get(iri));
+    }
+
+    public static Unit unitRequired(String iri) {
+        return Optional.ofNullable(units.get(iri))
+                .orElseThrow(() -> new NotFoundException("No unit found for Iri " + iri));
     }
 
     /**
@@ -204,22 +210,11 @@ public static Unit scaledUnit(Prefix prefix, Unit baseUnit) {
      * @param unit the scaled unit
      * @return the base unit
      */
-    public static Unit unscale(Unit unit) {
+    public static Unit unscaledUnit(Unit unit) {
         if (unit.getScalingOfIri().isEmpty()) {
             return unit;
         }
-        return unit(unit.getScalingOfIri().get());
-    }
-
-    /**
-     * Returns the list of {@link FactorUnit}s of the unit identified by the specified unit IRI.
-     *
-     * @param unitIri the IRI of the unit
-     * @return the factors of the unit or an empty list if the unit is not a derived unit
-     * @throws NotFoundException if the unit IRI does not identify a unit in the model
-     */
-    public static List factorUnits(String unitIri) {
-        return factorUnits(unit(unitIri));
+        return unitRequired(unit.getScalingOfIri().get());
     }
 
     /**
@@ -257,104 +252,157 @@ public static List simplifyFactorUnits(List factorUnits)
      * @param factorUnits the factor units to unscale
      * @return the unscaled factor units
      */
-    public static List unscaleFactorUnits(List factorUnits) {
+    public static List unscaledFactorUnits(List factorUnits) {
         return factorUnits.stream()
-                .map(uf -> new FactorUnit(unscale(uf.getUnit()), uf.getExponent()))
+                .map(uf -> new FactorUnit(unscaledUnit(uf.getUnit()), uf.getExponent()))
                 .collect(toList());
     }
 
     /**
-     * Obtains a unit from factor units.
+     * Obtains units based on factor units, using the specified {@link FactorUnitMatchingMode}.
      *
      * 

For example, * *

{@code
-     * Qudt.derivedUnit(
+     * Qudt.derivedUnitsFrom Map(
+     *                     FactorUnitMatchingMode.EXACT,
+     *                     Map.of(
      *                     Qudt.Units.M, 1,
      *                     Qudt.Units.KiloGM, 1,
-     *                     Qudt.Units.SEC, -2);
+     *                     Qudt.Units.SEC, -2));
      * }
* * will yield a Set containing the Newton Unit ({@code Qudt.Units.N}) * + * @param searchMode the {@link DerivedUnitSearchMode} to use * @param factorUnits a map containing unit to exponent entries. * @return the derived units that match the given factor units */ - public static Set derivedUnit(List> factorUnits) { + public static Set derivedUnitsFromMap( + DerivedUnitSearchMode searchMode, Map factorUnits) { Object[] arr = new Object[factorUnits.size() * 2]; - return derivedUnitFromFactors( - factorUnits.stream() + return derivedUnitsFromUnitExponentPairs( + searchMode, + factorUnits.entrySet().stream() .flatMap(e -> Stream.of(e.getKey(), e.getValue())) .collect(Collectors.toList()) .toArray(arr)); } /** - * Obtains a unit from factor units. + * Obtains units based on factor units. * - * @see #derivedUnit(List) + * @param searchMode the {@link DerivedUnitSearchMode} to use * @param factorUnits the factor units * @return the derived unit that match the given factor units + * @see #derivedUnitsFromMap(DerivedUnitSearchMode, Map) */ - public static Set derivedUnitFromFactorUnits(List factorUnits) { - Object[] arr = new Object[factorUnits.size() * 2]; - return derivedUnitFromFactors( - factorUnits.stream() - .flatMap(e -> Stream.of(e.getUnit(), e.getExponent())) - .collect(Collectors.toList()) - .toArray(arr)); + public static Set derivedUnitsFromFactorUnits( + DerivedUnitSearchMode searchMode, List factorUnits) { + FactorUnitSelection selection = FactorUnitSelection.fromFactorUnits(factorUnits); + return derivedUnitsFromFactorUnitSelection(searchMode, selection); } /** * Vararg method, must be an even number of arguments, always alternating types of Unit|String * and Integer. * - * @param factorUnitSpecs alternating Unit|String (representing a unit IRI) and Integer (the - * exponent) + * @param searchMode the {@link DerivedUnitSearchMode} to use + * @param factorUnitSpec alternating (unit, exponent) pairs. The unit can be specified as {@link + * Unit} or String. In the latter case, it can be a unit IRI, a unit IRI's local name or a + * unit's label. The exponent must be an and Integer. * @return the units that match - */ - static Set derivedUnitFromFactors(final Object... factorUnitSpecs) { - if (factorUnitSpecs.length % 2 != 0) { - throw new IllegalArgumentException("An even number of arguments is required"); - } - if (factorUnitSpecs.length > 14) { - throw new IllegalArgumentException( - "No more than 14 arguments (7 factor units) supported"); + * @see #derivedUnitsFromMap(DerivedUnitSearchMode, Map) + */ + public static Set derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode searchMode, final Object... factorUnitSpec) { + Object[] spec = new Object[factorUnitSpec.length]; + for (int i = 0; i < factorUnitSpec.length; i++) { + if (i % 2 == 0 && factorUnitSpec[i] instanceof Unit) { + spec[i] = factorUnitSpec[i]; + } else if (i % 2 == 0 && factorUnitSpec[i] instanceof String) { + String unitString = (String) factorUnitSpec[i]; + Optional unitOpt = unit(unitString); + if (unitOpt.isEmpty()) { + unitOpt = unitFromLocalname(unitString); + } + if (unitOpt.isEmpty()) { + unitOpt = unitFromLabel(unitString); + } + if (unitOpt.isEmpty()) { + throw new NotFoundException( + String.format( + "Unable to find unit for string %s, interpreted as iri, label, or localname", + unitString)); + } + spec[i] = unitOpt.get(); + } else if (i % 2 == 1 && factorUnitSpec[i] instanceof Integer) { + spec[i] = factorUnitSpec[i]; + } else { + throw new IllegalArgumentException( + String.format( + "Cannot handle input '%s' at 0-base position %d", + factorUnitSpec[i].toString(), i)); + } } + FactorUnitSelection selection = FactorUnitSelection.fromFactorUnitSpec(spec); + return derivedUnitsFromFactorUnitSelection(searchMode, selection); + } + + /** + * @param searchMode the {@link DerivedUnitSearchMode} to use + * @param selection the factor unit selection + * @return the units that match + * @see #derivedUnitsFromMap(DerivedUnitSearchMode, Map) + */ + private static Set derivedUnitsFromFactorUnitSelection( + DerivedUnitSearchMode searchMode, FactorUnitSelection selection) { + FactorUnitMatchingMode matchingMode = + searchMode.isExactInFirstRound() + ? FactorUnitMatchingMode.EXACT + : FactorUnitMatchingMode.ALLOW_SCALED; List matchingUnits = units.values().stream() - .filter(d -> d.matches(factorUnitSpecs)) + .filter(d -> d.matches(selection, matchingMode)) .collect(Collectors.toList()); - if (matchingUnits.isEmpty()) { - throw new NotFoundException( - "No derived unit found for factors " + Arrays.toString(factorUnitSpecs)); + if (searchMode == DerivedUnitSearchMode.EXACT + || searchMode == DerivedUnitSearchMode.ALLOW_SCALED) { + return new HashSet<>(matchingUnits); + } + if (searchMode == DerivedUnitSearchMode.EXACT_ONLY_ONE) { + return retainOnlyOne(matchingUnits); } - return new HashSet<>(matchingUnits); + if (searchMode == DerivedUnitSearchMode.BEST_EFFORT_ONLY_ONE) { + if (matchingUnits.isEmpty()) { + matchingUnits = + units.values().stream() + .filter( + d -> + d.matches( + selection, + FactorUnitMatchingMode.ALLOW_SCALED)) + .collect(Collectors.toList()); + } + return retainOnlyOne(matchingUnits); + } + throw new IllegalStateException( + "Search mode " + + searchMode + + " was not handled properly, this should never happen - please report as bug."); } - /** - * Returns the specifed {@code unit} scaled with the specified {@code prefix}. - * - * @param unit the unit to scale - * @param prefix the prefix to scale by - * @return the scaled unit - * @throws NotFoundException if the result is not found in the model - */ - public static Unit scale(Unit unit, Prefix prefix) { - Optional scaled = - units.values().stream() - .filter( - u -> - unit.getIri().equals(u.getScalingOfIri().orElse(null)) - && prefix.getIri() - .equals(u.getPrefixIri().orElse(null))) - .findFirst(); - return scaled.orElseThrow( - () -> - new NotFoundException( - String.format( - "Qudt does not contain the unit %s, scaled with prefix %s", - unit, prefix))); + private static Set retainOnlyOne(List matchingUnits) { + if (matchingUnits.isEmpty()) { + return Set.of(); + } + return Set.of( + matchingUnits.stream() + .reduce( + null, + (p, n) -> + p == null + ? n + : p.getIri().compareTo(n.getIri()) > 0 ? n : p)); } /** @@ -378,183 +426,6 @@ public static Map.Entry scaleToBaseUnit(Unit unit) { return Map.entry(baseUnit, multiplier); } - /** - * Find the {@link Unit} derived from the specified {@code baseUnit} and {@code exponent}. - * - * @param baseUnit the base unit - * @param exponent the exponent - * @return the unit - * @throws NotFoundException if no such unit is found in the model - */ - public static Set derivedUnit(Unit baseUnit, int exponent) { - return derivedUnitFromFactors(baseUnit, exponent); - } - - /** - * Find the {@link Unit} derived from the specified {@code baseUnitIri} and {@code exponent}. - * - * @param baseUnitIri the base unit - * @param exponent the exponent - * @return the unit - * @throws NotFoundException if no such unit is found in the model or the baseUnitIri does not - * identify a unit - */ - public static Set derivedUnit(String baseUnitIri, int exponent) { - return derivedUnitFromFactors(unit(baseUnitIri), exponent); - } - - public static Set derivedUnit( - Unit baseUnit1, int exponent1, Unit baseUnit2, int exponent2) { - return derivedUnitFromFactors(baseUnit1, exponent1, baseUnit2, exponent2); - } - - public static Set derivedUnit( - Unit baseUnit1, - int exponent1, - Unit baseUnit2, - int exponent2, - Unit baseUnit3, - int exponent3) { - return derivedUnitFromFactors( - baseUnit1, exponent1, baseUnit2, exponent2, baseUnit3, exponent3); - } - - public static Set derivedUnit( - String baseUnitIri1, - int exponent1, - String baseUnitIri2, - int exponent2, - String baseUnitIri3, - int exponent3) { - return derivedUnitFromFactors( - unit(baseUnitIri1), - exponent1, - unit(baseUnitIri2), - exponent2, - unit(baseUnitIri3), - exponent3); - } - - public static Set derivedUnit( - Unit baseUnit1, - int exponent1, - Unit baseUnit2, - int exponent2, - Unit baseUnit3, - int exponent3, - Unit baseUnit4, - int exponent4) { - return derivedUnitFromFactors( - baseUnit1, exponent1, baseUnit2, exponent2, baseUnit3, exponent3, baseUnit4, - exponent4); - } - - public static Set derivedUnit( - String baseUnitIri1, - int exponent1, - String baseUnitIri2, - int exponent2, - String baseUnitIri3, - int exponent3, - String baseUnitIri4, - int exponent4) { - return derivedUnitFromFactors( - unit(baseUnitIri1), - exponent1, - unit(baseUnitIri2), - exponent2, - unit(baseUnitIri3), - exponent3, - unit(baseUnitIri4), - exponent4); - } - - public static Set derivedUnit( - Unit baseUnit1, - int exponent1, - Unit baseUnit2, - int exponent2, - Unit baseUnit3, - int exponent3, - Unit baseUnit4, - int exponent4, - Unit baseUnit5, - int exponent5) { - return derivedUnitFromFactors( - baseUnit1, exponent1, baseUnit2, exponent2, baseUnit3, exponent3, baseUnit4, - exponent4, baseUnit5, exponent5); - } - - public static Set derivedUnit( - String baseUnitIri1, - int exponent1, - String baseUnitIri2, - int exponent2, - String baseUnitIri3, - int exponent3, - String baseUnitIri4, - int exponent4, - String baseUnitIri5, - int exponent5) { - return derivedUnitFromFactors( - unit(baseUnitIri1), - exponent1, - unit(baseUnitIri2), - exponent2, - unit(baseUnitIri3), - exponent3, - unit(baseUnitIri4), - exponent4, - unit(baseUnitIri5), - exponent5); - } - - public static Set derivedUnit( - Unit baseUnit1, - int exponent1, - Unit baseUnit2, - int exponent2, - Unit baseUnit3, - int exponent3, - Unit baseUnit4, - int exponent4, - Unit baseUnit5, - int exponent5, - Unit baseUnit6, - int exponent6) { - return derivedUnitFromFactors( - baseUnit1, exponent1, baseUnit2, exponent2, baseUnit3, exponent3, baseUnit4, - exponent4, baseUnit5, exponent5, baseUnit6, exponent6); - } - - public static Set derivedUnit( - String baseUnitIri1, - int exponent1, - String baseUnitIri2, - int exponent2, - String baseUnitIri3, - int exponent3, - String baseUnitIri4, - int exponent4, - String baseUnitIri5, - int exponent5, - String baseUnitIri6, - int exponent6) { - return derivedUnitFromFactors( - unit(baseUnitIri1), - exponent1, - unit(baseUnitIri2), - exponent2, - unit(baseUnitIri3), - exponent3, - unit(baseUnitIri4), - exponent4, - unit(baseUnitIri5), - exponent5, - unit(baseUnitIri6), - exponent6); - } - /** * Returns a {@link QuantityKind} for the specified localname (i.e. the last element of the Unit * IRI). For example, quantityKindFromLocalName("Width") yields the quantityKind @@ -564,10 +435,14 @@ public static Set derivedUnit( * @return the quantityKind * @throws NotFoundException if no such quantityKind is found. */ - public static QuantityKind quantityKindFromLocalname(String localname) { + public static Optional quantityKindFromLocalname(String localname) { return quantityKind(quantityKindIriFromLocalname(localname)); } + public static QuantityKind quantityKindFromLocalnameRequired(String localname) { + return quantityKindRequired(quantityKindIriFromLocalname(localname)); + } + /** * Returns the {@link QuantityKind} identified the specified IRI. For example, * quantityKind("http://qudt.org/vocab/quantitykind/Width") yields {@code @@ -577,12 +452,13 @@ public static QuantityKind quantityKindFromLocalname(String localname) { * @return the quantityKind * @throws NotFoundException if no such quantityKind is found. */ - public static QuantityKind quantityKind(String iri) { - QuantityKind quantityKind = quantityKinds.get(iri); - if (quantityKind == null) { - throw new NotFoundException("QuantityKind not found: " + iri); - } - return quantityKind; + public static Optional quantityKind(String iri) { + return Optional.ofNullable(quantityKinds.get(iri)); + } + + public static QuantityKind quantityKindRequired(String iri) { + return quantityKind(iri) + .orElseThrow(() -> new NotFoundException("QuantityKind not found: " + iri)); } /** @@ -593,7 +469,7 @@ public static QuantityKind quantityKind(String iri) { */ public static Set quantityKinds(Unit unit) { return unit.getQuantityKindIris().stream() - .map(Qudt::quantityKind) + .map(Qudt::quantityKindRequired) .collect(Collectors.toSet()); } @@ -610,8 +486,8 @@ public static Set quantityKindsBroad(Unit unit) { while (!current.isEmpty()) { current = current.stream() - .flatMap(qk -> qk.getBroaderQuantityKinds().stream()) - .map(Qudt::quantityKind) + .flatMap(qk -> qk.getBroaderQuantityKindIris().stream()) + .map(Qudt::quantityKindRequired) .collect(Collectors.toSet()); result.addAll(current); } @@ -649,7 +525,11 @@ public static String prefixIriFromLocalname(String localname) { * @return the prefix * @throws NotFoundException if no such prefix is found. */ - public static Prefix prefixFromLocalname(String localname) { + public static Prefix prefixFromLocalnameRequired(String localname) { + return prefixRequired(prefixIriFromLocalname(localname)); + } + + public static Optional prefixFromLocalname(String localname) { return prefix(prefixIriFromLocalname(localname)); } @@ -661,12 +541,12 @@ public static Prefix prefixFromLocalname(String localname) { * @return the prefix * @throws NotFoundException if no such prefix is found. */ - public static Prefix prefix(String iri) { - Prefix prefix = prefixes.get(iri); - if (prefix == null) { - throw new NotFoundException("Prefix not found: " + iri); - } - return prefix; + public static Optional prefix(String iri) { + return Optional.ofNullable(prefixes.get(iri)); + } + + public static Prefix prefixRequired(String iri) { + return prefix(iri).orElseThrow(() -> new NotFoundException("Prefix not found: " + iri)); } /** @@ -678,7 +558,7 @@ public static Prefix prefix(String iri) { * @throws NotFoundException if no unit is found for the specified unitIri */ public static QuantityValue quantityValue(BigDecimal value, String unitIri) { - return new QuantityValue(value, unit(unitIri)); + return new QuantityValue(value, unitRequired(unitIri)); } /** @@ -717,7 +597,7 @@ public static QuantityValue convert(QuantityValue from, Unit toUnit) { */ public static QuantityValue convert(QuantityValue from, String toUnitIri) throws InconvertibleQuantitiesException { - Unit toUnit = unit(toUnitIri); + Unit toUnit = unitRequired(toUnitIri); return convert(from.getValue(), from.getUnit(), toUnit); } @@ -736,7 +616,7 @@ public static QuantityValue convert(QuantityValue from, String toUnitIri) */ public static QuantityValue convert( BigDecimal fromValue, String fromUnitIri, String toUnitIri) { - return convert(fromValue, unit(fromUnitIri), unit(toUnitIri)); + return convert(fromValue, unitRequired(fromUnitIri), unitRequired(toUnitIri)); } /** diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/DerivedUnitSearchMode.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/DerivedUnitSearchMode.java new file mode 100644 index 0000000..36fc0a7 --- /dev/null +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/DerivedUnitSearchMode.java @@ -0,0 +1,30 @@ +package io.github.qudtlib.model; + +/** + * Governs the algorithm used to find units based on their derived units. The DerivedUnitSearchMode + * is mapped to the {@link FactorUnitMatchingMode} which governs individual unit/factor unit + * matching. + */ +public enum DerivedUnitSearchMode { + /** Only select exact matches. */ + EXACT, + /** + * Only select exact matches, and if there are multiple matches, select only one of them. The + * Unit's IRI is used as the tie-breaker, so the result is stable over multiple executions. + */ + EXACT_ONLY_ONE, + /** + * Select exact matches and units whose factor units have different scale, but the scale of the + * result is equivalent to the cumulative scale of the original factor units + */ + ALLOW_SCALED, + /** + * Select only one unit. Try EXACT mode first. If no match is found, try ALLOW_SCALED. Break + * ties using the matching units' IRIs. + */ + BEST_EFFORT_ONLY_ONE; + + public boolean isExactInFirstRound() { + return this == EXACT || this == EXACT_ONLY_ONE || this == BEST_EFFORT_ONLY_ONE; + } +} diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnit.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnit.java index e95bde3..e83d67b 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnit.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnit.java @@ -53,44 +53,21 @@ Set match( Set selection, int cumulativeExponent, Deque matchedPath, - ScaleFactor scaleFactor) { + ScaleFactor scaleFactor, + FactorUnitMatchingMode mode) { Set mySelection = new HashSet<>(selection); - // descend into unit - mySelection = - this.unit.match( - mySelection, - getExponentCumulated(cumulativeExponent), - matchedPath, - scaleFactor); - // now match this one - Set ret = new HashSet<>(); - for (FactorUnitSelection factorUnitSelection : mySelection) { - // add one solution where this node is matched - FactorUnitSelection processedSelection = - factorUnitSelection.forMatch( - this, cumulativeExponent, matchedPath, scaleFactor); - if (!processedSelection.equals(factorUnitSelection)) { - // if there was a match, (i.e, we modified the selection), - // it's a new partial solution - return it - ret.add(processedSelection); - } - // also regard the selection without the match as a possible partial solution - ret.add(factorUnitSelection); - } - // lower level - return ret; - } - - boolean isMatched(FactorUnitSelection selection, Deque checkedPath) { - if (selection.isSelected(this, checkedPath)) { - return true; - } - return unit.isMatched(selection, checkedPath); + // descend into unit, with cumulated exponent + return this.unit.match( + mySelection, + getExponentCumulated(cumulativeExponent), + matchedPath, + scaleFactor, + mode); } @Override public String toString() { - return "FU{" + unit + "^" + exponent + "}"; + return unit + "^" + exponent; } @Override diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatch.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatch.java index 5e3fff9..8352470 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatch.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatch.java @@ -1,10 +1,9 @@ package io.github.qudtlib.model; -import static java.util.stream.Collectors.joining; - import java.math.BigDecimal; import java.util.*; +/** Represents a unit that has been matched in a FactorUnitSelection. */ public class FactorUnitMatch { private final FactorUnit matchedFactorUnit; private final List matchedPath; @@ -44,15 +43,27 @@ public ScaleFactor getScaleFactor() { @Override public String toString() { - return "FUM{at " - + Optional.ofNullable(matchedPath).orElse(List.of()).stream() - .map(Object::toString) - .collect(joining("/")) - + ", MM{" - + this.matchedMultiplier - + "}, " - + this.scaleFactor - + '}'; + return getPathAsString() + + (this.matchedMultiplier.compareTo(BigDecimal.ONE) == 0 + ? "" + : "*" + this.matchedMultiplier) + + (this.scaleFactor.getValue().compareTo(BigDecimal.ONE) == 0 + ? "" + : "*" + this.scaleFactor); + } + + private String getPathAsString() { + StringBuilder sb = new StringBuilder("/"); + if (matchedPath != null) { + ListIterator li = matchedPath.listIterator(matchedPath.size()); + while (li.hasPrevious()) { + sb.append(li.previous()); + if (li.hasPrevious()) { + sb.append("/"); + } + } + } + return sb.toString(); } @Override diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatchingMode.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatchingMode.java new file mode 100644 index 0000000..ce075d0 --- /dev/null +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitMatchingMode.java @@ -0,0 +1,12 @@ +package io.github.qudtlib.model; + +/** Specifies the check whether a Unit matches a given set of factor units. */ +public enum FactorUnitMatchingMode { + /** Only select exact matches */ + EXACT, + /** + * Select exact matches and units whose factor units have different scale, but the scale of the + * result is equivalent to the cumulative scale of the original factor units + */ + ALLOW_SCALED, +} diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelection.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelection.java index 2f109f8..fef9cf0 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelection.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelection.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Represents a query by factor units while it is being evaluated. @@ -19,28 +20,47 @@ public FactorUnitSelection(List selectors) { this.selectors = selectors; } - public FactorUnitSelection copy() { + public static FactorUnitSelection fromFactorUnits(List factorUnits) { return new FactorUnitSelection( - this.selectors.stream().map(FactorUnitSelector::copy).collect(Collectors.toList())); + factorUnits.stream() + .map(fu -> new FactorUnitSelector(fu.getUnit(), fu.getExponent())) + .collect(Collectors.toList())); } - public List getSelectors() { - return selectors; + /** + * Accepts up to 7 pairs of <Unit, Integer> which are interpreted as factor units and + * respective exponents. + * + * @param factorUnitSpec array of up to 7 %lt;Unit, Integer%gt; pairs + * @return true if the specified unit/exponent combination identifies this unit. + * (overspecification is counted as a match) + */ + public static FactorUnitSelection fromFactorUnitSpec(Object... factorUnitSpec) { + if (factorUnitSpec.length % 2 != 0) { + throw new IllegalArgumentException("An even number of arguments is required"); + } + if (factorUnitSpec.length > 14) { + throw new IllegalArgumentException( + "No more than 14 arguments (7 factor units) supported"); + } + List selectors = new ArrayList<>(); + for (int i = 0; i < factorUnitSpec.length; i += 2) { + Unit requestedUnit; + requestedUnit = ((Unit) factorUnitSpec[i]); + Integer requestedExponent = (Integer) factorUnitSpec[i + 1]; + selectors.add(new FactorUnitSelector(requestedUnit, requestedExponent)); + } + return new FactorUnitSelection(selectors); } - public boolean isSelected(FactorUnit factorUnit, Deque checkedPath) { - return this.selectors.stream() - .anyMatch( - s -> - s.getFactorUnitMatch().isPresent() - && factorUnit.equals( - s.getFactorUnitMatch().get().getMatchedFactorUnit()) - && Arrays.equals( - checkedPath.toArray(), - s.getFactorUnitMatch() - .get() - .getMatchedPath() - .toArray())); + public static FactorUnitSelection fromFactorUnitSpec( + Collection> factorUnitSpec) { + Object[] arr = new Object[factorUnitSpec.size() * 2]; + return FactorUnitSelection.fromFactorUnitSpec( + factorUnitSpec.stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()) + .toArray(arr)); } public boolean isCompleteMatch() { @@ -67,34 +87,16 @@ public boolean isCompleteMatch() { return cumulativeScale.compareTo(BigDecimal.ONE) == 0; } - public boolean allMarked(Collection factorUnits) { - return this.selectors.stream() - .allMatch( - s -> - factorUnits.stream() - .anyMatch( - u -> - s.getFactorUnitMatch().isPresent() - && u.equals( - s.getFactorUnitMatch() - .get() - .getMatchedFactorUnit()))); - } - - public boolean isMatchingSelectorAvailable(FactorUnit factorUnit, int cumulativeExponent) { - return selectors.stream() - .anyMatch(s -> s.isAvailable() && s.matches(factorUnit, cumulativeExponent)); - } - - public FactorUnitSelection forMatch( + public FactorUnitSelection forPotentialMatch( FactorUnit factorUnit, int cumulativeExponent, Deque matchedPath, - ScaleFactor scaleFactor) { + ScaleFactor scaleFactor, + FactorUnitMatchingMode mode) { List newSelectors = new ArrayList<>(); boolean matched = false; for (FactorUnitSelector s : this.selectors) { - if (!matched && s.isAvailable() && s.matches(factorUnit, cumulativeExponent)) { + if (!matched && s.isAvailable() && s.matches(factorUnit, cumulativeExponent, mode)) { matched = true; newSelectors.addAll( s.forMatch(factorUnit, cumulativeExponent, matchedPath, scaleFactor)); @@ -105,13 +107,9 @@ public FactorUnitSelection forMatch( return new FactorUnitSelection(newSelectors); } - public boolean allBound() { - return selectors.stream().allMatch(FactorUnitSelector::isBound); - } - @Override public String toString() { - return "FUSel{" + selectors + '}'; + return "Select" + selectors; } @Override diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelector.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelector.java index 3a40a8f..091b80b 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelector.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/FactorUnitSelector.java @@ -5,7 +5,9 @@ import java.util.*; /** - * Represents a part of a query by factor units while it is being evaluated. + * Represents a part of a query by factor units while it is being evaluated. The + * factorUnitMatch is null if no unit has been found to match yet, otherwise it + * encapsulates the matched unit and accompanying data. * * @author Florian Kleedorfer * @version 1.0 @@ -32,6 +34,37 @@ public FactorUnitSelector matched(FactorUnitMatch factorUnitMatch) { return new FactorUnitSelector(this.unit, this.exponent, factorUnitMatch); } + /** + * Creates a List of {@link FactorUnitSelector} objects that result from matching this + * FactorUnitSelector with the specified {@link FactorUnit}. If the match is exact, the + * resulting list contains only one object, if the exponent of the selector is higher than the + * cumulative exponent of the factorUnit's , a selector for the remaining power + * (the selector's exponent minus the factorUnit's exponent) is added. + * + *

The cumulative exponent is the exponent obtained by following the path from the + * top-level unit to the factorUnit we have found a match for. + * + *

For example, let the unit be A-PER-J and we have just found a match for + * M^-1. What's the cumulative exponent of M in this case? + * A-PER-J = (A J^-1), and J = (N M) = (KiloGM M^2 SEC^-2), so the path to + * find M is /A-PER-J/J/M or A-PER-J/J/N/M. In each case, the + * cumulative exponent of M is -1 because the exponent of J is -1. After matching one M^-1 + * , there is (A M^-1 KiloGM^-^1 SEC^2) left to be matched. + * + *

The matchedMultiplier is the factor needed to scale from this FactorUnitSelector's + * unit to the factorUnit we are matching with it. + * + *

For example, if this.unit is KiloM^2 and the factorUnit + * is M^2, the matchedMultiplier is (10^3)^2 = 10^6 + * . + * + * @param factorUnit the matched unit + * @param cumulativeExponent the + * @param matchedPath the (inverted) list of Units traversed on the way here + * @param scaleFactor the scale factor + * @return the list of FactorUnitSelectors resulting from matching this FactorUnitSelector in + * the specified situation + */ public List forMatch( FactorUnit factorUnit, int cumulativeExponent, @@ -44,8 +77,9 @@ public List forMatch( throw new IllegalArgumentException("epxonents do not match"); } int matchedPower = factorUnit.getExponentCumulated(cumulativeExponent); - BigDecimal matchedMultiplier = calculateMatchedMultiplier(factorUnit, matchedPower); - if (matchedMultiplier == null) { + Optional matchedMultiplier = + calculateMatchedMultiplier(factorUnit, matchedPower); + if (matchedMultiplier.isEmpty()) { throw new IllegalArgumentException("units do not match"); } int remainingPower = this.exponent - matchedPower; @@ -53,20 +87,21 @@ public List forMatch( ret.add( matched( new FactorUnitMatch( - factorUnit, matchedMultiplier, matchedPath, scaleFactor))); + factorUnit, matchedMultiplier.get(), matchedPath, scaleFactor))); if (remainingPower != 0) { ret.add(new FactorUnitSelector(this.unit, remainingPower)); } return ret; } - private BigDecimal calculateMatchedMultiplier(FactorUnit factorUnit, int matchedExponent) { + private Optional calculateMatchedMultiplier( + FactorUnit factorUnit, int matchedExponent) { Objects.requireNonNull(factorUnit); if (!this.unit.isConvertible(factorUnit.getUnit())) { - return null; + return Optional.empty(); } BigDecimal conversionMultiplier = factorUnit.getUnit().getConversionMultiplier(this.unit); - return conversionMultiplier.pow(matchedExponent, MathContext.DECIMAL128); + return Optional.of(conversionMultiplier.pow(matchedExponent, MathContext.DECIMAL128)); } private boolean exponentMatches(FactorUnit factorUnit, int cumulativeExponent) { @@ -76,9 +111,18 @@ private boolean exponentMatches(FactorUnit factorUnit, int cumulativeExponent) { && Integer.signum(this.exponent) == Integer.signum(cumulatedFactorUnitExponent); } - public boolean matches(FactorUnit factorUnit, int cumulativeExponent) { - return exponentMatches(factorUnit, cumulativeExponent) - && this.unit.isSameScaleAs(factorUnit.getUnit()); + public boolean matches( + FactorUnit factorUnit, int cumulativeExponent, FactorUnitMatchingMode mode) { + switch (mode) { + case EXACT: + return unit.equals(factorUnit.getUnit()) + && exponentMatches(factorUnit, cumulativeExponent); + case ALLOW_SCALED: + return this.unit.isSameScaleAs(factorUnit.getUnit()) + && exponentMatches(factorUnit, cumulativeExponent); + default: + throw new IllegalStateException("Cannot handle mode: " + mode); + } } public FactorUnitSelector copy() { @@ -107,12 +151,11 @@ public boolean isBound() { @Override public String toString() { - return "FUS{" + return "" + unit - + "^" - + exponent - + ((factorUnitMatch == null) ? ",(not matched)" : "," + factorUnitMatch) - + '}'; + + (exponent == 1 ? "" : "^" + exponent) + + "@" + + ((factorUnitMatch == null) ? "?" : factorUnitMatch); } @Override diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/Prefix.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/Prefix.java index 95013ff..8bbd4ff 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/Prefix.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/Prefix.java @@ -47,8 +47,8 @@ public String getSymbol() { return symbol; } - public String getUcumCode() { - return ucumCode; + public Optional getUcumCode() { + return Optional.ofNullable(ucumCode); } public void addLabel(LangString langString) { 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 dfe2968..50c1f6f 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 @@ -12,7 +12,7 @@ * @version 1.0 */ public class Quantity { - Set quantityValues; + final Set quantityValues; public Quantity(Set quantityValues) { this.quantityValues = quantityValues; diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityKind.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityKind.java index 87dafbe..4e5e456 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityKind.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/QuantityKind.java @@ -11,32 +11,32 @@ public class QuantityKind { private final String iri; private final Set labels; - private final Set applicableUnits; - private final Set broaderQuantityKinds; - private final String dimensionVector; + private final Set applicableUnitIris; + private final Set broaderQuantityKindIris; + private final String dimensionVectorIri; private final String symbol; public QuantityKind( String iri, Set labels, - Set applicableUnits, - Set broaderQuantityKinds, - String dimensionVector, + Set applicableUnitIris, + Set broaderQuantityKindIris, + String dimensionVectorIri, String symbol) { this.iri = iri; this.labels = labels; - this.applicableUnits = new HashSet<>(applicableUnits); - this.broaderQuantityKinds = new HashSet<>(broaderQuantityKinds); - this.dimensionVector = dimensionVector; + this.applicableUnitIris = new HashSet<>(applicableUnitIris); + this.broaderQuantityKindIris = new HashSet<>(broaderQuantityKindIris); + this.dimensionVectorIri = dimensionVectorIri; this.symbol = symbol; } - public QuantityKind(String iri, String dimensionVector, String symbol) { + public QuantityKind(String iri, String dimensionVectorIri, String symbol) { this.iri = iri; - this.dimensionVector = dimensionVector; + this.dimensionVectorIri = dimensionVectorIri; this.symbol = symbol; - this.applicableUnits = new HashSet<>(); - this.broaderQuantityKinds = new HashSet<>(); + this.applicableUnitIris = new HashSet<>(); + this.broaderQuantityKindIris = new HashSet<>(); this.labels = new HashSet<>(); } @@ -44,16 +44,16 @@ public String getIri() { return iri; } - public Set getApplicableUnits() { - return Collections.unmodifiableSet(applicableUnits); + public Set getApplicableUnitIris() { + return Collections.unmodifiableSet(applicableUnitIris); } - public Set getBroaderQuantityKinds() { - return Collections.unmodifiableSet(broaderQuantityKinds); + public Set getBroaderQuantityKindIris() { + return Collections.unmodifiableSet(broaderQuantityKindIris); } - public Optional getDimensionVector() { - return Optional.ofNullable(dimensionVector); + public Optional getDimensionVectorIri() { + return Optional.ofNullable(dimensionVectorIri); } public Optional getSymbol() { @@ -82,14 +82,14 @@ public boolean hasLabel(String label) { return labels.stream().anyMatch(s -> s.getString().equals(label)); } - public void addApplicableUnit(String unit) { + public void addApplicableUnitIri(String unit) { Objects.requireNonNull(unit); - this.applicableUnits.add(unit); + this.applicableUnitIris.add(unit); } - public void addBroaderQuantityKind(String quantitkyKind) { - Objects.requireNonNull(quantitkyKind); - this.broaderQuantityKinds.add(quantitkyKind); + public void addBroaderQuantityKindIri(String quantitkyKindIri) { + Objects.requireNonNull(quantitkyKindIri); + this.broaderQuantityKindIris.add(quantitkyKindIri); } @Override diff --git a/qudtlib-model/src/main/java/io/github/qudtlib/model/ScaleFactor.java b/qudtlib-model/src/main/java/io/github/qudtlib/model/ScaleFactor.java index c56947a..903d92a 100644 --- a/qudtlib-model/src/main/java/io/github/qudtlib/model/ScaleFactor.java +++ b/qudtlib-model/src/main/java/io/github/qudtlib/model/ScaleFactor.java @@ -33,6 +33,6 @@ public ScaleFactor copy() { @Override public String toString() { - return "SF{" + value + '}'; + return value.toString(); } } 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 db8d577..6cf354e 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 @@ -5,7 +5,6 @@ import java.math.MathContext; import java.util.*; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Represents a QUDT Unit. @@ -131,12 +130,7 @@ public boolean isConvertible(Unit toUnit) { } public boolean matches(Collection> factorUnitSpec) { - Object[] arr = new Object[factorUnitSpec.size() * 2]; - return matches( - factorUnitSpec.stream() - .flatMap(e -> Stream.of(e.getKey(), e.getValue())) - .collect(Collectors.toList()) - .toArray(arr)); + return matches(FactorUnitSelection.fromFactorUnitSpec((factorUnitSpec))); } /** @@ -148,49 +142,47 @@ public boolean matches(Collection> factorUnitSpec) { * (overspecification is counted as a match) */ public boolean matches(Object... factorUnitSpec) { - if (factorUnitSpec.length % 2 != 0) { - throw new IllegalArgumentException("An even number of arguments is required"); - } - if (factorUnitSpec.length > 14) { - throw new IllegalArgumentException( - "No more than 14 arguments (7 factor units) supported"); - } - List selectors = new ArrayList<>(); - for (int i = 0; i < factorUnitSpec.length; i += 2) { - Unit requestedUnit; - requestedUnit = ((Unit) factorUnitSpec[i]); - Integer requestedExponent = (Integer) factorUnitSpec[i + 1]; - selectors.add(new FactorUnitSelector(requestedUnit, requestedExponent)); - } - Set selections = Set.of(new FactorUnitSelection(selectors)); - selections = match(selections, 1, new ArrayDeque<>(), new ScaleFactor()); - if (selections == null || selections.isEmpty()) return false; - return selections.stream() - .filter(FactorUnitSelection::isCompleteMatch) - .anyMatch(sel -> this.isMatched(sel, new ArrayDeque<>())); + return matches(FactorUnitSelection.fromFactorUnitSpec(factorUnitSpec)); } - boolean isMatched(FactorUnitSelection selection, Deque checkedPath) { - checkedPath.push(this); - boolean match = false; - if (hasFactorUnits()) { - match = getFactorUnits().stream().allMatch(fu -> fu.isMatched(selection, checkedPath)); - } - if (!match && isScaled()) { - match = getScalingOf().map(u -> u.isMatched(selection, checkedPath)).orElse(false); - } - checkedPath.pop(); - return match; + /** + * Checks if this unit matches the specified FactorUnitSelection, i.e. if it is made up of the + * specified factor units. + * + *

For example, the unit Nm (Newton Meter) would match a factor Unit selection containing + * only the still unmatched selectors of (N^1? m^1?), as well as the selection containing + * + * @param initialSelection the selection criteria + * @return true if the unit matches the criteria + */ + public boolean matches(FactorUnitSelection initialSelection) { + return matches(initialSelection, FactorUnitMatchingMode.EXACT); + } + + public boolean matches(FactorUnitSelection initialSelection, FactorUnitMatchingMode mode) { + Set selections = Set.of(initialSelection); + selections = match(selections, 1, new ArrayDeque<>(), new ScaleFactor(), mode); + if (selections == null || selections.isEmpty()) return false; + return selections.stream().anyMatch(FactorUnitSelection::isCompleteMatch); } Set match( Set selections, int cumulativeExponent, Deque matchedPath, - ScaleFactor scaleFactor) { + ScaleFactor scaleFactor, + FactorUnitMatchingMode mode) { Set results = new HashSet<>(); matchedPath.push(this); - if (this.getScalingOf().isPresent() && getPrefix().isPresent()) { + // match factor units (if any) + if (hasFactorUnits()) { + results.addAll( + matchFactorUnits( + selections, cumulativeExponent, matchedPath, scaleFactor, mode)); + } else // try to match the unscaled version of the unit, if any + if (this.getScalingOf().isPresent() + && getPrefix().isPresent() + && getScalingOf().get().hasFactorUnits()) { results.addAll( this.getScalingOf() .get() @@ -198,20 +190,63 @@ Set match( selections, cumulativeExponent, matchedPath, - scaleFactor.multiplyBy( - this.getPrefix().get().getMultiplier()))); + scaleFactor.multiplyBy(this.getPrefix().get().getMultiplier()), + mode)); } - if (hasFactorUnits()) { - for (FactorUnit factorUnit : factorUnits) { - selections = - factorUnit.match(selections, cumulativeExponent, matchedPath, scaleFactor); - } - } - results.addAll(selections); + // match this unit + results.addAll( + matchThisUnit(selections, cumulativeExponent, matchedPath, scaleFactor, mode)); matchedPath.pop(); return results; } + private Set matchFactorUnits( + Set selections, + int cumulativeExponent, + Deque matchedPath, + ScaleFactor scaleFactor, + FactorUnitMatchingMode mode) { + Set subResults = new HashSet<>(selections); + Set lastResults = subResults; + for (FactorUnit factorUnit : factorUnits) { + subResults = + factorUnit.match( + lastResults, cumulativeExponent, matchedPath, scaleFactor, mode); + if (lastResults.equals(subResults)) { + // no new matches for current factor unit - abort + return selections; + } + lastResults = subResults; + } + return subResults; + } + + private Set matchThisUnit( + Set selection, + int cumulativeExponent, + Deque matchedPath, + ScaleFactor scaleFactor, + FactorUnitMatchingMode mode) { + // now match this one + Set ret = new HashSet<>(); + for (FactorUnitSelection factorUnitSelection : selection) { + // add one solution where this node is matched + FactorUnitSelection processedSelection = + factorUnitSelection.forPotentialMatch( + new FactorUnit(this, 1), + cumulativeExponent, + matchedPath, + scaleFactor, + mode); + if (!processedSelection.equals(factorUnitSelection)) { + // if there was a match, (i.e, we modified the selection), + // it's a new partial solution - return it + ret.add(processedSelection); + } + } + return ret; + } + public boolean hasFactorUnits() { return this.factorUnits != null && !this.factorUnits.isEmpty(); } diff --git a/qudtlib-test/src/test/java/io/github/qudtlib/DerivedUnitTests.java b/qudtlib-test/src/test/java/io/github/qudtlib/DerivedUnitTests.java index 79af6b4..cbd001a 100644 --- a/qudtlib-test/src/test/java/io/github/qudtlib/DerivedUnitTests.java +++ b/qudtlib-test/src/test/java/io/github/qudtlib/DerivedUnitTests.java @@ -1,16 +1,13 @@ package io.github.qudtlib; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; -import io.github.qudtlib.model.FactorUnit; -import io.github.qudtlib.model.Unit; +import io.github.qudtlib.model.*; import java.math.BigDecimal; import java.util.*; import java.util.stream.Stream; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; @@ -26,9 +23,10 @@ public class DerivedUnitTests { public void testSingleFactorUnit() { Unit du = Qudt.Units.PER__M; assertTrue(du.matches(Qudt.Units.M, -1)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -1, Qudt.Units.KiloGM, 1)); - Assertions.assertFalse(du.matches(Qudt.Units.KiloGM, -1)); - Assertions.assertFalse(du.matches(Qudt.Units.SEC, -2)); + assertFalse(du.matches(Qudt.Units.M, -1, Qudt.Units.KiloGM, 1)); + assertFalse(du.matches(Qudt.Units.KiloGM, -1)); + assertFalse(du.matches(Qudt.Units.SEC, -2)); + assertTrue(Qudt.Units.M.matches(Qudt.Units.M, 1)); } @Test @@ -36,9 +34,9 @@ public void testTwoFactorUnit() { Unit du = Qudt.Units.KiloGM__PER__M2; assertTrue(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 1)); assertTrue(du.matches(Qudt.Units.KiloGM, 1, Qudt.Units.M, -2)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 2)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -2)); - Assertions.assertFalse(du.matches(Qudt.Units.KiloGM, 1)); + assertFalse(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 2)); + assertFalse(du.matches(Qudt.Units.M, -2)); + assertFalse(du.matches(Qudt.Units.KiloGM, 1)); } @Test @@ -55,7 +53,7 @@ public void testDeepFactorUnit() { Qudt.Units.SEC, -2)); assertTrue(du.matches(Qudt.Units.N, 1, Qudt.Units.KiloGM, -1)); - assertTrue( + assertFalse( du.matches( Qudt.Units.KiloGM, -1, @@ -67,7 +65,7 @@ public void testDeepFactorUnit() { -2, Qudt.Units.N, 1)); - Assertions.assertFalse( + assertFalse( du.matches( Qudt.Units.KiloGM, -1, @@ -81,9 +79,71 @@ public void testDeepFactorUnit() { 1, Qudt.Units.KiloGM, -1)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 2)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -2)); - Assertions.assertFalse(du.matches(Qudt.Units.KiloGM, 1)); + assertFalse(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 2)); + assertFalse(du.matches(Qudt.Units.M, -2)); + assertFalse(du.matches(Qudt.Units.KiloGM, 1)); + } + + @Test + public void testMatchingModeAllowScaled() { + assertTrue( + Qudt.Units.GM__PER__DeciM3.matches( + FactorUnitSelection.fromFactorUnitSpec( + Qudt.Units.KiloGM, 1, Qudt.Units.M, -3), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.KiloGM__PER__M3.matches( + FactorUnitSelection.fromFactorUnitSpec(Qudt.Units.GM, 1, Qudt.Units.M, -3), + FactorUnitMatchingMode.ALLOW_SCALED)); + } + + @Test + public void testMatchingModeExact() { + assertFalse(Qudt.Units.GM__PER__DeciM3.matches(Qudt.Units.KiloGM, 1, Qudt.Units.M, -3)); + assertFalse(Qudt.Units.KiloGM__PER__M3.matches(Qudt.Units.GM, 1, Qudt.Units.M, -3)); + } + + @Test + public void testSearchModeExactOnlyOne() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT_ONLY_ONE, Qudt.Units.N, 1, Qudt.Units.M, 1); + assertEquals(1, units.size()); + assertTrue(units.contains(Qudt.Units.J)); + } + + @Test + public void testSearchModeExact_2Results() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.N, 1, Qudt.Units.M, 1); + assertEquals(2, units.size()); + assertTrue(units.contains(Qudt.Units.J)); + assertTrue(units.contains(Qudt.Units.N__M)); + } + + @Test + public void testSearchModeBestEffortOnlyOne() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.BEST_EFFORT_ONLY_ONE, "KiloGM", 1, "M", -3); + assertEquals(1, units.size()); + assertTrue(units.contains(Qudt.Units.KiloGM__PER__M3)); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.BEST_EFFORT_ONLY_ONE, "KiloN", 1, "MilliM", 1); + assertEquals(1, units.size()); + assertTrue(units.contains(Qudt.Units.J)); + } + + @Test + public void testSearchModeAllowScaled() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.ALLOW_SCALED, "KiloGM", 1, "M", -3); + assertEquals(2, units.size()); + assertTrue(units.contains(Qudt.Units.KiloGM__PER__M3)); + assertTrue(units.contains(Qudt.Units.GM__PER__DeciM3)); } @Test @@ -91,7 +151,7 @@ public void testDeepFactorUnitWithDuplicateUnitExponentCombination() { Unit du = Qudt.Units.N__M__PER__KiloGM; boolean matches = du.matches(Qudt.Units.N, 1, Qudt.Units.KiloGM, -1, Qudt.Units.M, 1); assertTrue(matches); - assertTrue( + assertFalse( du.matches( Qudt.Units.KiloGM, -1, @@ -105,7 +165,7 @@ public void testDeepFactorUnitWithDuplicateUnitExponentCombination() { 1, Qudt.Units.N, 1)); - assertTrue( + assertFalse( du.matches( Qudt.Units.KiloGM, -1, @@ -117,7 +177,7 @@ public void testDeepFactorUnitWithDuplicateUnitExponentCombination() { -2, Qudt.Units.N, 1)); - Assertions.assertFalse( + assertFalse( du.matches( Qudt.Units.KiloGM, -1, @@ -138,10 +198,10 @@ public void testDeepFactorUnitWithDuplicateUnitExponentCombination() { Qudt.Units.SEC, -2, Qudt.Units.M, 1, Qudt.Units.KiloGM, -1)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 2)); - Assertions.assertFalse(du.matches(Qudt.Units.M, -2)); - Assertions.assertFalse(du.matches(Qudt.Units.KiloGM, 1)); - Assertions.assertFalse(du.matches(Qudt.Units.N, 1, Qudt.Units.KiloGM, -1)); + assertFalse(du.matches(Qudt.Units.M, -2, Qudt.Units.KiloGM, 2)); + assertFalse(du.matches(Qudt.Units.M, -2)); + assertFalse(du.matches(Qudt.Units.KiloGM, 1)); + assertFalse(du.matches(Qudt.Units.N, 1, Qudt.Units.KiloGM, -1)); } @Test @@ -157,7 +217,7 @@ public void testDeepFactorUnitWithDuplicateUnitExponentCombination() { Qudt.Units.SEC, -2, Qudt.Units.KiloGM, -1)); // now simplify: wrongly aggregate the KiloGM^1, KiloGM^-1 to KiloGM^0: should not work - Assertions.assertFalse( + assertFalse( du.matches( Qudt.Units.M, 2, Qudt.Units.SEC, -2, @@ -174,19 +234,40 @@ public void testScaledFactors() { new Object[] { Qudt.Units.SEC, -2, Qudt.Units.KiloGM, 1, Qudt.Units.M, 1, Qudt.Units.KiloM, 1 }; - assertTrue(Qudt.Units.KiloN__M.matches(factors)); + assertTrue( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); factors = new Object[] { Qudt.Units.KiloGM, 1, Qudt.Units.SEC, -2, Qudt.Units.M, 1, Qudt.Units.KiloM, 1 }; - assertTrue(Qudt.Units.KiloN__M.matches(factors)); - assertTrue(Qudt.Units.KiloJ.matches(factors)); - Assertions.assertFalse(Qudt.Units.MilliOHM.matches(factors)); - Assertions.assertFalse(Qudt.Units.MilliS.matches(factors)); + assertTrue( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertTrue( + Qudt.Units.KiloJ.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.MilliOHM.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.MilliS.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); factors = new Object[] {Qudt.Units.KiloGM, 1, Qudt.Units.K, -1, Qudt.Units.SEC, -3}; - Assertions.assertFalse(Qudt.Units.W__PER__K.matches(factors)); - Assertions.assertFalse(Qudt.Units.V__PER__K.matches(factors)); + assertFalse( + Qudt.Units.W__PER__K.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.V__PER__K.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); } @Test @@ -198,20 +279,32 @@ public void testScaledFactors_negExpFirst() { new Object[] { Qudt.Units.SEC, -2, Qudt.Units.KiloGM, 1, Qudt.Units.M, 1, Qudt.Units.KiloM, 1 }; - assertTrue(Qudt.Units.KiloN__M.matches(factors)); + assertTrue( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); factors = new Object[] { Qudt.Units.KiloGM, 1, Qudt.Units.SEC, -2, Qudt.Units.M, 1, Qudt.Units.KiloM, 1 }; - assertTrue(Qudt.Units.KiloN__M.matches(factors)); + assertTrue( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); } @Test public void test_squareInNominator() { Object[] factors = new Object[] {Qudt.Units.MilliM, 2, Qudt.Units.SEC, -1}; - assertTrue(Qudt.Units.MilliM2__PER__SEC.matches(factors)); + assertTrue( + Qudt.Units.MilliM2__PER__SEC.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); factors = new Object[] {Qudt.Units.KiloGM, 2, Qudt.Units.SEC, -2}; - assertTrue(Qudt.Units.KiloGM2__PER__SEC2.matches(factors)); + assertTrue( + Qudt.Units.KiloGM2__PER__SEC2.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); } @Test @@ -241,7 +334,11 @@ public void testScale_squareInDenominator1() { Qudt.Units.KiloSEC, -2 }; - assertTrue(Qudt.Units.N__PER__M2.matches(factors)); + assertTrue( + Qudt.Units.N__PER__M2.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse(Qudt.Units.N__PER__M2.matches(FactorUnitSelection.fromFactorUnitSpec(factors))); } @Test @@ -257,7 +354,14 @@ public void testScale_squareInDenominator2() { Qudt.Units.MilliSEC, -2 }; - assertTrue(Qudt.Units.N__PER__M2.matches(factors)); + assertTrue( + Qudt.Units.N__PER__M2.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.N__PER__M2.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.EXACT)); } @Test @@ -282,7 +386,15 @@ public void testScaledFactorsWithChangingOrder() { factorUnits.stream() .flatMap(fu -> Stream.of(fu.getUnit(), fu.getExponent())) .toArray(); - assertTrue(Qudt.Units.KiloN__M.matches(factors), () -> "failed for " + factorUnits); + assertTrue( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED), + () -> "failed for " + factorUnits); + assertFalse( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors)), + () -> "failed for " + factorUnits); successfulFor.add(new ArrayList<>(factorUnits)); } } catch (AssertionFailedError e) { @@ -292,15 +404,35 @@ public void testScaledFactorsWithChangingOrder() { System.err.println(factorUnits); throw e; } - assertTrue(Qudt.Units.KiloN__M.matches(factors), () -> "failed for " + factorUnits); - assertTrue(Qudt.Units.KiloJ.matches(factors), () -> "failed for " + factorUnits); - Assertions.assertFalse( - Qudt.Units.MilliOHM.matches(factors), () -> "failed for " + factorUnits); - Assertions.assertFalse( - Qudt.Units.MilliS.matches(factors), () -> "failed for " + factorUnits); + assertTrue( + Qudt.Units.KiloN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED), + () -> "failed for " + factorUnits); + assertTrue( + Qudt.Units.KiloJ.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED), + () -> "failed for " + factorUnits); + assertFalse( + Qudt.Units.MilliOHM.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED), + () -> "failed for " + factorUnits); + assertFalse( + Qudt.Units.MilliS.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED), + () -> "failed for " + factorUnits); factors = new Object[] {Qudt.Units.KiloGM, 1, Qudt.Units.K, -1, Qudt.Units.SEC, -3}; - Assertions.assertFalse(Qudt.Units.W__PER__K.matches(factors)); - Assertions.assertFalse(Qudt.Units.V__PER__K.matches(factors)); + assertFalse( + Qudt.Units.W__PER__K.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.V__PER__K.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); } @Test @@ -309,9 +441,18 @@ public void testMilliJ() { new Object[] { Qudt.Units.KiloGM, 1, Qudt.Units.SEC, -2, Qudt.Units.M, 1, Qudt.Units.MilliM, 1 }; - assertTrue(Qudt.Units.MilliN__M.matches(factors)); - Assertions.assertFalse(Qudt.Units.MilliH__PER__KiloOHM.matches(factors)); - assertTrue(Qudt.Units.MilliJ.matches(factors)); + assertTrue( + Qudt.Units.MilliN__M.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertFalse( + Qudt.Units.MilliH__PER__KiloOHM.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); + assertTrue( + Qudt.Units.MilliJ.matches( + FactorUnitSelection.fromFactorUnitSpec(factors), + FactorUnitMatchingMode.ALLOW_SCALED)); } @Test @@ -323,8 +464,12 @@ public void testSimplifyFactorUnits() { new FactorUnit(Qudt.Units.M, -1), new FactorUnit(Qudt.Units.M, -1))); assertEquals(2, simplified.size()); - assertTrue(Qudt.derivedUnitFromFactorUnits(simplified).contains(Qudt.Units.N__PER__M2)); - assertTrue(Qudt.derivedUnitFromFactorUnits(simplified).contains(Qudt.Units.PA)); + assertTrue( + Qudt.derivedUnitsFromFactorUnits(DerivedUnitSearchMode.EXACT, simplified) + .contains(Qudt.Units.N__PER__M2)); + assertTrue( + Qudt.derivedUnitsFromFactorUnits(DerivedUnitSearchMode.EXACT, simplified) + .contains(Qudt.Units.PA)); } @Test diff --git a/qudtlib-test/src/test/java/io/github/qudtlib/QudtTests.java b/qudtlib-test/src/test/java/io/github/qudtlib/QudtTests.java index 673b0e0..42611a7 100644 --- a/qudtlib-test/src/test/java/io/github/qudtlib/QudtTests.java +++ b/qudtlib-test/src/test/java/io/github/qudtlib/QudtTests.java @@ -27,8 +27,8 @@ public void testPrefix() { Assertions.assertNotNull(kilo); MatcherAssert.assertThat( new BigDecimal("1000"), Matchers.comparesEqualTo(kilo.getMultiplier())); - Assertions.assertEquals(Qudt.prefix(kilo.getIri()), kilo); - Assertions.assertEquals(Qudt.prefix(kilo.getIri()), kilo); + Assertions.assertEquals(Qudt.prefixRequired(kilo.getIri()), kilo); + Assertions.assertEquals(Qudt.prefixRequired(kilo.getIri()), kilo); } @SuppressWarnings("OptionalGetWithoutIsPresent") @@ -38,24 +38,25 @@ public void testUnit() { Assertions.assertTrue(metre.hasLabel("Metre")); Assertions.assertTrue(metre.hasLabel("Meter")); Assertions.assertEquals("Metre", metre.getLabelForLanguageTag("en").get().getString()); - Assertions.assertEquals(Qudt.unit(metre.getIri()), metre); - Assertions.assertEquals(Qudt.unit(metre.getIri()), metre); + Assertions.assertEquals(Qudt.unitRequired(metre.getIri()), metre); + Assertions.assertEquals(Qudt.unitRequired(metre.getIri()), metre); } @Test public void testQuantityKind() { QuantityKind length = Qudt.QuantityKinds.Length; Assertions.assertTrue(length.hasLabel("Length")); - Assertions.assertEquals(Qudt.quantityKind(length.getIri()), length); - Assertions.assertEquals(Qudt.quantityKind(length.getIri()), length); + Assertions.assertEquals(Qudt.quantityKindRequired(length.getIri()), length); + Assertions.assertEquals(Qudt.quantityKindRequired(length.getIri()), length); } @Test public void testQuantityKindForUnit() { - Unit unit = Qudt.unitFromLabel("Newton Meter"); + Unit unit = Qudt.unitFromLabelRequired("Newton Meter"); Set broad = Qudt.quantityKinds(unit); - Assertions.assertTrue(broad.contains(Qudt.quantityKindFromLocalname("Torque"))); - Assertions.assertTrue(broad.contains(Qudt.quantityKindFromLocalname("MomentOfForce"))); + Assertions.assertTrue(broad.contains(Qudt.quantityKindFromLocalnameRequired("Torque"))); + Assertions.assertTrue( + broad.contains(Qudt.quantityKindFromLocalnameRequired("MomentOfForce"))); unit = Qudt.Units.PA__PER__BAR; broad = Qudt.quantityKindsBroad(unit); Assertions.assertTrue(broad.contains(Qudt.QuantityKinds.PressureRatio)); @@ -65,53 +66,87 @@ public void testQuantityKindForUnit() { @Test public void testDerivedUnitFromMap() { Assertions.assertTrue( - Qudt.derivedUnit(List.of(Map.entry(Qudt.Units.M, -3))) + Qudt.derivedUnitsFromMap(DerivedUnitSearchMode.EXACT, Map.of(Qudt.Units.M, -3)) .contains(Qudt.Units.PER__M3)); Assertions.assertTrue( - Qudt.derivedUnit(Qudt.Units.MilliA, 1, Qudt.Units.IN, -1) + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.Units.MilliA, + 1, + Qudt.Units.IN, + -1) .contains(Qudt.Units.MilliA__PER__IN)); Assertions.assertTrue( - Qudt.derivedUnit(Qudt.Units.MOL, 1, Qudt.Units.M, -2, Qudt.Units.SEC, -1) + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.Units.MOL, + 1, + Qudt.Units.M, + -2, + Qudt.Units.SEC, + -1) .contains(Qudt.Units.MOL__PER__M2__SEC)); } @Test public void testUnitFromLabel() { - Assertions.assertEquals(Qudt.Units.N, Qudt.unitFromLabel("Newton")); - Assertions.assertEquals(Qudt.Units.M, Qudt.unitFromLabel("Metre")); - Assertions.assertEquals(Qudt.Units.M2, Qudt.unitFromLabel("SQUARE_METRE")); - Assertions.assertEquals(Qudt.Units.M2, Qudt.unitFromLabel("SQUARE METRE")); - Assertions.assertEquals(Qudt.Units.M3, Qudt.unitFromLabel("Cubic Metre")); - Assertions.assertEquals(Qudt.Units.GM, Qudt.unitFromLabel("Gram")); - Assertions.assertEquals(Qudt.Units.SEC, Qudt.unitFromLabel("second")); - Assertions.assertEquals(Qudt.Units.HZ, Qudt.unitFromLabel("Hertz")); - Assertions.assertEquals(Qudt.Units.DEG_C, Qudt.unitFromLabel("degree celsius")); - Assertions.assertEquals(Qudt.Units.DEG_F, Qudt.unitFromLabel("degree fahrenheit")); - Assertions.assertEquals(Qudt.Units.A, Qudt.unitFromLabel("ampere")); - Assertions.assertEquals(Qudt.Units.V, Qudt.unitFromLabel("volt")); - Assertions.assertEquals(Qudt.Units.W, Qudt.unitFromLabel("Watt")); - Assertions.assertEquals(Qudt.Units.LUX, Qudt.unitFromLabel("Lux")); - Assertions.assertEquals(Qudt.Units.LM, Qudt.unitFromLabel("Lumen")); - Assertions.assertEquals(Qudt.Units.CD, Qudt.unitFromLabel("Candela")); - Assertions.assertEquals(Qudt.Units.PA, Qudt.unitFromLabel("Pascal")); - Assertions.assertEquals(Qudt.Units.RAD, Qudt.unitFromLabel("Radian")); - Assertions.assertEquals(Qudt.Units.J, Qudt.unitFromLabel("Joule")); - Assertions.assertEquals(Qudt.Units.K, Qudt.unitFromLabel("Kelvin")); - Assertions.assertEquals(Qudt.Units.SR, Qudt.unitFromLabel("Steradian")); + Assertions.assertEquals(Qudt.Units.N, Qudt.unitFromLabelRequired("Newton")); + Assertions.assertEquals(Qudt.Units.M, Qudt.unitFromLabelRequired("Metre")); + Assertions.assertEquals(Qudt.Units.M2, Qudt.unitFromLabelRequired("SQUARE_METRE")); + Assertions.assertEquals(Qudt.Units.M2, Qudt.unitFromLabelRequired("SQUARE METRE")); + Assertions.assertEquals(Qudt.Units.M3, Qudt.unitFromLabelRequired("Cubic Metre")); + Assertions.assertEquals(Qudt.Units.GM, Qudt.unitFromLabelRequired("Gram")); + Assertions.assertEquals(Qudt.Units.SEC, Qudt.unitFromLabelRequired("second")); + Assertions.assertEquals(Qudt.Units.HZ, Qudt.unitFromLabelRequired("Hertz")); + Assertions.assertEquals(Qudt.Units.DEG_C, Qudt.unitFromLabelRequired("degree celsius")); + Assertions.assertEquals(Qudt.Units.DEG_F, Qudt.unitFromLabelRequired("degree fahrenheit")); + Assertions.assertEquals(Qudt.Units.A, Qudt.unitFromLabelRequired("ampere")); + Assertions.assertEquals(Qudt.Units.V, Qudt.unitFromLabelRequired("volt")); + Assertions.assertEquals(Qudt.Units.W, Qudt.unitFromLabelRequired("Watt")); + Assertions.assertEquals(Qudt.Units.LUX, Qudt.unitFromLabelRequired("Lux")); + Assertions.assertEquals(Qudt.Units.LM, Qudt.unitFromLabelRequired("Lumen")); + Assertions.assertEquals(Qudt.Units.CD, Qudt.unitFromLabelRequired("Candela")); + Assertions.assertEquals(Qudt.Units.PA, Qudt.unitFromLabelRequired("Pascal")); + Assertions.assertEquals(Qudt.Units.RAD, Qudt.unitFromLabelRequired("Radian")); + Assertions.assertEquals(Qudt.Units.J, Qudt.unitFromLabelRequired("Joule")); + Assertions.assertEquals(Qudt.Units.K, Qudt.unitFromLabelRequired("Kelvin")); + Assertions.assertEquals(Qudt.Units.SR, Qudt.unitFromLabelRequired("Steradian")); } @Test public void testUnitFromFactors() { Assertions.assertThrows( - IllegalArgumentException.class, () -> Qudt.derivedUnitFromFactors(Qudt.Units.M)); - Set units = Qudt.derivedUnitFromFactors(Qudt.Units.M, 3); + IllegalArgumentException.class, + () -> + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M)); + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 3); Assertions.assertTrue(units.contains(Qudt.Units.M3)); - units = Qudt.derivedUnitFromFactors(Qudt.Units.KiloGM, 1, Qudt.Units.M, -3); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.KiloGM, 1, Qudt.Units.M, -3); Assertions.assertTrue(units.contains(Qudt.Units.KiloGM__PER__M3)); - units = Qudt.derivedUnit(Qudt.Units.MOL, 1, Qudt.Units.M, -2, Qudt.Units.SEC, -1); + Assertions.assertEquals(1, units.size()); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.GM, 1, Qudt.Units.M, -3); + Assertions.assertTrue(units.contains(Qudt.Units.GM__PER__M3)); + Assertions.assertEquals(1, units.size()); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.Units.MOL, + 1, + Qudt.Units.M, + -2, + Qudt.Units.SEC, + -1); Assertions.assertTrue(units.contains(Qudt.Units.MOL__PER__M2__SEC)); units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.K, 1, Qudt.Units.M, @@ -122,7 +157,8 @@ public void testUnitFromFactors() { -1); Assertions.assertTrue(units.contains(Qudt.Units.K__M2__PER__KiloGM__SEC)); units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.BTU_IT, 1, Qudt.Units.FT, @@ -134,52 +170,141 @@ public void testUnitFromFactors() { Qudt.Units.DEG_F, -1); Assertions.assertTrue(units.contains(Qudt.Units.BTU_IT__FT__PER__FT2__HR__DEG_F)); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 1); + Assertions.assertTrue(units.contains(Qudt.Units.M)); + Assertions.assertFalse(units.contains(Qudt.Units.RAD)); // m per m should not match here! + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.KiloGM, 1, Qudt.Units.A, -1); + Assertions.assertEquals(0, units.size()); } @Test - public void testDerivedUnit1() { - Set units = Qudt.derivedUnit(Qudt.Units.M, 3); + public void testDerivedUnit() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 3); Assertions.assertTrue(units.contains(Qudt.Units.M3)); - units = Qudt.derivedUnit(Qudt.Units.M, 2); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 2); Assertions.assertTrue(units.contains(Qudt.Units.M2)); - units = Qudt.derivedUnit(Qudt.Units.K, -1); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.K, -1); Assertions.assertTrue(units.contains(Qudt.Units.PER__K)); - units = Qudt.derivedUnit(Qudt.Units.M, -2); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, -2); Assertions.assertTrue(units.contains(Qudt.Units.PER__M2)); } @Test - public void testDerivedUnitByIri1() { - Set units = Qudt.derivedUnit(Qudt.Units.M.getIri(), 3); + public void testDerivedUnitByIri() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M.getIri(), 3); Assertions.assertTrue(units.contains(Qudt.Units.M3)); - units = Qudt.derivedUnit(Qudt.Units.M.getIri(), 2); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M.getIri(), 2); Assertions.assertTrue(units.contains(Qudt.Units.M2)); - units = Qudt.derivedUnit(Qudt.Units.K.getIri(), -1); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.K.getIri(), -1); Assertions.assertTrue(units.contains(Qudt.Units.PER__K)); - units = Qudt.derivedUnit(Qudt.Units.M.getIri(), -2); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M.getIri(), -2); Assertions.assertTrue(units.contains(Qudt.Units.PER__M2)); } + @Test + public void testDerivedUnitByLocalname() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "M", 3); + Assertions.assertTrue(units.contains(Qudt.Units.M3)); + units = Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "M", 2); + Assertions.assertTrue(units.contains(Qudt.Units.M2)); + units = Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "K", -1); + Assertions.assertTrue(units.contains(Qudt.Units.PER__K)); + units = Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "M", -2); + Assertions.assertTrue(units.contains(Qudt.Units.PER__M2)); + } + + @Test + public void testDerivedUnitByLabel() { + Set units = + Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "Meter", 3); + Assertions.assertTrue(units.contains(Qudt.Units.M3)); + units = Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "Metre", 2); + Assertions.assertTrue(units.contains(Qudt.Units.M2)); + units = Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "Kelvin", -1); + Assertions.assertTrue(units.contains(Qudt.Units.PER__K)); + units = Qudt.derivedUnitsFromUnitExponentPairs(DerivedUnitSearchMode.EXACT, "Bar", -1); + Assertions.assertTrue(units.contains(Qudt.Units.PER__BAR)); + } + @Test public void testDerivedUnit2() { - Set units = Qudt.derivedUnit(Qudt.Units.KiloGM, 1, Qudt.Units.M, -3); + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 1, Qudt.Units.N, 1); + Assertions.assertTrue(units.contains(Qudt.Units.N__M)); + Assertions.assertTrue(units.contains(Qudt.Units.J)); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.KiloGM, 1, Qudt.Units.M, -3); Assertions.assertTrue(units.contains(Qudt.Units.KiloGM__PER__M3)); - units = Qudt.derivedUnit(Qudt.scaledUnit("Kilo", "Gram"), 1, Qudt.Units.M, -3); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.scaledUnit("Kilo", "Gram"), + 1, + Qudt.Units.M, + -3); Assertions.assertTrue(units.contains(Qudt.Units.KiloGM__PER__M3)); - units = Qudt.derivedUnit(Qudt.Units.N, 1, Qudt.Units.M, -2); + units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.N, 1, Qudt.Units.M, -2); Assertions.assertTrue(units.contains(Qudt.Units.N__PER__M2)); + Assertions.assertTrue(units.contains(Qudt.Units.PA)); + Assertions.assertEquals(2, units.size()); } @Test public void testDerivedUnit3() { - Set units = Qudt.derivedUnit(Qudt.Units.MOL, 1, Qudt.Units.M, -2, Qudt.Units.SEC, -1); + // test making sure overspecifying factors are rejected + Assertions.assertTrue( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.Units.M, + 1, + Qudt.Units.N, + 1, + Qudt.Units.SEC, + -2) + .isEmpty()); + + Set units = + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, + Qudt.Units.MOL, + 1, + Qudt.Units.M, + -2, + Qudt.Units.SEC, + -1); Assertions.assertTrue(units.contains(Qudt.Units.MOL__PER__M2__SEC)); } @Test public void testDerivedUnit4() { Set units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.K, 1, Qudt.Units.M, @@ -190,7 +315,8 @@ public void testDerivedUnit4() { -1); Assertions.assertTrue(units.contains(Qudt.Units.K__M2__PER__KiloGM__SEC)); units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 1, Qudt.Units.KiloGM, @@ -205,7 +331,8 @@ public void testDerivedUnit4() { @Test public void testDerivedUnit5() { Set units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.BTU_IT, 1, Qudt.Units.FT, @@ -218,7 +345,8 @@ public void testDerivedUnit5() { -1); Assertions.assertTrue(units.contains(Qudt.Units.BTU_IT__FT__PER__FT2__HR__DEG_F)); units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 1, Qudt.Units.KiloGM, @@ -231,7 +359,8 @@ public void testDerivedUnit5() { 1); Assertions.assertTrue(units.contains(Qudt.Units.N__M__PER__M2)); units = - Qudt.derivedUnit( + Qudt.derivedUnitsFromUnitExponentPairs( + DerivedUnitSearchMode.EXACT, Qudt.Units.M, 2, Qudt.Units.KiloGM, @@ -251,28 +380,32 @@ public void testScaledUnit() { Assertions.assertEquals(Qudt.Units.GigaHZ, unit); unit = Qudt.scaledUnit("Kilo", "Gram"); Assertions.assertEquals(Qudt.Units.KiloGM, unit); + unit = Qudt.scaledUnit("KILO", "GRAM"); + Assertions.assertEquals(Qudt.Units.KiloGM, unit); unit = Qudt.scaledUnit(Qudt.Prefixes.Nano, Qudt.Units.M); Assertions.assertEquals(Qudt.Units.NanoM, unit); } @Test public void testGetUnitFactors() { - Unit unit = Qudt.unitFromLabel("newton meter"); + Unit unit = Qudt.unitFromLabelRequired("newton meter"); List unitFactors = Qudt.factorUnits(unit); - Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("meter"), 2))); Assertions.assertTrue( - unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("kilogram"), 1))); + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("meter"), 2))); + Assertions.assertTrue( + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("kilogram"), 1))); Assertions.assertTrue( - unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("second"), -2))); - unit = Qudt.unitFromLabel("newton meter per square meter"); + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("second"), -2))); + unit = Qudt.unitFromLabelRequired("newton meter per square meter"); unitFactors = Qudt.factorUnits(unit); - Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("meter"), 2))); Assertions.assertTrue( - unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("meter"), -2))); + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("meter"), 2))); + Assertions.assertTrue( + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("meter"), -2))); Assertions.assertTrue( - unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("kilogram"), 1))); + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("kilogram"), 1))); Assertions.assertTrue( - unitFactors.contains(new FactorUnit(Qudt.unitFromLabel("second"), -2))); + unitFactors.contains(new FactorUnit(Qudt.unitFromLabelRequired("second"), -2))); unit = Qudt.Units.KiloN__M; unitFactors = Qudt.factorUnits(unit); Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.Units.KiloN, 1))); @@ -285,7 +418,7 @@ public void testGetUnitFactorsUnscaled() { List unitFactors = Qudt.factorUnits(unit); Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.Units.KiloN, 1))); Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.Units.M, 1))); - unitFactors = Qudt.unscaleFactorUnits(unitFactors); + unitFactors = Qudt.unscaledFactorUnits(unitFactors); Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.Units.N, 1))); Assertions.assertTrue(unitFactors.contains(new FactorUnit(Qudt.Units.M, 1))); } @@ -330,7 +463,7 @@ public void testConvert_L_to_GAL_US() { @Test public void testConvert_Celsius_to_Fahrenheit() { QuantityValue celsius100 = - new QuantityValue(new BigDecimal("100"), Qudt.unitFromLocalname("DEG_C")); + new QuantityValue(new BigDecimal("100"), Qudt.unitFromLocalnameRequired("DEG_C")); QuantityValue fahrenheit = Qudt.convert(celsius100, Qudt.unitIriFromLocalname("DEG_F")); Assertions.assertNotNull(fahrenheit); MatcherAssert.assertThat(