package de.axone.data.weighted;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.Random;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;
public abstract class AbstractWeightedCollection<W extends WeightedCollection<W, T>, T>
implements WeightedCollection<W, T>, Serializable {
private static final long serialVersionUID = 1L;
@SuppressWarnings( "unchecked" )
private final W self = (W) this;
private Map<T,T> map = new HashMap<>();
private static final int CONSTANT_SEED =
(new Random()).nextInt();
private final Supplier<W> supplier;
private final Weighter<T> weighter;
private final Cloner<T> cloner;
private final Combiner<T> combiner;
private final ItemCollector<T,W> collector;
public AbstractWeightedCollection( Supplier<W> supplier, Weighter<T> weighter, Cloner<T> cloner, Combiner<T> merger ){
this.supplier = supplier;
this.weighter = weighter;
this.cloner = cloner;
this.collector = new ItemCollector<>( supplier );
this.combiner = merger;
}
public AbstractWeightedCollection( Supplier<W> supplier, Weighter<T> weighter, Cloner<T> cloner ){
this( supplier, weighter, cloner,
(t1,t2) -> cloner.clone( t1, weighter.weight( t1 ) + weighter.weight( t2 ) ) );
}
protected Supplier<W> supplier(){
return supplier;
}
public Collector<T,W,W> collector(){
return collector;
}
public Weighter<T> weighter(){
return weighter;
}
public Cloner<T> cloner(){
return cloner;
}
public Combiner<T> combiner(){
return combiner;
}
@Override
public W add( T item ) {
if( item == null ) return self;
if( map.containsKey( item ) ){
T old = map.get( item );
map.remove( old );
T merged = combiner.combine( old, item );
map.put( merged, merged );
} else {
map.put( item, item );
}
return self;
}
@Override
public W addIf( T item, Predicate<T> condition ) {
if( condition.test( item ) ) add( item );
return self;
}
@Override
public W copy(){
W result = supplier.get();
result.addAll( map.keySet() );
return result;
}
@SuppressWarnings( "unchecked" )
public W addAll( T ... items ) {
if( items == null || items.length == 0 ) return self;
for( T item : items ) add( item );
return self;
}
@Override
public W addAll( Collection<? extends T> items ){
if( items == null || items.size() == 0 ) return self;
for( T item : items ) add( item );
return self;
}
@Override
public W addAll( W items ){
if( items == null || items.size() == 0 ) return self;
for( T item : items ) add( item );
return self;
}
@Override
public W removeAll( W items ) {
if( items == null || items.size() == 0 ) return self;
for( Object item : items ) map.remove( item );
return self;
}
@Override
public W retainAll( W items ) {
if( items == null || items.size() == 0 ) return supplier.get();
for( Iterator<T> it = iterator(); it.hasNext(); ){
T item = it.next();
if( ! items.contains( item ) ) it.remove();
}
return self;
}
@Override
public W distinct( W items ) {
for( Iterator<T> it = iterator(); it.hasNext(); ){
T item = it.next();
if( items.contains( item ) ) it.remove();
}
return self;
}
/**
* @return the stored version of the given item
* @param item
*/
public T get( T item ){
return map.get( item );
}
@Override
public int size() {
return map.size();
}
@Override
public boolean contains( Object item ) {
return map.containsKey( item );
}
@Override
public Stream<T> stream(){
return map.keySet().stream();
}
@Override
public Optional<T> findFirst( Predicate<? super T> predicate ){
return stream().filter( predicate )
.findFirst();
}
@Override
public Optional<T> findFirst( String name, Function<T, String> mapper ){
return stream()
.filter( t -> name.equals( mapper.apply( t ) ) )
.findFirst()
;
}
@Override
public double weight(){
return weightStream()
.sum();
}
@Override
public double maxWeight(){
OptionalDouble max = weightStream().max();
return max.isPresent() ? max.getAsDouble() : 0;
}
@Override
public double avgWeight(){
OptionalDouble avg = weightStream().average();
return avg.isPresent() ? avg.getAsDouble() : 0;
}
private DoubleStream weightStream() {
return stream()
.mapToDouble( weighter::weight )
;
}
/*
private DoubleSummaryStatistics metrics() {
return weightStream()
.summaryStatistics()
;
}
*/
@Override
public int hashCode() {
return map.hashCode();
}
@Override
public boolean equals( Object obj ) {
if( this == obj ) return true;
if( obj == null ) return false;
if( !( obj instanceof AbstractWeightedCollection ) ) return false;
@SuppressWarnings( "rawtypes" )
AbstractWeightedCollection other = (AbstractWeightedCollection) obj;
return map.equals( other.map );
}
@Override
public Iterator<T> iterator() {
return map.keySet().iterator();
}
@Override
public List<T> asList() {
return new ArrayList<T>( map.keySet() );
}
@Override
public List<T> sorted() {
return sorted( new WeightComparator<>( weighter ) );
}
@Override
public List<T> sorted( Comparator<T> comparator ) {
List<T> result = asList();
Collections.sort( result, comparator );
return result;
}
protected static void shuffle( List<?> list ){
Collections.shuffle( list );
}
protected static void shuffleStable( List<?> list ){
Collections.shuffle( list, new Random( CONSTANT_SEED ) );
}
@Override
public List<T> shuffled() {
List<T> result = asList();
shuffle( result );
return result;
}
@Override
public List<T> shuffledStable() {
List<T> result = asList();
shuffleStable( result );
return result;
}
@Override
public Set<T> asSet() {
return map.keySet();
}
@Override
public Stream<T> sortedStream() {
return stream()
.sorted( new WeightComparator<>( weighter ) )
;
}
public Stream<T> normalizedBestStream( int maxAmount ){
if( size() == 0 || maxAmount <= 0 ) return Stream.empty();
return normalizedStream()
.sorted( new WeightComparator<>( weighter ) )
.limit( maxAmount )
;
}
public List<T> normalizedBest( int maxAmount ){
return normalizedBestStream( maxAmount )
.collect( Collectors.toList() )
;
}
@Override
public Stream<T> bestStream( int maxAmount, Predicate<T> filter ) {
if( size() == 0 || maxAmount <= 0 ) return Stream.empty();
return stream()
.sorted( new WeightComparator<>( weighter::weight ) )
.filter( filter )
.limit( maxAmount )
;
}
@Override
public W best( int maxAmount, Predicate<T> filter ){
return bestStream( maxAmount, filter )
.collect( collector() )
;
}
@Override
public Stream<T> bestStream( int maxAmount ) {
if( size() == 0 || maxAmount <= 0 ) return Stream.empty();
return stream()
.sorted( new WeightComparator<>( weighter::weight ) )
.limit( maxAmount )
;
}
@Override
public W best( int maxAmount ){
return bestStream( maxAmount )
.collect( collector() )
;
}
@Override
public Stream<T> filteredStream( Predicate<T> filter ) {
if( size() == 0 ) return Stream.empty();
return stream()
.filter( filter )
;
}
@Override
public W filter( Predicate<T> filter ) {
return filteredStream( filter )
.collect( collector() )
;
}
@Override
public Stream<T> normalizedStream() {
if( size() == 0 ) return Stream.empty();
double max = maxWeight();
double avg = avgWeight();
return stream()
.map( (item) -> cloner.clone( item, normalize( item, max, avg ) ) )
;
}
/**
* Normalise so that values near the average value end up
* with a 0.5 value.
*
* This gives a "visually more pleasing" distribution of the values.
*
* This prevents that for values like 1,2,5,7,100 (avg: 23)
* we and up with a distribution like: .01,.02,.05,.07,1
* but we get with: 0.02, 0.04, 0.11, 0.15, 1
*
* @param item
* @param max
* @param avg
* @return
*/
private double normalize( T item, double max, double avg ){
double weight = weighter.weight( item ),
result;
if( weight <= avg ){
result = (weight/avg) / 2.0;
} else {
result = weight/max;
}
return result;
}
@Override
public W normalized(){
return normalizedStream()
.collect( collector() )
;
}
/**
* Return a Set containing all elements combined from A AND B
*
* @param a First list to combine
* @param b Second list to combine
* @return A ∪ B
*/
public static <T, W extends AbstractWeightedCollection<W,T>> W join( W a, W b ){
if( a == null ) return b;
if( b == null ) return a;
W result = a.supplier().get();
result.addAll( a );
result.addAll( b );
return result;
}
/**
* Return a Set containing all elements combined from A AND B
*
* @param a First list to combine
* @param b Second list to combine
* @return A ∪ B
*/
public static <T, W extends AbstractWeightedCollection<W,T>> W override( W a, W b ){
if( a == null ) return b;
if( b == null ) return a;
W result = a.supplier().get();
result.addAll( a );
result.removeAll( b );
result.addAll( b );
return result;
}
/**
* Return a Set containing the elements which are in both A and B
*
* @see #join
*
* @param a First list to combine
* @param b Second list to combine
* @return A ∩ B
*/
public static <T, W extends AbstractWeightedCollection<W,T>> W intersection( W a, W b ){
if( a == null ) return null;
if( b == null ) return null;
W result = a.supplier().get();
result.addAll( a );
result.retainAll( b );
return result;
}
/**
* Return a Set containing elements from A which are not in B
*
* @see #join
*
* @param a First list to combine
* @param b Second list to combine
* @return A \ B
*/
public static <T, W extends AbstractWeightedCollection<W,T>> W complement( W a, W b ){
if( a == null ) return null;
if( b == null ) return a;
W result = a.supplier().get();
result.addAll( a );
result.removeAll( b );
return result;
}
@Override
public String toString(){
return map.keySet().toString();
}
public WeightComparator<T> weightComparator(){
return new WeightComparator<>( this.weighter );
}
public static class WeightComparator<T>
implements Comparator<T>, Serializable {
private final Weighter<T> weighter;
public WeightComparator( Weighter<T> weighter ){
this.weighter = weighter;
}
@Override
public int compare( T o1, T o2 ) {
double diff = weighter.weight( o2 ) - weighter.weight( o1 );
return (diff > 0) ? 1 : ((diff < 0) ? -1 : 0);
}
}
public static class ItemCollector<T,W extends WeightedCollection<W,T>>
implements Collector<T, W, W>, Serializable {
private final Supplier<W> supplier;
public ItemCollector( Supplier<W> supplier ){
this.supplier = supplier;
}
@Override
public Supplier<W> supplier() {
return supplier;
}
@Override
public BiConsumer<W, T> accumulator() {
return ( items, it ) -> items.add( it );
}
@Override
public BinaryOperator<W> combiner() {
return ( items1, items2 ) -> items1.addAll( items2 );
}
@Override
public Function<W, W> finisher() {
return ( items ) -> items;
}
@Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
return EnumSet.of(
Characteristics.UNORDERED,
Characteristics.IDENTITY_FINISH );
}
}
}