package lipstone.joshua.parser;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.MathContext;
import java.net.URLDecoder;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Stack;
import java.util.TreeMap;
import java.util.concurrent.ForkJoinPool;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lipstone.joshua.customStructures.lists.SortedList;
import lipstone.joshua.parser.exceptions.InvalidOperationException;
import lipstone.joshua.parser.exceptions.ParserException;
import lipstone.joshua.parser.exceptions.PluginConflictException;
import lipstone.joshua.parser.exceptions.SyntaxException;
import lipstone.joshua.parser.exceptions.UnbalancedParenthesesException;
import lipstone.joshua.parser.exceptions.UndefinedOperationException;
import lipstone.joshua.parser.exceptions.UndefinedResultException;
import lipstone.joshua.parser.exceptions.UntypedPluginException;
import lipstone.joshua.parser.plugin.ParserPlugin;
import lipstone.joshua.parser.plugin.PluginController;
import lipstone.joshua.parser.plugin.helpdata.Command;
import lipstone.joshua.parser.plugin.helpdata.Keyword;
import lipstone.joshua.parser.plugin.helpdata.Operation;
import lipstone.joshua.parser.plugin.types.CommandPlugin;
import lipstone.joshua.parser.plugin.types.InputFilterPlugin;
import lipstone.joshua.parser.plugin.types.KeywordPlugin;
import lipstone.joshua.parser.plugin.types.OperationPlugin;
import lipstone.joshua.parser.plugin.types.OutputFilterPlugin;
import lipstone.joshua.parser.plugin.types.SettingsPlugin;
import lipstone.joshua.parser.types.BigDec;
import lipstone.joshua.parser.types.Matrix;
import lipstone.joshua.parser.util.AngleType;
import lipstone.joshua.parser.util.ConsCell;
import lipstone.joshua.parser.util.ConsType;
import lipstone.joshua.parser.util.Equation;
import lipstone.joshua.parser.util.History;
import lipstone.joshua.parser.util.LengthComparison;
import pluginLibrary.PluginUser;
/**
* This is meant to be a library for solving equations. Add this class to your project in the form Parser parser = new
* Parser(); Call parser.parse(String equations), which will return the answer as a *String*.
*
* @author Joshua Lipstone
*/
public final class Parser extends PluginUser<ParserPlugin> {
public static final String operators = "%+-*/^";
/**
* To use WORD_BREAK, be sure to append <code>m.group().length() - 1</code> where m is a Matcher to get the index of the
* break. This does not count String boundary matches.
*/
public static final String WORD_BREAK = "(\\n|[\\Q" + operators + "E\\E]|[a-zA-Z]\\d|\\d[a-zA-Z]|\\(|\\)| )";
/**
* The various fields and actions that PropertyListeners can be added to
*/
public static enum Property {
PluginLoaded, PluginUnloaded, KeywordLoaded, KeywordUnloaded, OperationLoaded, OperationUnloaded, CommandLoaded, CommandUnloaded,
AngleTypeChanged, AngleTypeAdded, AngleTypeRemoved, DefaultLogBase, SaveHistory, DataPersistence
};
/**
* Fires when the {@link lipstone.joshua.parser.util.AngleType AngleType} that this <tt>Parser</tt> uses is changed
*/
public static final Property AngleTypeChanged = Property.AngleTypeChanged;
/**
* Fires when an {@link lipstone.joshua.parser.util.AngleType AngleType} is added to this <tt>Parser</tt></br> The new
* {@link lipstone.joshua.parser.util.AngleType AngleType} is in the newValue parameter and the
* {@link lipstone.joshua.parser.util.AngleType AngleType} that it overrode, if it did override one is in the oldValue
* parameter, otherwise it is null.
*/
public static final Property AngleTypeAdded = Property.AngleTypeAdded;
/**
* Fires when an {@link lipstone.joshua.parser.util.AngleType AngleType} is removed from this <tt>Parser</tt></br> The
* old {@link lipstone.joshua.parser.util.AngleType AngleType} is in the oldValue parameter if it existed, otherwise both
* parameters are null
*/
public static final Property AngleTypeRemoved = Property.AngleTypeRemoved;
/**
* Fires when the log base that this <tt>Parser</tt> defaults to is changed
*/
public static final Property DefaultLogBase = Property.DefaultLogBase;
/**
* Fires when the {@link #saveHistory} flag is toggled
*/
public static final Property SaveHistory = Property.SaveHistory;
/**
* Fires when the {@link #dataPersistence} flag is toggled
*/
public static final Property DataPersistence = Property.DataPersistence;
/**
* The loaded plugin is passed in the newValue parameter.
*/
public static final Property PluginLoaded = Property.PluginLoaded;
/**
* The unloaded plugin is passed in the oldValue parameter.
*/
public static final Property PluginUnloaded = Property.PluginUnloaded;
/**
* Fires when a <tt>Keyword</tt> is loaded into this <tt>Parser</tt>. The <tt>Keyword</tt> is passed in the newValue
* parameter.
*/
public static final Property KeywordLoaded = Property.KeywordLoaded;
/**
* Fires when a <tt>Keyword</tt> is unloaded from this <tt>Parser</tt>. The <tt>Keyword</tt> is passed in the oldValue
* parameter.
*/
public static final Property KeywordUnloaded = Property.KeywordUnloaded;
/**
* Fires when a <tt>Operation</tt> is loaded into this <tt>Parser</tt>. The <tt>Operation</tt> is passed in the newValue
* parameter.
*/
public static final Property OperationLoaded = Property.OperationLoaded;
/**
* Fires when a <tt>Operation</tt> is unloaded from this <tt>Parser</tt>. The <tt>Operation</tt> is passed in the
* oldValue parameter.
*/
public static final Property OperationUnloaded = Property.OperationUnloaded;
/**
* Fires when a <tt>Command</tt> is loaded into this <tt>Parser</tt>. The <tt>Command</tt> is passed in the newValue
* parameter.
*/
public static final Property CommandLoaded = Property.CommandLoaded;
/**
* Fires when a <tt>Command</tt> is unloaded from this <tt>Parser</tt>. The <tt>Command</tt> is passed in the oldValue
* parameter.
*/
public static final Property CommandUnloaded = Property.CommandUnloaded;
/**
* Indicates that the angle measurement system in use is degrees
*/
public static final AngleType Degrees = new AngleType("Degrees") {
private final BigDec factor180 = new BigDec(180);
/**
* @param angle
* the angle to convert in degrees
*/
@Override
public BigDec toRadians(BigDec angle) throws UndefinedResultException {
return angle.multiply(BigDec.PI).divide(factor180);
}
/**
* @return the given angle in degrees
* @throws UndefinedResultException
*/
@Override
public BigDec fromRadians(BigDec angle) throws UndefinedResultException {
return angle.multiply(factor180).divide(BigDec.PI);
}
};
/**
* Indicates that the angle measurement system in use is radians
*/
public static final AngleType Radians = new AngleType("Radians") {
/**
* @param angle
* the angle to convert in radians
*/
@Override
public BigDec toRadians(BigDec angle) throws UndefinedResultException {
return angle;
}
/**
* @return the given angle in radians
* @throws UndefinedResultException
*/
@Override
public BigDec fromRadians(BigDec angle) throws UndefinedResultException {
return angle;
}
};
/**
* Indicates that the angle measurement system in use is grads
*/
public static final AngleType Grads = new AngleType("Grads") {
private final BigDec factor200 = new BigDec(200);
/**
* @param angle
* the angle to convert in grads
*/
@Override
public BigDec toRadians(BigDec angle) throws UndefinedResultException {
return angle.multiply(BigDec.PI).divide(factor200);
}
/**
* @return the given angle in grads
* @throws UndefinedResultException
*/
@Override
public BigDec fromRadians(BigDec angle) throws UndefinedResultException {
return angle.multiply(factor200).divide(BigDec.PI);
}
};
private String command = "";
private String initial;
private ParserPlugin lastPlugin;
private ArrayList<ParserPlugin> plugins;
private TreeMap<String, Operation> operations;
private ArrayList<String> finalOperations; //Final operations are operations that should not be parsed further.
private TreeMap<String, Command> commands; //All commands are final processes, and should not be parsed further.
private ArrayList<InputFilterPlugin> preProcessFilters;
private ArrayList<OutputFilterPlugin> postProcessFilters;
private ArrayList<SettingsPlugin> settingsPlugins;
private TreeMap<String, Keyword> keywords;
private HashMap<String, String> abbreviations;
private SortedList<String> allNames; //A sorted registry (via LengthComparison) of all named operations, keywords, commands, etc. in this Parser
//private int bits = 32, base = 10, IEEE754exp = 8, IEEE754sig = 23;
private MathContext precision = MathContext.DECIMAL64;
public boolean graphExists = false, useConstants = false;
private String outputType = "normal", defaultLogBase = "10";
private Path baseLocation;
/**
* The system of angle measure currently in use by this <tt>Parser</tt>
*/
private AngleType angleType = Degrees;
private HashMap<String, AngleType> angleTypes;
private ArrayList<String> vars = new ArrayList<String>();
private ArrayList<ConsCell> finalSubstitutions = new ArrayList<ConsCell>();
private PropertyChangeSupport propertyChangeSupport;
/**
* A queue of commands to be processed after the current input is processed
*/
private Stack<ConsCell> commandQueue = new Stack<ConsCell>();
private Equation currentEqn = new Equation();
private final History history;
private final CAS ns;
private final Log log;
private boolean isProcessing = false;
/**
* This defaults to true. Set it to false to disable saving history across runs.
*/
private boolean saveHistory = true;
/**
* This defaults to true. Set it to false to disable saving any data (history, log, and plugin data) across runs.
*/
private boolean dataPersistence = true;
/**
* Use this for all threading operations.
*/
private static final ForkJoinPool fjPool = new ForkJoinPool();
/**
* Constructs a new <tt>Parser</tt> object with an automatically determined baseLocation, and initializes the File IO
* systems
*/
public Parser() {
this(null, FileSystems.getDefault().getPath(getPath()).getParent());
}
private static final String getPath() {
try {
String path = Parser.class.getProtectionDomain().getCodeSource().getLocation().getPath();
String decodedPath = URLDecoder.decode(path, "UTF-8");
return decodedPath;
}
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "";
}
/**
* Constructs a new <tt>Parser</tt> object with the specified base location, and initializes the File IO systems
*
* @param baseLocation
* the complete file path to the location containing the data and plugins directories
*/
public Parser(String baseLocation) {
this(null, FileSystems.getDefault().getPath(baseLocation));
}
/**
* Constructs a new <tt>Parser</tt> object with the specified base location, and initializes the File IO systems
*
* @param baseLocation
* the complete file path to the location containing the data and plugins directories
*/
public Parser(Path baseLocation) {
this(null, baseLocation);
}
/**
* Clones the plugins and basic configuration fields in another <tt>Parser</tt> into this <tt>Parser</tt>
*
* @param parser
* the <tt>Parser</tt> to clone from
* @param baseLocation
* the complete file path to the new base location for this <tt>Parser</tt>
*/
public Parser(Parser parser, String baseLocation) {
this(parser, FileSystems.getDefault().getPath(baseLocation));
}
/**
* Clones the plugins and basic configuration fields in another <tt>Parser</tt> into this <tt>Parser</tt>
*
* @param parser
* the <tt>Parser</tt> to clone from
* @param baseLocation
* the complete file path to the new base location for this <tt>Parser</tt>
*/
public Parser(Parser parser, Path baseLocation) {
initializePluginMaps();
propertyChangeSupport = new PropertyChangeSupport(this);
this.baseLocation = baseLocation;
log = new Log();
File checkFile = baseLocation.toFile();
if (!checkFile.exists())
checkFile.mkdirs();
try {
log.open(baseLocation.toString() + PluginController.PATH_SEPARATOR + "log.txt");
log.clearLog();
}
catch (IOException e) {}
command = "";
history = new History(baseLocation);
File pluginsFolder = baseLocation.getFileSystem().getPath(baseLocation.toString(), "plugins/").toFile();
if (!pluginsFolder.exists())
pluginsFolder.mkdirs();
File dataFolder = baseLocation.getFileSystem().getPath(baseLocation.toString(), "data/").toFile();
if (!dataFolder.exists())
dataFolder.mkdirs();
ns = new CAS(this);
linkFields();
if (parser != null) {
saveHistory = parser.saveHistory;
dataPersistence = parser.dataPersistence;
angleType = parser.angleType;
outputType = parser.outputType;
defaultLogBase = parser.defaultLogBase;
vars = new ArrayList<String>(parser.vars);
for (ParserPlugin plugin : parser.plugins)
try {
loadPlugin(plugin);
}
catch (PluginConflictException | UntypedPluginException e) {
e.printStackTrace();
}
angleTypes = new HashMap<String, AngleType>(parser.angleTypes);
}
else
initializeAngleTypes();
}
private final void linkFields() {
this.addPropertyListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(boolean) evt.getNewValue())
setSaveHistory(false);
}
}, DataPersistence);
this.addPropertyListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ((boolean) evt.getNewValue())
setDataPersistence(true);
}
}, SaveHistory);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (!saveHistory)
new File(history.getFileLocation()).delete();
if (!dataPersistence) {
File erase = baseLocation.getFileSystem().getPath(baseLocation.toString(), "data/").toFile();
if (erase.exists())
eraseDirectory(erase);
if ((erase = baseLocation.getFileSystem().getPath(baseLocation.toString(), "configuration/").toFile()).exists())
eraseDirectory(erase);
try {
log.close();
if ((erase = baseLocation.getFileSystem().getPath(baseLocation.toString(), "log.txt").toFile()).exists())
erase.delete();
}
catch (IOException e) {}
}
}
});
}
/**
* Completely erases the given directory from the computer. (Deletes all subfolders and files)
*
* @param directory
* the directory to erase
*/
private final void eraseDirectory(File directory) {
for (File file : directory.listFiles()) {
if (file.isDirectory())
eraseDirectory(file);
file.delete();
}
directory.delete();
}
private final void initializeAngleTypes() {
angleTypes = new HashMap<>();
angleTypes.put(Degrees.getName(), Degrees);
angleTypes.put(Radians.getName(), Radians);
angleTypes.put(Grads.getName(), Grads);
}
/**
* Use this to initialize or reset the plugin maps
*/
private void initializePluginMaps() {
operations = new TreeMap<String, Operation>(new TreeMapSorter());
finalOperations = new ArrayList<String>();
preProcessFilters = new ArrayList<InputFilterPlugin>();
postProcessFilters = new ArrayList<OutputFilterPlugin>();
settingsPlugins = new ArrayList<SettingsPlugin>();
keywords = new TreeMap<String, Keyword>(new TreeMapSorter());
commands = new TreeMap<String, Command>(new TreeMapSorter());
plugins = new ArrayList<ParserPlugin>();
allNames = new SortedList<String>(new LengthComparison());
abbreviations = new HashMap<String, String>();
}
/**
* Removes all loaded plugins from the Parser
*/
public void clearPlugins() throws PluginConflictException {
while (plugins.size() > 0)
unloadPlugin(plugins.get(0));
log.logInfo("Cleared all plugins.");
}
/**
* Checks if this <tt>Parser</tt> contains the specified plugin by ID
*
* @param plugin
* the plugin to check for
* @return whether this <tt>Parser</tt> contains the specified plugin in its plugin registry
*/
public boolean containsPlugin(ParserPlugin plugin) {
for (ParserPlugin pl : plugins)
if (pl.getID().equals(plugin.getID()))
return true;
return false;
}
/**
* Loads a ParserPlugin into the plugins registry. If the plugin passed implements one of the various interfaces also in
* the lipstone.joshua.parser.plugins package it adds the pointers into the appropriate registers.
*
* @param plugin
* the plugin to be loaded into this Parser
* @throws PluginConflictException
* if another plugin has registered operations, keywords, or commands that this plugin is trying to register
* @throws UntypedPluginException
* if this plugin does not implement at least one of the type interfaces
* @see lipstone.joshua.parser.plugin.types.OperationPlugin OperationPlugin
* @see lipstone.joshua.parser.plugin.types.KeywordPlugin KeywordPlugin
* @see lipstone.joshua.parser.plugin.types.CommandPlugin CommandPlugin
* @see lipstone.joshua.parser.plugin.types.InputFilterPlugin InputFilterPlugin
* @see lipstone.joshua.parser.plugin.types.OutputFilterPlugin OutputFilterPlugin
*/
@Override
public void loadPlugin(ParserPlugin plugin) throws PluginConflictException, UntypedPluginException {
if (!(plugin instanceof OperationPlugin || plugin instanceof KeywordPlugin || plugin instanceof InputFilterPlugin ||
plugin instanceof OutputFilterPlugin || plugin instanceof CommandPlugin || plugin instanceof SettingsPlugin))
throw new UntypedPluginException(plugin);
ParserPlugin temp = lastPlugin;
lastPlugin = plugin;
plugin.loadPlugin(this);
if (plugin instanceof InputFilterPlugin)
preProcessFilters.add((InputFilterPlugin) plugin);
if (plugin instanceof OutputFilterPlugin)
postProcessFilters.add((OutputFilterPlugin) plugin);
if (plugin instanceof SettingsPlugin)
settingsPlugins.add((SettingsPlugin) plugin);
if (!plugins.contains(plugin))
plugins.add(plugin);
log.logInfo("Loaded " + plugin.getID());
propertyChangeSupport.firePropertyChange("PluginLoaded", null, plugin);
lastPlugin = temp;
}
/**
* Unloads a ParserPlugin from the plugins registry
*
* @param plugin
* the plugin to be removed
* @throws PluginConflictException
* if the plugin tries to remove operations, keywords, or commands that another plugin registered
*/
@Override
public void unloadPlugin(ParserPlugin plugin) throws PluginConflictException {
ParserPlugin temp = lastPlugin;
lastPlugin = plugin;
if (plugin instanceof InputFilterPlugin)
preProcessFilters.remove(plugin);
if (plugin instanceof OutputFilterPlugin)
postProcessFilters.remove(plugin);
if (plugin instanceof SettingsPlugin)
settingsPlugins.remove(plugin);
if (plugins.contains(plugin))
plugins.remove(plugin);
plugin.unloadPlugin(this);
log.logInfo("Unloaded " + plugin.getID());
propertyChangeSupport.firePropertyChange("PluginUnloaded", plugin, null);
lastPlugin = temp;
}
/**
* Reloads all plugin data from the plugin registry
*
* @throws PluginConflictException
*/
public void refreshPlugins() throws PluginConflictException {
ArrayList<ParserPlugin> plugins = new ArrayList<ParserPlugin>(this.plugins);
for (ParserPlugin plugin : plugins)
unloadPlugin(plugin);
initializePluginMaps();
boolean success = true;
for (ParserPlugin plugin : plugins)
try {
loadPlugin(plugin);
}
catch (UntypedPluginException e) {
log.logError("Failed to load " + plugin.getID() + " Reason: UntypedPluginException");
success = false;
}
if (success)
log.logInfo("Successfully refreshed the plugins.");
else
log.logError("Failed to refresh the plugins.");
}
/**
* Adds the specified operation to the operations HashMap<String, Plugin>. A plugin cannot override an already-loaded
* plugin's operation
*
* @param operation
* the operation to add in object form
* @param plugin
* the plugin that this operation should be mapped to
* @throws PluginConflictException
* when the operation to be loaded already exists and does not belong to the plugin being mapped
*/
public synchronized void addOperation(Operation operation, ParserPlugin plugin) throws PluginConflictException {
if (!(plugin instanceof OperationPlugin))
throw new PluginConflictException("A plugin without operation support attempted to load an operation", plugin);
if (!plugin.getID().equals(operation.getPlugin().getID()))
throw new PluginConflictException("A plugin attempted to load an operation under a different ID", plugin);
if (!operations.containsKey(operation.getName())) {
operations.put(operation.getName(), operation);
if (operation.isFinal()) {
finalOperations.add(operation.getName());
log.logInfo("Added an operation, " + operation.getName() + ", to " + plugin.getID() + ", as a Final Operation");
}
else
log.logInfo("Added an operation, " + operation.getName() + ", to " + plugin.getID());
}
else if (operations.containsKey(operation.getName()) && operations.get(operation.getName()).getPlugin() != plugin)
throw new PluginConflictException("The operation: " + operation.getName() + " is already mapped to " + operations.get(operation.getName()).getPlugin().getID(), plugin);
allNames.add(operation.getName());
for (String abbreviation : operation.getAlternateNames())
abbreviations.put(abbreviation, operation.getName());
propertyChangeSupport.firePropertyChange("Operation", operation, null);
}
/**
* Removes a operation from the operations map. Throws a PluginConflictException when the operation is not owned by the
* plugin trying to remove it.
*
* @param operation
* the operation to be removed
* @param plugin
* the plugin removing the operation
* @throws PluginConflictException
*/
public synchronized void removeOperation(String operation, ParserPlugin plugin) throws PluginConflictException {
if (operations.containsKey(operation) && !plugin.getID().equals(operations.get(operation).getPlugin().getID()))
throw new PluginConflictException("The operation: " + operation + " is mapped to " + operations.get(operation).getPlugin().getID(), plugin);
operations.remove(operation);
finalOperations.remove(operation);
allNames.remove(operation);
abbreviations.values().remove(operation);
log.logInfo("Removed an operation, " + operation + ", from " + plugin.getID());
propertyChangeSupport.firePropertyChange("Operation", null, operation);
}
/**
* Adds the specified keyword to the keywords HashMap<String, Plugin>. A plugin cannot override a keyword that has
* already been loaded plugin's keyword, it will instead throw a PluginConflictException.
*
* @param keyword
* the keyword to add in object form
* @param plugin
* the plugin that this keyword should be mapped to
* @throws PluginConflictException
* when the keyword to be loaded already exists and does not belong to the plugin being mapped
*/
public synchronized void addKeyword(Keyword keyword, ParserPlugin plugin) throws PluginConflictException {
if (!(plugin instanceof KeywordPlugin))
throw new PluginConflictException("A plugin without keyword support attempted to load an keyword", plugin);
if (!plugin.getID().equals(keyword.getPlugin().getID()))
throw new PluginConflictException("A plugin attempted to load an keyword under a different ID", plugin);
if (keywords.containsKey(keyword.getName()) && keywords.get(keyword.getName()).getPlugin() != plugin)
throw new PluginConflictException("The keyword: " + keyword.getName() + " is already mapped to " + keywords.get(keyword.getName()).getPlugin().getID(), plugin);
keywords.put(keyword.getName(), keyword);
allNames.add(keyword.getName());
for (String abbreviation : keyword.getAlternateNames())
abbreviations.put(abbreviation, keyword.getName());
log.logInfo("Added a keyword, " + keyword.getName() + ", to " + plugin.getID());
propertyChangeSupport.firePropertyChange("Keyword", null, keyword);
}
/**
* Removes a keyword from the keywords map. Throws a PluginConflictException when the keyword is not owned by the plugin
* trying to remove it.
*
* @param keyword
* the keyword to be removed
* @param plugin
* the plugin removing the keyword
* @throws PluginConflictException
*/
public synchronized void removeKeyword(String keyword, ParserPlugin plugin) throws PluginConflictException {
if (keywords.containsKey(keyword) && !keywords.get(keyword).getPlugin().getID().equals(plugin.getID()))
throw new PluginConflictException("The keyword: " + keyword + " is mapped to " + keywords.get(keyword).getPlugin().getID(), plugin);
keywords.remove(keyword);
allNames.remove(keyword);
abbreviations.values().remove(keyword);
log.logInfo("Removed a keyword, " + keyword + ", from " + plugin.getID());
propertyChangeSupport.firePropertyChange("Keyword", keyword, null);
}
/**
* Adds the specified command to the commands HashMap<String, Plugin>. A plugin cannot override a command that has
* already been loaded plugin's command, it will instead throw a PluginConflictException.
*
* @param command
* the command to add in object form
* @param plugin
* the plugin that this command should be mapped to
* @throws PluginConflictException
* when the command to be loaded already exists and does not belong to the plugin being mapped
*/
public synchronized void addCommand(Command command, ParserPlugin plugin) throws PluginConflictException {
if (!(plugin instanceof CommandPlugin))
throw new PluginConflictException("A plugin without command support attempted to load an command", plugin);
if (!plugin.getID().equals(command.getPlugin().getID()))
throw new PluginConflictException("A plugin attempted to load an command under a different ID", plugin);
if (commands.containsKey(command.getName()) && commands.get(command.getName()).getPlugin() != plugin)
throw new PluginConflictException("The command: " + command.getName() + " is already mapped to " + commands.get(command.getName()).getPlugin().getID(), plugin);
commands.put(command.getName(), command);
allNames.add(command.getName());
for (String abbreviation : command.getAlternateNames())
abbreviations.put(abbreviation, command.getName());
log.logInfo("Added a command, " + command.getName() + ", to " + plugin.getID());
propertyChangeSupport.firePropertyChange("Command", null, command);
}
/**
* Removes a command from the commands map. Throws a PluginConflictException when the command is not owned by the plugin
* trying to remove it.
*
* @param command
* the command to be removed
* @param plugin
* the plugin removing the command
* @throws PluginConflictException
*/
public synchronized void removeCommand(String command, ParserPlugin plugin) throws PluginConflictException {
if (commands.containsKey(command) && !commands.get(command).getPlugin().getID().equals(plugin.getID()))
throw new PluginConflictException("The command: " + command + " is mapped to " + commands.get(command).getPlugin().getID(), plugin);
commands.remove(command);
allNames.remove(command);
abbreviations.values().remove(command);
log.logInfo("Removed a command, " + command + ", from " + plugin.getID());
propertyChangeSupport.firePropertyChange("Command", command, null);
}
/**
* This is a convenience method for {@link #parse(String equation)}, primarily for more advanced base input
*
* @param equation
* the equation to be parsed, as an Equation object.
* @return the output from the parsed Equation. If there is an error, the String returned is a description of the error
* @see #parse(String equation)
*/
public String parse(Equation equation) {
currentEqn = equation;
return parse(equation);
}
/**
* The recommended starting method for all of the math capabilities of this program. - THIS IS FOR UI USE ONLY
*
* @param equation
* the equation to be parsed, as a String
* @return the output from the parsed String. If there is an error, the String returned is a description of the error
*/
public String parse(String equation) {
String output = "";
if (equation.length() < 1)
return "0";
try {
command = equation;
output = innerParse(command);
log.logInfo("Successfully parsed " + command + " -> " + output, false);
}
catch (ParserException pe) {
output = "Error: " + pe.getMessage();
if (pe.getThrower() != null) {
if (output.charAt(output.length() - 1) == '.')
output = output.substring(0, output.length() - 1);
output = output + ", in plugin: " + pe.getThrower().getID();
}
else if (lastPlugin != null) {
if (output.charAt(output.length() - 1) == '.')
output = output.substring(0, output.length() - 1);
output = output + ", last-called plugin (not nessesarily the cause of the error): " + lastPlugin.getID();
}
log.logError(output);
currentEqn.hasError = true;
}
finally {
isProcessing = false;
currentEqn.eqn = initial;
currentEqn.answer = output;
history.appendEquation(new Equation(currentEqn));
if (saveHistory)
history.writeToXML();
currentEqn = new Equation();
command = "";
}
return output;
}
/**
* This is allows for threading on the parse command instead of having complicated threading crap within each plugin
*
* @param input
* the equation passed to parse
* @return the result of that equation, or "" if there is an error
* @throws ParserException
* thrown when an error happens.
*/
private String innerParse(String input) throws ParserException {
try {
isProcessing = true;
commandQueue.push(new ConsCell("END_OF_STACK", ConsType.IDENTIFIER));
String output = "";
input = input.trim();
initial = new String(input);
ConsCell query = removeDoubles(Tokenizer.tokenizeString(input));
vars = getVariables(query);
if (output.equals(""))
output = runCommands(query).toString();
if (!output.equals(""))
return output;
if (output.equals("")) {
query = preProcess(query);
query = run(query);
processCommandQueue();
}
this.command = input;
output = postProcess(query);
return output;
}
finally {
isProcessing = false;
}
}
public ConsCell runCommands(ConsCell input) throws ParserException {
ArrayList<ConsCell> outputs = new ArrayList<ConsCell>();
ConsCell current = input;
do {
if (current.getCarType() == ConsType.IDENTIFIER && commands.containsKey(current.getCar())) {
ConsCell arguments = new ConsCell();
String command = (String) current.getCar();
while (!((current = current.remove()).isNull()) && !(current.getCarType() == ConsType.IDENTIFIER && commands.containsKey(current.getCar())))
arguments = arguments.append(current.singular());
if (commands.containsKey(current.getCar()))
current = current.getPreviousConsCell();
outputs.add(((CommandPlugin) commands.get(command).getPlugin()).runCommand(command, arguments.getFirstConsCell().splitOnSeparator()));
}
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
ConsCell output = new ConsCell();
ConsCell head = output;
for (ConsCell out : outputs)
head = head.append(out).getLastConsCell().append(new ConsCell("\n", ConsType.SEPARATOR));
head.remove();
return output;
}
/**
* Primary processing method for this program. THIS RETURNS DATA FOR PLUGIN USE ONLY - use parse(String) or
* parse(Equation) for UI calls
*
* @param equation
* the input that is to be processed as a String
* @return the result of the parsed equation as a String
*/
public ConsCell run(String equation) throws ParserException {
equation = equation.trim();
ConsCell input = Tokenizer.tokenizeString(equation);
if (!isValid(input)) {
if (equation.equals("+"))
return new ConsCell(BigDec.ONE, ConsType.NUMBER);
if (equation.equals("-"))
return new ConsCell(BigDec.MINUSONE, ConsType.NUMBER);
throw new ParserException("Invalid Input", null);
}
// split based on '=' here
ArrayList<String> initVars = new ArrayList<String>(vars);
if (!equation.contains("="))
input = getValue(input);
else
input = ns.solve(input);
vars = initVars;
return input;
}
public ConsCell run(ConsCell input) throws ParserException {
if (!isValid(input)) {
if (input.toString().equals("+"))
return new ConsCell(BigDec.ONE, ConsType.NUMBER);
if (input.toString().equals("-"))
return new ConsCell(BigDec.MINUSONE, ConsType.NUMBER);
//throw new ParserException("Invalid Input", null);
}
input = input.clone();
ArrayList<String> initVars = new ArrayList<String>(vars);
if (!input.containsIdentifier("="))
input = getValue(input);
else
input = ns.solve(input);
vars = initVars;
return input;
}
/**
* Reverses the substitutions that keep the finalOperations from being further processed by
* {@link #processEquation(ConsCell)}.</br><i>This should ONLY be used at the end of processing.</i>
*
* @param output
* the result of parsing the input.
* @return the output with all finalOperation substitutions reversed.
*/
public ConsCell reverseFinalOperationSubstitutions(ConsCell output) {
if (finalSubstitutions.size() == 0)
return output;
ConsCell current = output;
do {
if (current.getCarType() == ConsType.OBJECT && ((String) current.getCar()).startsWith("{FIN")) {
int finalSub = Integer.parseInt(((String) current.getCar()).substring(4, ((String) current.getCar()).length() - 1));
current.replaceCar(finalSubstitutions.get(finalSub));
}
} while (!((current = current.getNextConsCell()).isNull())); //This steps output forward while checking for nulls
return output;
}
private String postProcess(ConsCell output) throws ParserException {
String result = output.toString();
if (result.contains("NaN"))
throw new UndefinedResultException("The result is not a number.", null);
// Handles the matrix replacement for display purposes
output = reverseFinalOperationSubstitutions(output);
ConsCell current = output;
do {
if (current.getCarType() == ConsType.OBJECT && ((String) current.getCar()).startsWith("{M")) {
int matrixNum = Integer.parseInt(((String) current.getCar()).substring(2, ((String) current.getCar()).length() - 1));
current.replaceCar(new ConsCell(currentEqn.matrices.get(matrixNum).toString(), ConsType.OBJECT));
}
} while (!((current = current.getNextConsCell()).isNull())); //This steps output forward while checking for nulls
finalSubstitutions.clear();
output = ns.removeExcessParentheses(output);
for (OutputFilterPlugin plugin : postProcessFilters)
output = plugin.postProcess(output);
result = output.toString();
// Attempts to eliminate trailing zeros from inexact numbers due to
// binary representation issues
Matcher m = Pattern.compile("000000[1-9]").matcher(result);
int end = result.length();
while (m.find()) {
if (m.end() == end) {
result = result.substring(0, m.start()) + result.substring(m.end());
end -= 7;
String number = result.substring(0, end);
for (int i = number.length() - 1; i > 0; i--)
if (number.charAt(i) == '0' && number.charAt(i - 1) == '0')
number = number.substring(0, i) + number.substring(i + 1);
else {
break;
}
result = number + result.substring(end);
break;
}
}
// Just some minor formatting checks
if (result.length() >= 4 && result.substring(0, 4).equalsIgnoreCase("0.0+"))
result = result.substring(4);
currentEqn = new Equation();
return result;
}
/**
* Convenience method; returns {@link #fromDelimiterList(String, char)}
*
* @param input
* the comma-separated list
* @return the ArrayList<String> form of the list
*/
public static ArrayList<String> fromCommaList(String input) {
return fromDelimiterList(input, ',');
}
/**
* Faster delimiter method for static delimiters; returns equivalent to {@link #fromDelimiterList(String, String)
* fromDelimiterList(input, "[\\Q" + char + "\\E]")} Convenience method - forwards to
* {@link #fromDelimiterList(String, char, boolean) fromDelimiterList(input, delimiter, true)}
*
* @param input
* the delimiter-separated list
* @param delimiter
* the delimiter to be used
* @return the ArrayList<String> form of the list
*/
public static ArrayList<String> fromDelimiterList(String input, char delimiter) {
return fromDelimiterList(input, ',', true);
}
/**
* Faster delimiter method for static delimiters; returns equivalent to {@link #fromDelimiterList(String, String)
* fromDelimiterList(input, "[\\Q" + char + "\\E]")}
*
* @param input
* the delimiter-separated list
* @param delimiter
* the delimiter to be used
* @param skipOverParentheses
* set to true in order to ignore delimiters enclosed within parentheses
* @return the ArrayList<String> form of the list
*/
public static ArrayList<String> fromDelimiterList(String input, char delimiter, boolean skipOverParentheses) {
ArrayList<String> output = new ArrayList<String>();
for (int i = 0; i < input.length(); i++) {
if (skipOverParentheses && input.charAt(i) == '(') {
try {
i = getEndIndex(input, i);
}
catch (UnbalancedParenthesesException e) {}
}
else if (input.charAt(i) == delimiter) {
output.add(input.substring(0, i));
input = input.substring(i + 1).trim();
i = -1;
}
}
if (input.length() > 0)
output.add(input);
for (int i = 0; i < output.size(); i++) {
if (output.get(i).trim().equals("")) {
output.remove(i);
i--;
}
}
return output;
}
/**
* A bit slower than {@link #fromDelimiterList(String input, char delimiter)}, but allows for a regex delimiter.
* Convenience method - forwards to {@link #fromDelimiterList(String, String, boolean) fromDelimiterList(input, regex,
* true)}
*
* @param input
* the delimiter-separated list
* @param regex
* the delimiter to be used
* @return the ArrayList<String> form of the list
*/
public static ArrayList<String> fromDelimiterList(String input, String regex) {
return fromDelimiterList(input, regex, true);
}
/**
* A bit slower than {@link #fromDelimiterList(String input, char delimiter)}, but allows for a regex delimiter.
*
* @param input
* the list
* @param regex
* the delimiter
* @param skipOverParentheses
* set to true in order to ignore delimiters enclosed within parentheses
* @return the ArrayList<String> form of the list
*/
public static ArrayList<String> fromDelimiterList(String input, String regex, boolean skipOverParentheses) {
ArrayList<String> output = new ArrayList<String>();
Matcher m = Pattern.compile(regex).matcher(input);
HashMap<Integer, Integer> hits = new HashMap<Integer, Integer>();
int last = 0;
while (m.find()) {
hits.put(m.start(), m.end());
}
for (Integer i = 0; i < input.length(); i++) {
if (skipOverParentheses && input.charAt(i) == '(') {
try {
i = getEndIndex(input, i);
}
catch (UnbalancedParenthesesException e) {}
}
else if (hits.containsKey(i)) {
output.add(input.substring(last, i));
i = hits.get(i);
last = i;
}
}
if (input.length() > 0)
output.add(input.substring(last));
for (int i = 0; i < output.size(); i++) {
if (output.get(i).trim().equals("")) {
output.remove(i);
i--;
}
}
return output;
}
/**
* Replaces all instances of a symbol pair with a new symbol pair. This should be useful in filters that convert
* mathematical shorthand to what this parser can process
*
* @param input
* the <tt>ConsCell</tt> in which these replacements are to be performed
* @param startSymbol
* the original starting symbol
* @param endSymbol
* the original ending symbol
* @param newStart
* the new starting symbol
* @param newEnd
* the new ending symbol
* @return the input with the symbols replaced
* @throws ParserException
*/
public ConsCell replaceSymbolPair(ConsCell input, String startSymbol, String endSymbol, String newStart, String newEnd) throws ParserException {
ConsCell current = input;
do {
if (current.getCarType() == ConsType.CONS_CELL)
current.replaceCar(replaceSymbolPair((ConsCell) current.getCar(), startSymbol, endSymbol, newStart, newEnd));
if (current.getCarType() == ConsType.IDENTIFIER && ((String) current.getCar()).equals(startSymbol)) {
ConsCell inner = new ConsCell(), head = inner;
int count = 1;
boolean reset = current == input;
while (!(current = current.remove()).isNull()) {
if (current.getCarType() == ConsType.IDENTIFIER) {
if (((String) current.getCar()).equals(endSymbol))
count--;
else if (((String) current.getCar()).equals(startSymbol))
count++;
}
if (count == 0)
break;
head = head.append(current.singular());
}
if (newStart.endsWith("(") && newEnd.startsWith(")")) { //So this is basically an operator replacement
current.replaceCar(new ConsCell(newStart.substring(0, newStart.length() - 1), ConsType.IDENTIFIER));
if (newEnd.length() > 1)
inner.getLastConsCell().append(new ConsCell(newEnd.substring(1), ConsType.IDENTIFIER));
current.insert(new ConsCell(inner, ConsType.CONS_CELL));
}
else {
inner = new ConsCell(newStart, ConsType.IDENTIFIER, inner, ConsType.CONS_CELL);
inner.getLastConsCell().append(new ConsCell(newEnd, ConsType.IDENTIFIER));
}
if (reset)
input = current;
}
} while (!(current = current.getNextConsCell()).isNull());
return input;
}
/**
* Performs various tasks that make it easier for the <tt>Parser</tt> to handle the input. Also, this is where the
* {@link lipstone.joshua.parser.plugin.types.InputFilterPlugin InputFilterPlugins} are applied.
*
* @param input
* the user's query converted into ConsCell format
* @return the query with the relevant input filters applied
* @throws ParserException
*/
public ConsCell preProcess(ConsCell input) throws ParserException {
ConsCell current = input;
if (current.length() == 0 || (current.length() == 1 && current.getCarType() == ConsType.EMPTY))
return current;
do {
if (current.getCarType() == ConsType.CONS_CELL)
current.replaceCar(preProcess((ConsCell) current.getCar()));
if (current.getCarType() == ConsType.OBJECT && ((String) current.getCar()).startsWith("{[")) {
currentEqn.matrices.add(new Matrix((String) current.getCar()));
current.replaceCar(new ConsCell("{M" + (currentEqn.matrices.size() - 1) + "}", ConsType.OBJECT));
}
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
current = input;
for (InputFilterPlugin plugin : preProcessFilters)
current = plugin.preProcess(current).getFirstConsCell();
input = current;
do {
if (current.getCarType() == ConsType.IDENTIFIER && abbreviations.containsKey(current.getCar()))
current.replaceCar(new ConsCell(abbreviations.get(current.getCar()), ConsType.IDENTIFIER));
if (!current.getNextConsCell().isNull() && (current.getCarType() == ConsType.NUMBER || current.getCarType() == ConsType.CONS_CELL ||
current.getCarType() == ConsType.OBJECT || current.getCarType() == ConsType.STRING)) {
ConsCell cdr = current.getNextConsCell();
if (cdr.getCarType() == ConsType.NUMBER || cdr.getCarType() == ConsType.CONS_CELL || cdr.getCarType() == ConsType.OBJECT ||
(cdr.getCarType() == ConsType.IDENTIFIER && !cdr.getCar().equals("="))) //If there is an implied multiplication sign
current.insert(new ConsCell('*', ConsType.OPERATOR));
if (cdr.getCarType() == ConsType.STRING) //String concatenation
current.insert(new ConsCell('+', ConsType.OPERATOR));
}
} while (!(current = current.getNextConsCell()).isNull()); //This steps current forward while checking for nulls
return input; //Returns the first ConsCell in the list because we don't care about which one was last processed, just the whole thing.
}
/**
* Splits the <tt>String</tt> on word breaks, defined by the regex pattern:
* <p>
* <code>"(\\n|[\\Q" + operators + "\\E]|[a-zA-Z]\\d|\\d[a-zA-Z]|\\(|\\)| )"</code>
* </p>
*
* @param input
* the <tt>String</tt> to split
* @return the words as defined by the word break pattern within the input <tt>String</tt>
*/
public ArrayList<String> splitOnWordBreak(String input) {
ArrayList<String> output = new ArrayList<String>();
ArrayList<Integer> breaks = new ArrayList<Integer>();
Matcher m = Pattern.compile(WORD_BREAK).matcher(input);
int step = 0;
while (m.find(step)) {
breaks.add(new Integer(m.start() + m.group().length() - 1));
step = breaks.get(breaks.size() - 1);
if (m.group().length() == 1)
step++;
}
if (breaks.size() == 0 || breaks.get(0) != 0) //Parentheses or curly-brackets would cause breaks to already have a zero.
breaks.add(0, 0);
for (int i = 0; i < breaks.size(); i++)
output.add(input.substring(breaks.remove(0), breaks.get(0)));
return output;
}
private void processCommandQueue() throws ParserException {
ConsCell command = new ConsCell();
ConsCell end = new ConsCell("END_OF_STACK", ConsType.IDENTIFIER);
while (!(command = commandQueue.pop()).equals(end))
runCommands(command);
}
private ConsCell getValue(ConsCell input) throws ParserException {
input = seekOperations(input);
input = tokenizedEvaluationEntry(input);
return removeDoubles(input);
}
private ConsCell tokenizedEvaluationEntry(ConsCell input) throws ParserException {
return tokenizedEvaluation(input);
}
private ConsCell tokenizedEvaluation(ConsCell input) throws ParserException {
ConsCell current = input;
do {
if (current.getCarType() == ConsType.IDENTIFIER) {
if (keywords.containsKey(current.getCar()))
current.replaceCar(Tokenizer.tokenizeString(((KeywordPlugin) this.keywords.get(current.getCar()).getPlugin()).getKeywordData((String) current.getCar())));
else if (isOperation((String) current.getCar()))
current.replaceCar(runOperation(current));
}
if (current.getCarType() == ConsType.CONS_CELL)
current.replaceCar(tokenizedEvaluation((ConsCell) current.getCar()));
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
return processEquation(input);
}
/**
* Performs named function operations
*
* @param input
* The ConsCell whose car is the name of the operation to be performed, and whose cdr's car is the argument or
* a pointer to the arguments for the operation
* @return the resulting value or null if the operation was invalid
* @throws ParserException
*/
public ConsCell runOperation(ConsCell input) throws ParserException {
return runOperation(input, true);
}
/**
* Performs named function operations
*
* @param input
* The ConsCell whose car is the name of the operation to be performed, and whose cdr's car is the argument or
* a pointer to the arguments for the operation
* @param fullEval
* recurse (if necessary) to {@link #tokenizedEvaluation(ConsCell)} if true, otherwise
* {@link #seekOperations(ConsCell)}.
* @return the resulting value or null if the operation was invalid
* @throws ParserException
*/
private ConsCell runOperation(ConsCell input, boolean fullEval) throws ParserException {
if (input.getCdrType() != ConsType.CONS_CELL || input.getNextConsCell().isNull())
throw new InvalidOperationException(lastPlugin, (String) input.getCar(), new ArrayList<ConsCell>());
if (!operations.containsKey(input.getCar()))
throw new InvalidOperationException(((String) input.getCar()) + " is not a valid operation.", lastPlugin, (String) input.getCar(),
(input.getNextConsCell().getCarType() == ConsType.CONS_CELL ? (ConsCell) input.getNextConsCell().singular().getCar() : input.getNextConsCell().singular()).splitOnSeparator());
lastPlugin = operations.get(input.getCar()).getPlugin();
ConsCell current = input.getNextConsCell(), number = new ConsCell();
boolean skipCheck = true;
do {
number = number.append(current.singular());
skipCheck = (current.getCarType() == ConsType.IDENTIFIER && operations.containsKey(current.getCar())) || (skipCheck && current.getCarType() == ConsType.OPERATOR);
} while (!(current = current.remove()).isNull() && (skipCheck || (current.getCarType() == ConsType.IDENTIFIER && operations.containsKey(current.getCar()))));
number = number.getFirstConsCell();
if (!operations.get(input.getCar()).isFinal())
number = fullEval ? tokenizedEvaluation(number) : seekOperations(number);
lastPlugin = operations.get(input.getCar()).getPlugin();
ConsCell result = ((OperationPlugin) operations.get(input.getCar()).getPlugin()).runOperation((String) input.getCar(), number.getCarType() == ConsType.CONS_CELL ? (ConsCell) number.getCar() : number);
if (operations.get(input.getCar()).isFinal()) {
finalSubstitutions.add(result);
return new ConsCell("{FIN" + (finalSubstitutions.size() - 1) + "}", ConsType.OBJECT);
}
return result;
}
public ConsCell seekOperations(ConsCell input) throws ParserException {
ConsCell current = input;
do {
if (current.getCarType() == ConsType.CONS_CELL)
current.replaceCar(seekOperations((ConsCell) current.getCar()));
if (current.getCarType() == ConsType.IDENTIFIER && operations.containsKey(current.getCar()))
current.replaceCar(runOperation(current, false));
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
return input;
}
private ConsCell processEquation(ConsCell input) throws ParserException {
input = input.clone();
input = removeDoubles(input);
if (containsVariables(input))
return ns.simplifyTerms(input);
if (input.length() < 3) {
if (input.getCarType() == ConsType.OPERATOR && input.getNextConsCell().getCarType() == ConsType.NUMBER) {
if ((Character) input.getCar() == '-')
input.getNextConsCell().replaceCar(new ConsCell(((BigDec) input.getNextConsCell().getCar()).multiply(BigDec.MINUSONE), ConsType.NUMBER));
if ((Character) input.getCar() == '/')
input.getNextConsCell().replaceCar(new ConsCell(BigDec.ONE.divide((BigDec) input.getNextConsCell().getCar()), ConsType.NUMBER));
input = input.remove();
}
return input;
}
if (input.getCarType() == ConsType.OPERATOR)
input = new ConsCell(BigDec.ZERO, ConsType.NUMBER, input, ConsType.CONS_CELL);
ConsCell current = input;
char steps[] = {'%', '%', '^', '^', '*', '/', '+', '-'};
for (int i = 0; i < steps.length - 1; i += 2) {
boolean forward = true;
do {
forward = true;
if (current.getNextConsCell(2).isNull() || current.getNextConsCell().isNull())
break;
ConsCell second = current.getNextConsCell(2);
if (current.getNextConsCell().getCarType() == ConsType.OPERATOR && (((Character) current.getNextConsCell().getCar()) == steps[i] || ((Character) current.getNextConsCell().getCar()) == steps[i + 1])) {
char operator = (Character) current.getNextConsCell().getCar();
boolean makeNegative = false;
if (second.getCarType() == ConsType.OPERATOR) {
if (((Character) second.getCar()) == '-')
makeNegative = true;
second.remove();
second = current.getNextConsCell(2);
}
if (current.getCarType() == ConsType.NUMBER && second.getCarType() == ConsType.NUMBER) {
BigDec num1 = (BigDec) current.getCar(), num2 = ((BigDec) second.getCar()).multiply(makeNegative ? BigDec.MINUSONE : BigDec.ONE), output = BigDec.ZERO;
second.remove(); //num2
current.getNextConsCell().remove(); //operator
if (operator == '%')
output = num1.mod(num2);
else if (operator == '^') {
while (current.getNextConsCell().getCarType() == ConsType.OPERATOR && (Character) current.getNextConsCell().getCar() == '^') {
boolean negate = false;
while (current.getNextConsCell(2).getCarType() == ConsType.OPERATOR) {
if ((Character) current.getNextConsCell(2).getCar() == '-')
negate = !negate;
current.getNextConsCell(2).remove();
}
if (current.getNextConsCell(2).getCarType() != ConsType.NUMBER)
throw new UndefinedResultException("For stacked exponents, each subsequent exponent must evaluate to a number", null);
num2 = num2.pow(negate ? ((BigDec) current.getNextConsCell(2).getCar()).multiply(BigDec.MINUSONE) : (BigDec) current.getNextConsCell(2).getCar());
current.getNextConsCell().remove();
current.getNextConsCell().remove();
}
output = num1.pow(num2);
}
else if (operator == '*')
output = num1.multiply(num2);
else if (operator == '/')
output = num1.divide(num2);
else if (operator == '+')
output = num1.add(num2);
else if (operator == '-')
output = num1.subtract(num2);
else
throw new UndefinedResultException(lastPlugin);
current.replaceCar(new ConsCell(output, ConsType.NUMBER));
forward = false;
}
else if ((current.getCarType() == ConsType.STRING || second.getCarType() == ConsType.STRING) &&
!(current.getCarType() == ConsType.NUMBER || second.getCarType() == ConsType.NUMBER)) {
if (operator != '+')
throw new UndefinedResultException(lastPlugin);
current.replaceCar(new ConsCell(current.carToString() + second.carToString(), ConsType.STRING));
second.remove();
current.getNextConsCell().remove();
forward = false;
}
else if (current.getCarType() == ConsType.NUMBER && second.getCarType() == ConsType.STRING) {
if (operator == '+')
current.replaceCar(new ConsCell(current.carToString() + second.carToString(), ConsType.STRING));
else if (operator == '*')
current.replaceCar(new ConsCell(stringMultiplier(second.carToString(), ((BigDec) current.getCar()).intValue()), ConsType.STRING));
else
throw new UndefinedResultException(lastPlugin);
second.remove();
current.getNextConsCell().remove();
forward = false;
}
else if (current.getCarType() == ConsType.STRING && second.getCarType() == ConsType.NUMBER) {
if (operator == '+')
current.replaceCar(new ConsCell(current.carToString() + second.carToString(), ConsType.STRING));
else if (operator == '*')
current.replaceCar(new ConsCell(stringMultiplier(current.carToString(), ((BigDec) second.getCar()).intValue()), ConsType.STRING));
else
throw new UndefinedResultException(lastPlugin);
second.remove();
current.getNextConsCell().remove();
forward = false;
}
else if (current.getCarType() == ConsType.OBJECT && second.getCarType() == ConsType.OBJECT) {
Matrix m1 = currentEqn.matrices.get(Integer.parseInt(((String) current.getCar()).substring(2, ((String) current.getCar()).length() - 1)));
Matrix m2 = currentEqn.matrices.get(Integer.parseInt(((String) second.getCar()).substring(2, ((String) second.getCar()).length() - 1)));
Matrix result = m1.matrixOp(m2, new Character(operator).toString());
currentEqn.matrices.add(result);
second.remove();
current.getNextConsCell().remove();
current.replaceCar(new ConsCell("{M" + (currentEqn.matrices.size() - 1) + "}", ConsType.OBJECT));
forward = false;
}
else if (current.getCarType() == ConsType.NUMBER && second.getCarType() == ConsType.OBJECT) {
if (operator == '/')
throw new UndefinedOperationException(operator + " is not defined for a matrix and a scalar.", lastPlugin);
Matrix m = currentEqn.matrices.get(Integer.parseInt(((String) current.getCar()).substring(2, ((String) current.getCar()).length() - 1)));
Matrix result = m.scalarOp((BigDec) current.getCar(), new Character(operator).toString());
currentEqn.matrices.add(result);
second.remove();
current.getNextConsCell().remove();
current.replaceCar(new ConsCell("{M" + (currentEqn.matrices.size() - 1) + "}", ConsType.OBJECT));
forward = false;
}
else if (current.getCarType() == ConsType.OBJECT && second.getCarType() == ConsType.NUMBER) {
Matrix m = currentEqn.matrices.get(Integer.parseInt(((String) second.getCar()).substring(2, ((String) second.getCar()).length() - 1)));
Matrix result = m.scalarOp((BigDec) second.getCar(), new Character(operator).toString());
currentEqn.matrices.add(result);
second.remove();
current.getNextConsCell().remove();
current.replaceCar(new ConsCell("{M" + (currentEqn.matrices.size() - 1) + "}", ConsType.OBJECT));
forward = false;
}
}
} while (!forward || (forward && !((current = current.getNextConsCell()).isNull()))); //This steps current forward while checking for nulls
current = input;
}
return input;
}
private String stringMultiplier(String str, int num) {
String output = "";
for (int i = 0; i < num; i++)
output = output + str;
return output;
}
/**
* @param check
* the character to check
* @return true if check is a number (0-9), false otherwise
*/
public static boolean isNumber(Character check) {
String numbers = "0123456789";
if (numbers.contains(check.toString()))
return true;
return false;
}
/**
* @param check
* the String to check
* @return true if check is a number, false otherwise
*/
public static boolean isNumber(String check) {
try {
new Double(check);
return true;
}
catch (NumberFormatException e) {
return false;
}
}
/**
* @param check
* the character to check
* @return true if check is an operator (+-/*^%), false otherwise
*/
public static boolean isOperator(Character check) {
if (operators.contains(check.toString()))
return true;
return false;
}
/**
* Convenience method for {@link #isOperation(String, ArrayList) isOperation(check, new ArrayList<String>())}
*
* @param check
* the <tt>String</tt> to check
* @return true if <tt>check</tt> is the name of an operation, otherwise false
*/
public boolean isOperation(String check) {
return isOperation(check, new ArrayList<String>());
}
/**
* @param check
* the <tt>String</tt> to check
* @param extraOps
* additional operations that should be included
* @return true if <tt>check</tt> is the name of an operation, otherwise false
*/
public boolean isOperation(String check, ArrayList<String> extraOps) {
for (String op : operations.keySet())
if (check.equalsIgnoreCase(op))
return true;
for (String op : extraOps)
if (check.equalsIgnoreCase(op))
return true;
return false;
}
/**
* Convenience method for {@link #getEndIndex(String section, int start, String startSymbol, String endSymbol)} returns
* the index of the parenthesis that closes the open parenthesis at start.
*
* @param section
* the String containing the parentheses to be checked
* @param start
* index of the opening parenthesis to be checked
* @return index of the closing parenthesis
* @throws UnbalancedParenthesesException
* for most String parsing purposes this can be safely ignored
*/
public static int getEndIndex(String section, int start) throws UnbalancedParenthesesException {
return getEndIndex(section, start, "(", ")");
}
/**
* returns the endSymbol that closes the startSymbol at start, factoring in that there may be additional startSymbols and
* endSymbols
*
* @param section
* the String to be checked
* @param start
* index of the opening startSymbol to be checked
* @param startSymbol
* the symbol that opens this block
* @param endSymbol
* the symbol that closes this block
* @return index of the closing endSymbol
* @throws UnbalancedParenthesesException
* for most String parsing purposes this can be safely ignored
*/
public static int getEndIndex(String section, int start, String startSymbol, String endSymbol) throws UnbalancedParenthesesException {
int index = 0, parenthesis = 0;
for (int i = start; i < section.length() - startSymbol.length() + 1 && i < section.length() - endSymbol.length() + 1; i++) {
if (section.substring(i, i + startSymbol.length()).equals(startSymbol))
parenthesis++;
if (section.substring(i, i + endSymbol.length()).equals(endSymbol))
parenthesis--;
if (parenthesis == 0) {
index = i;
break;
}
}
if (parenthesis != 0)
throw new UnbalancedParenthesesException(start, section, null);
return index;
}
/**
* Convenience method for {@link #getStartIndex(String section, int end, String startSymbol, String endSymbol)} returns
* the index of the parenthesis that opens the closing parenthesis at end.
*
* @param section
* the String containing the parentheses to be checked
* @param end
* index of the opening parenthesis to be checked
* @return index of the opening parenthesis
* @throws UnbalancedParenthesesException
* for most String parsing purposes this can be safely ignored
*/
public static int getStartIndex(String section, int end) throws UnbalancedParenthesesException {
return getStartIndex(section, end, ")", "(");
}
/**
* returns the endSymbol that closes the startSymbol at start, factoring in that there may be additional startSymbols and
* endSymbols
*
* @param section
* the String to be checked
* @param end
* index of the closing endSymbol to be checked
* @param startSymbol
* the symbol that opens this block
* @param endSymbol
* the symbol that closes this block
* @return index of the opening startSymbol
* @throws UnbalancedParenthesesException
* for most String parsing purposes this can be safely ignored
*/
public static int getStartIndex(String section, int end, String startSymbol, String endSymbol) throws UnbalancedParenthesesException {
int index = 0, parenthesis = 0;
for (int i = end; i >= 0; i--) {
if (section.substring(i, i + startSymbol.length()).equals(startSymbol))
parenthesis++;
if (section.substring(i, i + endSymbol.length()).equals(endSymbol))
parenthesis--;
if (parenthesis == 0) {
index = i;
break;
}
}
if (parenthesis != 0)
throw new UnbalancedParenthesesException(end, section, null);
return index;
}
/**
* @param input
* String to be checked
* @return whether input contains 0-9, infinity, Infinity, operations, or keywords by regex.
*/
public boolean isValid(ConsCell input) {
ArrayList<Pattern> patterns = new ArrayList<Pattern>();
patterns.add(Pattern.compile("[0-9]"));
patterns.add(Pattern.compile("[Ii]nfinity")); //Infinity with both cases
for (String keyword : keywords.keySet())
patterns.add(Pattern.compile("\\Q" + keyword + "\\E"));
for (String operation : operations.keySet())
patterns.add(Pattern.compile("\\Q" + operation + "\\E"));
return isValid(input, patterns);
}
/**
* @param input
* String to be checked
* @param patterns
* A specified set of regex patterns to be checked for
* @return whether input contains at least one of the patterns specified in regex.
*/
public boolean isValid(ConsCell input, ArrayList<Pattern> patterns) {
String inp = input.toString();
for (Pattern pattern : patterns)
if (pattern.matcher(inp).find())
return true;
return false;
}
/* protected String graphing(String input){ if(!(input.contains("(") && input.contains(")"))) return ""; String command =
* input.substring(0, input.indexOf('(')); if(getEndIndex(input, input.indexOf('(')) <= 0) return ""; input =
* input.substring(input.indexOf('(')+1, getEndIndex(input, input.indexOf('('))); if(command.equalsIgnoreCase("graph")){
* makeGraph(input); return "graph created"; } else if(command.equalsIgnoreCase("graphLine")){ makeGraph(input, true);
* return "graph created"; } else if(command.equalsIgnoreCase("removeSet") && graphExists){ graph.removeSet(input);
* return input + " removed"; } else if(command.equalsIgnoreCase("addSet")){ makeGraph(input); return "added " + input; }
* else return ""; } */
/* public void makeGraph(String sets){ makeGraph(sets, false); } public void makeGraph(String sets, boolean isLine){
* if(sets.length() >= 3 && !isLine && !sets.contains("(") && !sets.contains(")")) sets = "(" + sets + ")";
* if(!graphExists) graph = new Graph(); if(sets.length() > 0 || graphExists) graph.addPoints(sets, isLine);
* graph.repaint(); if(!graphExists) graphWindow = new GraphWindow(); else graphWindow.updateGraph(); } */
/**
* Gets the variables in an equation
*
* @param input
* the equation to check
* @return the variables in input, if there are any, an empty ArrayList<String> otherwise
*/
public ArrayList<String> getVariables(ConsCell input) {
ArrayList<String> output = new ArrayList<String>();
ConsCell current = input;
do {
if (current.getCarType() == ConsType.IDENTIFIER && !allNames.contains(current.getCar()) && isAllLetters((String) current.getCar()) && !output.contains(current.getCar()))
output.add((String) current.getCar());
if (current.getCarType() == ConsType.CONS_CELL) {
ArrayList<String> temp = getVariables((ConsCell) current.getCar());
for (String var : temp)
if (!output.contains(var))
output.add(var);
}
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
return output;
}
/**
* Convenience method for {@link #containsVariables(ConsCell input, ArrayList variables, ArrayList otherOps)} This runs
* {@link #containsVariables(ConsCell input, ArrayList variables, ArrayList otherOps)} using the currently selected
* variables in this <tt>Parser</tt> and without any additional operations.
*
* @param input
* the equation to check
* @return true if the equation contains at least one of the variables, false otherwise
* @see #containsVariables(ConsCell, ArrayList) containsVariable(ConsCell input, ArrayList<String> variables)
* @see #containsVariables(ConsCell, ArrayList, ArrayList) containsVariable(ConsCell input, ArrayList<String> variables,
* ArrayList<String> otherOps)
*/
public boolean containsVariables(ConsCell input) {
return containsVariables(input, getVars(), new ArrayList<String>());
}
/**
* Convenience method for {@link #containsVariables(ConsCell input, ArrayList variables, ArrayList otherOps)}</br> This
* runs {@link #containsVariables(ConsCell input, ArrayList variables, ArrayList otherOps)} without any additional
* operations.
*
* @param input
* the equation to check
* @param variables
* the variables to check for
* @return true if the equation contains at least one of the variables, false otherwise
* @see #containsVariables(ConsCell) containsVariable(ConsCell input)
* @see #containsVariables(ConsCell, ArrayList, ArrayList) containsVariable(ConsCell input, ArrayList<String> variables,
* ArrayList<String> otherOps)
*/
public boolean containsVariables(ConsCell input, ArrayList<String> variables) {
return containsVariables(input, variables, new ArrayList<String>());
}
/**
* Determines if the equation contains any variables in variables
*
* @param input
* the equation to check
* @param variables
* the variables to check for
* @param otherOps
* other items that are operations, and should be excluded from the variable searching (which doesn't check
* for word endings by necessity)
* @return true if the equation contains at least one of the variables, false otherwise
* @see #containsVariables(ConsCell, ArrayList) containsVariable(ConsCell input, ArrayList<String> variables)
* @see #containsVariables(ConsCell) containsVariable(ConsCell input)
*/
public boolean containsVariables(ConsCell input, ArrayList<String> variables, ArrayList<String> otherOps) {
ConsCell current = input;
do {
if (current.getCarType() == ConsType.IDENTIFIER && !allNames.contains(current.getCar()) && !otherOps.contains(current.getCar()) && variables.contains(current.getCar()))
return true;
if (current.getCarType() == ConsType.CONS_CELL && containsVariables(((ConsCell) current.getCar()), variables, otherOps))
return true;
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
return false;
}
/**
* Removes '^+', '^-', '^*', '^/', '--', '++', '+-', and '-+' cases.
*
* @param input
* the section of the equation to be checked and fixed, if applicable
* @return the input with the above cases removed
*/
public ConsCell removeDoubles(ConsCell input) {
ConsCell current = input, next = current.getNextConsCell();
if (next.isNull())
return input;
boolean step = true;
do {
step = true;
if (current.getCarType() == ConsType.CONS_CELL) {
current.replaceCar(removeDoubles((ConsCell) current.getCar()));
continue;
}
if (current.getCarType() != ConsType.OPERATOR || next.getCarType() != ConsType.OPERATOR)
continue;
char currentCar = (Character) current.getCar(), nextCar = (Character) next.getCar();
if ((currentCar == '*' && nextCar == '/') || (currentCar == '/' && nextCar == '*')) {
next.remove();
current.replaceCar(new ConsCell('/', ConsType.OPERATOR));
step = false;
}
if ((currentCar == '-' && nextCar == '-') || (currentCar == '+' && nextCar == '+')) {
next.remove();
current.replaceCar(new ConsCell('+', ConsType.OPERATOR));
step = false;
}
if ((currentCar == '-' && nextCar == '+') || (currentCar == '+' && nextCar == '-')) {
next.remove();
current.replaceCar(new ConsCell('-', ConsType.OPERATOR));
step = false;
}
if (nextCar == '^')
current.insert(new ConsCell(BigDec.ZERO, ConsType.NUMBER));
next = current.getNextConsCell();
} while (!step || (step && !((current = current.getNextConsCell()).isNull() || (next = current.getNextConsCell()).isNull()))); //This steps current forward while checking for nulls
return input;
}
/**
* Gets all of the Operations loaded into this <tt>Parser</tt>
*
* @return a HashMap<String, Operation> that contains all the Operations (final and normal) loaded into this
* <tt>Parser</tt>
*/
public HashMap<String, Operation> getOperations() {
return new HashMap<String, Operation>(operations);
}
/**
* Gets all of the Commands loaded into this <tt>Parser</tt>
*
* @return a HashMap<String, Command> that contains all the Commands loaded into this <tt>Parser</tt>
*/
public HashMap<String, Command> getCommands() {
return new HashMap<String, Command>(commands);
}
/**
* Gets all of the Keywords loaded into this <tt>Parser</tt>
*
* @return a HashMap<String, Keyword> that contains all the Keywords loaded into this <tt>Parser</tt>
*/
public HashMap<String, Keyword> getKeywords() {
return new HashMap<String, Keyword>(keywords);
}
/**
* @return a list of the names of all the Commands, Keywords, and Operations in this <tt>Parser</tt>
*/
public ArrayList<String> getAllNames() {
return new ArrayList<String>(allNames);
}
/**
* @return a HashMap<String, String> of all of the abbreviations for Commands, Keywords, and Operations in this
* <tt>Parser</tt>
*/
public HashMap<String, String> getAbbreviations() {
return new HashMap<String, String>(abbreviations);
}
public String getOutputType() {
return outputType;
}
public void setOutputType(String outputType) {
this.outputType = new String(outputType);
}
public String getDefaultLogBase() {
return defaultLogBase;
}
public void setDefaultLogBase(String defaultLogBase) {
if (!defaultLogBase.equals(this.defaultLogBase))
propertyChangeSupport.firePropertyChange("DefaultLogBase", this.defaultLogBase, (this.defaultLogBase = defaultLogBase));
}
/**
* @return the {@link lipstone.joshua.parser.util.AngleType AngleType} that this <tt>Parser</tt> is currently using
*/
public AngleType getAngleType() {
return angleType;
}
/**
* @param name
* the name of the {@link lipstone.joshua.parser.util.AngleType AngleType} to retrieve
* @return the {@link lipstone.joshua.parser.util.AngleType AngleType} denoted by the given name if it is registered in
* this <tt>Parser</tt>, otherwise null
*/
public AngleType getAngleType(String name) {
return angleTypes.get(name);
}
/**
* @param angleType
* the new {@link lipstone.joshua.parser.util.AngleType AngleType} for this parser to use
* @return the AngleType that was previously in use
*/
public AngleType setAngleType(AngleType angleType) {
if (!angleTypes.containsKey(angleType.getName()))
addAngleType(angleType);
AngleType old = this.angleType;
if (!angleType.equals(this.angleType))
propertyChangeSupport.firePropertyChange("AngleTypeChanged", this.angleType, (this.angleType = angleType));
return old;
}
/**
* Adds an {@link lipstone.joshua.parser.util.AngleType AngleType} to this <tt>Parser</tt>.
*
* @param angleType
* the {@link lipstone.joshua.parser.util.AngleType AngleType} to add
* @return the {@link lipstone.joshua.parser.util.AngleType AngleType} that this overrode if it did, otherwise null
*/
public AngleType addAngleType(AngleType angleType) {
AngleType old = angleTypes.put(angleType.getName(), angleType);
propertyChangeSupport.firePropertyChange("AngleTypeAdded", old, angleType);
return old;
}
/**
* Removes an {@link lipstone.joshua.parser.util.AngleType AngleType} from this <tt>Parser</tt> by name.
*
* @param angleType
* the name of the {@link lipstone.joshua.parser.util.AngleType AngleType} to remove
* @return the {@link lipstone.joshua.parser.util.AngleType AngleType} that was removed, otherwise null
*/
public AngleType removeAngleType(String angleType) {
AngleType old = angleTypes.remove(angleType);
propertyChangeSupport.firePropertyChange("AngleTypeRemoved", old, null);
return old;
}
public Equation getCurrentEqn() {
return currentEqn;
}
public void setCurrentEqn(Equation currentEqn) {
this.currentEqn = new Equation(currentEqn);
}
public HashMap<String, Operation> getFinalOperations() {
HashMap<String, Operation> output = new HashMap<String, Operation>();
for (String operation : finalOperations)
output.put(operation, operations.get(operation));
return output;
}
public ArrayList<InputFilterPlugin> getPreProcessFilters() {
return new ArrayList<InputFilterPlugin>(preProcessFilters);
}
public ArrayList<OutputFilterPlugin> getPostProcessFilters() {
return new ArrayList<OutputFilterPlugin>(postProcessFilters);
}
public ArrayList<SettingsPlugin> getSettingsPlugins() {
return new ArrayList<SettingsPlugin>(settingsPlugins);
}
public History getHistory() {
return history;
}
/**
* @return the CAS associated with this <tt>Parser</tt>
*/
public CAS getCAS() {
return ns;
}
/**
* @return the {@link lipstone.joshua.parser.Log Log} that is associated with this <tt>Parser</tt>
*/
public Log getLog() {
return log;
}
public ArrayList<ParserPlugin> getPlugins() {
return plugins;
}
public ConsCell replaceKeywords(ConsCell input) throws SyntaxException, UnbalancedParenthesesException, ParserException {
ConsCell current = input;
do {
if (current.getCarType() == ConsType.IDENTIFIER) {
if (keywords.containsKey(current.getCar()))
current.replaceCar(Tokenizer.tokenizeString(((KeywordPlugin) this.keywords.get(current.getCar()).getPlugin()).getKeywordData((String) current.getCar())));
}
if (current.getCarType() == ConsType.CONS_CELL)
current.replaceCar(replaceKeywords((ConsCell) current.getCar()));
} while (!((current = current.getNextConsCell()).isNull())); //This steps current forward while checking for nulls
return input;
}
/**
* Checks if the given string is composed exclusively of characters from the Latin and Greek alphabets.
*
* @param string
* the <tt>String</tt> to check
* @return true if the <tt>String</tt> is composed exclusively of characters from the Latin and Greek alphabets,
* otherwise false
*/
public boolean isAllLetters(String string) {
return Pattern.compile("[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039A\u039B\u039C\u039D\u039E\u039F\u03A0\u03A1"
+ "\u03A2\u03A3\u03A4\u03A5\u03A6\u03A7\u03A8\u03A9\u03B1\u03B2\u03B3\u03B4\u03B5\u03B6\u03B7\u03B8\u03B9\u03BA\u03BB\u03BC\u03BD\u03BE\u03BF\u03C0\u03C1\u03C2\u03C3\u03C4"
+ "\u03C5\u03C6\u03C7\u03C8\u03C9]*+").matcher(string).matches();
}
/**
* Adds the given {@link java.beans.PropertyChangeListener PropertyChangeListener} to this <tt>Parser</tt>. If no
* properties are specified, it adds the listener to all the properties.
*
* @param listener
* the {@link java.beans.PropertyChangeListener PropertyChangeListener} to add
* @param properties
* the properties to add this listener to
* @see #PluginLoaded
* @see #PluginUnloaded
* @see #OperationLoaded
* @see #OperationUnloaded
* @see #KeywordLoaded
* @see #KeywordUnloaded
* @see #CommandLoaded
* @see #CommandUnloaded
* @see #AngleTypeChanged
* @see #DefaultLogBase
* @see #SaveHistory
* @see #DataPersistence
*/
public void addPropertyListener(PropertyChangeListener listener, Property... properties) {
if (properties.length == 0)
propertyChangeSupport.addPropertyChangeListener(listener);
else
for (Property property : properties)
propertyChangeSupport.addPropertyChangeListener(property.toString(), listener);
}
/**
* This is <i>not</i> the default plugin location. To get the default plugin location, use:
* <code>getBaseLocation() + "/plugins/"</code>
*
* @return the location that this <tt>PluginUser</tt> is running from.
*/
@Override
public String getBaseLocation() {
return baseLocation.toString() + "/";
}
/**
* Gets the variables that are currently in use
*
* @return the variables currently in use
*/
public ArrayList<String> getVars() {
return vars;
}
/**
* Sets the variables to be ONLY the variable in the String
*
* @param var
* the variable
*/
public void setVars(String var) {
vars.clear();
vars.add(var);
}
/**
* Remove all variables contained in the Collection<String> vars
*
* @param vars
* the variables to remove
*/
public void removeVars(Collection<String> vars) {
this.vars.removeAll(vars);
}
/**
* Add the variable in var to the variables list
*
* @param var
* the variable to add
*/
public void addVar(String var) {
vars.add(var);
}
/**
* Sets the variable list to be the ArrayList<String> vars, and erases the old set of variables
*
* @param vars
* the new set of variables
*/
public void setVars(ArrayList<String> vars) {
this.vars = new ArrayList<String>(vars);
}
/**
* Gets the last called plugin. This is used for error handling when the thrower is null.
*
* @return the lastPlugin
*/
public ParserPlugin getLastPlugin() {
return lastPlugin;
}
/**
* @return the fjpool
*/
public static ForkJoinPool getFjPool() {
return fjPool;
}
/**
* @return whether this <tt>Parser</tt> is currently saving an input history and whether the history.xml file will be
* saved (true) or deleted (false) when the program closes
* @see #setSaveHistory(boolean saveHistory)
*/
public final boolean isSavingHistory() {
return saveHistory;
}
/**
* The saveHistory flag determines whether this <tt>Parser</tt> should record each input and whether the history.xml file
* should be saved (true) or deleted (false) when the program is closed. It defaults to true.
*
* @param saveHistory
* the new value for the saveHistory flag
* @see #isSavingHistory()
*/
public final void setSaveHistory(boolean saveHistory) {
if (saveHistory ^ this.saveHistory)
propertyChangeSupport.firePropertyChange("SaveHistory", this.saveHistory, (this.saveHistory = saveHistory));
}
/**
* @return whether any data (plugin, history, or logs) is going to persist across runs
* @see #setDataPersistence(boolean dataPersistence)
*/
public final boolean isDataPersisting() {
return dataPersistence;
}
/**
* The dataPersistence flag determines whether all the data associated with this parser other than the plugins folder
* will be saved (true) or deleted (false) when the program is closed. It defaults to true.
*
* @param dataPersistence
* whether any data (plugin, history, or logs) should persist across runs
* @see #isDataPersisting()
*/
public final void setDataPersistence(boolean dataPersistence) {
if (dataPersistence ^ this.dataPersistence)
propertyChangeSupport.firePropertyChange("DataPersistence", this.dataPersistence, (this.dataPersistence = dataPersistence));
}
/**
* Adds a command to the command queue to be run after the <tt>Parser</tt> finishes processing this equation
*
* @param command
* the command to add to the queue
* @throws ParserException
*/
public void queueCommand(ConsCell command, ParserPlugin plugin) throws ParserException {
if (commands.get(command.getCar()) == null)
throw new ParserException("The plugin, " + plugin.getID() + ", attempted to queue a non-existent command", plugin);
if (!commands.get(command.getCar()).getPlugin().getID().equals(plugin.getID()))
throw new ParserException("The plugin, " + plugin.getID() + ", attempted to queue a command that is mapped to " + commands.get(command.getCar()).getPlugin().getID(), plugin);
commandQueue.push(command);
}
/**
* @return whether the <tt>Parser</tt> is currently processing an equation
*/
public boolean isProcessing() {
return isProcessing;
}
/**
* @return the default precision
*/
public MathContext getPrecision() {
return precision;
}
private static final class TreeMapSorter implements Comparator<Object> {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof String && o2 instanceof String) {
if (((String) o1).length() > ((String) o2).length())
return -1;
else if (((String) o1).length() < ((String) o2).length())
return 1;
return ((String) o1).compareTo(((String) o2));
}
return 0;
}
}
}