Page MenuHomedesp's stash

ExpressionGenerator.java
No OneTemporary

ExpressionGenerator.java

package me.despawningbone.arithtrain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The utility class for expression evaluation and generation.<br>
* The generator can be instantiated with different configurable values, allowing for flexible expression generation.<br>
* The evaluator can also be statically accessed in this class, since it does not have configurable values.
* <p>
* <i>Intended for use in the main arithmetic trainer, but third-party programs may also utilize this class as an expression generation and evaluation library if needed.</i>
* @author despawningbone
*/
public class ExpressionGenerator {
/**
* A weighted representation of the operators' weighted chance to appear in generation.
* <p>
* <b>Note: This is a universal value across all generators!</b>
*/
public final static TreeMap<Integer, String> operators = new TreeMap<>();
public int bracketChance, operatorCount, origin, bound;
/**
* Instantiates a generator with specifications according to the configuration passed in the parameters.<br>
*
* @param bracketChance chance for brackets to appear (value should be from 0-100)
* @param operatorCount how many operators the generated expression will have
* @param origin lower bound of the generated numbers in the expressions (inclusive)
* @param bound upper bound of the generated numbers in the expressions (inclusive)
*/
public ExpressionGenerator(int bracketChance, int operatorCount, int origin, int bound) {
this.bracketChance = bracketChance;
this.operatorCount = operatorCount;
this.origin = origin;
this.bound = bound;
}
/**
* Generates a direct {@link ExpressionGenerator#stepEval(List)} parsable expression along with the steps needed to solve according to the generator object's specifications. <br>
* Note that as real number operations can be extremely difficult to calculate mentally, the arithmetic trainer will only generate integers.
*
* @return a generated expression string
*/
public List<String> generate() { //generates only integers
ThreadLocalRandom ran = ThreadLocalRandom.current();
int startingValue = ran.nextInt(origin, bound); //start it with a value so it wont be invalid
String exp = String.valueOf(startingValue);
for(int i = 0; i < operatorCount; i++) {
int opnd = ran.nextInt(origin, bound);
String operator = operators.floorEntry(ran.nextInt(100)).getValue(); //choose one randomly
if(ran.nextBoolean()) { //randomly choose to put in front or behind the old expression
exp += operator + opnd;
} else {
exp = opnd + operator + exp;
}
if(ran.nextInt(100) + 1 < bracketChance) { //add brackets
exp = "(" + exp + ")";
}
}
try {
List<String> steps = stepEval(new ArrayList<String>(Arrays.asList(exp))); //validate if whole expression is valid; might induce performance overhead but its the quickest way to do so
//no need evaluateWithSteps() coz the generated expression format can be directly parsed without reformatting
return steps;
} catch (IllegalArgumentException e) {
return generate(); //generate a new one until theres no divide by zero anymore
}
}
/**
* Compares the current expression generator with another and determines if the current generator has strictly more difficult specifications than the other.<br>
* The comparing criteria is as follows:
* <ul>
* <li>Higher bracket chance</li>
* <li>Higher operator count</li>
* <li>Larger numeric range</li>
* </ul><p>
* All of the above must be true in order for the current generator to be considered harder.<br>
* @param gen the generator to be compared with
* @return true if the current generator is harder, false otherwise
*/
public boolean isHarder(ExpressionGenerator gen) {
return this.bracketChance >= gen.bracketChance && this.operatorCount >= gen.operatorCount && (this.bound - this.origin) > (gen.bound - gen.origin);
}
/**
* Returns a textual representation of the specifications defined in this generator object.<br>
* @return an array-like representation of the generator
*/
@Override
public String toString() { //allow people to get the config values via toString()
return "ExpGen{bracket=" + this.bracketChance + ",opCount=" + this.operatorCount + ",range=" + this.origin + "-" + this.bound + "}";
}
/**
* Lints and evaluates a string expression, and returns the evaluated result with steps.<br>
*
* @param exp the expression to be evaluated
* @return an array storing the steps involved in evaluating the result
*/
public static List<String> evaluateWithSteps(String exp) { //wrapper for the stepEval function to make it easier to understand externally, and also acts as an expression linter
if(exp == null || Pattern.compile("[^+\\-*/%().0-9E ]").matcher(exp).find()) { //checking for anything other than the operators supported
throw new IllegalArgumentException("Invalid expression!");
}
Pattern pattern = Pattern.compile("\\(([0-9.E]*)\\)|\\((-*[0-9.E]*)\\)"); //remove unnecessary brackets, eg 1-(-6) -> 1--6 so splitting later on will not break
exp = exp.replaceAll("\\s", "") //DONE remove all spaces
.replaceAll("\\)([0-9(])", ")*$1").replaceAll("([0-9)])\\(", "$1*("); //add back *s between brackets so it wont break, but the steps will include it
Matcher match = pattern.matcher(exp);
while(match.find()) { //to remove multiple brackets if present, eg ((x))
exp = match.replaceAll("$1$2");
match = pattern.matcher(exp);
}
return stepEval(new ArrayList<String>(Arrays.asList(exp
.replaceAll("(?<![0-9)])[+]+", "") //remove + for positive indicators
.replaceAll("(?<![0-9)])[-]((-{4})*)-", "")))); // make consecutive negative signs become one, excluding x--y because it can be x-(-x)
}
/**
* An internal recursive function that evaluates the last expression in the given list step by step until a single numeric value is yielded.<br>
* <p>
* This method is not expected to be used alone! As a result, it has the private modifier to prevent outside access.<br>
* It is strongly recommended that third-party programs should utilize {@link ExpressionGenerator#evaluateWithSteps(String)} instead of accessing this method through reflection.<br>
* @param steps a list containing the expression(s)
* @return a list containing the expressions including the newly evaluated one
*/
private static List<String> stepEval(List<String> steps) {
String newestExp = steps.get(steps.size() - 1);
if(newestExp.matches("-{0,1}[0-9.]*(?:E-{0,1})?[0-9.]*")) return steps; //stopping anchor; only case where - sign should exist is at the first char or after E for negative
String[] splits = newestExp.split("(?<=[^0-9.E])(?=(?<![^0-9]-))(?=(?<!^-))|(?=[^0-9.E])(?<=[^E])"); //split with delimiter, aka expressions; second group defines - without preceding number as negative indicator instead of operator, and 3rd group checks the start of line; last group escapes negative E notations (e.g. E-2)
boolean isPrior = false, sameLevel = true; int index = splits.length - 1;
for(int i = 0; i < splits.length; i++) {
String split = splits[i];
if(split.matches("[^0-9.E]*")) { //if its an operand we dont need to do anything
if(split.equals("(")) {
isPrior = false; //because parentheses preceed any operators, so remove other priorities
sameLevel = false;
} else if(!isPrior && (split.equals("*") || split.equals("/") || split.equals("%"))) { //only 3 with elevated priorities
isPrior = true;
index = i;
} else if(!isPrior && (!sameLevel || !(splits[index].equals("+") || splits[index].equals("-"))) && (split.equals("+") || split.equals("-"))) { //as its not prioritized, only when there is no prioritized operator will it be evaluated; if it is in the same bracket, evaluate the first operator
index = i;
sameLevel = true;
} else if(split.equals(")")) {
isPrior = true; //because parentheses are of highest priority
}
}
}
/*
* start eval
*/
String op = splits[index];
try {
double x = Double.parseDouble(splits[index - 1]);
double y = Double.parseDouble(splits[index + 1]);
double val = 0;
switch(op) {
case "+": val = x + y;
break;
case "-": val = x - y;
break;
case "*": val = x * y;
break;
case "/": val = x / y;
break;
case "%": val = x % y; //apparently in java its remainder, not modulus
break;
}
splits[index - 1] = ""; //wipe the old values
splits[index + 1] = "";
splits[index] = String.valueOf(val); //put the new value in
if(index + 2 < splits.length && index - 2 >= 0) {
if(splits[index + 2].equals(")") && splits[index - 2].equals("(")) { //throws ArrayIndexOutOfBoundsException theres inbalanced parentheses
splits[index + 2] = ""; //wipes used parentheses
splits[index - 2] = "";
}
}
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
throw new IllegalArgumentException("Invalid expression!"); //this should only throw if there is 2 operators at the same place, result reaches infinity or undefined, or if there is unfinished operators at the ends
}
steps.add(String.join("", splits));
return stepEval(steps);
}
}

File Metadata

Mime Type
text/x-java
Expires
Mon, Oct 19, 3:47 PM (1 d, 3 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fd/50/a06b0476937771cd274b6f4232ac

Event Timeline