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

Compare commits

..

64 Commits

Author SHA1 Message Date
Riley Jones
c03e191c36 add live load and unload 2017-08-02 14:31:29 -07:00
5de9453bec Merge branch 'config-rewrite' 2017-08-01 16:42:58 -07:00
d205651332 Add a number implementation selector box. 2017-08-01 11:52:48 -07:00
6f99f07150 Add comment to PreciseNumber. 2017-08-01 10:49:50 -07:00
2cf41c1029 Add comments to the JavaFX codebase 2017-08-01 10:49:00 -07:00
76677ef494 Merge the two configuration classes into one. 2017-08-01 10:24:32 -07:00
0cd40b028a Merge branch 'javafx' 2017-08-01 09:53:47 -07:00
7cb04a1222 Switch to the new UI. 2017-08-01 09:53:38 -07:00
0a97eeb442 Resize table columns. 2017-08-01 09:25:13 -07:00
Arthur Drobot
1ee8c7d231 Keep 15 additional decimal places. 2017-07-31 23:16:37 -07:00
Arthur Drobot
f97d16c640 Comment out debugging output. 2017-07-31 23:09:11 -07:00
Arthur Drobot
a0bba03c2c Separate power and factorial calculations to fix large precision loss in exp. 2017-07-31 22:56:55 -07:00
05d0755526 Implement a cell that copies input when clicked, and add it to table. 2017-07-31 22:53:42 -07:00
0b97a935bf Enable cell selection to later allow for data copying. 2017-07-31 22:34:49 -07:00
211e963db0 Populate the history table. 2017-07-31 22:29:44 -07:00
d8145acc8f Implement the history data model. 2017-07-31 17:49:57 -07:00
63b8162a9b Format the fxml. 2017-07-31 17:18:08 -07:00
c655c63233 Link up the evaluation and the UI buttons. 2017-07-31 17:17:56 -07:00
27ff1a47b5 Add the inputs to the calculator tab. 2017-07-31 17:16:26 -07:00
2941252f7d Add a tabbed pane as the main focus of the window. 2017-07-31 16:56:38 -07:00
44ed0199d4 Add initialization code to AbacusController 2017-07-31 16:52:34 -07:00
5b582a7dbe Add FXML loading application. 2017-07-31 16:50:39 -07:00
a02086e791 Create new fxml file and controller for it. 2017-07-31 16:48:04 -07:00
Arthur Drobot
8666e96420 Remove unused code and functions in StandardPlugin. 2017-07-31 14:53:41 -07:00
Arthur Drobot
fd40e6b297 Rewrite exp. (Now works faster.) Add private factorial function to StandardPlugin as well. 2017-07-31 14:49:25 -07:00
Arthur Drobot
79ccd61af3 Add ceiling to NumberInterface and the two numbers that implement it. 2017-07-31 13:25:23 -07:00
Arthur Drobot
699ba9e193 Merge branch 'master' of https://github.com/DanilaFe/abacus 2017-07-31 12:40:19 -07:00
Arthur Drobot
e43f223086 Optimize log. 2017-07-31 12:39:56 -07:00
2dbc91f79e Merge branch 'master' of github.com:DanilaFe/abacus 2017-07-31 11:54:25 -07:00
Arthur Drobot
782636a982 Fix comment. 2017-07-31 10:28:39 -07:00
97d63489cc Focus on the text field on startup. 2017-07-30 21:25:21 -07:00
a0a4f1fbfe Add comments to the newly defined static functions. 2017-07-30 21:15:01 -07:00
763683b6b4 Move OP_CARET back into place. 2017-07-30 21:12:50 -07:00
3ce74303ed Format code. 2017-07-30 21:11:32 -07:00
122874b97a Move all functions to a static context, stopping unnecessary lookups. 2017-07-30 21:10:11 -07:00
0125980c5a Write tests involving plugin loading, and generalize token testing code. 2017-07-30 19:21:26 -07:00
cb7d0f309b Remove the window as a part of the Abacus class. 2017-07-30 15:00:08 -07:00
cb98601ae5 Add some comments. 2017-07-30 14:59:20 -07:00
67b95edd44 Write basic tokenizer tests. 2017-07-30 14:52:10 -07:00
960f891393 Remove abacus dependency from PluginManager. 2017-07-30 14:42:06 -07:00
b599bef775 Write more tests for the Lexer. 2017-07-30 14:12:17 -07:00
28004ed98d Write tests for special cases. 2017-07-30 00:46:42 -07:00
317cc552e6 Write basic tests for string matching. 2017-07-29 23:48:01 -07:00
43c11f8454 Move the source files into a new default directory. 2017-07-29 23:44:21 -07:00
3131d96d07 Merge branch 'new-parser-prep' into unit-tests 2017-07-29 23:42:23 -07:00
542f4b26ab Fix README formatting.
[ci skip]
2017-07-29 23:35:56 -07:00
d449e58888 Add the build badge to the README page.
[ci skip]
2017-07-29 23:35:15 -07:00
085569900b Add a .travis.yml to run TravisCI tests. 2017-07-29 23:29:10 -07:00
7b2ee1c87a Create empty class, getting ready to write tests. 2017-07-29 21:49:36 -07:00
274826cc09 Replace the old TreeBuilder with the new TreeBuilder. 2017-07-29 21:37:55 -07:00
bfee4ec322 Implement a LexerTokenizer and a ShuntingYard parser.
These are basically two pieces of the old TreeBuilder, but decoupled
and reimplemented conventionally.
2017-07-29 21:37:32 -07:00
bd1f7b8786 Add comments to the two parsing interfaces. 2017-07-29 21:36:39 -07:00
90c6625108 Change matches to store the string they matched. 2017-07-29 21:20:11 -07:00
a99b6b647f Implement the components of a new tree builder. 2017-07-29 21:02:41 -07:00
d12d53032b Merge branch 'architecture' 2017-07-29 19:30:36 -07:00
ff31dd6e47 Update README.md 2017-07-28 23:26:35 -07:00
9454620489 Remove precision specification as it seems detrimental. 2017-07-28 22:51:59 -07:00
1160768ee5 Allow plugins to register number implementations and use user's choice. 2017-07-28 22:17:22 -07:00
1ce9fc6b1c Add configuration object to Abacus. 2017-07-28 21:37:47 -07:00
acf3d85584 Merge branch 'configuration' into architecture 2017-07-28 21:31:45 -07:00
6c80d8fe93 Rewrite Abacus to be the central class of the application. 2017-07-28 21:25:02 -07:00
c230675855 Change precision to getMaxPrecision, as precision can be configured. 2017-07-28 20:04:13 -07:00
bd44307f2b Create a ConfigurationObject class. 2017-07-28 20:03:50 -07:00
a949a27da4 Make ClassFinder return generic List objects. 2017-07-28 19:36:34 -07:00
53 changed files with 2200 additions and 1013 deletions

1
.travis.yml Normal file
View File

@@ -0,0 +1 @@
language: java

View File

@@ -1,2 +1,26 @@
# abacus
[![Build Status](https://travis-ci.org/DanilaFe/abacus.svg?branch=master)](https://travis-ci.org/DanilaFe/abacus)
Summer project for NWAPW.
Created by Arthur Drobot, Danila Fedorin and Riley Jones.
## Project Description
Abacus is a calculator built with extensibility and usability in mind. It provides a plugin interface, via Java, as Lua provides too difficult to link up to the Java core. The description of the internals of the project can be found on the wiki page.
## Current State
Abacus is being built for the Northwest Advanced Programming Workshop, a 3 week program in which students work in treams to complete a single project, following principles of agile development. Because of its short timeframe, Abacus is not even close to completed state. Below is a list of the current features and problems.
- [x] Basic number class
- [x] Implementation of basic functions
- [x] Implementation of `exp`, `ln`, `sqrt` using the basic functions and Taylor Series
- [x] Plugin loading from JAR files
- [x] Regular expression pattern construction and matching
- [x] Infix and postfix operators
- [ ] __Correct__ handling of postfix operators (`12+!3` parses to `12!+3`, which is wrong)
- [ ] User-defined precision
## Project Proposal
>There is currently no calculator that is up to par with a sophisticated programmer's needs. The standard system ones are awful, not respecting the order of operations and having only a few basic functions programmed into them. The web ones are tied to the Internet and don't function offline. Physical ones like the TI-84 come close in terms of functionality, but they make the user have to switch between the computer and the device.
>
>My proposal is a more ergonomic calculator for advanced users. Of course, for a calculator, being able to do the actual math is a requirement. However, in this project I also would like to include other features that would make it much more pleasant to use. The first of these features is a wide collection of built in functions, designed with usefulness and consistency in mind. The second is scripting capabilities - most simply using Lua and its provided library. By allowing the users to script in a standardized language that isn't TI-BASIC, the calculator could simplify a variety of tasks and not have to clutter up the default provided functions with overly specific things. Lastly, it's important for the calculator to have a good design that doesn't get in the way of its use, on the two major desktop platforms (macOS and Windows).
>
>With these features I believe that this is a calculator that I would use (and frequently find myself wanting to use). It also seems to have a diverse array of tasks, such as UI design, implementing the math functions to be fast and optimized (fast inverse square root, anyone?), parsing code, and working with Lua integration.

View File

@@ -1,19 +1,12 @@
apply plugin: 'java'
apply plugin: 'application'
sourceSets {
main {
java {
srcDirs = ['src']
}
}
}
repositories {
mavenCentral()
}
dependencies {
compile 'com.moandjiezana.toml:toml4j:0.7.1'
testCompile 'junit:junit:4.12'
}

View File

@@ -0,0 +1,167 @@
package org.nwapw.abacus;
import org.nwapw.abacus.config.Configuration;
import org.nwapw.abacus.fx.AbacusApplication;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.parsing.LexerTokenizer;
import org.nwapw.abacus.parsing.ShuntingYardParser;
import org.nwapw.abacus.parsing.TreeBuilder;
import org.nwapw.abacus.plugin.ClassFinder;
import org.nwapw.abacus.plugin.PluginManager;
//import org.nwapw.abacus.plugin.StandardPlugin;
import org.nwapw.abacus.tree.NumberReducer;
import org.nwapw.abacus.tree.TreeNode;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
/**
* The main calculator class. This is responsible
* for piecing together all of the components, allowing
* their interaction with each other.
*/
public class Abacus {
/**
* The default implementation to use for the number representation.
*/
public static final Class<? extends NumberInterface> DEFAULT_NUMBER = NaiveNumber.class;
/**
* The file used for saving and loading configuration.
*/
public static final File CONFIG_FILE = new File("config.toml");
/**
* The plugin manager responsible for
* loading and unloading plugins,
* and getting functions from them.
*/
private PluginManager pluginManager;
/**
* The reducer used to evaluate the tree.
*/
private NumberReducer numberReducer;
/**
* The configuration loaded from a file.
*/
private Configuration configuration;
/**
* The tree builder used to construct a tree
* from a string.
*/
private TreeBuilder treeBuilder;
/**
* Creates a new instance of the Abacus calculator.
*/
public Abacus() {
pluginManager = new PluginManager();
numberReducer = new NumberReducer(this);
configuration = new Configuration(CONFIG_FILE);
configuration.saveTo(CONFIG_FILE);
LexerTokenizer lexerTokenizer = new LexerTokenizer();
ShuntingYardParser shuntingYardParser = new ShuntingYardParser(this);
treeBuilder = new TreeBuilder<>(lexerTokenizer, shuntingYardParser);
pluginManager.addListener(lexerTokenizer);
pluginManager.addListener(shuntingYardParser);
//pluginManager.addInstantiated(new StandardPlugin(pluginManager));
/*
try {
ClassFinder.loadJars("plugins")
.forEach(plugin -> pluginManager.addClass(plugin));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}//*/
pluginManager.load();
}
public void loadClass(Class<?> newClass){
pluginManager.addClass(newClass);
}
public void unloadClass(Class<?> newClass){
pluginManager.removeClass(newClass);
}
public static void main(String[] args) {
AbacusApplication.launch(AbacusApplication.class, args);
}
/**
* Gets the current tree builder.
*
* @return the main tree builder in this abacus instance.
*/
public TreeBuilder getTreeBuilder() {
return treeBuilder;
}
/**
* Gets the current plugin manager,
*
* @return the plugin manager in this abacus instance.
*/
public PluginManager getPluginManager() {
return pluginManager;
}
/**
* Get the reducer that is responsible for transforming
* an expression into a number.
*
* @return the number reducer in this abacus instance.
*/
public NumberReducer getNumberReducer() {
return numberReducer;
}
/**
* Gets the configuration object associated with this instance.
*
* @return the configuration object.
*/
public Configuration getConfiguration() {
return configuration;
}
/**
* Parses a string into a tree structure using the main
* tree builder.
*
* @param input the input string to parse
* @return the resulting tree, null if the tree builder or the produced tree are null.
*/
public TreeNode parseString(String input) {
return treeBuilder.fromString(input);
}
/**
* Evaluates the given tree using the main
* number reducer.
*
* @param tree the tree to reduce, must not be null.
* @return the resulting number, or null of the reduction failed.
*/
public NumberInterface evaluateTree(TreeNode tree) {
return tree.reduce(numberReducer);
}
/**
* Creates a number from a string.
*
* @param numberString the string to create the number from.
* @return the resulting number.
*/
public NumberInterface numberFromString(String numberString) {
Class<? extends NumberInterface> toInstantiate =
pluginManager.numberFor(configuration.getNumberImplementation());
if (toInstantiate == null) toInstantiate = DEFAULT_NUMBER;
try {
return toInstantiate.getConstructor(String.class).newInstance(numberString);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,83 @@
package org.nwapw.abacus.config;
import com.moandjiezana.toml.Toml;
import com.moandjiezana.toml.TomlWriter;
import java.io.File;
import java.io.IOException;
/**
* The configuration object that stores
* options that the user can change.
*/
public class Configuration {
/**
* The TOML writer used to write this configuration to a file.
*/
private static final TomlWriter TOML_WRITER = new TomlWriter();
/**
* The TOML reader used to load this config from a file.
*/
private static final Toml TOML_READER = new Toml();
/**
* The implementation of the number that should be used.
*/
private String numberImplementation = "naive";
/**
* Creates a new configuration with the given values.
* @param numberImplementation the number implementation, like "naive" or "precise"
*/
public Configuration(String numberImplementation){
this.numberImplementation = numberImplementation;
}
/**
* Loads a configuration from a given file, keeping non-specified fields default.
* @param fromFile the file to load from.
*/
public Configuration(File fromFile){
if(!fromFile.exists()) return;
copyFrom(TOML_READER.read(fromFile).to(Configuration.class));
}
/**
* Copies the values from the given configuration into this one.
* @param otherConfiguration the configuration to copy from.
*/
public void copyFrom(Configuration otherConfiguration){
this.numberImplementation = otherConfiguration.numberImplementation;
}
/**
* Saves this configuration to the given file, creating
* any directories that do not exist.
* @param file the file to save to.
*/
public void saveTo(File file){
if(file.getParentFile() != null) file.getParentFile().mkdirs();
try {
TOML_WRITER.write(this, file);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Gets the number implementation from this configuration.
* @return the number implementation.
*/
public String getNumberImplementation() {
return numberImplementation;
}
/**
* Sets the number implementation for the configuration
* @param numberImplementation the number implementation.
*/
public void setNumberImplementation(String numberImplementation) {
this.numberImplementation = numberImplementation;
}
}

View File

@@ -23,6 +23,7 @@ public abstract class Function {
/**
* Checks whether the given params will work for the given function.
*
* @param params the given params
* @return true if the params can be used with this function.
*/
@@ -31,6 +32,7 @@ public abstract class Function {
/**
* Internal apply implementation, which already receives appropriately promoted
* parameters that have bee run through matchesParams
*
* @param params the promoted parameters.
* @return the return value of the function.
*/
@@ -38,11 +40,12 @@ public abstract class Function {
/**
* Function to check, promote arguments and run the function.
*
* @param params the raw input parameters.
* @return the return value of the function, or null if an error occurred.
*/
public NumberInterface apply(NumberInterface...params) {
if(!matchesParams(params)) return null;
public NumberInterface apply(NumberInterface... params) {
if (!matchesParams(params)) return null;
return applyInternal(params);
}

View File

@@ -24,11 +24,12 @@ public class Operator {
/**
* Creates a new operator with the given parameters.
*
* @param associativity the associativity of the operator.
* @param precedence the precedence of the operator.
* @param function the function that the operator calls.
* @param precedence the precedence of the operator.
* @param function the function that the operator calls.
*/
public Operator(OperatorAssociativity associativity, OperatorType operatorType, int precedence, Function function){
public Operator(OperatorAssociativity associativity, OperatorType operatorType, int precedence, Function function) {
this.associativity = associativity;
this.type = operatorType;
this.precedence = precedence;
@@ -37,6 +38,7 @@ public class Operator {
/**
* Gets the operator's associativity.
*
* @return the associativity.
*/
public OperatorAssociativity getAssociativity() {
@@ -45,6 +47,7 @@ public class Operator {
/**
* Gets the operator's type.
*
* @return the type.
*/
public OperatorType getType() {
@@ -53,6 +56,7 @@ public class Operator {
/**
* Gets the operator's precedence.
*
* @return the precedence.
*/
public int getPrecedence() {
@@ -61,6 +65,7 @@ public class Operator {
/**
* Gets the operator's function.
*
* @return the function.
*/
public Function getFunction() {

View File

@@ -0,0 +1,24 @@
package org.nwapw.abacus.fx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* The main application class for JavaFX responsible for loading
* and displaying the fxml file.
*/
public class AbacusApplication extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent parent = FXMLLoader.load(getClass().getResource("/abacus.fxml"));
Scene mainScene = new Scene(parent, 320, 480);
primaryStage.setScene(mainScene);
primaryStage.setTitle("Abacus");
primaryStage.show();
}
}

View File

@@ -0,0 +1,152 @@
package org.nwapw.abacus.fx;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.text.Text;
import javafx.util.Callback;
import org.nwapw.abacus.Abacus;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.plugin.ClassFinder;
import org.nwapw.abacus.tree.TreeNode;
import java.io.IOException;
/**
* The controller for the abacus FX UI, responsible
* for all the user interaction.
*/
public class AbacusController {
/**
* Constant string that is displayed if the text could not be lexed or parsed.
*/
private static final String ERR_SYNTAX = "Syntax Error";
/**
* Constant string that is displayed if the tree could not be reduced.
*/
private static final String ERR_EVAL = "Evaluation Error";
@FXML
private TableView<HistoryModel> historyTable;
@FXML
private TableColumn<HistoryModel, String> inputColumn;
@FXML
private TableColumn<HistoryModel, String> parsedColumn;
@FXML
private TableColumn<HistoryModel, String> outputColumn;
@FXML
private Text outputText;
@FXML
private TextField inputField;
@FXML
private Button inputButton;
@FXML
private ComboBox<String> numberImplementationBox;
@FXML
private Button loadButton;
@FXML
private Button unloadButton;
@FXML
private TextField loadField;
/**
* The list of history entries, created by the users.
*/
private ObservableList<HistoryModel> historyData;
/**
* The abacus instance used for calculations and all
* other main processing code.
*/
private ObservableList<String> numberImplementationOptions;
private Abacus abacus;
@FXML
public void initialize(){
Callback<TableColumn<HistoryModel, String>, TableCell<HistoryModel, String>> cellFactory =
param -> new CopyableCell<>();
historyData = FXCollections.observableArrayList();
historyTable.setItems(historyData);
numberImplementationOptions = FXCollections.observableArrayList();
numberImplementationBox.setItems(numberImplementationOptions);
numberImplementationBox.valueProperty().addListener((observable, oldValue, newValue)
-> {
abacus.getConfiguration().setNumberImplementation(newValue);
abacus.getConfiguration().saveTo(Abacus.CONFIG_FILE);
});
historyTable.getSelectionModel().setCellSelectionEnabled(true);
inputColumn.setCellFactory(cellFactory);
inputColumn.setCellValueFactory(cell -> cell.getValue().inputProperty());
parsedColumn.setCellFactory(cellFactory);
parsedColumn.setCellValueFactory(cell -> cell.getValue().parsedProperty());
outputColumn.setCellFactory(cellFactory);
outputColumn.setCellValueFactory(cell -> cell.getValue().outputProperty());
abacus = new Abacus();
numberImplementationOptions.addAll(abacus.getPluginManager().getAllNumbers());
String actualImplementation = abacus.getConfiguration().getNumberImplementation();
String toSelect = (numberImplementationOptions.contains(actualImplementation)) ? actualImplementation : "naive";
numberImplementationBox.getSelectionModel().select(toSelect);
}
@FXML
private void performCalculation(){
inputButton.setDisable(true);
TreeNode constructedTree = abacus.parseString(inputField.getText());
if(constructedTree == null){
outputText.setText(ERR_SYNTAX);
inputButton.setDisable(false);
return;
}
NumberInterface evaluatedNumber = abacus.evaluateTree(constructedTree);
if(evaluatedNumber == null){
outputText.setText(ERR_EVAL);
inputButton.setDisable(false);
return;
}
outputText.setText(evaluatedNumber.toString());
historyData.add(new HistoryModel(inputField.getText(), constructedTree.toString(), evaluatedNumber.toString()));
inputButton.setDisable(false);
inputField.setText("");
}
@FXML
private void loadClass(){
try {
for(Class<?> plugin :ClassFinder.loadJars("plugins")){
String name = "";
//String name = plugin.getName();
while(!(name.indexOf('/') ==-1)){
name=name.substring(name.indexOf('/')+1);
}
if(loadField.getText().equals("")||loadField.getText().equals(name)||(loadField.getText()+".class").equals(name)){
//abacus.loadClass(plugin);
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
@FXML
private void unloadClass(){
try {
for(Class<?> plugin :ClassFinder.loadJars("plugins")){
String name = plugin.getName();
while(!(name.indexOf('/') ==-1)){
name=name.substring(name.indexOf('/')+1);
}
if(loadField.getText().equals("")||loadField.getText().equals(name)||(loadField.getText()+".class").equals(name)){
//abacus.unloadClass(plugin);
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,35 @@
package org.nwapw.abacus.fx;
import javafx.scene.control.TableCell;
import javafx.scene.input.MouseEvent;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
/**
* A cell that copies its value to the clipboard
* when double clicked.
* @param <S> The type of the table view generic type.
* @param <T> The type of the value contained in the cell.
*/
public class CopyableCell<S, T> extends TableCell<S, T> {
/**
* Creates a new copyable cell.
*/
public CopyableCell(){
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if(event.getClickCount() == 2){
Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new StringSelection(getText()), null);
}
});
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
setText((empty || item == null) ? null : item.toString());
setGraphic(null);
}
}

View File

@@ -0,0 +1,87 @@
package org.nwapw.abacus.fx;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* The data model used for storing history entries.
*/
public class HistoryModel {
/**
* The property used for displaying the column
* for the user input.
*/
private final StringProperty input;
/**
* The property used for displaying the column
* that contains the parsed input.
*/
private final StringProperty parsed;
/**
* The property used for displaying the column
* that contains the program output.
*/
private final StringProperty output;
/**
* Creates a new history model with the given variables.
* @param input the user input
* @param parsed the parsed input
* @param output the program output.
*/
public HistoryModel(String input, String parsed, String output){
this.input = new SimpleStringProperty();
this.parsed = new SimpleStringProperty();
this.output = new SimpleStringProperty();
this.input.setValue(input);
this.parsed.setValue(parsed);
this.output.setValue(output);
}
/**
* Gets the input property.
* @return the input property.
*/
public StringProperty inputProperty() {
return input;
}
/**
* Gets the input.
* @return the input.
*/
public String getInput() {
return input.get();
}
/**
* Gets the parsed input property.
* @return the parsed input property.
*/
public StringProperty parsedProperty() {
return parsed;
}
/**
* Gets the parsed input.
* @return the parsed input.
*/
public String getParsed() {
return parsed.get();
}
/**
* Gets the output property.
* @return the output property.
*/
public StringProperty outputProperty() {
return output;
}
/**
* Gets the program output.
* @return the output.
*/
public String getOutput() {
return output.get();
}
}

View File

@@ -10,15 +10,111 @@ import java.util.*;
/**
* A lexer that can generate tokens of a given type given a list of regular expressions
* to operate on.
*
* @param <T> the type used to identify which match belongs to which pattern.
*/
public class Lexer<T> {
/**
* The registered patterns.
*/
private Map<PatternEntry<T>, Pattern<T>> patterns;
/**
* Creates a new lexer with no registered patterns.
*/
public Lexer() {
patterns = new HashMap<>();
}
/**
* Registers a single pattern.
*
* @param pattern the pattern regex
* @param id the ID by which to identify the pattern.
*/
public void register(String pattern, T id) {
Pattern<T> compiledPattern = new Pattern<>(pattern, id);
if (compiledPattern.getHead() != null) patterns.put(new PatternEntry<>(pattern, id), compiledPattern);
}
/**
* Unregisters a pattern.
*
* @param pattern the pattern to unregister
* @param id the ID by which to identify the pattern.
*/
public void unregister(String pattern, T id) {
patterns.remove(new PatternEntry<>(pattern, id));
}
/**
* Reads one token from the given string.
*
* @param from the string to read from
* @param startAt the index to start at
* @param compare the comparator used to sort tokens by their ID.
* @return the best match.
*/
public Match<T> lexOne(String from, int startAt, Comparator<T> compare) {
ArrayList<Match<T>> matches = new ArrayList<>();
HashSet<PatternNode<T>> currentSet = new HashSet<>();
HashSet<PatternNode<T>> futureSet = new HashSet<>();
int index = startAt;
for (Pattern<T> pattern : patterns.values()) {
pattern.getHead().addInto(currentSet);
}
while (!currentSet.isEmpty()) {
for (PatternNode<T> node : currentSet) {
if (index < from.length() && node.matches(from.charAt(index))) {
node.addOutputsInto(futureSet);
} else if (node instanceof EndNode) {
matches.add(new Match<>(from.substring(startAt, index), ((EndNode<T>) node).getPatternId()));
}
}
HashSet<PatternNode<T>> tmp = currentSet;
currentSet = futureSet;
futureSet = tmp;
futureSet.clear();
index++;
}
matches.sort((a, b) -> compare.compare(a.getType(), b.getType()));
if (compare != null) {
matches.sort(Comparator.comparingInt(a -> a.getContent().length()));
}
return matches.isEmpty() ? null : matches.get(matches.size() - 1);
}
/**
* Reads all tokens from a string.
*
* @param from the string to start from.
* @param startAt the index to start at.
* @param compare the comparator used to sort matches by their IDs.
* @return the resulting list of matches, in order, or null on error.
*/
public List<Match<T>> lexAll(String from, int startAt, Comparator<T> compare) {
int index = startAt;
ArrayList<Match<T>> matches = new ArrayList<>();
Match<T> lastMatch = null;
while (index < from.length() && (lastMatch = lexOne(from, index, compare)) != null) {
int length = lastMatch.getContent().length();
if (length == 0) return null;
matches.add(lastMatch);
index += length;
}
if (lastMatch == null) return null;
return matches;
}
/**
* An entry that represents a pattern that has been registered with the lexer.
*
* @param <T> the type used to identify the pattern.
*/
private static class PatternEntry<T>{
private static class PatternEntry<T> {
/**
* The name of the entry.
*/
@@ -30,10 +126,11 @@ public class Lexer<T> {
/**
* Creates a new pattern entry with the given name and id.
*
* @param name the name of the pattern entry.
* @param id the id of the pattern entry.
* @param id the id of the pattern entry.
*/
public PatternEntry(String name, T id){
public PatternEntry(String name, T id) {
this.name = name;
this.id = id;
}
@@ -51,93 +148,4 @@ public class Lexer<T> {
}
}
/**
* The registered patterns.
*/
private Map<PatternEntry<T>, Pattern<T>> patterns;
/**
* Creates a new lexer with no registered patterns.
*/
public Lexer(){
patterns = new HashMap<>();
}
/**
* Registers a single pattern.
* @param pattern the pattern regex
* @param id the ID by which to identify the pattern.
*/
public void register(String pattern, T id){
Pattern<T> compiledPattern = new Pattern<>(pattern, id);
if(compiledPattern.getHead() != null) patterns.put(new PatternEntry<>(pattern, id), compiledPattern);
}
/**
* Unregisters a pattern.
* @param pattern the pattern to unregister
* @param id the ID by which to identify the pattern.
*/
public void unregister(String pattern, T id){
patterns.remove(new PatternEntry<>(pattern, id));
}
/**
* Reads one token from the given string.
* @param from the string to read from
* @param startAt the index to start at
* @param compare the comparator used to sort tokens by their ID.
* @return the best match.
*/
public Match<T> lexOne(String from, int startAt, Comparator<T> compare){
ArrayList<Match<T>> matches = new ArrayList<>();
HashSet<PatternNode<T>> currentSet = new HashSet<>();
HashSet<PatternNode<T>> futureSet = new HashSet<>();
int index = startAt;
for(Pattern<T> pattern : patterns.values()){
pattern.getHead().addInto(currentSet);
}
while(!currentSet.isEmpty()){
for(PatternNode<T> node : currentSet){
if(index < from.length() && node.matches(from.charAt(index))) {
node.addOutputsInto(futureSet);
} else if(node instanceof EndNode){
matches.add(new Match<>(startAt, index, ((EndNode<T>) node).getPatternId()));
}
}
HashSet<PatternNode<T>> tmp = currentSet;
currentSet = futureSet;
futureSet = tmp;
futureSet.clear();
index++;
}
matches.sort((a, b) -> compare.compare(a.getType(), b.getType()));
if(compare != null) {
matches.sort(Comparator.comparingInt(a -> a.getTo() - a.getFrom()));
}
return matches.isEmpty() ? null : matches.get(matches.size() - 1);
}
/**
* Reads all tokens from a string.
* @param from the string to start from.
* @param startAt the index to start at.
* @param compare the comparator used to sort matches by their IDs.
* @return the resulting list of matches, in order, or null on error.
*/
public List<Match<T>> lexAll(String from, int startAt, Comparator<T> compare){
int index = startAt;
ArrayList<Match<T>> matches = new ArrayList<>();
Match<T> lastMatch = null;
while(index < from.length() && (lastMatch = lexOne(from, index, compare)) != null){
if(lastMatch.getTo() == lastMatch.getFrom()) return null;
matches.add(lastMatch);
index += lastMatch.getTo() - lastMatch.getFrom();
}
if(lastMatch == null) return null;
return matches;
}
}

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.lexing.pattern;
/**
* A pattern node that matches any character.
*
* @param <T> the type that's used to tell which pattern this node belongs to.
*/
public class AnyNode<T> extends PatternNode<T> {

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.lexing.pattern;
/**
* A node that represents a successful match.
*
* @param <T> the type that's used to tell which pattern this node belongs to.
*/
public class EndNode<T> extends PatternNode<T> {
@@ -13,17 +14,19 @@ public class EndNode<T> extends PatternNode<T> {
/**
* Creates a new end node with the given ID.
*
* @param patternId the pattern ID.
*/
public EndNode(T patternId){
public EndNode(T patternId) {
this.patternId = patternId;
}
/**
* Gets the pattern ID.
*
* @return the pattern ID.
*/
public T getPatternId(){
public T getPatternId() {
return patternId;
}

View File

@@ -1,17 +1,17 @@
package org.nwapw.abacus.lexing.pattern;
import java.util.ArrayList;
import java.util.Collection;
/**
* A node that is used as structural glue in pattern compilation.
*
* @param <T> the type that's used to tell which pattern this node belongs to.
*/
public class LinkNode<T> extends PatternNode<T> {
@Override
public void addInto(Collection<PatternNode<T>> into) {
if(!into.contains(this)) {
if (!into.contains(this)) {
into.add(this);
addOutputsInto(into);
}

View File

@@ -0,0 +1,47 @@
package org.nwapw.abacus.lexing.pattern;
/**
* A match that has been generated by the lexer.
*
* @param <T> the type used to represent the ID of the pattern this match belongs to.
*/
public class Match<T> {
/**
* The content of this match.
*/
private String content;
/**
* The pattern type this match matched.
*/
private T type;
/**
* Creates a new match with the given parameters.
*
* @param content the content of this match.
* @param type the type of the match.
*/
public Match(String content, T type) {
this.content = content;
this.type = type;
}
/**
* Gets the content of this match.
*
* @return the content.
*/
public String getContent() {
return content;
}
/**
* Gets the pattern type of the node.
*
* @return the ID of the pattern that this match matched.
*/
public T getType() {
return type;
}
}

View File

@@ -8,6 +8,7 @@ import java.util.function.Function;
/**
* A pattern that can be compiled from a string and used in lexing.
*
* @param <T> the type that is used to identify and sort this pattern.
*/
public class Pattern<T> {
@@ -40,13 +41,53 @@ public class Pattern<T> {
put('?', Pattern.this::transformQuestion);
}};
/**
* Creates / compiles a new pattern with the given id from the given string.
*
* @param from the string to compile a pattern from.
* @param id the ID to use.
*/
public Pattern(String from, T id) {
this.id = id;
index = 0;
source = from;
PatternChain<T> chain = parseSegment(false);
if (chain == null) {
head = null;
} else {
chain.append(new EndNode<>(id));
head = chain.head;
}
}
/**
* Removes all characters that are considered "special" from
* the given string.
*
* @param from the string to sanitize.
* @return the resulting string.
*/
public static String sanitize(String from) {
Pattern<Integer> pattern = new Pattern<>("", 0);
from = from.replace(".", "\\.");
from = from.replace("|", "\\|");
from = from.replace("(", "\\(");
from = from.replace(")", "\\)");
for (Character key : pattern.operations.keySet()) {
from = from.replace("" + key, "\\" + key);
}
return from;
}
/**
* A regex operator function that turns the chain
* into a one-or-more chain.
*
* @param chain the chain to transform.
* @return the modified chain.
*/
private PatternChain<T> transformPlus(PatternChain<T> chain){
private PatternChain<T> transformPlus(PatternChain<T> chain) {
chain.tail.outputStates.add(chain.head);
return chain;
}
@@ -54,10 +95,11 @@ public class Pattern<T> {
/**
* A regex operator function that turns the chain
* into a zero-or-more chain.
*
* @param chain the chain to transform.
* @return the modified chain.
*/
private PatternChain<T> transformStar(PatternChain<T> chain){
private PatternChain<T> transformStar(PatternChain<T> chain) {
LinkNode<T> newTail = new LinkNode<>();
LinkNode<T> newHead = new LinkNode<>();
newHead.outputStates.add(chain.head);
@@ -72,10 +114,11 @@ public class Pattern<T> {
/**
* A regex operator function that turns the chain
* into a zero-or-one chain.
*
* @param chain the chain to transform.
* @return the modified chain.
*/
private PatternChain<T> transformQuestion(PatternChain<T> chain){
private PatternChain<T> transformQuestion(PatternChain<T> chain) {
LinkNode<T> newTail = new LinkNode<>();
LinkNode<T> newHead = new LinkNode<>();
newHead.outputStates.add(chain.head);
@@ -88,14 +131,15 @@ public class Pattern<T> {
/**
* Combines a collection of chains into one OR chain.
*
* @param collection the collection of chains to combine.
* @return the resulting OR chain.
*/
private PatternChain<T> combineChains(Collection<PatternChain<T>> collection){
private PatternChain<T> combineChains(Collection<PatternChain<T>> collection) {
LinkNode<T> head = new LinkNode<>();
LinkNode<T> tail = new LinkNode<>();
PatternChain<T> newChain = new PatternChain<>(head, tail);
for(PatternChain<T> chain : collection){
for (PatternChain<T> chain : collection) {
head.outputStates.add(chain.head);
chain.tail.outputStates.add(tail);
}
@@ -104,105 +148,108 @@ public class Pattern<T> {
/**
* Parses a single value from the input into a chain.
*
* @return the resulting chain, or null on error.
*/
private PatternChain<T> parseValue(){
if(index >= source.length()) return null;
if(source.charAt(index) == '\\'){
if(++index >= source.length()) return null;
private PatternChain<T> parseValue() {
if (index >= source.length()) return null;
if (source.charAt(index) == '\\') {
if (++index >= source.length()) return null;
}
return new PatternChain<>(new ValueNode<>(source.charAt(index++)));
}
/**
* Parses a [] range from the input into a chain.
*
* @return the resulting chain, or null on error.
*/
private PatternChain<T> parseOr(){
private PatternChain<T> parseOr() {
Stack<PatternChain<T>> orStack = new Stack<>();
index++;
while(index < source.length() && source.charAt(index) != ']'){
if(source.charAt(index) == '-'){
while (index < source.length() && source.charAt(index) != ']') {
if (source.charAt(index) == '-') {
index++;
if(orStack.empty() || orStack.peek().tail.range() == '\0') return null;
if (orStack.empty() || orStack.peek().tail.range() == '\0') return null;
PatternChain<T> bottomRange = orStack.pop();
PatternChain<T> topRange = parseValue();
if(topRange == null || topRange.tail.range() == '\0') return null;
if (topRange == null || topRange.tail.range() == '\0') return null;
orStack.push(new PatternChain<>(new RangeNode<>(bottomRange.tail.range(), topRange.tail.range())));
} else {
PatternChain<T> newChain = parseValue();
if(newChain == null) return null;
if (newChain == null) return null;
orStack.push(newChain);
}
}
if(index++ >= source.length()) return null;
if (index++ >= source.length()) return null;
return (orStack.size() == 1) ? orStack.pop() : combineChains(orStack);
}
/**
* Parses a repeatable segment from the input into a chain
*
* @param isSubsegment whether the segment is a sub-expression "()", and therefore
* whether to expect a closing brace.
* @return the resulting chain, or null on error.
*/
private PatternChain<T> parseSegment(boolean isSubsegment){
if(index >= source.length() || ((source.charAt(index) != '(') && isSubsegment)) return null;
if(isSubsegment) index++;
private PatternChain<T> parseSegment(boolean isSubsegment) {
if (index >= source.length() || ((source.charAt(index) != '(') && isSubsegment)) return null;
if (isSubsegment) index++;
Stack<PatternChain<T>> orChain = new Stack<>();
PatternChain<T> fullChain = new PatternChain<>();
PatternChain<T> currentChain = null;
while (index < source.length() && source.charAt(index) != ')'){
while (index < source.length() && source.charAt(index) != ')') {
char currentChar = source.charAt(index);
if(operations.containsKey(currentChar)){
if(currentChain == null) return null;
if (operations.containsKey(currentChar)) {
if (currentChain == null) return null;
currentChain = operations.get(currentChar).apply(currentChain);
fullChain.append(currentChain);
currentChain = null;
index++;
} else if(currentChar == '|'){
if(currentChain == null) return null;
} else if (currentChar == '|') {
if (currentChain == null) return null;
fullChain.append(currentChain);
orChain.push(fullChain);
currentChain = null;
fullChain = new PatternChain<>();
if(++index >= source.length()) return null;
} else if(currentChar == '('){
if(currentChain != null) {
if (++index >= source.length()) return null;
} else if (currentChar == '(') {
if (currentChain != null) {
fullChain.append(currentChain);
}
currentChain = parseSegment(true);
if(currentChain == null) return null;
} else if(currentChar == '['){
if(currentChain != null){
if (currentChain == null) return null;
} else if (currentChar == '[') {
if (currentChain != null) {
fullChain.append(currentChain);
}
currentChain = parseOr();
if(currentChain == null) return null;
} else if(currentChar == '.'){
if(currentChain != null){
if (currentChain == null) return null;
} else if (currentChar == '.') {
if (currentChain != null) {
fullChain.append(currentChain);
}
currentChain = new PatternChain<>(new AnyNode<>());
index++;
} else {
if(currentChain != null){
if (currentChain != null) {
fullChain.append(currentChain);
}
currentChain = parseValue();
if(currentChain == null) return null;
if (currentChain == null) return null;
}
}
if(!(!isSubsegment || (index < source.length() && source.charAt(index) == ')'))) return null;
if(isSubsegment) index++;
if (!(!isSubsegment || (index < source.length() && source.charAt(index) == ')'))) return null;
if (isSubsegment) index++;
if(currentChain != null) fullChain.append(currentChain);
if(!orChain.empty()){
if (currentChain != null) fullChain.append(currentChain);
if (!orChain.empty()) {
orChain.push(fullChain);
fullChain = combineChains(orChain);
}
@@ -210,48 +257,12 @@ public class Pattern<T> {
return fullChain;
}
/**
* Creates / compiles a new pattern with the given id from the given string.
* @param from the string to compile a pattern from.
* @param id the ID to use.
*/
public Pattern(String from, T id){
this.id = id;
index = 0;
source = from;
PatternChain<T> chain = parseSegment(false);
if(chain == null) {
head = null;
} else {
chain.append(new EndNode<>(id));
head = chain.head;
}
}
/**
* Gets the head PatternNode, for use in matching
*
* @return the pattern node.
*/
public PatternNode<T> getHead() {
return head;
}
/**
* Removes all characters that are considered "special" from
* the given string.
* @param from the string to sanitize.
* @return the resulting string.
*/
public static String sanitize(String from){
Pattern<Integer> pattern = new Pattern<>("", 0);
from = from.replace(".", "\\.");
from = from.replace("|", "\\|");
from = from.replace("(", "\\(");
from = from.replace(")", "\\)");
for(Character key : pattern.operations.keySet()){
from = from.replace("" + key, "\\" + key);
}
return from;
}
}

View File

@@ -3,6 +3,7 @@ package org.nwapw.abacus.lexing.pattern;
/**
* A chain of nodes that can be treated as a single unit.
* Used during pattern compilation.
*
* @param <T> the type used to identify which pattern has been matched.
*/
public class PatternChain<T> {
@@ -18,26 +19,28 @@ public class PatternChain<T> {
/**
* Creates a new chain with the given start and end.
*
* @param head the start of the chain.
* @param tail the end of the chain.
*/
public PatternChain(PatternNode<T> head, PatternNode<T> tail){
public PatternChain(PatternNode<T> head, PatternNode<T> tail) {
this.head = head;
this.tail = tail;
}
/**
* Creates a chain that starts and ends with the same node.
*
* @param node the node to use.
*/
public PatternChain(PatternNode<T> node){
public PatternChain(PatternNode<T> node) {
this(node, node);
}
/**
* Creates an empty chain.
*/
public PatternChain(){
public PatternChain() {
this(null);
}
@@ -45,10 +48,11 @@ public class PatternChain<T> {
* Appends the other chain to this one. This modifies
* the nodes, as well.
* If this chain is empty, it is set to the other.
*
* @param other the other chain to append.
*/
public void append(PatternChain<T> other){
if(other.head == null || tail == null) {
public void append(PatternChain<T> other) {
if (other.head == null || tail == null) {
this.head = other.head;
this.tail = other.tail;
} else {
@@ -61,10 +65,11 @@ public class PatternChain<T> {
* Appends a single node to this chain. This modifies
* the nodes, as well.
* If this chain is empty, it is set to the node.
*
* @param node the node to append to this chain.
*/
public void append(PatternNode<T> node){
if(tail == null){
public void append(PatternNode<T> node) {
if (tail == null) {
head = tail = node;
} else {
tail.outputStates.add(node);

View File

@@ -1,6 +1,5 @@
package org.nwapw.abacus.lexing.pattern;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -9,6 +8,7 @@ import java.util.Set;
* A base class for a pattern node. Provides all functions
* necessary for matching, and is constructed by a Pattern instance
* from a string.
*
* @param <T> the type that's used to tell which pattern this node belongs to.
*/
public class PatternNode<T> {
@@ -22,42 +22,46 @@ public class PatternNode<T> {
/**
* Creates a new pattern node.
*/
public PatternNode(){
public PatternNode() {
outputStates = new HashSet<>();
}
/**
* Determines whether the current input character can
* be matched by this node.
*
* @param other the character being matched.
* @return true if the character can be matched, false otherwise.
*/
public boolean matches(char other){
public boolean matches(char other) {
return false;
}
/**
* If this node can be used as part of a range, returns that value.
*
* @return a NULL terminator if this character cannot be converted
* into a range bound, or the appropriate range bound if it can.
*/
public char range(){
public char range() {
return '\0';
}
/**
* Adds this node in a collection of other nodes.
*
* @param into the collection to add into.
*/
public void addInto(Collection<PatternNode<T>> into){
public void addInto(Collection<PatternNode<T>> into) {
into.add(this);
}
/**
* Adds the node's children into a collection of other nodes.
*
* @param into the collection to add into.
*/
public void addOutputsInto(Collection<PatternNode<T>> into){
public void addOutputsInto(Collection<PatternNode<T>> into) {
outputStates.forEach(e -> e.addInto(into));
}

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.lexing.pattern;
/**
* A node that matches a range of characters.
*
* @param <T> the type that's used to tell which pattern this node belongs to.
*/
public class RangeNode<T> extends PatternNode<T> {
@@ -17,10 +18,11 @@ public class RangeNode<T> extends PatternNode<T> {
/**
* Creates a new range node from the given range.
*
* @param from the bottom bound of the range.
* @param to the top bound of hte range.
* @param to the top bound of hte range.
*/
public RangeNode(char from, char to){
public RangeNode(char from, char to) {
this.from = from;
this.to = to;
}

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.lexing.pattern;
/**
* A node that matches a single value.
*
* @param <T> the type that's used to tell which pattern this node belongs to.
*/
public class ValueNode<T> extends PatternNode<T> {
@@ -13,9 +14,10 @@ public class ValueNode<T> extends PatternNode<T> {
/**
* Creates a new node that matches the given character.
*
* @param value
*/
public ValueNode(char value){
public ValueNode(char value) {
this.value = value;
}

View File

@@ -5,19 +5,6 @@ package org.nwapw.abacus.number;
*/
public class NaiveNumber implements NumberInterface {
/**
* The value of this number.
*/
private double value;
/**
* Creates a new NaiveNumber with the given value.
* @param value the value to use.
*/
public NaiveNumber(double value) {
this.value = value;
}
/**
* The number zero.
*/
@@ -26,30 +13,51 @@ public class NaiveNumber implements NumberInterface {
* The number one.
*/
public static final NaiveNumber ONE = new NaiveNumber(1);
/**
* The value of this number.
*/
private double value;
/**
* Creates a new NaiveNumber with the given string.
*
* @param value the value, which will be parsed as a double.
*/
public NaiveNumber(String value) {
this(Double.parseDouble(value));
}
/**
* Creates a new NaiveNumber with the given value.
*
* @param value the value to use.
*/
public NaiveNumber(double value) {
this.value = value;
}
@Override
public int precision() {
public int getMaxPrecision() {
return 18;
}
@Override
public NumberInterface multiply(NumberInterface multiplier) {
return new NaiveNumber(value * ((NaiveNumber)multiplier).value);
return new NaiveNumber(value * ((NaiveNumber) multiplier).value);
}
@Override
public NumberInterface divide(NumberInterface divisor) {
return new NaiveNumber(value / ((NaiveNumber)divisor).value);
return new NaiveNumber(value / ((NaiveNumber) divisor).value);
}
@Override
public NumberInterface add(NumberInterface summand) {
return new NaiveNumber(value + ((NaiveNumber)summand).value);
return new NaiveNumber(value + ((NaiveNumber) summand).value);
}
@Override
public NumberInterface subtract(NumberInterface subtrahend) {
return new NaiveNumber(value - ((NaiveNumber)subtrahend).value);
return new NaiveNumber(value - ((NaiveNumber) subtrahend).value);
}
@Override
@@ -59,16 +67,16 @@ public class NaiveNumber implements NumberInterface {
@Override
public NumberInterface intPow(int exponent) {
if(exponent == 0){
if (exponent == 0) {
return NaiveNumber.ONE;
}
boolean takeReciprocal = exponent < 0;
exponent = Math.abs(exponent);
NumberInterface power = this;
for(int currentExponent = 1; currentExponent < exponent; currentExponent++){
for (int currentExponent = 1; currentExponent < exponent; currentExponent++) {
power = power.multiply(this);
}
if(takeReciprocal){
if (takeReciprocal) {
power = NaiveNumber.ONE.divide(power);
}
return power;
@@ -85,16 +93,21 @@ public class NaiveNumber implements NumberInterface {
return this.compareTo(ZERO);
}
@Override
public int ceiling() {
return (int) Math.ceil(value);
}
@Override
public NumberInterface promoteTo(Class<? extends NumberInterface> toClass) {
if(toClass == this.getClass()) return this;
else if(toClass == PreciseNumber.class){
if (toClass == this.getClass()) return this;
else if (toClass == PreciseNumber.class) {
return new PreciseNumber(Double.toString(value));
}
return null;
}
public String toString(){
public String toString() {
double shiftBy = Math.pow(10, 10);
return Double.toString(Math.round(value * shiftBy) / shiftBy);
}

View File

@@ -6,35 +6,43 @@ package org.nwapw.abacus.number;
public interface NumberInterface {
/**
* The precision to which this number operates.
* The maximum precision to which this number operates.
*
* @return the precision.
*/
int precision();
int getMaxPrecision();
/**
* Multiplies this number by another, returning
* a new number instance.
*
* @param multiplier the multiplier
* @return the result of the multiplication.
*/
NumberInterface multiply(NumberInterface multiplier);
/**
* Divides this number by another, returning
* a new number instance.
*
* @param divisor the divisor
* @return the result of the division.
*/
NumberInterface divide(NumberInterface divisor);
/**
* Adds this number to another, returning
* a new number instance.
*
* @param summand the summand
* @return the result of the summation.
*/
NumberInterface add(NumberInterface summand);
/**
* Subtracts another number from this number,
* a new number instance.
*
* @param subtrahend the subtrahend.
* @return the result of the subtraction.
*/
@@ -43,12 +51,14 @@ public interface NumberInterface {
/**
* Returns a new instance of this number with
* the sign flipped.
*
* @return the new instance.
*/
NumberInterface negate();
/**
* Raises this number to an integer power.
*
* @param exponent the exponent to which to take the number.
* @return the resulting value.
*/
@@ -56,6 +66,7 @@ public interface NumberInterface {
/**
* Compares this number to another.
*
* @param number the number to compare to.
* @return same as Integer.compare();
*/
@@ -63,12 +74,20 @@ public interface NumberInterface {
/**
* Same as Math.signum().
*
* @return 1 if this number is positive, -1 if this number is negative, 0 if this number is 0.
*/
int signum();
/**
* Returns the least integer greater than or equal to the number.
* @return the least integer >= the number, if int can hold the value.
*/
int ceiling();
/**
* Promotes this class to another number class.
*
* @param toClass the class to promote to.
* @return the resulting new instance.
*/

View File

@@ -1,11 +1,13 @@
package org.nwapw.abacus.number;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
public class PreciseNumber implements NumberInterface{
/**
* A number that uses a BigDecimal to store its value,
* leading to infinite possible precision.
*/
public class PreciseNumber implements NumberInterface {
/**
* The number one.
@@ -27,34 +29,36 @@ public class PreciseNumber implements NumberInterface{
/**
* Constructs a precise number from the given string.
*
* @param string a string representation of the number meeting the same conditions
* as the BidDecimal(String) constructor.
*/
public PreciseNumber(String string){
public PreciseNumber(String string) {
value = new BigDecimal(string);
}
/**
* Constructs a precise number from the given BigDecimal.
*
* @param value a BigDecimal object representing the value of the number.
*/
public PreciseNumber(BigDecimal value){
public PreciseNumber(BigDecimal value) {
this.value = value;
}
@Override
public int precision() {
return 54;
public int getMaxPrecision() {
return 65;
}
@Override
public NumberInterface multiply(NumberInterface multiplier) {
return new PreciseNumber(value.multiply(((PreciseNumber) multiplier).value));
return new PreciseNumber(this.value.multiply(((PreciseNumber) multiplier).value));
}
@Override
public NumberInterface divide(NumberInterface divisor) {
return new PreciseNumber(value.divide(((PreciseNumber) divisor).value, this.precision(), RoundingMode.HALF_UP));
return new PreciseNumber(value.divide(((PreciseNumber) divisor).value, this.getMaxPrecision(), RoundingMode.HALF_UP));
}
@Override
@@ -69,16 +73,16 @@ public class PreciseNumber implements NumberInterface{
@Override
public NumberInterface intPow(int exponent) {
if(exponent == 0){
if (exponent == 0) {
return PreciseNumber.ONE;
}
boolean takeReciprocal = exponent < 0;
exponent = Math.abs(exponent);
NumberInterface power = this;
for(int currentExponent = 1; currentExponent < exponent; currentExponent++){
for (int currentExponent = 1; currentExponent < exponent; currentExponent++) {
power = power.multiply(this);
}
if(takeReciprocal){
if (takeReciprocal) {
power = PreciseNumber.ONE.divide(power);
}
return power;
@@ -95,13 +99,18 @@ public class PreciseNumber implements NumberInterface{
}
@Override
public NumberInterface negate(){
public int ceiling() {
return (int) Math.ceil(value.doubleValue());
}
@Override
public NumberInterface negate() {
return new PreciseNumber(value.negate());
}
@Override
public NumberInterface promoteTo(Class<? extends NumberInterface> toClass) {
if(toClass == this.getClass()){
if (toClass == this.getClass()) {
return this;
}
return null;
@@ -109,7 +118,7 @@ public class PreciseNumber implements NumberInterface{
@Override
public String toString() {
BigDecimal rounded = value.setScale(precision() - 4, RoundingMode.HALF_UP);
BigDecimal rounded = value.setScale(getMaxPrecision() - 15, RoundingMode.HALF_UP);
return rounded.stripTrailingZeros().toPlainString();
}
}

View File

@@ -0,0 +1,67 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.lexing.Lexer;
import org.nwapw.abacus.lexing.pattern.Match;
import org.nwapw.abacus.lexing.pattern.Pattern;
import org.nwapw.abacus.plugin.PluginListener;
import org.nwapw.abacus.plugin.PluginManager;
import org.nwapw.abacus.tree.TokenType;
import java.util.Comparator;
import java.util.List;
/**
* A tokenzier that uses the lexer class and registered function and operator
* names to turn input into tokens in O(n) time.
*/
public class LexerTokenizer implements Tokenizer<Match<TokenType>>, PluginListener {
/**
* Comparator used to sort the tokens produced by the lexer.
*/
protected static final Comparator<TokenType> TOKEN_SORTER = Comparator.comparingInt(e -> e.priority);
/**
* The lexer instance used to turn strings into matches.
*/
private Lexer<TokenType> lexer;
/**
* Creates a new lexer tokenizer.
*/
public LexerTokenizer() {
lexer = new Lexer<TokenType>() {{
register(" ", TokenType.WHITESPACE);
register(",", TokenType.COMMA);
register("[0-9]*(\\.[0-9]+)?", TokenType.NUM);
register("\\(", TokenType.OPEN_PARENTH);
register("\\)", TokenType.CLOSE_PARENTH);
}};
}
@Override
public List<Match<TokenType>> tokenizeString(String string) {
return lexer.lexAll(string, 0, TOKEN_SORTER);
}
@Override
public void onLoad(PluginManager manager) {
for (String operator : manager.getAllOperators()) {
lexer.register(Pattern.sanitize(operator), TokenType.OP);
}
for (String function : manager.getAllFunctions()) {
lexer.register(Pattern.sanitize(function), TokenType.FUNCTION);
}
}
@Override
public void onUnload(PluginManager manager) {
for (String operator : manager.getAllOperators()) {
lexer.unregister(Pattern.sanitize(operator), TokenType.OP);
}
for (String function : manager.getAllFunctions()) {
lexer.unregister(Pattern.sanitize(function), TokenType.FUNCTION);
}
}
}

View File

@@ -0,0 +1,22 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.tree.TreeNode;
import java.util.List;
/**
* An itnerface that provides the ability to convert a list of tokens
* into a parse tree.
*
* @param <T> the type of tokens accepted by this parser.
*/
public interface Parser<T> {
/**
* Constructs a tree out of the given tokens.
*
* @param tokens the tokens to construct a tree from.
* @return the constructed tree, or null on error.
*/
public TreeNode constructTree(List<T> tokens);
}

View File

@@ -0,0 +1,176 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.Abacus;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.function.OperatorType;
import org.nwapw.abacus.lexing.pattern.Match;
import org.nwapw.abacus.plugin.PluginListener;
import org.nwapw.abacus.plugin.PluginManager;
import org.nwapw.abacus.tree.*;
import java.util.*;
/**
* A parser that uses shunting yard to rearranged matches into postfix
* and then convert them into a parse tree.
*/
public class ShuntingYardParser implements Parser<Match<TokenType>>, PluginListener {
/**
* The Abacus instance used to create number instances.
*/
private Abacus abacus;
/**
* Map of operator precedences, loaded from the plugin operators.
*/
private Map<String, Integer> precedenceMap;
/**
* Map of operator associativity, loaded from the plugin operators.
*/
private Map<String, OperatorAssociativity> associativityMap;
/**
* Map of operator types, loaded from plugin operators.
*/
private Map<String, OperatorType> typeMap;
/**
* Creates a new Shunting Yard parser with the given Abacus instance.
*
* @param abacus the abacus instance.
*/
public ShuntingYardParser(Abacus abacus) {
this.abacus = abacus;
precedenceMap = new HashMap<>();
associativityMap = new HashMap<>();
typeMap = new HashMap<>();
}
/**
* Rearranges tokens into a postfix list, using Shunting Yard.
*
* @param from the tokens to be rearranged.
* @return the resulting list of rearranged tokens.
*/
public List<Match<TokenType>> intoPostfix(List<Match<TokenType>> from) {
ArrayList<Match<TokenType>> output = new ArrayList<>();
Stack<Match<TokenType>> tokenStack = new Stack<>();
while (!from.isEmpty()) {
Match<TokenType> match = from.remove(0);
TokenType matchType = match.getType();
if (matchType == TokenType.NUM) {
output.add(match);
} else if (matchType == TokenType.FUNCTION) {
output.add(new Match<>("", TokenType.INTERNAL_FUNCTION_END));
tokenStack.push(match);
} else if (matchType == TokenType.OP) {
String tokenString = match.getContent();
OperatorType type = typeMap.get(tokenString);
int precedence = precedenceMap.get(tokenString);
OperatorAssociativity associativity = associativityMap.get(tokenString);
if (type == OperatorType.UNARY_POSTFIX) {
output.add(match);
continue;
}
while (!tokenStack.empty()) {
Match<TokenType> otherMatch = tokenStack.peek();
TokenType otherMatchType = otherMatch.getType();
if (!(otherMatchType == TokenType.OP || otherMatchType == TokenType.FUNCTION)) break;
if (otherMatchType == TokenType.OP) {
int otherPrecedence = precedenceMap.get(match.getContent());
if (otherPrecedence < precedence ||
(associativity == OperatorAssociativity.RIGHT && otherPrecedence == precedence)) {
break;
}
}
output.add(tokenStack.pop());
}
tokenStack.push(match);
} else if (matchType == TokenType.OPEN_PARENTH) {
tokenStack.push(match);
} else if (matchType == TokenType.CLOSE_PARENTH || matchType == TokenType.COMMA) {
while (!tokenStack.empty() && tokenStack.peek().getType() != TokenType.OPEN_PARENTH) {
output.add(tokenStack.pop());
}
if (tokenStack.empty()) return null;
if (matchType == TokenType.CLOSE_PARENTH) {
tokenStack.pop();
}
}
}
while (!tokenStack.empty()) {
Match<TokenType> match = tokenStack.peek();
TokenType matchType = match.getType();
if (!(matchType == TokenType.OP || matchType == TokenType.FUNCTION)) return null;
output.add(tokenStack.pop());
}
return output;
}
/**
* Constructs a tree recursively from a list of tokens.
*
* @param matches the list of tokens from the source string.
* @return the construct tree expression.
*/
public TreeNode constructRecursive(List<Match<TokenType>> matches) {
if (matches.size() == 0) return null;
Match<TokenType> match = matches.remove(0);
TokenType matchType = match.getType();
if (matchType == TokenType.OP) {
String operator = match.getContent();
OperatorType type = typeMap.get(operator);
if (type == OperatorType.BINARY_INFIX) {
TreeNode right = constructRecursive(matches);
TreeNode left = constructRecursive(matches);
if (left == null || right == null) return null;
else return new BinaryInfixNode(operator, left, right);
} else {
TreeNode applyTo = constructRecursive(matches);
if (applyTo == null) return null;
else return new UnaryPrefixNode(operator, applyTo);
}
} else if (matchType == TokenType.NUM) {
return new NumberNode(abacus.numberFromString(match.getContent()));
} else if (matchType == TokenType.FUNCTION) {
String functionName = match.getContent();
FunctionNode node = new FunctionNode(functionName);
while (!matches.isEmpty() && matches.get(0).getType() != TokenType.INTERNAL_FUNCTION_END) {
TreeNode argument = constructRecursive(matches);
if (argument == null) return null;
node.prependChild(argument);
}
if (matches.isEmpty()) return null;
matches.remove(0);
return node;
}
return null;
}
@Override
public TreeNode constructTree(List<Match<TokenType>> tokens) {
tokens = intoPostfix(new ArrayList<>(tokens));
Collections.reverse(tokens);
return constructRecursive(tokens);
}
@Override
public void onLoad(PluginManager manager) {
for (String operator : manager.getAllOperators()) {
Operator operatorInstance = manager.operatorFor(operator);
precedenceMap.put(operator, operatorInstance.getPrecedence());
associativityMap.put(operator, operatorInstance.getAssociativity());
typeMap.put(operator, operatorInstance.getType());
}
}
@Override
public void onUnload(PluginManager manager) {
precedenceMap.clear();
associativityMap.clear();
typeMap.clear();
}
}

View File

@@ -0,0 +1,20 @@
package org.nwapw.abacus.parsing;
import java.util.List;
/**
* Interface that provides the ability to convert a string into a list of tokens.
*
* @param <T> the type of the tokens produced.
*/
public interface Tokenizer<T> {
/**
* Converts a string into tokens.
*
* @param string the string to convert.
* @return the list of tokens, or null on error.
*/
public List<T> tokenizeString(String string);
}

View File

@@ -0,0 +1,50 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.tree.TreeNode;
import java.util.List;
/**
* TreeBuilder class used to piece together a Tokenizer and
* Parser of the same kind. This is used to essentially avoid
* working with any parameters at all, and the generics
* in this class are used only to ensure the tokenizer and parser
* are of the same type.
*
* @param <T> the type of tokens created by the tokenizer and used by the parser.
*/
public class TreeBuilder<T> {
/**
* The tokenizer used to convert a string into tokens.
*/
private Tokenizer<T> tokenizer;
/**
* The parser used to parse a list of tokens into a tree.
*/
private Parser<T> parser;
/**
* Create a new Tree Builder with the given tokenizer and parser
*
* @param tokenizer the tokenizer to turn strings into tokens
* @param parser the parser to turn tokens into a tree
*/
public TreeBuilder(Tokenizer<T> tokenizer, Parser<T> parser) {
this.tokenizer = tokenizer;
this.parser = parser;
}
/**
* Parse the given string into a tree.
*
* @param input the string to parse into a tree.
* @return the resulting tree.
*/
public TreeNode fromString(String input) {
List<T> tokens = tokenizer.tokenizeString(input);
if (tokens == null) return null;
return parser.constructTree(tokens);
}
}

View File

@@ -8,6 +8,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
@@ -19,30 +20,32 @@ public class ClassFinder {
/**
* Loads all the plugin classes from the given plugin folder.
*
* @param filePath the path for the plugin folder.
* @return the list of all loaded classes.
* @throws IOException thrown if an error occurred scanning the plugin folder.
* @throws IOException thrown if an error occurred scanning the plugin folder.
* @throws ClassNotFoundException thrown if the class listed in the file doesn't get loaded.
*/
public static ArrayList<Class<?>> loadJars(String filePath) throws IOException, ClassNotFoundException {
public static List<Class<?>> loadJars(String filePath) throws IOException, ClassNotFoundException {
return loadJars(new File(filePath));
}
/**
* Loads all the plugin classes from the given plugin folder.
*
* @param pluginFolderPath the folder in which to look for plugins.
* @return the list of all loaded classes.
* @throws IOException thrown if an error occurred scanning the plugin folder.
* @throws IOException thrown if an error occurred scanning the plugin folder.
* @throws ClassNotFoundException thrown if the class listed in the file doesn't get loaded.
*/
public static ArrayList<Class<?>> loadJars(File pluginFolderPath) throws IOException, ClassNotFoundException {
public static List<Class<?>> loadJars(File pluginFolderPath) throws IOException, ClassNotFoundException {
ArrayList<Class<?>> toReturn = new ArrayList<>();
if(!pluginFolderPath.exists()) return toReturn;
if (!pluginFolderPath.exists()) return toReturn;
ArrayList<File> files = Files.walk(pluginFolderPath.toPath())
.map(Path::toFile)
.filter(f -> f.getName().endsWith(".jar"))
.collect(Collectors.toCollection(ArrayList::new));
for (File file : files){
for (File file : files) {
toReturn.addAll(loadJar(file));
}
return toReturn;
@@ -50,12 +53,13 @@ public class ClassFinder {
/**
* Loads the classes from a single path, given by the file.
*
* @param jarLocation the location of the jar to load.
* @return the list of loaded classes loaded from the jar.
* @throws IOException thrown if there was an error reading the file
* @throws IOException thrown if there was an error reading the file
* @throws ClassNotFoundException thrown if the class could not be loaded.
*/
public static ArrayList<Class<?>> loadJar(File jarLocation) throws IOException, ClassNotFoundException {
public static List<Class<?>> loadJar(File jarLocation) throws IOException, ClassNotFoundException {
ArrayList<Class<?>> loadedClasses = new ArrayList<>();
String path = jarLocation.getPath();
URL[] urls = new URL[]{new URL("jar:file:" + path + "!/")};

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.plugin;
import org.nwapw.abacus.function.Function;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.number.NumberInterface;
import java.util.HashMap;
import java.util.Map;
@@ -24,6 +25,10 @@ public abstract class Plugin {
* A hash map of operators mapped to their string names.
*/
private Map<String, Operator> operators;
/**
* A hash map of operators mapped to their string names.
*/
private Map<String, Class<? extends NumberInterface>> numbers;
/**
* The plugin manager in which to search for functions
* not inside this package,
@@ -34,37 +39,52 @@ public abstract class Plugin {
*/
private boolean enabled;
private Plugin(){ }
private Plugin() {
}
/**
* Creates a new plugin with the given PluginManager.
*
* @param manager the manager controlling this plugin.
*/
public Plugin(PluginManager manager) {
this.manager = manager;
functions = new HashMap<>();
operators = new HashMap<>();
numbers = new HashMap<>();
enabled = false;
}
/**
* Gets the list of functions provided by this plugin.
*
* @return the list of registered functions.
*/
public final Set<String> providedFunctions(){
public final Set<String> providedFunctions() {
return functions.keySet();
}
/**
* Gets the list of functions provided by this plugin.
*
* @return the list of registered functions.
*/
public final Set<String> providedOperators(){
public final Set<String> providedOperators() {
return operators.keySet();
}
/**
* Gets the list of all numbers provided by this plugin.
*
* @return the list of registered numbers.
*/
public final Set<String> providedNumbers() {
return numbers.keySet();
}
/**
* Gets a function under the given function name.
*
* @param functionName the name of the function to get
* @return the function, or null if this plugin doesn't provide it.
*/
@@ -74,6 +94,7 @@ public abstract class Plugin {
/**
* Gets an operator under the given operator name.
*
* @param operatorName the name of the operator to get.
* @return the operator, or null if this plugin doesn't provide it.
*/
@@ -81,12 +102,22 @@ public abstract class Plugin {
return operators.get(operatorName);
}
/**
* Gets the class under the given name.
*
* @param numberName the name of the class.
* @return the class, or null if the plugin doesn't provide it.
*/
public final Class<? extends NumberInterface> getNumber(String numberName) {
return numbers.get(numberName);
}
/**
* Enables the function, loading the necessary instances
* of functions.
*/
public final void enable(){
if(enabled) return;
public final void enable() {
if (enabled) return;
onEnable();
enabled = true;
}
@@ -95,8 +126,8 @@ public abstract class Plugin {
* Disables the plugin, clearing loaded data store by default
* and calling its disable() method.
*/
public final void disable(){
if(!enabled) return;
public final void disable() {
if (!enabled) return;
onDisable();
functions.clear();
operators.clear();
@@ -106,7 +137,8 @@ public abstract class Plugin {
/**
* To be used in load(). Registers a function abstract class with the
* plugin internally, which makes it accessible to the plugin manager.
* @param name the name to register by.
*
* @param name the name to register by.
* @param toRegister the function implementation.
*/
protected final void registerFunction(String name, Function toRegister) {
@@ -117,17 +149,32 @@ public abstract class Plugin {
* To be used in load(). Registers an operator abstract class
* with the plugin internally, which makes it accessible to
* the plugin manager.
* @param name the name of the operator.
*
* @param name the name of the operator.
* @param operator the operator to register.
*/
protected final void registerOperator(String name, Operator operator) {
operators.put(name, operator);
}
/**
* To be used in load(). Registers a number class
* with the plugin internally, which makes it possible
* for the user to select it as an "implementation" for the
* number that they would like to use.
*
* @param name the name to register it under.
* @param toRegister the class to register.
*/
protected final void registerNumber(String name, Class<? extends NumberInterface> toRegister) {
numbers.put(name, toRegister);
}
/**
* Searches the PluginManager for the given function name.
* This can be used by the plugins internally in order to call functions
* they do not provide.
*
* @param name the name for which to search
* @return the resulting function, or null if none was found for that name.
*/
@@ -139,6 +186,7 @@ public abstract class Plugin {
* Searches the PluginManager for the given operator name.
* This can be used by the plugins internally in order to call
* operations they do not provide.
*
* @param name the name for which to search
* @return the resulting operator, or null if none was found for that name.
*/

View File

@@ -7,12 +7,14 @@ public interface PluginListener {
/**
* Called when the PluginManager loads plugins.
*
* @param manager the manager that fired the event.
*/
public void onLoad(PluginManager manager);
/**
* Called when the PluginManager unloads all its plugins.
*
* @param manager the manager that fired the event.
*/
public void onUnload(PluginManager manager);

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.plugin;
import org.nwapw.abacus.function.Function;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.number.NumberInterface;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
@@ -26,10 +27,15 @@ public class PluginManager {
*/
private Map<String, Function> cachedFunctions;
/**
* List of operators tha have been cached,
* List of operators that have been cached,
* that is, found in a plugin and returned.
*/
private Map<String, Operator> cachedOperators;
/**
* List of registered number implementations that have
* been cached, that is, found in a plugin and returned.
*/
private Map<String, Class<? extends NumberInterface>> cachedNumbers;
/**
* List of all functions loaded by the plugins.
*/
@@ -38,6 +44,10 @@ public class PluginManager {
* List of all operators loaded by the plugins.
*/
private Set<String> allOperators;
/**
* List of all numbers loaded by the plugins.
*/
private Set<String> allNumbers;
/**
* The list of plugin listeners attached to this instance.
*/
@@ -46,13 +56,15 @@ public class PluginManager {
/**
* Creates a new plugin manager.
*/
public PluginManager(){
public PluginManager() {
loadedPluginClasses = new HashSet<>();
plugins = new HashSet<>();
cachedFunctions = new HashMap<>();
cachedOperators = new HashMap<>();
cachedNumbers = new HashMap<>();
allFunctions = new HashSet<>();
allOperators = new HashSet<>();
allNumbers = new HashSet<>();
listeners = new HashSet<>();
}
@@ -61,23 +73,24 @@ public class PluginManager {
* list of items of the type using the setFunction and getting the value
* of it is available via getFunction. If the value is contained
* in the cache, it returns the cached value instead.
* @param plugins the plugin list to search.
* @param cache the cache to use
*
* @param plugins the plugin list to search.
* @param cache the cache to use
* @param setFunction the function to retrieve a set of available T's from the plugin
* @param getFunction the function to get the T value under the given name
* @param name the name to search for
* @param <T> the type of element being search
* @param name the name to search for
* @param <T> the type of element being search
* @return the retrieved element, or null if it was not found.
*/
private static <T> T searchCached(Collection<Plugin> plugins, Map<String, T> cache,
java.util.function.Function<Plugin, Set<String>> setFunction,
java.util.function.BiFunction<Plugin, String, T> getFunction,
String name){
if(cache.containsKey(name)) return cache.get(name);
String name) {
if (cache.containsKey(name)) return cache.get(name);
T loadedValue = null;
for(Plugin plugin : plugins){
if(setFunction.apply(plugin).contains(name)){
for (Plugin plugin : plugins) {
if (setFunction.apply(plugin).contains(name)) {
loadedValue = getFunction.apply(plugin, name);
break;
}
@@ -86,56 +99,85 @@ public class PluginManager {
cache.put(name, loadedValue);
return loadedValue;
}
/**
* Gets a function under the given name.
*
* @param name the name of the function
* @return the function under the given name.
*/
public Function functionFor(String name){
public Function functionFor(String name) {
return searchCached(plugins, cachedFunctions, Plugin::providedFunctions, Plugin::getFunction, name);
}
/**
* Gets an operator under the given name.
*
* @param name the name of the operator.
* @return the operator under the given name.
*/
public Operator operatorFor(String name){
public Operator operatorFor(String name) {
return searchCached(plugins, cachedOperators, Plugin::providedOperators, Plugin::getOperator, name);
}
/**
* Gets a numer implementation under the given name.
*
* @param name the name of the implementation.
* @return the implementation class
*/
public Class<? extends NumberInterface> numberFor(String name) {
return searchCached(plugins, cachedNumbers, Plugin::providedNumbers, Plugin::getNumber, name);
}
/**
* Adds an instance of Plugin that already has been instantiated.
*
* @param plugin the plugin to add.
*/
public void addInstantiated(Plugin plugin){
if(loadedPluginClasses.contains(plugin.getClass())) return;
public void addInstantiated(Plugin plugin) {
if (loadedPluginClasses.contains(plugin.getClass())) return;
plugins.add(plugin);
loadedPluginClasses.add(plugin.getClass());
}
public void removeInstantiated(Plugin plugin){
if (loadedPluginClasses.contains(plugin.getClass())) return;
plugins.remove(plugin);
loadedPluginClasses.remove(plugin.getClass());
}
/**
* Instantiates a class of plugin, and adds it to this
* plugin manager.
*
* @param newClass the new class to instantiate.
*/
public void addClass(Class<?> newClass){
if(!Plugin.class.isAssignableFrom(newClass) || newClass == Plugin.class) return;
public void addClass(Class<?> newClass) {
if (!Plugin.class.isAssignableFrom(newClass) || newClass == Plugin.class) return;
try {
addInstantiated((Plugin) newClass.getConstructor(PluginManager.class).newInstance(this));
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
public void removeClass(Class<?> newClass){
if (!Plugin.class.isAssignableFrom(newClass) || newClass == Plugin.class) return;
try {
removeInstantiated((Plugin) newClass.getConstructor(PluginManager.class).newInstance(this));
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* Loads all the plugins in the PluginManager.
*/
public void load(){
for(Plugin plugin : plugins) plugin.enable();
for(Plugin plugin : plugins){
public void load() {
for (Plugin plugin : plugins) plugin.enable();
for (Plugin plugin : plugins) {
allFunctions.addAll(plugin.providedFunctions());
allOperators.addAll(plugin.providedOperators());
allNumbers.addAll(plugin.providedNumbers());
}
listeners.forEach(e -> e.onLoad(this));
}
@@ -143,23 +185,25 @@ public class PluginManager {
/**
* Unloads all the plugins in the PluginManager.
*/
public void unload(){
for(Plugin plugin : plugins) plugin.disable();
public void unload() {
for (Plugin plugin : plugins) plugin.disable();
allFunctions.clear();
allOperators.clear();
allNumbers.clear();
listeners.forEach(e -> e.onUnload(this));
}
/**
* Reloads all the plugins in the PluginManager.
*/
public void reload(){
public void reload() {
unload();
reload();
}
/**
* Gets all the functions loaded by the Plugin Manager.
*
* @return the set of all functions that were loaded.
*/
public Set<String> getAllFunctions() {
@@ -168,26 +212,38 @@ public class PluginManager {
/**
* Gets all the operators loaded by the Plugin Manager.
*
* @return the set of all operators that were loaded.
*/
public Set<String> getAllOperators() {
return allOperators;
}
/**
* Gets all the number implementations loaded by the Plugin Manager
*
* @return the set of all implementations that were loaded
*/
public Set<String> getAllNumbers() {
return allNumbers;
}
/**
* Adds a plugin change listener to this plugin manager.
*
* @param listener the listener to add.
*/
public void addListener(PluginListener listener){
public void addListener(PluginListener listener) {
listeners.add(listener);
}
/**
* Remove the plugin change listener from this plugin manager.
*
* @param listener the listener to remove.
*/
public void removeListener(PluginListener listener){
public void removeListener(PluginListener listener) {
listeners.remove(listener);
}
}

View File

@@ -0,0 +1,351 @@
package org.nwapw.abacus.plugin;
import org.nwapw.abacus.function.Function;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.function.OperatorType;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.number.PreciseNumber;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.function.BiFunction;
/**
* The plugin providing standard functions such as addition and subtraction to
* the calculator.
*/
public class StandardPlugin extends Plugin {
private static HashMap<Class<? extends NumberInterface>, ArrayList<NumberInterface>> factorialLists = new HashMap<Class<? extends NumberInterface>, ArrayList<NumberInterface>>();
/**
* The addition operator, +
*/
public static final Operator OP_ADD = new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX, 0, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length >= 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface sum = params[0];
for (int i = 1; i < params.length; i++) {
sum = sum.add(params[i]);
}
return sum;
}
});
/**
* The subtraction operator, -
*/
public static final Operator OP_SUBTRACT = new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX, 0, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].subtract(params[1]);
}
});
/**
* The multiplication operator, *
*/
public static final Operator OP_MULTIPLY = new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX, 1, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length >= 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface product = params[0];
for (int i = 1; i < params.length; i++) {
product = product.multiply(params[i]);
}
return product;
}
});
/**
* The division operator, /
*/
public static final Operator OP_DIVIDE = new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX, 1, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length >= 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface product = params[0];
for (int i = 1; i < params.length; i++) {
product = product.multiply(params[i]);
}
return product;
}
});
/**
* The factorial operator, !
*/
public static final Operator OP_FACTORIAL = new Operator(OperatorAssociativity.RIGHT, OperatorType.UNARY_POSTFIX, 0, new Function() {
//private HashMap<Class<? extends NumberInterface>, ArrayList<NumberInterface>> storedList = new HashMap<Class<? extends NumberInterface>, ArrayList<NumberInterface>>();
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
if (params[0].signum() == 0) {
return (new NaiveNumber(1)).promoteTo(params[0].getClass());
}
NumberInterface factorial = params[0];
NumberInterface multiplier = params[0];
//It is necessary to later prevent calls of factorial on anything but non-negative integers.
while ((multiplier = multiplier.subtract(NaiveNumber.ONE.promoteTo(multiplier.getClass()))).signum() == 1) {
factorial = factorial.multiply(multiplier);
}
return factorial;
/*if(!storedList.containsKey(params[0].getClass())){
storedList.put(params[0].getClass(), new ArrayList<NumberInterface>());
storedList.get(params[0].getClass()).add(NaiveNumber.ONE.promoteTo(params[0].getClass()));
storedList.get(params[0].getClass()).add(NaiveNumber.ONE.promoteTo(params[0].getClass()));
}*/
}
});
/**
* The caret / pow operator, ^
*/
public static final Operator OP_CARET = new Operator(OperatorAssociativity.RIGHT, OperatorType.BINARY_INFIX, 2, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return FUNCTION_EXP.apply(FUNCTION_LN.apply(params[0]).multiply(params[1]));
}
});
/**
* The absolute value function, abs(-3) = 3
*/
public static final Function FUNCTION_ABS = new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].multiply((new NaiveNumber(params[0].signum())).promoteTo(params[0].getClass()));
}
};
/**
* The exponential function, exp(1) = e^1 = 2.71...
*/
public static final Function FUNCTION_EXP = new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface maxError = getMaxError(params[0]);
int n = 0;
if(params[0].signum() <= 0){
NumberInterface currentTerm = NaiveNumber.ONE.promoteTo(params[0].getClass()), sum = currentTerm;
while(FUNCTION_ABS.apply(currentTerm).compareTo(maxError) > 0){
n++;
currentTerm = currentTerm.multiply(params[0]).divide((new NaiveNumber(n)).promoteTo(params[0].getClass()));
sum = sum.add(currentTerm);
}
return sum;
}
else{
//We need n such that x^(n+1) * 3^ceil(x) <= maxError * (n+1)!.
//right and left refer to lhs and rhs in the above inequality.
NumberInterface sum = NaiveNumber.ONE.promoteTo(params[0].getClass());
NumberInterface nextNumerator = params[0];
NumberInterface left = params[0].multiply((new NaiveNumber(3)).promoteTo(params[0].getClass()).intPow(params[0].ceiling())), right = maxError;
do{
sum = sum.add(nextNumerator.divide(factorial(params[0].getClass(), n+1)));
n++;
nextNumerator = nextNumerator.multiply(params[0]);
left = left.multiply(params[0]);
NumberInterface nextN = (new NaiveNumber(n+1)).promoteTo(params[0].getClass());
right = right.multiply(nextN);
//System.out.println(left + ", " + right);
}
while(left.compareTo(right) > 0);
//System.out.println(n+1);
return sum;
}
}
};
/**
* The natural log function.
*/
public static final Function FUNCTION_LN = new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface param = params[0];
int powersOf2 = 0;
while (FUNCTION_ABS.apply(param.subtract(NaiveNumber.ONE.promoteTo(param.getClass()))).compareTo((new NaiveNumber(0.1)).promoteTo(param.getClass())) >= 0) {
if (param.subtract(NaiveNumber.ONE.promoteTo(param.getClass())).signum() == 1) {
param = param.divide(new NaiveNumber(2).promoteTo(param.getClass()));
powersOf2++;
if (param.subtract(NaiveNumber.ONE.promoteTo(param.getClass())).signum() != 1) {
break;
//No infinite loop for you.
}
} else {
param = param.multiply(new NaiveNumber(2).promoteTo(param.getClass()));
powersOf2--;
if (param.subtract(NaiveNumber.ONE.promoteTo(param.getClass())).signum() != 1) {
break;
//No infinite loop for you.
}
}
}
return getLog2(param).multiply((new NaiveNumber(powersOf2)).promoteTo(param.getClass())).add(getLogPartialSum(param));
}
/**
* Returns the partial sum of the Taylor series for logx (around x=1).
* Automatically determines the number of terms needed based on the precision of x.
* @param x value at which the series is evaluated. 0 < x < 2. (x=2 is convergent but impractical.)
* @return the partial sum.
*/
private NumberInterface getLogPartialSum(NumberInterface x) {
NumberInterface maxError = getMaxError(x);
x = x.subtract(NaiveNumber.ONE.promoteTo(x.getClass())); //Terms used are for log(x+1).
NumberInterface currentNumerator = x, currentTerm = x, sum = x;
int n = 1;
while (FUNCTION_ABS.apply(currentTerm).compareTo(maxError) > 0) {
n++;
currentNumerator = currentNumerator.multiply(x).negate();
currentTerm = currentNumerator.divide(new NaiveNumber(n).promoteTo(x.getClass()));
sum = sum.add(currentTerm);
}
return sum;
}
/**
* Returns natural log of 2 to the required precision of the class of number.
* @param number a number of the same type as the return type. (Used for precision.)
* @return the value of log(2) with the appropriate precision.
*/
private NumberInterface getLog2(NumberInterface number) {
NumberInterface maxError = getMaxError(number);
//NumberInterface errorBound = (new NaiveNumber(1)).promoteTo(number.getClass());
//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 = (new NaiveNumber(1)).promoteTo(number.getClass()), b = a, c = a;
NumberInterface sum = NaiveNumber.ZERO.promoteTo(number.getClass());
int n = 0;
while (a.compareTo(maxError) >= 1) {
n++;
a = a.divide((new NaiveNumber(3)).promoteTo(number.getClass()));
b = b.divide((new NaiveNumber(4)).promoteTo(number.getClass()));
c = NaiveNumber.ONE.promoteTo(number.getClass()).divide((new NaiveNumber(n)).promoteTo(number.getClass()));
sum = sum.add(a.add(b).multiply(c));
}
return sum;
}
};
/**
* The square root function.
*/
public static final Function FUNCTION_SQRT = new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return OP_CARET.getFunction().apply(params[0], ((new NaiveNumber(0.5)).promoteTo(params[0].getClass())));
}
};
public StandardPlugin(PluginManager manager) {
super(manager);
}
/**
* Returns a partial sum of a series whose terms are given by the nthTermFunction, evaluated at x.
*
* @param x the value at which the series is evaluated.
* @param nthTermFunction the function that returns the nth term of the series, in the format term(x, n).
* @param n the number of terms in the partial sum.
* @return the value of the partial sum that has the same class as x.
*/
private static NumberInterface sumSeries(NumberInterface x, BiFunction<Integer, NumberInterface, NumberInterface> nthTermFunction, int n) {
NumberInterface sum = NaiveNumber.ZERO.promoteTo(x.getClass());
for (int i = 0; i <= n; i++) {
sum = sum.add(nthTermFunction.apply(i, x));
}
return sum;
}
/**
* Returns the maximum error based on the precision of the class of number.
*
* @param number Any instance of the NumberInterface in question (should return an appropriate precision).
* @return the maximum error.
*/
private static NumberInterface getMaxError(NumberInterface number) {
return (new NaiveNumber(10)).promoteTo(number.getClass()).intPow(-number.getMaxPrecision());
}
@Override
public void onEnable() {
registerNumber("naive", NaiveNumber.class);
registerNumber("precise", PreciseNumber.class);
registerOperator("+", OP_ADD);
registerOperator("-", OP_SUBTRACT);
registerOperator("*", OP_MULTIPLY);
registerOperator("/", OP_DIVIDE);
registerOperator("^", OP_CARET);
registerOperator("!", OP_FACTORIAL);
registerFunction("abs", FUNCTION_ABS);
registerFunction("exp", FUNCTION_EXP);
registerFunction("ln", FUNCTION_LN);
registerFunction("sqrt", FUNCTION_SQRT);
}
@Override
public void onDisable() {
}
public static NumberInterface factorial(Class<? extends NumberInterface> numberClass, int n){
if(!factorialLists.containsKey(numberClass)){
factorialLists.put(numberClass, new ArrayList<NumberInterface>());
factorialLists.get(numberClass).add(NaiveNumber.ONE.promoteTo(numberClass));
factorialLists.get(numberClass).add(NaiveNumber.ONE.promoteTo(numberClass));
}
ArrayList<NumberInterface> list = factorialLists.get(numberClass);
if(n >= list.size()){
while(list.size() < n + 16){
list.add(list.get(list.size()-1).multiply(new NaiveNumber(list.size()).promoteTo(numberClass)));
}
}
return list.get(n);
}
}

View File

@@ -18,25 +18,28 @@ public class BinaryInfixNode extends TreeNode {
*/
private TreeNode right;
private BinaryInfixNode() {}
private BinaryInfixNode() {
}
/**
* Creates a new operation node with the given operation
* and no child nodes.
*
* @param operation the operation.
*/
public BinaryInfixNode(String operation){
public BinaryInfixNode(String operation) {
this(operation, null, null);
}
/**
* Creates a new operation node with the given operation
* and child nodes.
*
* @param operation the operation.
* @param left the left node of the expression.
* @param right the right node of the expression.
* @param left the left node of the expression.
* @param right the right node of the expression.
*/
public BinaryInfixNode(String operation, TreeNode left, TreeNode right){
public BinaryInfixNode(String operation, TreeNode left, TreeNode right) {
this.operation = operation;
this.left = left;
this.right = right;
@@ -44,6 +47,7 @@ public class BinaryInfixNode extends TreeNode {
/**
* Gets the operation in this node.
*
* @return the operation in this node.
*/
public String getOperation() {
@@ -52,6 +56,7 @@ public class BinaryInfixNode extends TreeNode {
/**
* Gets the left sub-expression of this node.
*
* @return the left node.
*/
public TreeNode getLeft() {
@@ -60,6 +65,7 @@ public class BinaryInfixNode extends TreeNode {
/**
* Sets the left sub-expression of this node.
*
* @param left the sub-expression to apply.
*/
public void setLeft(TreeNode left) {
@@ -68,6 +74,7 @@ public class BinaryInfixNode extends TreeNode {
/**
* Gets the right sub-expression of this node.
*
* @return the right node.
*/
public TreeNode getRight() {
@@ -76,6 +83,7 @@ public class BinaryInfixNode extends TreeNode {
/**
* Sets the right sub-expression of this node.
*
* @param right the sub-expression to apply.
*/
public void setRight(TreeNode right) {
@@ -86,7 +94,7 @@ public class BinaryInfixNode extends TreeNode {
public <T> T reduce(Reducer<T> reducer) {
T leftReduce = left.reduce(reducer);
T rightReduce = right.reduce(reducer);
if(leftReduce == null || rightReduce == null) return null;
if (leftReduce == null || rightReduce == null) return null;
return reducer.reduceNode(this, leftReduce, rightReduce);
}

View File

@@ -20,19 +20,22 @@ public class FunctionNode extends TreeNode {
/**
* Creates a function node with no function.
*/
private FunctionNode() { }
private FunctionNode() {
}
/**
* Creates a new function node with the given function name.
*
* @param function the function name.
*/
public FunctionNode(String function){
public FunctionNode(String function) {
this.function = function;
children = new ArrayList<>();
}
/**
* Gets the function name for this node.
*
* @return the function name.
*/
public String getFunction() {
@@ -41,14 +44,16 @@ public class FunctionNode extends TreeNode {
/**
* Adds a child to the end of this node's child list.
*
* @param node the child to add.
*/
public void appendChild(TreeNode node){
public void appendChild(TreeNode node) {
children.add(node);
}
/**
* Adds a new child to the beginning of this node's child list.
*
* @param node the node to add.
*/
public void prependChild(TreeNode node) {
@@ -58,9 +63,9 @@ public class FunctionNode extends TreeNode {
@Override
public <T> T reduce(Reducer<T> reducer) {
Object[] reducedChildren = new Object[children.size()];
for(int i = 0; i < reducedChildren.length; i++){
for (int i = 0; i < reducedChildren.length; i++) {
reducedChildren[i] = children.get(i).reduce(reducer);
if(reducedChildren[i] == null) return null;
if (reducedChildren[i] == null) return null;
}
return reducer.reduceNode(this, reducedChildren);
}
@@ -70,7 +75,7 @@ public class FunctionNode extends TreeNode {
StringBuilder buffer = new StringBuilder();
buffer.append(function);
buffer.append("(");
for(int i = 0; i < children.size(); i++){
for (int i = 0; i < children.size(); i++) {
buffer.append(children.get(i));
buffer.append(i == children.size() - 1 ? "" : ", ");
}

View File

@@ -1,6 +1,5 @@
package org.nwapw.abacus.tree;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.NumberInterface;
/**
@@ -16,20 +15,22 @@ public class NumberNode extends TreeNode {
/**
* Creates a number node with no number.
*/
public NumberNode(){
public NumberNode() {
number = null;
}
/**
* Creates a new number node with the given double value.
*
* @param newNumber the number for which to create a number node.
*/
public NumberNode(NumberInterface newNumber){
public NumberNode(NumberInterface newNumber) {
this.number = newNumber;
}
/**
* Gets the number value of this node.
*
* @return the number value of this node.
*/
public NumberInterface getNumber() {

View File

@@ -1,8 +1,8 @@
package org.nwapw.abacus.tree;
import org.nwapw.abacus.Abacus;
import org.nwapw.abacus.function.Function;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.plugin.PluginManager;
/**
* A reducer implementation that turns a tree into a single number.
@@ -13,38 +13,39 @@ public class NumberReducer implements Reducer<NumberInterface> {
/**
* The plugin manager from which to draw the functions.
*/
private PluginManager manager;
private Abacus abacus;
/**
* Creates a new number reducer with the given plugin manager.
* @param manager the plugin manager.
* Creates a new number reducer.
*
* @param abacus the calculator instance.
*/
public NumberReducer(PluginManager manager){
this.manager = manager;
public NumberReducer(Abacus abacus) {
this.abacus = abacus;
}
@Override
public NumberInterface reduceNode(TreeNode node, Object... children) {
if(node instanceof NumberNode) {
if (node instanceof NumberNode) {
return ((NumberNode) node).getNumber();
} else if(node instanceof BinaryInfixNode){
} else if (node instanceof BinaryInfixNode) {
NumberInterface left = (NumberInterface) children[0];
NumberInterface right = (NumberInterface) children[1];
Function function = manager.operatorFor(((BinaryInfixNode) node).getOperation()).getFunction();
if(function == null) return null;
Function function = abacus.getPluginManager().operatorFor(((BinaryInfixNode) node).getOperation()).getFunction();
if (function == null) return null;
return function.apply(left, right);
} else if(node instanceof UnaryPrefixNode) {
} else if (node instanceof UnaryPrefixNode) {
NumberInterface child = (NumberInterface) children[0];
Function functionn = manager.operatorFor(((UnaryPrefixNode) node).getOperation()).getFunction();
if(functionn == null) return null;
Function functionn = abacus.getPluginManager().operatorFor(((UnaryPrefixNode) node).getOperation()).getFunction();
if (functionn == null) return null;
return functionn.apply(child);
} else if(node instanceof FunctionNode){
} else if (node instanceof FunctionNode) {
NumberInterface[] convertedChildren = new NumberInterface[children.length];
for(int i = 0; i < convertedChildren.length; i++){
for (int i = 0; i < convertedChildren.length; i++) {
convertedChildren[i] = (NumberInterface) children[i];
}
Function function = manager.functionFor(((FunctionNode) node).getFunction());
if(function == null) return null;
Function function = abacus.getPluginManager().functionFor(((FunctionNode) node).getFunction());
if (function == null) return null;
return function.apply(convertedChildren);
}
return null;

View File

@@ -2,16 +2,18 @@ package org.nwapw.abacus.tree;
/**
* Interface used to reduce a tree into a single value.
*
* @param <T> the value to reduce into.
*/
public interface Reducer<T> {
/**
* Reduces the given tree into a single value of type T.
* @param node the node being passed in to be reduced.
*
* @param node the node being passed in to be reduced.
* @param children the already-reduced children of this node.
* @return the resulting value from the reduce.
*/
public T reduceNode(TreeNode node, Object...children);
public T reduceNode(TreeNode node, Object... children);
}

View File

@@ -16,9 +16,10 @@ public enum TokenType {
/**
* Creates a new token type with the given priority.
*
* @param priority the priority of this token type.
*/
TokenType(int priority){
TokenType(int priority) {
this.priority = priority;
}

View File

@@ -1,11 +1,5 @@
package org.nwapw.abacus.tree;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.lexing.Lexer;
import org.nwapw.abacus.lexing.pattern.Match;
import java.util.*;
/**
* An abstract class that represents an expression tree node.
*/
@@ -13,8 +7,9 @@ public abstract class TreeNode {
/**
* The function that reduces a tree to a single vale.
*
* @param reducer the reducer used to reduce the tree.
* @param <T> the type the reducer produces.
* @param <T> the type the reducer produces.
* @return the result of the reduction, or null on error.
*/
public abstract <T> T reduce(Reducer<T> reducer);

View File

@@ -13,18 +13,20 @@ public class UnaryPrefixNode extends TreeNode {
/**
* Creates a new node with the given operation and no child.
*
* @param operation the operation for this node.
*/
public UnaryPrefixNode(String operation){
public UnaryPrefixNode(String operation) {
this(operation, null);
}
/**
* Creates a new node with the given operation and child.
*
* @param operation the operation for this node.
* @param applyTo the node to apply the function to.
* @param applyTo the node to apply the function to.
*/
public UnaryPrefixNode(String operation, TreeNode applyTo){
public UnaryPrefixNode(String operation, TreeNode applyTo) {
this.operation = operation;
this.applyTo = applyTo;
}
@@ -32,12 +34,13 @@ public class UnaryPrefixNode extends TreeNode {
@Override
public <T> T reduce(Reducer<T> reducer) {
Object reducedChild = applyTo.reduce(reducer);
if(reducedChild == null) return null;
if (reducedChild == null) return null;
return reducer.reduceNode(this, reducedChild);
}
/**
* Gets the operation of this node.
*
* @return the operation this node performs.
*/
public String getOperation() {
@@ -46,6 +49,7 @@ public class UnaryPrefixNode extends TreeNode {
/**
* Gets the node to which this node's operation applies.
*
* @return the tree node to which the operation will be applied.
*/
public TreeNode getApplyTo() {

View File

@@ -2,9 +2,7 @@ package org.nwapw.abacus.window;
import org.nwapw.abacus.tree.TreeNode;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import java.util.ArrayList;
import java.util.List;
@@ -31,30 +29,6 @@ public class HistoryTableModel extends AbstractTableModel {
TreeNode.class,
String.class
};
/**
* Class used specifically to hold data about
* the previous entries into the calculator.
*/
public static class HistoryEntry {
public String input;
public TreeNode parsedInput;
public String output;
public HistoryEntry(String input, TreeNode parsedInput, String output){
this.input = input;
this.parsedInput = parsedInput;
this.output = output;
}
Object nthValue(int n){
if(n == 0) return input;
if(n == 1) return parsedInput;
if(n == 2) return output;
return null;
}
}
/**
* The list of entries.
*/
@@ -69,9 +43,10 @@ public class HistoryTableModel extends AbstractTableModel {
/**
* Adds an entry to the model.
*
* @param entry the entry to add.
*/
public void addEntry(HistoryEntry entry){
public void addEntry(HistoryEntry entry) {
entries.add(entry);
}
@@ -105,4 +80,27 @@ public class HistoryTableModel extends AbstractTableModel {
return entries.get(rowIndex).nthValue(columnIndex);
}
/**
* Class used specifically to hold data about
* the previous entries into the calculator.
*/
public static class HistoryEntry {
public String input;
public TreeNode parsedInput;
public String output;
public HistoryEntry(String input, TreeNode parsedInput, String output) {
this.input = input;
this.parsedInput = parsedInput;
this.output = output;
}
Object nthValue(int n) {
if (n == 0) return input;
if (n == 1) return parsedInput;
if (n == 2) return output;
return null;
}
}
}

View File

@@ -1,11 +1,7 @@
package org.nwapw.abacus.window;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.Abacus;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.plugin.PluginListener;
import org.nwapw.abacus.plugin.PluginManager;
import org.nwapw.abacus.tree.NumberReducer;
import org.nwapw.abacus.tree.TreeBuilder;
import org.nwapw.abacus.tree.TreeNode;
import javax.swing.*;
@@ -18,7 +14,7 @@ import java.awt.event.MouseEvent;
/**
* The main UI window for the calculator.
*/
public class Window extends JFrame implements PluginListener {
public class Window extends JFrame {
private static final String CALC_STRING = "Calculate";
private static final String SYNTAX_ERR_STRING = "Syntax Error";
@@ -47,18 +43,10 @@ public class Window extends JFrame implements PluginListener {
};
/**
* The plugin manager used to retrieve functions.
* The instance of the Abacus class, used
* for interaction with plugins and configuration.
*/
private PluginManager manager;
/**
* The builder used to construct the parse trees.
*/
private TreeBuilder builder;
/**
* The reducer used to evaluate the tree.
*/
private NumberReducer reducer;
private Abacus abacus;
/**
* The last output by the calculator.
*/
@@ -130,15 +118,14 @@ public class Window extends JFrame implements PluginListener {
* Action listener that causes the input to be evaluated.
*/
private ActionListener evaluateListener = (event) -> {
if(builder == null) return;
TreeNode parsedExpression = builder.fromString(inputField.getText());
if(parsedExpression == null){
TreeNode parsedExpression = abacus.parseString(inputField.getText());
if (parsedExpression == null) {
lastOutputArea.setText(SYNTAX_ERR_STRING);
return;
}
NumberInterface numberInterface = parsedExpression.reduce(reducer);
if(numberInterface == null){
lastOutputArea.setText(EVAL_ERR_STRING);;
NumberInterface numberInterface = abacus.evaluateTree(parsedExpression);
if (numberInterface == null) {
lastOutputArea.setText(EVAL_ERR_STRING);
return;
}
lastOutput = numberInterface.toString();
@@ -159,13 +146,12 @@ public class Window extends JFrame implements PluginListener {
/**
* Creates a new window with the given manager.
* @param manager the manager to use.
*
* @param abacus the calculator instance to interact with other components.
*/
public Window(PluginManager manager){
public Window(Abacus abacus) {
this();
this.manager = manager;
manager.addListener(this);
reducer = new NumberReducer(manager);
this.abacus = abacus;
}
/**
@@ -233,11 +219,11 @@ public class Window extends JFrame implements PluginListener {
inputField.setEnabled(enabled);
inputEnterButton.setEnabled(enabled);
for(ActionListener removingListener : inputEnterButton.getActionListeners()){
for (ActionListener removingListener : inputEnterButton.getActionListeners()) {
inputEnterButton.removeActionListener(removingListener);
inputField.removeActionListener(removingListener);
}
if(listener != null){
if (listener != null) {
inputEnterButton.addActionListener(listener);
inputField.addActionListener(listener);
}
@@ -251,7 +237,7 @@ public class Window extends JFrame implements PluginListener {
@Override
public void mouseClicked(MouseEvent e) {
Point clickPoint = e.getPoint();
if(e.getClickCount() == 2){
if (e.getClickCount() == 2) {
int row = historyTable.rowAtPoint(clickPoint);
int column = historyTable.columnAtPoint(clickPoint);
String toCopy = historyTable.getValueAt(row, column).toString();
@@ -262,22 +248,8 @@ public class Window extends JFrame implements PluginListener {
}
@Override
public void onLoad(PluginManager manager) {
builder = new TreeBuilder();
for(String function : manager.getAllFunctions()){
builder.registerFunction(function);
}
for(String operator : manager.getAllOperators()){
Operator operatorObject = manager.operatorFor(operator);
builder.registerOperator(operator,
operatorObject.getAssociativity(),
operatorObject.getType(),
operatorObject.getPrecedence());
}
}
@Override
public void onUnload(PluginManager manager) {
builder = null;
public void setVisible(boolean b) {
super.setVisible(b);
if(b) inputField.requestFocusInWindow();
}
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.GridPane?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.nwapw.abacus.fx.AbacusController">
<center>
<TabPane>
<Tab text="Calculator" closable="false">
<BorderPane>
<center>
<TableView fx:id="historyTable">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
<columns>
<TableColumn fx:id="inputColumn" text="Input" sortable="false"/>
<TableColumn fx:id="parsedColumn" text="Parsed" sortable="false"/>
<TableColumn fx:id="outputColumn" text="Output" sortable="false"/>
</columns>
</TableView>
</center>
<bottom>
<VBox>
<ScrollPane prefHeight="50" vbarPolicy="NEVER">
<padding>
<Insets top="10" bottom="10" left="10" right="10"/>
</padding>
<Text fx:id="outputText"/>
</ScrollPane>
<TextField fx:id="inputField" onAction="#performCalculation"/>
<Button fx:id="inputButton" text="Calculate" maxWidth="Infinity"
onAction="#performCalculation"/>
</VBox>
</bottom>
</BorderPane>
</Tab>
<Tab text="Settings" closable="false">
<GridPane hgap="10" vgap="10">
<padding><Insets left="10" right="10" top="10" bottom="10"/></padding>
<Label text="Number Implementation" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
<ComboBox fx:id="numberImplementationBox" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
<Button fx:id="loadButton" text="Load" GridPane.rowIndex="1" GridPane.columnIndex="0" maxWidth = "Infinity" onAction="#loadClass"/>
<Button fx:id="unloadButton" text="Unload" GridPane.rowIndex="1" GridPane.columnIndex="1" maxWidth = "Infinity" onAction="#unloadClass"/>
<TextField fx:id="loadField" GridPane.rowIndex="1" GridPane.columnIndex="2"/>
</GridPane>
</Tab>
</TabPane>
</center>
</BorderPane>

View File

@@ -1,46 +0,0 @@
package org.nwapw.abacus;
import org.nwapw.abacus.plugin.PluginManager;
//import org.nwapw.abacus.plugin.StandardPlugin;
import org.nwapw.abacus.plugin.StandardPlugin;
import org.nwapw.abacus.window.Window;
import org.nwapw.abacus.plugin.ClassFinder;
import javax.swing.*;
import java.io.IOException;
import java.util.ArrayList;
public class Abacus {
private Window mainUi;
private PluginManager manager;
public Abacus(){
init();
}
private void init() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | UnsupportedLookAndFeelException | IllegalAccessException e) {
e.printStackTrace();
}
manager = new PluginManager();
manager.addInstantiated(new StandardPlugin(manager));
try {
ClassFinder.loadJars("plugins")
.forEach(plugin -> manager.addClass(plugin));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
mainUi = new Window(manager);
mainUi.setVisible(true);
manager.load();
}
public static void main(String[] args){
new Abacus();
}
}

View File

@@ -1,57 +0,0 @@
package org.nwapw.abacus.lexing.pattern;
/**
* A match that has been generated by the lexer.
* @param <T> the type used to represent the ID of the pattern this match belongs to.
*/
public class Match<T> {
/**
* The bottom range of the string, inclusive.
*/
private int from;
/**
* The top range of the string, exclusive.
*/
private int to;
/**
* The pattern type this match matched.
*/
private T type;
/**
* Creates a new match with the given parameters.
* @param from the bottom range of the string.
* @param to the top range of the string.
* @param type the type of the match.
*/
public Match(int from, int to, T type){
this.from = from;
this.to = to;
this.type = type;
}
/**
* Gets the bottom range bound of the string.
* @return the bottom range bound of the string.
*/
public int getFrom() {
return from;
}
/**
* Gets the top range bound of the string.
* @return the top range bound of the string.
*/
public int getTo() {
return to;
}
/**
* Gets the pattern type of the node.
* @return the ID of the pattern that this match matched.
*/
public T getType() {
return type;
}
}

View File

@@ -1,298 +0,0 @@
package org.nwapw.abacus.plugin;
import org.nwapw.abacus.function.Function;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.function.OperatorType;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.NumberInterface;
import java.util.function.BiFunction;
/**
* The plugin providing standard functions such as addition and subtraction to
* the calculator.
*/
public class StandardPlugin extends Plugin {
public StandardPlugin(PluginManager manager) {
super(manager);
}
@Override
public void onEnable() {
registerOperator("+", new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX, 0, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length >= 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface sum = params[0];
for(int i = 1; i < params.length; i++){
sum = sum.add(params[i]);
}
return sum;
}
}));
registerOperator("-", new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX, 0, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].subtract(params[1]);
}
}));
registerOperator("*", new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX,1, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length >= 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface product = params[0];
for(int i = 1; i < params.length; i++){
product = product.multiply(params[i]);
}
return product;
}
}));
registerOperator("/", new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX,1, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].divide(params[1]);
}
}));
registerOperator("^", new Operator(OperatorAssociativity.RIGHT, OperatorType.BINARY_INFIX, 2, new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return StandardPlugin.this.getFunction("exp").apply(StandardPlugin.this.getFunction("ln").apply(params[0]).multiply(params[1]));
}
}));
registerOperator("!", new Operator(OperatorAssociativity.RIGHT, OperatorType.UNARY_POSTFIX, 0, new Function() {
//private HashMap<Class<? extends NumberInterface>, ArrayList<NumberInterface>> storedList = new HashMap<Class<? extends NumberInterface>, ArrayList<NumberInterface>>();
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
if(params[0].signum() == 0){
return (new NaiveNumber(1)).promoteTo(params[0].getClass());
}
NumberInterface factorial = params[0];
NumberInterface multiplier = params[0];
//It is necessary to later prevent calls of factorial on anything but non-negative integers.
while((multiplier = multiplier.subtract(NaiveNumber.ONE.promoteTo(multiplier.getClass()))).signum() == 1){
factorial = factorial.multiply(multiplier);
}
return factorial;
/*if(!storedList.containsKey(params[0].getClass())){
storedList.put(params[0].getClass(), new ArrayList<NumberInterface>());
storedList.get(params[0].getClass()).add(NaiveNumber.ONE.promoteTo(params[0].getClass()));
storedList.get(params[0].getClass()).add(NaiveNumber.ONE.promoteTo(params[0].getClass()));
}*/
}
}));
registerFunction("abs", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].multiply((new NaiveNumber(params[0].signum())).promoteTo(params[0].getClass()));
}
});
registerFunction("exp", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
boolean takeReciprocal = params[0].signum() == -1;
params[0] = StandardPlugin.this.getFunction("abs").apply(params[0]);
NumberInterface sum = sumSeries(params[0], StandardPlugin.this::getExpSeriesTerm, getNTermsExp(getMaxError(params[0]), params[0]));
if(takeReciprocal){
sum = NaiveNumber.ONE.promoteTo(sum.getClass()).divide(sum);
}
return sum;
}
});
registerFunction("ln", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
NumberInterface param = params[0];
int powersOf2 = 0;
while(StandardPlugin.this.getFunction("abs").apply(param.subtract(NaiveNumber.ONE.promoteTo(param.getClass()))).compareTo((new NaiveNumber(0.1)).promoteTo(param.getClass())) >= 0){
if(param.subtract(NaiveNumber.ONE.promoteTo(param.getClass())).signum() == 1) {
param = param.divide(new NaiveNumber(2).promoteTo(param.getClass()));
powersOf2++;
if(param.subtract(NaiveNumber.ONE.promoteTo(param.getClass())).signum() != 1) {
break;
//No infinite loop for you.
}
}
else {
param = param.multiply(new NaiveNumber(2).promoteTo(param.getClass()));
powersOf2--;
if(param.subtract(NaiveNumber.ONE.promoteTo(param.getClass())).signum() != 1) {
break;
//No infinite loop for you.
}
}
}
return getLog2(param).multiply((new NaiveNumber(powersOf2)).promoteTo(param.getClass())).add(getLogPartialSum(param));
}
/**
* Returns the partial sum of the Taylor series for logx (around x=1).
* Automatically determines the number of terms needed based on the precision of x.
* @param x value at which the series is evaluated. 0 < x < 2. (x=2 is convergent but impractical.)
* @return the partial sum.
*/
private NumberInterface getLogPartialSum(NumberInterface x){
NumberInterface maxError = StandardPlugin.this.getMaxError(x);
x = x.subtract(NaiveNumber.ONE.promoteTo(x.getClass())); //Terms used are for log(x+1).
NumberInterface currentTerm = x, sum = x;
int n = 1;
while(StandardPlugin.this.getFunction("abs").apply(currentTerm).compareTo(maxError) > 0){
n++;
currentTerm = currentTerm.multiply(x).multiply((new NaiveNumber(n-1)).promoteTo(x.getClass())).divide((new NaiveNumber(n)).promoteTo(x.getClass())).negate();
sum = sum.add(currentTerm);
}
return sum;
}
/**
* Returns natural log of 2 to the required precision of the class of number.
* @param number a number of the same type as the return type. (Used for precision.)
* @return the value of log(2) with the appropriate precision.
*/
private NumberInterface getLog2(NumberInterface number){
NumberInterface maxError = StandardPlugin.this.getMaxError(number);
//NumberInterface errorBound = (new NaiveNumber(1)).promoteTo(number.getClass());
//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 = (new NaiveNumber(1)).promoteTo(number.getClass()), b = a, c = a;
NumberInterface sum = NaiveNumber.ZERO.promoteTo(number.getClass());
int n = 0;
while(a.compareTo(maxError) >= 1){
n++;
a = a.divide((new NaiveNumber(3)).promoteTo(number.getClass()));
b = b.divide((new NaiveNumber(4)).promoteTo(number.getClass()));
c = NaiveNumber.ONE.promoteTo(number.getClass()).divide((new NaiveNumber(n)).promoteTo(number.getClass()));
sum = sum.add(a.add(b).multiply(c));
}
return sum;
}
});
registerFunction("sqrt", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return StandardPlugin.this.getOperator("^").getFunction().apply(params[0], ((new NaiveNumber(0.5)).promoteTo(params[0].getClass())));
}
});
}
@Override
public void onDisable() {
}
/**
* Returns the nth term of the Taylor series (centered at 0) of e^x
* @param n the term required (n >= 0).
* @param x the real number at which the series is evaluated.
* @return the nth term of the series.
*/
private NumberInterface getExpSeriesTerm(int n, NumberInterface x){
return x.intPow(n).divide(this.getOperator("!").getFunction().apply((new NaiveNumber(n)).promoteTo(x.getClass())));
}
/**
* Returns the number of terms needed to evaluate the exponential function (at x)
* such that the error is at most maxError.
* @param maxError Maximum error permissible (This should probably be positive.)
* @param x where the function is evaluated.
* @return the number of terms needed to evaluated the exponential function.
*/
private int getNTermsExp(NumberInterface maxError, NumberInterface x) {
//We need n such that |x^(n+1)| <= (n+1)! * maxError
//The variables LHS and RHS refer to the above inequality.
int n = 0;
x = this.getFunction("abs").apply(x);
NumberInterface LHS = x, RHS = maxError;
while (LHS.compareTo(RHS) > 0) {
n++;
LHS = LHS.multiply(x);
RHS = RHS.multiply(new NaiveNumber(n + 1).promoteTo(RHS.getClass()));
}
return n;
}
/**
* Returns a partial sum of a series whose terms are given by the nthTermFunction, evaluated at x.
* @param x the value at which the series is evaluated.
* @param nthTermFunction the function that returns the nth term of the series, in the format term(x, n).
* @param n the number of terms in the partial sum.
* @return the value of the partial sum that has the same class as x.
*/
private NumberInterface sumSeries(NumberInterface x, BiFunction<Integer, NumberInterface, NumberInterface> nthTermFunction, int n){
NumberInterface sum = NaiveNumber.ZERO.promoteTo(x.getClass());
for(int i = 0; i <= n; i++){
sum = sum.add(nthTermFunction.apply(i, x));
}
return sum;
}
/**
* Returns the maximum error based on the precision of the class of number.
* @param number Any instance of the NumberInterface in question (should return an appropriate precision).
* @return the maximum error.
*/
private NumberInterface getMaxError(NumberInterface number){
return (new NaiveNumber(10)).promoteTo(number.getClass()).intPow(-number.precision());
}
}

View File

@@ -1,207 +0,0 @@
package org.nwapw.abacus.tree;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.function.OperatorType;
import org.nwapw.abacus.lexing.Lexer;
import org.nwapw.abacus.lexing.pattern.Match;
import org.nwapw.abacus.lexing.pattern.Pattern;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.PreciseNumber;
import java.util.*;
/**
* The builder responsible for turning strings into trees.
*/
public class TreeBuilder {
/**
* The lexer used to get the input tokens.
*/
private Lexer<TokenType> lexer;
/**
* The map of operator precedences.
*/
private Map<String, Integer> precedenceMap;
/**
* The map of operator associativity.
*/
private Map<String, OperatorAssociativity> associativityMap;
/**
* The map of operator types.
*/
private Map<String, OperatorType> typeMap;
/**
* Comparator used to sort token types.
*/
protected static Comparator<TokenType> tokenSorter = Comparator.comparingInt(e -> e.priority);
/**
* Creates a new TreeBuilder.
*/
public TreeBuilder(){
lexer = new Lexer<TokenType>(){{
register(" ", TokenType.WHITESPACE);
register(",", TokenType.COMMA);
register("[0-9]*(\\.[0-9]+)?", TokenType.NUM);
register("\\(", TokenType.OPEN_PARENTH);
register("\\)", TokenType.CLOSE_PARENTH);
}};
precedenceMap = new HashMap<>();
associativityMap = new HashMap<>();
typeMap = new HashMap<>();
}
/**
* Registers a function with the TreeBuilder.
* @param function the function to register.
*/
public void registerFunction(String function){
lexer.register(Pattern.sanitize(function), TokenType.FUNCTION);
}
/**
* Registers an operator with the TreeBuilder.
* @param operator the operator to register.
* @param precedence the precedence of the operator.
* @param associativity the associativity of the operator.
*/
public void registerOperator(String operator, OperatorAssociativity associativity,
OperatorType operatorType, int precedence){
lexer.register(Pattern.sanitize(operator), TokenType.OP);
precedenceMap.put(operator, precedence);
associativityMap.put(operator, associativity);
typeMap.put(operator, operatorType);
}
/**
* Tokenizes a string, converting it into matches
* @param string the string to tokenize.
* @return the list of tokens produced.
*/
public List<Match<TokenType>> tokenize(String string){
return lexer.lexAll(string, 0, tokenSorter);
}
/**
* Rearranges tokens into a postfix list, using Shunting Yard.
* @param source the source string.
* @param from the tokens to be rearranged.
* @return the resulting list of rearranged tokens.
*/
public List<Match<TokenType>> intoPostfix(String source, List<Match<TokenType>> from){
ArrayList<Match<TokenType>> output = new ArrayList<>();
Stack<Match<TokenType>> tokenStack = new Stack<>();
while(!from.isEmpty()){
Match<TokenType> match = from.remove(0);
TokenType matchType = match.getType();
if(matchType == TokenType.NUM) {
output.add(match);
} else if(matchType == TokenType.FUNCTION) {
output.add(new Match<>(0, 0, TokenType.INTERNAL_FUNCTION_END));
tokenStack.push(match);
} else if(matchType == TokenType.OP){
String tokenString = source.substring(match.getFrom(), match.getTo());
OperatorType type = typeMap.get(tokenString);
int precedence = precedenceMap.get(tokenString);
OperatorAssociativity associativity = associativityMap.get(tokenString);
if(type == OperatorType.UNARY_POSTFIX){
output.add(match);
continue;
}
while(!tokenStack.empty()) {
Match<TokenType> otherMatch = tokenStack.peek();
TokenType otherMatchType = otherMatch.getType();
if(!(otherMatchType == TokenType.OP || otherMatchType == TokenType.FUNCTION)) break;
if(otherMatchType == TokenType.OP){
int otherPrecedence = precedenceMap.get(source.substring(otherMatch.getFrom(), otherMatch.getTo()));
if(otherPrecedence < precedence ||
(associativity == OperatorAssociativity.RIGHT && otherPrecedence == precedence)) {
break;
}
}
output.add(tokenStack.pop());
}
tokenStack.push(match);
} else if(matchType == TokenType.OPEN_PARENTH){
tokenStack.push(match);
} else if(matchType == TokenType.CLOSE_PARENTH || matchType == TokenType.COMMA){
while(!tokenStack.empty() && tokenStack.peek().getType() != TokenType.OPEN_PARENTH){
output.add(tokenStack.pop());
}
if(tokenStack.empty()) return null;
if(matchType == TokenType.CLOSE_PARENTH){
tokenStack.pop();
}
}
}
while(!tokenStack.empty()){
Match<TokenType> match = tokenStack.peek();
TokenType matchType = match.getType();
if(!(matchType == TokenType.OP || matchType == TokenType.FUNCTION)) return null;
output.add(tokenStack.pop());
}
return output;
}
/**
* Constructs a tree recursively from a list of tokens.
* @param source the source string.
* @param matches the list of tokens from the source string.
* @return the construct tree expression.
*/
public TreeNode fromStringRecursive(String source, List<Match<TokenType>> matches){
if(matches.size() == 0) return null;
Match<TokenType> match = matches.remove(0);
TokenType matchType = match.getType();
if(matchType == TokenType.OP){
String operator = source.substring(match.getFrom(), match.getTo());
OperatorType type = typeMap.get(operator);
if(type == OperatorType.BINARY_INFIX){
TreeNode right = fromStringRecursive(source, matches);
TreeNode left = fromStringRecursive(source, matches);
if(left == null || right == null) return null;
else return new BinaryInfixNode(operator, left, right);
} else {
TreeNode applyTo = fromStringRecursive(source, matches);
if(applyTo == null) return null;
else return new UnaryPrefixNode(operator, applyTo);
}
} else if(matchType == TokenType.NUM){
return new NumberNode(new NaiveNumber(Double.parseDouble(source.substring(match.getFrom(), match.getTo()))));
} else if(matchType == TokenType.FUNCTION){
String functionName = source.substring(match.getFrom(), match.getTo());
FunctionNode node = new FunctionNode(functionName);
while(!matches.isEmpty() && matches.get(0).getType() != TokenType.INTERNAL_FUNCTION_END){
TreeNode argument = fromStringRecursive(source, matches);
if(argument == null) return null;
node.prependChild(argument);
}
if(matches.isEmpty()) return null;
matches.remove(0);
return node;
}
return null;
}
/**
* Creates a tree node from a string.
* @param string the string to create a node from.
* @return the resulting tree.
*/
public TreeNode fromString(String string){
List<Match<TokenType>> matches = tokenize(string);
if(matches == null) return null;
matches.removeIf(m -> m.getType() == TokenType.WHITESPACE);
matches = intoPostfix(string, matches);
if(matches == null) return null;
Collections.reverse(matches);
return fromStringRecursive(string, matches);
}
}

View File

@@ -0,0 +1,133 @@
package org.nwapw.abacus.tests;
import org.junit.Assert;
import org.junit.Test;
import org.nwapw.abacus.lexing.Lexer;
import org.nwapw.abacus.lexing.pattern.Match;
import java.util.List;
public class LexerTests {
@Test
public void testBasicSuccess() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("abc", 0);
lexer.register("def", 1);
List<Match<Integer>> matchedIntegers = lexer.lexAll("abcdefabc", 0, Integer::compare);
Assert.assertNotNull(matchedIntegers);
Assert.assertEquals(matchedIntegers.get(0).getType(), Integer.valueOf(0));
Assert.assertEquals(matchedIntegers.get(1).getType(), Integer.valueOf(1));
Assert.assertEquals(matchedIntegers.get(2).getType(), Integer.valueOf(0));
}
@Test
public void testBasicFailure() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("abc", 0);
lexer.register("def", 1);
Assert.assertNull(lexer.lexAll("abcdefabcz", 0, Integer::compare));
}
@Test
public void testNoPatterns() {
Lexer<Integer> lexer = new Lexer<>();
Assert.assertNull(lexer.lexAll("abcdefabc", 0, Integer::compare));
}
@Test
public void testEmptyMatches() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("a?", 0);
Assert.assertNull(lexer.lexAll("", 0, Integer::compare));
}
@Test
public void testOneOrMore() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("a+", 0);
List<Match<Integer>> tokens = lexer.lexAll("aaaa", 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), 1);
}
@Test
public void testZeroOrMore() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("a*", 0);
List<Match<Integer>> tokens = lexer.lexAll("aaaa", 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), 1);
}
@Test
public void testZeroOrOne() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("a?", 0);
List<Match<Integer>> tokens = lexer.lexAll("aaaa", 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), 4);
}
@Test
public void testGreedyMatching() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("a*a", 0);
List<Match<Integer>> tokens = lexer.lexAll("aaaa", 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), 1);
}
@Test
public void testAnyCharacter() {
String testString = "abcdef";
Lexer<Integer> lexer = new Lexer<>();
lexer.register(".", 0);
List<Match<Integer>> tokens = lexer.lexAll(testString, 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), testString.length());
for (int i = 0; i < tokens.size(); i++) {
Assert.assertEquals(testString.substring(i, i + 1), tokens.get(i).getContent());
}
}
@Test
public void testBasicGroup() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("(abc)", 0);
List<Match<Integer>> tokens = lexer.lexAll("abc", 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), 1);
Assert.assertEquals(tokens.get(0).getContent(), "abc");
}
@Test
public void testBasicRangeSuccess() {
String testString = "abcdef";
Lexer<Integer> lexer = new Lexer<>();
lexer.register("[a-f]", 0);
List<Match<Integer>> tokens = lexer.lexAll(testString, 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(testString.length(), tokens.size());
for (int i = 0; i < tokens.size(); i++) {
Assert.assertEquals(testString.substring(i, i + 1), tokens.get(i).getContent());
}
}
@Test
public void testBasicRangeFailure() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("[a-f]", 0);
Assert.assertNull(lexer.lexAll("g", 0, Integer::compare));
}
@Test
public void testGroupAndOperator() {
Lexer<Integer> lexer = new Lexer<>();
lexer.register("(abc)+", 0);
List<Match<Integer>> tokens = lexer.lexAll("abcabc", 0, Integer::compare);
Assert.assertNotNull(tokens);
Assert.assertEquals(tokens.size(), 1);
}
}

View File

@@ -0,0 +1,124 @@
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.function.Function;
import org.nwapw.abacus.function.Operator;
import org.nwapw.abacus.function.OperatorAssociativity;
import org.nwapw.abacus.function.OperatorType;
import org.nwapw.abacus.lexing.pattern.Match;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.parsing.LexerTokenizer;
import org.nwapw.abacus.plugin.Plugin;
import org.nwapw.abacus.tree.TokenType;
import java.util.List;
public class TokenizerTests {
private static Abacus abacus = new Abacus();
private static LexerTokenizer lexerTokenizer = new LexerTokenizer();
private static Function subtractFunction = new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].subtract(params[1]);
}
};
private static Plugin testPlugin = new Plugin(abacus.getPluginManager()) {
@Override
public void onEnable() {
registerOperator("+", new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX,
0, subtractFunction));
registerOperator("-", new Operator(OperatorAssociativity.LEFT, OperatorType.BINARY_INFIX,
0, subtractFunction));
registerFunction("subtract", subtractFunction);
}
@Override
public void onDisable() {
}
};
private static void assertTokensMatch(List<Match<TokenType>> tokenList, TokenType[] expectedTypes) {
Assert.assertNotNull(tokenList);
Assert.assertEquals(tokenList.size(), expectedTypes.length);
for (int i = 0; i < expectedTypes.length; i++) {
Assert.assertEquals(expectedTypes[i], tokenList.get(i).getType());
}
}
@BeforeClass
public static void prepareTests() {
abacus.getPluginManager().addListener(lexerTokenizer);
abacus.getPluginManager().addInstantiated(testPlugin);
abacus.getPluginManager().load();
}
@Test
public void testInteger() {
assertTokensMatch(lexerTokenizer.tokenizeString("11"), new TokenType[]{TokenType.NUM});
}
@Test
public void testLeadingZeroDecimal() {
assertTokensMatch(lexerTokenizer.tokenizeString("0.1"), new TokenType[]{TokenType.NUM});
}
@Test
public void testNonLeadingDecimal() {
assertTokensMatch(lexerTokenizer.tokenizeString(".1"), new TokenType[]{TokenType.NUM});
}
@Test
public void testSimpleChars() {
TokenType[] types = {
TokenType.OPEN_PARENTH,
TokenType.WHITESPACE,
TokenType.COMMA,
TokenType.CLOSE_PARENTH
};
assertTokensMatch(lexerTokenizer.tokenizeString("( ,)"), types);
}
@Test
public void testFunctionParsing() {
TokenType[] types = {
TokenType.FUNCTION,
TokenType.OPEN_PARENTH,
TokenType.NUM,
TokenType.COMMA,
TokenType.NUM,
TokenType.CLOSE_PARENTH
};
assertTokensMatch(lexerTokenizer.tokenizeString("subtract(1,2)"), types);
}
@Test
public void testOperatorParsing() {
TokenType[] types = {
TokenType.NUM,
TokenType.OP,
TokenType.NUM
};
assertTokensMatch(lexerTokenizer.tokenizeString("1-1"), types);
}
@Test
public void testSanitizedOperators() {
TokenType[] types = {
TokenType.NUM,
TokenType.OP,
TokenType.NUM
};
assertTokensMatch(lexerTokenizer.tokenizeString("1+1"), types);
}
}