1
0
mirror of https://github.com/DanilaFe/abacus synced 2026-01-25 08:05:19 +00:00

Compare commits

..

15 Commits

Author SHA1 Message Date
ea4588be44 Add more descriptive message to context exceptions. 2017-09-23 15:43:07 -07:00
31996219ad Switch the Lexer and TreeBuilder to using exceptions. 2017-09-23 15:31:35 -07:00
a3bfc34c1c Throw parse exceptions instead of returning null. 2017-09-22 16:35:08 -07:00
8dc7acd4b3 Add a separate class of exceptions for NumberReducer. 2017-09-22 11:58:19 -07:00
76fcd8ec1c Remove unused elvis. 2017-09-21 23:17:45 -07:00
fbdf2c7e52 Eliminate warnings related to null returns that have been removed. 2017-09-21 23:09:13 -07:00
3057f66e66 Throw the exception instead of returning null. 2017-09-21 23:05:48 -07:00
f385a48aa2 Merge pull request #34 from DanilaFe/number-range
Add a NumberRange utility.
2017-09-20 15:42:05 -07:00
fd3f56aa8f Write some tests for the Ranges. 2017-09-20 15:23:17 -07:00
e364f4e94b Have the NumberInterface provide the Kotlin rangeTo method. 2017-09-20 13:22:18 -07:00
4c94abb18b Create the NumberRangeBuilder utility class. 2017-09-20 13:19:55 -07:00
ba63dd7874 Implement a range that works for NumberInterfaces. 2017-09-20 13:04:20 -07:00
566598b702 Merge pull request #33 from DanilaFe/promotion-fix
Fix a bug that made some same-priority implementations not convert.
2017-09-20 12:56:12 -07:00
eb91a5b875 Fix a bug that made some same-priority implementations not convert. 2017-09-20 12:47:30 -07:00
fcd4694203 Merge pull request #32 from DanilaFe/promotion-exception
Add an exception thrown when promotion fails.
2017-09-20 12:16:00 -07:00
21 changed files with 295 additions and 50 deletions

View File

@@ -0,0 +1,24 @@
package org.nwapw.abacus.exception;
/**
* Exception thrown by the Context in cases where lookup fails
* where it should not.
*/
public class ContextException extends AbacusException {
/**
* Creates a new ContextException without an extra message.
*/
public ContextException() {
this("");
}
/**
* Creates a ContextException with the given message.
* @param message the message to use.
*/
public ContextException(String message){
super("Context exception", message);
}
}

View File

@@ -0,0 +1,25 @@
package org.nwapw.abacus.exception;
/**
* Exception thrown by the NumberReducer if something goes wrong when
* transforming a parse tree into a single value.
*/
public class NumberReducerException extends AbacusException {
/**
* Creates a new NumberReducerException with
* no additional message.
*/
public NumberReducerException() {
this("");
}
/**
* Creates a new NumberReducerException with the given message.
* @param message the message.
*/
public NumberReducerException(String message) {
super("Error evaluating expression", message);
}
}

View File

@@ -0,0 +1,23 @@
package org.nwapw.abacus.exception;
/**
* Exception thrown by parsers.
*/
public class ParseException extends AbacusException {
/**
* Creates a new ParseException with no additional message.
*/
public ParseException(){
this("");
}
/**
* Creates a new ParseException with the given additional message.
* @param message the message.
*/
public ParseException(String message){
super("Failed to parse string", message);
}
}

View File

@@ -18,7 +18,7 @@ public class PromotionException extends AbacusException {
* @param message the additional message to include with the error.
*/
public PromotionException(String message) {
super("Failed to promote number instances.", message);
super("Failed to promote number instances", message);
}
}

View File

@@ -1,16 +1,14 @@
package org.nwapw.abacus.exception;
/**
* An exception thrown primarily from Tree Value operators and functions,
* which have to deal with the result of a Reducer as well as the results
* of Applicable.
* An exception thrown from TreeReducers.
*/
public class EvaluationException extends AbacusException {
public class ReductionException extends AbacusException {
/**
* Creates a new EvaluationException with the default string.
*/
public EvaluationException() {
public ReductionException() {
this("");
}
@@ -18,7 +16,7 @@ public class EvaluationException extends AbacusException {
* Creates a new EvaluationError with the given message string.
* @param message the message string.
*/
public EvaluationException(String message) {
public ReductionException(String message) {
super("Evaluation error", message);
}

View File

@@ -0,0 +1,23 @@
package org.nwapw.abacus.exception;
/**
* Exception thrown by Lexers when they are unable to tokenize the input string.
*/
public class TokenizeException extends AbacusException {
/**
* Create a new tokenize exception with no additional data.
*/
public TokenizeException() {
this("");
}
/**
* Create a new tokenize exception with the given message.
* @param message the message to use.
*/
public TokenizeException(String message){
super("Failed to tokenize string", message);
}
}

View File

@@ -238,4 +238,16 @@ public abstract class NumberInterface implements Comparable<NumberInterface> {
*/
public abstract NumberInterface getMaxError();
/**
* Returns a NumberRangeBuilder object, which is used to create a range.
* The reason that this returns a builder and not an actual range is that
* the NumberRange needs to promote values passed to it, which
* requires an abacus instance.
* @param other the value at the bottom of the range.
* @return the resulting range builder.
*/
public NumberRangeBuilder rangeTo(NumberInterface other){
return new NumberRangeBuilder(this, other);
}
}

View File

@@ -1,5 +1,6 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.exception.TokenizeException;
import org.nwapw.abacus.lexing.Lexer;
import org.nwapw.abacus.lexing.pattern.Match;
import org.nwapw.abacus.lexing.pattern.Pattern;
@@ -42,7 +43,9 @@ public class LexerTokenizer implements Tokenizer<Match<TokenType>>, PluginListen
@Override
public List<Match<TokenType>> tokenizeString(String string) {
return lexer.lexAll(string, 0, TOKEN_SORTER);
List<Match<TokenType>> tokens = lexer.lexAll(string, 0, TOKEN_SORTER);
if(tokens == null) throw new TokenizeException();
return tokens;
}
@Override

View File

@@ -1,5 +1,6 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.exception.ParseException;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.function.OperatorType;
@@ -99,7 +100,7 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
while (!tokenStack.empty() && tokenStack.peek().getType() != TokenType.OPEN_PARENTH) {
output.add(tokenStack.pop());
}
if (tokenStack.empty()) return null;
if (tokenStack.empty()) throw new ParseException("mismatched parentheses");
if (matchType == TokenType.CLOSE_PARENTH) {
tokenStack.pop();
}
@@ -111,7 +112,7 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
if (!(newMatchType == TokenType.OP ||
newMatchType == TokenType.TREE_VALUE_OP ||
newMatchType == TokenType.FUNCTION ||
newMatchType == TokenType.TREE_VALUE_FUNCTION)) return null;
newMatchType == TokenType.TREE_VALUE_FUNCTION)) throw new ParseException("mismatched parentheses");
output.add(tokenStack.pop());
}
return output;
@@ -124,7 +125,7 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
* @return the construct tree expression.
*/
public TreeNode constructRecursive(List<Match<TokenType>> matches) {
if (matches.size() == 0) return null;
if (matches.size() == 0) throw new ParseException("no tokens left in input");
Match<TokenType> match = matches.remove(0);
TokenType matchType = match.getType();
if (matchType == TokenType.OP || matchType == TokenType.TREE_VALUE_OP) {
@@ -133,7 +134,6 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
if (type == OperatorType.BINARY_INFIX) {
TreeNode right = constructRecursive(matches);
TreeNode left = constructRecursive(matches);
if (left == null || right == null) return null;
if (matchType == TokenType.OP) {
return new NumberBinaryNode(operator, left, right);
} else {
@@ -141,7 +141,6 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
}
} else {
TreeNode applyTo = constructRecursive(matches);
if (applyTo == null) return null;
if (matchType == TokenType.OP) {
return new NumberUnaryNode(operator, applyTo);
} else {
@@ -157,10 +156,9 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
List<TreeNode> children = new ArrayList<>();
while (!matches.isEmpty() && matches.get(0).getType() != TokenType.INTERNAL_FUNCTION_END) {
TreeNode argument = constructRecursive(matches);
if (argument == null) return null;
children.add(0, argument);
}
if (matches.isEmpty()) return null;
if (matches.isEmpty()) throw new ParseException("incorrectly formatted function call");
matches.remove(0);
CallNode node;
if (matchType == TokenType.FUNCTION) {
@@ -170,16 +168,17 @@ public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListe
}
return node;
}
return null;
throw new ParseException("unrecognized token");
}
@Override
public TreeNode constructTree(List<Match<TokenType>> tokens) {
if (tokens.isEmpty()) throw new ParseException("no input tokens");
tokens = intoPostfix(new ArrayList<>(tokens));
if (tokens == null) return null;
Collections.reverse(tokens);
TreeNode constructedTree = constructRecursive(tokens);
return tokens.size() == 0 ? constructedTree : null;
if(tokens.size() == 0) return constructedTree;
throw new ParseException("could not parse all input");
}
@Override

View File

@@ -42,9 +42,7 @@ public class TreeBuilder<T> {
* @return the resulting tree.
*/
public TreeNode fromString(String input) {
List<T> tokens = tokenizer.tokenizeString(input);
if (tokens == null) return null;
return parser.constructTree(tokens);
return parser.constructTree(tokenizer.tokenizeString(input));
}
}

View File

@@ -332,7 +332,7 @@ public class StandardPlugin extends Plugin {
//We'll use the series \sigma_{n >= 1) ((1/3^n + 1/4^n) * 1/n)
//In the following, a=1/3^n, b=1/4^n, c = 1/n.
//a is also an error bound.
NumberInterface a = implementation.instanceForString("1"), b = a, c = a;
NumberInterface a = implementation.instanceForString("1"), b = a, c;
NumberInterface sum = implementation.instanceForString("0");
NumberInterface one = implementation.instanceForString("1");
int n = 0;
@@ -715,7 +715,7 @@ public class StandardPlugin extends Plugin {
* @return the value of the series
*/
private static NumberInterface sinTaylor(MutableEvaluationContext context, NumberInterface x) {
NumberInterface power = x, multiplier = x.multiply(x).negate(), currentTerm = x, sum = x;
NumberInterface power = x, multiplier = x.multiply(x).negate(), currentTerm, sum = x;
NumberInterface maxError = x.getMaxError();
int n = 1;
do {

View File

@@ -88,7 +88,7 @@ class Abacus(val configuration: Configuration) {
* @param input the input string to parse
* @return the resulting tree, null if the tree builder or the produced tree are null.
*/
fun parseString(input: String): TreeNode? = treeBuilder.fromString(input)
fun parseString(input: String): TreeNode = treeBuilder.fromString(input)
/**
* Evaluates the given tree.
*

View File

@@ -1,5 +1,6 @@
package org.nwapw.abacus.context
import org.nwapw.abacus.exception.ContextException
import kotlin.reflect.KProperty
/**
@@ -16,14 +17,14 @@ import kotlin.reflect.KProperty
*/
class ChainSearchDelegate<out V>(private val valueGetter: EvaluationContext.() -> V?) {
operator fun getValue(selfRef: Any, property: KProperty<*>): V? {
var currentRef = selfRef as? EvaluationContext ?: return null
operator fun getValue(selfRef: Any, property: KProperty<*>): V {
var currentRef = selfRef as EvaluationContext
var returnedValue = currentRef.valueGetter()
while (returnedValue == null) {
currentRef = currentRef.parent ?: break
returnedValue = currentRef.valueGetter()
}
return returnedValue
return returnedValue ?: throw ContextException("context chain does not contain value")
}
}

View File

@@ -43,13 +43,13 @@ open class EvaluationContext(val parent: EvaluationContext? = null,
/**
* The implementation inherited from this context's parent.
*/
val inheritedNumberImplementation: NumberImplementation?
by ChainSearchDelegate { numberImplementation}
val inheritedNumberImplementation: NumberImplementation
by ChainSearchDelegate { numberImplementation }
/**
* The reducer inherited from this context's parent.
*/
val inheritedReducer: Reducer<NumberInterface>?
val inheritedReducer: Reducer<NumberInterface>
by ChainSearchDelegate { reducer }
/**

View File

@@ -0,0 +1,28 @@
package org.nwapw.abacus.number
import org.nwapw.abacus.Abacus
/**
* A closed range designed specifically for [NumberInterface]
*
* Besides providing the usual functionality of a [ClosedRange], this range
* also handles promotion - that is, it's safe to use it with numbers of different
* implementations, even as starting and ending points.
*
* @property abacus the abacus instance used for promotion.
* @property start the starting point of the range.
* @property endInclusive the ending point of the range.
*/
class NumberRange(val abacus: Abacus,
override val start: NumberInterface,
override val endInclusive: NumberInterface): ClosedRange<NumberInterface> {
override operator fun contains(value: NumberInterface): Boolean {
val promotionResult = abacus.promotionManager.promote(start, endInclusive, value)
val newStart = promotionResult.items[0]
val newEnd = promotionResult.items[1]
val newValue = promotionResult.items[2]
return newValue >= newStart && newValue <= newEnd
}
}

View File

@@ -0,0 +1,24 @@
package org.nwapw.abacus.number
import org.nwapw.abacus.Abacus
/**
* A utility class for creating [NumberRange] instances.
*
* Unlike a regular [ClosedRange], a NumberRange must have a third parameter,
* which is the [Abacus] instance that is used for promotion. However, the ".." operator
* is infix, and can only take two parameters. The solution is, instead of returning instances
* of NumberRange directly, to return a builder, which then provides a [with] infix function
* to attach it to an instance of Abacus.
* @property start the beginning of the range.
* @property endInclusive the end of the range.
*/
class NumberRangeBuilder(private val start: NumberInterface, private val endInclusive: NumberInterface) {
/**
* Generate a [NumberRange] with the given instance of [abacus].
* @return a new range with the given instance of Abacus.
*/
infix fun with(abacus: Abacus) = NumberRange(abacus, start, endInclusive)
}

View File

@@ -59,14 +59,12 @@ class PromotionManager(val abacus: Abacus) : PluginListener {
override fun onLoad(manager: PluginManager) {
val implementations = manager.allNumberImplementations.map { manager.numberImplementationFor(it) }
for((index, value) in implementations.withIndex()){
for(i in index until implementations.size){
val other = implementations[i]
val promoteFrom = if(other.priority > value.priority) value else other
val promoteTo = if(other.priority > value.priority) other else value
for(first in implementations) {
for(second in implementations) {
val promoteFrom = if(second.priority > first.priority) first else second
val promoteTo = if(second.priority > first.priority) second else first
val path = computePathBetween(promoteFrom, promoteTo)
computePaths.put(promoteFrom to promoteTo, path)
computePaths[promoteFrom to promoteTo] = path
}
}
}

View File

@@ -2,7 +2,8 @@ package org.nwapw.abacus.tree
import org.nwapw.abacus.Abacus
import org.nwapw.abacus.context.EvaluationContext
import org.nwapw.abacus.exception.EvaluationException
import org.nwapw.abacus.exception.NumberReducerException
import org.nwapw.abacus.exception.ReductionException
import org.nwapw.abacus.number.NumberInterface
class NumberReducer(val abacus: Abacus, context: EvaluationContext) : Reducer<NumberInterface> {
@@ -17,15 +18,14 @@ class NumberReducer(val abacus: Abacus, context: EvaluationContext) : Reducer<Nu
val promotionManager = abacus.promotionManager
return when(treeNode){
is NumberNode -> {
context.inheritedNumberImplementation?.instanceForString(treeNode.number)
?: throw EvaluationException("no number implementation selected.")
context.inheritedNumberImplementation.instanceForString(treeNode.number)
}
is VariableNode -> {
val variable = context.getVariable(treeNode.variable)
if(variable != null) return variable
val definition = context.getDefinition(treeNode.variable)
if(definition != null) return definition.reduce(this)
throw EvaluationException("variable is not defined.")
throw NumberReducerException("variable is not defined.")
}
is NumberUnaryNode -> {
val child = children[0] as NumberInterface
@@ -58,7 +58,7 @@ class NumberReducer(val abacus: Abacus, context: EvaluationContext) : Reducer<Nu
abacus.pluginManager.treeValueFunctionFor(treeNode.callTo)
.apply(context, *treeNode.children.toTypedArray())
}
else -> throw EvaluationException("unrecognized tree node.")
else -> throw ReductionException("unrecognized tree node.")
}
}

View File

@@ -22,7 +22,6 @@ public class CalculationTests {
private void testOutput(String input, String parseOutput, String output) {
TreeNode parsedTree = abacus.parseString(input);
Assert.assertNotNull(parsedTree);
Assert.assertEquals(parsedTree.toString(), parseOutput);
NumberInterface result = abacus.evaluateTree(parsedTree).getValue();
Assert.assertNotNull(result);
@@ -31,7 +30,6 @@ public class CalculationTests {
private void testDomainException(String input, String parseOutput) {
TreeNode parsedTree = abacus.parseString(input);
Assert.assertNotNull(parsedTree);
Assert.assertEquals(parsedTree.toString(), parseOutput);
try {
abacus.evaluateTree(parsedTree);

View File

@@ -0,0 +1,97 @@
package org.nwapw.abacus.tests;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.nwapw.abacus.Abacus;
import org.nwapw.abacus.config.Configuration;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.number.NumberRange;
import org.nwapw.abacus.number.PreciseNumber;
import org.nwapw.abacus.plugin.StandardPlugin;
import java.util.function.Function;
public class RangeTests {
private static Abacus abacus = new Abacus(new Configuration( "precise", new String[]{}));
private static Function<NumberInterface, NumberInterface> naivePromotion = i -> new NaiveNumber((i.toString()));
private static Function<NumberInterface, NumberInterface> precisePromotion = i -> new PreciseNumber((i.toString()));
@BeforeClass
public static void prepareTests() {
abacus.getPluginManager().addInstantiated(new StandardPlugin(abacus.getPluginManager()));
abacus.reload();
}
public static NumberRange naiveRange(String bottom, String top) {
return new NaiveNumber(bottom).rangeTo(new NaiveNumber(top)).with(abacus);
}
@Test
public void testNaiveRange(){
NumberRange range = naiveRange("0", "10");
Assert.assertTrue(range.getStart().toString().startsWith("0"));
Assert.assertTrue(range.getEndInclusive().toString().startsWith("10"));
}
@Test
public void testNaiveRangeBelow() {
NumberRange range = naiveRange("0", "10");
Assert.assertFalse(range.contains(new NaiveNumber("-10")));
}
@Test
public void testNaiveRangeAbove() {
NumberRange range = naiveRange("0", "10");
Assert.assertFalse(range.contains(new NaiveNumber("20")));
}
@Test
public void testNaiveRangeJustWithinBottom() {
NumberRange range = naiveRange("0", "10");
Assert.assertTrue(range.contains(new NaiveNumber("0")));
}
@Test
public void testNaiveRangeJustWithinTop() {
NumberRange range = naiveRange("0", "10");
Assert.assertTrue(range.contains(new NaiveNumber("10")));
}
@Test
public void testNaiveRangeWithin() {
NumberRange range = naiveRange("0", "10");
Assert.assertTrue(range.contains(new NaiveNumber("5")));
}
public static void addTestPromotionPaths() {
StandardPlugin.IMPLEMENTATION_NAIVE.getPromotionPaths().put("precise", precisePromotion);
StandardPlugin.IMPLEMENTATION_PRECISE.getPromotionPaths().put("naive", naivePromotion);
abacus.reload();
}
public static void removeTestPromotionPaths() {
StandardPlugin.IMPLEMENTATION_NAIVE.getPromotionPaths().remove("precise");
StandardPlugin.IMPLEMENTATION_NAIVE.getPromotionPaths().remove("naive");
abacus.reload();
}
@Test
public void testPromotionWithin() {
addTestPromotionPaths();
NumberRange range = naiveRange("0", "10");
Assert.assertTrue(range.contains(new PreciseNumber("5")));
removeTestPromotionPaths();
}
@Test
public void testPromotionOutside(){
addTestPromotionPaths();
NumberRange range = naiveRange("0","10");
Assert.assertFalse(range.contains(new PreciseNumber("20")));
removeTestPromotionPaths();
}
}

View File

@@ -13,11 +13,8 @@ import javafx.util.StringConverter;
import org.nwapw.abacus.Abacus;
import org.nwapw.abacus.config.Configuration;
import org.nwapw.abacus.exception.AbacusException;
import org.nwapw.abacus.exception.ComputationInterruptedException;
import org.nwapw.abacus.function.Documentation;
import org.nwapw.abacus.function.DocumentationType;
import org.nwapw.abacus.exception.DomainException;
import org.nwapw.abacus.exception.EvaluationException;
import org.nwapw.abacus.number.*;
import org.nwapw.abacus.plugin.ClassFinder;
import org.nwapw.abacus.plugin.PluginListener;
@@ -145,9 +142,6 @@ public class AbacusController implements PluginListener {
private String attemptCalculation() {
try {
TreeNode constructedTree = abacus.parseString(inputField.getText());
if (constructedTree == null) {
return ERR_SYNTAX;
}
EvaluationResult result = abacus.evaluateTree(constructedTree);
NumberInterface evaluatedNumber = result.getValue();
String resultingString = evaluatedNumber.toString();