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

Compare commits

...

70 Commits

Author SHA1 Message Date
21d88fe256 Update README.md 2017-07-30 14:04:24 -07:00
3d61ead0f6 Update README.md 2017-07-30 14:03:58 -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
5f2f2c8589 Add more entries to .gitignore. 2017-07-28 18:48:37 -07:00
7b74b734a3 Generate the application configuration for abacus. 2017-07-28 18:45:56 -07:00
352c578d15 Set up a gradle wrapper for abacus. 2017-07-28 17:18:14 -07:00
5c301e4afa Load default plugin without jars. 2017-07-28 15:22:38 -07:00
8c5306051e Add a toString for UnaryPrefixNode 2017-07-28 14:57:11 -07:00
c3bb3d7d3f Remove unused default value from PreciseNumber 2017-07-28 14:55:01 -07:00
556a72f946 Change the constructor for NumberNode 2017-07-28 14:47:34 -07:00
f303093a3f Add a promotion priority to PerciseNumber 2017-07-28 14:39:37 -07:00
8dae4a880e Comment class finder. 2017-07-28 14:32:27 -07:00
243dc81deb Change ClassFinder code to be static. 2017-07-28 14:21:43 -07:00
0c07695991 Merge branch 'unary' 2017-07-28 13:28:59 -07:00
ff689f9bd5 Merge branch 'big-decimal' 2017-07-28 13:18:44 -07:00
Arthur Drobot
c184b55738 Implement preciseNumber for arbitrary precision. 2017-07-28 11:38:22 -07:00
dc410917b3 Remove old ClassFinder and hardcoded folder names, and fix class filter. 2017-07-28 11:35:23 -07:00
9850f896bb Remove unneeded files from git. 2017-07-28 11:23:25 -07:00
0b3648d4f3 Fix null pointer exceptions associated with turning ! into an operator. 2017-07-28 11:19:41 -07:00
rileyJones
69e3b55643 Add external plugin support 2017-07-28 11:17:54 -07:00
2ba6e22fcb Rename OpNode to BinaryInfixNode. 2017-07-28 11:15:36 -07:00
5228773b5e Implement unary operators. 2017-07-28 11:14:45 -07:00
42393ca6a6 Add operator types. 2017-07-28 10:26:25 -07:00
b20ddc2013 Add rounding to naive number. 2017-07-28 09:57:39 -07:00
a881640bf6 Make the decimal matching optional. 2017-07-28 09:52:14 -07:00
0263086e10 Add link nodes to collections and check for their presence.
This prevents infinite loops, as Link nodes do not follow regular
addInto behavior.
2017-07-28 09:51:59 -07:00
c9fad36d16 Fix bug causing an NPE when sqrt() was called. 2017-07-27 19:30:40 -07:00
2cc4bd14ce Switch all uses of *List, *Map to just List and Map. 2017-07-27 18:19:12 -07:00
f119f19c04 Make pow an operator, represented by caret. 2017-07-27 16:55:18 -07:00
65772c8d57 Fix function argument order. 2017-07-27 16:52:16 -07:00
bbbb2e855e Fix typo. 2017-07-27 16:37:54 -07:00
8a29019852 Add ignoring whitespace and fix function precedence. 2017-07-27 16:36:13 -07:00
0d7a416446 Add a missing comment. 2017-07-27 16:27:26 -07:00
167e13cfe1 Merge branch 'master' of github.com:DanilaFe/abacus 2017-07-27 15:28:21 -07:00
b0ae3f90fc Add sanitization to TreeBuilder. 2017-07-27 15:26:02 -07:00
a7c2084254 Remove backslashes that will be obsolete. 2017-07-27 15:02:13 -07:00
rileyJones
bf6f48bf82 Jar Plugin Loader 2017-07-27 14:33:08 -07:00
f7da896fc0 Fix several bugs and register operations as operations. 2017-07-27 14:15:45 -07:00
6813643b15 Merge branch 'plugins' 2017-07-27 14:08:40 -07:00
e6cb755ec9 Merge branch 'master' of github.com:DanilaFe/abacus 2017-07-27 14:08:31 -07:00
Arthur Drobot
088a45cf4c Add sqrt function. 2017-07-27 13:47:51 -07:00
Arthur Drobot
557bc66e53 Begin working on memoization for factorial. (Commented out for now.) 2017-07-27 13:39:19 -07:00
Arthur Drobot
9666ef9019 Add pow function. 2017-07-27 13:17:22 -07:00
Arthur Drobot
ba30227b28 Add natural log function. May not be terribly efficient currently, but it works and is usable. 2017-07-27 13:04:41 -07:00
Arthur Drobot
ea5a7a9558 Increase precision of NaiveNumber to 18. 2017-07-27 10:32:09 -07:00
Arthur Drobot
3e52a9d645 Modify exp to work properly with the new changes and support all reals. 2017-07-27 10:16:38 -07:00
Arthur Drobot
7a0fa31cad Merge branch 'master' of https://github.com/DanilaFe/abacus 2017-07-27 10:07:07 -07:00
Arthur Drobot
aec37b6720 Add absolute value function to standard plugin. Modify getNTermsExp to work on negative exponents instead (and correctly). 2017-07-27 10:03:26 -07:00
54 changed files with 1758 additions and 544 deletions

9
.gitignore vendored
View File

@@ -20,3 +20,12 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Custom Stuff
# Gradle
.gradle/*
build/*
# IntelliJ
.idea/*
abacus.iml

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 proves 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 teams 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.

15
build.gradle Normal file
View File

@@ -0,0 +1,15 @@
apply plugin: 'java'
apply plugin: 'application'
repositories {
mavenCentral()
}
dependencies {
compile 'com.moandjiezana.toml:toml4j:0.7.1'
testCompile 'junit:junit:4.12'
}
// Define the main class for the application
mainClassName = 'org.nwapw.abacus.Abacus'

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Fri Jul 28 17:18:51 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip

172
gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'abacus'

View File

@@ -0,0 +1,171 @@
package org.nwapw.abacus;
import org.nwapw.abacus.config.ConfigurationObject;
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 org.nwapw.abacus.window.Window;
import javax.swing.*;
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 main Abacus UI.
*/
private Window mainUi;
/**
* 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 ConfigurationObject 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(this);
mainUi = new Window(this);
numberReducer = new NumberReducer(this);
configuration = new ConfigurationObject(CONFIG_FILE);
configuration.save(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();
mainUi.setVisible(true);
}
/**
* 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;
}
/**
* Gets the current UI.
* @return the UI window in this abacus instance.
*/
public Window getMainUi() {
return mainUi;
}
/**
* 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 ConfigurationObject 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);
}
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;
}
public static void main(String[] args){
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | UnsupportedLookAndFeelException | IllegalAccessException e) {
e.printStackTrace();
}
new Abacus();
}
}

View File

@@ -0,0 +1,14 @@
package org.nwapw.abacus.config;
/**
* Serializable class that will be used to load TOML
* configurations.
*/
public class Configuration {
/**
* The type of number this calculator should use.
*/
public String numberType;
}

View File

@@ -0,0 +1,105 @@
package org.nwapw.abacus.config;
import com.moandjiezana.toml.Toml;
import com.moandjiezana.toml.TomlWriter;
import java.io.File;
import java.io.IOException;
/**
* A configuration object, which essentially
* manages saving, loading, and getting values
* from the configuration. While Configuration is
* the data model, this is the interface with it.
*/
public class ConfigurationObject {
/**
* The writer used to store the configuration.
*/
private static final TomlWriter TOML_WRITER = new TomlWriter();
/**
* The configuration instance being modeled.
*/
private Configuration configuration;
/**
* Sets up the ConfigurationObject.
* different constructors do different things,
* but they all lead here.
* @param configuration the configuration to set up with.
*/
private void setup(Configuration configuration){
this.configuration = configuration;
}
/**
* Creates a default configuration.
* @return the newly created default configuration.
*/
private Configuration getDefaultConfig(){
configuration = new Configuration();
configuration.numberType = "naive";
return configuration;
}
/**
* Returns the implementation the user has requested to
* represent their numbers.
* @return the implementation name.
*/
public String getNumberImplementation() {
return configuration.numberType;
}
/**
* Saves the ConfigurationObject to the given file.
* @param toFile the file to save ot.
* @return true if the save succeed, false if otherwise.
*/
public boolean save(File toFile){
if(toFile.getParentFile() != null) toFile.getParentFile().mkdirs();
try {
TOML_WRITER.write(configuration, toFile);
return true;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* Creates a new configuration object with the given config.
* @param config the config to use.
*/
public ConfigurationObject(Configuration config){
setup(config);
}
/**
* Create a configuration object by attempting to
* load a config from the given path, using the
* default configuration otherwise.
* @param path the path to attempt to load.
*/
public ConfigurationObject(File path){
Configuration config;
if(!path.exists()) {
config = getDefaultConfig();
} else {
Toml parse = new Toml();
parse.read(path);
config = parse.to(Configuration.class);
}
setup(config);
}
/**
* Creates a new configuration object with the
* default configuration.
*/
public ConfigurationObject(){
setup(getDefaultConfig());
}
}

View File

@@ -2,6 +2,7 @@ package org.nwapw.abacus.function;
import org.nwapw.abacus.number.NaiveNumber;
import org.nwapw.abacus.number.NumberInterface;
import org.nwapw.abacus.number.PreciseNumber;
import java.util.HashMap;
@@ -17,6 +18,7 @@ public abstract class Function {
private static final HashMap<Class<? extends NumberInterface>, Integer> priorityMap =
new HashMap<Class<? extends NumberInterface>, Integer>() {{
put(NaiveNumber.class, 0);
put(PreciseNumber.class, 1);
}};
/**

View File

@@ -3,12 +3,16 @@ package org.nwapw.abacus.function;
/**
* A class that represents a single infix operator.
*/
public abstract class Operator {
public class Operator {
/**
* The associativity of the operator.
*/
private OperatorAssociativity associativity;
/**
* The type of this operator.
*/
private OperatorType type;
/**
* The precedence of the operator.
*/
@@ -24,8 +28,9 @@ public abstract class Operator {
* @param precedence the precedence of the operator.
* @param function the function that the operator calls.
*/
public Operator(OperatorAssociativity associativity, int precedence, Function function){
public Operator(OperatorAssociativity associativity, OperatorType operatorType, int precedence, Function function){
this.associativity = associativity;
this.type = operatorType;
this.precedence = precedence;
this.function = function;
}
@@ -38,6 +43,14 @@ public abstract class Operator {
return associativity;
}
/**
* Gets the operator's type.
* @return the type.
*/
public OperatorType getType() {
return type;
}
/**
* Gets the operator's precedence.
* @return the precedence.

View File

@@ -0,0 +1,8 @@
package org.nwapw.abacus.function;
/**
* The type of an operator, describing how it should behave.
*/
public enum OperatorType {
BINARY_INFIX, UNARY_POSTFIX
}

View File

@@ -54,7 +54,7 @@ public class Lexer<T> {
/**
* The registered patterns.
*/
private HashMap<PatternEntry<T>, Pattern<T>> patterns;
private Map<PatternEntry<T>, Pattern<T>> patterns;
/**
* Creates a new lexer with no registered patterns.
@@ -102,7 +102,7 @@ public class Lexer<T> {
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()));
matches.add(new Match<>(from.substring(startAt, index), ((EndNode<T>) node).getPatternId()));
}
}
@@ -115,7 +115,7 @@ public class Lexer<T> {
}
matches.sort((a, b) -> compare.compare(a.getType(), b.getType()));
if(compare != null) {
matches.sort(Comparator.comparingInt(a -> a.getTo() - a.getFrom()));
matches.sort(Comparator.comparingInt(a -> a.getContent().length()));
}
return matches.isEmpty() ? null : matches.get(matches.size() - 1);
}
@@ -127,14 +127,15 @@ public class Lexer<T> {
* @param compare the comparator used to sort matches by their IDs.
* @return the resulting list of matches, in order, or null on error.
*/
public ArrayList<Match<T>> lexAll(String from, int startAt, Comparator<T> compare){
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;
int length = lastMatch.getContent().length();
if(length == 0) return null;
matches.add(lastMatch);
index += lastMatch.getTo() - lastMatch.getFrom();
index += length;
}
if(lastMatch == null) return null;
return matches;

View File

@@ -11,7 +11,10 @@ public class LinkNode<T> extends PatternNode<T> {
@Override
public void addInto(Collection<PatternNode<T>> into) {
addOutputsInto(into);
if(!into.contains(this)) {
into.add(this);
addOutputsInto(into);
}
}
}

View File

@@ -0,0 +1,43 @@
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

@@ -2,6 +2,7 @@ package org.nwapw.abacus.lexing.pattern;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.function.Function;
@@ -32,7 +33,7 @@ public class Pattern<T> {
* A map of regex operator to functions that modify a PatternChain
* with the appropriate operation.
*/
private HashMap<Character, Function<PatternChain<T>, PatternChain<T>>> operations =
private Map<Character, Function<PatternChain<T>, PatternChain<T>>> operations =
new HashMap<Character, Function<PatternChain<T>, PatternChain<T>>>() {{
put('+', Pattern.this::transformPlus);
put('*', Pattern.this::transformStar);
@@ -235,4 +236,22 @@ public class Pattern<T> {
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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* A base class for a pattern node. Provides all functions
@@ -16,7 +17,7 @@ public class PatternNode<T> {
* The set of states to which the lexer should continue
* should this node be correctly matched.
*/
protected HashSet<PatternNode<T>> outputStates;
protected Set<PatternNode<T>> outputStates;
/**
* Creates a new pattern node.

View File

@@ -10,6 +10,13 @@ public class NaiveNumber implements NumberInterface {
*/
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.
@@ -28,8 +35,8 @@ public class NaiveNumber implements NumberInterface {
public static final NaiveNumber ONE = new NaiveNumber(1);
@Override
public int precision() {
return 15;
public int getMaxPrecision() {
return 18;
}
@Override
@@ -88,11 +95,15 @@ public class NaiveNumber implements NumberInterface {
@Override
public NumberInterface promoteTo(Class<? extends NumberInterface> toClass) {
if(toClass == this.getClass()) return this;
else if(toClass == PreciseNumber.class){
return new PreciseNumber(Double.toString(value));
}
return null;
}
public String toString(){
return Double.toString(value);
double shiftBy = Math.pow(10, 10);
return Double.toString(Math.round(value * shiftBy) / shiftBy);
}
}

View File

@@ -6,10 +6,10 @@ 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

View File

@@ -0,0 +1,113 @@
package org.nwapw.abacus.number;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class PreciseNumber implements NumberInterface{
/**
* The number one.
*/
static final PreciseNumber ONE = new PreciseNumber(BigDecimal.ONE);
/**
* The number zero.
*/
static final PreciseNumber ZERO = new PreciseNumber(BigDecimal.ZERO);
/**
* The number ten.
*/
static final PreciseNumber TEN = new PreciseNumber(BigDecimal.TEN);
/**
* The value of the PreciseNumber.
*/
BigDecimal value;
/**
* 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){
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){
this.value = value;
}
@Override
public int getMaxPrecision() {
return 54;
}
@Override
public NumberInterface multiply(NumberInterface multiplier) {
return new PreciseNumber(value.multiply(((PreciseNumber) multiplier).value));
}
@Override
public NumberInterface divide(NumberInterface divisor) {
return new PreciseNumber(value.divide(((PreciseNumber) divisor).value, this.getMaxPrecision(), RoundingMode.HALF_UP));
}
@Override
public NumberInterface add(NumberInterface summand) {
return new PreciseNumber(value.add(((PreciseNumber) summand).value));
}
@Override
public NumberInterface subtract(NumberInterface subtrahend) {
return new PreciseNumber(value.subtract(((PreciseNumber) subtrahend).value));
}
@Override
public NumberInterface intPow(int exponent) {
if(exponent == 0){
return PreciseNumber.ONE;
}
boolean takeReciprocal = exponent < 0;
exponent = Math.abs(exponent);
NumberInterface power = this;
for(int currentExponent = 1; currentExponent < exponent; currentExponent++){
power = power.multiply(this);
}
if(takeReciprocal){
power = PreciseNumber.ONE.divide(power);
}
return power;
}
@Override
public int compareTo(NumberInterface number) {
return value.compareTo(((PreciseNumber) number).value);
}
@Override
public int signum() {
return value.signum();
}
@Override
public NumberInterface negate(){
return new PreciseNumber(value.negate());
}
@Override
public NumberInterface promoteTo(Class<? extends NumberInterface> toClass) {
if(toClass == this.getClass()){
return this;
}
return null;
}
@Override
public String toString() {
BigDecimal rounded = value.setScale(getMaxPrecision() - 4, 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,20 @@
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,173 @@
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,18 @@
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,23 @@
package org.nwapw.abacus.parsing;
import org.nwapw.abacus.tree.TreeNode;
import java.util.List;
public class TreeBuilder<T> {
private Tokenizer<T> tokenizer;
private Parser<T> parser;
public TreeBuilder(Tokenizer<T> tokenizer, Parser<T> parser){
this.tokenizer = tokenizer;
this.parser = parser;
}
public TreeNode fromString(String input){
List<T> tokens = tokenizer.tokenizeString(input);
if(tokens == null) return null;
return parser.constructTree(tokens);
}
}

View File

@@ -0,0 +1,77 @@
package org.nwapw.abacus.plugin;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
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;
/**
* Class that loads plugin classes from their jars.
*/
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 ClassNotFoundException thrown if the class listed in the file doesn't get loaded.
*/
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 ClassNotFoundException thrown if the class listed in the file doesn't get loaded.
*/
public static List<Class<?>> loadJars(File pluginFolderPath) throws IOException, ClassNotFoundException {
ArrayList<Class<?>> toReturn = new ArrayList<>();
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){
toReturn.addAll(loadJar(file));
}
return toReturn;
}
/**
* 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 ClassNotFoundException thrown if the class could not be loaded.
*/
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 + "!/")};
URLClassLoader classLoader = URLClassLoader.newInstance(urls);
JarFile jarFolder = new JarFile(jarLocation);
Enumeration jarEntityList = jarFolder.entries();
while (jarEntityList.hasMoreElements()) {
JarEntry jarEntity = (JarEntry) jarEntityList.nextElement();
if (jarEntity.getName().endsWith(".class")) {
loadedClasses.add(classLoader.loadClass(jarEntity.getName().replace('/', '.').substring(0, jarEntity.getName().length() - 6)));
}
}
return loadedClasses;
}
}

View File

@@ -2,8 +2,10 @@ 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;
import java.util.Set;
/**
@@ -18,11 +20,15 @@ public abstract class Plugin {
/**
* A hash map of functions mapped to their string names.
*/
private HashMap<String, Function> functions;
private Map<String, Function> functions;
/**
* A hash map of operators mapped to their string names.
*/
private HashMap<String, Operator> operators;
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,
@@ -43,6 +49,7 @@ public abstract class Plugin {
this.manager = manager;
functions = new HashMap<>();
operators = new HashMap<>();
numbers = new HashMap<>();
enabled = false;
}
@@ -62,6 +69,14 @@ public abstract class Plugin {
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
@@ -80,6 +95,15 @@ 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.
@@ -123,6 +147,18 @@ public abstract class Plugin {
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

View File

@@ -1,7 +1,9 @@
package org.nwapw.abacus.plugin;
import org.nwapw.abacus.Abacus;
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.*;
@@ -12,42 +14,64 @@ import java.util.*;
*/
public class PluginManager {
/**
* List of classes loaded by this manager.
*/
private Set<Class<?>> loadedPluginClasses;
/**
* A list of loaded plugins.
*/
private ArrayList<Plugin> plugins;
private Set<Plugin> plugins;
/**
* List of functions that have been cached,
* that is, found in a plugin and returned.
*/
private HashMap<String, Function> cachedFunctions;
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 HashMap<String, Operator> cachedOperators;
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.
*/
private HashSet<String> allFunctions;
private Set<String> allFunctions;
/**
* List of all operators loaded by the plugins.
*/
private HashSet<String> allOperators;
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.
*/
private HashSet<PluginListener> listeners;
private Set<PluginListener> listeners;
/**
* The instance of Abacus that is used to interact with its other
* components.
*/
private Abacus abacus;
/**
* Creates a new plugin manager.
*/
public PluginManager(){
plugins = new ArrayList<>();
public PluginManager(Abacus abacus){
this.abacus = abacus;
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<>();
}
@@ -99,12 +123,23 @@ public class PluginManager {
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;
plugins.add(plugin);
loadedPluginClasses.add(plugin.getClass());
}
/**
@@ -113,7 +148,7 @@ public class PluginManager {
* @param newClass the new class to instantiate.
*/
public void addClass(Class<?> newClass){
if(!Plugin.class.isAssignableFrom(newClass)) return;
if(!Plugin.class.isAssignableFrom(newClass) || newClass == Plugin.class) return;
try {
addInstantiated((Plugin) newClass.getConstructor(PluginManager.class).newInstance(this));
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
@@ -129,6 +164,7 @@ public class PluginManager {
for(Plugin plugin : plugins){
allFunctions.addAll(plugin.providedFunctions());
allOperators.addAll(plugin.providedOperators());
allNumbers.addAll(plugin.providedNumbers());
}
listeners.forEach(e -> e.onLoad(this));
}
@@ -140,6 +176,7 @@ public class PluginManager {
for(Plugin plugin : plugins) plugin.disable();
allFunctions.clear();
allOperators.clear();
allNumbers.clear();
listeners.forEach(e -> e.onUnload(this));
}
@@ -155,7 +192,7 @@ public class PluginManager {
* Gets all the functions loaded by the Plugin Manager.
* @return the set of all functions that were loaded.
*/
public HashSet<String> getAllFunctions() {
public Set<String> getAllFunctions() {
return allFunctions;
}
@@ -163,10 +200,18 @@ public class PluginManager {
* Gets all the operators loaded by the Plugin Manager.
* @return the set of all operators that were loaded.
*/
public HashSet<String> getAllOperators() {
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.

View File

@@ -0,0 +1,302 @@
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.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() {
registerNumber("naive", NaiveNumber.class);
registerNumber("precise", PreciseNumber.class);
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.getMaxPrecision());
}
}

View File

@@ -3,7 +3,7 @@ package org.nwapw.abacus.tree;
/**
* A tree node that represents an operation being applied to two operands.
*/
public class OpNode extends TreeNode {
public class BinaryInfixNode extends TreeNode {
/**
* The operation being applied.
@@ -18,14 +18,14 @@ public class OpNode extends TreeNode {
*/
private TreeNode right;
private OpNode() {}
private BinaryInfixNode() {}
/**
* Creates a new operation node with the given operation
* and no child nodes.
* @param operation the operation.
*/
public OpNode(String operation){
public BinaryInfixNode(String operation){
this(operation, null, null);
}
@@ -36,7 +36,7 @@ public class OpNode extends TreeNode {
* @param left the left node of the expression.
* @param right the right node of the expression.
*/
public OpNode(String operation, TreeNode left, TreeNode right){
public BinaryInfixNode(String operation, TreeNode left, TreeNode right){
this.operation = operation;
this.left = left;
this.right = right;

View File

@@ -1,6 +1,7 @@
package org.nwapw.abacus.tree;
import java.util.ArrayList;
import java.util.List;
/**
* A node that represents a function call.
@@ -14,7 +15,7 @@ public class FunctionNode extends TreeNode {
/**
* The list of arguments to the function.
*/
private ArrayList<TreeNode> children;
private List<TreeNode> children;
/**
* Creates a function node with no function.
@@ -39,13 +40,21 @@ public class FunctionNode extends TreeNode {
}
/**
* Adds a child to this node.
* Adds a child to the end of this node's child list.
* @param node the child to add.
*/
public void addChild(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) {
children.add(0, node);
}
@Override
public <T> T reduce(Reducer<T> reducer) {
Object[] reducedChildren = new Object[children.size()];

View File

@@ -22,19 +22,10 @@ public class NumberNode extends TreeNode {
/**
* Creates a new number node with the given double value.
* @param value the value to use.
* @param newNumber the number for which to create a number node.
*/
public NumberNode(double value){
number = new NaiveNumber(value);
}
/**
* Creates a new number node with the given string value, converted
* to a double.
* @param value the value.
*/
public NumberNode(String value){
this(Double.parseDouble(value));
public NumberNode(NumberInterface newNumber){
this.number = newNumber;
}
/**

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,32 +13,37 @@ 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) {
return ((NumberNode) node).getNumber();
} else if(node instanceof OpNode){
} else if(node instanceof BinaryInfixNode){
NumberInterface left = (NumberInterface) children[0];
NumberInterface right = (NumberInterface) children[1];
Function function = manager.functionFor(((OpNode) node).getOperation());
Function function = abacus.getPluginManager().operatorFor(((BinaryInfixNode) node).getOperation()).getFunction();
if(function == null) return null;
return function.apply(left, right);
} else if(node instanceof UnaryPrefixNode) {
NumberInterface child = (NumberInterface) children[0];
Function functionn = abacus.getPluginManager().operatorFor(((UnaryPrefixNode) node).getOperation()).getFunction();
if(functionn == null) return null;
return functionn.apply(child);
} else if(node instanceof FunctionNode){
NumberInterface[] convertedChildren = new NumberInterface[children.length];
for(int i = 0; i < convertedChildren.length; i++){
convertedChildren[i] = (NumberInterface) children[i];
}
Function function = manager.functionFor(((FunctionNode) node).getFunction());
Function function = abacus.getPluginManager().functionFor(((FunctionNode) node).getFunction());
if(function == null) return null;
return function.apply(convertedChildren);
}

View File

@@ -7,7 +7,7 @@ package org.nwapw.abacus.tree;
public enum TokenType {
INTERNAL_FUNCTION_END(-1),
ANY(0), COMMA(1), OP(2), NUM(3), FUNCTION(4), OPEN_PARENTH(5), CLOSE_PARENTH(6);
ANY(0), WHITESPACE(1), COMMA(2), OP(3), NUM(4), FUNCTION(5), OPEN_PARENTH(6), CLOSE_PARENTH(7);
/**
* The priority by which this token gets sorted.

View File

@@ -0,0 +1,59 @@
package org.nwapw.abacus.tree;
public class UnaryPrefixNode extends TreeNode {
/**
* The operation this node will apply.
*/
private String operation;
/**
* The tree node to apply the operation to.
*/
private TreeNode applyTo;
/**
* Creates a new node with the given operation and no child.
* @param operation the operation for this node.
*/
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.
*/
public UnaryPrefixNode(String operation, TreeNode applyTo){
this.operation = operation;
this.applyTo = applyTo;
}
@Override
public <T> T reduce(Reducer<T> reducer) {
Object reducedChild = applyTo.reduce(reducer);
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() {
return operation;
}
/**
* Gets the node to which this node's operation applies.
* @return the tree node to which the operation will be applied.
*/
public TreeNode getApplyTo() {
return applyTo;
}
@Override
public String toString() {
return "(" + (applyTo == null ? "null" : applyTo.toString()) + ")" + operation;
}
}

View File

@@ -6,6 +6,7 @@ import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import java.util.ArrayList;
import java.util.List;
/**
* A table model to store data about the history of inputs
@@ -57,7 +58,7 @@ public class HistoryTableModel extends AbstractTableModel {
/**
* The list of entries.
*/
ArrayList<HistoryEntry> entries;
List<HistoryEntry> entries;
/**
* Creates a new empty history table model

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());
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,11 @@ 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;
}
/**
@@ -261,20 +246,4 @@ 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.getPrecedence(), operatorObject.getAssociativity());
}
}
@Override
public void onUnload(PluginManager manager) {
builder = null;
}
}

View File

@@ -1,35 +0,0 @@
package org.nwapw.abacus;
import org.nwapw.abacus.plugin.PluginManager;
import org.nwapw.abacus.plugin.StandardPlugin;
import org.nwapw.abacus.window.Window;
import javax.swing.*;
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));
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,171 +0,0 @@
package org.nwapw.abacus.plugin;
import org.nwapw.abacus.function.Function;
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() {
registerFunction("+", 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;
}
});
registerFunction("-", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].subtract(params[1]);
}
});
registerFunction("*", 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;
}
});
registerFunction("/", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 2;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return params[0].divide(params[1]);
}
});
registerFunction("!", new Function() {
@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;
}
});
registerFunction("exp", new Function() {
@Override
protected boolean matchesParams(NumberInterface[] params) {
return params.length == 1;
}
@Override
protected NumberInterface applyInternal(NumberInterface[] params) {
return sumSeries(params[0], StandardPlugin.this::getExpSeriesTerm, getNTermsExp(getMaxError(params[0]), params[0]));
}
});
}
@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.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+2) <= (n+1)! * maxError
//The variables LHS and RHS refer to the above inequality.
int n = 0;
NumberInterface LHS = x.intPow(2), RHS = maxError;
while(LHS.compareTo(RHS) > 0){
n++;
LHS = LHS.multiply(x);
RHS = RHS.multiply(new NaiveNumber(n).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,178 +0,0 @@
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.*;
/**
* 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 HashMap<String, Integer> precedenceMap;
/**
* The map of operator associativity.
*/
private HashMap<String, OperatorAssociativity> associativityMap;
/**
* 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.COMMA);
register("[0-9]+(\\.[0-9]+)?", TokenType.NUM);
register("\\(", TokenType.OPEN_PARENTH);
register("\\)", TokenType.CLOSE_PARENTH);
}};
precedenceMap = new HashMap<>();
associativityMap = new HashMap<>();
}
/**
* Registers a function with the TreeBuilder.
* @param function the function to register.
*/
public void registerFunction(String function){
lexer.register(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, int precedence, OperatorAssociativity associativity){
lexer.register(operator, TokenType.OP);
precedenceMap.put(operator, precedence);
associativityMap.put(operator, associativity);
}
/**
* Tokenizes a string, converting it into matches
* @param string the string to tokenize.
* @return the list of tokens produced.
*/
public ArrayList<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 ArrayList<Match<TokenType>> intoPostfix(String source, ArrayList<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());
int precedence = precedenceMap.get(tokenString);
OperatorAssociativity associativity = associativityMap.get(tokenString);
while(!tokenStack.empty()) {
Match<TokenType> otherMatch = tokenStack.peek();
TokenType otherMatchType = otherMatch.getType();
if(otherMatchType != TokenType.OP) break;
int otherPrecdence = precedenceMap.get(source.substring(otherMatch.getFrom(), otherMatch.getTo()));
if(otherPrecdence < precedence ||
(associativity == OperatorAssociativity.RIGHT && otherPrecdence == 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, ArrayList<Match<TokenType>> matches){
if(matches.size() == 0) return null;
Match<TokenType> match = matches.remove(0);
TokenType matchType = match.getType();
if(matchType == TokenType.OP){
TreeNode right = fromStringRecursive(source, matches);
TreeNode left = fromStringRecursive(source, matches);
if(left == null || right == null) return null;
else return new OpNode(source.substring(match.getFrom(), match.getTo()), left, right);
} else if(matchType == TokenType.NUM){
return new NumberNode(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.addChild(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){
ArrayList<Match<TokenType>> matches = tokenize(string);
if(matches == null) return null;
matches = intoPostfix(string, matches);
if(matches == null) return null;
Collections.reverse(matches);
return fromStringRecursive(string, matches);
}
}

View File

@@ -0,0 +1,44 @@
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.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));
}
}