/*******************************************************************************
* LastCalc - The last calculator you'll ever need
* Copyright (C) 2011, 2012 Uprizer Labs LLC
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero 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 Affero General Public License for more
* details.
******************************************************************************/
package com.lastcalc.parsers;
import java.util.*;
import java.util.Map.Entry;
import com.google.common.collect.*;
import org.jscience.mathematics.number.Number;
import org.jscience.physics.amount.Amount;
import com.lastcalc.TokenList;
import com.lastcalc.parsers.PreParser.ListWithTail;
import com.lastcalc.parsers.PreParser.MapWithTail;
public class UserDefinedParserParser extends Parser {
private static final long serialVersionUID = -6964937711038633291L;
private static TokenList template;
static {
template = TokenList.createD((Lists.<Object> newArrayList("=", "is", "means")));
}
@Override
public ParseResult parse(final TokenList tokens, final int templatePos, final ParserContext context) {
// Identify the beginning and end of the UDPP
final int start = PreParser.findEdgeOrObjectBackwards(tokens, templatePos, null);
final int end = PreParser.findEdgeOrObjectForwards(tokens, templatePos, null) + 1;
final TokenList before = tokens.subList(start, templatePos);
if (before.isEmpty())
return ParseResult.fail();
final TokenList after = PreParser.flatten(tokens.subList(templatePos + 1, end));
if (after.isEmpty())
return ParseResult.fail();
final Set<String> variables = Sets.newHashSet();
for (final Object o : PreParser.flatten(before)) {
if (o instanceof String && !PreParser.reserved.contains(o)
&& Character.isUpperCase(((String) o).charAt(0))) {
variables.add((String) o);
}
}
final UserDefinedParser udp = new UserDefinedParser(before, after, variables);
return ParseResult.success(
tokens.replaceWithTokens(Math.max(0, start - 1), Math.min(tokens.size(), end + 1), udp));
}
public static class BindException extends Exception {
private static final long serialVersionUID = -2021162162489202197L;
public BindException(final String err) {
super(err);
}
}
@SuppressWarnings("unchecked")
protected static void bind(final Object from, final Object to, final Set<String> variables,
final Map<String, Object> bound)
throws BindException {
if (from.equals(to) || from.equals("?"))
return;
if (from instanceof String && variables.contains(from)) {
if (bound.containsKey(from) && !bound.get(from).equals(to)) {
throw new BindException("Variable "+from+" is already bound to "+bound.get(from)+" which isn't the same as "+to);
}
bound.put((String) from, to);
} else if (from instanceof Iterable && to instanceof Iterable) {
final Iterator<Object> fromL = ((Iterable<Object>) from).iterator();
final Iterator<Object> toL = ((Iterable<Object>) to).iterator();
while (fromL.hasNext() && toL.hasNext()) {
bind(fromL.next(), toL.next(), variables, bound);
}
if (fromL.hasNext() || toL.hasNext())
throw new BindException("Iterables are not of the same size");
} else if (from instanceof MapWithTail && to instanceof Map) {
final MapWithTail fromMWT = (MapWithTail) from;
final Map<Object, Object> toM = (Map<Object, Object>) to;
final Map<Object, Object> tail = Maps.newLinkedHashMap(toM);
if (toM.size() < fromMWT.map.size())
throw new BindException("Map must be at least size of head of MWT");
for (final Entry<Object, Object> e : fromMWT.map.entrySet()) {
if (!variables.contains(e.getValue()))
throw new BindException("Map value '"+e.getValue()+" is not a variable");
if (variables.contains(e.getKey()) && !bound.containsKey(e.getKey())) {
// The key is a variable, bind to any value and remove from
// tail
final Map.Entry<Object, Object> nextEntry = tail.entrySet().iterator().next();
bound.put((String) e.getKey(), nextEntry.getKey());
bind(e.getValue(), nextEntry.getValue(), variables, bound);
tail.remove(nextEntry.getKey());
} else {
final Object aKey = bound.containsKey(e.getKey()) ? bound.get(e.getKey()) : e.getKey();
final Object value = toM.get(aKey);
if (value == null)
throw new BindException("Map doesn't contain key '" + e.getKey() + "'");
bind(e.getValue(), value, variables, bound);
tail.remove(aKey);
}
}
bind(fromMWT.tail, tail, variables, bound);
} else if (from instanceof Map && to instanceof Map) {
final Map<Object, Object> fromM = (Map<Object, Object>) from;
final Map<Object, Object> toM = (Map<Object, Object>) to;
final Map<Object, Object> tail = Maps.newLinkedHashMap(toM);
if (toM.size() != fromM.size())
throw new BindException("Maps are not the same size");
for (final Entry<Object, Object> e : fromM.entrySet()) {
if (!variables.contains(e.getValue()))
throw new BindException("Map value '" + e.getValue() + " is not a variable");
if (variables.contains(e.getKey()) && !bound.containsKey(e.getKey())) {
// The key is a variable, bind to any value and remove from
// tail
final Map.Entry<Object, Object> nextEntry = tail.entrySet().iterator().next();
bound.put((String) e.getKey(), nextEntry.getKey());
bind(e.getValue(), nextEntry.getValue(), variables, bound);
tail.remove(nextEntry.getKey());
} else {
final Object aKey = bound.containsKey(e.getKey()) ? bound.get(e.getKey()) : e.getKey();
final Object value = toM.get(aKey);
if (value == null)
throw new BindException("Map doesn't contain key '" + e.getKey() + "'");
bind(e.getValue(), value, variables, bound);
tail.remove(e.getKey());
}
}
} else if (from instanceof ListWithTail && to instanceof List) {
final ListWithTail fromLWT = (ListWithTail) from;
final List<Object> toList = (List<Object>) to;
if (toList.size() < fromLWT.list.size())
throw new BindException("List must be at least size of head of LWT");
if (toList.isEmpty())
throw new BindException("Can't bind list with tail to empty list");
final LinkedList<Object> tail = Lists.newLinkedList(toList);
for (int x = 0; x < fromLWT.list.size(); x++) {
bind(fromLWT.list.get(x), toList.get(x), variables, bound);
tail.removeFirst();
}
bind(fromLWT.tail, tail, variables, bound);
} else
throw new BindException("Don't know how to bind " + from + ":" + from.getClass().getSimpleName() + " to "
+ to + ":" + to.getClass().getSimpleName());
}
public static class UserDefinedParser extends Parser {
private static final long serialVersionUID = -4928516936219533258L;
public final TokenList after;
private final TokenList template;
private final TokenList before;
public final Set<String> variables;
public UserDefinedParser(final TokenList before, final TokenList after, final Set<String> variables) {
this.before = before;
this.after = after;
this.variables = variables;
final List<Object> tpl = Lists.newArrayList();
for (final Object o : before) {
if (variables.contains(o)) {
final String var = (String) o;
if (var.endsWith("List")) {
tpl.add(List.class);
} else if (var.endsWith("Map")) {
tpl.add(Map.class);
} else if (var.endsWith("Num") || var.endsWith("Number")) {
tpl.add(Number.class);
} else if (var.endsWith("Bool") || var.endsWith("Boolean")) {
tpl.add(Boolean.class);
} else if (var.endsWith("Amount")) {
tpl.add(Amount.class);
} else if (var.endsWith("Fun") || var.endsWith("Function")) {
tpl.add(UserDefinedParser.class);
tpl.add(Amount.class);
} else {
tpl.add(Object.class);
}
} else if (o instanceof String) {
tpl.add(o);
} else if (o instanceof MapWithTail || o instanceof Map) {
tpl.add(Map.class);
} else if (o instanceof ListWithTail || o instanceof List) {
tpl.add(List.class);
} else {
tpl.add(o.getClass());
}
}
template = TokenList.create(tpl);
}
@Override
public ParseResult parse(final TokenList tokens, final int templatePos, final ParserContext context) {
if (tokens.get(PreParser.findEdgeOrObjectBackwards(tokens, templatePos, "then")).equals("then"))
return ParseResult.fail();
final List<Object> result = Lists.newArrayListWithCapacity(after.size() + 2);
final boolean useBrackets = tokens.size() > template.size();
if (useBrackets) {
result.add("(");
}
final TokenList input = tokens.subList(templatePos, templatePos + template.size());
final Map<String, Object> varMap = Maps.newHashMapWithExpectedSize(variables.size());
try {
bind(before, input, variables, varMap);
for (final Object o : after) {
final Object val = varMap.get(o);
if (val != null) {
result.add(val);
} else {
result.add(o);
}
}
} catch (final BindException e) {
return ParseResult.fail();
}
if (useBrackets) {
result.add(")");
}
final TokenList resultTL = TokenList.create(result);
final TokenList flattened = PreParser.flatten(resultTL);
return ParseResult.success(
tokens.replaceWithTokenList(templatePos, templatePos + template.size(), flattened),
Math.min(0, -flattened.size())
);
}
@Override
public TokenList getTemplate() {
return template;
}
@Override
public String toString() {
return before + " = " + after;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((after == null) ? 0 : after.hashCode());
result = prime * result + ((before == null) ? 0 : before.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof UserDefinedParser))
return false;
final UserDefinedParser other = (UserDefinedParser) obj;
if (after == null) {
if (other.after != null)
return false;
} else if (!after.equals(other.after))
return false;
if (before == null) {
if (other.before != null)
return false;
} else if (!before.equals(other.before))
return false;
return true;
}
public boolean hasVariables() {
return !variables.isEmpty();
}
}
@Override
public TokenList getTemplate() {
return template;
}
@Override
public int hashCode() {
return "UserDefinedParserParser".hashCode();
}
@Override
public boolean equals(final Object obj) {
return obj instanceof UserDefinedParserParser;
}
}