First commit of the library
This commit is contained in:
commit
b92caea4ab
23 changed files with 1272 additions and 0 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
9
justfile
Normal file
9
justfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
set dotenv-load := true
|
||||
set export := true
|
||||
|
||||
# Publish the core module to Maven repository
|
||||
publish:
|
||||
#!/usr/bin/env bash
|
||||
cd ..
|
||||
set -euxo pipefail
|
||||
mvn -pl core clean deploy
|
85
pom.xml
Normal file
85
pom.xml
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>ninja.the-fire-archmage</groupId>
|
||||
<artifactId>datastructure-utils</artifactId>
|
||||
<version>1.0</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- credentials in ~/.m2/settings.xml -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>fire-archmage-forgejo</id>
|
||||
<url>https://forgejo-for.the-fire-archmage.ninja/api/packages/loic/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>fire-archmage-forgejo</id>
|
||||
<url>https://forgejo-for.the-fire-archmage.ninja/api/packages/loic/maven</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>fire-archmage-forgejo</id>
|
||||
<url>https://forgejo-for.the-fire-archmage.ninja/api/packages/loic/maven</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
<executions>
|
||||
<!-- First pass with lombok -->
|
||||
<execution>
|
||||
<id>default-compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals><goal>compile</goal></goals>
|
||||
</execution>
|
||||
|
||||
<!-- Second pass to build module without lombok -->
|
||||
<execution>
|
||||
<id>compile-module-info</id>
|
||||
<phase>process-classes</phase>
|
||||
<goals><goal>compile</goal></goals>
|
||||
<configuration>
|
||||
<compileSourceRoots>${project.basedir}/src/module-info/java</compileSourceRoots>
|
||||
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
8
src/main/java/module-info.java
Normal file
8
src/main/java/module-info.java
Normal file
|
@ -0,0 +1,8 @@
|
|||
module datastructure.utils {
|
||||
requires java.base;
|
||||
|
||||
exports ninja.thefirearchmage.utils.datastructures;
|
||||
exports ninja.thefirearchmage.utils.functions;
|
||||
|
||||
requires static lombok;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class Either<L,R> {
|
||||
private final Optional<L> leftResult;
|
||||
private final Optional<R> rightResult;
|
||||
|
||||
private Either(Optional<L> leftResult, Optional<R> rightResult) {
|
||||
this.leftResult = leftResult;
|
||||
this.rightResult = rightResult;
|
||||
}
|
||||
|
||||
public static <L, R> Either<L, R> right(R value) {
|
||||
return new Either<>(Optional.empty(), Optional.of(value));
|
||||
}
|
||||
|
||||
public static <L, R> Either<L, R> left(L value) {
|
||||
return new Either<>(Optional.of(value), Optional.empty());
|
||||
}
|
||||
|
||||
public boolean isLeft() {
|
||||
return this.leftResult.isPresent();
|
||||
}
|
||||
|
||||
public boolean isRight() {
|
||||
return this.rightResult.isPresent();
|
||||
}
|
||||
|
||||
public R getRight() {
|
||||
return this.rightResult.orElseThrow(()-> new IllegalStateException("This either is Left"));
|
||||
}
|
||||
public R get() {
|
||||
return getRight();
|
||||
}
|
||||
public <U> Either<L, U> map(Function<R,U> mapperFunction) {
|
||||
return new Either<>(this.leftResult, this.rightResult.map(mapperFunction));
|
||||
}
|
||||
|
||||
public L getLeft() {
|
||||
return this.leftResult.orElseThrow(() -> new IllegalStateException("This either is Right"));
|
||||
}
|
||||
|
||||
public void ifRight(Consumer<R> function) {
|
||||
rightResult.ifPresent(function);
|
||||
}
|
||||
|
||||
public void ifLeft(Consumer<L> function) {
|
||||
leftResult.ifPresent(function);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
public class MathUtils {
|
||||
/**
|
||||
* Returns an integer from rounding down a double, applying the following rounding method:
|
||||
* if decimal fraction <=0.5 return the number by default
|
||||
* if decimal fraction >0.5 return the number by excess
|
||||
* The method is name after the {@link java.math.RoundingMode} used in {@link java.math.BigDecimal}, but
|
||||
* this method uses way less memory and cpu to accomplish the same, due to a very different use case
|
||||
*/
|
||||
public static int roundHalfDown(double d) {
|
||||
double integerPart = Math.floor(d);
|
||||
double decimalPart = d - integerPart;
|
||||
if (decimalPart <= 0.5) {
|
||||
return (int) integerPart;
|
||||
} else {
|
||||
return (int) Math.ceil(d);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
public class MutableList {
|
||||
@SafeVarargs
|
||||
public static <T> List<T> of(T... items) {
|
||||
return Stream.of(items).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutable list collector
|
||||
* @return a mutable list collector
|
||||
* @param <T> the type of the items contained in the list
|
||||
*/
|
||||
public static <T> Collector<T, List<T>, List<T>> collector() {
|
||||
return new MutableListCollector<>();
|
||||
}
|
||||
|
||||
private static class MutableListCollector<R> implements Collector<R, List<R>, List<R>> {
|
||||
public MutableListCollector() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<List<R>> supplier() {
|
||||
return ArrayList::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<List<R>, R> accumulator() {
|
||||
return List::add;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<List<R>> combiner() {
|
||||
return (list1, list2) -> {
|
||||
list1.addAll(list2);
|
||||
return list1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<List<R>, List<R>> finisher() {
|
||||
return identity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class MutableMap {
|
||||
|
||||
public static <K,V> Map<K,V> empty() {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Mutable version of an OptionalMap. This is implemented with a backing HashMap.
|
||||
* @param <K> key type
|
||||
* @param <V> value type
|
||||
*/
|
||||
public class MutableOptionalMap<K,V> implements OptionalMap<K,V>{
|
||||
private final Map<K,V> backingMap;
|
||||
|
||||
private MutableOptionalMap(Map<K, V> wrappedMap) {
|
||||
this.backingMap = wrappedMap;
|
||||
}
|
||||
|
||||
public static <K,V> MutableOptionalMap<K, V> of(Map<K,V> wrappedMap) {
|
||||
return new MutableOptionalMap<>(wrappedMap);
|
||||
}
|
||||
|
||||
public static <K,V> MutableOptionalMap<K, V> empty() {
|
||||
return new MutableOptionalMap<>(new HashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<V> getOptional(K key) {
|
||||
return Optional.ofNullable(backingMap.get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutableOptionalMap<K,V> put(Tuple2<K,V> entry) {
|
||||
backingMap.put(entry._1, entry._2);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
|
||||
return backingMap.merge(key, value, remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
||||
return backingMap.compute(key, remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
||||
return backingMap.computeIfPresent(key, remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
return backingMap.computeIfAbsent(key, mappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V replace(K key, V value) {
|
||||
return backingMap.replace(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replace(K key, V oldValue, V newValue) {
|
||||
return backingMap.replace(key, oldValue, newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object key, Object value) {
|
||||
return backingMap.remove(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V putIfAbsent(K key, V value) {
|
||||
return backingMap.putIfAbsent(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
|
||||
backingMap.replaceAll(function);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(BiConsumer<? super K, ? super V> action) {
|
||||
backingMap.forEach(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getOrDefault(Object key, V defaultValue) {
|
||||
return backingMap.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
return backingMap.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<V> values() {
|
||||
return backingMap.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
return backingMap.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
backingMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends K, ? extends V> m) {
|
||||
backingMap.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
return backingMap.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
return backingMap.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
return backingMap.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return backingMap.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return backingMap.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return backingMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return backingMap.size();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
/**
|
||||
* Utility class to easily generate guaranteed mutable sets, either as simple varargs items
|
||||
* or as a streaming collecting operation
|
||||
*/
|
||||
public abstract class MutableSet {
|
||||
/**
|
||||
* Returns a mutable set from the given items.
|
||||
* @param items varargs items
|
||||
* @return mutable set
|
||||
* @param <T> the type of the items
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> Set<T> of(T... items) {
|
||||
return Stream.of(items).collect(MutableSet.collector());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable empty set
|
||||
* @return mutable empty set
|
||||
* @param <T> type of the items inside
|
||||
*/
|
||||
public static <T> Set<T> empty() {
|
||||
return new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutable set collector
|
||||
* @return a mutable set collector
|
||||
* @param <T> the type of the items contained in the set
|
||||
*/
|
||||
public static <T> Collector<T, Set<T>, Set<T>> collector() {
|
||||
return new MutableSetCollector<>();
|
||||
}
|
||||
|
||||
private static class MutableSetCollector<R> implements Collector<R, Set<R>, Set<R>> {
|
||||
public MutableSetCollector() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Set<R>> supplier() {
|
||||
return HashSet::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<Set<R>, R> accumulator() {
|
||||
return Set::add;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<Set<R>> combiner() {
|
||||
return (set1, set2) -> {
|
||||
set1.addAll(set2);
|
||||
return set1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<Set<R>, Set<R>> finisher() {
|
||||
return identity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Adds an Optional retrieval method to a Map and some convenience methods to instantiate mutable optional maps.
|
||||
* @param <K> Key type
|
||||
* @param <V> Value type
|
||||
*/
|
||||
public interface OptionalMap<K,V> extends Map<K,V> {
|
||||
/**
|
||||
* Returns an optional value of a given key.
|
||||
* @param key the key for the value
|
||||
* @return Some(Value) if key exists, None otherwise
|
||||
*/
|
||||
Optional<V> getOptional(K key);
|
||||
default <R> Optional<R> getOptional(K key,Class<R> clazz) {
|
||||
return getOptional(key).map(clazz::cast);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chainable put operation, with a tuple instead of two arguments.
|
||||
*
|
||||
* @param entry key/value tuple
|
||||
* @return the map itself to keep chaining methods
|
||||
*/
|
||||
OptionalMap<K,V> put(Tuple2<K,V> entry);
|
||||
|
||||
/**
|
||||
* Returns an empty mutable OptionalMap
|
||||
* @return empty mutable OptionalMap
|
||||
* @param <K> Key type
|
||||
* @param <V> Value type
|
||||
*/
|
||||
static <K,V> OptionalMap<K,V> empty() {
|
||||
return MutableOptionalMap.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Optional view of a given map. No copy is performed, the original map is used and muted.
|
||||
* @param wrappedMap the original map to wrap
|
||||
* @return a wrapped Optional view of the given map.
|
||||
* @param <K> the Key type
|
||||
* @param <V> the Value type
|
||||
*/
|
||||
static <K,V> OptionalMap<K,V> of(Map<K,V> wrappedMap) {
|
||||
return MutableOptionalMap.of(wrappedMap);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
/**
|
||||
* Utility functions related to ordered sets.
|
||||
* Returns mutable sets.
|
||||
*/
|
||||
public abstract class OrderedSet {
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> SortedSet<T> of(Comparator<T> sorterFunction, T... items) {
|
||||
var orderedSet = new TreeSet<>(sorterFunction);
|
||||
orderedSet.addAll(Arrays.asList(items));
|
||||
|
||||
return orderedSet;
|
||||
}
|
||||
public static <T> Set<T> empty(Comparator<T> sorterFunction) {
|
||||
return new TreeSet<>(sorterFunction);
|
||||
}
|
||||
|
||||
public static <T> Collector<T, SortedSet<T>, SortedSet<T>> collector(Comparator<T> sorterFunction) {
|
||||
return new MutableSortedSetCollector<>(sorterFunction);
|
||||
}
|
||||
|
||||
private record MutableSortedSetCollector<R>(
|
||||
Comparator<R> sorterFunction) implements Collector<R, SortedSet<R>, SortedSet<R>> {
|
||||
|
||||
@Override
|
||||
public Supplier<SortedSet<R>> supplier() {
|
||||
return () -> new TreeSet<>(this.sorterFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<SortedSet<R>, R> accumulator() {
|
||||
return SortedSet::add;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<SortedSet<R>> combiner() {
|
||||
return (set1, set2) -> {
|
||||
set1.addAll(set2);
|
||||
return set1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<SortedSet<R>, SortedSet<R>> finisher() {
|
||||
return identity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
/**
|
||||
* A simplified wrapper over a collection that allows fetching elements from it in a random order.
|
||||
* It is not a Collection itself, so it's only to be used in very simple ways. This can change if the need arises.
|
||||
*/
|
||||
public class RandomAccessCollection<T> {
|
||||
private List<T> backingList;
|
||||
private Random random;
|
||||
|
||||
public RandomAccessCollection(Collection<T> wrappedCollection) {
|
||||
this.backingList = new ArrayList<>(wrappedCollection);
|
||||
this.random = new Random();
|
||||
}
|
||||
public RandomAccessCollection() {
|
||||
this(List.of());
|
||||
}
|
||||
public RandomAccessCollection(T[] wrappedCollection) {
|
||||
this(Arrays.stream(wrappedCollection).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public void add(T item) {
|
||||
this.backingList.add(item);
|
||||
}
|
||||
public void addAll(Collection<T> items) {
|
||||
this.backingList.addAll(items);
|
||||
}
|
||||
public void addAll(RandomAccessCollection<T> items) {
|
||||
this.backingList.addAll(items.backingList);
|
||||
}
|
||||
|
||||
public static <T> RandomAccessCollection<T> from(Collection<T> wrappedCollection) {
|
||||
return new RandomAccessCollection<>(wrappedCollection);
|
||||
}
|
||||
|
||||
public static <T> Optional<T> randomFrom(Collection<T> wrappedStream) {
|
||||
return from(wrappedStream).nextItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return randomly true or false.
|
||||
*/
|
||||
public static boolean randomBool() {
|
||||
var r = new Random();
|
||||
return switch (r.nextInt(0, 2)) {
|
||||
case 0 -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next random item from this collection.
|
||||
* Returns none if the collection is empty.
|
||||
*/
|
||||
public Optional<T> nextItem() {
|
||||
if (!backingList.isEmpty()) {
|
||||
var position = random.nextInt(0, backingList.size());
|
||||
return Optional.of(backingList.get(position));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Collector code
|
||||
public static <R> Collector<R, RandomAccessCollection<R>, RandomAccessCollection<R>> collector() {
|
||||
return new RandomAccesCollectionCollector<>();
|
||||
}
|
||||
|
||||
private record RandomAccesCollectionCollector<R>()
|
||||
implements Collector<R, RandomAccessCollection<R>, RandomAccessCollection<R>> {
|
||||
|
||||
@Override
|
||||
public Supplier<RandomAccessCollection<R>> supplier() {
|
||||
return RandomAccessCollection::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<RandomAccessCollection<R>, R> accumulator() {
|
||||
return RandomAccessCollection::add;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<RandomAccessCollection<R>> combiner() {
|
||||
return (collection1, collection2) -> {
|
||||
collection1.addAll(collection2);
|
||||
return collection1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<RandomAccessCollection<R>, RandomAccessCollection<R>> finisher() {
|
||||
return identity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return Set.of();
|
||||
}
|
||||
}}
|
|
@ -0,0 +1,272 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import ninja.thefirearchmage.utils.functions.CheckedCallable;
|
||||
import ninja.thefirearchmage.utils.functions.CheckedMapper;
|
||||
import ninja.thefirearchmage.utils.functions.CheckedRunnable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
/**
|
||||
* Very simple implementation of the Try object for the project.
|
||||
* Has most of the methods I finally end up using from
|
||||
* <a href="https://github.com/vavr-io/vavr/blob/master/src/main/java/io/vavr/control/Try.java">vavr Try class</a>
|
||||
*/
|
||||
public class Try<T> {
|
||||
|
||||
private final Optional<T> result;
|
||||
private final Optional<? extends Throwable> error;
|
||||
|
||||
private Try(Optional<T> result, Optional<? extends Throwable> error) {
|
||||
this.result = result;
|
||||
this.error = error;
|
||||
}
|
||||
public static Try<Void> success() {
|
||||
return new Try<>(Optional.empty(), Optional.empty());
|
||||
}
|
||||
public static <T> Try<T> success(T value) {
|
||||
return new Try<>(Optional.of(value), Optional.empty());
|
||||
}
|
||||
public static <T> Try<T> failure(Throwable error) {
|
||||
return new Try<>(Optional.empty(), Optional.of(error));
|
||||
}
|
||||
|
||||
public static <T,X extends Throwable> Try<T> of(CheckedCallable<T,X> function) {
|
||||
try {
|
||||
return success(function.call());
|
||||
} catch(Throwable e) {
|
||||
return failure(e);
|
||||
}
|
||||
}
|
||||
public static <X extends Throwable> Try<Void> of(CheckedRunnable<X> function) {
|
||||
try {
|
||||
function.run();
|
||||
return success();
|
||||
} catch (Throwable e) {
|
||||
return failure(e);
|
||||
}
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return this.result.get();
|
||||
}
|
||||
public T orElseThrow(Function<Throwable,Throwable> errorMappingFunction) throws Throwable {
|
||||
if(isFailure()) {
|
||||
throw errorMappingFunction.apply(this.error.get());
|
||||
}
|
||||
|
||||
return isVoidType() ? null : this.result.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lambdas don't play nice with checked exceptions, this allows to map the failure to a runtime exception
|
||||
* as supplied by the caller.
|
||||
* @param errorMappingFunction the checked to runtime exception mapper
|
||||
* @return the value if the try is not failing
|
||||
* @throws RuntimeException as provided by the user if the Try is a failure
|
||||
*/
|
||||
public T orElseThrowRuntime(Function<Throwable,? extends RuntimeException> errorMappingFunction) throws RuntimeException {
|
||||
if(isFailure()) {
|
||||
throw errorMappingFunction.apply(this.error.get());
|
||||
}
|
||||
|
||||
return isVoidType() ? null : this.result.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically wraps the failure into a runtime exception. Use this only if you're ok with transforming a potentially
|
||||
* checked exception into a runtime one.
|
||||
* @return the value of the Try if it is successful
|
||||
* @throws RuntimeException that wraps the original exception if the Try is a failure
|
||||
*/
|
||||
public T orElseThrowRuntime() throws RuntimeException {
|
||||
if(isFailure()) {
|
||||
throw new RuntimeException(this.error.get());
|
||||
}
|
||||
|
||||
return isVoidType() ? null : this.result.get();
|
||||
}
|
||||
|
||||
public T orElseThrow() throws Throwable {
|
||||
if(isFailure()) {
|
||||
throw this.error.get();
|
||||
}
|
||||
|
||||
return isVoidType() ? null : this.result.get();
|
||||
}
|
||||
|
||||
public <R> Try<R> map(CheckedMapper<T,R, Throwable> mapper) {
|
||||
if(this.error.isPresent()) {
|
||||
return Try.failure(this.error.get());
|
||||
}
|
||||
|
||||
try {
|
||||
var newValue = mapper.map(this.result.get());
|
||||
return Try.success(newValue);
|
||||
} catch (Throwable e) {
|
||||
return Try.failure(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the inner Try
|
||||
*/
|
||||
public <R> Try<R> flatMap(Function<T,Try<R>> mappingFunction) {
|
||||
if(this.error.isPresent()) {
|
||||
return Try.failure(this.error.get());
|
||||
}
|
||||
|
||||
return mappingFunction.apply(this.get());
|
||||
}
|
||||
/**
|
||||
* If this try has failed, maps the desired type exception to the one provided in the error mapper function.
|
||||
* @param failureType the type of exception we want to map. To specify other types, call the method again
|
||||
* @param errorMapper the mapping function
|
||||
* @return the try object to keep chaining
|
||||
*/
|
||||
public Try<T> mapFailure(Class<? extends Throwable> failureType, Function<Throwable,? extends Throwable> errorMapper) {
|
||||
this.error.map(e -> {
|
||||
if( failureType.isAssignableFrom(e.getClass()) ){
|
||||
return errorMapper.apply(e);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code with the result of the current try only if this try is a success.
|
||||
* Returns a new Try with the result of the called function.
|
||||
* @param function the function to call will receive the result of the try
|
||||
* @return a new try with the result of the function, else this try.
|
||||
*/
|
||||
public <R> Try<R> andThen(Function<T,R> function) {
|
||||
if(this.error.isEmpty()) {
|
||||
return Try.of(()-> function.apply(this.result.get()));
|
||||
}
|
||||
|
||||
return failure(this.error.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code that doesn't need the result from the current try if the current try is successful, then returns a
|
||||
* new try with the result of the function.<br />
|
||||
* The typical usage will be consuming a Try<Void> and returning something else
|
||||
* @param function the function to call
|
||||
* @return a new Try with the result of the function, or a new try with the current failure
|
||||
* @param <R> the result type of the function
|
||||
*/
|
||||
public <R> Try<R> andThen(Callable<R> function) {
|
||||
if(this.error.isEmpty()) {
|
||||
return Try.of(function::call);
|
||||
}
|
||||
|
||||
return failure(this.error.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code only if the current try is successful. Doesn't use the try result, nor returns any value.
|
||||
* @param function the function to call
|
||||
* @return a new try from the called function or a new try with the current one's failure
|
||||
*/
|
||||
public Try<Void> andThen(Runnable function) {
|
||||
if(this.error.isEmpty()) {
|
||||
return Try.of(function::run);
|
||||
}
|
||||
|
||||
return failure(this.error.get());
|
||||
}
|
||||
|
||||
public void ifFailure(Consumer<Throwable> errorConsumer) {
|
||||
this.error.ifPresent(errorConsumer);
|
||||
}
|
||||
|
||||
public Throwable getCause() {
|
||||
return this.error.get();
|
||||
}
|
||||
public boolean isFailure() {
|
||||
return error.isPresent();
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return error.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the type of result of this Try is Void
|
||||
*/
|
||||
private boolean isVoidType() {
|
||||
return result.isEmpty() && error.isEmpty();
|
||||
}
|
||||
|
||||
public static <R> Try<List<R>> sequence(Iterable<Try<R>> tries) {
|
||||
// Guard against empty collections coming in
|
||||
if(!tries.iterator().hasNext()) {
|
||||
return success(List.of());
|
||||
}
|
||||
|
||||
boolean isVoidType = tries.iterator().next().isVoidType();
|
||||
if(isVoidType) {
|
||||
for(var item : tries) {
|
||||
if(item.isFailure()) {
|
||||
return failure(item.getCause());
|
||||
}
|
||||
}
|
||||
return success(List.of());
|
||||
} else {
|
||||
var returns = new ArrayList<R>();
|
||||
for(var item : tries) {
|
||||
if(item.isSuccess()) {
|
||||
returns.add(item.get());
|
||||
} else {
|
||||
return failure(item.getCause());
|
||||
}
|
||||
}
|
||||
return success(returns);
|
||||
}
|
||||
}
|
||||
|
||||
public static <R> Collector<Try<R>, List<Try<R>>, Try<List<R>>> collector() {
|
||||
return new TryCollector<>();
|
||||
}
|
||||
|
||||
private static class TryCollector<R> implements Collector<Try<R>, List<Try<R>>, Try<List<R>>> {
|
||||
public TryCollector() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<List<Try<R>>> supplier() {
|
||||
return ArrayList::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<List<Try<R>>, Try<R>> accumulator() {
|
||||
return List::add;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<List<Try<R>>> combiner() {
|
||||
return (list1, list2) -> {
|
||||
list1.addAll(list2);
|
||||
return list1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<List<Try<R>>, Try<List<R>>> finisher() {
|
||||
return Try::sequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
public abstract class Tuple {
|
||||
public static <T,R> Tuple2<T,R> of(T firstValue, R secondValue) {
|
||||
return new Tuple2<>(firstValue, secondValue);
|
||||
}
|
||||
public static <T,R,U> Tuple3<T,R,U> of(T firstValue, R secondValue, U thirdValue) {
|
||||
return new Tuple3<>(firstValue, secondValue, thirdValue);
|
||||
}
|
||||
public static <T,R,U,Y> Tuple4<T,R,U,Y> of(T firstValue, R secondValue, U thirdValue, Y fourthValue) {
|
||||
return new Tuple4<>(firstValue, secondValue, thirdValue, fourthValue);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Data structure to hold 2 different fields
|
||||
* @param <T> First field type
|
||||
* @param <R> Second field type
|
||||
*/
|
||||
@Getter
|
||||
@ToString(of = {"_1", "_2"})
|
||||
@EqualsAndHashCode(of = {"_1", "_2"}, callSuper = false)
|
||||
public class Tuple2<T,R> extends Tuple {
|
||||
final public T _1;
|
||||
final public R _2;
|
||||
|
||||
Tuple2(T _1, R _2) {
|
||||
this._1 = _1;
|
||||
this._2 = _2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the first and second field with the given functions. Does not handle mapping errors.
|
||||
* @return a new Tuple with the mapped values
|
||||
* @param <U> the return type for the mapper
|
||||
*/
|
||||
public <U> U map(BiFunction<T,R,U> tuppleMapper) {
|
||||
return tuppleMapper.apply(this._1, this._2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new tuple with each field being the result of applying the corresponding mapper function.
|
||||
* @param f1Mapper the mapping function to apply to the first field
|
||||
* @param f2Mapper the mapping function to apply to the second field
|
||||
* @return a new tuple with the mapper functions applied
|
||||
* @param <A> the new return type for the first field
|
||||
* @param <S> the new return type for the second field
|
||||
*/
|
||||
public <A,S> Tuple2<A,S> map(Function<T,A> f1Mapper,
|
||||
Function<R,S> f2Mapper) {
|
||||
return of(f1Mapper.apply(_1), f2Mapper.apply(_2));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import ninja.thefirearchmage.utils.functions.Function3;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Data structure to hold 3 generic fields.
|
||||
* @param <T> First field type
|
||||
* @param <R> Second field type
|
||||
* @param <U> Third field type
|
||||
*/
|
||||
@Getter
|
||||
@ToString(of = {"_1", "_2", "_3"})
|
||||
@EqualsAndHashCode(of = {"_1", "_2", "_3"}, callSuper = false)
|
||||
public class Tuple3<T,R,U> extends Tuple {
|
||||
final public T _1;
|
||||
final public R _2;
|
||||
final public U _3;
|
||||
|
||||
Tuple3(T _1, R _2, U _3) {
|
||||
this._1 = _1;
|
||||
this._2 = _2;
|
||||
this._3 = _3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the fields of the tuple with the given functions. Does not handle mapping errors.
|
||||
* @return a mapped object from the original tuple
|
||||
*/
|
||||
public <A> A map(Function3<T,R,U,A> tuppleMapper) {
|
||||
return tuppleMapper.apply(_1, _2, _3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new tuple with each field being the result of applying the corresponding mapper function.
|
||||
* @param f1Mapper the mapping function to apply to the first field
|
||||
* @param f2Mapper the mapping function to apply to the second field
|
||||
* @param f3Mapper the mapping function to apply to the third field
|
||||
* @return a new tuple with the mapper functions applied
|
||||
* @param <A> the new return type for the first field
|
||||
* @param <S> the new return type for the second field
|
||||
* @param <D> the new return type for the third field
|
||||
*/
|
||||
public <A,S,D> Tuple3<A,S,D> map(Function<T,A> f1Mapper,
|
||||
Function<R,S> f2Mapper,
|
||||
Function<U,D> f3Mapper) {
|
||||
return of(f1Mapper.apply(_1), f2Mapper.apply(_2), f3Mapper.apply(_3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to use in reduce stream functions to accumulate tuples that contain only floats<br />
|
||||
* <code>
|
||||
* var t1 = Tuple.of(1f,2f,3f); <br />
|
||||
* var t2 = Tuple.of(4f, 5f, 6f); <br />
|
||||
* var t3 = Tuple3.sum(t1, t2) <br />
|
||||
* // t3 = (5f, 7f, 9f)
|
||||
* </code>
|
||||
* @param t1 the first float tuple
|
||||
* @param t2 the second float tuple
|
||||
* @return a new tuple with the result of adding each individual field
|
||||
*/
|
||||
public static Tuple3<Float, Float, Float> sum(Tuple3<Float, Float, Float> t1,
|
||||
Tuple3<Float, Float, Float> t2) {
|
||||
return of(t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3);
|
||||
}
|
||||
|
||||
public Tuple2<R, U> remove1() {
|
||||
return of(_2, _3);
|
||||
}
|
||||
public Tuple2<T, U> remove2() {
|
||||
return of(_1, _3);
|
||||
}
|
||||
public Tuple2<T, R> remove3() {
|
||||
return of(_1, _2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package ninja.thefirearchmage.utils.datastructures;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import ninja.thefirearchmage.utils.functions.Function4;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Data structure to hold 4 generic fields.
|
||||
* @param <T> First field type
|
||||
* @param <R> Second field type
|
||||
* @param <U> Third field type
|
||||
* @param <Y> Fourth field type
|
||||
*/
|
||||
@Getter
|
||||
@ToString(of = {"_1", "_2", "_3", "_4"})
|
||||
@EqualsAndHashCode(of = {"_1", "_2", "_3", "_4"}, callSuper = false)
|
||||
public class Tuple4<T,R,U,Y> extends Tuple {
|
||||
final public T _1;
|
||||
final public R _2;
|
||||
final public U _3;
|
||||
final public Y _4;
|
||||
|
||||
Tuple4(T _1, R _2, U _3, Y _4) {
|
||||
this._1 = _1;
|
||||
this._2 = _2;
|
||||
this._3 = _3;
|
||||
this._4 = _4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the fields of the tuple with the given functions. Does not handle mapping errors.
|
||||
* @return a mapped object from the original tuple
|
||||
*/
|
||||
public <A> A map(Function4<T, R, U,Y, A> tuppleMapper) {
|
||||
return tuppleMapper.apply(_1, _2, _3, _4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new tuple with each field being the result of applying the corresponding mapper function.
|
||||
* @param f1Mapper the mapping function to apply to the first field
|
||||
* @param f2Mapper the mapping function to apply to the second field
|
||||
* @param f3Mapper the mapping function to apply to the third field
|
||||
* @param f4Mapper the mapping function to apply to the fourth field
|
||||
* @return a new tuple with the mapper functions applied
|
||||
* @param <A> the new return type for the first field
|
||||
* @param <S> the new return type for the second field
|
||||
* @param <D> the new return type for the third field
|
||||
* @param <F> the new return type for the fourth field
|
||||
*/
|
||||
public <A,S,D,F> Tuple4<A,S,D,F> map(Function<T,A> f1Mapper,
|
||||
Function<R,S> f2Mapper,
|
||||
Function<U,D> f3Mapper,
|
||||
Function<Y,F> f4Mapper) {
|
||||
return of(f1Mapper.apply(_1), f2Mapper.apply(_2), f3Mapper.apply(_3), f4Mapper.apply(_4));
|
||||
}
|
||||
|
||||
public Tuple3<R,U,Y> remove1() {
|
||||
return of(_2, _3, _4);
|
||||
}
|
||||
public Tuple3<T,U,Y> remove2() {
|
||||
return of(_1, _3, _4);
|
||||
}
|
||||
public Tuple3<T,R,Y> remove3() {
|
||||
return of(_1, _2, _4);
|
||||
}
|
||||
public Tuple3<T,R,U> remove4() {
|
||||
return of(_1, _2, _3);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package ninja.thefirearchmage.utils.functions;
|
||||
|
||||
/**
|
||||
* A functional interface that allows to return a value and declare that it may throw exceptions, to be used
|
||||
* for Try.
|
||||
* @param <T> the return value
|
||||
* @param <X> the type of exception
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedCallable<T, X extends Throwable> {
|
||||
T call() throws X;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package ninja.thefirearchmage.utils.functions;
|
||||
|
||||
/**
|
||||
* Allows methods that throws exceptions to be used in lambda chains.
|
||||
* @param <T> input type
|
||||
* @param <R> result type
|
||||
* @param <X> exception type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedMapper<T,R,X extends Throwable> {
|
||||
R map(T input) throws X;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ninja.thefirearchmage.utils.functions;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CheckedRunnable<X extends Throwable> {
|
||||
void run() throws X;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ninja.thefirearchmage.utils.functions;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Function3<T1,T2,T3,R> {
|
||||
R apply(T1 arg1, T2 arg2, T3 arg3);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ninja.thefirearchmage.utils.functions;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Function4<T1,T2,T3,T4,R> {
|
||||
R apply(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
|
||||
}
|
Loading…
Add table
Reference in a new issue