Skip to content
Open
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
206 changes: 120 additions & 86 deletions src/main/java/ch/njol/skript/expressions/ExprIndicesOfValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ch.njol.skript.lang.simplification.SimplifiedLiteral;
import ch.njol.skript.util.LiteralUtils;
import ch.njol.util.StringUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;

Expand All @@ -17,13 +18,15 @@
import ch.njol.skript.lang.util.SimpleExpression;
import ch.njol.util.Kleenean;

import java.lang.reflect.Array;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.IntFunction;

@Name("Indices of Value")
@Description({
"Get the first, last or all positions of a character (or text) in another text using "
+ "'positions of %text% in %text%'. Nothing is returned when the value does not occur in the text. "
+ "'positions of %texts% in %text%'. Nothing is returned when the value does not occur in the text. "
+ "Positions range from 1 to the <a href='#ExprIndicesOf'>length</a> of the text (inclusive).",
"",
"Using 'indices/positions of %objects% in %objects%', you can get the indices or positions of "
Expand Down Expand Up @@ -77,15 +80,15 @@ public class ExprIndicesOfValue extends SimpleExpression<Object> {

static {
Skript.registerExpression(ExprIndicesOfValue.class, Object.class, ExpressionType.COMBINED,
"[the] [1:first|2:last|3:all] (position[mult:s]|mult:indices|index[mult:es]) of [[the] value] %string% in %string%",
"[the] [1:first|2:last|3:all] position[mult:s] of [[the] value] %object% in %~objects%",
"[the] [1:first|2:last|3:all] (mult:indices|index[mult:es]) of [[the] value] %object% in %~objects%"
"[the] [1:first|2:last|3:all] (position[mult:s]|mult:indices|index[mult:es]) of [[the] value] %strings% in %string%",
"[the] [1:first|2:last|3:all] position[mult:s] of [[the] value] %objects% in %~objects%",
"[the] [1:first|2:last|3:all] (mult:indices|index[mult:es]) of [[the] value] %objects% in %~objects%"
);
}

private IndexType indexType;
private boolean position, string;
private Expression<?> value, objects;
private Expression<?> needle, haystack;

@Override
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
Expand All @@ -107,113 +110,144 @@ public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelaye

position = matchedPattern <= 1;
string = matchedPattern == 0;
value = LiteralUtils.defendExpression(exprs[0]);
objects = exprs[1];
needle = LiteralUtils.defendExpression(exprs[0]);
haystack = exprs[1];

return LiteralUtils.canInitSafely(value);
return LiteralUtils.canInitSafely(needle);
}

@Override
protected Object @Nullable [] get(Event event) {
Object value = this.value.getSingle(event);
if (value == null)
return (Object[]) Array.newInstance(getReturnType(), 0);
Object[] needle = this.needle.getAll(event);
if (needle.length == 0)
return position ? new Long[0] : new String[0];

if (this.position) {
if (string) {
String haystack = (String) objects.getSingle(event);
if (haystack == null)
return new Long[0];

return getStringPositions(haystack, (String) value);
}

return getListPositions(objects, value, event);
if (!this.position) {
assert haystack instanceof KeyProviderExpression<?>;
return getIndices((KeyProviderExpression<?>) haystack, needle, event);
}

assert objects instanceof KeyProviderExpression<?>;

return getIndices((KeyProviderExpression<?>) objects, value, event);
}

private Long[] getStringPositions(String haystack, String needle) {
boolean caseSensitive = SkriptConfig.caseSensitive.value();

List<Long> positions = new ArrayList<>();
long position = StringUtils.indexOf(haystack, needle, caseSensitive);
if (!string)
return getListPositions(haystack, needle, event);

if (position == -1)
String haystack = (String) this.haystack.getSingle(event);
if (haystack == null)
return new Long[0];

if (indexType == IndexType.ALL) {
while (position != -1) {
positions.add(position + 1);
position = StringUtils.indexOf(haystack, needle, (int) position + 1, caseSensitive);
}
return positions.toArray(Long[]::new);
}

if (indexType == IndexType.LAST)
position = StringUtils.lastIndexOf(haystack, needle, caseSensitive);

return new Long[]{position + 1};
return getStringPositions(haystack, (String[]) needle);
}

private Long[] getListPositions(Expression<?> list, Object value, Event event) {
/**
* Get the positions of needles in a haystack string
* @param haystack the haystack
* @param needles the needles
* @return the found positions
*/
private Long[] getStringPositions(String haystack, String[] needles) {
boolean caseSensitive = SkriptConfig.caseSensitive.value();

Iterator<?> iterator = list.iterator(event);
if (iterator == null)
return new Long[0];

List<Long> positions = new ArrayList<>();

long position = 0;
while (iterator.hasNext()) {
position++;

if (!equals(iterator.next(), value, caseSensitive))
for (String needle : needles) {
long position = StringUtils.indexOf(haystack, needle, caseSensitive);
if (position == -1)
continue;

if (indexType == IndexType.FIRST)
return new Long[]{position};

positions.add(position);
switch (indexType) {
case FIRST -> positions.add(position + 1);
case LAST -> positions.add((long) StringUtils.lastIndexOf(haystack, needle, caseSensitive));
case ALL -> {
do {
positions.add(position + 1);
position = StringUtils.indexOf(haystack, needle, (int) position + 1, caseSensitive);
} while (position != -1);
}
}
}

if (indexType == IndexType.LAST)
return new Long[]{positions.get(positions.size() - 1)};

return positions.toArray(Long[]::new);
}

private String[] getIndices(KeyProviderExpression<?> expression, Object value, Event event) {
/**
* Generic method to get the positions/indices of needles in a haystack
* @param haystackIterator the haystack
* @param needles the values to look for in the haystack
* @param valueMapper maps an item in the haystack to its value
* @param indexMapper maps an item in the haystack to its index/position
* @param arrayFactory factory to create the resulting array
* @return the found indices/positions
* @param <Item> the type of items in the haystack
* @param <Index> the type of the resulting indices/positions
* @param <Value> the type of values to look for
*/
private <Item, Index, Value> Index[] getMatches(
Iterator<Item> haystackIterator,
Value[] needles,
Function<Item, Value> valueMapper,
BiFunction<Item, Long, Index> indexMapper,
IntFunction<Index[]> arrayFactory
) {
boolean caseSensitive = SkriptConfig.caseSensitive.value();

Iterator<? extends KeyedValue<?>> iterator = expression.keyedIterator(event);
if (iterator == null)
return new String[0];

List<String> indices = new ArrayList<>();
//noinspection unchecked
List<Index>[] results = new List[needles.length];
long index = 1;
boolean shouldBreak = false;
while (haystackIterator.hasNext()) {
Item item = haystackIterator.next();
for (int i = 0; i < needles.length; i++) {
Object needle = needles[i];
if (!equals(valueMapper.apply(item), needle, caseSensitive))
continue;

Index mappedIndex = indexMapper.apply(item, index);
switch (indexType) {
case FIRST, LAST -> results[i] = Collections.singletonList(mappedIndex);
case ALL -> {
if (results[i] == null)
results[i] = new ArrayList<>();
results[i].add(mappedIndex);
}
}
}
// break early if all first indices were found
if (indexType == IndexType.FIRST && !ArrayUtils.contains(results, null))
break;
index++;
}

while (iterator.hasNext()) {
var keyedValue = iterator.next();
return Arrays.stream(results)
.filter(Objects::nonNull)
.flatMap(List::stream)
.toArray(arrayFactory);
}

if (!equals(keyedValue.value(), value, caseSensitive))
continue;
if (indexType == IndexType.FIRST)
return new String[]{keyedValue.key()};
/**
* Get the positions of needles in a haystack list
* @param haystack the haystack
* @param needles the needles
* @param event the event
* @return the found positions
*/
private Long[] getListPositions(Expression<?> haystack, Object[] needles, Event event) {
Iterator<?> haystackIterator = haystack.iterator(event);
if (haystackIterator == null)
return new Long[0];

indices.add(keyedValue.key());
}
return getMatches(haystackIterator, needles, item -> item, (item, index) -> index, Long[]::new);
}

if (indices.isEmpty())
/**
* Get the indices of needles in a haystack keyed list
* @param haystack the haystack
* @param needles the needles
* @param event the event
* @return the found indices
*/
private String[] getIndices(KeyProviderExpression<?> haystack, Object[] needles, Event event) {
Iterator<? extends KeyedValue<?>> haystackIterator = haystack.keyedIterator(event);
if (haystackIterator == null)
return new String[0];

if (indexType == IndexType.LAST)
return new String[]{indices.get(indices.size() - 1)};

return indices.toArray(String[]::new);
return getMatches(haystackIterator, needles, KeyedValue::value, (item, index) -> item.key(), String[]::new);
}

private boolean equals(Object key, Object value, boolean caseSensitive) {
Expand All @@ -224,7 +258,7 @@ private boolean equals(Object key, Object value, boolean caseSensitive) {

@Override
public boolean isSingle() {
return indexType == IndexType.FIRST || indexType == IndexType.LAST;
return (indexType == IndexType.FIRST || indexType == IndexType.LAST) && needle.isSingle();
}

@Override
Expand All @@ -237,7 +271,7 @@ public Class<?> getReturnType() {
@Override
public Expression<?> simplify() {
if (this.position && this.string
&& value instanceof Literal<?> && objects instanceof Literal<?>
&& needle instanceof Literal<?> && haystack instanceof Literal<?>
) {
return SimplifiedLiteral.fromExpression(this);
}
Expand All @@ -254,7 +288,7 @@ public String toString(@Nullable Event event, boolean debug) {
} else {
builder.append("indices");
}
builder.append("of value", value, "in", objects);
builder.append("of value", needle, "in", haystack);

return builder.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,58 @@ test "index of":
set {_list::*} to 1, 2, 3, 1, 2 and 3
set {_indices::*} to indices of the value 1 in {_list::*}
assert {_indices::*} is "1" and "4" with "indices of 1 failed"
assert indices of (1 and 3) in {_list::*} is "1", "4", "3" and "6" with "indices of 1 and 3 failed"

set {_indices::*} to the first index of the value 3 in {_list::*}
assert {_indices::*} is "3" with "first index of 3 failed"
assert the first index of (2 and 3) in {_list::*} is "2" and "3" with "first index of 2 and 3 failed"

set {_indices::*} to the last index of the value 3 in {_list::*}
assert {_indices::*} is "6" with "last index of 3 failed"
assert last index of (1 and 2) in {_list::*} is "4" and "5" with "last index of 1 and 2 failed"

set {_indices::*} to the position of the value 3 in {_list::*}
assert {_indices::*} is 3 with "first position of 3 failed"
assert position of (2 and 3) in {_list::*} is 2 and 3 with "first position of 2 and 3 ailed"

set {_indices::*} to the last position of the value 3 in {_list::*}
assert {_indices::*} is 6 with "last position of 3 failed"
assert last position of (1 and 3) in {_list::*} is 4 and 6 with "last position of 1 and 3 failed"

set {_otherlist::burb} to test-location
set {_otherlist::_DJ8U3f;} to test-location
set {_otherlist::;'w20} to test-location
set {_otherlist::_DJ8U3f;} to test-location
set {_otherList::breh} to 2
set {_otherlist::burb} to test-location
set {_otherList::quatro} to 4

set {_indices::*} to all indices of the value test-location in {_otherlist::*}
assert {_indices::*} is ";'w20", "_dj8u3f;" and "burb" with "indices of test-location with symbols failed"
assert all indices of (test-location and 2) in {_otherlist::*} is ";'w20", "_dj8u3f;", "burb" and "breh" with "indices of test-location and 2 failed"

set {_indices::*} to all positions of the value test-location in {_otherList::*}
assert {_indices::*} is 1, 2 and 4 with "positions of test-location failed"
assert all positions of (test-location and 4) in {_otherList::*} is 1, 2, 4 and 5 with "positions of test-location and 4 failed"

set {_indices::*} to all positions of "b" in "abcabcabcabc"
assert {_indices::*} is 2, 5, 8 and 11 with "positions of char failed"
assert all positions of ("a" and "c") in "abcabcabcabc" is 1, 4, 7, 10, 3, 6, 9 and 12 with "positions of char failed"

set {_indices::*} to all positions of "abc" in "abcabcabcabc"
assert {_indices::*} is 1, 4, 7 and 10 with "positions of string failed"
assert all positions of ("abc" and "bc") in "abcabcabcabc" is 1, 4, 7, 10, 2, 5, 8 and 11 with "positions of multiple strings failed"

set {_index} to first position of "c" in "abraham lincoln"
assert {_index} is 12 with "first position of char failed"
assert first position of ("a" and "n") in "abraham lincoln" is 1 and 11 with "first position of multiple chars failed"

set {_index} to the position of "ham" in "abraham lincoln"
assert {_index} is 5 with "first position of string failed"
assert position of ("ab" and "lin") in "abraham lincoln" is 1 and 9 with "first position of multiple strings failed"

set {_position} to position of "abc" in "nothing"
assert {_position} is not set with "position of string is set"
assert position of ("a" and "b") in "nothing" is not set with "position of multiple strings is set"

set {_position::*} to all positions of "nothing" in "abc"
assert {_position::*} is not set with "all positions of string is set"
assert all positions of ("nothing1" and "nothing2") in "abc" is not set with "all positions of multiple strings is set"