├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── benchmarks ├── build.gradle.kts └── src │ ├── files │ └── logicalaggregation │ │ ├── NoOffset.perfasm │ │ ├── ZeroOffset.perfasm │ │ ├── layout.txt │ │ ├── output.perfasm │ │ ├── perfnorm.csv │ │ └── results.txt │ └── jmh │ └── java │ └── io │ └── github │ └── richardstartin │ └── multimatcher │ └── benchmarks │ ├── DomainObject.java │ ├── EnumSchemaMatcherState.java │ ├── FieldsEnum.java │ ├── LargeClassifierBenchmark.java │ ├── Layout.java │ ├── LogicalAggregationBenchmark.java │ ├── OverlappingRulesBenchmark.java │ ├── SmallBenchmarkRules.java │ ├── SmallRuleSetBenchmark.java │ ├── StringSchemaMatcherState.java │ └── TestDomainObject.java ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lgtm.yml ├── multi-matcher-core ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── io │ │ └── github │ │ └── richardstartin │ │ └── multimatcher │ │ └── core │ │ ├── Classifier.java │ │ ├── Constraint.java │ │ ├── ConstraintAccumulator.java │ │ ├── Mask.java │ │ ├── MaskedClassifier.java │ │ ├── Matcher.java │ │ ├── MatchingConstraint.java │ │ ├── Operation.java │ │ ├── RuleSet.java │ │ ├── Schema.java │ │ ├── Utils.java │ │ ├── masks │ │ ├── BitsetMask.java │ │ ├── MaskStore.java │ │ ├── RoaringMask.java │ │ └── WordMask.java │ │ ├── matchers │ │ ├── ClassificationNode.java │ │ ├── ComparableMatcher.java │ │ ├── DoubleMatcher.java │ │ ├── GenericConstraintAccumulator.java │ │ ├── GenericMatcher.java │ │ ├── IntMatcher.java │ │ ├── LongMatcher.java │ │ ├── MutableNode.java │ │ ├── SelectivityHeuristics.java │ │ ├── StringConstraintAccumulator.java │ │ └── nodes │ │ │ ├── ComparableNode.java │ │ │ ├── DoubleNode.java │ │ │ ├── IntNode.java │ │ │ ├── LongNode.java │ │ │ └── Nodes.java │ │ └── schema │ │ ├── Attribute.java │ │ ├── AttributeNotRegistered.java │ │ ├── ComparableAttribute.java │ │ ├── DoubleAttribute.java │ │ ├── EnumAttribute.java │ │ ├── GenericAttribute.java │ │ ├── IntAttribute.java │ │ ├── LongAttribute.java │ │ └── StringAttribute.java │ └── test │ ├── java │ └── io │ │ └── github │ │ └── richardstartin │ │ └── multimatcher │ │ └── core │ │ ├── ClassifierTest.java │ │ ├── FileRules.java │ │ ├── FooTest.java │ │ ├── LargeClassifierTest.java │ │ ├── NegationTest.java │ │ ├── PropertyBasedTest.java │ │ ├── TestDomainObject.java │ │ ├── masks │ │ └── MaskTest.java │ │ └── matchers │ │ ├── ComparableMutableNodeTest.java │ │ ├── DoubleMutableNodeTest.java │ │ ├── IntNodeTest.java │ │ ├── IntNodeTestTiny.java │ │ ├── LongMutableNodeTest.java │ │ └── StringMutableMatcherTest.java │ └── resources │ ├── invalid.yaml │ ├── junit-platform.properties │ └── test.yaml └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | dependency-reduced-pom.xml 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | *.iml 22 | target/ 23 | .idea/ 24 | 25 | */build 26 | .gradle/ 27 | */out 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | 4 | jdk: 5 | - openjdk11 6 | - openjdk12 7 | - openjdk13 8 | 9 | 10 | # from https://docs.travis-ci.com/user/languages/java/#caching 11 | before_cache: 12 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 13 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 14 | cache: 15 | directories: 16 | - $HOME/.gradle/caches/ 17 | - $HOME/.gradle/wrapper/ 18 | 19 | before_install: 20 | - chmod +x gradlew 21 | 22 | # Install silently to ensure all pom are installed and compilation is OK: actual checks will be processed by script: 23 | # Including testClasses so tests will compile too. 24 | install: "./gradlew assemble testClasses" 25 | 26 | branches: 27 | only: 28 | - master 29 | 30 | script: 31 | - ./gradlew test jacocoTestReport 32 | 33 | after_success: 34 | - ./gradlew coveralls 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multi-matcher 2 | [![Build Status](https://travis-ci.org/richardstartin/multi-matcher.svg?branch=master)](https://travis-ci.org/richardstartin/multi-matcher) 3 | [![Coverage Status](https://coveralls.io/repos/github/richardstartin/bitrules/badge.svg?branch=master)](https://coveralls.io/github/richardstartin/multi-matcher?branch=master) 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/uk.co.openkappa/bitrules/badge.svg)](https://maven-badges.herokuapp.com/maven-central/uk.co.openkappa/bitrules) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | [![Javadoc](https://javadoc-badge.appspot.com/uk.co.openkappa/multi-matcher.svg?label=javadoc)](http://www.javadoc.io/doc/uk.co.openkappa/multi-matcher) 7 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/richardstartin/multi-matcher.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/richardstartin/multi-matcher/alerts/) 8 | [![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/richardstartin/multi-matcher.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/richardstartin/multi-matcher/context:java) 9 | 10 | I have often needed to implement tedious classification logic in data processing projects. The requirements are often ambiguous to the extent that it would be difficult to implement them even in SQL, with aspects such as fallback and overlap. This logic often ends up expressed as large blocks of nested if statements which are hard to read or modify and perform poorly. This small project aims to make such classification logic easier, and improve performance too. 11 | 12 | # usage 13 | 14 | Build a generic classification engine 15 | ```java 16 | Classifier classifier = Classifier.builder( 17 | Schema.create() 18 | .withAttribute("productType", Product::getProductType) 19 | .withAttribute("issueDate", Product::getIssueDate, Comparator.naturalOrder().reversed()) 20 | .withAttribute("productName", Product::getProductName) 21 | .withAttribute("availability", Product::getAvailability) 22 | .withAttribute("discountedPrice", value -> 0.2 * value.getPrice()) 23 | ).build(Arrays.asList( 24 | MatchingConstraint.named("rule1") 25 | .eq("productType", "silk") 26 | .startsWith("productName", "luxury") 27 | .gt("discountedPrice", 1000) 28 | .priority(0) 29 | .classification("EXPENSIVE_LUXURY_PRODUCTS") 30 | .build(), 31 | MatchingConstraint.named("rule2") 32 | .eq("productType", "caviar") 33 | .gt("discountedPrice", 100) 34 | .priority(1) 35 | .classification("EXPENSIVE_LUXURY_PRODUCTS") 36 | .build(), 37 | MatchingConstraint.anonymous() 38 | .eq("productName", "baked beans") 39 | .priority(2) 40 | .classification("CHEAP_FOOD") 41 | .build() 42 | ) 43 | ); 44 | ``` 45 | 46 | Classify 47 | 48 | ```java 49 | Product p = getProduct(); 50 | String classification = classifier.classification(p).orElse("UNCLASSIFIED"); 51 | ``` 52 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /benchmarks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.net.URI 2 | 3 | plugins { 4 | id("me.champeau.gradle.jmh") version "0.4.8" 5 | id("com.github.johnrengelman.shadow") version "5.0.0" 6 | } 7 | 8 | val deps: Map by extra 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | 16 | 17 | listOf( 18 | project(":multi-matcher-core"), 19 | "org.openjdk.jol:jol-core:0.10" 20 | ).forEach { 21 | jmh(it) 22 | testRuntime(it) 23 | } 24 | } 25 | 26 | jmh { 27 | jmhVersion = "1.23" 28 | // tests depend on jmh, not the other way around 29 | isIncludeTests = false 30 | warmupIterations = 5 31 | iterations = 5 32 | fork = 1 33 | } 34 | 35 | tasks.assemble { 36 | dependsOn(tasks.shadowJar) 37 | } 38 | 39 | 40 | // jmhJar task provided by jmh gradle plugin is currently broken 41 | // https://github.com/melix/jmh-gradle-plugin/issues/97 42 | // so instead, we configure the shadowJar task to have JMH bits in it 43 | tasks.shadowJar { 44 | archiveBaseName.set("benchmarks") 45 | archiveVersion.set("") 46 | archiveClassifier.set("") 47 | 48 | manifest { 49 | attributes(Pair("Main-Class", "org.openjdk.jmh.Main")) 50 | attributes(Pair("Multi-Release", "true")) 51 | } 52 | 53 | // include dependencies 54 | configurations.add(project.configurations.jmh.get()) 55 | // include benchmark classes 56 | from(project.sourceSets.jmh.get().output) 57 | // include generated java source, BenchmarkList and other JMH resources 58 | from(tasks.jmhRunBytecodeGenerator.get().outputs) 59 | // include compiled generated classes 60 | from(tasks.jmhCompileGeneratedClasses.get().outputs) 61 | 62 | dependsOn(tasks.jmhCompileGeneratedClasses) 63 | } 64 | -------------------------------------------------------------------------------- /benchmarks/src/files/logicalaggregation/results.txt: -------------------------------------------------------------------------------- 1 | Benchmark (offset) (sourceSize) (targetSize) Mode Cnt Score Error Units 2 | LogicalAggregationBenchmark.intersection 0 1024 256 thrpt 5 6.118 ± 1.049 ops/us 3 | LogicalAggregationBenchmark.intersection:·asm 0 1024 256 thrpt NaN --- 4 | LogicalAggregationBenchmark.intersection 256 1024 256 thrpt 5 8.025 ± 1.413 ops/us 5 | LogicalAggregationBenchmark.intersection:·asm 256 1024 256 thrpt NaN --- 6 | LogicalAggregationBenchmark.intersection 512 1024 256 thrpt 5 6.099 ± 1.363 ops/us 7 | LogicalAggregationBenchmark.intersection:·asm 512 1024 256 thrpt NaN --- 8 | LogicalAggregationBenchmark.intersection 768 1024 256 thrpt 5 8.005 ± 1.472 ops/us 9 | LogicalAggregationBenchmark.intersection:·asm 768 1024 256 thrpt NaN --- 10 | LogicalAggregationBenchmark.intersectionNoOffset 0 1024 256 thrpt 5 13.596 ± 1.724 ops/us 11 | LogicalAggregationBenchmark.intersectionNoOffset:·asm 0 1024 256 thrpt NaN --- 12 | LogicalAggregationBenchmark.intersectionNoOffset 256 1024 256 thrpt 5 14.256 ± 3.171 ops/us 13 | LogicalAggregationBenchmark.intersectionNoOffset:·asm 256 1024 256 thrpt NaN --- 14 | LogicalAggregationBenchmark.intersectionNoOffset 512 1024 256 thrpt 5 13.948 ± 4.214 ops/us 15 | LogicalAggregationBenchmark.intersectionNoOffset:·asm 512 1024 256 thrpt NaN --- 16 | LogicalAggregationBenchmark.intersectionNoOffset 768 1024 256 thrpt 5 14.823 ± 3.453 ops/us 17 | LogicalAggregationBenchmark.intersectionNoOffset:·asm 768 1024 256 thrpt NaN --- 18 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/DomainObject.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | public class DomainObject { 4 | private final String currency; 5 | private final long ipAddress; 6 | private final long timestamp; 7 | private final long amount; 8 | private final double rating; 9 | private final int id; 10 | 11 | public DomainObject(String currency, long ipAddress, long timestamp, long amount, double rating, int id) { 12 | this.currency = currency; 13 | this.ipAddress = ipAddress; 14 | this.timestamp = timestamp; 15 | this.amount = amount; 16 | this.rating = rating; 17 | this.id = id; 18 | } 19 | 20 | public String getCurrency() { 21 | return currency; 22 | } 23 | 24 | public long getIpAddress() { 25 | return ipAddress; 26 | } 27 | 28 | public long getTimestamp() { 29 | return timestamp; 30 | } 31 | 32 | public long getAmount() { 33 | return amount; 34 | } 35 | 36 | public double getRating() { 37 | return rating; 38 | } 39 | 40 | public int getId() { 41 | return id; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/EnumSchemaMatcherState.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import io.github.richardstartin.multimatcher.core.Classifier; 4 | import io.github.richardstartin.multimatcher.core.Schema; 5 | import org.openjdk.jmh.annotations.Level; 6 | import org.openjdk.jmh.annotations.Scope; 7 | import org.openjdk.jmh.annotations.Setup; 8 | import org.openjdk.jmh.annotations.State; 9 | 10 | import static io.github.richardstartin.multimatcher.benchmarks.FieldsEnum.*; 11 | 12 | @State(Scope.Thread) 13 | public class EnumSchemaMatcherState { 14 | 15 | 16 | Schema schema; 17 | Classifier classifier; 18 | DomainObject matching; 19 | DomainObject nonMatching; 20 | 21 | @Setup(Level.Trial) 22 | public void init() { 23 | schema = enumSchema(); 24 | classifier = Classifier.builder(schema) 25 | .build(SmallBenchmarkRules.ENUM_RULES); 26 | this.matching = SmallBenchmarkRules.matching(); 27 | this.nonMatching = SmallBenchmarkRules.nonMatching(); 28 | } 29 | 30 | private Schema enumSchema() { 31 | return Schema.create() 32 | .withAttribute(AMOUNT, DomainObject::getAmount) 33 | .withStringAttribute(CURRENCY, DomainObject::getCurrency) 34 | .withAttribute(ID, DomainObject::getId) 35 | .withAttribute(IP_ADDRESS, DomainObject::getIpAddress) 36 | .withAttribute(RATING, DomainObject::getRating) 37 | .withAttribute(TIMESTAMP, DomainObject::getTimestamp); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/FieldsEnum.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | public enum FieldsEnum { 4 | CURRENCY, 5 | IP_ADDRESS, 6 | TIMESTAMP, 7 | AMOUNT, 8 | RATING, 9 | ID; 10 | } 11 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/LargeClassifierBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import io.github.richardstartin.multimatcher.core.Classifier; 4 | import io.github.richardstartin.multimatcher.core.MatchingConstraint; 5 | import io.github.richardstartin.multimatcher.core.Schema; 6 | import org.openjdk.jmh.annotations.*; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.stream.IntStream; 11 | 12 | import static java.util.stream.Collectors.toList; 13 | 14 | @State(Scope.Benchmark) 15 | public class LargeClassifierBenchmark { 16 | 17 | 18 | @Param({"15000", "20000", "50000"}) 19 | int size; 20 | 21 | 22 | private Map message; 23 | private Classifier, String> classifier; 24 | 25 | @Setup(Level.Trial) 26 | public void init() { 27 | this.message = message(); 28 | this.classifier = largeDiscreteClassifier(size); 29 | } 30 | 31 | @Benchmark 32 | public String match() { 33 | return classifier.classificationOrNull(message); 34 | } 35 | 36 | public static Map message() { 37 | Map msg = new HashMap<>(); 38 | msg.put("attr1", "value0"); 39 | msg.put("attr2", "value0"); 40 | msg.put("attr3", "value0"); 41 | msg.put("attr4", "value0"); 42 | msg.put("attr5", "value0"); 43 | msg.put("attr6", "value9"); 44 | return msg; 45 | } 46 | 47 | public static Classifier, String> largeDiscreteClassifier(int size) { 48 | return Classifier. 49 | , String>builder(Schema.>create() 50 | .withStringAttribute("attr1", (Map map) -> (String)map.get("attr1")) 51 | .withStringAttribute("attr2", (Map map) -> (String)map.get("attr2")) 52 | .withStringAttribute("attr3", (Map map) -> (String)map.get("attr3")) 53 | .withStringAttribute("attr4", (Map map) -> (String)map.get("attr4")) 54 | .withStringAttribute("attr5", (Map map) -> (String)map.get("attr5")) 55 | .withStringAttribute("attr6", (Map map) -> (String)map.get("attr6")) 56 | ) 57 | .useDirectBuffers(true) 58 | .withOptimisedStorageSpace(100 * 1024 * 1024) 59 | .build(IntStream.range(0, size) 60 | .mapToObj(i -> MatchingConstraint.anonymous() 61 | .eq("attr1", "value" + (i / 10000)) 62 | .eq("attr2", "value" + (i / 1000)) 63 | .eq("attr3", "value" + (i / 500)) 64 | .eq("attr4", "value" + (i / 250)) 65 | .eq("attr5", "value" + (i / 100)) 66 | .eq("attr6", "value" + (i / 10)) 67 | .classification("SEGMENT" + i).build() 68 | ).collect(toList())); 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/Layout.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | import org.openjdk.jol.info.GraphLayout; 3 | 4 | import static io.github.richardstartin.multimatcher.benchmarks.LargeClassifierBenchmark.largeDiscreteClassifier; 5 | 6 | public class Layout { 7 | 8 | public static void main(String... args) { 9 | int size = Integer.parseInt(args[0]); 10 | var classifier = largeDiscreteClassifier(size); 11 | var parsed = GraphLayout.parseInstance(classifier); 12 | System.out.println(parsed.toPrintable()); 13 | System.out.println(parsed.toFootprint()); 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/OverlappingRulesBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import io.github.richardstartin.multimatcher.core.Classifier; 4 | import io.github.richardstartin.multimatcher.core.MatchingConstraint; 5 | import io.github.richardstartin.multimatcher.core.Schema; 6 | import org.openjdk.jmh.annotations.*; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.concurrent.ThreadLocalRandom; 11 | import java.util.function.Function; 12 | 13 | import static io.github.richardstartin.multimatcher.benchmarks.TestDomainObject.Colour.RED; 14 | 15 | 16 | @State(Scope.Benchmark) 17 | public class OverlappingRulesBenchmark { 18 | 19 | static TestDomainObject prototype() { 20 | return new TestDomainObject("a_1", "b_1", 21 | "c_1", "d_1", "e_1", 22 | 0D, 0, 0, RED); 23 | } 24 | 25 | private static Schema schema() { 26 | return Schema.create() 27 | .withAttribute(0, TestDomainObject::getField1) 28 | .withAttribute(1, TestDomainObject::getField2) 29 | .withAttribute(2, TestDomainObject::getField3) 30 | .withAttribute(3, TestDomainObject::getField4) 31 | .withAttribute(4, TestDomainObject::getField5) 32 | .withAttribute(5, TestDomainObject::getMeasure1) 33 | .withAttribute(6, TestDomainObject::getMeasure2) 34 | .withAttribute(7, TestDomainObject::getMeasure3) 35 | .withAttribute(8, TestDomainObject::getColour); 36 | } 37 | 38 | 39 | @Param({"32", "63", "1500", "15000", "20000"}) 40 | int count; 41 | 42 | private List inputs; 43 | private Classifier classifier; 44 | 45 | 46 | int index; 47 | private int[] indices; 48 | 49 | @Setup(Level.Trial) 50 | public void init() { 51 | inputs = new ArrayList<>(count); 52 | int[] factors = new int[] { count, count / 2, count / 4, count / 8, count / 16}; 53 | var constraints = expand(prototype(), x -> nextOverlapping(x, factors), inputs, count); 54 | classifier = Classifier.builder(schema()) 55 | .useDirectBuffers(true) 56 | .withOptimisedStorageSpace(100 << 20) 57 | .build(constraints); 58 | 59 | indices = new int[Integer.lowestOneBit(inputs.size())]; 60 | for (int i = 0; i < inputs.size(); ++i) { 61 | if (i < indices.length) { 62 | indices[i] = i; 63 | } else { 64 | int replacement = ThreadLocalRandom.current().nextInt(0, i); 65 | if (replacement < indices.length) { 66 | indices[replacement] = i; 67 | } 68 | } 69 | } 70 | } 71 | 72 | private TestDomainObject next() { 73 | return inputs.get(indices[(index + 1) & (indices.length - 1)]); 74 | } 75 | 76 | 77 | @Benchmark 78 | public String classify() { 79 | return classifier.classificationOrNull(next()); 80 | } 81 | 82 | 83 | private static List> expand(TestDomainObject prototype, 84 | Function next, 85 | List inputs, 86 | int count) { 87 | var constraints = new ArrayList>(count); 88 | for (int i = 0; i < count; ++i) { 89 | constraints.add( 90 | MatchingConstraint.anonymous() 91 | .eq(0, prototype.getField1()) 92 | .eq(1, prototype.getField2()) 93 | .eq(2, prototype.getField3()) 94 | .eq(3, prototype.getField4()) 95 | .eq(4, prototype.getField5()) 96 | .gt(5, prototype.getMeasure1() - 1e-7) 97 | .le(6, prototype.getMeasure2()) 98 | .ge(7, prototype.getMeasure3()) 99 | .eq(8, prototype.getColour()) 100 | .priority(i) 101 | .classification("class" + i) 102 | .build() 103 | ); 104 | inputs.add(prototype); 105 | prototype = next.apply(prototype); 106 | } 107 | return constraints; 108 | } 109 | 110 | private static TestDomainObject nextOverlapping(TestDomainObject prototype, int[] counts) { 111 | return prototype.clone() 112 | .setField1(next(prototype.getField1(), counts[0])) 113 | .setField2(next(prototype.getField2(), counts[1])) 114 | .setField3(next(prototype.getField3(), counts[2])) 115 | .setField4(next(prototype.getField4(), counts[3])) 116 | .setField5(next(prototype.getField5(), counts[4])) 117 | .setMeasure1(prototype.getMeasure1() + 1) 118 | .setMeasure2(prototype.getMeasure2() - 1) 119 | .setMeasure3(prototype.getMeasure2() + 1) 120 | .setColour(TestDomainObject.Colour.next(prototype.getColour())); 121 | } 122 | 123 | private static String next(String x, int count) { 124 | var split = x.split("_"); 125 | return split[0] + "_" + ((Integer.parseInt(split[1]) + 1) % count); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/SmallBenchmarkRules.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import io.github.richardstartin.multimatcher.core.MatchingConstraint; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import static io.github.richardstartin.multimatcher.benchmarks.FieldsEnum.*; 9 | 10 | public class SmallBenchmarkRules { 11 | 12 | public static DomainObject matching() { 13 | return new DomainObject("EUR", -1L, 25L, 10, 1.0, 0); 14 | } 15 | 16 | public static DomainObject nonMatching() { 17 | return new DomainObject("GBP", -1L, 5L, 10, 1.0, 0); 18 | } 19 | 20 | public static final List> ENUM_RULES = Arrays.asList( 21 | MatchingConstraint.anonymous() 22 | .eq(CURRENCY, "GBP") 23 | .gt(TIMESTAMP, 10L) 24 | .classification("c1") 25 | .build(), 26 | MatchingConstraint.anonymous() 27 | .eq(CURRENCY, "EUR") 28 | .gt(TIMESTAMP, 15L) 29 | .classification("c2") 30 | .build(), 31 | MatchingConstraint.anonymous() 32 | .eq(CURRENCY, "USD") 33 | .gt(TIMESTAMP, 25L) 34 | .gt(AMOUNT, 0) 35 | .classification("c3") 36 | .build() 37 | ); 38 | 39 | public static final List> STRING_RULES = Arrays.asList( 40 | MatchingConstraint.anonymous() 41 | .eq(CURRENCY.name(), "GBP") 42 | .gt(TIMESTAMP.name(), 10L) 43 | .classification("c1") 44 | .build(), 45 | MatchingConstraint.anonymous() 46 | .eq(CURRENCY.name(), "EUR") 47 | .gt(TIMESTAMP.name(), 15L) 48 | .classification("c2") 49 | .build(), 50 | MatchingConstraint.anonymous() 51 | .eq(CURRENCY.name(), "USD") 52 | .gt(TIMESTAMP.name(), 25L) 53 | .gt(AMOUNT.name(), 0) 54 | .classification("c3") 55 | .build() 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/SmallRuleSetBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | 5 | public class SmallRuleSetBenchmark { 6 | 7 | @Benchmark 8 | public String matchingEnum(EnumSchemaMatcherState state) { 9 | return state.classifier.classification(state.matching).orElse("NA"); 10 | } 11 | 12 | @Benchmark 13 | public String nonMatchingEnum(EnumSchemaMatcherState state) { 14 | return state.classifier.classification(state.nonMatching).orElse("NA"); 15 | } 16 | 17 | @Benchmark 18 | public String matchingString(StringSchemaMatcherState state) { 19 | return state.classifier.classification(state.matching).orElse("NA"); 20 | } 21 | 22 | @Benchmark 23 | public String nonMatchingString(StringSchemaMatcherState state) { 24 | return state.classifier.classification(state.nonMatching).orElse("NA"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/StringSchemaMatcherState.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import io.github.richardstartin.multimatcher.core.Classifier; 4 | import io.github.richardstartin.multimatcher.core.Schema; 5 | import org.openjdk.jmh.annotations.Level; 6 | import org.openjdk.jmh.annotations.Scope; 7 | import org.openjdk.jmh.annotations.Setup; 8 | import org.openjdk.jmh.annotations.State; 9 | 10 | import static io.github.richardstartin.multimatcher.benchmarks.FieldsEnum.*; 11 | import static io.github.richardstartin.multimatcher.benchmarks.FieldsEnum.TIMESTAMP; 12 | 13 | @State(Scope.Benchmark) 14 | public class StringSchemaMatcherState { 15 | 16 | Schema schema; 17 | Classifier classifier; 18 | DomainObject matching; 19 | DomainObject nonMatching; 20 | 21 | @Setup(Level.Trial) 22 | public void init() { 23 | schema = stringSchema(); 24 | classifier = Classifier.builder(schema) 25 | .build(SmallBenchmarkRules.STRING_RULES); 26 | this.matching = SmallBenchmarkRules.matching(); 27 | this.nonMatching = SmallBenchmarkRules.nonMatching(); 28 | } 29 | 30 | 31 | private Schema stringSchema() { 32 | return Schema.create() 33 | .withAttribute(AMOUNT.name(), DomainObject::getAmount) 34 | .withStringAttribute(CURRENCY.name(), DomainObject::getCurrency) 35 | .withAttribute(ID.name(), DomainObject::getId) 36 | .withAttribute(IP_ADDRESS.name(), DomainObject::getIpAddress) 37 | .withAttribute(RATING.name(), DomainObject::getRating) 38 | .withAttribute(TIMESTAMP.name(), DomainObject::getTimestamp); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/io/github/richardstartin/multimatcher/benchmarks/TestDomainObject.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.benchmarks; 2 | 3 | import java.util.UUID; 4 | import java.util.concurrent.ThreadLocalRandom; 5 | 6 | public class TestDomainObject { 7 | 8 | private String field1; 9 | private String field2; 10 | private String field3; 11 | private String field4; 12 | private String field5; 13 | private double measure1; 14 | private int measure2; 15 | private long measure3; 16 | private Colour colour; 17 | 18 | public TestDomainObject(String field1, 19 | String field2, 20 | String field3, 21 | String field4, 22 | String field5, 23 | double measure1, 24 | int measure2, 25 | long measure3, 26 | Colour colour) { 27 | this.field1 = field1; 28 | this.field2 = field2; 29 | this.field3 = field3; 30 | this.field4 = field4; 31 | this.field5 = field5; 32 | this.measure1 = measure1; 33 | this.measure2 = measure2; 34 | this.measure3 = measure3; 35 | this.colour = colour; 36 | 37 | } 38 | 39 | public static TestDomainObject random() { 40 | return new TestDomainObject(UUID.randomUUID().toString(), 41 | UUID.randomUUID().toString(), 42 | UUID.randomUUID().toString(), 43 | UUID.randomUUID().toString(), 44 | UUID.randomUUID().toString(), 45 | ThreadLocalRandom.current().nextDouble(), 46 | ThreadLocalRandom.current().nextInt(), 47 | ThreadLocalRandom.current().nextLong(), 48 | Colour.RED 49 | ); 50 | } 51 | 52 | public String getField1() { 53 | return field1; 54 | } 55 | 56 | public TestDomainObject setField1(String field1) { 57 | this.field1 = field1; 58 | return this; 59 | } 60 | 61 | public String getField2() { 62 | return field2; 63 | } 64 | 65 | public TestDomainObject setField2(String field2) { 66 | this.field2 = field2; 67 | return this; 68 | } 69 | 70 | public String getField3() { 71 | return field3; 72 | } 73 | 74 | public TestDomainObject setField3(String field3) { 75 | this.field3 = field3; 76 | return this; 77 | } 78 | 79 | public String getField4() { 80 | return field4; 81 | } 82 | 83 | public TestDomainObject setField4(String field4) { 84 | this.field4 = field4; 85 | return this; 86 | } 87 | 88 | public String getField5() { 89 | return field5; 90 | } 91 | 92 | public TestDomainObject setField5(String field5) { 93 | this.field5 = field5; 94 | return this; 95 | } 96 | 97 | public double getMeasure1() { 98 | return measure1; 99 | } 100 | 101 | public TestDomainObject setMeasure1(double measure1) { 102 | this.measure1 = measure1; 103 | return this; 104 | } 105 | 106 | public int getMeasure2() { 107 | return measure2; 108 | } 109 | 110 | public TestDomainObject setMeasure2(int measure2) { 111 | this.measure2 = measure2; 112 | return this; 113 | } 114 | 115 | public long getMeasure3() { 116 | return measure3; 117 | } 118 | 119 | public TestDomainObject setMeasure3(long measure3) { 120 | this.measure3 = measure3; 121 | return this; 122 | } 123 | 124 | public Colour getColour() { 125 | return colour; 126 | } 127 | 128 | public TestDomainObject setColour(Colour colour) { 129 | this.colour = colour; 130 | return this; 131 | } 132 | 133 | public enum Fields { 134 | FIELD1, 135 | MEASURE1 136 | } 137 | 138 | public enum Colour { 139 | RED, BLUE, YELLOW; 140 | 141 | private static Colour[] VALUES = values(); 142 | 143 | public static Colour next(Colour colour) { 144 | return VALUES[(colour.ordinal() + 1) % VALUES.length]; 145 | } 146 | } 147 | 148 | public TestDomainObject clone() { 149 | return new TestDomainObject( 150 | field1, field2, field3, field4, field5, measure1, measure2, measure3, colour 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.jfrog.bintray.gradle.BintrayExtension 2 | 3 | plugins { 4 | id("net.researchgate.release") version "2.8.0" 5 | id("com.jfrog.bintray") version "1.8.4" apply false 6 | id("com.github.kt3k.coveralls") version "2.8.4" apply false 7 | } 8 | 9 | // some parts of the Kotlin DSL don't work inside a `subprojects` block yet, so we do them the old way 10 | // (without typesafe accessors) 11 | 12 | subprojects { 13 | // used in per-subproject dependencies 14 | @Suppress("UNUSED_VARIABLE") val deps by extra { 15 | mapOf( 16 | "jupiter" to "5.5.2", 17 | "jackson" to "2.10.0", 18 | "guava" to "28.1-jre", 19 | "roaringbitmap" to "0.8.13", 20 | "fastutil" to "8.3.1", 21 | "commons-collections" to "4.2" 22 | ) 23 | } 24 | 25 | apply(plugin = "java-library") 26 | apply(plugin = "jacoco") 27 | apply(plugin = "com.github.kt3k.coveralls") 28 | 29 | repositories { 30 | jcenter() 31 | } 32 | 33 | tasks.withType { 34 | options.isDeprecation = true 35 | options.isWarnings = true 36 | if (JavaVersion.current().isJava9Compatible) { 37 | options.compilerArgs = listOf("--release", "11") 38 | } 39 | } 40 | 41 | configure { 42 | sourceCompatibility = JavaVersion.VERSION_11 43 | targetCompatibility = JavaVersion.VERSION_11 44 | group = "io.github.richardstartin" 45 | } 46 | 47 | tasks.named("jacocoTestReport") { 48 | reports { 49 | // used by coveralls 50 | xml.isEnabled = true 51 | } 52 | } 53 | } 54 | 55 | subprojects.filter { listOf("multi-matcher-core").contains(it.name) }.forEach { project -> 56 | project.run { 57 | apply(plugin = "maven-publish") 58 | apply(plugin = "com.jfrog.bintray") 59 | 60 | tasks { 61 | register("sourceJar") { 62 | from(project.the()["main"].allJava) 63 | archiveClassifier.set("sources") 64 | } 65 | 66 | register("docJar") { 67 | from(project.tasks["javadoc"]) 68 | archiveClassifier.set("javadoc") 69 | } 70 | } 71 | 72 | configure { 73 | publications { 74 | register("bintray") { 75 | groupId = project.group.toString() 76 | artifactId = project.name 77 | version = project.version.toString() 78 | 79 | from(components["java"]) 80 | artifact(tasks["sourceJar"]) 81 | artifact(tasks["docJar"]) 82 | 83 | // requirements for maven central 84 | // https://central.sonatype.org/pages/requirements.html 85 | pom { 86 | name.set("${project.group}:${project.name}") 87 | description.set("Fast classification/tagging of objects.") 88 | url.set("https://github.com/richardstartin/multi-matcher") 89 | issueManagement { 90 | system.set("GitHub Issue Tracking") 91 | url.set("https://github.com/richardstartin/multi-matcher/issues") 92 | } 93 | licenses { 94 | license { 95 | name.set("Apache 2") 96 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 97 | distribution.set("repo") 98 | } 99 | } 100 | developers { 101 | developer { 102 | id.set("richardstartin") 103 | name.set("Richard Startin") 104 | email.set("richard@openkappa.co.uk") 105 | url.set("https://richardstartin.github.io/multi-matcher") 106 | roles.addAll("architect", "developer", "maintainer") 107 | timezone.set("0") 108 | } 109 | } 110 | scm { 111 | connection.set("scm:git:https://github.com/richardstartin/multi-matcher.git") 112 | developerConnection.set("scm:git:https://github.com/richardstartin/multi-matcher.git") 113 | url.set("https://github.com/richardstartin/multi-matcher") 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | configure { 121 | user = rootProject.findProperty("bintrayUser")?.toString() 122 | key = rootProject.findProperty("bintrayApiKey")?.toString() 123 | setPublications("bintray") 124 | 125 | with(pkg) { 126 | repo = "maven" 127 | setLicenses("Apache-2.0") 128 | vcsUrl = "https://github.com/richardstartin/multi-matcher" 129 | // use "bintray package per artifact" to match the auto-gen'd pkg structure inherited from 130 | // Maven Central's artifacts 131 | name = "uk.co.openkappa:${project.name}" 132 | userOrg = "multi-matcher" 133 | 134 | with(version) { 135 | name = project.version.toString() 136 | released = java.util.Date().toString() 137 | vcsTag = "multi-matcher-${project.version}" 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | tasks { 145 | create("build") { 146 | // dummy build task to appease release plugin 147 | } 148 | } 149 | 150 | release { 151 | tagTemplate = "multi-matcher-\$version" 152 | } 153 | 154 | tasks.afterReleaseBuild { 155 | dependsOn(tasks.named("bintrayUpload")) 156 | } 157 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version = 0.0.1-SNAPSHOT 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardstartin/multi-matcher/a9fdc099c1f96a41e0becc76b74170a0c3ff34f9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /lgtm.yml: -------------------------------------------------------------------------------- 1 | extraction: 2 | java: 3 | index: 4 | java_version: "11" 5 | -------------------------------------------------------------------------------- /multi-matcher-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val deps: Map by extra 2 | 3 | dependencies { 4 | implementation("org.roaringbitmap:RoaringBitmap:${deps["roaringbitmap"]}") 5 | implementation("org.apache.commons:commons-collections4:${deps["commons-collections"]}") 6 | implementation("it.unimi.dsi:fastutil:${deps["fastutil"]}") 7 | testImplementation("com.google.guava:guava:${deps["guava"]}") 8 | testImplementation("org.junit.jupiter:junit-jupiter-api:${deps["jupiter"]}") 9 | testImplementation("org.junit.jupiter:junit-jupiter-engine:${deps["jupiter"]}") 10 | testCompile("org.junit.jupiter:junit-jupiter-params:${deps["jupiter"]}") 11 | testImplementation("com.fasterxml.jackson.core:jackson-databind:${deps["jackson"]}") 12 | testImplementation("com.fasterxml.jackson.core:jackson-annotations:${deps["jackson"]}") 13 | testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${deps["jackson"]}") 14 | testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${deps["jackson"]}") 15 | 16 | 17 | } 18 | 19 | 20 | tasks.test { 21 | useJUnitPlatform() 22 | failFast = true 23 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Classifier.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | 4 | import io.github.richardstartin.multimatcher.core.masks.BitsetMask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.masks.RoaringMask; 7 | import io.github.richardstartin.multimatcher.core.masks.WordMask; 8 | 9 | import java.util.*; 10 | import java.util.function.Consumer; 11 | 12 | import static java.util.Comparator.comparingInt; 13 | 14 | /** 15 | * Classifies objects according to constraints applied to 16 | * registered attributes. 17 | * 18 | * @param the classification input type 19 | * @param the classification result type 20 | */ 21 | public interface Classifier { 22 | 23 | 24 | /** 25 | * Gets a new builder for a classifier 26 | * 27 | * @param the create key type 28 | * @param the type named the classified objects 29 | * @param the classification type 30 | * @param schema the schema 31 | * @return a new classifier builder 32 | */ 33 | static 34 | ClassifierBuilder builder(Schema schema) { 35 | return new ClassifierBuilder<>(schema); 36 | } 37 | 38 | /** 39 | * Visits all classifications matching the value 40 | * 41 | * @param value the value to match 42 | * @param consumer the classification consumer 43 | */ 44 | void forEachClassification(T value, Consumer consumer); 45 | 46 | /** 47 | * Counts how many rules match the value 48 | * 49 | * @param value the value to match 50 | * @return the number of matching rules 51 | */ 52 | int matchCount(T value); 53 | 54 | /** 55 | * Gets the highest priority classification, or none if no constraints are satisfied. 56 | * 57 | * @param value the value to classifications. 58 | * @return the best classification, or empty if no constraints are satisfied 59 | */ 60 | Optional classification(T value); 61 | 62 | /** 63 | * Gets the highest priority classification, or none if no constraints are satisfied. 64 | * 65 | * @param value the value to classifications. 66 | * @return the best classification, or null if no constraints are satisfied 67 | */ 68 | C classificationOrNull(T value); 69 | 70 | @SuppressWarnings("unchecked") 71 | class ClassifierBuilder { 72 | 73 | private final Schema schema; 74 | private final Map>> accumulators; 75 | private Classification[] classifications; 76 | private boolean useDirectBuffers = false; 77 | private int optimisedStorageSpace = 0; 78 | 79 | public ClassifierBuilder(Schema schema) { 80 | this.schema = schema; 81 | this.accumulators = schema.newMap(); 82 | } 83 | 84 | private static int order(int priority) { 85 | return Integer.MAX_VALUE - priority; 86 | } 87 | 88 | public ClassifierBuilder useDirectBuffers(boolean useDirectBuffers) { 89 | this.useDirectBuffers = useDirectBuffers; 90 | return this; 91 | } 92 | 93 | public ClassifierBuilder withOptimisedStorageSpace(int optimisedStorageSpace) { 94 | this.optimisedStorageSpace = optimisedStorageSpace; 95 | return this; 96 | } 97 | 98 | /** 99 | * Build a classifier from some matchers 100 | * 101 | * @param constraints the matching constraints 102 | * @return the classifier 103 | */ 104 | public Classifier build(List> constraints) { 105 | int maxPriority = constraints.size(); 106 | if (maxPriority < WordMask.MAX_CAPACITY) { 107 | return build(constraints, WordMask.store(maxPriority), maxPriority); 108 | } 109 | if (maxPriority < BitsetMask.MAX_CAPACITY) { 110 | return build(constraints, BitsetMask.store(maxPriority), maxPriority); 111 | } 112 | return build(constraints, RoaringMask.store(optimisedStorageSpace, useDirectBuffers), maxPriority); 113 | } 114 | 115 | private > 116 | MaskedClassifier build(List> specs, 117 | MaskStore maskStore, 118 | int max) { 119 | classifications = (Classification[]) new Object[max]; 120 | int sequence = 0; 121 | specs.sort(comparingInt(rd -> order(rd.getPriority()))); 122 | for (var spec : specs) { 123 | addMatchingConstraint(spec, sequence++, maskStore, max); 124 | } 125 | return new MaskedClassifier<>(classifications, freezeMatchers(), maskStore.contiguous(max)); 126 | } 127 | 128 | private > 129 | void addMatchingConstraint(MatchingConstraint matchInfo, 130 | int priority, 131 | MaskStore maskStore, 132 | int max) { 133 | classifications[priority] = matchInfo.getClassification(); 134 | for (var pair : matchInfo.getConstraints().entrySet()) { 135 | getOrCreateAccumulator(pair.getKey(), maskStore, max) 136 | .addConstraint(pair.getValue(), priority); 137 | } 138 | } 139 | 140 | private > 141 | ConstraintAccumulator getOrCreateAccumulator(Key key, 142 | MaskStore maskStore, 143 | int max) { 144 | var accumulator = (ConstraintAccumulator) accumulators.get(key); 145 | if (null == accumulator) { 146 | accumulator = schema.getAttribute(key).newAccumulator(maskStore, max); 147 | accumulators.put(key, accumulator); 148 | } 149 | return accumulator; 150 | } 151 | 152 | private > 153 | Matcher[] freezeMatchers() { 154 | var matchers = new Matcher[accumulators.size()]; 155 | int i = 0; 156 | for (var accumulator : accumulators.values()) { 157 | matchers[i++] = accumulator.toMatcher(); 158 | } 159 | Arrays.sort(matchers, comparingInt(x -> (int) (x.averageSelectivity() * 1000))); 160 | return (Matcher[]) matchers; 161 | } 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Constraint.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | public class Constraint { 4 | 5 | private Operation operation; 6 | private Object value; 7 | 8 | public Constraint() { 9 | } 10 | 11 | public static Constraint lessThan(Comparable value) { 12 | return condition(Operation.LT, value); 13 | } 14 | 15 | public static Constraint lessThanOrEqualTo(Comparable value) { 16 | return condition(Operation.LE, value); 17 | } 18 | 19 | public static Constraint equalTo(Object value) { 20 | return condition(Operation.EQ, value); 21 | } 22 | 23 | public static Constraint notEqualTo(Object value) { 24 | return condition(Operation.NE, value); 25 | } 26 | 27 | public static Constraint greaterThan(Comparable value) { 28 | return condition(Operation.GT, value); 29 | } 30 | 31 | public static Constraint greaterThanOrEqualTo(Comparable value) { 32 | return condition(Operation.GE, value); 33 | } 34 | 35 | public static Constraint startsWith(String prefix) { 36 | return condition(Operation.STARTS_WITH, prefix); 37 | } 38 | 39 | private static Constraint condition(Operation op, Object value) { 40 | Constraint rc = new Constraint(); 41 | rc.operation = op; 42 | rc.value = value; 43 | return rc; 44 | } 45 | 46 | public Operation getOperation() { 47 | return operation; 48 | } 49 | 50 | public T getValue() { 51 | return (T) value; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/ConstraintAccumulator.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | /** 4 | * A matcher is a column named constraints on the same attribute. 5 | * 6 | * @param the type named the classified objects 7 | */ 8 | public interface ConstraintAccumulator { 9 | 10 | /** 11 | * Adds a constraint to the rule 12 | * 13 | * @param constraint a condition which must be matched by inputs 14 | * @param priority the identity named the constraint 15 | */ 16 | boolean addConstraint(Constraint constraint, int priority); 17 | 18 | /** 19 | * Freezes the column. DO NOT remove constraints after calling this method. 20 | */ 21 | Matcher toMatcher(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Mask.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import java.util.function.IntConsumer; 4 | import java.util.stream.IntStream; 5 | 6 | public interface Mask> { 7 | 8 | static > U with(U mask, int priority) { 9 | mask.add(priority); 10 | return mask; 11 | } 12 | 13 | static > U without(U mask, int priority) { 14 | mask.remove(priority); 15 | return mask; 16 | } 17 | 18 | void add(int id); 19 | 20 | void remove(int id); 21 | 22 | T inPlaceAndNot(T other); 23 | 24 | T inPlaceAnd(T other); 25 | 26 | T inPlaceOr(T other); 27 | 28 | T inPlaceNot(int max); 29 | 30 | T resetTo(Mask other); 31 | 32 | void clear(); 33 | 34 | T unwrap(); 35 | 36 | IntStream stream(); 37 | 38 | void forEach(IntConsumer consumer); 39 | 40 | int first(); 41 | 42 | T clone(); 43 | 44 | void optimise(); 45 | 46 | boolean isEmpty(); 47 | 48 | int cardinality(); 49 | 50 | default T and(T other) { 51 | return clone().inPlaceAnd(other); 52 | } 53 | 54 | default T andNot(T other) { 55 | return clone().inPlaceAndNot(other); 56 | } 57 | 58 | default T or(T other) { 59 | return clone().inPlaceOr(other); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/MaskedClassifier.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Consumer; 5 | 6 | public class MaskedClassifier, Input, Classification> 7 | implements Classifier { 8 | 9 | private final Classification[] classifications; 10 | private final Matcher[] matchers; 11 | private final Mask mask; 12 | private final ThreadLocal context; 13 | 14 | public MaskedClassifier(Classification[] classifications, 15 | Matcher[] matchers, 16 | Mask mask) { 17 | this.classifications = classifications; 18 | this.matchers = matchers; 19 | this.mask = mask; 20 | this.context = ThreadLocal.withInitial(mask::clone); 21 | mask.optimise(); 22 | } 23 | 24 | @Override 25 | public void forEachClassification(Input value, Consumer consumer) { 26 | match(value).forEach(i -> consumer.accept(classifications[i])); 27 | } 28 | 29 | @Override 30 | public int matchCount(Input value) { 31 | return match(value).cardinality(); 32 | } 33 | 34 | @Override 35 | public Optional classification(Input value) { 36 | return Optional.ofNullable(classificationOrNull(value)); 37 | } 38 | 39 | @Override 40 | public Classification classificationOrNull(Input value) { 41 | var matches = match(value); 42 | return matches.isEmpty() 43 | ? null 44 | : classifications[matches.first()]; 45 | } 46 | 47 | private MaskType match(Input value) { 48 | var ctx = context.get().resetTo(mask); 49 | for (var matcher : matchers) { 50 | matcher.match(value, ctx); 51 | if (ctx.isEmpty()) { 52 | break; 53 | } 54 | } 55 | return ctx; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Matcher.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | public interface Matcher { 4 | /** 5 | * Returns the identities named all named the constraints which are satisfied bt the value, 6 | * so long as they have not already been invalidated by prior mismatches on other attributes 7 | * 8 | * @param value the value to match 9 | * @param context the identities named constraints satisfied prior to the match 10 | */ 11 | void match(T value, MaskType context); 12 | 13 | default float averageSelectivity() { 14 | return 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/MatchingConstraint.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.UUID; 7 | 8 | import static java.util.Objects.requireNonNull; 9 | 10 | 11 | public class MatchingConstraint { 12 | 13 | private String id; 14 | private Map constraints; 15 | private int priority; 16 | private Classification classification; 17 | @SuppressWarnings("unused") 18 | private MatchingConstraint() { 19 | } 20 | public MatchingConstraint(String id, 21 | Map constraints, 22 | int priority, 23 | Classification classification) { 24 | this.id = id; 25 | this.constraints = constraints; 26 | this.priority = priority; 27 | this.classification = classification; 28 | } 29 | 30 | public static Builder named(String ruleId) { 31 | return new Builder<>(ruleId); 32 | } 33 | 34 | public static Builder anonymous() { 35 | return new Builder<>(UUID.randomUUID().toString()); 36 | } 37 | 38 | public String getId() { 39 | return id; 40 | } 41 | 42 | public Map getConstraints() { 43 | return constraints; 44 | } 45 | 46 | public int getPriority() { 47 | return priority; 48 | } 49 | 50 | public Classification getClassification() { 51 | return classification; 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (this == o) return true; 57 | if (o == null || getClass() != o.getClass()) return false; 58 | MatchingConstraint that = (MatchingConstraint) o; 59 | return priority == that.priority && 60 | Objects.equals(id, that.id) && 61 | Objects.equals(constraints, that.constraints) && 62 | Objects.equals(classification, that.classification); 63 | } 64 | 65 | @Override 66 | public int hashCode() { 67 | return Objects.hash(id, constraints, priority, classification); 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "MatchingConstraint{" + 73 | "id='" + id + '\'' + 74 | ", constraints=" + constraints + 75 | ", priority=" + priority + 76 | ", classification=" + classification + 77 | '}'; 78 | } 79 | 80 | public static class Builder { 81 | 82 | private final String id; 83 | private Map constraints = new HashMap<>(); 84 | private int priority; 85 | private C classification; 86 | 87 | private Builder(String id) { 88 | this.id = id; 89 | } 90 | 91 | public Builder eq(K key, Object value) { 92 | return constraint(key, Constraint.equalTo(value)); 93 | } 94 | 95 | public Builder neq(K key, Object value) { 96 | return constraint(key, Constraint.notEqualTo(value)); 97 | } 98 | 99 | public Builder lt(K key, Comparable value) { 100 | return constraint(key, Constraint.lessThan(value)); 101 | } 102 | 103 | public Builder le(K key, Comparable value) { 104 | return constraint(key, Constraint.lessThanOrEqualTo(value)); 105 | } 106 | 107 | public Builder gt(K key, Comparable value) { 108 | return constraint(key, Constraint.greaterThan(value)); 109 | } 110 | 111 | public Builder ge(K key, Comparable value) { 112 | return constraint(key, Constraint.greaterThanOrEqualTo(value)); 113 | } 114 | 115 | public Builder startsWith(K key, String prefix) { 116 | return constraint(key, Constraint.startsWith(prefix)); 117 | } 118 | 119 | public Builder priority(int value) { 120 | this.priority = value; 121 | return this; 122 | } 123 | 124 | public Builder constraint(K key, Constraint constraint) { 125 | this.constraints.put(requireNonNull(key), requireNonNull(constraint)); 126 | return this; 127 | } 128 | 129 | public Builder classification(C classification) { 130 | this.classification = requireNonNull(classification); 131 | return this; 132 | } 133 | 134 | 135 | public MatchingConstraint build() { 136 | if (constraints.isEmpty()) { 137 | throw new IllegalStateException("Unconstrained rule"); 138 | } 139 | return new MatchingConstraint<>(id, constraints, priority, requireNonNull(classification)); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Operation.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | public enum Operation { 4 | GT(">"), 5 | LT("<"), 6 | LE("≤"), 7 | GE("≥"), 8 | EQ("="), 9 | NE("≠"), 10 | STARTS_WITH("starts_with"); 11 | 12 | public static int SIZE = values().length; 13 | 14 | private final String symbol; 15 | 16 | Operation(String symbol) { 17 | this.symbol = symbol; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return symbol; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/RuleSet.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | public interface RuleSet { 8 | 9 | List> constraints() throws IOException; 10 | 11 | default Optional> specification(String ruleId) throws IOException { 12 | for (var constraint : constraints()) { 13 | if (ruleId.equals(constraint.getId())) { 14 | return Optional.of(constraint); 15 | } 16 | } 17 | return Optional.empty(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Schema.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import io.github.richardstartin.multimatcher.core.schema.*; 4 | 5 | import java.util.Comparator; 6 | import java.util.EnumMap; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.function.*; 10 | 11 | /** 12 | * The attribute registry contains the association between 13 | * attribute keys and value accessors. A rule may not refer 14 | * to an attribute unless it is registered here. 15 | * 16 | * @param the type named key to store attributes against 17 | * @param the type named input object which may be classified. 18 | */ 19 | public class Schema { 20 | 21 | 22 | private final Supplier> prototype; 23 | private final Map> attributes; 24 | 25 | private Schema(Supplier> prototype, Map> attributes) { 26 | this.prototype = prototype; 27 | this.attributes = attributes; 28 | } 29 | 30 | private Schema(Map> attributes) { 31 | this.attributes = attributes; 32 | this.prototype = null; 33 | } 34 | 35 | /** 36 | * Create a new create 37 | * 38 | * @param the type named key to store attributes against 39 | * @param the type named objects that values can be extracted from 40 | * @return the new create 41 | */ 42 | public static Schema create() { 43 | return new Schema<>(new HashMap<>()); 44 | } 45 | 46 | /** 47 | * Create a new create 48 | * 49 | * @param enumType the type named the enum 50 | * @param the key type 51 | * @param the input type 52 | * @return a new create 53 | */ 54 | public static , Input> Schema create(Class enumType) { 55 | return new Schema<>(() -> new EnumMap<>(enumType), new EnumMap<>(enumType)); 56 | } 57 | 58 | 59 | /** 60 | * Registers a generic attribute with equality semantics only 61 | * 62 | * @param key the key named the attribute (rules refer to this) 63 | * @param accessor extracts a value named type Input from the classified object 64 | * @param the type named the attribute value 65 | * @return an attribute registry containing the attribute 66 | */ 67 | public Schema withAttribute(Key key, Function accessor) { 68 | attributes.put(key, new GenericAttribute<>(accessor)); 69 | return this; 70 | } 71 | 72 | /** 73 | * Registers a string attribute builder equality semantics only 74 | * 75 | * @param key the key named the attribute (rules refer to this) 76 | * @param accessor extracts a value named type Input from the classified object 77 | * @return an attribute registry containing the attribute 78 | */ 79 | public Schema withStringAttribute(Key key, Function accessor) { 80 | attributes.put(key, new StringAttribute<>(accessor)); 81 | return this; 82 | } 83 | 84 | /** 85 | * Registers an enum attribute with equality semantics only 86 | * 87 | * @param key the key named the attribute (rules refer to this) 88 | * @param accessor extracts a value named type Input from the classified object 89 | * @param type the enum type 90 | * @return an attribute registry containing the attribute 91 | */ 92 | public > Schema withEnumAttribute(Key key, Function accessor, Class type) { 93 | attributes.put(key, new EnumAttribute<>(type, accessor)); 94 | return this; 95 | } 96 | 97 | /** 98 | * Registers a generic attribute with equality and order semantics 99 | * 100 | * @param key the key named the attribute (rules refer to this) 101 | * @param accessor extracts a value named type Input from the classified object 102 | * @param the type named the attribute value 103 | * @return an attribute registry containing the attribute 104 | */ 105 | public Schema withAttribute(Key key, Function accessor, Comparator comparator) { 106 | attributes.put(key, new ComparableAttribute<>(comparator, accessor)); 107 | return this; 108 | } 109 | 110 | /** 111 | * Registers a double attribute with equality and order semantics 112 | * 113 | * @param key the key named the attribute (rules refer to this) 114 | * @param accessor extracts a value named type Input from the classified object 115 | * @return an attribute registry containing the attribute 116 | */ 117 | public Schema withAttribute(Key key, ToDoubleFunction accessor) { 118 | attributes.put(key, new DoubleAttribute<>(accessor)); 119 | return this; 120 | } 121 | 122 | /** 123 | * Registers an int attribute with equality and order semantics 124 | * 125 | * @param key the key named the attribute (rules refer to this) 126 | * @param accessor extracts a value named type Input from the classified object 127 | * @return an attribute registry containing the attribute 128 | */ 129 | public Schema withAttribute(Key key, ToIntFunction accessor) { 130 | attributes.put(key, new IntAttribute<>(accessor)); 131 | return this; 132 | } 133 | 134 | /** 135 | * Registers a long attribute with equality and order semantics 136 | * 137 | * @param key the key named the attribute (rules refer to this) 138 | * @param accessor extracts a value named type Input from the classified object 139 | * @return an attribute registry containing the attribute 140 | */ 141 | public Schema withAttribute(Key key, ToLongFunction accessor) { 142 | attributes.put(key, new LongAttribute<>(accessor)); 143 | return this; 144 | } 145 | 146 | /** 147 | * Get the attribute builder the supplied key if it exists 148 | * 149 | * @param key the key named the attribute 150 | * @return the attribute if registered, otherwise null 151 | */ 152 | Attribute getAttribute(Key key) { 153 | Attribute attribute = attributes.get(key); 154 | if (null == attribute) { 155 | throw new AttributeNotRegistered("No attribute " + key + " registered."); 156 | } 157 | return attribute; 158 | } 159 | 160 | @SuppressWarnings("unchecked") 161 | Map newMap() { 162 | if (null == prototype) { 163 | return newSizedHashMap(); 164 | } 165 | return (Map) prototype.get(); 166 | } 167 | 168 | private Map newSizedHashMap() { 169 | return new HashMap<>(attributes.size()); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/Utils.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import java.lang.reflect.Array; 4 | 5 | public class Utils { 6 | 7 | public static int nullCount(T... values) { 8 | int count = 0; 9 | for (T value : values) { 10 | count += null == value ? 1 : 0; 11 | } 12 | return count; 13 | } 14 | 15 | @SuppressWarnings("unchecked") 16 | public static T[] newArray(Class type, int size) { 17 | return (T[]) Array.newInstance(type, size); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/masks/MaskStore.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.masks; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | 5 | public interface MaskStore> { 6 | /** 7 | * Create an empty mask 8 | * 9 | * @return an empty mask 10 | */ 11 | MaskType newMask(); 12 | 13 | int newMaskId(); 14 | 15 | int storeMask(MaskType mask); 16 | 17 | MaskType getMask(int id); 18 | 19 | void add(int id, int bit); 20 | 21 | void remove(int id, int bit); 22 | 23 | void or(int from, int into); 24 | 25 | default void optimise(int id) { 26 | 27 | } 28 | 29 | MaskType getTemp(int copyAddress); 30 | 31 | void orInto(MaskType mask, int id); 32 | 33 | void andInto(MaskType mask, int id); 34 | 35 | /** 36 | * Create a contiguous mask starting at zero 37 | * 38 | * @param max the exclusive upper bound of the contiguous set 39 | * @return a contiguous mask with max bits set. 40 | */ 41 | MaskType contiguous(int max); 42 | 43 | int newContiguousMaskId(int max); 44 | 45 | 46 | boolean isEmpty(int id); 47 | 48 | /** 49 | * Create a mask from the specified values 50 | * 51 | * @param values the values to include 52 | * @return a mask from the values 53 | */ 54 | MaskType of(int... values); 55 | 56 | double averageSelectivity(int[] masks, int min, int max); 57 | 58 | default double averageSelectivity(int[] masks) { 59 | return averageSelectivity(masks, 0, masks.length); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/masks/RoaringMask.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.masks; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import org.roaringbitmap.IntIterator; 5 | import org.roaringbitmap.buffer.ImmutableRoaringBitmap; 6 | import org.roaringbitmap.buffer.MutableRoaringBitmap; 7 | 8 | import java.nio.ByteBuffer; 9 | import java.util.Arrays; 10 | import java.util.Objects; 11 | import java.util.function.IntConsumer; 12 | import java.util.stream.IntStream; 13 | 14 | public class RoaringMask implements Mask { 15 | 16 | private final OptimisedStorage storage; 17 | private ImmutableRoaringBitmap bitmap; 18 | private RoaringMask(OptimisedStorage storage, MutableRoaringBitmap bitmap) { 19 | this.storage = storage; 20 | this.bitmap = bitmap; 21 | } 22 | 23 | public RoaringMask(OptimisedStorage storage) { 24 | this(storage, new MutableRoaringBitmap()); 25 | } 26 | 27 | public static MaskStore store(int maxBufferSize, boolean direct) { 28 | return new Store(maxBufferSize, direct); 29 | } 30 | 31 | @Override 32 | public void add(int id) { 33 | ((MutableRoaringBitmap) bitmap).add(id); 34 | } 35 | 36 | @Override 37 | public void remove(int id) { 38 | ((MutableRoaringBitmap) bitmap).remove(id); 39 | } 40 | 41 | @Override 42 | public RoaringMask inPlaceAndNot(RoaringMask other) { 43 | ((MutableRoaringBitmap) bitmap).andNot(other.bitmap); 44 | return this; 45 | } 46 | 47 | @Override 48 | public RoaringMask inPlaceAnd(RoaringMask other) { 49 | ((MutableRoaringBitmap) bitmap).and(other.bitmap); 50 | return this; 51 | } 52 | 53 | @Override 54 | public RoaringMask inPlaceOr(RoaringMask other) { 55 | ((MutableRoaringBitmap) bitmap).or(other.bitmap); 56 | return this; 57 | } 58 | 59 | @Override 60 | public RoaringMask inPlaceNot(int max) { 61 | ((MutableRoaringBitmap) bitmap).flip(0L, max); 62 | return this; 63 | } 64 | 65 | @Override 66 | public RoaringMask resetTo(Mask other) { 67 | this.bitmap = other.unwrap().bitmap.toMutableRoaringBitmap(); 68 | return this; 69 | } 70 | 71 | @Override 72 | public void clear() { 73 | ((MutableRoaringBitmap) bitmap).clear(); 74 | } 75 | 76 | @Override 77 | public RoaringMask unwrap() { 78 | return this; 79 | } 80 | 81 | @Override 82 | public IntStream stream() { 83 | IntIterator it = bitmap.getIntIterator(); 84 | return IntStream.range(0, bitmap.getCardinality()) 85 | .map(i -> it.next()); 86 | } 87 | 88 | @Override 89 | public void forEach(IntConsumer consumer) { 90 | bitmap.forEach((org.roaringbitmap.IntConsumer) consumer::accept); 91 | } 92 | 93 | @Override 94 | public int first() { 95 | return bitmap.first(); 96 | } 97 | 98 | @Override 99 | public RoaringMask clone() { 100 | return new RoaringMask(storage, bitmap.toMutableRoaringBitmap()); 101 | } 102 | 103 | @Override 104 | public void optimise() { 105 | ((MutableRoaringBitmap) bitmap).trim(); 106 | ((MutableRoaringBitmap) bitmap).runOptimize(); 107 | this.bitmap = storage.consolidate(((MutableRoaringBitmap) bitmap)); 108 | } 109 | 110 | @Override 111 | public boolean isEmpty() { 112 | return bitmap.isEmpty(); 113 | } 114 | 115 | @Override 116 | public int cardinality() { 117 | return bitmap.getCardinality(); 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | if (this == o) return true; 123 | if (o == null || getClass() != o.getClass()) return false; 124 | RoaringMask that = (RoaringMask) o; 125 | return Objects.equals(bitmap, that.bitmap); 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | return Objects.hash(bitmap); 131 | } 132 | 133 | private static final class Store implements MaskStore { 134 | private final OptimisedStorage storage; 135 | 136 | private final ThreadLocal temp; 137 | 138 | private RoaringMask[] bitmaps = new RoaringMask[4]; 139 | private int maskId = 0; 140 | 141 | private Store(int bufferSize, boolean direct) { 142 | this.storage = new OptimisedStorage(direct 143 | ? ByteBuffer.allocateDirect(bufferSize) 144 | : ByteBuffer.allocate(bufferSize)); 145 | temp = ThreadLocal.withInitial(this::newMask); 146 | bitmaps[0] = newMask(); 147 | } 148 | 149 | @Override 150 | public RoaringMask newMask() { 151 | return new RoaringMask(storage); 152 | } 153 | 154 | @Override 155 | public int newMaskId() { 156 | ensureCapacity(++maskId); 157 | bitmaps[maskId] = new RoaringMask(storage); 158 | return maskId; 159 | } 160 | 161 | @Override 162 | public int storeMask(RoaringMask mask) { 163 | ensureCapacity(++maskId); 164 | bitmaps[maskId] = mask; 165 | return maskId; 166 | } 167 | 168 | @Override 169 | public RoaringMask getMask(int id) { 170 | return bitmaps[id & (bitmaps.length - 1)]; 171 | } 172 | 173 | @Override 174 | public void add(int id, int bit) { 175 | bitmaps[id & (bitmaps.length - 1)].add(bit); 176 | } 177 | 178 | @Override 179 | public void remove(int id, int bit) { 180 | bitmaps[id & (bitmaps.length - 1)].remove(bit); 181 | } 182 | 183 | @Override 184 | public void or(int from, int into) { 185 | bitmaps[into & (bitmaps.length - 1)].inPlaceOr(bitmaps[from & (bitmaps.length - 1)]); 186 | } 187 | 188 | @Override 189 | public void optimise(int id) { 190 | bitmaps[id & (bitmaps.length - 1)].optimise(); 191 | } 192 | 193 | @Override 194 | public RoaringMask getTemp(int copyAddress) { 195 | return temp.get().resetTo(bitmaps[copyAddress & (bitmaps.length - 1)]); 196 | } 197 | 198 | @Override 199 | public void orInto(RoaringMask mask, int id) { 200 | mask.inPlaceOr(bitmaps[id & (bitmaps.length - 1)]); 201 | } 202 | 203 | @Override 204 | public void andInto(RoaringMask mask, int id) { 205 | mask.inPlaceAnd(bitmaps[id & (bitmaps.length - 1)]); 206 | } 207 | 208 | @Override 209 | public RoaringMask contiguous(int max) { 210 | MutableRoaringBitmap range = new MutableRoaringBitmap(); 211 | range.add(0L, max & 0xFFFFFFFFL); 212 | return new RoaringMask(storage, range); 213 | } 214 | 215 | @Override 216 | public int newContiguousMaskId(int max) { 217 | ensureCapacity(++maskId); 218 | bitmaps[maskId] = contiguous(max); 219 | return maskId; 220 | } 221 | 222 | @Override 223 | public boolean isEmpty(int id) { 224 | return bitmaps[id & (bitmaps.length - 1)].isEmpty(); 225 | } 226 | 227 | @Override 228 | public RoaringMask of(int... values) { 229 | return new RoaringMask(storage, MutableRoaringBitmap.bitmapOf(values)); 230 | } 231 | 232 | @Override 233 | public double averageSelectivity(int[] ids, int min, int max) { 234 | double cardinality = 0; 235 | for (int i = min; i < max; ++i) { 236 | cardinality += this.bitmaps[ids[i]].cardinality(); 237 | } 238 | return cardinality / bitmaps.length; 239 | } 240 | 241 | private void ensureCapacity(int maskId) { 242 | if (maskId >= bitmaps.length) { 243 | bitmaps = Arrays.copyOf(bitmaps, bitmaps.length * 2); 244 | } 245 | } 246 | } 247 | 248 | private static class OptimisedStorage { 249 | private final ByteBuffer allocatedSpace; 250 | 251 | private OptimisedStorage(ByteBuffer allocatedSpace) { 252 | this.allocatedSpace = allocatedSpace; 253 | } 254 | 255 | ImmutableRoaringBitmap consolidate(MutableRoaringBitmap bitmap) { 256 | int requiredSize = bitmap.serializedSizeInBytes(); 257 | if (allocatedSpace.remaining() < requiredSize) { 258 | // can't consolidate 259 | return bitmap; 260 | } else { 261 | int pos = allocatedSpace.position(); 262 | bitmap.serialize(allocatedSpace); 263 | allocatedSpace.position(pos); 264 | var consolidated = new ImmutableRoaringBitmap(allocatedSpace); 265 | allocatedSpace.position(pos + requiredSize); 266 | return consolidated; 267 | } 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/ClassificationNode.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | 5 | public interface ClassificationNode> { 6 | 7 | int match(Input input); 8 | 9 | default double averageSelectivity() { 10 | return 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/ComparableMatcher.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.*; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import io.github.richardstartin.multimatcher.core.matchers.nodes.ComparableNode; 6 | 7 | import java.util.Arrays; 8 | import java.util.Comparator; 9 | import java.util.function.Function; 10 | 11 | import static io.github.richardstartin.multimatcher.core.Utils.newArray; 12 | import static io.github.richardstartin.multimatcher.core.Utils.nullCount; 13 | 14 | public class ComparableMatcher> implements ConstraintAccumulator, 15 | Matcher { 16 | 17 | private final Function accessor; 18 | private final int wildcards; 19 | private final Comparator comparator; 20 | private final MaskStore store; 21 | private ComparableNode[] children; 22 | 23 | @SuppressWarnings("unchecked") 24 | public ComparableMatcher(Function accessor, 25 | Comparator comparator, MaskStore maskStore, 26 | int max) { 27 | this.accessor = accessor; 28 | this.comparator = comparator; 29 | this.store = maskStore; 30 | this.wildcards = maskStore.newContiguousMaskId(max); 31 | this.children = (ComparableNode[]) newArray(ComparableNode.class, Operation.SIZE); 32 | } 33 | 34 | @Override 35 | public void match(T value, MaskType context) { 36 | MaskType temp = store.getTemp(wildcards); 37 | U comparable = accessor.apply(value); 38 | for (var component : children) { 39 | store.orInto(temp, component.match(comparable)); 40 | } 41 | context.inPlaceAnd(temp); 42 | temp.clear(); 43 | } 44 | 45 | @Override 46 | public boolean addConstraint(Constraint constraint, int priority) { 47 | add(constraint.getOperation(), constraint.getValue(), priority); 48 | store.remove(wildcards, priority); 49 | return true; 50 | } 51 | 52 | @Override 53 | public Matcher toMatcher() { 54 | optimise(); 55 | store.optimise(wildcards); 56 | return this; 57 | } 58 | 59 | @Override 60 | public float averageSelectivity() { 61 | return SelectivityHeuristics.avgCardinality(children, ComparableNode::averageSelectivity); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return Arrays.toString(children) + ", *: " + wildcards; 67 | } 68 | 69 | private void add(Operation relation, U threshold, int priority) { 70 | var existing = children[relation.ordinal()]; 71 | if (null == existing) { 72 | existing = children[relation.ordinal()] 73 | = new ComparableNode<>(store, comparator, relation); 74 | } 75 | existing.add(threshold, priority); 76 | } 77 | 78 | @SuppressWarnings("unchecked") 79 | public void optimise() { 80 | int nullCount = nullCount(children); 81 | if (nullCount > 0) { 82 | var newChildren = (ComparableNode[]) newArray(ComparableNode.class, children.length - nullCount); 83 | int i = 0; 84 | for (var child : children) { 85 | if (null != child) { 86 | newChildren[i++] = child.freeze(); 87 | } 88 | } 89 | children = newChildren; 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/DoubleMatcher.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.*; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import io.github.richardstartin.multimatcher.core.matchers.nodes.DoubleNode; 6 | 7 | import java.util.function.ToDoubleFunction; 8 | 9 | import static io.github.richardstartin.multimatcher.core.Utils.newArray; 10 | import static io.github.richardstartin.multimatcher.core.Utils.nullCount; 11 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 12 | 13 | public class DoubleMatcher> implements ConstraintAccumulator, 14 | Matcher { 15 | 16 | private final ToDoubleFunction accessor; 17 | private final MaskStore store; 18 | private final int wildcards; 19 | private DoubleNode[] children; 20 | 21 | @SuppressWarnings("unchecked") 22 | public DoubleMatcher(ToDoubleFunction accessor, MaskStore maskStore, int max) { 23 | this.accessor = accessor; 24 | this.wildcards = maskStore.newContiguousMaskId(max); 25 | this.store = maskStore; 26 | this.children = (DoubleNode[]) newArray(DoubleNode.class, Operation.SIZE); 27 | } 28 | 29 | @Override 30 | public void match(T value, MaskType context) { 31 | MaskType temp = store.getTemp(wildcards); 32 | double attributeValue = accessor.applyAsDouble(value); 33 | for (var component : children) { 34 | store.orInto(temp, component.match(attributeValue, 0)); 35 | } 36 | context.inPlaceAnd(temp); 37 | } 38 | 39 | @Override 40 | public boolean addConstraint(Constraint constraint, int priority) { 41 | Number number = constraint.getValue(); 42 | double value = number.doubleValue(); 43 | add(constraint.getOperation(), value, priority); 44 | store.remove(wildcards, priority); 45 | return true; 46 | } 47 | 48 | @Override 49 | public Matcher toMatcher() { 50 | optimise(); 51 | store.optimise(wildcards); 52 | return this; 53 | } 54 | 55 | private void add(Operation relation, double threshold, int priority) { 56 | var existing = children[relation.ordinal()]; 57 | if (null == existing) { 58 | existing = children[relation.ordinal()] 59 | = new DoubleNode<>(store, relation); 60 | } 61 | existing.add(threshold, priority); 62 | } 63 | 64 | @SuppressWarnings("unchecked") 65 | private void optimise() { 66 | int nullCount = nullCount(children); 67 | if (nullCount > 0) { 68 | var newChildren = (DoubleNode[]) newArray(DoubleNode.class, children.length - nullCount); 69 | int i = 0; 70 | for (var child : children) { 71 | if (null != child) { 72 | newChildren[i++] = child.optimise(); 73 | } 74 | } 75 | children = newChildren; 76 | } 77 | } 78 | 79 | @Override 80 | public float averageSelectivity() { 81 | return avgCardinality(children, DoubleNode::averageSelectivity); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/GenericConstraintAccumulator.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.*; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 6 | 7 | import java.util.Map; 8 | import java.util.function.Function; 9 | import java.util.function.Supplier; 10 | 11 | import static io.github.richardstartin.multimatcher.core.Operation.EQ; 12 | import static io.github.richardstartin.multimatcher.core.Operation.NE; 13 | 14 | public class GenericConstraintAccumulator> 15 | implements ConstraintAccumulator { 16 | 17 | protected final Function accessor; 18 | protected final Supplier> mapSupplier; 19 | protected final Map equality; 20 | protected final Map inequality; 21 | protected final int max; 22 | protected final MaskStore store; 23 | protected final MaskType wildcard; 24 | 25 | public GenericConstraintAccumulator(Supplier> primitiveMapSupplier, 26 | Supplier> mapSupplier, 27 | Function accessor, 28 | MaskStore store, 29 | int max) { 30 | this.accessor = accessor; 31 | this.mapSupplier = primitiveMapSupplier; 32 | this.store = store; 33 | this.max = max; 34 | this.wildcard = store.contiguous(max); 35 | this.equality = mapSupplier.get(); 36 | this.inequality = mapSupplier.get(); 37 | } 38 | 39 | @Override 40 | public boolean addConstraint(Constraint constraint, int priority) { 41 | U key = constraint.getValue(); 42 | if (constraint.getOperation() == EQ) { 43 | update(equality, key, priority); 44 | wildcard.remove(priority); 45 | } else if (constraint.getOperation() == NE) { 46 | update(inequality, key, priority); 47 | } else { 48 | return false; 49 | } 50 | return true; 51 | } 52 | 53 | private void update(Map map, U key, int priority) { 54 | var mask = map.get(key); 55 | if (null == mask) { 56 | mask = store.newMask(); 57 | map.put(key, mask); 58 | } 59 | mask.add(priority); 60 | } 61 | 62 | @Override 63 | public Matcher toMatcher() { 64 | var masks = computeLiteralMasks(); 65 | return new GenericMatcher<>(store, accessor, masks, store.storeMask(wildcard)); 66 | } 67 | 68 | protected Object2IntMap computeLiteralMasks() { 69 | // For each inequality mask, need to add the bits to each equality mask for mismatching values. 70 | // Then when lookups are done by equality, we will automatically get bits for NOT 71 | // constraints whenever the input matches any indexed value. 72 | // 73 | // Values with inequality constraints must not hit the wildcard, so 74 | // the complement of the inequality mask must be added to the equality masks, 75 | // but the wildcard does not require modification for inequality masks. 76 | // 77 | // This all means only one lookup needs to be done. 78 | // now process the relationships between equality and inequality masks 79 | if (!wildcard.isEmpty()) { 80 | for (var eq : equality.entrySet()) { 81 | eq.getValue() 82 | .inPlaceOr(wildcard); 83 | } 84 | } 85 | var it = inequality.entrySet().iterator(); 86 | while (it.hasNext()) { 87 | var ineq = it.next(); 88 | boolean hasCounterpart = false; 89 | for (var eq : equality.entrySet()) { 90 | if (!eq.getKey().equals(ineq.getKey())) { 91 | eq.getValue().inPlaceOr(ineq.getValue()); 92 | } else { 93 | hasCounterpart = true; 94 | eq.getValue().inPlaceAndNot(ineq.getValue()); 95 | } 96 | } 97 | if (hasCounterpart) { 98 | // use the equality mask instead 99 | it.remove(); 100 | } else { 101 | // complement and optimise the mask, will use it as an equality mask 102 | ineq.getValue() 103 | .inPlaceNot(max) 104 | .inPlaceAnd(wildcard) 105 | .optimise(); 106 | } 107 | } 108 | // put the processed masks in the store 109 | Object2IntMap masks = mapSupplier.get(); 110 | for (var eq : equality.entrySet()) { 111 | var mask = eq.getValue(); 112 | mask.optimise(); 113 | masks.put(eq.getKey(), store.storeMask(mask)); 114 | } 115 | wildcard.optimise(); 116 | for (var ineq : inequality.entrySet()) { 117 | masks.put(ineq.getKey(), store.storeMask(ineq.getValue())); 118 | } 119 | return masks; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/GenericMatcher.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Matcher; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 7 | 8 | import java.util.function.Function; 9 | 10 | class GenericMatcher> implements Matcher { 11 | 12 | private final Function accessor; 13 | private final Object2IntMap masks; 14 | private final int wildcard; 15 | private final MaskStore store; 16 | 17 | GenericMatcher(MaskStore store, 18 | Function accessor, 19 | Object2IntMap masks, 20 | int wildcard) { 21 | this.accessor = accessor; 22 | this.masks = masks; 23 | this.wildcard = wildcard; 24 | this.store = store; 25 | } 26 | 27 | @Override 28 | public void match(T input, MaskType context) { 29 | U value = accessor.apply(input); 30 | int mask = masks.getOrDefault(value, wildcard); 31 | store.andInto(context, mask); 32 | } 33 | 34 | @Override 35 | public float averageSelectivity() { 36 | return (float)store.averageSelectivity(masks.values().toIntArray()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/IntMatcher.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.*; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import io.github.richardstartin.multimatcher.core.matchers.nodes.IntNode; 6 | 7 | import java.util.function.ToIntFunction; 8 | 9 | import static io.github.richardstartin.multimatcher.core.Utils.newArray; 10 | import static io.github.richardstartin.multimatcher.core.Utils.nullCount; 11 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 12 | 13 | public class IntMatcher> implements ConstraintAccumulator, 14 | Matcher { 15 | 16 | private final ToIntFunction accessor; 17 | private final int wildcards; 18 | private final MaskStore store; 19 | private IntNode[] children; 20 | 21 | @SuppressWarnings("unchecked") 22 | public IntMatcher(ToIntFunction accessor, MaskStore maskStore, int max) { 23 | this.accessor = accessor; 24 | this.store = maskStore; 25 | this.wildcards = maskStore.newContiguousMaskId(max); 26 | this.children = (IntNode[]) newArray(IntNode.class, Operation.SIZE); 27 | } 28 | 29 | @Override 30 | public void match(T value, MaskType context) { 31 | MaskType temp = store.getTemp(wildcards); 32 | int i = accessor.applyAsInt(value); 33 | for (var component : children) { 34 | store.orInto(temp, component.match(i, 0)); 35 | } 36 | context.inPlaceAnd(temp); 37 | } 38 | 39 | @Override 40 | public boolean addConstraint(Constraint constraint, int priority) { 41 | Number number = constraint.getValue(); 42 | int value = number.intValue(); 43 | add(constraint.getOperation(), value, priority); 44 | store.remove(wildcards, priority); 45 | return true; 46 | } 47 | 48 | @Override 49 | public Matcher toMatcher() { 50 | optimise(); 51 | store.optimise(wildcards); 52 | return this; 53 | } 54 | 55 | public float averageSelectivity() { 56 | return avgCardinality(children, IntNode::averageSelectivity); 57 | } 58 | 59 | private void add(Operation relation, int threshold, int priority) { 60 | var existing = children[relation.ordinal()]; 61 | if (null == existing) { 62 | existing = children[relation.ordinal()] 63 | = new IntNode<>(store, relation); 64 | } 65 | existing.add(threshold, priority); 66 | } 67 | 68 | @SuppressWarnings("unchecked") 69 | private void optimise() { 70 | int nullCount = nullCount(children); 71 | if (nullCount > 0) { 72 | var newChildren = (IntNode[]) newArray(IntNode.class, children.length - nullCount); 73 | int i = 0; 74 | for (var child : children) { 75 | if (null != child) { 76 | newChildren[i++] = child.optimise(); 77 | } 78 | } 79 | children = newChildren; 80 | } 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/LongMatcher.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.*; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import io.github.richardstartin.multimatcher.core.matchers.nodes.LongNode; 6 | 7 | import java.util.function.ToLongFunction; 8 | 9 | import static io.github.richardstartin.multimatcher.core.Utils.newArray; 10 | import static io.github.richardstartin.multimatcher.core.Utils.nullCount; 11 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 12 | 13 | public class LongMatcher> implements ConstraintAccumulator, 14 | Matcher { 15 | 16 | private final ToLongFunction accessor; 17 | private final int wildcards; 18 | private final MaskStore store; 19 | private LongNode[] children; 20 | 21 | @SuppressWarnings("unchecked") 22 | public LongMatcher(ToLongFunction accessor, MaskStore maskStore, int max) { 23 | this.accessor = accessor; 24 | this.store = maskStore; 25 | this.wildcards = maskStore.newContiguousMaskId(max); 26 | this.children = (LongNode[]) newArray(LongNode.class, Operation.SIZE); 27 | } 28 | 29 | @Override 30 | public void match(T value, MaskType context) { 31 | MaskType temp = store.getTemp(wildcards); 32 | long attributeValue = accessor.applyAsLong(value); 33 | for (var component : children) { 34 | store.orInto(temp, component.match(attributeValue, 0)); 35 | } 36 | context.inPlaceAnd(temp); 37 | } 38 | 39 | @Override 40 | public boolean addConstraint(Constraint constraint, int priority) { 41 | Number number = constraint.getValue(); 42 | int value = number.intValue(); 43 | add(constraint.getOperation(), value, priority); 44 | store.remove(wildcards, priority); 45 | return true; 46 | } 47 | 48 | @Override 49 | public Matcher toMatcher() { 50 | optimise(); 51 | store.optimise(wildcards); 52 | return this; 53 | } 54 | 55 | public float averageSelectivity() { 56 | return avgCardinality(children, LongNode::averageSelectivity); 57 | } 58 | 59 | private void add(Operation relation, int threshold, int priority) { 60 | var existing = children[relation.ordinal()]; 61 | if (null == existing) { 62 | existing = children[relation.ordinal()] = new LongNode<>(store, relation); 63 | } 64 | existing.add(threshold, priority); 65 | } 66 | 67 | @SuppressWarnings("unchecked") 68 | private void optimise() { 69 | int nullCount = nullCount(children); 70 | if (nullCount > 0) { 71 | var newChildren = (LongNode[]) newArray(LongNode.class, children.length - nullCount); 72 | int i = 0; 73 | for (var child : children) { 74 | if (null != child) { 75 | newChildren[i++] = child.optimise(); 76 | } 77 | } 78 | children = newChildren; 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/MutableNode.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | 6 | import java.util.Map; 7 | 8 | public interface MutableNode> { 9 | ClassificationNode freeze(); 10 | 11 | // TODO - better handled by vistor 12 | default void link(Map> nodes) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/SelectivityHeuristics.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | 5 | import java.util.Collection; 6 | import java.util.function.ToDoubleFunction; 7 | 8 | public class SelectivityHeuristics { 9 | 10 | public static > float avgCardinality(Collection masks) { 11 | int total = 0; 12 | for (MaskType mask : masks) { 13 | total += mask.cardinality(); 14 | } 15 | return ((float) masks.size()) / total; 16 | } 17 | 18 | public static > float avgCardinality(MaskType[] masks) { 19 | int total = 0; 20 | for (MaskType mask : masks) { 21 | total += mask.cardinality(); 22 | } 23 | return ((float) masks.length) / total; 24 | } 25 | 26 | public static float avgCardinality(Collection nodes, ToDoubleFunction selectivity) { 27 | double avg = 0; 28 | int count = 0; 29 | for (Node node : nodes) { 30 | avg += selectivity.applyAsDouble(node); 31 | ++count; 32 | } 33 | return (float) (avg / count); 34 | } 35 | 36 | public static float avgCardinality(Node[] nodes, ToDoubleFunction selectivity) { 37 | double avg = 0; 38 | int count = 0; 39 | for (Node node : nodes) { 40 | avg += selectivity.applyAsDouble(node); 41 | ++count; 42 | } 43 | return (float) (avg / count); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/StringConstraintAccumulator.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 6 | import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; 7 | 8 | import java.util.HashMap; 9 | import java.util.function.Function; 10 | import java.util.function.Supplier; 11 | 12 | public class StringConstraintAccumulator> 13 | extends GenericConstraintAccumulator { 14 | 15 | public StringConstraintAccumulator(Function accessor, 16 | MaskStore maskStore, 17 | int max) { 18 | this(Object2IntOpenHashMap::new, accessor, maskStore, max); 19 | } 20 | 21 | private StringConstraintAccumulator(Supplier> mapSupplier, 22 | Function accessor, 23 | MaskStore maskStore, 24 | int max) { 25 | super(mapSupplier, HashMap::new, accessor, maskStore, max); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/nodes/ComparableNode.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers.nodes; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.ClassificationNode; 7 | import io.github.richardstartin.multimatcher.core.matchers.MutableNode; 8 | 9 | import java.util.Comparator; 10 | import java.util.NavigableMap; 11 | import java.util.TreeMap; 12 | 13 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 14 | 15 | public class ComparableNode> 16 | implements MutableNode, ClassificationNode { 17 | 18 | private final MaskStore store; 19 | private final NavigableMap sets; 20 | private final Operation operation; 21 | 22 | public ComparableNode(MaskStore store, 23 | Comparator comparator, 24 | Operation operation) { 25 | this.sets = new TreeMap<>(comparator); 26 | this.operation = operation; 27 | this.store = store; 28 | } 29 | 30 | public void add(T value, int priority) { 31 | int set = sets.getOrDefault(value, 0); 32 | if (0 == set) { 33 | set = store.newMaskId(); 34 | sets.put(value, set); 35 | } 36 | store.add(set, priority); 37 | } 38 | 39 | @Override 40 | public int match(T value) { 41 | switch (operation) { 42 | case GE: 43 | case EQ: 44 | case LE: 45 | return sets.getOrDefault(value, 0); 46 | case LT: 47 | var higher = sets.higherEntry(value); 48 | return null == higher ? 0 : higher.getValue(); 49 | case GT: 50 | var lower = sets.lowerEntry(value); 51 | return null == lower ? 0 : lower.getValue(); 52 | default: 53 | return 0; 54 | } 55 | } 56 | 57 | public ComparableNode freeze() { 58 | switch (operation) { 59 | case GE: 60 | case GT: 61 | rangeEncode(); 62 | return this; 63 | case LE: 64 | case LT: 65 | reverseRangeEncode(); 66 | return this; 67 | default: 68 | return this; 69 | } 70 | } 71 | 72 | public double averageSelectivity() { 73 | return store.averageSelectivity(sets.values().stream().mapToInt(Integer::intValue).toArray()); 74 | } 75 | 76 | private void rangeEncode() { 77 | int prev = 0; 78 | for (var set : sets.entrySet()) { 79 | int next = set.getValue(); 80 | store.or(prev, next); 81 | store.optimise(next); 82 | prev = set.getValue(); 83 | } 84 | } 85 | 86 | private void reverseRangeEncode() { 87 | int prev = 0; 88 | for (var set : sets.descendingMap().entrySet()) { 89 | int next = set.getValue(); 90 | store.or(prev, next); 91 | store.optimise(next); 92 | prev = set.getValue(); 93 | } 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return Nodes.toString(sets.size(), operation, sets); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/nodes/DoubleNode.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers.nodes; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | 7 | import java.util.Arrays; 8 | 9 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 10 | 11 | public class DoubleNode> { 12 | 13 | private final Operation relation; 14 | private final MaskStore store; 15 | 16 | private double[] thresholds = new double[4]; 17 | private int[] sets; 18 | private int count = 0; 19 | 20 | public DoubleNode(MaskStore store, Operation relation) { 21 | this.relation = relation; 22 | this.store = store; 23 | this.sets = new int[4]; 24 | } 25 | 26 | public void add(double value, int priority) { 27 | if (count > 0 && value > thresholds[count - 1]) { 28 | ensureCapacity(); 29 | int position = count; 30 | int maskId = sets[position]; 31 | if (0 == maskId) { 32 | maskId = store.newMaskId(); 33 | } 34 | store.add(maskId, priority); 35 | thresholds[position] = value; 36 | sets[position] = maskId; 37 | ++count; 38 | } else { 39 | int position = Arrays.binarySearch(thresholds, 0, count, value); 40 | int insertionPoint = -(position + 1); 41 | if (position < 0 && insertionPoint < count) { 42 | ensureCapacity(); 43 | for (int i = count; i > insertionPoint; --i) { 44 | sets[i] = sets[i - 1]; 45 | thresholds[i] = thresholds[i - 1]; 46 | } 47 | int maskId = store.newMaskId(); 48 | store.add(maskId, priority); 49 | sets[insertionPoint] = maskId; 50 | thresholds[insertionPoint] = value; 51 | ++count; 52 | } else if (position < 0) { 53 | ensureCapacity(); 54 | int maskId = store.newMaskId(); 55 | store.add(maskId, priority); 56 | sets[count] = maskId; 57 | thresholds[count] = value; 58 | ++count; 59 | } else { 60 | store.add(sets[position], priority); 61 | } 62 | } 63 | } 64 | 65 | public int match(double value, int defaultValue) { 66 | switch (relation) { 67 | case GT: 68 | return findRangeEncoded(value); 69 | case GE: 70 | return findRangeEncodedInclusive(value); 71 | case LT: 72 | return findReverseRangeEncoded(value); 73 | case LE: 74 | return findReverseRangeEncodedInclusive(value); 75 | case EQ: 76 | return findEqualityEncoded(value); 77 | default: 78 | return defaultValue; 79 | } 80 | } 81 | 82 | 83 | public double averageSelectivity() { 84 | return store.averageSelectivity(sets); 85 | } 86 | 87 | public DoubleNode optimise() { 88 | switch (relation) { 89 | case GE: 90 | case GT: 91 | rangeEncode(); 92 | break; 93 | case LE: 94 | case LT: 95 | reverseRangeEncode(); 96 | break; 97 | default: 98 | } 99 | trim(); 100 | return this; 101 | } 102 | 103 | private int findEqualityEncoded(double value) { 104 | int index = Arrays.binarySearch(thresholds, 0, count, value); 105 | return index >= 0 ? sets[index] : 0; 106 | } 107 | 108 | private int findRangeEncoded(double value) { 109 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 110 | int index = (pos >= 0 ? pos : -(pos + 1)) - 1; 111 | return index >= 0 && index < count ? sets[index] : 0; 112 | } 113 | 114 | private int findRangeEncodedInclusive(double value) { 115 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 116 | int index = (pos >= 0 ? pos : -(pos + 1) - 1); 117 | return index >= 0 && index < count ? sets[index] : 0; 118 | } 119 | 120 | private int findReverseRangeEncoded(double value) { 121 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 122 | int index = (pos >= 0 ? pos + 1 : -(pos + 1)); 123 | return index >= 0 && index < count ? sets[index] : 0; 124 | } 125 | 126 | private int findReverseRangeEncodedInclusive(double value) { 127 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 128 | int index = (pos >= 0 ? pos : -(pos + 1)); 129 | return index < count ? sets[index] : 0; 130 | } 131 | 132 | private void reverseRangeEncode() { 133 | for (int i = count - 2; i >= 0; --i) { 134 | store.or(sets[i + 1], sets[i]); 135 | store.optimise(sets[i]); 136 | } 137 | } 138 | 139 | private void rangeEncode() { 140 | for (int i = 1; i < count; ++i) { 141 | store.or(sets[i - 1], sets[i]); 142 | store.optimise(sets[i]); 143 | } 144 | } 145 | 146 | private void trim() { 147 | sets = Arrays.copyOf(sets, count); 148 | thresholds = Arrays.copyOf(thresholds, count); 149 | } 150 | 151 | private void ensureCapacity() { 152 | int newCount = count + 1; 153 | if (newCount == thresholds.length) { 154 | sets = Arrays.copyOf(sets, newCount * 2); 155 | thresholds = Arrays.copyOf(thresholds, newCount * 2); 156 | } 157 | } 158 | 159 | @Override 160 | public String toString() { 161 | return Nodes.toString(count, relation, 162 | Arrays.stream(thresholds).boxed().iterator(), 163 | Arrays.stream(sets).iterator()); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/nodes/IntNode.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers.nodes; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | 7 | import java.util.Arrays; 8 | 9 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 10 | 11 | public class IntNode> { 12 | 13 | private final Operation relation; 14 | private final MaskStore store; 15 | 16 | private int[] thresholds = new int[4]; 17 | private int[] sets; 18 | private int count = 0; 19 | 20 | public IntNode(MaskStore store, Operation relation) { 21 | this.relation = relation; 22 | this.store = store; 23 | this.sets = new int[4]; 24 | } 25 | 26 | public void add(int value, int priority) { 27 | if (count > 0 && value > thresholds[count - 1]) { 28 | ensureCapacity(); 29 | int position = count; 30 | int maskId = sets[position]; 31 | if (0 == maskId) { 32 | maskId = store.newMaskId(); 33 | } 34 | store.add(maskId, priority); 35 | thresholds[position] = value; 36 | sets[position] = maskId; 37 | ++count; 38 | } else { 39 | int position = Arrays.binarySearch(thresholds, 0, count, value); 40 | int insertionPoint = -(position + 1); 41 | if (position < 0 && insertionPoint < count) { 42 | ensureCapacity(); 43 | for (int i = count; i > insertionPoint; --i) { 44 | sets[i] = sets[i - 1]; 45 | thresholds[i] = thresholds[i - 1]; 46 | } 47 | int maskId = store.newMaskId(); 48 | store.add(maskId, priority); 49 | sets[insertionPoint] = maskId; 50 | thresholds[insertionPoint] = value; 51 | ++count; 52 | } else if (position < 0) { 53 | ensureCapacity(); 54 | int maskId = store.newMaskId(); 55 | store.add(maskId, priority); 56 | sets[count] = maskId; 57 | thresholds[count] = value; 58 | ++count; 59 | } else { 60 | store.add(sets[position], priority); 61 | } 62 | } 63 | } 64 | 65 | public int match(int value, int defaultValue) { 66 | switch (relation) { 67 | case GT: 68 | return findRangeEncoded(value); 69 | case GE: 70 | return findRangeEncodedInclusive(value); 71 | case LT: 72 | return findReverseRangeEncoded(value); 73 | case LE: 74 | return findReverseRangeEncodedInclusive(value); 75 | case EQ: 76 | return findEqualityEncoded(value); 77 | default: 78 | return defaultValue; 79 | } 80 | } 81 | 82 | 83 | public double averageSelectivity() { 84 | return store.averageSelectivity(sets); 85 | } 86 | 87 | public IntNode optimise() { 88 | switch (relation) { 89 | case GE: 90 | case GT: 91 | rangeEncode(); 92 | break; 93 | case LE: 94 | case LT: 95 | reverseRangeEncode(); 96 | break; 97 | default: 98 | } 99 | trim(); 100 | return this; 101 | } 102 | 103 | private int findEqualityEncoded(int value) { 104 | int index = Arrays.binarySearch(thresholds, 0, count, value); 105 | return index >= 0 ? sets[index] : 0; 106 | } 107 | 108 | private int findRangeEncoded(int value) { 109 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 110 | int index = (pos >= 0 ? pos : -(pos + 1)) - 1; 111 | return index >= 0 && index < count ? sets[index] : 0; 112 | } 113 | 114 | private int findRangeEncodedInclusive(int value) { 115 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 116 | int index = (pos >= 0 ? pos : -(pos + 1) - 1); 117 | return index >= 0 && index < count ? sets[index] : 0; 118 | } 119 | 120 | private int findReverseRangeEncoded(int value) { 121 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 122 | int index = (pos >= 0 ? pos + 1 : -(pos + 1)); 123 | return index >= 0 && index < count ? sets[index] : 0; 124 | } 125 | 126 | private int findReverseRangeEncodedInclusive(int value) { 127 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 128 | int index = (pos >= 0 ? pos : -(pos + 1)); 129 | return index < count ? sets[index] : 0; 130 | } 131 | 132 | private void reverseRangeEncode() { 133 | for (int i = count - 2; i >= 0; --i) { 134 | store.or(sets[i + 1], sets[i]); 135 | store.optimise(sets[i]); 136 | } 137 | } 138 | 139 | private void rangeEncode() { 140 | for (int i = 1; i < count; ++i) { 141 | store.or(sets[i - 1], sets[i]); 142 | store.optimise(sets[i]); 143 | } 144 | } 145 | 146 | private void trim() { 147 | sets = Arrays.copyOf(sets, count); 148 | thresholds = Arrays.copyOf(thresholds, count); 149 | } 150 | 151 | private void ensureCapacity() { 152 | int newCount = count + 1; 153 | if (newCount == thresholds.length) { 154 | sets = Arrays.copyOf(sets, newCount * 2); 155 | thresholds = Arrays.copyOf(thresholds, newCount * 2); 156 | } 157 | } 158 | 159 | @Override 160 | public String toString() { 161 | return Nodes.toString(count, relation, 162 | Arrays.stream(thresholds).boxed().iterator(), 163 | Arrays.stream(sets).iterator()); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/nodes/LongNode.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers.nodes; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | 7 | import java.util.Arrays; 8 | 9 | import static io.github.richardstartin.multimatcher.core.matchers.SelectivityHeuristics.avgCardinality; 10 | 11 | public class LongNode> { 12 | 13 | private final Operation relation; 14 | private final MaskStore factory; 15 | 16 | private long[] thresholds = new long[4]; 17 | private int[] sets; 18 | private int count = 0; 19 | 20 | public LongNode(MaskStore factory, Operation relation) { 21 | this.relation = relation; 22 | this.factory = factory; 23 | this.sets = new int[4]; 24 | } 25 | 26 | public void add(long value, int priority) { 27 | if (count > 0 && value > thresholds[count - 1]) { 28 | ensureCapacity(); 29 | int position = count; 30 | int maskId = sets[position]; 31 | if (0 == maskId) { 32 | maskId = factory.newMaskId(); 33 | } 34 | factory.add(maskId, priority); 35 | thresholds[position] = value; 36 | sets[position] = maskId; 37 | ++count; 38 | } else { 39 | int position = Arrays.binarySearch(thresholds, 0, count, value); 40 | int insertionPoint = -(position + 1); 41 | if (position < 0 && insertionPoint < count) { 42 | ensureCapacity(); 43 | for (int i = count; i > insertionPoint; --i) { 44 | sets[i] = sets[i - 1]; 45 | thresholds[i] = thresholds[i - 1]; 46 | } 47 | int maskId = factory.newMaskId(); 48 | factory.add(maskId, priority); 49 | sets[insertionPoint] = maskId; 50 | thresholds[insertionPoint] = value; 51 | ++count; 52 | } else if (position < 0) { 53 | ensureCapacity(); 54 | int maskId = factory.newMaskId(); 55 | factory.add(maskId, priority); 56 | sets[count] = maskId; 57 | thresholds[count] = value; 58 | ++count; 59 | } else { 60 | factory.add(sets[position], priority); 61 | } 62 | } 63 | } 64 | 65 | public int match(long value, int defaultValue) { 66 | switch (relation) { 67 | case GT: 68 | return findRangeEncoded(value); 69 | case GE: 70 | return findRangeEncodedInclusive(value); 71 | case LT: 72 | return findReverseRangeEncoded(value); 73 | case LE: 74 | return findReverseRangeEncodedInclusive(value); 75 | case EQ: 76 | return findEqualityEncoded(value); 77 | default: 78 | return defaultValue; 79 | } 80 | } 81 | 82 | 83 | public double averageSelectivity() { 84 | return factory.averageSelectivity(sets); 85 | } 86 | 87 | public LongNode optimise() { 88 | switch (relation) { 89 | case GE: 90 | case GT: 91 | rangeEncode(); 92 | break; 93 | case LE: 94 | case LT: 95 | reverseRangeEncode(); 96 | break; 97 | default: 98 | } 99 | trim(); 100 | return this; 101 | } 102 | 103 | private int findEqualityEncoded(long value) { 104 | int index = Arrays.binarySearch(thresholds, 0, count, value); 105 | return index >= 0 ? sets[index] : 0; 106 | } 107 | 108 | private int findRangeEncoded(long value) { 109 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 110 | int index = (pos >= 0 ? pos : -(pos + 1)) - 1; 111 | return index >= 0 && index < count ? sets[index] : 0; 112 | } 113 | 114 | private int findRangeEncodedInclusive(long value) { 115 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 116 | int index = (pos >= 0 ? pos : -(pos + 1) - 1); 117 | return index >= 0 && index < count ? sets[index] : 0; 118 | } 119 | 120 | private int findReverseRangeEncoded(long value) { 121 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 122 | int index = (pos >= 0 ? pos + 1 : -(pos + 1)); 123 | return index >= 0 && index < count ? sets[index] : 0; 124 | } 125 | 126 | private int findReverseRangeEncodedInclusive(long value) { 127 | int pos = Arrays.binarySearch(thresholds, 0, count, value); 128 | int index = (pos >= 0 ? pos : -(pos + 1)); 129 | return index < count ? sets[index] : 0; 130 | } 131 | 132 | private void reverseRangeEncode() { 133 | for (int i = count - 2; i >= 0; --i) { 134 | factory.or(sets[i + 1], sets[i]); 135 | factory.optimise(sets[i]); 136 | } 137 | } 138 | 139 | private void rangeEncode() { 140 | for (int i = 1; i < count; ++i) { 141 | factory.or(sets[i - 1], sets[i]); 142 | factory.optimise(sets[i]); 143 | } 144 | } 145 | 146 | private void trim() { 147 | sets = Arrays.copyOf(sets, count); 148 | thresholds = Arrays.copyOf(thresholds, count); 149 | } 150 | 151 | private void ensureCapacity() { 152 | int newCount = count + 1; 153 | if (newCount == thresholds.length) { 154 | sets = Arrays.copyOf(sets, newCount * 2); 155 | thresholds = Arrays.copyOf(thresholds, newCount * 2); 156 | } 157 | } 158 | 159 | @Override 160 | public String toString() { 161 | return Nodes.toString(count, relation, 162 | Arrays.stream(thresholds).boxed().iterator(), 163 | Arrays.stream(sets).iterator()); 164 | } 165 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/matchers/nodes/Nodes.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers.nodes; 2 | 3 | import io.github.richardstartin.multimatcher.core.Operation; 4 | 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | 8 | class Nodes { 9 | 10 | public static String toString(int count, 11 | Operation op, 12 | Iterator thresholds, 13 | Iterator matches) { 14 | StringBuilder sb = new StringBuilder().append(count).append(" thresholds): ["); 15 | while (thresholds.hasNext() && matches.hasNext()) { 16 | sb.append("(_ ") 17 | .append(op) 18 | .append(" ") 19 | .append(thresholds.next()).append(": ") 20 | .append(matches.next()) 21 | .append("), "); 22 | } 23 | sb.setCharAt(sb.length() - 2, ']'); 24 | return sb.toString(); 25 | } 26 | 27 | public static String toString(int count, 28 | Operation op, 29 | Map conditions) { 30 | StringBuilder sb = new StringBuilder().append(count).append(" thresholds): ["); 31 | for (var condition : conditions.entrySet()) { 32 | sb.append("(_ ") 33 | .append(op) 34 | .append(" ") 35 | .append(condition.getKey()) 36 | .append(": ") 37 | .append(condition.getValue()) 38 | .append("), "); 39 | } 40 | sb.setCharAt(sb.length() - 2, ']'); 41 | return sb.toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/Attribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | 7 | /** 8 | * Effectively a store for a column named constraints 9 | * 10 | * @param the type of the attribute values 11 | */ 12 | public interface Attribute { 13 | /** 14 | * Construct a matcher from the attribute 15 | * 16 | * @param maskStore the type named mask 17 | * @param max the maximum number named constraints supported 18 | * @return a new matcher 19 | */ 20 | > 21 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max); 22 | } 23 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/AttributeNotRegistered.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | /** 4 | * Thrown if a rule is specified on an attribute not found in the attribute registry. 5 | */ 6 | public class AttributeNotRegistered extends RuntimeException { 7 | 8 | public AttributeNotRegistered(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/ComparableAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.ComparableMatcher; 7 | 8 | import java.util.Comparator; 9 | import java.util.function.Function; 10 | 11 | /** 12 | * Creates a column named constraints builder equality and order semantics 13 | * 14 | * @param the type named the classified objects 15 | * @param the type named the attribute 16 | */ 17 | public class ComparableAttribute implements Attribute { 18 | 19 | private final Comparator comparator; 20 | private final Function accessor; 21 | 22 | public ComparableAttribute(Comparator comparator, Function accessor) { 23 | this.comparator = comparator; 24 | this.accessor = accessor; 25 | } 26 | 27 | @Override 28 | public > 29 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 30 | return new ComparableMatcher<>(accessor, comparator, maskStore, max); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/DoubleAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.DoubleMatcher; 7 | 8 | import java.util.function.ToDoubleFunction; 9 | 10 | /** 11 | * Creates a column named constraints builder floating point sematics 12 | * 13 | * @param 14 | */ 15 | public class DoubleAttribute implements Attribute { 16 | 17 | private final ToDoubleFunction accessor; 18 | 19 | public DoubleAttribute(ToDoubleFunction accessor) { 20 | this.accessor = accessor; 21 | } 22 | 23 | @Override 24 | public > 25 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 26 | return new DoubleMatcher<>(accessor, maskStore, max); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/EnumAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.GenericConstraintAccumulator; 7 | import it.unimi.dsi.fastutil.Hash; 8 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 9 | import it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap; 10 | 11 | import java.util.EnumMap; 12 | import java.util.function.Function; 13 | 14 | public class EnumAttribute, Input> implements Attribute { 15 | 16 | private final Function accessor; 17 | private final Class type; 18 | 19 | public EnumAttribute(Class type, Function accessor) { 20 | this.accessor = accessor; 21 | this.type = type; 22 | } 23 | 24 | @Override 25 | public > 26 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 27 | return new GenericConstraintAccumulator<>(this::newMap, () -> new EnumMap<>(type), accessor, maskStore, max); 28 | } 29 | 30 | 31 | @SuppressWarnings("unchecked") 32 | private Object2IntMap newMap() { 33 | return new Object2IntOpenCustomHashMap<>(type.getEnumConstants().length, 34 | 1f, (Hash.Strategy) STRATEGY); 35 | } 36 | 37 | private static final EnumHashStrategy STRATEGY = new EnumHashStrategy<>(); 38 | 39 | private static class EnumHashStrategy> implements Hash.Strategy { 40 | 41 | @Override 42 | public int hashCode(E o) { 43 | return o.ordinal(); 44 | } 45 | 46 | @Override 47 | public boolean equals(E a, E b) { 48 | return a == b; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/GenericAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.GenericConstraintAccumulator; 7 | import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; 8 | 9 | import java.util.HashMap; 10 | import java.util.function.Function; 11 | 12 | /** 13 | * Creates a column named constraints builder equality semantics only 14 | * 15 | * @param the type named the classified objects 16 | * @param the type named the attribute 17 | */ 18 | public class GenericAttribute implements Attribute { 19 | 20 | private final Function accessor; 21 | 22 | public GenericAttribute(Function accessor) { 23 | this.accessor = accessor; 24 | } 25 | 26 | @Override 27 | public > 28 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 29 | return new GenericConstraintAccumulator<>(Object2IntOpenHashMap::new, HashMap::new, accessor, maskStore, max); 30 | } 31 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/IntAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.IntMatcher; 7 | 8 | import java.util.function.ToIntFunction; 9 | 10 | /** 11 | * Creates a column named constraints builder integer semantics 12 | * 13 | * @param the type named the classified objects 14 | */ 15 | public class IntAttribute implements Attribute { 16 | 17 | private final ToIntFunction accessor; 18 | 19 | public IntAttribute(ToIntFunction accessor) { 20 | this.accessor = accessor; 21 | } 22 | 23 | @Override 24 | public > 25 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 26 | return new IntMatcher<>(accessor, maskStore, max); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/LongAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.LongMatcher; 7 | 8 | import java.util.function.ToLongFunction; 9 | 10 | /** 11 | * Creates a column named constraints builder long semantics 12 | * 13 | * @param the type named the classified objects 14 | */ 15 | public class LongAttribute implements Attribute { 16 | 17 | private final ToLongFunction accessor; 18 | 19 | public LongAttribute(ToLongFunction accessor) { 20 | this.accessor = accessor; 21 | } 22 | 23 | @Override 24 | public > 25 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 26 | return new LongMatcher<>(accessor, maskStore, max); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /multi-matcher-core/src/main/java/io/github/richardstartin/multimatcher/core/schema/StringAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.schema; 2 | 3 | import io.github.richardstartin.multimatcher.core.ConstraintAccumulator; 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 6 | import io.github.richardstartin.multimatcher.core.matchers.StringConstraintAccumulator; 7 | 8 | import java.util.function.Function; 9 | 10 | public class StringAttribute implements Attribute { 11 | 12 | private final Function accessor; 13 | 14 | public StringAttribute(Function accessor) { 15 | this.accessor = accessor; 16 | } 17 | 18 | @Override 19 | public > 20 | ConstraintAccumulator newAccumulator(MaskStore maskStore, int max) { 21 | return new StringConstraintAccumulator<>(accessor, maskStore, max); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/FileRules.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import com.fasterxml.jackson.databind.MappingIterator; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.List; 9 | 10 | public class FileRules implements RuleSet { 11 | 12 | private final String filename; 13 | private final ObjectMapper mapper; 14 | 15 | public FileRules(String filename, ObjectMapper mapper) { 16 | this.filename = filename; 17 | this.mapper = mapper; 18 | } 19 | 20 | @Override 21 | public List> constraints() throws IOException { 22 | try (InputStream in = ClassLoader.getSystemResourceAsStream(filename); 23 | MappingIterator> it = mapper.readerFor(MatchingConstraint.class).readValues(in)) { 24 | return it.readAll(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/FooTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Arrays; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class FooTest { 10 | 11 | @Test 12 | public void testSimpleConfig() { 13 | // declare a schema, associating attribute accessors with some kind of key (a string here) 14 | Schema schema = Schema.create() 15 | .withAttribute("productType", Foo::getProductType) 16 | .withAttribute("qty", Foo::getQuantity) 17 | .withAttribute("price", Foo::getPrice); 18 | // build the classifier from the rules and the schema 19 | Classifier classifier = Classifier.builder(schema).build( 20 | Arrays.asList( 21 | MatchingConstraint.anonymous() 22 | .eq("productType", "electronics") 23 | .gt("qty", 10) 24 | .lt("price", 200) 25 | .classification("class1") 26 | .build(), 27 | MatchingConstraint.anonymous() 28 | .eq("productType", "electronics") 29 | .lt("price", 300) 30 | .classification("class2") 31 | .build(), 32 | MatchingConstraint.anonymous() 33 | .eq("productType", "books") 34 | .eq("qty", 1) 35 | .classification("class3") 36 | .build() 37 | ) 38 | ); 39 | 40 | assertEquals("class2", 41 | classifier.classification(new Foo("electronics", 2, 199)).orElse("none")); 42 | } 43 | 44 | static class Foo { 45 | final String productType; 46 | final int quantity; 47 | final int price; 48 | 49 | Foo(String productType, int quantity, int price) { 50 | this.productType = productType; 51 | this.quantity = quantity; 52 | this.price = price; 53 | } 54 | 55 | public String getProductType() { 56 | return productType; 57 | } 58 | 59 | public int getQuantity() { 60 | return quantity; 61 | } 62 | 63 | public int getPrice() { 64 | return price; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/LargeClassifierTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.function.ToIntFunction; 8 | import java.util.stream.IntStream; 9 | 10 | import static java.util.stream.Collectors.toList; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | 14 | public class LargeClassifierTest { 15 | 16 | private static ToIntFunction extract(int feature) { 17 | return features -> features[feature]; 18 | } 19 | 20 | @Test 21 | public void testLargeClassifier() { 22 | Classifier classifier = Classifier. 23 | builder(Schema.create() 24 | .withAttribute(0, extract(0)) 25 | .withAttribute(1, extract(1)) 26 | .withAttribute(2, extract(2)) 27 | .withAttribute(3, extract(3)) 28 | .withAttribute(4, extract(4)) 29 | ).build(IntStream.range(0, 50000) 30 | .mapToObj(i -> 31 | MatchingConstraint.anonymous() 32 | .eq(0, i) 33 | .eq(1, i) 34 | .eq(2, i) 35 | .eq(3, i) 36 | .eq(4, i) 37 | .classification("SEGMENT" + i) 38 | .build()) 39 | 40 | .collect(toList()) 41 | ); 42 | int[] vector = new int[]{5, 5, 5, 5, 5}; 43 | String classification = classifier.classification(vector).orElseThrow(RuntimeException::new); 44 | assertEquals(classification, "SEGMENT5"); 45 | } 46 | 47 | @Test 48 | public void testLargeDiscreteClassifier() { 49 | Classifier, String> classifier = Classifier. 50 | , String>builder(Schema.>create() 51 | .withStringAttribute("attr1", (Map map) -> (String) map.get("attr1")) 52 | .withStringAttribute("attr2", (Map map) -> (String) map.get("attr2")) 53 | .withStringAttribute("attr3", (Map map) -> (String) map.get("attr3")) 54 | .withStringAttribute("attr4", (Map map) -> (String) map.get("attr4")) 55 | .withStringAttribute("attr5", (Map map) -> (String) map.get("attr5")) 56 | .withStringAttribute("attr6", (Map map) -> (String) map.get("attr6")) 57 | ).build(IntStream.range(0, 50000) 58 | .mapToObj(i -> MatchingConstraint.anonymous() 59 | .eq("attr1", "value" + (i / 10000)) 60 | .eq("attr2", "value" + (i / 1000)) 61 | .eq("attr3", "value" + (i / 500)) 62 | .eq("attr4", "value" + (i / 250)) 63 | .eq("attr5", "value" + (i / 100)) 64 | .eq("attr6", "value" + (i / 10)) 65 | .classification("SEGMENT" + i).build() 66 | ).collect(toList())); 67 | 68 | Map msg = new HashMap<>(); 69 | msg.put("attr1", "value0"); 70 | msg.put("attr2", "value0"); 71 | msg.put("attr3", "value0"); 72 | msg.put("attr4", "value0"); 73 | msg.put("attr5", "value0"); 74 | msg.put("attr6", "value9"); 75 | 76 | String classification = classifier.classification(msg).orElseThrow(RuntimeException::new); 77 | assertEquals("SEGMENT90", classification); 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/NegationTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import org.junit.jupiter.api.parallel.Execution; 4 | import org.junit.jupiter.api.parallel.ExecutionMode; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.Arguments; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | 9 | import java.util.Arrays; 10 | import java.util.stream.Stream; 11 | 12 | import static io.github.richardstartin.multimatcher.core.TestDomainObject.Colour.*; 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | 15 | @Execution(ExecutionMode.CONCURRENT) 16 | public class NegationTest { 17 | 18 | 19 | private static final Schema SCHEMA = Schema.create() 20 | .withEnumAttribute("w", TestDomainObject::getColour, TestDomainObject.Colour.class) 21 | .withAttribute("x", TestDomainObject::getField1) 22 | .withAttribute("y", TestDomainObject::getField2) 23 | .withAttribute("z", TestDomainObject::getField3); 24 | 25 | 26 | /* 27 | RED BLUE YELLOW 28 | x1 * x1 * x1 * 29 | 0 1 0 0 0 0 30 | 0 0 0 1 0 0 31 | 0 0 0 0 0 1 32 | 0 0 1 0 1 0 33 | 1 0 0 0 1 0 34 | 1 0 1 0 0 0 35 | 36 | 37 | RED BLUE YELLOW 38 | 1 0 0 39 | 0 1 0 40 | 0 0 1 41 | 0 1 1 42 | 1 0 1 43 | 1 1 0 44 | 45 | 46 | x1 * 47 | 0 1 48 | 0 1 49 | 0 1 50 | 1 0 51 | 1 0 52 | 1 0 53 | 54 | 55 | x1 & RED = 110001 & 111000 = 110000 = class 5 (x=x1 & colour != BLUE) 56 | x1 & BLUE = 101010 & 111000 = 101000 = class 4 (x=x1 & colour != RED) 57 | x1 & YELLOW = 011100 & 111000 = 011000 = class 4 (x=x1 & colour != RED) 58 | x2 & RED = * & RED = 110001 & 100111 = 1 = class 1 (x!=x1 & colour=RED) 59 | x2 & BLUE = * & BLUE = 101010 & 000111 = 10 = class 2 (x!=x1 & colour=BLUE) 60 | x2 & YELLOW = * & YELLOW = 011100 & 000111 = 100 = class 2 (x!=x1 & colour=YELLOW) 61 | */ 62 | 63 | private static final Classifier CLASSIFIER = 64 | Classifier.builder(SCHEMA).build( 65 | Arrays.asList( 66 | MatchingConstraint.anonymous() 67 | .eq("w", RED) 68 | .neq("x", "x1") 69 | .classification("class1") 70 | .build(), 71 | MatchingConstraint.anonymous() 72 | .eq("w", BLUE) 73 | .neq("x", "x1") 74 | .classification("class2") 75 | .build(), 76 | MatchingConstraint.anonymous() 77 | .eq("w", YELLOW) 78 | .neq("x", "x1") 79 | .classification("class3") 80 | .build(), 81 | MatchingConstraint.anonymous() 82 | .neq("w", RED) 83 | .eq("x", "x1") 84 | .classification("class4") 85 | .build(), 86 | MatchingConstraint.anonymous() 87 | .neq("w", BLUE) 88 | .eq("x", "x1") 89 | .classification("class5") 90 | .build(), 91 | MatchingConstraint.anonymous() 92 | .neq("w", YELLOW) 93 | .eq("x", "x1") 94 | .classification("class6") 95 | .build() 96 | ) 97 | ); 98 | 99 | 100 | public static Stream examples() { 101 | return Stream.of( 102 | Arguments.of(TestDomainObject.random().setColour(RED), "class1"), 103 | Arguments.of(TestDomainObject.random().setColour(BLUE), "class2"), 104 | Arguments.of(TestDomainObject.random().setColour(YELLOW), "class3"), 105 | Arguments.of(TestDomainObject.random().setColour(RED).setField1("x1"), "class5"), 106 | Arguments.of(TestDomainObject.random().setColour(BLUE).setField1("x1"), "class4"), 107 | Arguments.of(TestDomainObject.random().setColour(YELLOW).setField1("x1"), "class4") 108 | ); 109 | } 110 | 111 | 112 | 113 | 114 | @ParameterizedTest 115 | @MethodSource("examples") 116 | public void testSimpleConfig(TestDomainObject object, String expected) { 117 | assertEquals(expected, CLASSIFIER.classificationOrNull(object)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/PropertyBasedTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import org.junit.jupiter.api.parallel.Execution; 4 | import org.junit.jupiter.api.parallel.ExecutionMode; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.Arguments; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | import org.junit.jupiter.params.provider.ValueSource; 9 | 10 | import java.util.*; 11 | import java.util.function.Function; 12 | import java.util.stream.Stream; 13 | 14 | import static io.github.richardstartin.multimatcher.core.TestDomainObject.Colour.RED; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | @Execution(ExecutionMode.CONCURRENT) 18 | public class PropertyBasedTest { 19 | 20 | private static final Schema SCHEMA = Schema.create() 21 | .withAttribute(0, TestDomainObject::getField1) 22 | .withAttribute(1, TestDomainObject::getField2) 23 | .withAttribute(2, TestDomainObject::getField3) 24 | .withAttribute(3, TestDomainObject::getField4) 25 | .withAttribute(4, TestDomainObject::getField5) 26 | .withAttribute(5, TestDomainObject::getMeasure1) 27 | .withAttribute(6, TestDomainObject::getMeasure2) 28 | .withAttribute(7, TestDomainObject::getMeasure3) 29 | .withAttribute(8, TestDomainObject::getColour); 30 | 31 | 32 | public static Stream prototypes() { 33 | return Stream.of( 34 | Arguments.of(new TestDomainObject("a_1", "b_1", 35 | "c_1", "d_1", "e_1", 36 | 0D, 0, 0, RED), 37 | (Function) PropertyBasedTest::nextDisjoint, 38 | (Function) PropertyBasedTest::changeColour, 39 | 5), 40 | Arguments.of(new TestDomainObject("a_1", "b_1", 41 | "c_1", "d_1", "e_1", 42 | 0D, 0, 0, RED), 43 | (Function) PropertyBasedTest::nextDisjoint, 44 | (Function) PropertyBasedTest::changeColour, 45 | 63), 46 | Arguments.of(new TestDomainObject("a_1", "b_1", 47 | "c_1", "d_1", "e_1", 48 | 0D, 0, 0, RED), 49 | (Function) PropertyBasedTest::nextDisjoint, 50 | (Function) PropertyBasedTest::changeColour, 51 | 65), 52 | Arguments.of(new TestDomainObject("a_1", "b_1", 53 | "c_1", "d_1", "e_1", 54 | 0D, 0, 0, RED), 55 | (Function) PropertyBasedTest::nextDisjoint, 56 | (Function) PropertyBasedTest::changeColour, 57 | 1500), 58 | Arguments.of(new TestDomainObject("a_1", "b_1", 59 | "c_1", "d_1", "e_1", 60 | 0D, 0, 0, RED), 61 | (Function) PropertyBasedTest::nextDisjoint, 62 | (Function) PropertyBasedTest::changeColour, 63 | 15000), 64 | Arguments.of(new TestDomainObject("a_1", "b_1", 65 | "c_1", "d_1", "e_1", 66 | 0D, 0, 0, RED), 67 | (Function) PropertyBasedTest::nextDisjoint, 68 | (Function) PropertyBasedTest::changeColour, 69 | 20000) 70 | ); 71 | } 72 | 73 | 74 | @ParameterizedTest 75 | @MethodSource("prototypes") 76 | public void testDisjoint(TestDomainObject prototype, 77 | Function next, 78 | Function wontMatch, 79 | int count) { 80 | var classifier = Classifier.builder(SCHEMA) 81 | .build(expand(prototype, next, count)); 82 | var input = prototype.clone(); 83 | for (int i = 0; i < count; ++i) { 84 | var classification = classifier.classificationOrNull(input); 85 | assertNotNull(classification); 86 | assertEquals(i, (int)classification); 87 | assertNull(classifier.classificationOrNull(wontMatch.apply(input))); 88 | input = next.apply(input); 89 | } 90 | } 91 | 92 | @ValueSource(ints = {5, 63, 1500, 16485}) 93 | @ParameterizedTest 94 | public void consistentOverlaps(int count) { 95 | var prototype = new TestDomainObject("a_1", "b_1", 96 | "c_1", "d_1", "e_1", 97 | 0D, 0, 0, RED); 98 | var classifier = Classifier.builder(SCHEMA) 99 | .build(expand(prototype.clone(), PropertyBasedTest::nextOverlapping, count)); 100 | var matchesByClassification = new HashMap>(); 101 | classifier.forEachClassification(prototype, 102 | classification -> matchesByClassification.computeIfAbsent(classification, HashSet::new).add(0)); 103 | classifier.forEachClassification(nextOverlapping(prototype), 104 | classification -> matchesByClassification.computeIfAbsent(classification, HashSet::new).add(1)); 105 | assertEquals(Set.of(0, 1), matchesByClassification.get(0)); 106 | assertEquals(Set.of(1), matchesByClassification.get(1)); 107 | 108 | } 109 | 110 | 111 | private static List> expand(TestDomainObject prototype, 112 | Function next, 113 | int count) { 114 | var constraints = new ArrayList>(count); 115 | for (int i = 0; i < count; ++i) { 116 | constraints.add( 117 | MatchingConstraint.anonymous() 118 | .eq(0, prototype.getField1()) 119 | .eq(1, prototype.getField2()) 120 | .eq(2, prototype.getField3()) 121 | .eq(3, prototype.getField4()) 122 | .eq(4, prototype.getField5()) 123 | .gt(5, prototype.getMeasure1() - 1e-7) 124 | .le(6, prototype.getMeasure2()) 125 | .ge(7, prototype.getMeasure3()) 126 | .eq(8, prototype.getColour()) 127 | .priority(i) 128 | .classification(i) 129 | .build() 130 | ); 131 | prototype = next.apply(prototype); 132 | } 133 | return constraints; 134 | } 135 | 136 | 137 | private static TestDomainObject changeColour(TestDomainObject prototype) { 138 | return prototype.clone().setColour(TestDomainObject.Colour.next(prototype.getColour())); 139 | } 140 | 141 | private static TestDomainObject nextDisjoint(TestDomainObject prototype) { 142 | return prototype.clone() 143 | .setField1(next(prototype.getField1())) 144 | .setField2(next(prototype.getField2())) 145 | .setField3(next(prototype.getField3())) 146 | .setField4(next(prototype.getField4())) 147 | .setField5(next(prototype.getField5())) 148 | .setMeasure1(prototype.getMeasure1() + 1) 149 | .setMeasure2(prototype.getMeasure2() + 1) 150 | .setMeasure3(prototype.getMeasure2() + 1); 151 | } 152 | 153 | private static TestDomainObject nextOverlapping(TestDomainObject prototype) { 154 | return prototype.clone() 155 | .setMeasure1(prototype.getMeasure1() + 1) 156 | .setMeasure2(prototype.getMeasure2() - 1) 157 | .setMeasure3(prototype.getMeasure2() + 1); 158 | } 159 | 160 | private static String next(String x) { 161 | var split = x.split("_"); 162 | return split[0] + "_" + (Integer.parseInt(split[1]) + 1); 163 | } 164 | 165 | 166 | } 167 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/TestDomainObject.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core; 2 | 3 | import java.util.UUID; 4 | import java.util.concurrent.ThreadLocalRandom; 5 | 6 | public class TestDomainObject { 7 | 8 | private String field1; 9 | private String field2; 10 | private String field3; 11 | private String field4; 12 | private String field5; 13 | private double measure1; 14 | private int measure2; 15 | private long measure3; 16 | private Colour colour; 17 | 18 | public TestDomainObject(String field1, 19 | String field2, 20 | String field3, 21 | String field4, 22 | String field5, 23 | double measure1, 24 | int measure2, 25 | long measure3, 26 | Colour colour) { 27 | this.field1 = field1; 28 | this.field2 = field2; 29 | this.field3 = field3; 30 | this.field4 = field4; 31 | this.field5 = field5; 32 | this.measure1 = measure1; 33 | this.measure2 = measure2; 34 | this.measure3 = measure3; 35 | this.colour = colour; 36 | 37 | } 38 | 39 | public static TestDomainObject random() { 40 | return new TestDomainObject(UUID.randomUUID().toString(), 41 | UUID.randomUUID().toString(), 42 | UUID.randomUUID().toString(), 43 | UUID.randomUUID().toString(), 44 | UUID.randomUUID().toString(), 45 | ThreadLocalRandom.current().nextDouble(), 46 | ThreadLocalRandom.current().nextInt(), 47 | ThreadLocalRandom.current().nextLong(), 48 | Colour.RED 49 | ); 50 | } 51 | 52 | public String getField1() { 53 | return field1; 54 | } 55 | 56 | public TestDomainObject setField1(String field1) { 57 | this.field1 = field1; 58 | return this; 59 | } 60 | 61 | public String getField2() { 62 | return field2; 63 | } 64 | 65 | public TestDomainObject setField2(String field2) { 66 | this.field2 = field2; 67 | return this; 68 | } 69 | 70 | public String getField3() { 71 | return field3; 72 | } 73 | 74 | public TestDomainObject setField3(String field3) { 75 | this.field3 = field3; 76 | return this; 77 | } 78 | 79 | public String getField4() { 80 | return field4; 81 | } 82 | 83 | public TestDomainObject setField4(String field4) { 84 | this.field4 = field4; 85 | return this; 86 | } 87 | 88 | public String getField5() { 89 | return field5; 90 | } 91 | 92 | public TestDomainObject setField5(String field5) { 93 | this.field5 = field5; 94 | return this; 95 | } 96 | 97 | public double getMeasure1() { 98 | return measure1; 99 | } 100 | 101 | public TestDomainObject setMeasure1(double measure1) { 102 | this.measure1 = measure1; 103 | return this; 104 | } 105 | 106 | public int getMeasure2() { 107 | return measure2; 108 | } 109 | 110 | public TestDomainObject setMeasure2(int measure2) { 111 | this.measure2 = measure2; 112 | return this; 113 | } 114 | 115 | public long getMeasure3() { 116 | return measure3; 117 | } 118 | 119 | public TestDomainObject setMeasure3(long measure3) { 120 | this.measure3 = measure3; 121 | return this; 122 | } 123 | 124 | public Colour getColour() { 125 | return colour; 126 | } 127 | 128 | public TestDomainObject setColour(Colour colour) { 129 | this.colour = colour; 130 | return this; 131 | } 132 | 133 | public enum Fields { 134 | FIELD1, 135 | MEASURE1 136 | } 137 | 138 | public enum Colour { 139 | RED, BLUE, YELLOW; 140 | 141 | private static Colour[] VALUES = values(); 142 | 143 | public static Colour next(Colour colour) { 144 | return VALUES[(colour.ordinal() + 1) % VALUES.length]; 145 | } 146 | } 147 | 148 | public TestDomainObject clone() { 149 | return new TestDomainObject( 150 | field1, field2, field3, field4, field5, measure1, measure2, measure3, colour 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/masks/MaskTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.masks; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.parallel.Execution; 6 | import org.junit.jupiter.api.parallel.ExecutionMode; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | @Execution(ExecutionMode.CONCURRENT) 11 | class MaskTest { 12 | 13 | private MaskStore bitmapMaskStore; 14 | private MaskStore roaringMaskStore; 15 | private MaskStore wordMaskStore; 16 | 17 | @BeforeEach 18 | public void init() { 19 | bitmapMaskStore = BitsetMask.store(1 << 12); 20 | roaringMaskStore = RoaringMask.store(1024 * 1024, false); 21 | wordMaskStore = WordMask.store(64); 22 | } 23 | 24 | @Test 25 | public void testTinyMask() { 26 | var range = wordMaskStore.contiguous(50); 27 | var set = wordMaskStore.of(1, 10); 28 | assertEquals(set, range.and(set)); 29 | assertEquals(range, range.or(set)); 30 | assertEquals(wordMaskStore.of(), set.andNot(range)); 31 | assertTrue(wordMaskStore.of().isEmpty()); 32 | assertFalse(wordMaskStore.contiguous(1).isEmpty()); 33 | } 34 | 35 | @Test 36 | public void testStreamTinyMask() { 37 | assertEquals(50, wordMaskStore.contiguous(50).stream().count()); 38 | assertEquals(50, wordMaskStore.contiguous(50).stream().distinct().count()); 39 | assertEquals(48, wordMaskStore.contiguous(50).inPlaceAndNot(wordMaskStore.of(1, 2)).stream().count()); 40 | assertEquals(48, wordMaskStore.contiguous(50).inPlaceAndNot(wordMaskStore.of(1, 2)).stream().distinct().count()); 41 | } 42 | 43 | @Test 44 | public void testTinyMaskInPlace() { 45 | assertEquals(wordMaskStore.contiguous(11).inPlaceAnd(wordMaskStore.of(1, 2)), wordMaskStore.contiguous(11).inPlaceAnd(wordMaskStore.of(1, 2))); 46 | assertEquals(wordMaskStore.contiguous(10).inPlaceOr(wordMaskStore.of(11, 12)), wordMaskStore.contiguous(10).inPlaceOr(wordMaskStore.of(11, 12))); 47 | } 48 | 49 | @Test 50 | public void testBitmapMask() { 51 | BitsetMask range = bitmapMaskStore.contiguous(1 << 12); 52 | BitsetMask set = bitmapMaskStore.of(1, 1 << 11); 53 | assertEquals(set, range.and(set)); 54 | assertEquals(range, range.or(set)); 55 | assertEquals(bitmapMaskStore.of(), set.andNot(range)); 56 | assertTrue(bitmapMaskStore.of().isEmpty()); 57 | assertFalse(bitmapMaskStore.contiguous(1).isEmpty()); 58 | } 59 | 60 | @Test 61 | public void testStreamBitmapMask() { 62 | assertEquals(1 << 12, bitmapMaskStore.contiguous(1 << 12).stream().count()); 63 | assertEquals(1 << 12, bitmapMaskStore.contiguous(1 << 12).stream().distinct().count()); 64 | assertEquals((1 << 12) - 2, bitmapMaskStore.contiguous(1 << 12).inPlaceAndNot(bitmapMaskStore.of(1, 2)).stream().count()); 65 | assertEquals((1 << 12) - 2, bitmapMaskStore.contiguous(1 << 12).inPlaceAndNot(bitmapMaskStore.of(1, 2)).stream().distinct().count()); 66 | } 67 | 68 | @Test 69 | public void testBitmapMaskInPlace() { 70 | assertEquals(bitmapMaskStore.contiguous(1 << 11).and(bitmapMaskStore.of(1, 2)), bitmapMaskStore.contiguous(1 << 11).inPlaceAnd(bitmapMaskStore.of(1, 2))); 71 | assertEquals(bitmapMaskStore.contiguous(100).or(bitmapMaskStore.of(101, 102)), bitmapMaskStore.contiguous(100).inPlaceOr(bitmapMaskStore.of(101, 102))); 72 | } 73 | 74 | @Test 75 | public void testHugeMask() { 76 | RoaringMask range = roaringMaskStore.contiguous(1 << 22); 77 | RoaringMask set = roaringMaskStore.of(1, 1 << 11); 78 | assertEquals(set, range.and(set)); 79 | assertEquals(range, range.or(set)); 80 | assertEquals(roaringMaskStore.of(), set.andNot(range)); 81 | assertTrue(roaringMaskStore.of().isEmpty()); 82 | assertFalse(roaringMaskStore.contiguous(1).isEmpty()); 83 | } 84 | 85 | 86 | @Test 87 | public void testOptimiseRoaringMask() { 88 | RoaringMask range = roaringMaskStore.contiguous(1 << 22); 89 | range.optimise(); 90 | RoaringMask set = roaringMaskStore.of(1, 1 << 11); 91 | assertEquals(set, range.and(set)); 92 | assertEquals(range, range.or(set)); 93 | assertEquals(roaringMaskStore.of(), set.andNot(range)); 94 | assertTrue(roaringMaskStore.of().isEmpty()); 95 | assertFalse(roaringMaskStore.contiguous(1).isEmpty()); 96 | } 97 | 98 | @Test 99 | public void testStreamHugeMask() { 100 | assertEquals(1 << 22, roaringMaskStore.contiguous(1 << 22).stream().count()); 101 | assertEquals(1 << 22, roaringMaskStore.contiguous(1 << 22).stream().distinct().count()); 102 | assertEquals((1 << 22) - 2, roaringMaskStore.contiguous(1 << 22).andNot(roaringMaskStore.of(1, 2)).stream().count()); 103 | assertEquals((1 << 22) - 2, roaringMaskStore.contiguous(1 << 22).andNot(roaringMaskStore.of(1, 2)).stream().distinct().count()); 104 | } 105 | 106 | @Test 107 | public void testHugeMaskInPlace() { 108 | assertEquals(roaringMaskStore.contiguous(1 << 11).and(roaringMaskStore.of(1, 2)), roaringMaskStore.contiguous(1 << 11).inPlaceAnd(roaringMaskStore.of(1, 2))); 109 | assertEquals(roaringMaskStore.contiguous(100).or(roaringMaskStore.of(101, 102)), roaringMaskStore.contiguous(100).inPlaceOr(roaringMaskStore.of(101, 102))); 110 | } 111 | 112 | 113 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/matchers/ComparableMutableNodeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.BitsetMask; 6 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 7 | import io.github.richardstartin.multimatcher.core.matchers.nodes.ComparableNode; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.parallel.Execution; 11 | import org.junit.jupiter.api.parallel.ExecutionMode; 12 | 13 | import java.time.LocalDate; 14 | import java.util.Comparator; 15 | 16 | import static io.github.richardstartin.multimatcher.core.Mask.with; 17 | import static io.github.richardstartin.multimatcher.core.Operation.*; 18 | import static io.github.richardstartin.multimatcher.core.masks.BitsetMask.store; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertTrue; 21 | 22 | @Execution(ExecutionMode.CONCURRENT) 23 | public class ComparableMutableNodeTest { 24 | 25 | private MaskStore store; 26 | private BitsetMask zero; 27 | private BitsetMask one; 28 | private BitsetMask zeroOrOne; 29 | 30 | @BeforeEach 31 | public void init() { 32 | store = store(200); 33 | zero = with(store.newMask(), 0); 34 | one = with(store.newMask(), 1); 35 | zeroOrOne = zero.or(one); 36 | } 37 | 38 | @Test 39 | public void testGreaterThan() { 40 | var node = build(100, GT); 41 | assertTrue(store.isEmpty(node.match(LocalDate.ofEpochDay(0)))); 42 | assertEquals(zero, store.getMask(node.match(LocalDate.ofEpochDay(1)))); 43 | assertEquals(zeroOrOne, store.getMask(node.match(LocalDate.ofEpochDay(11)))); 44 | } 45 | 46 | @Test 47 | public void testEqual() { 48 | var node = build(100, EQ); 49 | assertTrue(store.isEmpty(node.match(LocalDate.ofEpochDay(1)))); 50 | assertEquals(zero, store.getMask(node.match(LocalDate.ofEpochDay(0)))); 51 | assertEquals(one, store.getMask(node.match(LocalDate.ofEpochDay(10)))); 52 | } 53 | 54 | @Test 55 | public void testLessThan() { 56 | var node = build(100, LT); 57 | BitsetMask mask = store.contiguous(100); 58 | assertTrue(store.isEmpty(node.match(LocalDate.ofEpochDay(1001)))); 59 | assertEquals(mask.andNot(zero), store.getMask(node.match(LocalDate.ofEpochDay(0)))); 60 | assertEquals(mask.andNot(zeroOrOne), store.getMask(node.match(LocalDate.ofEpochDay(10)))); 61 | } 62 | 63 | @Test 64 | public void testGreaterThanRev() { 65 | var node = buildRev(100, GT); 66 | assertTrue(store.isEmpty(node.match(LocalDate.ofEpochDay(0)))); 67 | assertEquals(zero, store.getMask(node.match(LocalDate.ofEpochDay(1)))); 68 | } 69 | 70 | @Test 71 | public void testEqualRev() { 72 | var node = buildRev(100, EQ); 73 | assertTrue(store.isEmpty(node.match(LocalDate.ofEpochDay(1)))); 74 | assertEquals(zero, store.getMask(node.match(LocalDate.ofEpochDay(0)))); 75 | assertEquals(one, store.getMask(node.match(LocalDate.ofEpochDay(10)))); 76 | } 77 | 78 | @Test 79 | public void testLessThanRev() { 80 | var node = buildRev(100, LT); 81 | BitsetMask mask = store.contiguous(100); 82 | assertTrue(store.isEmpty(node.match(LocalDate.ofEpochDay(1001)))); 83 | assertEquals(mask.andNot(zero), store.getMask(node.match(LocalDate.ofEpochDay(0)))); 84 | assertEquals(mask.andNot(zeroOrOne), store.getMask(node.match(LocalDate.ofEpochDay(10)))); 85 | } 86 | 87 | @Test 88 | public void testBuildNode() { 89 | var node = new ComparableNode<>(store, Comparator.comparingDouble(Double::doubleValue), GT); 90 | node.add(0D, 0); 91 | assertEquals(store.contiguous(1), store.getMask(node.match(1D))); 92 | node.add(10D, 1); 93 | node.freeze(); 94 | assertEquals(store.contiguous(2), store.getMask(node.match(11D))); 95 | } 96 | 97 | private ComparableNode build(int count, Operation operation) { 98 | var node = new ComparableNode<>(store, Comparator.naturalOrder(), operation); 99 | for (int i = 0; i < count; ++i) { 100 | node.add(LocalDate.ofEpochDay(i * 10), i); 101 | } 102 | return node.freeze(); 103 | } 104 | 105 | private ComparableNode buildRev(int count, Operation operation) { 106 | ComparableNode node = new ComparableNode<>(store, Comparator.naturalOrder(), operation); 107 | for (int i = count - 1; i >= 0; --i) { 108 | node.add(LocalDate.ofEpochDay(i * 10), i); 109 | } 110 | return node.freeze(); 111 | } 112 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/matchers/DoubleMutableNodeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | 4 | import io.github.richardstartin.multimatcher.core.Mask; 5 | import io.github.richardstartin.multimatcher.core.Operation; 6 | import io.github.richardstartin.multimatcher.core.masks.BitsetMask; 7 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 8 | import io.github.richardstartin.multimatcher.core.masks.RoaringMask; 9 | import io.github.richardstartin.multimatcher.core.masks.WordMask; 10 | import io.github.richardstartin.multimatcher.core.matchers.nodes.DoubleNode; 11 | import io.github.richardstartin.multimatcher.core.matchers.nodes.LongNode; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.parallel.Execution; 15 | import org.junit.jupiter.api.parallel.ExecutionMode; 16 | import org.junit.jupiter.params.ParameterizedTest; 17 | import org.junit.jupiter.params.provider.Arguments; 18 | import org.junit.jupiter.params.provider.MethodSource; 19 | 20 | import java.util.stream.IntStream; 21 | import java.util.stream.Stream; 22 | 23 | import static io.github.richardstartin.multimatcher.core.Mask.with; 24 | import static io.github.richardstartin.multimatcher.core.Mask.without; 25 | import static io.github.richardstartin.multimatcher.core.masks.BitsetMask.store; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertTrue; 28 | 29 | @Execution(ExecutionMode.CONCURRENT) 30 | public class DoubleMutableNodeTest { 31 | 32 | public static Stream stores() { 33 | return IntStream.of(32, 64, 100, 200).boxed() 34 | .flatMap(i -> { 35 | if (i <= 64) { 36 | return Stream.of( 37 | Arguments.of(i/2, WordMask.store(i)), 38 | Arguments.of(i/2, BitsetMask.store(i)), 39 | Arguments.of(i/2, RoaringMask.store(0, false)) 40 | ); 41 | } else { 42 | return Stream.of( 43 | Arguments.of(i/2, BitsetMask.store(i)), 44 | Arguments.of(i/2, RoaringMask.store(0, false)) 45 | ); 46 | } 47 | }); 48 | } 49 | 50 | @ParameterizedTest 51 | @MethodSource("stores") 52 | public > void testGreaterThan(int maxElement, MaskStore store) { 53 | var node = build(store, maxElement, Operation.GT); 54 | int mask = store.newContiguousMaskId(maxElement); 55 | assertTrue(store.isEmpty(node.match(0L, mask))); 56 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(1L, mask))); 57 | } 58 | 59 | 60 | @ParameterizedTest 61 | @MethodSource("stores") 62 | public > void testGreaterThanOrEqual(int maxElement, MaskStore store) { 63 | var node = build(store, maxElement, Operation.GE); 64 | int mask = store.newContiguousMaskId(maxElement); 65 | assertTrue(store.isEmpty(node.match(-1L, mask))); 66 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 67 | assertEquals(with(with(store.newMask(), 0), 1), store.getMask(node.match(10L, mask))); 68 | } 69 | 70 | @ParameterizedTest 71 | @MethodSource("stores") 72 | public > void testEqual(int maxElement, MaskStore store) { 73 | var node = build(store, maxElement, Operation.EQ); 74 | int mask = store.newContiguousMaskId(maxElement); 75 | assertTrue(store.isEmpty(node.match(-1L, mask))); 76 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 77 | assertEquals(with(store.newMask(), 1), store.getMask(node.match(10L, mask))); 78 | } 79 | 80 | 81 | @ParameterizedTest 82 | @MethodSource("stores") 83 | public > void testLessThanOrEqual(int maxElement, MaskStore store) { 84 | var node = build(store, maxElement, Operation.LE); 85 | int mask = store.newContiguousMaskId(maxElement); 86 | assertTrue(store.isEmpty(node.match(1001, mask))); 87 | assertEquals(store.getMask(mask), store.getMask(node.match(0L, mask))); 88 | assertEquals(without(store.getMask(mask), 0), store.getMask(node.match(10L, mask))); 89 | } 90 | 91 | @ParameterizedTest 92 | @MethodSource("stores") 93 | public > void testLessThan(int maxElement, MaskStore store) { 94 | var node = build(store, maxElement, Operation.LT); 95 | int mask = store.newContiguousMaskId(maxElement); 96 | assertTrue(store.isEmpty(node.match(1001, mask))); 97 | assertEquals(without(store.getMask(mask).clone(), 0), 98 | store.getMask(node.match(0L, mask))); 99 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), 100 | store.getMask(node.match(10L, mask))); 101 | } 102 | 103 | @ParameterizedTest 104 | @MethodSource("stores") 105 | public > void testGreaterThanRev(int maxElement, MaskStore store) { 106 | var node = buildRev(store, maxElement, Operation.GT); 107 | int mask = store.newContiguousMaskId(maxElement); 108 | assertTrue(store.isEmpty(node.match(0L, mask))); 109 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(1L, mask))); 110 | } 111 | 112 | @ParameterizedTest 113 | @MethodSource("stores") 114 | public > void testBuildNode(int maxElement, MaskStore store) { 115 | var node = new LongNode<>(store, Operation.EQ); 116 | node.add(0, 0); 117 | assertEquals(store.contiguous(1), store.getMask(node.match(0, store.newContiguousMaskId(1)))); 118 | node.add(0, 1); 119 | assertEquals(store.contiguous(2), store.getMask(node.match(0, store.newContiguousMaskId(2)))); 120 | } 121 | 122 | @ParameterizedTest 123 | @MethodSource("stores") 124 | public > void testGreaterThanOrEqualRev(int maxElement, MaskStore store) { 125 | var node = buildRev(store, maxElement, Operation.GE); 126 | int mask = store.newContiguousMaskId(maxElement); 127 | assertTrue(store.isEmpty(node.match(-1L, mask))); 128 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 129 | assertEquals(with(with(store.newMask(), 0), 1), store.getMask(node.match(10L, mask))); 130 | } 131 | 132 | @ParameterizedTest 133 | @MethodSource("stores") 134 | public > void testEqualRev(int maxElement, MaskStore store) { 135 | var node = buildRev(store, maxElement, Operation.EQ); 136 | int mask = store.newContiguousMaskId(maxElement); 137 | assertTrue(store.isEmpty(node.match(-1L, mask))); 138 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 139 | assertEquals(with(store.newMask(), 1), store.getMask(node.match(10L, mask))); 140 | } 141 | 142 | 143 | @ParameterizedTest 144 | @MethodSource("stores") 145 | public > void testLessThanOrEqualRev(int maxElement, MaskStore store) { 146 | var node = buildRev(store, maxElement, Operation.LE); 147 | int mask = store.newContiguousMaskId(maxElement); 148 | assertTrue(store.isEmpty(node.match(1001L, mask))); 149 | assertEquals(store.getMask(mask), store.getMask(node.match(0L, mask))); 150 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(10L, mask))); 151 | } 152 | 153 | @ParameterizedTest 154 | @MethodSource("stores") 155 | public > void testLessThanRev(int maxElement, MaskStore store) { 156 | var node = buildRev(store, maxElement, Operation.LT); 157 | int mask = store.newContiguousMaskId(maxElement); 158 | assertTrue(store.isEmpty(node.match(1001L, mask))); 159 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(0L, mask))); 160 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), store.getMask(node.match(10L, mask))); 161 | } 162 | 163 | 164 | private > 165 | DoubleNode build(MaskStore store, int count, Operation relation) { 166 | DoubleNode node = new DoubleNode<>(store, relation); 167 | for (int i = 0; i < count; ++i) { 168 | node.add(i * 10, i); 169 | } 170 | return node.optimise(); 171 | } 172 | 173 | private > 174 | DoubleNode buildRev(MaskStore store, int count, Operation relation) { 175 | DoubleNode node = new DoubleNode<>(store, relation); 176 | for (int i = count - 1; i >= 0; --i) { 177 | node.add(i * 10, i); 178 | } 179 | return node.optimise(); 180 | } 181 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/matchers/IntNodeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.BitsetMask; 6 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 7 | import io.github.richardstartin.multimatcher.core.masks.RoaringMask; 8 | import io.github.richardstartin.multimatcher.core.masks.WordMask; 9 | import io.github.richardstartin.multimatcher.core.matchers.nodes.IntNode; 10 | import io.github.richardstartin.multimatcher.core.matchers.nodes.LongNode; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.parallel.Execution; 13 | import org.junit.jupiter.api.parallel.ExecutionMode; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.Arguments; 16 | import org.junit.jupiter.params.provider.MethodSource; 17 | 18 | import java.util.stream.IntStream; 19 | import java.util.stream.Stream; 20 | 21 | import static io.github.richardstartin.multimatcher.core.Mask.with; 22 | import static io.github.richardstartin.multimatcher.core.Mask.without; 23 | import static io.github.richardstartin.multimatcher.core.Operation.*; 24 | import static io.github.richardstartin.multimatcher.core.masks.BitsetMask.store; 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | import static org.junit.jupiter.api.Assertions.assertTrue; 27 | 28 | @Execution(ExecutionMode.CONCURRENT) 29 | public class IntNodeTest { 30 | 31 | 32 | public static Stream stores() { 33 | return IntStream.of(32, 64, 100, 200).boxed() 34 | .flatMap(i -> { 35 | if (i <= 64) { 36 | return Stream.of( 37 | Arguments.of(i/2, WordMask.store(i)), 38 | Arguments.of(i/2, BitsetMask.store(i)), 39 | Arguments.of(i/2, RoaringMask.store(0, false)) 40 | ); 41 | } else { 42 | return Stream.of( 43 | Arguments.of(i/2, BitsetMask.store(i)), 44 | Arguments.of(i/2, RoaringMask.store(0, false)) 45 | ); 46 | } 47 | }); 48 | } 49 | 50 | @ParameterizedTest 51 | @MethodSource("stores") 52 | public > void testGreaterThan(int maxElement, MaskStore store) { 53 | var node = build(store, maxElement, Operation.GT); 54 | int mask = store.newContiguousMaskId(maxElement); 55 | assertTrue(store.isEmpty(node.match(0, mask))); 56 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(1, mask))); 57 | } 58 | 59 | 60 | @ParameterizedTest 61 | @MethodSource("stores") 62 | public > void testGreaterThanOrEqual(int maxElement, MaskStore store) { 63 | var node = build(store, maxElement, Operation.GE); 64 | int mask = store.newContiguousMaskId(maxElement); 65 | assertTrue(store.isEmpty(node.match(-1, mask))); 66 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0, mask))); 67 | assertEquals(with(with(store.newMask(), 0), 1), store.getMask(node.match(10, mask))); 68 | } 69 | 70 | @ParameterizedTest 71 | @MethodSource("stores") 72 | public > void testEqual(int maxElement, MaskStore store) { 73 | var node = build(store, maxElement, Operation.EQ); 74 | int mask = store.newContiguousMaskId(maxElement); 75 | assertTrue(store.isEmpty(node.match(-1, mask))); 76 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0, mask))); 77 | assertEquals(with(store.newMask(), 1), store.getMask(node.match(10, mask))); 78 | } 79 | 80 | 81 | @ParameterizedTest 82 | @MethodSource("stores") 83 | public > void testLessThanOrEqual(int maxElement, MaskStore store) { 84 | var node = build(store, maxElement, Operation.LE); 85 | int mask = store.newContiguousMaskId(maxElement); 86 | assertTrue(store.isEmpty(node.match(1001, mask))); 87 | assertEquals(store.getMask(mask), store.getMask(node.match(0, mask))); 88 | assertEquals(without(store.getMask(mask), 0), store.getMask(node.match(10, mask))); 89 | } 90 | 91 | @ParameterizedTest 92 | @MethodSource("stores") 93 | public > void testLessThan(int maxElement, MaskStore store) { 94 | var node = build(store, maxElement, Operation.LT); 95 | int mask = store.newContiguousMaskId(maxElement); 96 | assertTrue(store.isEmpty(node.match(1001, mask))); 97 | assertEquals(without(store.getMask(mask).clone(), 0), 98 | store.getMask(node.match(0, mask))); 99 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), 100 | store.getMask(node.match(10, mask))); 101 | } 102 | 103 | @ParameterizedTest 104 | @MethodSource("stores") 105 | public > void testGreaterThanRev(int maxElement, MaskStore store) { 106 | var node = buildRev(store, maxElement, Operation.GT); 107 | int mask = store.newContiguousMaskId(maxElement); 108 | assertTrue(store.isEmpty(node.match(0, mask))); 109 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(1, mask))); 110 | } 111 | 112 | @ParameterizedTest 113 | @MethodSource("stores") 114 | public > void testBuildNode(int maxElement, MaskStore store) { 115 | var node = new LongNode<>(store, Operation.EQ); 116 | node.add(0, 0); 117 | assertEquals(store.contiguous(1), store.getMask(node.match(0, store.newContiguousMaskId(1)))); 118 | node.add(0, 1); 119 | assertEquals(store.contiguous(2), store.getMask(node.match(0, store.newContiguousMaskId(2)))); 120 | } 121 | 122 | @ParameterizedTest 123 | @MethodSource("stores") 124 | public > void testGreaterThanOrEqualRev(int maxElement, MaskStore store) { 125 | var node = buildRev(store, maxElement, Operation.GE); 126 | int mask = store.newContiguousMaskId(maxElement); 127 | assertTrue(store.isEmpty(node.match(-1, mask))); 128 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0, mask))); 129 | assertEquals(with(with(store.newMask(), 0), 1), store.getMask(node.match(10, mask))); 130 | } 131 | 132 | @ParameterizedTest 133 | @MethodSource("stores") 134 | public > void testEqualRev(int maxElement, MaskStore store) { 135 | var node = buildRev(store, maxElement, Operation.EQ); 136 | int mask = store.newContiguousMaskId(maxElement); 137 | assertTrue(store.isEmpty(node.match(-1, mask))); 138 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0, mask))); 139 | assertEquals(with(store.newMask(), 1), store.getMask(node.match(10, mask))); 140 | } 141 | 142 | 143 | @ParameterizedTest 144 | @MethodSource("stores") 145 | public > void testLessThanOrEqualRev(int maxElement, MaskStore store) { 146 | var node = buildRev(store, maxElement, Operation.LE); 147 | int mask = store.newContiguousMaskId(maxElement); 148 | assertTrue(store.isEmpty(node.match(1001, mask))); 149 | assertEquals(store.getMask(mask), store.getMask(node.match(0, mask))); 150 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(10, mask))); 151 | } 152 | 153 | @ParameterizedTest 154 | @MethodSource("stores") 155 | public > void testLessThanRev(int maxElement, MaskStore store) { 156 | var node = buildRev(store, maxElement, Operation.LT); 157 | int mask = store.newContiguousMaskId(maxElement); 158 | assertTrue(store.isEmpty(node.match(1001, mask))); 159 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(0, mask))); 160 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), 161 | store.getMask(node.match(10, mask))); 162 | } 163 | 164 | 165 | private > 166 | IntNode build(MaskStore store, int count, Operation relation) { 167 | var node = new IntNode<>(store, relation); 168 | for (int i = 0; i < count; ++i) { 169 | node.add(i * 10, i); 170 | } 171 | return node.optimise(); 172 | } 173 | 174 | private > 175 | IntNode buildRev(MaskStore store, int count, Operation relation) { 176 | var node = new IntNode<>(store, relation); 177 | for (int i = count - 1; i >= 0; --i) { 178 | node.add(i * 10, i); 179 | } 180 | return node.optimise(); 181 | } 182 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/matchers/IntNodeTestTiny.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Operation; 4 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 5 | import io.github.richardstartin.multimatcher.core.masks.WordMask; 6 | import io.github.richardstartin.multimatcher.core.matchers.nodes.IntNode; 7 | import org.junit.jupiter.api.parallel.Execution; 8 | import org.junit.jupiter.api.parallel.ExecutionMode; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | 13 | import java.util.stream.Stream; 14 | 15 | import static io.github.richardstartin.multimatcher.core.Mask.with; 16 | import static io.github.richardstartin.multimatcher.core.Mask.without; 17 | import static io.github.richardstartin.multimatcher.core.Operation.*; 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.junit.jupiter.api.Assertions.assertTrue; 20 | 21 | @Execution(ExecutionMode.CONCURRENT) 22 | public class IntNodeTestTiny { 23 | 24 | public static Stream stores() { 25 | return Stream.of(WordMask.store(32), WordMask.store(64)) 26 | .map(Arguments::of); 27 | } 28 | 29 | @ParameterizedTest 30 | @MethodSource("stores") 31 | public void testGreaterThan() { 32 | var store = WordMask.store(64); 33 | IntNode node = build(store, 5, GT); 34 | int mask = store.newContiguousMaskId(5); 35 | assertTrue(store.isEmpty(node.match(0, mask))); 36 | assertEquals(with(new WordMask(), 0), store.getMask(node.match(1, mask))); 37 | } 38 | 39 | 40 | @ParameterizedTest 41 | @MethodSource("stores") 42 | public void testGreaterThanOrEqual(MaskStore store) { 43 | IntNode node = build(store, 5, GE); 44 | int mask = store.newContiguousMaskId(5); 45 | assertTrue(store.isEmpty(node.match(-1, mask))); 46 | assertEquals(with(new WordMask(), 0), store.getMask(node.match(0, mask))); 47 | assertEquals(with(with(new WordMask(), 1), 0), store.getMask(node.match(10, mask))); 48 | } 49 | 50 | @ParameterizedTest 51 | @MethodSource("stores") 52 | public void testEqual(MaskStore store) { 53 | IntNode node = build(store, 5, EQ); 54 | int mask = store.newContiguousMaskId(5); 55 | assertTrue(store.isEmpty(node.match(-1, mask))); 56 | assertEquals(with(new WordMask(), 0), store.getMask(node.match(0, mask))); 57 | assertEquals(with(new WordMask(), 1), store.getMask(node.match(10, mask))); 58 | } 59 | 60 | 61 | @ParameterizedTest 62 | @MethodSource("stores") 63 | public void testLessThanOrEqual(MaskStore store) { 64 | IntNode node = build(store, 5, LE); 65 | int mask = store.newContiguousMaskId(5); 66 | assertTrue(store.isEmpty(node.match(1001, mask))); 67 | assertEquals(store.getMask(mask), store.getMask(node.match(0, mask))); 68 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(10, mask))); 69 | } 70 | 71 | @ParameterizedTest 72 | @MethodSource("stores") 73 | public void testLessThan(MaskStore store) { 74 | IntNode node = build(store, 5, LT); 75 | int mask = store.newContiguousMaskId(5); 76 | assertTrue(store.isEmpty(node.match(1001, mask))); 77 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(0, mask))); 78 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), store.getMask(node.match(10, mask))); 79 | } 80 | 81 | @ParameterizedTest 82 | @MethodSource("stores") 83 | public void testGreaterThanRev(MaskStore store) { 84 | IntNode node = buildRev(store, 5, GT); 85 | int mask = store.newContiguousMaskId(5); 86 | assertTrue(store.isEmpty(node.match(0, mask))); 87 | assertEquals(with(new WordMask(), 0), store.getMask(node.match(1, mask))); 88 | } 89 | 90 | 91 | @ParameterizedTest 92 | @MethodSource("stores") 93 | public void testGreaterThanOrEqualRev(MaskStore store) { 94 | IntNode node = buildRev(store, 5, GE); 95 | int mask = store.newContiguousMaskId(5); 96 | assertTrue(store.isEmpty(node.match(-1, mask))); 97 | assertEquals(with(new WordMask(), 0), store.getMask(node.match(0, mask))); 98 | assertEquals(with(with(new WordMask(), 0), 1), store.getMask(node.match(10, mask))); 99 | } 100 | 101 | @ParameterizedTest 102 | @MethodSource("stores") 103 | public void testEqualRev(MaskStore store) { 104 | IntNode node = buildRev(store, 5, EQ); 105 | int mask = store.newContiguousMaskId(5); 106 | assertTrue(store.isEmpty(node.match(-1, mask))); 107 | assertEquals(with(new WordMask(), 0), store.getMask(node.match(0, mask))); 108 | assertEquals(with(new WordMask(), 1), store.getMask(node.match(10, mask))); 109 | } 110 | 111 | 112 | @ParameterizedTest 113 | @MethodSource("stores") 114 | public void testLessThanOrEqualRev(MaskStore store) { 115 | IntNode node = buildRev(store, 5, LE); 116 | int mask = store.newContiguousMaskId(5); 117 | assertTrue(store.isEmpty(node.match(1001, mask))); 118 | assertEquals(store.getMask(mask), store.getMask(node.match(0, mask))); 119 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(10, mask))); 120 | } 121 | 122 | @ParameterizedTest 123 | @MethodSource("stores") 124 | public void testLessThanRev(MaskStore store) { 125 | IntNode node = buildRev(store, 5, LT); 126 | int mask = store.newContiguousMaskId(5); 127 | assertTrue(store.isEmpty(node.match(1001, mask))); 128 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(0, mask))); 129 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), store.getMask(node.match(10, mask))); 130 | } 131 | 132 | @ParameterizedTest 133 | @MethodSource("stores") 134 | public void testBuildNode(MaskStore store) { 135 | IntNode node = new IntNode<>(store, EQ); 136 | node.add(0, 0); 137 | assertEquals(store.contiguous(1), store.getMask(node.match(0, store.newContiguousMaskId(1)))); 138 | node.add(0, 1); 139 | assertEquals(store.contiguous(2), store.getMask(node.match(0, store.newContiguousMaskId(2)))); 140 | } 141 | 142 | private IntNode build(MaskStore store, int count, Operation relation) { 143 | IntNode node = new IntNode<>(store, relation); 144 | for (int i = 0; i < count; ++i) { 145 | node.add(i * 10, i); 146 | } 147 | return node.optimise(); 148 | } 149 | 150 | private IntNode buildRev(MaskStore store, int count, Operation relation) { 151 | IntNode node = new IntNode<>(store, relation); 152 | for (int i = count - 1; i >= 0; --i) { 153 | node.add(i * 10, i); 154 | } 155 | return node.optimise(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/matchers/LongMutableNodeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.Mask; 4 | import io.github.richardstartin.multimatcher.core.Operation; 5 | import io.github.richardstartin.multimatcher.core.masks.BitsetMask; 6 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 7 | import io.github.richardstartin.multimatcher.core.masks.RoaringMask; 8 | import io.github.richardstartin.multimatcher.core.masks.WordMask; 9 | import io.github.richardstartin.multimatcher.core.matchers.nodes.LongNode; 10 | import org.junit.jupiter.api.parallel.Execution; 11 | import org.junit.jupiter.api.parallel.ExecutionMode; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.Arguments; 14 | import org.junit.jupiter.params.provider.MethodSource; 15 | 16 | import java.util.stream.IntStream; 17 | import java.util.stream.Stream; 18 | 19 | import static io.github.richardstartin.multimatcher.core.Mask.with; 20 | import static io.github.richardstartin.multimatcher.core.Mask.without; 21 | import static io.github.richardstartin.multimatcher.core.masks.BitsetMask.store; 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | 25 | @Execution(ExecutionMode.CONCURRENT) 26 | public class LongMutableNodeTest { 27 | 28 | public static Stream stores() { 29 | return IntStream.of(32, 64, 100, 200).boxed() 30 | .flatMap(i -> { 31 | if (i <= 64) { 32 | return Stream.of( 33 | Arguments.of(i/2, WordMask.store(i)), 34 | Arguments.of(i/2, BitsetMask.store(i)), 35 | Arguments.of(i/2, RoaringMask.store(0, false)) 36 | ); 37 | } else { 38 | return Stream.of( 39 | Arguments.of(i/2, BitsetMask.store(i)), 40 | Arguments.of(i/2, RoaringMask.store(0, false)) 41 | ); 42 | } 43 | }); 44 | } 45 | 46 | @ParameterizedTest 47 | @MethodSource("stores") 48 | public > void testGreaterThan(int maxElement, MaskStore store) { 49 | var node = build(store, maxElement, Operation.GT); 50 | int mask = store.newContiguousMaskId(maxElement); 51 | assertTrue(store.isEmpty(node.match(0L, mask))); 52 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(1L, mask))); 53 | } 54 | 55 | 56 | @ParameterizedTest 57 | @MethodSource("stores") 58 | public > void testGreaterThanOrEqual(int maxElement, MaskStore store) { 59 | var node = build(store, maxElement, Operation.GE); 60 | int mask = store.newContiguousMaskId(maxElement); 61 | assertTrue(store.isEmpty(node.match(-1L, mask))); 62 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 63 | assertEquals(with(with(store.newMask(), 0), 1), store.getMask(node.match(10L, mask))); 64 | } 65 | 66 | @ParameterizedTest 67 | @MethodSource("stores") 68 | public > void testEqual(int maxElement, MaskStore store) { 69 | var node = build(store, maxElement, Operation.EQ); 70 | int mask = store.newContiguousMaskId(maxElement); 71 | assertTrue(store.isEmpty(node.match(-1L, mask))); 72 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 73 | assertEquals(with(store.newMask(), 1), store.getMask(node.match(10L, mask))); 74 | } 75 | 76 | 77 | @ParameterizedTest 78 | @MethodSource("stores") 79 | public > void testLessThanOrEqual(int maxElement, MaskStore store) { 80 | var node = build(store, maxElement, Operation.LE); 81 | int mask = store.newContiguousMaskId(maxElement); 82 | assertTrue(store.isEmpty(node.match(1001, mask))); 83 | assertEquals(store.getMask(mask), store.getMask(node.match(0L, mask))); 84 | assertEquals(without(store.getMask(mask), 0), store.getMask(node.match(10L, mask))); 85 | } 86 | 87 | @ParameterizedTest 88 | @MethodSource("stores") 89 | public > void testLessThan(int maxElement, MaskStore store) { 90 | var node = build(store, maxElement, Operation.LT); 91 | int mask = store.newContiguousMaskId(maxElement); 92 | assertTrue(store.isEmpty(node.match(1001, mask))); 93 | assertEquals(without(store.getMask(mask).clone(), 0), 94 | store.getMask(node.match(0L, mask))); 95 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), 96 | store.getMask(node.match(10L, mask))); 97 | } 98 | 99 | @ParameterizedTest 100 | @MethodSource("stores") 101 | public > void testGreaterThanRev(int maxElement, MaskStore store) { 102 | var node = buildRev(store, maxElement, Operation.GT); 103 | int mask = store.newContiguousMaskId(maxElement); 104 | assertTrue(store.isEmpty(node.match(0L, mask))); 105 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(1L, mask))); 106 | } 107 | 108 | @ParameterizedTest 109 | @MethodSource("stores") 110 | public > void testBuildNode(int maxElement, MaskStore store) { 111 | var node = new LongNode<>(store, Operation.EQ); 112 | node.add(0, 0); 113 | assertEquals(store.contiguous(1), store.getMask(node.match(0, store.newContiguousMaskId(1)))); 114 | node.add(0, 1); 115 | assertEquals(store.contiguous(2), store.getMask(node.match(0, store.newContiguousMaskId(2)))); 116 | } 117 | 118 | @ParameterizedTest 119 | @MethodSource("stores") 120 | public > void testGreaterThanOrEqualRev(int maxElement, MaskStore store) { 121 | var node = buildRev(store, maxElement, Operation.GE); 122 | int mask = store.newContiguousMaskId(maxElement); 123 | assertTrue(store.isEmpty(node.match(-1L, mask))); 124 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 125 | assertEquals(with(with(store.newMask(), 0), 1), store.getMask(node.match(10L, mask))); 126 | } 127 | 128 | @ParameterizedTest 129 | @MethodSource("stores") 130 | public > void testEqualRev(int maxElement, MaskStore store) { 131 | var node = buildRev(store, maxElement, Operation.EQ); 132 | int mask = store.newContiguousMaskId(maxElement); 133 | assertTrue(store.isEmpty(node.match(-1L, mask))); 134 | assertEquals(with(store.newMask(), 0), store.getMask(node.match(0L, mask))); 135 | assertEquals(with(store.newMask(), 1), store.getMask(node.match(10L, mask))); 136 | } 137 | 138 | 139 | @ParameterizedTest 140 | @MethodSource("stores") 141 | public > void testLessThanOrEqualRev(int maxElement, MaskStore store) { 142 | var node = buildRev(store, maxElement, Operation.LE); 143 | int mask = store.newContiguousMaskId(maxElement); 144 | assertTrue(store.isEmpty(node.match(1001L, mask))); 145 | assertEquals(store.getMask(mask), store.getMask(node.match(0L, mask))); 146 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(10L, mask))); 147 | } 148 | 149 | @ParameterizedTest 150 | @MethodSource("stores") 151 | public > void testLessThanRev(int maxElement, MaskStore store) { 152 | var node = buildRev(store, maxElement, Operation.LT); 153 | int mask = store.newContiguousMaskId(maxElement); 154 | assertTrue(store.isEmpty(node.match(1001L, mask))); 155 | assertEquals(without(store.getMask(mask).clone(), 0), store.getMask(node.match(0L, mask))); 156 | assertEquals(without(without(store.getMask(mask).clone(), 0), 1), store.getMask(node.match(10L, mask))); 157 | } 158 | 159 | 160 | private > 161 | LongNode build(MaskStore store, int count, Operation relation) { 162 | var node = new LongNode<>(store, relation); 163 | for (int i = 0; i < count; ++i) { 164 | node.add(i * 10, i); 165 | } 166 | return node.optimise(); 167 | } 168 | 169 | private > 170 | LongNode buildRev(MaskStore store, int count, Operation relation) { 171 | var node = new LongNode<>(store, relation); 172 | for (int i = count - 1; i >= 0; --i) { 173 | node.add(i * 10, i); 174 | } 175 | return node.optimise(); 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /multi-matcher-core/src/test/java/io/github/richardstartin/multimatcher/core/matchers/StringMutableMatcherTest.java: -------------------------------------------------------------------------------- 1 | package io.github.richardstartin.multimatcher.core.matchers; 2 | 3 | import io.github.richardstartin.multimatcher.core.masks.MaskStore; 4 | import io.github.richardstartin.multimatcher.core.masks.WordMask; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.parallel.Execution; 7 | import org.junit.jupiter.api.parallel.ExecutionMode; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | 12 | import java.util.function.Function; 13 | import java.util.stream.IntStream; 14 | import java.util.stream.Stream; 15 | 16 | import static io.github.richardstartin.multimatcher.core.Constraint.equalTo; 17 | import static io.github.richardstartin.multimatcher.core.Constraint.startsWith; 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | 20 | @Execution(ExecutionMode.CONCURRENT) 21 | public class StringMutableMatcherTest { 22 | 23 | 24 | public static Stream stores() { 25 | return IntStream.of(32, 64) 26 | .mapToObj(i -> Arguments.of(i - 1, WordMask.store(i))); 27 | } 28 | 29 | @ParameterizedTest 30 | @MethodSource("stores") 31 | public void test1(int maxElement, MaskStore store) { 32 | StringConstraintAccumulator matcher = new StringConstraintAccumulator<>(Function.identity(), store, 4); 33 | matcher.addConstraint(equalTo("foo"), 0); 34 | matcher.addConstraint(equalTo("bar"), 1); 35 | matcher.addConstraint(startsWith("foo"), 2); 36 | matcher.addConstraint(startsWith("f"), 3); 37 | var mask = store.contiguous(maxElement); 38 | matcher.toMatcher().match("foo", mask); 39 | assertEquals(store.of(0, 2, 3), mask); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /multi-matcher-core/src/test/resources/invalid.yaml: -------------------------------------------------------------------------------- 1 | hdsakjdh -------------------------------------------------------------------------------- /multi-matcher-core/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.execution.parallel.enabled=true -------------------------------------------------------------------------------- /multi-matcher-core/src/test/resources/test.yaml: -------------------------------------------------------------------------------- 1 | - id: "rule1" 2 | constraints: 3 | field1: 4 | operation: EQ 5 | value: "foo" 6 | measure1: 7 | operation: GT 8 | value: 0 9 | priority: 0 10 | classification: "RED" 11 | - id: "rule2" 12 | constraints: 13 | field1: 14 | operation: EQ 15 | value: "bar" 16 | measure1: 17 | operation: GT 18 | value: 1 19 | priority: 1 20 | classification: "BLUE" -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include( 2 | "multi-matcher-core", 3 | "benchmarks" 4 | ) 5 | --------------------------------------------------------------------------------