~/home of geeks

Collection Randomization

· 522 Wörter · 3 Minute(n) Lesedauer

thousands of dices rolling on a diceball table, photography, shallow depth

Vor einiger Zeit brauchte ich aus einer Menge an Daten in einer Collection zufällige Elemente. In einigen Fällen sollten es eine feste Anzahl (z. B. 13 Stück), in anderen ein prozentualer Anteil (z. B. 25%) der Daten sein, die ermittelt und verarbeitet werden sollten.

Generell könnte man die Aufgabe dadurch lösen, dass man mit Collections.shuffle(myData) eine zufällige Anordnung der Elemente erzeugt und dann die benötigte Anzahl der Elemente entnimmt. Damit ich aber nicht ständig Prozente im Code umrechnen muss, schrieb ich eine Abstraktion auf die Menge der auszuwählenden Einträge:

/**
 * Abstraction of a Unit.
 * Whatever unit it has, it has to be convertible to
 * an absolute amount.
 */
public interface Unit {
    int getAmount(final int totalCount);
}

Hierzu dann zwei Implementierungen, eine für absolute Angaben und eine für prozentuale Angaben:

/**
 * If the given amount is greater than the existing amount, the existing amount is used.
 */
public class Absolute implements Unit {
    private final int amount;

    /**
     * Absolute Anzahl der zu liefernden Daten.
     *
     * @param amount
     */
    public Absolute(final int amount) {
        if (amount < 1) {
            throw new IllegalArgumentException("Amount must be a value greater 0");
        }
        this.amount = amount;
    }

    @Override
    public int getAmount(final int totalCount) {
        return Math.min(amount, totalCount);
    }

    public static Absolute amount(int amount){
        return new Absolute(amount);
    }
}
/**
 * Converts a relative percentual amount (1-100) to an absolute value.
 */
public class Percent implements Unit {
    private final int percent;

    /**
     * @param percent Ganzzahliger prozentualer Anteil, 1 - 100
     */
    public Percent(final int percent) {
        if (percent > 100 || percent < 1) {
            throw new IllegalArgumentException("Percent must be a value between 1-100");
        }
        this.percent = percent;
    }

    @Override
    public int getAmount(final int totalCount) {
        final int tmpAmount = Math.round(Math.round((totalCount / 100D) * (percent)));
        final int amount = Math.max(Math.min(tmpAmount, 100), 1);
        return amount;
    }

    public static Percent amount(int amount) {
        return new Percent(amount);
    }
}

Die statischen Factory-Methoden amount(int) sollten den Code lediglich etwas lesbarer machen, wenn es Absolute.amount(30) statt new Absolute(30) heißt.

An dieser Stelle hatte ich auch für den gesamten Aufruf eine flüssige und lesbarere Form im Kopf. Der Aufruf sollte die Form Randomizer.on(myData).get(Absolute.amount(50)); haben. Um das zu realisieren, benötigte ich noch die eigentliche Klasse, welche die Daten ermittelt sowie die Hilfsklasse Randomizer für den Einstieg:

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ThreadLocalRandom;

public class RandomizerData<T> {
    private final Collection<T> data;

    public RandomizerData(Collection<T> data) {
        if (data == null) {
            throw new NullPointerException();
        }
        this.data = data;
    }

    /**
     * Returns the specified amount on each call.
     * Every call can return any of the elements in the data.
     *
     * @param unit
     * @return Collection of random elements
     */
    public Collection<T> get(final Unit unit) {
        final int maxAmount = unit.getAmount(data.size());
        if (data.size() <= maxAmount) {
            return data;
        }
        final ArrayList<T> randomData = new ArrayList<>(maxAmount);
        final ArrayList<T> workingCopy = new ArrayList<>(data);
        while (randomData.size() < maxAmount) {
            randomData.add(workingCopy.remove(ThreadLocalRandom.current().nextInt(workingCopy.size())));
        }
        return randomData;
    }
}
import java.util.Collection;

/**
 * Creates a random subset of the given Collection.
 * <p/>
 * Example:<br/>
 * <pre>Randomizer.on(myData).get(Absolute.amount(50));</pre>
 */
public final class Randomizer {
    public static final <T> RandomizerData<T> on(Collection<T> data){
        return new RandomizerData<>(data);
    }
}