Skip to content

Commit

Permalink
Initial application of filter engine for filtering and classification
Browse files Browse the repository at this point in the history
Also fixes import of 0 payments, these are now correctly being ignored
  • Loading branch information
Bios-Marcel committed Jul 11, 2024
1 parent 78f2928 commit b83745e
Show file tree
Hide file tree
Showing 13 changed files with 412 additions and 41 deletions.
11 changes: 7 additions & 4 deletions src/main/antlr4/link/biosmarcel/baka/FilterLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ options {
caseInsensitive = true;
}

HAS: ('contains'|'has');
AND: 'and';
OR: 'or';
OPEN_PAR: '(';
Expand All @@ -13,7 +14,9 @@ LT_EQ: '<=';
GT: '>';
GT_EQ: '>=';
EQ: '=';
NOT_EQ1: '!=';
IDENTIFIER: [a-z_]+;
STRING_LITERAL: ["].+?["];
SPACES: [ \u000B\t\r\n] -> channel(HIDDEN);
NOT_EQ: '!=';
STRING: ["].+?["];
BOOLEAN: ('true'|'false');
NUMBER: [,.0-9]+;
WORD: ~[ \u000B\t\r\n]+;
SPACES: [ \u000B\t\r\n] -> channel(HIDDEN);
13 changes: 8 additions & 5 deletions src/main/antlr4/link/biosmarcel/baka/FilterParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ options {
}

query: expression? EOF;
expression: field operator=(LT | LT_EQ | GT | GT_EQ | EQ | NOT_EQ1) value #comparatorExpression
| expression operator=(AND | OR) expression #binaryExpression
| OPEN_PAR expression CLOSE_PAR #groupedExpression
expression: field operator=(LT | LT_EQ | GT | GT_EQ | EQ | NOT_EQ | HAS) value #comparatorExpression
| expression operator=(AND | OR) expression #binaryExpression
| OPEN_PAR expression CLOSE_PAR #groupedExpression
;

field: IDENTIFIER;
value: STRING_LITERAL
field: WORD;
value: STRING
| BOOLEAN
| NUMBER
| WORD
;
10 changes: 7 additions & 3 deletions src/main/java/link/biosmarcel/baka/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import javafx.stage.Stage;
import link.biosmarcel.baka.data.Data;
import link.biosmarcel.baka.view.AccountsView;
import link.biosmarcel.baka.view.ClassificationsView;
import link.biosmarcel.baka.view.EvaluationView;
import link.biosmarcel.baka.view.PaymentsView;
import org.eclipse.store.storage.embedded.configuration.types.EmbeddedStorageConfiguration;
Expand Down Expand Up @@ -49,19 +50,22 @@ public void start(Stage stage) {
TabPane tabs = new TabPane(
new PaymentsView(state),
new AccountsView(state),
new EvaluationView(state)
new EvaluationView(state),
new ClassificationsView(state)
);
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);

Scene scene = new Scene(tabs, 800, 600);
scene.getStylesheets().add(Objects.requireNonNull(Main.class.getResource("base.css")).toExternalForm());
stage.setTitle("Baka");
stage.getIcons().add(new Image(getClass().getResourceAsStream("icon.png")));
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("icon.png"))));
stage.setScene(scene);

// FIXME This seems dumb?
Platform.setImplicitExit(true);
stage.setOnCloseRequest((ae) -> {
stage.setOnCloseRequest(_ -> {
// Not shutting down might result in data loss
storageManager.shutdown();
Platform.exit();
System.exit(0);
});
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/link/biosmarcel/baka/data/ClassificationRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package link.biosmarcel.baka.data;

import link.biosmarcel.baka.view.PaymentFilter;
import org.jspecify.annotations.Nullable;

public class ClassificationRule {
public String name = "";
public String tag = "";
public String query = "";

public void setQuery(final String query) {
this.compiled = null;
this.query = query;
}

private transient @Nullable PaymentFilter compiled = null;

public boolean test(final Payment payment) {
if (compiled == null) {
compiled = new PaymentFilter();
if (!compiled.setQuery(query)) {
return false;
}
}

return compiled.test(payment);
}
}
4 changes: 3 additions & 1 deletion src/main/java/link/biosmarcel/baka/data/Data.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public class Data {

public List<Account> accounts = new ArrayList<>();

public List<ClassificationRule> classificationRules = new ArrayList<>();

/**
* Import payments will add new payments to the data, if the data doesn't already exist.
*
Expand Down Expand Up @@ -50,7 +52,7 @@ public Map<Payment, Payment> importPayments(final Collection<Payment> newPayment

final Map<Payment, Payment> possibleDuplicates = new HashMap<>();
OUTER_LOOP:
for (final var newPayment : newPayments) {
for (final var newPayment : filteredPayments) {
for (int i = payments.size() - 1; i >= 0; i--) {
final var existingPayment = payments.get(i);
if (newPayment.bookingDate.isBefore(existingPayment.bookingDate)) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/link/biosmarcel/baka/filter/FieldData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package link.biosmarcel.baka.filter;

import java.util.function.Function;

public class FieldData<FilterTarget> {
final FilterTargetPredicate<FilterTarget> operate;
final Function<String, Object> convertString;

public FieldData(final FilterTargetPredicate<FilterTarget> operate, final Function<String, Object> convertString) {
this.operate = operate;
this.convertString = convertString;
}
}
72 changes: 54 additions & 18 deletions src/main/java/link/biosmarcel/baka/filter/Filter.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import link.biosmarcel.baka.FilterParserBaseListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ErrorNodeImpl;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.jspecify.annotations.Nullable;
Expand All @@ -17,20 +18,32 @@
public class Filter<FilterTarget> implements Predicate<FilterTarget> {
private @Nullable Expression<FilterTarget> query;

final Map<String, Map<String /* FIXME Custom Compare Operator Type*/, Function<FilterTarget, Comparable<?>>>> fieldToOperatorToExtractor = new HashMap<>();
final Map<String, Map<Operator, FieldData<FilterTarget>>> fieldToOperatorToExtractor = new HashMap<>();

public void register(
final String field,
final String operator,
final Function<FilterTarget, Comparable<?>> extractor) {
final Operator operator,
final FilterTargetPredicate<FilterTarget> predicate,
final Function<String, Object> convertString) {
fieldToOperatorToExtractor
.computeIfAbsent(field, _ -> new HashMap<>(/* FIXME Fixed Operator Size*/))
.put(operator, extractor);
.put(operator, new FieldData<>(predicate, convertString));
}

public void setQuery(final String textQuery) {
public boolean setQuery(final String textQuery) {
try {
_setQuery(textQuery);
return true;
} catch (final RuntimeException exception) {
System.out.println("Failed to compile query: " + exception.getMessage());
return false;
}
}

private void _setQuery(final String textQuery) {
final var lexer = new FilterLexer(CharStreams.fromString(textQuery));
final var parser = new FilterParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
final var parsedQuery = parser.query();

ThreadLocal<Expression<FilterTarget>> root = new ThreadLocal<>();
Expand Down Expand Up @@ -58,6 +71,12 @@ private void insertPredicate(final Expression<FilterTarget> expression, final Pr

@Override
public void enterGroupedExpression(final FilterParser.GroupedExpressionContext ctx) {
for (final var child : ctx.children) {
if (child instanceof ErrorNodeImpl) {
throw new IllegalStateException("Query incomplete");
}
}

var targetContext = contextToExpression.get(ctx.parent);
final var expression = new Expression<FilterTarget>();
contextToExpression.put(ctx, expression);
Expand All @@ -66,6 +85,12 @@ public void enterGroupedExpression(final FilterParser.GroupedExpressionContext c

@Override
public void enterBinaryExpression(final FilterParser.BinaryExpressionContext ctx) {
for (final var child : ctx.children) {
if (child instanceof ErrorNodeImpl) {
throw new IllegalStateException("Query incomplete");
}
}

final var expression = new Expression<FilterTarget>();
if (ctx.operator.getText().equalsIgnoreCase("AND")) {
expression.binaryExpressionType = BinaryExpressionType.AND;
Expand All @@ -83,28 +108,39 @@ public void enterBinaryExpression(final FilterParser.BinaryExpressionContext ctx

@Override
public void enterComparatorExpression(final FilterParser.ComparatorExpressionContext ctx) {
final var targetContext = contextToExpression.get(ctx.parent);
for (final var child : ctx.children) {
if (child instanceof ErrorNodeImpl) {
throw new IllegalStateException("Query incomplete");
}
}

final var targetContext = contextToExpression.get(ctx.parent);
final var operatorToExtractor = fieldToOperatorToExtractor.get(ctx.field().getText());
if (operatorToExtractor == null) {
throw new IllegalStateException("Field not registered: " + ctx.field().getText());
}
final var extractor = operatorToExtractor.get(ctx.operator.getText());

final Operator operator = switch (ctx.operator.getText()) {
case "=" -> Operator.EQ;
case "!=" -> Operator.NOT_EQ;
case "contains", "has" -> Operator.HAS;
default -> throw new UnsupportedOperationException("Unknown operator: " + ctx.operator.getText());
};
final var extractor = operatorToExtractor.get(operator);
if (extractor == null) {
throw new IllegalStateException("Extractor not registered: " + ctx.field().getText() + "." + ctx.operator.getText());
}
final Predicate<FilterTarget> predicate = switch (ctx.operator.getText()) {
case "=" -> x -> {
Comparable<String> apply = (Comparable<String>) extractor.apply(x);
return apply.compareTo(ctx.value().getText().substring(1, ctx.value().getText().length() - 1)) == 0;
};
case "!=" -> x -> {
Comparable<String> apply = (Comparable<String>) extractor.apply(x);
return apply.compareTo(ctx.value().getText().substring(1, ctx.value().getText().length() - 1)) != 0;
};
default -> throw new UnsupportedOperationException("operator not yet support");
};

String unquoted;
final var value = ctx.value();
final var stringToken = value.STRING();
if (stringToken != null) {
unquoted = stringToken.getText().substring(1, stringToken.getText().length() - 1);
} else {
unquoted = value.getText();
}
final Object filterValue = extractor.convertString.apply(unquoted);
final Predicate<FilterTarget> predicate = x -> extractor.operate.test(x, filterValue);
insertPredicate(targetContext, predicate);
}
}, parsedQuery);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package link.biosmarcel.baka.filter;

@FunctionalInterface
public interface FilterTargetPredicate<FilterTarget> {
boolean test(FilterTarget target, Object value);
}
7 changes: 7 additions & 0 deletions src/main/java/link/biosmarcel/baka/filter/Operator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package link.biosmarcel.baka.filter;

public enum Operator {
EQ,
NOT_EQ,
HAS,
}
Loading

0 comments on commit b83745e

Please sign in to comment.