/*
* Joinery -- Data frames for Java
* Copyright (c) 2014, 2015 IBM Corp.
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package joinery.impl.js;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import joinery.DataFrame;
import joinery.DataFrame.Aggregate;
import joinery.DataFrame.JoinType;
import joinery.DataFrame.KeyFunction;
import joinery.DataFrame.PlotType;
import joinery.DataFrame.Predicate;
import joinery.DataFrame.RowFunction;
import joinery.impl.Grouping;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
/*
* there are basically two options for assisting in method resolution
* from javascript:
* 1. wrap data frames in a subclass of NativeJavaObject and use get()
* to return the appropriate NativeJavaMethod object
* issues with this approach include:
* - needing to wrap both the object and the method definition
* - difficulty in determining the correct method from the argument types
* 2. wrap data frames in a custom scriptable object with unambiguous
* methods that delegates to the underlying data frame
* issues with this approach include:
* - need to redefine every bit of data frame functionality in terms of javascript
* - need to keep up to data as new methods are added to make them available in js
* after trying each, getting the correct method dynamically is not
* worth the effort so for now the more verbose delegate class solution
* wins out. Java8s nashorn interpreter appears to do a better job
* resolving methods (via dynalink) so hopefully this is short-lived
*/
public class DataFrameAdapter
extends ScriptableObject {
private static final long serialVersionUID = 1L;
private final DataFrame<Object> df;
private static final DataFrame<Object> EMPTY_DF = new DataFrame<>();
public DataFrameAdapter() {
this.df = EMPTY_DF;
}
public DataFrameAdapter(final DataFrame<Object> df) {
this.df = df;
}
public DataFrameAdapter(final Scriptable scope, final DataFrame<Object> df) {
this.df = df;
setParentScope(scope.getParentScope());
setPrototype(scope.getPrototype());
}
public static Scriptable jsConstructor(final Context ctx, final Object[] args, final Function ctor, final boolean newExpr) {
if (args.length == 3 && args[0] instanceof NativeArray) {
final List<List<Object>> data = new ArrayList<>();
final NativeArray array = NativeArray.class.cast(args[2]);
final Object[] ids = array.getIds();
for (int i = 0; i < array.getLength(); i++) {
data.add(asList(array.get((int)ids[i], null)));
}
return new DataFrameAdapter(
new DataFrame<Object>(
asList(args[0]),
asList(args[1]),
data
)
);
} else if (args.length == 2 && args[0] instanceof NativeArray) {
return new DataFrameAdapter(new DataFrame<Object>(
asList(args[0]),
asList(args[1])
));
} else if (args.length == 1 && args[0] instanceof NativeArray) {
return new DataFrameAdapter(new DataFrame<Object>(
asList(args[0])
));
} else if (args.length > 0) {
final String[] columns = new String[args.length];
for (int i = 0; i < args.length; i++) {
columns[i] = Context.toString(args[i]);
}
return new DataFrameAdapter(new DataFrame<>(columns));
}
return new DataFrameAdapter(new DataFrame<>());
}
private static DataFrameAdapter cast(final Scriptable object) {
return DataFrameAdapter.class.cast(object);
}
public static Scriptable jsFunction_add(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
if (args.length == 2 && args[1] instanceof NativeArray) {
return new DataFrameAdapter(object, cast(object).df.add(args[0], asList(args[1])));
}
if (args.length == 1 && args[0] instanceof NativeArray) {
return new DataFrameAdapter(object, cast(object).df.add(asList(args[0])));
}
return new DataFrameAdapter(object, cast(object).df.add(args));
}
public static Scriptable jsFunction_drop(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
return new DataFrameAdapter(object, cast(object).df.drop(args));
}
public static Scriptable jsFunction_retain(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
return new DataFrameAdapter(object, cast(object).df.retain(args));
}
public static Scriptable jsFunction_reindex(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
if (args.length > 0 && args[0] instanceof NativeArray) {
if (args.length > 1) {
return new DataFrameAdapter(object, cast(object).df.reindex(
asList(args[0]).toArray(), Context.toBoolean(args[1])));
}
return new DataFrameAdapter(object, cast(object).df.reindex(asList(args[0]).toArray()));
}
return new DataFrameAdapter(object, cast(object).df.reindex(args));
}
public DataFrameAdapter jsFunction_resetIndex() {
return new DataFrameAdapter(this, df.resetIndex());
}
public DataFrameAdapter jsFunction_rename(final Object old, final Object name) {
return new DataFrameAdapter(this, df.rename(old, name));
}
public static Scriptable jsFunction_append(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
if (args.length == 2 && args[1] instanceof NativeArray) {
return new DataFrameAdapter(object, cast(object).df.append(args[0], asList(args[1])));
}
return new DataFrameAdapter(object, cast(object).df.append(asList(args[0])));
}
public DataFrameAdapter jsFunction_reshape(final Integer rows, final Integer cols) {
return new DataFrameAdapter(this, df.reshape(rows, cols));
}
public static Scriptable jsFunction_join(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final DataFrame<Object> other = DataFrameAdapter.class.cast(args[0]).df;
final JoinType type = args.length > 1 && args[1] instanceof NativeJavaObject ?
JoinType.class.cast(Context.jsToJava(args[1], JoinType.class)) : null;
if (args.length > 1 && args[args.length - 1] instanceof Function) {
@SuppressWarnings("unchecked")
final KeyFunction<Object> f = (KeyFunction<Object>)Context.jsToJava(args[args.length - 1], KeyFunction.class);
if (type != null) {
return new DataFrameAdapter(object, cast(object).df.join(other, type, f));
}
return new DataFrameAdapter(object, cast(object).df.join(other, f));
}
if (type != null) {
return new DataFrameAdapter(object, cast(object).df.join(other, type));
}
return new DataFrameAdapter(object, cast(object).df.join(other));
}
public static Scriptable jsFunction_joinOn(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final DataFrame<Object> other = DataFrameAdapter.class.cast(args[0]).df;
final JoinType type = args.length > 1 && args[1] instanceof NativeJavaObject ?
JoinType.class.cast(Context.jsToJava(args[1], JoinType.class)) : null;
if (type != null) {
return new DataFrameAdapter(object, cast(object).df.joinOn(other, type, Arrays.copyOfRange(args, 2, args.length)));
}
return new DataFrameAdapter(object, cast(object).df.joinOn(other, Arrays.copyOfRange(args, 1, args.length)));
}
public static Scriptable jsFunction_merge(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final DataFrame<Object> other = DataFrameAdapter.class.cast(args[0]).df;
final JoinType type = args.length > 1 && args[1] instanceof NativeJavaObject ?
JoinType.class.cast(Context.jsToJava(args[1], JoinType.class)) : null;
if (type != null) {
return new DataFrameAdapter(object, cast(object).df.merge(other, type));
}
return new DataFrameAdapter(object, cast(object).df.merge(other));
}
public static Scriptable jsFunction_update(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final DataFrame<?>[] others = new DataFrame[args.length];
for (int i = 0; i < args.length; i++) {
others[i] = DataFrameAdapter.class.cast(args[i]).df;
}
return new DataFrameAdapter(object, cast(object).df.update(others));
}
public static Scriptable jsFunction_coalesce(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final DataFrame<?>[] others = new DataFrame[args.length];
for (int i = 0; i < args.length; i++) {
others[i] = DataFrameAdapter.class.cast(args[i]).df;
}
return new DataFrameAdapter(object, cast(object).df.coalesce(others));
}
public int jsFunction_size() {
return df.size();
}
public int jsFunction_length() {
return df.length();
}
public boolean jsFunction_isEmpty() {
return df.isEmpty();
}
public Set<Object> jsFunction_index() {
return df.index();
}
public Set<Object> jsFunction_columns() {
return df.columns();
}
public Object jsFunction_get(final Integer row, final Integer col) {
return df.get(row, col);
}
public DataFrameAdapter jsFunction_slice(final Integer rowStart, final Integer rowEnd, final Integer colStart, final Integer colEnd) {
return new DataFrameAdapter(this, df.slice(rowStart, rowEnd, colStart, colEnd));
}
public void jsFunction_set(final Integer row, final Integer col, final Scriptable value) {
df.set(row, col, Context.jsToJava(value, Object.class));
}
public List<Object> jsFunction_col(final Integer column) {
return df.col(column);
}
public List<Object> jsFunction_row(final Integer row) {
return df.row(row);
}
public DataFrameAdapter jsFunction_select(final Function predicate) {
@SuppressWarnings("unchecked")
final Predicate<Object> p = (Predicate<Object>)Context.jsToJava(predicate, Predicate.class);
return new DataFrameAdapter(this, df.select(p));
}
public static Scriptable jsFunction_head(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final Number limit = args.length == 1 ? Context.toNumber(args[0]) : null;
return new DataFrameAdapter(object,
limit != null ?
cast(object).df.head(limit.intValue()) :
cast(object).df.head()
);
}
public static Scriptable jsFunction_tail(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final Number limit = args.length == 1 ? Context.toNumber(args[0]) : null;
return new DataFrameAdapter(object,
limit != null ?
cast(object).df.tail(limit.intValue()) :
cast(object).df.tail()
);
}
public List<Object> jsFunction_flatten() {
return df.flatten();
}
public DataFrameAdapter jsFunction_transpose() {
return new DataFrameAdapter(this, df.transpose());
}
public DataFrameAdapter jsFunction_apply(final Function function) {
@SuppressWarnings("unchecked")
final DataFrame.Function<Object, Object> f = (DataFrame.Function<Object, Object>)Context.jsToJava(function, DataFrame.Function.class);
return new DataFrameAdapter(this, df.apply(f));
}
public DataFrameAdapter jsFunction_transform(final Function function) {
@SuppressWarnings("unchecked")
final RowFunction<Object, Object> f = (RowFunction<Object, Object>)Context.jsToJava(function, DataFrame.RowFunction.class);
return new DataFrameAdapter(this, df.transform(f));
}
public static Scriptable jsFunction_convert(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
if (args.length > 0) {
final Class<?>[] types = new Class[args.length];
for (int i = 0; i < args.length; i++) {
types[i] = Class.class.cast(Context.jsToJava(args[i], Class.class));
}
return new DataFrameAdapter(object, cast(object).df.convert(types));
}
return new DataFrameAdapter(object, cast(object).df.convert());
}
public DataFrameAdapter jsFunction_isnull() {
return new DataFrameAdapter(this, df.isnull().cast(Object.class));
}
public DataFrameAdapter jsFunction_notnull() {
return new DataFrameAdapter(this, df.notnull().cast(Object.class));
}
public static Scriptable jsFunction_groupBy(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
if (args.length == 1 && args[0] instanceof Function) {
@SuppressWarnings("unchecked")
final KeyFunction<Object> f = (KeyFunction<Object>)Context.jsToJava(args[0], KeyFunction.class);
return new DataFrameAdapter(object, cast(object).df.groupBy(f));
}
if (args.length == 1 && args[0] instanceof NativeArray) {
return new DataFrameAdapter(object, cast(object).df.groupBy(asList(args[0]).toArray()));
}
return new DataFrameAdapter(object, cast(object).df.groupBy(args));
}
public Grouping jsFunction_groups() {
return df.groups();
}
public Map<Object, DataFrame<Object>> jsFunction_explode() {
return df.explode();
}
public DataFrameAdapter jsFunction_aggregate(final Function function) {
@SuppressWarnings("unchecked")
final Aggregate<Object, Object> f = (Aggregate<Object, Object>)Context.jsToJava(function, Aggregate.class);
return new DataFrameAdapter(this, df.aggregate(f));
}
public DataFrameAdapter jsFunction_count() {
return new DataFrameAdapter(this, df.count());
}
public DataFrameAdapter jsFunction_collapse() {
return new DataFrameAdapter(this, df.collapse());
}
public DataFrameAdapter jsFunction_sum() {
return new DataFrameAdapter(this, df.sum());
}
public DataFrameAdapter jsFunction_prod() {
return new DataFrameAdapter(this, df.prod());
}
public DataFrameAdapter jsFunction_mean() {
return new DataFrameAdapter(this, df.mean());
}
public DataFrameAdapter jsFunction_stddev() {
return new DataFrameAdapter(this, df.stddev());
}
public DataFrameAdapter jsFunction_var() {
return new DataFrameAdapter(this, df.var());
}
public DataFrameAdapter jsFunction_skew() {
return new DataFrameAdapter(this, df.skew());
}
public DataFrameAdapter jsFunction_kurt() {
return new DataFrameAdapter(this, df.kurt());
}
public DataFrameAdapter jsFunction_min() {
return new DataFrameAdapter(this, df.min());
}
public DataFrameAdapter jsFunction_max() {
return new DataFrameAdapter(this, df.max());
}
public DataFrameAdapter jsFunction_median() {
return new DataFrameAdapter(this, df.median());
}
public DataFrameAdapter jsFunction_cumsum() {
return new DataFrameAdapter(this, df.cumsum());
}
public DataFrameAdapter jsFunction_cumprod() {
return new DataFrameAdapter(this, df.cumprod());
}
public DataFrameAdapter jsFunction_cummin() {
return new DataFrameAdapter(this, df.cummin());
}
public DataFrameAdapter jsFunction_cummax() {
return new DataFrameAdapter(this, df.cummax());
}
public DataFrameAdapter jsFunction_describe() {
return new DataFrameAdapter(this, df.describe());
}
public static Scriptable jsFunction_pivot(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final Object row = Context.jsToJava(args[0], Object.class);
final Object col = Context.jsToJava(args[0], Object.class);
final Object[] values = new Object[args.length - 2];
for (int i = 0; i < values.length; i++) {
values[i] = Context.jsToJava(args[i + 2], Object.class);
}
return new DataFrameAdapter(object, cast(object).df.pivot(row, col, values));
}
public static Scriptable jsFunction_sortBy(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
return new DataFrameAdapter(object, cast(object).df.sortBy(args));
}
public List<Class<?>> jsFunction_types() {
return df.types();
}
public DataFrameAdapter jsFunction_numeric() {
return new DataFrameAdapter(this, df.numeric().cast(Object.class));
}
public DataFrameAdapter jsFunction_nonnumeric() {
return new DataFrameAdapter(this, df.nonnumeric());
}
public Map<Object, List<Object>> jsFunction_map(final Object key, final Object value) {
return df.map(key, value);
}
public static Scriptable jsFunction_unique(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
return new DataFrameAdapter(object, cast(object).df.unique(args));
}
public static Scriptable jsFunction_diff(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final Number period = args.length == 1 ? Context.toNumber(args[0]) : 1;
return new DataFrameAdapter(object, cast(object).df.diff(period.intValue()));
}
public static Scriptable jsFunction_percentChange(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final Number period = args.length == 1 ? Context.toNumber(args[0]) : 1;
return new DataFrameAdapter(object, cast(object).df.percentChange(period.intValue()));
}
public static Scriptable jsFunction_rollapply(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
@SuppressWarnings("unchecked")
final DataFrame.Function<List<Object>, Object> f = (DataFrame.Function<List<Object>, Object>)Context.jsToJava(args[0], DataFrame.Function.class);
final Number period = args.length == 2 ? Context.toNumber(args[1]) : 1;
return new DataFrameAdapter(object, cast(object).df.rollapply(f, period.intValue()));
}
public static Object jsFunction_plot(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
if (args.length > 0) {
final PlotType type = PlotType.class.cast(Context.jsToJava(args[0], PlotType.class));
cast(object).df.plot(type);
return Context.getUndefinedValue();
}
cast(object).df.plot();
return Context.getUndefinedValue();
}
public void jsFunction_show() {
df.show();
}
public static Scriptable jsStaticFunction_readCsv(final Context ctx, final Scriptable object, final Object[] args, final Function func)
throws IOException {
final String file = Context.toString(args[0]);
final DataFrame<Object> df = DataFrame.readCsv(file);
return new DataFrameAdapter(ctx.newObject(object, df.getClass().getSimpleName()), df);
}
public void jsFunction_writeCsv(final String file)
throws IOException {
df.writeCsv(file);
}
public static Scriptable jsStaticFunction_readXls(final Context ctx, final Scriptable object, final Object[] args, final Function func)
throws IOException {
final String file = Context.toString(args[0]);
final DataFrame<Object> df = DataFrame.readXls(file);
return new DataFrameAdapter(ctx.newObject(object, df.getClass().getSimpleName()), df);
}
public void jsFunction_writeXls(final String file)
throws IOException {
df.writeXls(file);
}
public static Scriptable jsFunction_toArray(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
return ctx.newArray(object, cast(object).df.toArray());
}
public static Object jsFunction_toString(final Context ctx, final Scriptable object, final Object[] args, final Function func) {
final Number limit = args.length == 1 ? Context.toNumber(args[0]) : null;
return limit != null ?
cast(object).df.toString(limit.intValue()) :
cast(object).df.toString();
}
@Override
public Object getDefaultValue(final Class<?> hint) {
if (hint == ScriptRuntime.BooleanClass) {
return df.isEmpty();
}
return df.toString();
}
@Override
public String getClassName() {
return df.getClass().getSimpleName();
}
@Override
public Object[] getIds() {
final List<String> ids = new ArrayList<>();
for (final Method m : getClass().getMethods()) {
final String name = m.getName();
if (name.startsWith("js") && name.contains("_")) {
ids.add(name.substring(name.indexOf('_') + 1));
}
}
return ids.toArray();
}
@Override
public Object[] getAllIds() {
return getIds();
}
@Override
public boolean equals(final Object o) {
return df.equals(o);
}
@Override
public int hashCode() {
return df.hashCode();
}
private static List<Object> asList(final Object array) {
return asList(NativeArray.class.cast(array));
}
private static List<Object> asList(final NativeArray array) {
final List<Object> list = new ArrayList<>((int)array.getLength());
for (final Object id : array.getIds()) {
list.add(array.get((int)id, null));
}
return list;
}
}