Update range function to follow Python's range function
This commit is contained in:
parent
e2793daf54
commit
35c991d9c2
@ -4,7 +4,6 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONWriter;
|
||||
|
||||
@ -16,32 +15,31 @@ import com.google.refine.grel.Function;
|
||||
* Implements the logic behind the range function.
|
||||
*
|
||||
* The range function can take in a single string of the form 'a, b, c' or three
|
||||
* integers a, b, c where a and b represents the first and last numbers in the range respectively.
|
||||
*
|
||||
* If b is not given, a defaults to the range end and 0 becomes the range start.
|
||||
* c is optional and represents the step (increment) for the generated sequence.
|
||||
* integers a, b, c where a and b represents the first (inclusive) and last (exclusive)
|
||||
* numbers in the range respectively. If b is not given, a defaults to the range end
|
||||
* and 0 becomes the range start. c is optional and represents the step (increment)
|
||||
* for the generated sequence.
|
||||
*/
|
||||
public class Range implements Function {
|
||||
|
||||
private static final int STRING_ARG_LENGTH = 1;
|
||||
|
||||
private static final String SEPARATOR = ",";
|
||||
|
||||
private static final int INTEGER_ARGS_LENGTH = 2;
|
||||
private static final int INTEGER_ARGS_WITH_STEP = 3;
|
||||
|
||||
private static final String DEFAULT_RANGE_START = "0";
|
||||
|
||||
|
||||
private static final String lastCharacterCommaRegex = ",$";
|
||||
private static final Pattern lastCharacterCommaPattern = Pattern.compile(lastCharacterCommaRegex);
|
||||
|
||||
private static final int DEFAULT_START = 0;
|
||||
private static final int DEFAULT_STEP = 1;
|
||||
|
||||
private static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
|
||||
@Override
|
||||
public Object call(Properties bindings, Object[] args) {
|
||||
|
||||
if (args.length == STRING_ARG_LENGTH) {
|
||||
if (args.length == 1) {
|
||||
return createRangeWithOneGivenArgument(args);
|
||||
} else if (args.length == INTEGER_ARGS_LENGTH) {
|
||||
} else if (args.length == 2) {
|
||||
return createRangeWithTwoGivenArguments(args);
|
||||
} else if (args.length == INTEGER_ARGS_WITH_STEP) {
|
||||
} else if (args.length == 3) {
|
||||
return createRangeWithThreeGivenArguments(args);
|
||||
}
|
||||
|
||||
@ -49,175 +47,257 @@ public class Range implements Function {
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a given string has a comma as the last character.
|
||||
*
|
||||
* This is primarily used against abusing the range function like doing range("1,").
|
||||
* This is primarily used to detect edge cases like doing range("1,").
|
||||
*/
|
||||
private boolean hasCommaAsLastCharacter(String test) {
|
||||
Matcher lastCharacterCommaMatcher = lastCharacterCommaPattern.matcher(test);
|
||||
return lastCharacterCommaMatcher.find();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the single argument given to determine if the argument is a valid string, a
|
||||
* valid integer, or an invalid argument.
|
||||
* Processes the single argument given to determine if the argument is (i) a valid string, (ii) a
|
||||
* valid integer, or (iii) an invalid argument.
|
||||
*
|
||||
* If the argument is a valid string, it can either be in the form 'a', or 'a, b' or 'a, b, c'
|
||||
* where a and b are the start and end of the range respectively, and c is the optional
|
||||
* step argument. In the case where 'a' is the only argument, 'a' becomes the range end and
|
||||
* 0 becomes the default range start.
|
||||
* step argument. In the case where 'a' is the only argument, 'a' becomes the range end (exclusive)
|
||||
* and 0 becomes the default range start.
|
||||
*
|
||||
* If the argument is a valid integer, it can will default to become the range end, and 0 defaults
|
||||
* to become the range start.
|
||||
*
|
||||
* In all other cases, the argument is considered invalid.
|
||||
*/
|
||||
private Object createRangeWithOneGivenArgument(Object[] args) {
|
||||
Object range = args[0];
|
||||
|
||||
int rangeStart = DEFAULT_START;
|
||||
int rangeEnd = 0;
|
||||
int rangeStep = DEFAULT_STEP;
|
||||
|
||||
// Check for valid string argument(s)
|
||||
if (range != null && range instanceof String) {
|
||||
String rangeString = ((String) range).trim();
|
||||
String[] rangeValues = rangeString.split(SEPARATOR);
|
||||
|
||||
if (rangeValues.length == 1 && StringUtils.isNumeric(rangeValues[0])
|
||||
&& !hasCommaAsLastCharacter(rangeString)) {
|
||||
String rangeEnd = rangeValues[0].trim();
|
||||
|
||||
return createRange(DEFAULT_RANGE_START, rangeEnd);
|
||||
} else if (rangeValues.length == INTEGER_ARGS_LENGTH) {
|
||||
String rangeStart = rangeValues[0].trim();
|
||||
String rangeEnd = rangeValues[1].trim();
|
||||
if (hasCommaAsLastCharacter(rangeString)) {
|
||||
return new EvalError("the last character in the input string should not be a comma");
|
||||
}
|
||||
|
||||
if (StringUtils.isNumeric(rangeStart) && StringUtils.isNumeric(rangeEnd)) {
|
||||
return createRange(rangeStart, rangeEnd);
|
||||
}
|
||||
} else if (rangeValues.length == INTEGER_ARGS_WITH_STEP) {
|
||||
String rangeStart = rangeValues[0].trim();
|
||||
String rangeEnd = rangeValues[1].trim();
|
||||
String rangeStep = rangeValues[2].trim();
|
||||
|
||||
if (StringUtils.isNumeric(rangeStart) && StringUtils.isNumeric(rangeEnd)
|
||||
&& StringUtils.isNumeric(rangeStep)) {
|
||||
try {
|
||||
if (rangeValues.length == 1) {
|
||||
rangeEnd = Integer.parseInt(rangeValues[0].trim());
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
} else if (rangeValues.length == 2) {
|
||||
rangeStart = Integer.parseInt(rangeValues[0].trim());
|
||||
rangeEnd = Integer.parseInt(rangeValues[1].trim());
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
} else if (rangeValues.length == 3) {
|
||||
rangeStart = Integer.parseInt(rangeValues[0].trim());
|
||||
rangeEnd = Integer.parseInt(rangeValues[1].trim());
|
||||
rangeStep = Integer.parseInt(rangeValues[2].trim());
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
}
|
||||
} catch (NumberFormatException nfe) {
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
}
|
||||
|
||||
if (range != null && StringUtils.isNumeric(range.toString())) {
|
||||
String rangeEnd = range.toString();
|
||||
|
||||
return createRange(DEFAULT_RANGE_START, rangeEnd);
|
||||
// Check for valid negative integer argument
|
||||
if (range != null && range instanceof Double && (Double) range % 1 == 0) {
|
||||
range = ((Double) range).intValue();
|
||||
return createRange(DEFAULT_START, rangeEnd, DEFAULT_STEP);
|
||||
}
|
||||
|
||||
|
||||
// Check for valid positive integer argument
|
||||
if (range != null) {
|
||||
try {
|
||||
rangeEnd = Integer.parseInt(String.valueOf(range));
|
||||
return createRange(DEFAULT_START, rangeEnd, DEFAULT_STEP);
|
||||
} catch (NumberFormatException nfe) {
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
}
|
||||
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the two arguments given to determine if the arguments are two valid integers or
|
||||
* a valid string and step or invalid arguments.
|
||||
* Processes the two arguments given to determine if the arguments are (i) two valid strings,
|
||||
* (ii) two valid integers or (iii) a valid string and an integer or (iv) invalid arguments.
|
||||
*
|
||||
* If the arguments are valid strings, the strings can either be such that (i) each string contains
|
||||
* single arguments (i.e. two arguments in total), or (ii) one string contains one argument and the other
|
||||
* string contains two argument (i.e. three arguments in total).
|
||||
*
|
||||
* If the arguments are a valid string and a valid integer, the string can be such that (i) the string
|
||||
* contains a single argument (i.e. two arguments in total) or (ii) the string contains two arguments
|
||||
* (i.e. three arguments in total).
|
||||
*
|
||||
* In all other cases, the arguments are considered invalid.
|
||||
*/
|
||||
private Object createRangeWithTwoGivenArguments(Object[] args) {
|
||||
String rangeStart = args[0].toString().trim();
|
||||
String rangeEnd = args[1].toString().trim();
|
||||
Object firstArg = args[0];
|
||||
Object secondArg = args[1];
|
||||
|
||||
String range = rangeStart;
|
||||
String rangeStep = rangeEnd;
|
||||
|
||||
boolean isTwoValidIntegers = false;
|
||||
boolean isValidStringWithStep = false;
|
||||
|
||||
String[] rangeValues = range.split(SEPARATOR);
|
||||
|
||||
if (rangeValues.length == 1) {
|
||||
isTwoValidIntegers = true;
|
||||
} else if (rangeValues.length == 2) {
|
||||
isValidStringWithStep = true;
|
||||
int rangeStart = DEFAULT_START;
|
||||
int rangeEnd = 0;
|
||||
int rangeStep = DEFAULT_STEP;
|
||||
|
||||
if (firstArg == null || secondArg == null) {
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
if (isTwoValidIntegers && StringUtils.isNumeric(rangeStart) &&
|
||||
StringUtils.isNumeric(rangeEnd)) {
|
||||
return createRange(rangeStart, rangeEnd);
|
||||
} else if (isValidStringWithStep) {
|
||||
rangeStart = rangeValues[0].trim();
|
||||
rangeEnd = rangeValues[1].trim();
|
||||
|
||||
if (StringUtils.isNumeric(rangeStart) && StringUtils.isNumeric(rangeEnd)
|
||||
&& StringUtils.isNumeric(rangeStep)) {
|
||||
|
||||
boolean hasString = false;
|
||||
boolean hasTwoIntegers = true;
|
||||
|
||||
if (firstArg instanceof String || secondArg instanceof String) {
|
||||
hasString = true;
|
||||
hasTwoIntegers = false;
|
||||
}
|
||||
|
||||
boolean hasTwoArguments = hasTwoIntegers;
|
||||
boolean hasThreeArguments = false;
|
||||
|
||||
// Deal with valid negative integers
|
||||
if (firstArg instanceof Double && (Double) firstArg % 1 == 0) {
|
||||
firstArg = ((Double) firstArg).intValue();
|
||||
}
|
||||
|
||||
if (secondArg instanceof Double && (Double) secondArg % 1 == 0) {
|
||||
secondArg = ((Double) secondArg).intValue();
|
||||
}
|
||||
|
||||
String firstArgStringified = String.valueOf(firstArg).trim();
|
||||
String secondArgStringified = String.valueOf(secondArg).trim();
|
||||
String thirdArgStringified = "";
|
||||
|
||||
if (hasCommaAsLastCharacter(firstArgStringified) || hasCommaAsLastCharacter(secondArgStringified)) {
|
||||
return new EvalError("the last character in the input string should not be a comma");
|
||||
}
|
||||
|
||||
// Check if the strings are valid strings (e.g. range("1, 2", "3, 4") should fail but
|
||||
// range("1, 2", "1") should pass)
|
||||
if (hasString) {
|
||||
String[] firstArgArray = firstArgStringified.split(SEPARATOR);
|
||||
String[] secondArgArray = secondArgStringified.split(SEPARATOR);
|
||||
|
||||
int combinedArrayLength = firstArgArray.length + secondArgArray.length;
|
||||
|
||||
if (combinedArrayLength == 3) {
|
||||
hasThreeArguments = true;
|
||||
|
||||
if (firstArgArray.length == 1) {
|
||||
secondArgStringified = secondArgArray[0].trim();
|
||||
thirdArgStringified = secondArgArray[1].trim();
|
||||
} else {
|
||||
firstArgStringified = firstArgArray[0].trim();
|
||||
secondArgStringified = firstArgArray[1].trim();
|
||||
thirdArgStringified = secondArgArray[0].trim();
|
||||
}
|
||||
|
||||
} else if (combinedArrayLength == 2) {
|
||||
hasTwoArguments = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (hasTwoArguments) {
|
||||
rangeStart = Integer.parseInt(firstArgStringified);
|
||||
rangeEnd = Integer.parseInt(secondArgStringified);
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
}
|
||||
} else if (hasThreeArguments) {
|
||||
rangeStart = Integer.parseInt(firstArgStringified);
|
||||
rangeEnd = Integer.parseInt(secondArgStringified);
|
||||
rangeStep = Integer.parseInt(thirdArgStringified);
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
}
|
||||
} catch (NumberFormatException nfe) {
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the three arguments given to determine if the arguments are three valid integers or
|
||||
* invalid arguments.
|
||||
* Processes the three arguments given to determine if the arguments are (i) three valid strings,
|
||||
* (ii) three valid integers, (iii) two valid strings and a valid integer, (iv) a valid string and
|
||||
* two valid integers or (v) invalid arguments.
|
||||
*
|
||||
* In this case, all valid strings can only contain a single argument.
|
||||
*/
|
||||
private Object createRangeWithThreeGivenArguments(Object[] args) {
|
||||
String rangeStart = args[0].toString().trim();
|
||||
String rangeEnd = args[1].toString().trim();
|
||||
String rangeStep = args[2].toString().trim();
|
||||
if (StringUtils.isNumeric(rangeStart) && StringUtils.isNumeric(rangeEnd)
|
||||
&& StringUtils.isNumeric(rangeStep)) {
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
Object firstArg = args[0];
|
||||
Object secondArg = args[1];
|
||||
Object thirdArg = args[2];
|
||||
|
||||
// Deal with negative integers first
|
||||
if (firstArg != null && firstArg instanceof Double && (Double) firstArg % 1 == 0) {
|
||||
firstArg = ((Double) firstArg).intValue();
|
||||
}
|
||||
|
||||
if (secondArg != null && secondArg instanceof Double && (Double) secondArg % 1 == 0) {
|
||||
secondArg = ((Double) secondArg).intValue();
|
||||
}
|
||||
|
||||
if (thirdArg != null && thirdArg instanceof Double && (Double) thirdArg % 1 == 0) {
|
||||
thirdArg = ((Double) thirdArg).intValue();
|
||||
}
|
||||
|
||||
try {
|
||||
int rangeStart = Integer.parseInt(String.valueOf(firstArg).trim());
|
||||
int rangeEnd = Integer.parseInt(String.valueOf(secondArg).trim());
|
||||
int rangeStep = Integer.parseInt(String.valueOf(thirdArg).trim());
|
||||
return createRange(rangeStart, rangeEnd, rangeStep);
|
||||
} catch (NumberFormatException nfe) {
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
return new EvalError(ControlFunctionRegistry.getFunctionName(this)
|
||||
+ " expects a string of the form 'a, b, c' or integers a, b, c where a and b "
|
||||
+ "are the start and the end of the range respectively and c is the step (increment)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a range from the given range values.
|
||||
*
|
||||
* The generated range is either an increasing sequence or a decreasing sequence, and
|
||||
* each number in the sequence differs from the next number by one.
|
||||
*/
|
||||
private static Object createRange(String rangeStart, String rangeEnd) {
|
||||
return createRange(rangeStart, rangeEnd, "1");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a range from the given range values with the given step.
|
||||
*
|
||||
* The generated range is either an increasing sequence or a decreasing sequence, and
|
||||
* each number in the sequence differs from the next number by the step value.
|
||||
* each number in the sequence differs from the next number by the step value.
|
||||
*/
|
||||
private static Object createRange(String rangeStart, String rangeEnd, String rangeStep) {
|
||||
int start = Integer.parseInt(rangeStart);
|
||||
int end = Integer.parseInt(rangeEnd);
|
||||
int step = Integer.parseInt(rangeStep);
|
||||
int negativeStep = -step;
|
||||
int rangeSize = 0;
|
||||
|
||||
if (step != 0) {
|
||||
rangeSize = (int) (Math.ceil((double) (Math.abs(start - end) + 1)/ step));
|
||||
private static Object createRange(int start, int stop, int step) {
|
||||
if ((start > stop && step > 0) || (start < stop && step < 0) || step == 0) {
|
||||
return EMPTY_STRING_ARRAY;
|
||||
}
|
||||
|
||||
|
||||
int rangeSize = (int) (Math.ceil(((double) Math.abs(start - stop))/ Math.abs(step)));
|
||||
|
||||
String[] generatedRange = new String[rangeSize];
|
||||
|
||||
if (start < end) {
|
||||
for (int i = 0; i < rangeSize; i++) {
|
||||
generatedRange[i] = Integer.toString(start + step * i);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < rangeSize; i++) {
|
||||
generatedRange[i] = Integer.toString(start + negativeStep * i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < rangeSize; i++) {
|
||||
generatedRange[i] = Integer.toString(start + step * i);
|
||||
}
|
||||
|
||||
|
||||
return generatedRange;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void write(JSONWriter writer, Properties options)
|
||||
throws JSONException {
|
||||
|
||||
|
||||
writer.object();
|
||||
writer.key("description"); writer.value(
|
||||
"Returns an array where a and b are the start and the end of the range respectively and c is the step (increment).");
|
||||
|
Loading…
Reference in New Issue
Block a user