Skip to content

Commit

Permalink
Date arithmetic (#136)
Browse files Browse the repository at this point in the history
* initial date arithmetic support

#91

* code generation and interpreter date arithmetic

plus tests

* remove obsolete comment about operator selection

* prevent adding two dates at parse time

attempting to add (not subtract) two dates raises an InvalidOperation parse error because there aren't any sane semantics for doing so
while subtraction gives the duration between the dates, there's hardly any good reason to add them

* add individual function tests

add test for subtracting 10000 years
  • Loading branch information
kroepke authored and bernd committed Nov 24, 2016
1 parent 1bfcda4 commit 255a919
Show file tree
Hide file tree
Showing 20 changed files with 693 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import org.antlr.v4.runtime.Token;
import org.graylog.plugins.pipelineprocessor.EvaluationContext;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Period;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -53,6 +56,43 @@ public Object evaluateUnsafe(EvaluationContext context) {
final Object leftValue = left.evaluateUnsafe(context);
final Object rightValue = right.evaluateUnsafe(context);

// special case for date arithmetic
final boolean leftDate = DateTime.class.equals(leftValue.getClass());
final boolean leftPeriod = Period.class.equals(leftValue.getClass());
final boolean rightDate = DateTime.class.equals(rightValue.getClass());
final boolean rightPeriod = Period.class.equals(rightValue.getClass());

if (leftDate && rightPeriod) {
final DateTime date = (DateTime) leftValue;
final Period period = (Period) rightValue;

return isPlus() ? date.plus(period) : date.minus(period);
} else if (leftPeriod && rightDate) {
final DateTime date = (DateTime) rightValue;
final Period period = (Period) leftValue;

return isPlus() ? date.plus(period) : date.minus(period);
} else if (leftPeriod && rightPeriod) {
final Period period1 = (Period) leftValue;
final Period period2 = (Period) rightValue;

return isPlus() ? period1.plus(period2) : period1.minus(period2);
} else if (leftDate && rightDate) {
// the most uncommon, this is only defined for - really and means "interval between them"
// because adding two dates makes no sense
if (isPlus()) {
// makes no sense to compute and should be handles in the parser already
return null;
}
final DateTime left = (DateTime) leftValue;
final DateTime right = (DateTime) rightValue;

if (left.isBefore(right)) {
return new Duration(left, right);
} else {
return new Duration(right, left);
}
}
if (isIntegral()) {
final long l = (long) leftValue;
final long r = (long) rightValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
import org.graylog.plugins.pipelineprocessor.ast.functions.FunctionDescriptor;
import org.graylog.plugins.pipelineprocessor.ast.statements.VarAssignStatement;
import org.graylog.plugins.pipelineprocessor.parser.FunctionRegistry;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Period;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -403,7 +406,6 @@ public void exitEquality(EqualityExpression expr) {
final CodeBlock leftBlock = codeSnippet.get(expr.left());
final CodeBlock rightBlock = codeSnippet.get(expr.right());

// TODO optimize operator selection, Objects.equals isn't the ideal candidate because of auto-boxing
final Class leftType = expr.left().getType();
final Class rightType = expr.right().getType();
boolean useOperator = false;
Expand All @@ -419,6 +421,20 @@ public void exitEquality(EqualityExpression expr) {
blockOrMissing(leftBlock, expr.left()),
blockOrMissing(rightBlock, expr.right()));
} else {
// Dates
if (DateTime.class.equals(leftType)) {
if (DateTime.class.equals(rightType)) {
codeSnippet.putIfAbsent(expr, CodeBlock.of("$L.isEqual($L)", leftBlock, rightBlock));
return;
}
} else if (Period.class.equals(leftType)) {
if (Period.class.equals(rightType)) {
codeSnippet.putIfAbsent(expr,
CodeBlock.of("$L.toDuration().equals($L.toDuration())", leftBlock, rightBlock));
return;
}
}

statement += (checkEquality ? "" : "!") + "$T.equals($L, $L)";
currentMethod.addStatement(statement,
intermediateName,
Expand All @@ -434,7 +450,45 @@ public void exitEquality(EqualityExpression expr) {
public void exitComparison(ComparisonExpression expr) {
final CodeBlock left = codeSnippet.get(expr.left());
final CodeBlock right = codeSnippet.get(expr.right());
// TODO dates

final Class leftType = expr.left().getType();
final Class rightType = expr.right().getType();

if (DateTime.class.equals(leftType)) {
if (DateTime.class.equals(rightType)) {
CodeBlock block;
switch (expr.getOperator()) {
case ">":
block = CodeBlock.of("$L.isAfter($L)", left, right);
break;
case ">=":
block = CodeBlock.of("!$L.isBefore($L)", left, right);
break;
case "<":
block = CodeBlock.of("$L.isBefore($L)", left, right);
break;
case "<=":
block = CodeBlock.of("!$L.isAfter($L)", left, right);
break;
default:
block = null;
}
if (block != null) {
codeSnippet.putIfAbsent(expr, block);
return;
}
}
} else if (Period.class.equals(leftType)) {
if (Period.class.equals(rightType)) {
codeSnippet.putIfAbsent(expr,
CodeBlock.of("($L.toDuration().getMillis() " + expr.getOperator() + " $L.toDuration().getMillis())",
blockOrMissing(left, expr.left()),
blockOrMissing(right, expr.right())));
return;

}
}

codeSnippet.putIfAbsent(expr, CodeBlock.of("($L " + expr.getOperator() + " $L)",
blockOrMissing(left, expr.left()),
blockOrMissing(right, expr.right())));
Expand Down Expand Up @@ -559,6 +613,36 @@ public void exitAddition(AdditionExpression expr) {
final Object leftBlock = blockOrMissing(codeSnippet.get(expr.left()), expr.left());
final Object rightBlock = blockOrMissing(codeSnippet.get(expr.right()), expr.right());

Class leftType = expr.left().getType();
Class rightType = expr.right().getType();

if (DateTime.class.equals(leftType)) {
if (DateTime.class.equals(rightType)) {
// calculate duration between two dates (adding two dates is invalid)
if (expr.isPlus()) {
throw new IllegalStateException("Cannot add two dates, this is a parser bug");
}
codeSnippet.putIfAbsent(expr, CodeBlock.of(
"new $T($L, $L)", Duration.class, leftBlock, rightBlock));
} else if (Period.class.equals(rightType)) {
// new datetime
codeSnippet.putIfAbsent(expr,
CodeBlock.of("$L." + (expr.isPlus() ? "plus" : "minus") + "($L)", leftBlock, rightBlock));
}
return;
} else if (Period.class.equals(leftType)) {
if (DateTime.class.equals(rightType)) {
// invert the arguments, adding the period to the date, yielding a new DateTime
codeSnippet.putIfAbsent(expr,
CodeBlock.of("$L." + (expr.isPlus() ? "plus" : "minus") + "($L)", rightBlock, leftBlock));
} else if (Period.class.equals(rightType)) {
// adding two periods yields a new period
codeSnippet.putIfAbsent(expr,
CodeBlock.of("$L." + (expr.isPlus() ? "plus" : "minus") + "($L)", leftBlock, rightBlock));
}
return;
}

codeSnippet.putIfAbsent(expr,
CodeBlock.of("$L " + (expr.isPlus() ? "+" : "-") + " $L", leftBlock, rightBlock));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.inject.Binder;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;

import org.graylog.plugins.pipelineprocessor.ast.functions.Function;
import org.graylog.plugins.pipelineprocessor.functions.conversion.BooleanConversion;
import org.graylog.plugins.pipelineprocessor.functions.conversion.DoubleConversion;
Expand All @@ -28,6 +29,15 @@
import org.graylog.plugins.pipelineprocessor.functions.dates.FormatDate;
import org.graylog.plugins.pipelineprocessor.functions.dates.Now;
import org.graylog.plugins.pipelineprocessor.functions.dates.ParseDate;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Days;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Hours;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Millis;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Minutes;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Months;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.PeriodParseFunction;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Seconds;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Weeks;
import org.graylog.plugins.pipelineprocessor.functions.dates.periods.Years;
import org.graylog.plugins.pipelineprocessor.functions.hashing.CRC32;
import org.graylog.plugins.pipelineprocessor.functions.hashing.CRC32C;
import org.graylog.plugins.pipelineprocessor.functions.hashing.MD5;
Expand Down Expand Up @@ -115,6 +125,15 @@ protected void configure() {
addMessageProcessorFunction(ParseDate.NAME, ParseDate.class);
addMessageProcessorFunction(FlexParseDate.NAME, FlexParseDate.class);
addMessageProcessorFunction(FormatDate.NAME, FormatDate.class);
addMessageProcessorFunction(Years.NAME, Years.class);
addMessageProcessorFunction(Months.NAME, Months.class);
addMessageProcessorFunction(Weeks.NAME, Weeks.class);
addMessageProcessorFunction(Days.NAME, Days.class);
addMessageProcessorFunction(Hours.NAME, Hours.class);
addMessageProcessorFunction(Minutes.NAME, Minutes.class);
addMessageProcessorFunction(Seconds.NAME, Seconds.class);
addMessageProcessorFunction(Millis.NAME, Millis.class);
addMessageProcessorFunction(PeriodParseFunction.NAME, PeriodParseFunction.class);

// hash digest
addMessageProcessorFunction(CRC32.NAME, CRC32.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.graylog.plugins.pipelineprocessor.functions.dates.periods;

import com.google.common.primitives.Ints;

import org.graylog.plugins.pipelineprocessor.EvaluationContext;
import org.graylog.plugins.pipelineprocessor.ast.functions.AbstractFunction;
import org.graylog.plugins.pipelineprocessor.ast.functions.FunctionArgs;
import org.graylog.plugins.pipelineprocessor.ast.functions.FunctionDescriptor;
import org.graylog.plugins.pipelineprocessor.ast.functions.ParameterDescriptor;
import org.joda.time.Period;

import javax.annotation.Nonnull;

public abstract class AbstractPeriodComponentFunction extends AbstractFunction<Period> {

private final ParameterDescriptor<Long, Period> value =
ParameterDescriptor
.integer("value", Period.class)
.transform(this::getPeriodOfInt)
.build();

private Period getPeriodOfInt(long period) {
return getPeriod(Ints.saturatedCast(period));
}

@Nonnull
protected abstract Period getPeriod(int period);

@Override
public Period evaluate(FunctionArgs args, EvaluationContext context) {
return value.required(args, context);
}

@Override
public FunctionDescriptor<Period> descriptor() {
return FunctionDescriptor.<Period>builder()
.name(getName())
.description(getDescription())
.pure(true)
.returnType(Period.class)
.params(value)
.build();
}

@Nonnull
protected abstract String getName();

@Nonnull
protected abstract String getDescription();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.graylog.plugins.pipelineprocessor.functions.dates.periods;

import org.joda.time.Period;

import javax.annotation.Nonnull;

public class Days extends AbstractPeriodComponentFunction {

public static final String NAME = "days";

@Nonnull
@Override
protected Period getPeriod(int period) {
return Period.days(period);
}

@Nonnull
@Override
protected String getName() {
return NAME;
}

@Nonnull
@Override
protected String getDescription() {
return "Create a period with a specified number of days.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.graylog.plugins.pipelineprocessor.functions.dates.periods;

import org.joda.time.Period;

import javax.annotation.Nonnull;

public class Hours extends AbstractPeriodComponentFunction {

public static final String NAME = "hours";

@Nonnull
@Override
protected Period getPeriod(int period) {
return Period.hours(period);
}

@Nonnull
@Override
protected String getName() {
return NAME;
}

@Nonnull
@Override
protected String getDescription() {
return "Create a period with a specified number of hours.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.graylog.plugins.pipelineprocessor.functions.dates.periods;

import org.joda.time.Period;

import javax.annotation.Nonnull;

public class Millis extends AbstractPeriodComponentFunction {

public static final String NAME = "millis";

@Nonnull
@Override
protected Period getPeriod(int period) {
return Period.millis(period);
}

@Nonnull
@Override
protected String getName() {
return NAME;
}

@Nonnull
@Override
protected String getDescription() {
return "Create a period with a specified number of millis.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.graylog.plugins.pipelineprocessor.functions.dates.periods;

import org.joda.time.Period;

import javax.annotation.Nonnull;

public class Minutes extends AbstractPeriodComponentFunction {

public static final String NAME = "minutes";

@Nonnull
@Override
protected Period getPeriod(int period) {
return Period.minutes(period);
}

@Nonnull
@Override
protected String getName() {
return NAME;
}

@Nonnull
@Override
protected String getDescription() {
return "Create a period with a specified number of minutes.";
}
}
Loading

0 comments on commit 255a919

Please sign in to comment.