Skip to content

kodlan/elegant-java-snippets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

47 Commits
Β 
Β 
Β 
Β 

Repository files navigation

elegant-java-snippets

Java code snippets

Table of contents

πŸ”’ Arrays

Β Β Β Initializa a stream and then:

🍱 Sets

Β Β Β Common for collections:

Β Β Β Initializa a stream and then:

πŸ—‚ Lists

Β Β Β Common for collections:

Β Β Β Initializa a stream and then:

πŸ“¦ Collections

Β Β Β Initializa a stream and then:

🎏 Streams
πŸ—Ί Maps
πŸ‘¨β€πŸ‘¦ Min Heap, Max Heap, PriorityQueue
πŸ“ Comparators
➑ Functional

πŸ”’ Arrays

Array initialization

int[] array = { 1, 2, 3, 4, 5 };


⬆ back to contents

Fill the array

int[] array = new int[10];

Arrays.fill(array, 1);     // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Arrays.fill(ar, 1, 5, 2);  // [1, 2, 2, 2, 2, 1, 1, 1, 1, 1]


⬆ back to contents

Output the array

System.out.println(Arrays.toString(array));     // int[] for example
System.out.println(Arrays.deepToString(array)); // int[][] for example


⬆ back to contents

Sort the array

Arrays.sort(array);

Arrays.sort(arr, 1, 5); // sort only elements 1-5

Arrays.sort(arr, Collections.reverseOrder()); 


⬆ back to contents

Concatenate two arrays

public static <T> T[] arrayConcat(T[] first, T[] second) {
    var result = Arrays.copyOf(first, first.length + second.length);
    System.arraycopy(second, 0, result, first.length, second.length);
    return result;
}

or using streams:

public static <T> T[] concat(T[] first, T[] second) {
    return Stream.concat(
            Stream.of(first),
            Stream.of(second)
    ).toArray(i -> (T[]) Arrays.copyOf(new Object[0], i, first.getClass()));
}


⬆ back to contents

🍱 Sets

Set initialization

Using add():

Set<Integer> set = new HashSet<Integer>() {{
    add(1);
    add(2);
    add(3);
}};

Using other collections:

Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3));

Using Collections.addAll:

Set<Integer> mutableSet;
Collections.addAll(mutableSet = new HashSet<>(), 1, 2, 3);


⬆ back to contents

Unmodifiable set initialization

Set<Integer> set = Set.of(1, 2, 3);
    
set = Collections.singleton(1);

set = Collections.emptySet();


⬆ back to contents

πŸ—‚ Lists

List initialization

Using add():

List<Integer> list = new LinkedList<>() {{
  add(1);
  add(3);
}};

Using Arrays.asList():

String[] array = new String[] {"one", "two", "three"};
List<String> list = new ArrayList<>(Arrays.asList(array));  // not for int[] due to cannot infer type arguments for java.util.ArrayList<>

// for int[]:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

About Arrays.asList:

Creates a wrapper that implements List<Integer>, which makes the original 
array available as a list. Nothing is copied. Operations are propageted to the
original array. Adding or removing elements from the list are not allowed.
List returned is not usual ArrayList.


⬆ back to contents

Unmodifiable list initialization

List<Integer> list = Arrays.asList(1, 2, 3); // can be modified. add, remove is not supported

list = Collections.unmodifiableList(Arrays.asList(1, 2, 3));

list = List.of(1, 2, 3);

list = Collections.singletonList(1);

list = Collections.emptyList();


⬆ back to contents

List output

System.out.println(list);  // toString()


⬆ back to contents

List sort

Using Collections

Collections.sort(list);
Collections.sort(list, Collections.reverseOrder());

Collections.sort(list, (a, b) -> Integer.compare(a.start, b.start))
Collections.sort(list, Comparator.comparingInt(a -> a.start))

Using List.sort(Comparator<? super E> c):

list.sort(Comparator.naturalOrder());
list.sort(Comparator.reverseOrder());

list.sort(Collections.reverseOrder());

Using Stream.sorted():

List<String> sorted = list.stream()
    .sorted()
    .collect(Collectors.toList());

sorted = list.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());


⬆ back to contents

Binary search

int index = Collections.binarySearch(list, key);
index = Collections.binarySearch(list, key, comparator);


⬆ back to contents

Reverse list

Collections.reverse(list);


⬆ back to contents

Rotate list

Collections.rotate(list, distance);

for example list 1, 2, 3, 4, 5 rotated by distance 2 becomes 4, 5, 1, 2, 3. Can be used with negative values.
⬆ back to contents

πŸ“¦ Collections

Min, max elements

Collections.min(collection);
Collections.min(collection, comparator);


Collections.max(collection);
Collections.max(collection, comparator);


⬆ back to contents

🎏 Streams

Stream initialization

From a Collection

Stream<Integer> stream = set.stream();
stream = list.stream();
stream = queue.stream();

Using Stream.of(T…t):

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);

From array:

Stream<T> streamOfArray = Arrays.stream(array);
Stream<T> streamOfArray = Stream.of(array);

Empty stream:

Stream<String> emptyStream = Stream.empty();

Using Stream.builder():

Stream.Builder<String> builder = Stream.builder();
  
Stream<String> stream = builder.add("a")
    .add("b")
    .add("c")
    .build();

Using Stream.iterate():

// The iterate() method returns an infinite sequential ordered Stream
// produced by iterative application of a function f to an initial element seed.
Stream.iterate(seedValue, (Integer n) -> n * n)

Using Stream.generate():

Stream.generate(Math::random)


⬆ back to contents

All intermediate and terminal operations

Operation Type Return type Fn interface Fn descriptor
filter Intermediate Stream<T> Predicate<T> T -> boolean
distinct Intermediate (stateful-unbounded) Stream<T>
takeWhile Intermediate Stream<T> Predicate<T> T -> boolean
dropWhile Intermediate Stream<T> Predicate<T> T -> boolean
skip Intermediate (stateful-bounded) Stream<T> long
limit Intermediate (stateful-bounded) Stream<T> long
map Intermediate Stream<R> Function<T, R> T -> R
mapToDouble Intermediate DoubleStream ToDoubleFunction<T> T -> DoubleStream
mapToInt Intermediate IntStream ToIntFunction<T> T -> IntStream
mapToLong Intermediate LongStream ToLongFunction<T> T -> LongStream
flatMap Intermediate Stream<R> Function<T, Stream<R>> T -> Stream<R>
flatMapToDouble Intermediate DoubleStream Function<T, DoubleStream> T -> DoubleStream
flatMapToInt Intermediate IntStream Function<T, IntStream> T -> IntStream
flatMapToLong Intermediate LongStream Function<T, LongStream> T -> LongStream
sorted Intermediate (stateful-unbounded) Stream<T> Comparator<T> (T, T) -> int
anyMatch Terminal boolean Predicate<T> T -> boolean
noneMatch Terminal boolean Predicate<T> T -> boolean
allMatch Terminal boolean Predicate<T> T -> boolean
findAny Terminal Optional<T>
findFirst Terminal Optional<T>
forEach Terminal void Consumer<T> T -> void
collect Terminal R Collector<T, A, R>
reduce Terminal (stateful-bounded) Optional<T> BinaryOperaror<T> (T, T) -> T
count Terminal long
max Terminal Optional<T> Comparator<T>
min Terminal Optional<T> Comparator<T>

bounded - the internal state is of bounded size no matter how many elements are in the stream being processed

unbounded - for example, sorting requires all the elements to be buffered before single item can be added to the output stream.

stateful - requires processing of all the elements in the stream (the largest number for example)


⬆ back to contents

IntStream methods (not all of them)

Method Signature
allMatch boolean allMatch(IntPredicate predicate)
anyMatch boolean anyMatch(IntPredicate predicate)
asDoubleStream DoubleStream asDoubleStream()
asLongStream LongStream asLongStream()
average OptionalDouble average()
boxed Stream<Integer> boxed()
count long count()
distinct IntStream distinct()
filter IntStream filter(IntPredicate predicate)
findAny OptionalInt findAny()
findFirst OptionalInt findFirst()
flatMap IntStream flatMap(IntFunction<? extends IntStream> mapper)
forEach IntStream forEach(IntConsumer action)
limit IntStream limit(long maxSize)
map IntStream map(IntUnaryOperator mapper)
mapToDouble DoubleStream mapToDouble(IntToDoubleFunction mapper)
mapToLong LongStream mapToLong(IntToLongFunction mapper)
mapToObj <U> Stream<U> mapToObj(IntFunction<? extends U> mapper)
max OptionalInt max()
min OptionalInt min()
noneMatch boolean noneMatch(IntPredicate predicate)
parallel IntStream parallel()
reduce OptionalInt reduce(IntBinaryOperator op)
reduce OptionalInt reduce(int identity, IntBinaryOperator op)
sequential IntStream sequential()
skip IntStream skip(long n)
sorted IntStream sorted()
sum int sum()
toArray int[] toArray()

Static initialization method:

Method Signature
concat static IntStream concat(IntStream a, IntStream b)
empty() static IntStream empty()
generate static IntStream generate(IntSupplier s)
iterate static IntStream iterate(int seed, IntUnaryOperator f)
of static IntStream of(int... values)
of static IntStream of(int t)
range static IntStream range(int startInclusive, int endExclusive)
rangeClosed static IntStream rangeClosed(int startInclusive, int endInclusive)


⬆ back to contents

LongStream methods (not all of them)

Method Signature
allMatch boolean allMatch(LongPredicate predicate)
anyMatch boolean anyMatch(LongPredicate predicate)
asDoubleStream DoubleStream asDoubleStream()
average OptionalDouble average()
boxed Stream<Long> boxed()
count long count()
distinct LongStream distinct()
filter LongStream filter(LongPredicate predicate)
findAny OptionalLong findAny()
findFirst OptionalLong findFirst()
flatMap LongStream flatMap(LongFunction<? extends LongStream> mapper)
forEach LongStream forEach(LongConsumer action)
limit LongStream limit(long maxSize)
map LongStream map(LongUnaryOperator mapper)
mapToDouble DoubleStream mapToDouble(LongToDoubleFunction mapper)
mapToInt IntStream mapToLong(LongToIntFunction mapper)
mapToObj <U> Stream<U> mapToObj(IntFunction<? extends U> mapper)
max OptionalLong max()
min OptionalLong min()
noneMatch boolean noneMatch(LongPredicate predicate)
parallel LongStream parallel()
reduce OptionalLong reduce(LongBinaryOperator op)
reduce OptionalLong reduce(long identity, LongBinaryOperator op)
sequential LongStream sequential()
skip LongStream skip(long n)
sorted LongStream sorted()
sum long sum()
toArray long[] toArray()

Static initialization method:

Method Signature
concat static LongStream concat(LongStream a, LongStream b)
empty() static LongStream empty()
generate static LongStream generate(LongSupplier s)
iterate static LongStream iterate(int seed, LongUnaryOperator f)
of static LongStream of(long... values)
of static LongStream of(long t)
range static LongStream range(long startInclusive, long endExclusive)
rangeClosed static LongStream rangeClosed(long startInclusive, long endInclusive)


⬆ back to contents

DoubleStream methods (not all of them)

Method Signature
allMatch boolean allMatch(DoublePredicate predicate)
anyMatch boolean anyMatch(DoublePredicate predicate)
average OptionalDouble average()
boxed Stream<Double> boxed()
count Double count()
distinct DoubleStream distinct()
filter DoubleStream filter(DoublePredicate predicate)
findAny OptionalDouble findAny()
findFirst OptionalDouble findFirst()
flatMap DoubleStream flatMap(DoubleFunction<? extends DoubleStream> mapper)
forEach DoubleStream forEach(DoubleConsumer action)
limit DoubleStream limit(double maxSize)
map DoubleStream map(DoubleUnaryOperator mapper)
mapToLong DoubleStream mapToLong(DoubleToLongFunction mapper)
mapToInt IntStream mapToDouble(DoubleToIntFunction mapper)
mapToObj <U> Stream<U> mapToObj(IntFunction<? extends U> mapper)
max OptionalDouble max()
min OptionalDouble min()
noneMatch boolean noneMatch(DoublePredicate predicate)
parallel DoubleStream parallel()
reduce OptionalDouble reduce(DoubleBinaryOperator op)
reduce OptionalDouble reduce(double identity, DoubleBinaryOperator op)
sequential DoubleStream sequential()
skip DoubleStream skip(long n)
sorted DoubleStream sorted()
sum double sum()
toArray double[] toArray()

Static initialization method:

Method Signature
concat static DoubleStream concat(DoubleStream a, DoubleStream b)
empty() static DoubleStream empty()
generate static DoubleStream generate(DoubleSupplier s)
iterate static DoubleStream iterate(int seed, DoubleUnaryOperator f)
of static DoubleStream of(long... values)
of static DoubleStream of(long t)
range static DoubleStream range(long startInclusive, long endExclusive)
rangeClosed static DoubleStream rangeClosed(long startInclusive, long endInclusive)


⬆ back to contents

Collectors methods

Collector<T,A,R>:

T - the type of input elements to the reduction operation

A - the mutable accumulation type of the reduction operation (often hidden as an implementation detail)

R - the result type of the reduction operation

Return type Method (concurent methods skipped, all methods are static)
Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper)
Collector<T,?,Double> averagingInt(ToIntFunction<? super T> mapper)
Collector<T,?,Double> averagingLong(ToLongFunction<? super T> mapper)
Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher)
Collector<T,?,Long> counting()
Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
Collector<T,?,M extends Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
Collector<CharSequence,?,String> joining()
Collector<CharSequence,?,String> joining(CharSequence delimiter)
Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)
Collector<T,?,Optional<T>> maxBy(Comparator<? super T> comparator)
Collector<T,?,Optional<T>> minBy(Comparator<? super T> comparator)
Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)
Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream)
Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op)
Collector<T,?,T> reducing(T identity, BinaryOperator<T> op)
Collector<T,?,U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
Collector<T,?,DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
Collector<T,?,IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
Collector<T,?,LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
Collector<T,?,Double> summingDouble(ToDoubleFunction<? super T> mapper)
Collector<T,?,Integer> summingInt(ToIntFunction<? super T> mapper)
Collector<T,?,Long> summingLong(ToLongFunction<? super T> mapper)
Collector<T,?,C extends Collection<T>> toCollection(Supplier<C> collectionFactory)
Collector<T,?,List<T>> toList()
Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)
Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction)
Collector<T,?,M extends Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
Collector<T,?,Set<T>> toSet()


⬆ back to contents

Check if all elements are equal

return intStream.distinct().count() == 1;


⬆ back to contents

Find maximum integer from the stream

return intStream.reduce(Integer.MIN_VALUE, Integer::max);


⬆ back to contents

Count occurrences of some number in a stream

return intStream
    .filter(number -> number == value)
    .count();


⬆ back to contents

Get distinct values

return intStream.distinct().toArray();


⬆ back to contents

All pairs of numbers from two lists

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(4, 5, 6);

List<int[]> pairs = numbers1.stream()
    .flatMap(i -> numbers2.stream()
        .map(j -> new int[] {i, j}))
    .collect(toList());


⬆ back to contents

πŸ—Ί Maps

Map initialization

Using add():

Map<String, String> doubleBraceMap  = new HashMap<String, String>() {{
    put("key1", "value1");
    put("key2", "value2");
}};


⬆ back to contents

Unmodifiable map initialization

Map<String, String> map = Map.of();

map = Map.of("key", "value");

map = Map.of("key1","value1", "key2", "value2")
    
// ... upto 10 key-value pairs

map = Map.ofEntries(
    new AbstractMap.SimpleEntry<>("key1", "value1"),
    new AbstractMap.SimpleEntry<>("key2", "value2"),
    new AbstractMap.SimpleEntry<>("key3", "value3"),
    new AbstractMap.SimpleEntry<>("key4", "value4")
);
    
map = Collections.singletonMap("key", "value");

map = Collections.emptyMap();


⬆ back to contents

Sort map by keys

Using TreeMap:

Map<String, String> sortedMap = new TreeMap<>(map);

Using comparingByKey:

List<Entry<String, String>> entries = new ArrayList<>(map.entrySet());
entries.sort(Map.Entry.comparingByKey());
// or reversed
entries.sort(Map.Entry.<String, String>comparingByKey().reversed());


⬆ back to contents

Sort map by values

Using comparingByValue:

List<Entry<String, String>> entries = new ArrayList<>(map.entrySet());
entries.sort(Map.Entry.comparingByValue());
// or reversed
entries.sort(Map.Entry.<String, String>comparingByValue().reversed());


⬆ back to contents

Merge two maps

Using Map.putAll():

map1.putAll(map2); // duplicate values will be overridden by values from map2

Using Map.merge()

map2.forEach((key, value) ->
    map1.merge(key, value, (v1, v2) -> v1 + v2));
// If the specified key is not already associated with a value 
// or is associated with null, the Map.merge() method associates 
// it with the given non-null value.
    
// Otherwise, the Map.merge() method replaces the value with the 
// results of the given remapping function. If the result of the 
// remapping function is null, it removes the key altogether.

Using Stream.concat():

Stream<Map.Entry<String, Integer>> combined = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream());
Map<String, Integer> merged = combined.collect(
    Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// in case of duplicate entries this throw an IllegalStateException exception.

// To handle duplicate entries pass a merger function as a third parameter to the collector:
Map<String, Integer> merged = combined.collect(
    Collectors.toMap(Map.Entry::getKey,
           Map.Entry::getValue,
           (v1, v2) -> v1 + v2));


⬆ back to contents

πŸ‘¨β€πŸ‘¦ Min Heap, Max Heap, PriorityQueue

Min Heap initialization

PriorityQueue<Integer> minHeap = new PriorityQueue<>();

PriorityQueue<Integer> minHeap = new PriorityQueue<>((a, b) -> a - b);

PriorityQueue<Integer> minHeap = new PriorityQueue<>(Comparator.comparingInt(a -> a));

PriorityQueue<Integer> minHeap = new PriorityQueue<>(Comparator.naturalOrder());


⬆ back to contents

Max Heap initialization

PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);

PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());


⬆ back to contents

πŸ“ Comparators

Comparator.comparing

Comparator.comparing(Employee::getName).reversed();
Comparator.comparing(Employee::getName, comparator).reversed();

// or 
Comparator.comparingInt(Employee::getIntValue);
Comparator.comparingDouble(Employee::getDoubleValue);
Comparator.comparingLong(Employee::getLongValue);


⬆ back to contents

Comparator reversing

Comparator.comparing(Employee::getName).reversed();


⬆ back to contents

Comparator grouping

Comparator<Employee> groupByComparator = Comparator.comparing(Employee::getName)
    .thenComparing(Employee::getDob)
    .thenComparing(Employee::getId);

// also
Comparator.thenComparingInt(Employee::getIntValue);
Comparator.thenComparingDouble(Employee::getDoubleValue);
Comparator.thenComparingLong(Employee::getLongValue);


⬆ back to contents

Handling null values

Comparator<Employee> comparing =
    comparing(Employee::getName, nullsFirst(naturalOrder()));

Comparator<Employee> comparing =
    comparing(Employee::getName, nullsLast(naturalOrder()));


⬆ back to contents

➑ Functional

Common functional interfaces

lamdba Functional interface Other interfaces
T -> boolean Precdicate<T> IntPredicate, LongPredicate, DoublePredicate
T -> void Consumer<T> IntCosumer, LongConsumer, DoubleConsumer
T -> R Function<T, R> IntFunction<R>, IntToDoubleFunctiona, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunctiction, DoubleFunction<R>, DoubleToIntFunction, DoubleToLongFunction, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
() -> T Supplier<T> BooleanSuppier, IntSupplier, LongSupplier, DoubleSupplier
T -> T UnaryOperator<T> IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperartor
(T, T) -> T BinaryOperator<T> IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
(T, U) -> boolean BiPredicate<T, U>
(T, U) -> void BiConsumer<T, U> ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
(T, U) -> R BiFunction<T, U, R> ToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U>


⬆ back to contents

Partial initialization

public interface TriFunction<T, U, V, R> {
  R apply(T t, U u, V v);
}

TriFunction<Integer, Integer, Integer, Integer> add = (x, y, z) -> x + y + z;

Function<Integer, BiFunction<Integer, Integer, Integer>> addPartial = (x) -> (y, z) -> add.apply(x, y, z);

BiFunction<Integer, Integer, Integer> add5 = addPartial.apply(5);

add5.apply(6, 7);


⬆ back to contents

Handling exceptions when interface doesn't throw exception

For example in case of Runnable:

@FunctionalInterface
public interface Runnable {
  public abstract void run();
}

Create a Thread using lambda that throws exception:

new Thread(() -> {
      try {
        methodThatThrowsException();
      } catch (Exception e) {
        // do something
      }
    });

To fix the try/catch in lambda, add ThrowingRunnable interface:

@FunctionalInterface
public interface ThrowingRunnable<E extends Exception> {

  void run() throws E;

  static <E extends Exception> Runnable handleThrowingRunnable(
      ThrowingRunnable<E> throwingRunnable, Consumer<Exception> handler) {

    return () -> {
      try {
        throwingRunnable.run();
      } catch (Exception ex) {
        handler.accept(ex);
      }
    };
  }
}

And then:

new Thread(
    ThrowingRunnable.handleThrowingRunnable(
        this::methodThatThrowsException,
        exception -> logger.info("Something happened", exception))));


⬆ back to contents

Constructor references

Supplier<Apple> appleSupplier = Apple::new;
Apple apple = appleSupplier.get();

// or if constructor has arguments:

BiFunction<Color, Integer, Apple> supplier = Apple::new;
Apple apple = supplier.apply(GREEN, 110);


⬆ back to contents

About

Java code snippets

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published