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
183 changes: 162 additions & 21 deletions src/main/java/ch/njol/skript/expressions/ExprSortedList.java
Original file line number Diff line number Diff line change
@@ -1,52 +1,79 @@
package ch.njol.skript.expressions;

import ch.njol.skript.Skript;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Example;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.doc.*;
import ch.njol.skript.lang.*;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.lang.simplification.SimplifiedLiteral;
import ch.njol.skript.lang.util.SimpleExpression;
import ch.njol.skript.util.LiteralUtils;
import ch.njol.util.Kleenean;
import ch.njol.util.coll.CollectionUtils;
import ch.njol.util.coll.iterator.EmptyIterator;
import com.google.common.collect.Iterators;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.skriptlang.skript.lang.comparator.Comparator;
import org.skriptlang.skript.lang.comparator.Comparators;
import org.skriptlang.skript.lang.comparator.Relation;

import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.*;

@Name("Sorted List")
@Description("Sorts given list in natural order. All objects in list must be comparable; if they're not, this expression will return nothing.")
@Description("""
Sorts given list in natural order. All objects in list must be comparable;
if they're not, this expression will return nothing.

When using the <code>sorted by</code> pattern,
the input expression can be used to refer to the current element being sorted.
(See input expression for more information.)""")
@Example("set {_sorted::*} to sorted {_players::*}")
@Example("""
command /leaderboard:
trigger:
loop reversed sorted {most-kills::*}:
send "%loop-counter%. %loop-index% with %loop-value% kills" to sender
""")
@Since("2.2-dev19, 2.14 (retain indices when looping)")
public class ExprSortedList extends SimpleExpression<Object> implements KeyedIterableExpression<Object> {
@Example("set {_sorted::*} to {_words::*} sorted in descending order by (length of input)")
@Since("2.2-dev19, 2.14 (retain indices when looping), INSERT_VERSION (sort by)")
@Keywords("input")
public class ExprSortedList extends SimpleExpression<Object> implements KeyedIterableExpression<Object>, InputSource {

private record MappedValue(Object original, Object mapped) { }
private record KeyedMappedValue(KeyedValue<?> keyed, Object mapped) { }

static {
Skript.registerExpression(ExprSortedList.class, Object.class, ExpressionType.PROPERTY, "sorted %objects%");
Skript.registerExpression(ExprSortedList.class, Object.class, ExpressionType.PROPERTY,
"sorted %objects%",
"%objects% sorted [in (:descending|ascending) order] [(by|based on) \\(<.+>\\)]",
"%objects% sorted [in (:descending|ascending) order] [(by|based on) \\[<.+>\\]]"
);
if (!ParserInstance.isRegistered(InputData.class))
ParserInstance.registerData(InputData.class, InputData::new);
}

private Expression<?> list;
private boolean keyed;
private @Nullable Expression<?> mappingExpr;
private boolean descendingOrder;

private final Set<ExprInput<?>> dependentInputs = new HashSet<>();

private @Nullable Object currentValue;
private @UnknownNullability String currentIndex;

public ExprSortedList() {
}

public ExprSortedList(Expression<?> list) {
private ExprSortedList(Expression<?> list, @Nullable Expression<?> mappingExpr, boolean descendingOrder) {
this.list = list;
this.keyed = KeyedIterableExpression.canIterateWithKeys(list);
this.mappingExpr = mappingExpr;
this.descendingOrder = descendingOrder;
}

@Override
Expand All @@ -57,20 +84,75 @@ public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean is
return false;
}
keyed = KeyedIterableExpression.canIterateWithKeys(list);
descendingOrder = parseResult.hasTag("descending");

//noinspection DuplicatedCode
if (!parseResult.regexes.isEmpty()) {
@Nullable String unparsedExpression = parseResult.regexes.get(0).group();
assert unparsedExpression != null;
mappingExpr = parseExpression(unparsedExpression, getParser(), SkriptParser.PARSE_EXPRESSIONS);
if (mappingExpr == null)
return false;
if (!mappingExpr.isSingle()) {
Skript.error("The mapping expression in the sort expression must only return a single value for a single input.");
return false;
}
}
return LiteralUtils.canInitSafely(list);
}

@Override
protected Object @Nullable [] get(Event event) {
public @NotNull Iterator<?> iterator(Event event) {
if (keyed)
return Iterators.transform(keyedIterator(event), KeyedValue::value);

currentIndex = null;
int sortingMultiplier = descendingOrder ? -1 : 1;

if (mappingExpr == null) {
try {
// toList() forces eager evaluation so the comparator exceptions are caught here
return list.stream(event)
.sorted((o1, o2) -> compare(o1, o2) * sortingMultiplier)
.toList().iterator();
} catch (IllegalArgumentException | ClassCastException e) {
error("Sorting failed because the elements in the list could not be compared with each other.");
return Collections.emptyIterator();
}
}

List<MappedValue> mappedValues = new ArrayList<>();
Iterator<?> it = list.iterator(event);
if (it == null)
return Collections.emptyIterator();
while (it.hasNext()) {
currentValue = it.next();
Object mappedValue = mappingExpr.getSingle(event);
if (mappedValue == null) {
error("Sorting failed because Skript cannot sort null values. "
+ "The mapping expression '" + mappingExpr.toString(event, false)
+ "' returned a null value when given the input '" + currentValue + "'.");
return Collections.emptyIterator();
}
mappedValues.add(new MappedValue(currentValue, mappedValue));
}
try {
return list.stream(event)
.sorted(ExprSortedList::compare)
.toArray();
return mappedValues.stream()
.sorted((o1, o2) -> compare(o1.mapped(), o2.mapped()) * sortingMultiplier)
.map(MappedValue::original)
.toList().iterator();
} catch (IllegalArgumentException | ClassCastException e) {
return (Object[]) Array.newInstance(getReturnType(), 0);
error("Sorting failed because the mapped values could not be compared with each other.");
return Collections.emptyIterator();
}
}

@Override
@SuppressWarnings("unchecked")
protected Object @Nullable [] get(Event event) {
return Iterators.toArray(iterator(event), (Class<Object>) getReturnType());
}

@Override
public boolean canIterateWithKeys() {
return keyed;
Expand All @@ -80,12 +162,41 @@ public boolean canIterateWithKeys() {
public Iterator<KeyedValue<Object>> keyedIterator(Event event) {
if (!keyed)
throw new UnsupportedOperationException();
int sortingMultiplier = descendingOrder ? -1 : 1;
if (mappingExpr == null) {
try {
//noinspection unchecked,rawtypes
return (Iterator) ((KeyedIterableExpression<?>) list).keyedStream(event)
.sorted((a, b) -> compare(a.value(), b.value()) * sortingMultiplier)
.toList().iterator();
} catch (IllegalArgumentException | ClassCastException e) {
error("Sorting failed because the elements in the list could not be compared with each other.");
return EmptyIterator.get();
}
}

List<KeyedMappedValue> keyedMappedValues = new ArrayList<>();
for (Iterator<? extends KeyedValue<?>> it = ((KeyedIterableExpression<?>) list).keyedIterator(event); it.hasNext(); ) {
KeyedValue<?> keyedValue = it.next();
currentIndex = keyedValue.key();
currentValue = keyedValue.value();
Object mappedValue = mappingExpr.getSingle(event);
if (mappedValue == null) {
error("Sorting failed because Skript cannot sort null values. "
+ "The mapping expression '" + mappingExpr.toString(event, false)
+ "' returned a null value when given the input '" + currentValue + "'.");
return EmptyIterator.get();
}
keyedMappedValues.add(new KeyedMappedValue(keyedValue, mappedValue));
}
try {
//noinspection unchecked,rawtypes
return (Iterator) ((KeyedIterableExpression<?>) list).keyedStream(event)
.sorted((a, b) -> compare(a.value(), b.value()))
.iterator();
return (Iterator) keyedMappedValues.stream()
.sorted((a, b) -> compare(a.mapped(), b.mapped()) * sortingMultiplier)
.map(KeyedMappedValue::keyed)
.toList().iterator();
} catch (IllegalArgumentException | ClassCastException e) {
error("Sorting failed because the mapped values could not be compared with each other.");
return EmptyIterator.get();
}
}
Expand All @@ -110,10 +221,14 @@ public static <A, B> int compare(A a, B b) throws IllegalArgumentException, Clas
//noinspection unchecked
return (Expression<? extends R>) this;

// when a mapping expression is present, InputSource wiring prevents safe shallow-copying
if (mappingExpr != null)
return super.getConvertedExpression(to);

Comment thread
sovdeeth marked this conversation as resolved.
Expression<? extends R> convertedList = list.getConvertedExpression(to);
if (convertedList != null)
//noinspection unchecked
return (Expression<? extends R>) new ExprSortedList(convertedList);
return (Expression<? extends R>) new ExprSortedList(convertedList, mappingExpr, descendingOrder);

return null;
}
Expand Down Expand Up @@ -152,14 +267,40 @@ public boolean isLoopOf(String input) {

@Override
public Expression<?> simplify() {
if (list instanceof Literal<?>)
if (list instanceof Literal<?> && mappingExpr == null)
return SimplifiedLiteral.fromExpression(this);
return this;
}

@Override
public Set<ExprInput<?>> getDependentInputs() {
return dependentInputs;
}

@Override
public @Nullable Object getCurrentValue() {
return currentValue;
}

@Override
public boolean hasIndices() {
return keyed;
}

@Override
public @UnknownNullability String getCurrentIndex() {
return currentIndex;
}

@Override
public String toString(@Nullable Event event, boolean debug) {
return "sorted " + list.toString(event, debug);
SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug);
if (mappingExpr == null && !descendingOrder)
Comment thread
ethanrjs marked this conversation as resolved.
return builder.append("sorted", list).toString();
builder.append(list, "sorted", "in", descendingOrder ? "descending" : "ascending", "order");
if (mappingExpr != null)
builder.append("by", mappingExpr);
return builder.toString();
}

}
Loading