Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions common/src/main/java/io/druid/math/expr/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -990,26 +990,6 @@ public ExprEval apply(List<Expr> args, Expr.ObjectBinding bindings)
}
}

class TrimFunc implements Function
{
@Override
public String name()
{
return "trim";
}

@Override
public ExprEval apply(List<Expr> args, Expr.ObjectBinding bindings)
{
if (args.size() != 1) {
throw new IAE("Function[%s] needs 1 argument", name());
}

final String arg = args.get(0).eval(bindings).asString();
return ExprEval.of(Strings.nullToEmpty(arg).trim());
}
}

class LowerFunc implements Function
{
@Override
Expand Down
6 changes: 0 additions & 6 deletions common/src/test/java/io/druid/math/expr/FunctionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,6 @@ public void testStrlen()
assertExpr("strlen(nonexistent)", 0L);
}

@Test
public void testTrim()
{
assertExpr("trim(concat(' ',x,' '))", "foo");
}

@Test
public void testLower()
{
Expand Down
10 changes: 6 additions & 4 deletions docs/content/misc/math-expr.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ Also, the following built-in functions are supported.
|regexp_extract|regexp_extract(expr, pattern[, index]) applies a regular expression pattern and extracts a capture group index, or null if there is no match. If index is unspecified or zero, returns the substring that matched the pattern.|
|replace|replace(expr, pattern, replacement) replaces pattern with replacement|
|substring|substring(expr, index, length) behaves like java.lang.String's substring|
|strlen|returns length of a string in UTF-16 code units|
|trim|remove leading and trailing whitespace from a string|
|lower|convert a string to lowercase|
|upper|convert a string to uppercase|
|strlen|strlen(expr) returns length of a string in UTF-16 code units|
|trim|trim(expr[, chars]) remove leading and trailing characters from `expr` if they are present in `chars`. `chars` defaults to ' ' (space) if not provided.|
|ltrim|ltrim(expr[, chars]) remove leading characters from `expr` if they are present in `chars`. `chars` defaults to ' ' (space) if not provided.|
|rtrim|rtrim(expr[, chars]) remove trailing characters from `expr` if they are present in `chars`. `chars` defaults to ' ' (space) if not provided.|
|lower|lower(expr) converts a string to lowercase|
|upper|upper(expr) converts a string to uppercase|

## Time functions

Expand Down
5 changes: 4 additions & 1 deletion docs/content/querying/sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ String functions accept strings, and return a type appropriate to the function.
|`REGEXP_EXTRACT(expr, pattern, [index])`|Apply regular expression pattern and extract a capture group, or null if there is no match. If index is unspecified or zero, returns the substring that matched the pattern.|
|`REPLACE(expr, pattern, replacement)`|Replaces pattern with replacement in expr, and returns the result.|
|`SUBSTRING(expr, index, [length])`|Returns a substring of expr starting at index, with a max length, both measured in UTF-16 code units.|
|`TRIM(expr)`|Returns expr with leading and trailing whitespace removed.|
|`TRIM([BOTH | LEADING | TRAILING] [<chars> FROM] expr)`|Returns expr with characters removed from the leading, trailing, or both ends of "expr" if they are in "chars". If "chars" is not provided, it defaults to " " (a space). If the directional argument is not provided, it defaults to "BOTH".|
|`BTRIM(expr[, chars])`|Alternate form of `TRIM(BOTH <chars> FROM <expr>`).|
|`LTRIM(expr[, chars])`|Alternate form of `TRIM(LEADING <chars> FROM <expr>`).|
|`RTRIM(expr[, chars])`|Alternate form of `TRIM(TRAILING <chars> FROM <expr>`).|
|`UPPER(expr)`|Returns expr in all uppercase.|

### Time functions
Expand Down
276 changes: 276 additions & 0 deletions processing/src/main/java/io/druid/query/expression/TrimExprMacro.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package io.druid.query.expression;

import io.druid.java.util.common.IAE;
import io.druid.math.expr.Expr;
import io.druid.math.expr.ExprEval;
import io.druid.math.expr.ExprMacroTable;

import javax.annotation.Nonnull;
import java.util.List;

public abstract class TrimExprMacro implements ExprMacroTable.ExprMacro
{
private static final char[] EMPTY_CHARS = new char[0];
private static final char[] DEFAULT_CHARS = new char[]{' '};

enum TrimMode
{
BOTH(true, true),
LEFT(true, false),
RIGHT(false, true);

private final boolean left;
private final boolean right;

TrimMode(final boolean left, final boolean right)
{
this.left = left;
this.right = right;
}

public boolean isLeft()
{
return left;
}

public boolean isRight()
{
return right;
}
}

private final TrimMode mode;
private final String name;

public TrimExprMacro(final String name, final TrimMode mode)
{
this.name = name;
this.mode = mode;
}

@Override
public String name()
{
return name;
}

@Override
public Expr apply(final List<Expr> args)
{
if (args.size() < 1 || args.size() > 2) {
throw new IAE("Function[%s] must have 1 or 2 arguments", name());
}

if (args.size() == 1) {
return new TrimStaticCharsExpr(mode, args.get(0), DEFAULT_CHARS);
} else {
final Expr charsArg = args.get(1);
if (charsArg.isLiteral()) {
final String charsString = charsArg.eval(ExprUtils.nilBindings()).asString();
final char[] chars = charsString == null ? EMPTY_CHARS : charsString.toCharArray();
return new TrimStaticCharsExpr(mode, args.get(0), chars);
} else {
return new TrimDynamicCharsExpr(mode, args.get(0), args.get(1));
}
}
}

private static class TrimStaticCharsExpr implements Expr
{
private final TrimMode mode;
private final Expr stringExpr;
private final char[] chars;

public TrimStaticCharsExpr(final TrimMode mode, final Expr stringExpr, final char[] chars)
{
this.mode = mode;
this.stringExpr = stringExpr;
this.chars = chars;
}

@Nonnull
@Override
public ExprEval eval(final ObjectBinding bindings)
{
final ExprEval stringEval = stringExpr.eval(bindings);

if (chars.length == 0 || stringEval.isNull()) {
return stringEval;
}

final String s = stringEval.asString();

int start = 0;
int end = s.length();

if (mode.isLeft()) {
while (start < s.length()) {
if (arrayContains(chars, s.charAt(start))) {
start++;
} else {
break;
}
}
}

if (mode.isRight()) {
while (end > start) {
if (arrayContains(chars, s.charAt(end - 1))) {
end--;
} else {
break;
}
}
}

if (start == 0 && end == s.length()) {
return stringEval;
} else {
return ExprEval.of(s.substring(start, end));
}
}

@Override
public void visit(final Visitor visitor)
{
stringExpr.visit(visitor);
visitor.visit(this);
}
}

private static class TrimDynamicCharsExpr implements Expr
{
private final TrimMode mode;
private final Expr stringExpr;
private final Expr charsExpr;

public TrimDynamicCharsExpr(final TrimMode mode, final Expr stringExpr, final Expr charsExpr)
{
this.mode = mode;
this.stringExpr = stringExpr;
this.charsExpr = charsExpr;
}

@Nonnull
@Override
public ExprEval eval(final ObjectBinding bindings)
{
final ExprEval stringEval = stringExpr.eval(bindings);

if (stringEval.isNull()) {
return stringEval;
}

final ExprEval charsEval = charsExpr.eval(bindings);

if (charsEval.isNull()) {
return stringEval;
}

final String s = stringEval.asString();
final String chars = charsEval.asString();

int start = 0;
int end = s.length();

if (mode.isLeft()) {
while (start < s.length()) {
if (stringContains(chars, s.charAt(start))) {
start++;
} else {
break;
}
}
}

if (mode.isRight()) {
while (end > start) {
if (stringContains(chars, s.charAt(end - 1))) {
end--;
} else {
break;
}
}
}

if (start == 0 && end == s.length()) {
return stringEval;
} else {
return ExprEval.of(s.substring(start, end));
}
}

@Override
public void visit(final Visitor visitor)
{
stringExpr.visit(visitor);
charsExpr.visit(visitor);
visitor.visit(this);
}
}

private static boolean arrayContains(char[] array, char c)
{
for (final char arrayChar : array) {
if (arrayChar == c) {
return true;
}
}

return false;
}

private static boolean stringContains(String string, char c)
{
for (int i = 0; i < string.length(); i++) {
if (string.charAt(i) == c) {
return true;
}
}

return false;
}

public static class BothTrimExprMacro extends TrimExprMacro
{
public BothTrimExprMacro()
{
super("trim", TrimMode.BOTH);
}
}

public static class LeftTrimExprMacro extends TrimExprMacro
{
public LeftTrimExprMacro()
{
super("ltrim", TrimMode.LEFT);
}
}

public static class RightTrimExprMacro extends TrimExprMacro
{
public RightTrimExprMacro()
{
super("rtrim", TrimMode.RIGHT);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class ExprMacroTest
.put("y", 2)
.put("z", 3.1)
.put("CityOfAngels", "America/Los_Angeles")
.put("spacey", " hey there ")
.build()
);

Expand Down Expand Up @@ -135,6 +136,42 @@ public void testTimestampFormat()
assertExpr("timestamp_format(t,'yyyy-MM-dd HH:mm:ss','America/Los_Angeles')", "2000-02-02 20:05:06");
}

@Test
public void testTrim()
{
assertExpr("trim('')", null);
assertExpr("trim(concat(' ',x,' '))", "foo");
assertExpr("trim(spacey)", "hey there");
assertExpr("trim(spacey, '')", " hey there ");
assertExpr("trim(spacey, 'he ')", "y ther");
assertExpr("trim(spacey, spacey)", null);
assertExpr("trim(spacey, substring(spacey, 0, 4))", "y ther");
}

@Test
public void testLTrim()
{
assertExpr("ltrim('')", null);
assertExpr("ltrim(concat(' ',x,' '))", "foo ");
assertExpr("ltrim(spacey)", "hey there ");
assertExpr("ltrim(spacey, '')", " hey there ");
assertExpr("ltrim(spacey, 'he ')", "y there ");
assertExpr("ltrim(spacey, spacey)", null);
assertExpr("ltrim(spacey, substring(spacey, 0, 4))", "y there ");
}

@Test
public void testRTrim()
{
assertExpr("rtrim('')", null);
assertExpr("rtrim(concat(' ',x,' '))", " foo");
assertExpr("rtrim(spacey)", " hey there");
assertExpr("rtrim(spacey, '')", " hey there ");
assertExpr("rtrim(spacey, 'he ')", " hey ther");
assertExpr("rtrim(spacey, spacey)", null);
assertExpr("rtrim(spacey, substring(spacey, 0, 4))", " hey ther");
}

private void assertExpr(final String expression, final Object expectedResult)
{
final Expr expr = Parser.parse(expression, TestExprMacroTable.INSTANCE);
Expand Down
Loading