Package lipstone.joshua.parser

Source Code of lipstone.joshua.parser.Parser$TreeMapSorter

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;
    }
   
  }
}
TOP

Related Classes of lipstone.joshua.parser.Parser$TreeMapSorter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.