/*
GeoGebra - Dynamic Mathematics for Everyone
http://www.geogebra.org
This file is part of GeoGebra.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation.
*/
package org.geogebra.common.kernel.algos;
import java.util.Arrays;
import java.util.TreeMap;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.arithmetic.MyDouble;
import org.geogebra.common.kernel.arithmetic.NumberValue;
import org.geogebra.common.kernel.commands.Commands;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoList;
import org.geogebra.common.kernel.geos.GeoNumeric;
/**
* Sort a list. Adapted from AlgoSort
*
* @author Michael Borcherds
* @version 2008-02-16
*/
public class AlgoMedian extends AlgoElement {
private GeoList inputList, freqList; // input
private GeoNumeric median; // output
private int size;
public AlgoMedian(Construction cons, String label, GeoList inputList) {
this(cons, inputList);
median.setLabel(label);
}
public AlgoMedian(Construction cons, String label, GeoList inputList,
GeoList freqList) {
this(cons, inputList, freqList);
median.setLabel(label);
}
public AlgoMedian(Construction cons, GeoList inputList) {
this(cons, inputList, null);
}
public AlgoMedian(Construction cons, GeoList inputList, GeoList freqList) {
super(cons);
this.inputList = inputList;
this.freqList = freqList;
median = new GeoNumeric(cons);
setInputOutput();
compute();
}
@Override
public Commands getClassName() {
return Commands.Median;
}
@Override
protected void setInputOutput() {
if (freqList == null) {
input = new GeoElement[1];
input[0] = inputList;
} else {
input = new GeoElement[2];
input[0] = inputList;
input[1] = freqList;
}
setOnlyOutput(median);
setDependencies(); // done by AlgoElement
}
public GeoNumeric getMedian() {
return median;
}
@Override
public final void compute() {
size = inputList.size();
if (!inputList.isDefined() || size == 0) {
median.setUndefined();
return;
}
// ========================================
// CASE 1: raw data
// ========================================
if (freqList == null) {
double[] sortList = new double[size];
// copy inputList into an array
for (int i = 0; i < size; i++) {
GeoElement geo = inputList.get(i);
if (geo instanceof NumberValue) {
NumberValue num = (NumberValue) geo;
sortList[i] = num.getDouble();
} else {
median.setUndefined();
return;
}
}
// do the sorting
Arrays.sort(sortList);
if (MyDouble.exactEqual(Math.floor((double) size / 2),
size / 2.0)) {
median.setValue(
(sortList[size / 2] + sortList[size / 2 - 1]) / 2);
} else {
median.setValue(sortList[(size - 1) / 2]);
}
}
// ================================================
// CASE 2: data from value/frequency lists
// ================================================
else if (inputList.size() == freqList.size()) {
if (!freqList.isDefined() || !(inputList.size() == freqList.size()
|| inputList.size() == freqList.size() + 1)) {
median.setUndefined();
return;
}
// check for bad frequency
for (int i = 0; i < freqList.size(); i++) {
if (!(freqList.get(i) instanceof NumberValue)
|| ((NumberValue) freqList.get(i)).getDouble() < 0) {
median.setUndefined();
return;
}
}
for (int i = 0; i < inputList.size(); i++) {
if (!(inputList.get(i) instanceof NumberValue)) {
median.setUndefined();
return;
}
}
// extract value and frequency arrays
Object[] obj = convertValueFreqListToArrays(inputList, freqList);
Double[] v = (Double[]) obj[0];
Integer[] f = (Integer[]) obj[1];
int n = (Integer) obj[2];
if (n == 0) {
median.setUndefined();
return;
}
// find the median
if (MyDouble.exactEqual(Math.floor((double) n / 2), n / 2.0)) {
median.setValue(
(getValueAt(n / 2, v, f) + getValueAt(n / 2 - 1, v, f))
/ 2);
} else {
median.setValue(getValueAt((n - 1) / 2, v, f));
}
}
// ============================================
// CASE 3: data grouped by class and frequency
// ============================================
else {
if (!freqList.isDefined() || !(inputList.size() == freqList.size()
|| inputList.size() == freqList.size() + 1)) {
median.setUndefined();
return;
}
// TODO ok to assume classes are valid? (sorted no gaps)
double n = 0;
for (int i = 0; i < freqList.size(); i++) {
n += ((NumberValue) freqList.get(i)).getDouble();
}
int cf = 0;
int f = 0;
double lowBound, highBound;
for (int i = 0; i < freqList.size(); i++) {
lowBound = ((NumberValue) inputList.get(i)).getDouble();
highBound = ((NumberValue) inputList.get(i + 1)).getDouble();
f = (int) ((NumberValue) freqList.get(i)).getDouble();
if (f < 0) {
median.setUndefined();
return;
}
if (cf + f >= n / 2) {
double width = highBound - lowBound;
// apply grouped median linear interpolation formula
median.setValue(lowBound + width * (n / 2 - cf) / f);
break;
}
cf += f;
}
}
}
/**
* Returns the value at an index position in a list of data constructed from
* a sorted list of values and frequencies
*
* @param index
* @param val
* @param freq
* @return
*/
public static Double getValueAt(int index, Double[] val, Integer[] freq) {
int cf = 0; // cumulative frequency
for (int i = 0; i < val.length; i++) {
cf += freq[i];
if (index < cf) {
return val[i];
}
}
return null;
}
public static Object[] convertValueFreqListToArrays(GeoList inputList,
GeoList freqList) {
// create a tree map to sort the value/frequency pairs
double val;
int freq;
TreeMap<Double, Integer> tm = new TreeMap<Double, Integer>();
for (int i = 0; i < freqList.size(); i++) {
val = ((NumberValue) inputList.get(i)).getDouble();
freq = (int) ((NumberValue) freqList.get(i)).getDouble();
// handle repeated values
if (tm.containsKey(val)) {
freq += tm.get(val);
}
tm.put(val, freq);
}
// extract value and frequency arrays
Double[] v = tm.keySet().toArray(new Double[tm.size()]);
Integer[] f = tm.values().toArray(new Integer[tm.size()]);
// get data size n
int n = 0;
for (int i = 0; i < f.length; i++) {
n += f[i]; // add the frequency for this value
}
// return the arrays in an Object array
Object[] obj = { v, f, n };
return obj;
}
}