package edu.stanford.nlp.sempre.tables.lambdadcs;
import java.util.*;
import edu.stanford.nlp.sempre.*;
import edu.stanford.nlp.sempre.tables.lambdadcs.LambdaDCSException.Type;
import fig.basic.*;
/**
* Utilities for denotations.
*
* Handle aggregation, superlative, and arithmetic operations on different types of values.
*
* @author ppasupat
*/
public final class DenotationUtils {
private DenotationUtils() { }
// ============================================================
// Type Enforcer
// ============================================================
/**
* Try to convert to a number
*/
public static int convertToInteger(Value value) {
if (value instanceof NumberValue) {
return (int) ((NumberValue) value).value;
} else {
throw new LambdaDCSException(Type.typeMismatch, "Cannot convert %s to number", value);
}
}
/**
* Try to convert to a number
*/
public static double convertToNumber(Value value) {
if (value instanceof NumberValue) {
return ((NumberValue) value).value;
} else {
throw new LambdaDCSException(Type.typeMismatch, "Cannot convert %s to number", value);
}
}
/**
* Ensure that the unary only has a single positive integer and return that integer
*/
public static int getSinglePositiveInteger(UnaryDenotation unary) {
if (unary.size() != 1)
throw new LambdaDCSException(Type.nonSingletonList, "getSinglePositiveInteger(): denotation %s has != 1 elements", unary);
int amount = convertToInteger(unary.iterator().next());
if (amount > 0) return amount;
throw new LambdaDCSException(Type.typeMismatch, "getSinglePositiveInteger(): denotation %s is not a positive integer", unary);
}
/**
* Ensure that the unary only has a single number and return that number
*/
public static double getSingleNumber(UnaryDenotation unary) {
if (unary.size() != 1)
throw new LambdaDCSException(Type.nonSingletonList, "getSingleNumber(): denotation %s has != 1 elements", unary);
return convertToNumber(unary.iterator().next());
}
/**
* Ensure that the unary has only a single value and return that value
*/
public static Value getSingleValue(UnaryDenotation unary) {
if (unary.size() != 1)
throw new LambdaDCSException(Type.nonSingletonList, "getSingleValue(): denotation %s has != 1 elements", unary);
return unary.iterator().next();
}
// ============================================================
// Operations on Denotations
// ============================================================
/**
* Join between a BinaryDenotation and a UnarylikeDenotation.
*/
public static Unarylike genericJoin(Binarylike b, Unarylike u) {
if (u instanceof UnaryDenotation)
return b.joinOnKey((UnaryDenotation) u);
Map<Value, Collection<Value>> mapping = new HashMap<>();
if (u.domain().size() != Integer.MAX_VALUE) {
for (Value value : u.domain())
mapping.put(value, b.joinOnKey(u.get(value)));
return new MappingDenotation<>(u.getDomainVar(), new ExplicitPairList(mapping));
} else {
ExplicitPairList binary = b.explicitlyFilterOnKey(u.range()).pairList;
for (Map.Entry<Value, UnaryDenotation> entry : binary.mapping.entrySet()) {
for (Value key : u.inverseGet(entry.getKey())) {
if (!mapping.containsKey(key))
mapping.put(key, new ArrayList<>(entry.getValue()));
else
mapping.get(key).addAll(entry.getValue());
}
}
return new MappingDenotation<>(u.getDomainVar(), new ExplicitPairList(mapping));
}
}
/**
* Aggregate values of the same type.
*/
public static Value aggregate(Collection<Value> values, AggregateFormula.Mode mode) {
// Handle basic cases
if (mode == AggregateFormula.Mode.count) {
if (values.size() == Integer.MAX_VALUE)
throw new LambdaDCSException(Type.infiniteList, "Cannot call %s on an infinite list.", mode);
return new NumberValue(values.size());
}
if (values.isEmpty()) {
if (LambdaDCSExecutor.opts.aggregatesFailOnEmptyLists)
throw new LambdaDCSException(Type.emptyList, "Cannot call %s on an empty list.", mode);
return new ListValue(Collections.emptyList());
}
// General cases
TypeProcessor processor = getTypeProcessor(values);
switch (mode) {
case max: return processor.max(values);
case min: return processor.min(values);
case sum: return processor.sum(values);
case avg: return processor.avg(values);
default: throw new LambdaDCSException(Type.invalidFormula, "Unknown aggregate mode: %s", mode);
}
}
/**
* Helper: Check if the two Unarylikes have the same domain variable and return it.
* Throw an exception if the domain variables are not the same.
*/
public static String checkDomainVars(Unarylike u1, Unarylike u2) {
if (u1 == null || u1.getDomainVar() == null) return u2.getDomainVar();
if (u2 == null || u2.getDomainVar() == null) return u1.getDomainVar();
if (u1.getDomainVar().equals(u2.getDomainVar())) return u1.getDomainVar();
throw new LambdaDCSException(Type.invalidFormula, "Different domain variables: %s != %s",
u1.getDomainVar(), u2.getDomainVar());
}
/**
* Merge values.
*/
public static Unarylike merge(Unarylike u1, Unarylike u2, MergeFormula.Mode mode) {
if (u1 instanceof UnaryDenotation && u2 instanceof UnaryDenotation)
return ((UnaryDenotation) u1).merge((UnaryDenotation) u2, mode);
// MappingDenotation: Go over the union of the domains
String domainVar = checkDomainVars(u1, u2);
Map<Value, UnaryDenotation> answer = new HashMap<>();
for (Value key : u1.domain()) {
if (!answer.containsKey(key))
answer.put(key, u1.get(key).merge(u2.get(key), mode));
}
for (Value key : u2.domain()) {
if (!answer.containsKey(key))
answer.put(key, u1.get(key).merge(u2.get(key), mode));
}
if (!answer.containsKey(null))
answer.put(null, u1.get(null).merge(u2.get(null), mode));
answer.entrySet().removeIf(e -> e.getValue().size() == 0);
return new MappingDenotation<>(domainVar, new ExplicitPairList(answer));
}
/**
* Perform arithmetic operation. Currently each Value must be a NumberValue.
*
* If arithmeticsFailOnMultipleElements is specified, throw an error if
* both children contain more than 1 Value.
*/
public static Unarylike arithmetic(Unarylike u1, Unarylike u2, ArithmeticFormula.Mode mode) {
TypeProcessor processor = getTypeProcessor(u1.range(), u2.range());
if (u1 instanceof UnaryDenotation && u2 instanceof UnaryDenotation)
return arithmeticUnary((UnaryDenotation) u1, (UnaryDenotation) u2, mode, processor);
// MappingDenotation: Go over the union of the domains
String domainVar = checkDomainVars(u1, u2);
Map<Value, UnaryDenotation> answer = new HashMap<>();
for (Value key : u1.domain()) {
if (!answer.containsKey(key))
answer.put(key, arithmeticUnary(u1.get(key), u2.get(key), mode, processor));
}
for (Value key : u2.domain()) {
if (!answer.containsKey(key))
answer.put(key, arithmeticUnary(u1.get(key), u2.get(key), mode, processor));
}
if (!answer.containsKey(null))
answer.put(null, arithmeticUnary(u1.get(null), u2.get(null), mode, processor));
answer.entrySet().removeIf(e -> e.getValue().size() == 0);
return new MappingDenotation<>(domainVar, new ExplicitPairList(answer));
}
public static UnaryDenotation arithmeticUnary(UnaryDenotation u1, UnaryDenotation u2,
ArithmeticFormula.Mode mode, TypeProcessor processor) {
if (LambdaDCSExecutor.opts.arithmeticsFailOnEmptyLists && (u1.size() == 0 || u2.size() == 0))
throw new LambdaDCSException(Type.emptyList, "Cannot call %s on an empty list.", mode);
if (LambdaDCSExecutor.opts.arithmeticsFailOnMultipleElements && u1.size() > 1 && u2.size() > 1)
throw new LambdaDCSException(Type.nonSingletonList, "Cannot call %s when both denotations have > 1 values.", mode);
if (processor == null) processor = getTypeProcessor(u1, u2);
List<Value> answer = new ArrayList<>();
switch (mode) {
case add: for (Value v1 : u1) for (Value v2 : u2) answer.add(processor.add(v1, v2)); break;
case sub: for (Value v1 : u1) for (Value v2 : u2) answer.add(processor.sub(v1, v2)); break;
case mul: for (Value v1 : u1) for (Value v2 : u2) answer.add(processor.mul(v1, v2)); break;
case div: for (Value v1 : u1) for (Value v2 : u2) answer.add(processor.div(v1, v2)); break;
default:
throw new LambdaDCSException(Type.invalidFormula, "Unknown arithmetic mode: %s", mode);
}
return new ExplicitUnaryDenotation(answer);
}
/**
* Compute the superlative (argmax/min rank count head (reverse relation)).
*
* Note that |relation| is reversed so that |relation| can be directly joined with |head|.
*
* If opt.aggregateReturnAllTopTies is true, (argmin 1 1 ...) and (argmax 1 1 ...) will return
* the list of all values that produce the min or max. Otherwise, only 1 arbitrary value will be returned.
*/
public static Unarylike superlative(int rank, int count, Unarylike head,
Binarylike relation, SuperlativeFormula.Mode mode) {
// Filter the relation with the possible keys
BinaryDenotation<ExplicitPairList> filtered = relation.explicitlyFilterOnKey(head.range());
TypeProcessor processor = getTypeProcessor(filtered.pairList.range());
if (head instanceof UnaryDenotation) {
return superlativeUnary(rank, count, filtered.pairList.pairs, mode, processor);
}
// MappingDenotation: Go over the domain
Map<Value, UnaryDenotation> answer = new HashMap<>();
for (Value key : head.domain()) {
BinaryDenotation<ExplicitPairList> refiltered = filtered.explicitlyFilterOnKey(head.get(key));
answer.put(key, superlativeUnary(rank, count, refiltered.pairList.pairs, mode, processor));
}
return new MappingDenotation<>(head.getDomainVar(), new ExplicitPairList(answer));
}
/**
* Perform superlative on a list of (value, key).
*/
public static UnaryDenotation superlativeUnary(int rank, int count, List<Pair<Value, Value>> pairs,
SuperlativeFormula.Mode mode, TypeProcessor processor) {
if (pairs.isEmpty()) {
if (LambdaDCSExecutor.opts.superlativesFailOnEmptyLists)
throw new LambdaDCSException(Type.emptyList, "Cannot call %s on an empty list.", mode);
return UnaryDenotation.EMPTY;
}
List<Value> values = new ArrayList<>(), answer = new ArrayList<>();
for (Pair<Value, Value> pair : pairs)
values.add(pair.getFirst());
if (processor == null) processor = getTypeProcessor(values);
if (LambdaDCSExecutor.opts.superlativesReturnAllTopTies && rank == 1 && count == 1) {
// Special case: Return all ties at the top
Value topValue;
switch (mode) {
case argmax: topValue = processor.max(values); break;
case argmin: topValue = processor.min(values); break;
default: throw new LambdaDCSException(Type.invalidFormula, "Unknown superlative mode: %s", mode);
}
for (Pair<Value, Value> pair : pairs)
if (topValue.equals(pair.getFirst()) && !answer.contains(pair.getSecond()))
answer.add(pair.getSecond());
} else {
// Other cases
List<Integer> indices;
switch (mode) {
case argmax: indices = processor.argsort(values); Collections.reverse(indices); break;
case argmin: indices = processor.argsort(values); break;
default: throw new LambdaDCSException(Type.invalidFormula, "Unknown superlative mode: %s", mode);
}
int from = Math.min(rank - 1, indices.size()),
to = Math.min(from + count, indices.size());
for (int index : indices.subList(from, to))
answer.add(pairs.get(index).getSecond());
}
return new ExplicitUnaryDenotation(answer);
}
// ============================================================
// Processor for each data type
// ============================================================
/**
* Processor for each data type.
*/
public abstract static class TypeProcessor {
// Is the value v compatible with this processor?
public abstract boolean isCompatible(Value v);
// Is the collection sortable? (Is there a total order on the elements?)
public abstract boolean isSortable(Collection<Value> values);
// positive if v1 > v2 | negative if v1 < v2 | 0 if v1 == v2
public int compareValues(Value v1, Value v2) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compare values with " + getClass().getSimpleName()); }
public Value sum(Collection<Value> values) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compute sum with " + getClass().getSimpleName()); }
public Value avg(Collection<Value> values) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compute avg with " + getClass().getSimpleName()); }
public Value add(Value v1, Value v2) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compute add with " + getClass().getSimpleName()); }
public Value sub(Value v1, Value v2) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compute sub with " + getClass().getSimpleName()); }
public Value mul(Value v1, Value v2) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compute mul with " + getClass().getSimpleName()); }
public Value div(Value v1, Value v2) { throw new LambdaDCSException(Type.typeMismatch, "Cannot compute div with " + getClass().getSimpleName()); }
public Value max(Collection<Value> values) {
if (!isSortable(values))
throw new LambdaDCSException(Type.typeMismatch, "Values cannot be sorted.");
Value max = null;
for (Value value : values) {
if (max == null || compareValues(max, value) < 0)
max = value;
}
return max;
}
public Value min(Collection<Value> values) {
if (!isSortable(values))
throw new LambdaDCSException(Type.typeMismatch, "Values cannot be sorted.");
Value min = null;
for (Value value : values) {
if (min == null || compareValues(min, value) > 0)
min = value;
}
return min;
}
public List<Integer> argsort(List<Value> values) {
if (!isSortable(values))
throw new LambdaDCSException(Type.typeMismatch, "Values cannot be sorted.");
List<Integer> indices = new ArrayList<>();
for (int i = 0; i < values.size(); i++)
indices.add(i);
Collections.sort(indices, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return compareValues(values.get(o1), values.get(o2));
}
});
return indices;
}
}
/**
* Handle NumberValue. All operations are possible.
*/
public static class NumberProcessor extends TypeProcessor {
public static TypeProcessor singleton = new NumberProcessor();
@Override
public boolean isCompatible(Value v) {
return v instanceof NumberValue;
}
@Override
public boolean isSortable(Collection<Value> values) {
return true;
}
@Override
public int compareValues(Value v1, Value v2) {
double x1 = ((NumberValue) v1).value, x2 = ((NumberValue) v2).value;
return (x1 > x2) ? 1 : (x1 < x2) ? -1 : 0;
}
@Override
public Value sum(Collection<Value> values) {
double sum = 0;
for (Value value : values)
sum += ((NumberValue) value).value;
return new NumberValue(sum);
}
@Override
public Value avg(Collection<Value> values) {
double sum = 0;
for (Value value : values)
sum += ((NumberValue) value).value;
return new NumberValue(sum / values.size());
}
@Override public Value add(Value v1, Value v2) { return new NumberValue(((NumberValue) v1).value + ((NumberValue) v2).value); }
@Override public Value sub(Value v1, Value v2) { return new NumberValue(((NumberValue) v1).value - ((NumberValue) v2).value); }
@Override public Value mul(Value v1, Value v2) { return new NumberValue(((NumberValue) v1).value * ((NumberValue) v2).value); }
@Override public Value div(Value v1, Value v2) { return new NumberValue(((NumberValue) v1).value / ((NumberValue) v2).value); }
}
/**
* Handle DateValue. Only comparison is possible.
*/
public static class DateProcessor extends TypeProcessor {
public static TypeProcessor singleton = new DateProcessor();
@Override
public boolean isCompatible(Value v) {
return v instanceof DateValue;
}
@Override
public boolean isSortable(Collection<Value> values) {
DateValue firstDate = null;
for (Value value : values) {
DateValue date = (DateValue) value;
if (firstDate == null) {
firstDate = date;
} else {
if ((firstDate.year == -1) != (date.year == -1)) return false;
if ((firstDate.month == -1) != (date.month == -1)) return false;
if ((firstDate.day == -1) != (date.day == -1)) return false;
}
}
return true;
}
@Override
public int compareValues(Value v1, Value v2) {
DateValue d1 = ((DateValue) v1), d2 = ((DateValue) v2);
if (d1.year == -1 || d2.year == -1 || d1.year == d2.year) {
if (d1.month == -1 || d2.month == -1 || d1.month == d2.month) {
if (d1.day == -1 || d2.day == -1 || d1.day == d2.day) {
return 0;
} else {
return d1.day - d2.day;
}
} else {
return d1.month - d2.month;
}
} else {
return d1.year - d2.year;
}
}
}
/**
* Get the TypeProcessor corresponding to the strictest type.
*/
public static TypeProcessor getTypeProcessor(Value value) {
return getTypeProcessor(Collections.singleton(value), Collections.emptySet());
}
/**
* Get the TypeProcessor corresponding to the strictest type.
*/
public static TypeProcessor getTypeProcessor(Value value1, Value value2) {
return getTypeProcessor(Collections.singleton(value1), Collections.singleton(value2));
}
/**
* Get the TypeProcessor corresponding to the strictest type.
*/
public static TypeProcessor getTypeProcessor(Collection<Value> values) {
return getTypeProcessor(values, Collections.emptySet());
}
/**
* Get the TypeProcessor corresponding to the strictest type.
*/
public static TypeProcessor getTypeProcessor(Collection<Value> values1, Collection<Value> values2) {
boolean canBeNumber = true, canBeDate = true;
for (Value value : values1) {
if (!(value instanceof NumberValue)) canBeNumber = false;
if (!(value instanceof DateValue)) canBeDate = false;
}
for (Value value : values2) {
if (!(value instanceof NumberValue)) canBeNumber = false;
if (!(value instanceof DateValue)) canBeDate = false;
}
if (canBeNumber) {
return NumberProcessor.singleton;
} else if (canBeDate) {
return DateProcessor.singleton;
} else {
throw new LambdaDCSException(Type.typeMismatch, "Cannot compare values");
}
}
}