Skip to content

Commit

Permalink
added simple expression parsing in number fields.
Browse files Browse the repository at this point in the history
SelectDouble, SelectInteger now use DelayedDocumentValidator
which uses SimpleParser
which uses exp4j
  • Loading branch information
i-make-robots committed Feb 11, 2025
1 parent 755da53 commit 2281244
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 84 deletions.
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@
<artifactId>flatlaf</artifactId>
<version>3.5</version>
</dependency>

<!-- simple math expression parser -->
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>0.4.8</version>
</dependency>

<dependency>
<!--
When I run locally, I use groupID com.marginallyclever so that it grabs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.marginallyclever.donatello.select;

import com.marginallyclever.donatello.simpleparser.SimpleParser;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Consumer;

/**
* {@link DelayedDocumentValidator} validates the text in a number field using a {@link SimpleParser} so it can
* handle simple math expressions.
*/
public class DelayedDocumentValidator implements DocumentListener {
private final JTextComponent field;
private Timer timer=null;
private final Consumer<Double> consumer;

public DelayedDocumentValidator(JTextComponent field, Consumer<Double> consumer) {
super();
this.field = field;
this.consumer = consumer;
}

@Override
public void changedUpdate(DocumentEvent arg0) {
if(arg0.getLength()==0) return;
validate();
}

@Override
public void insertUpdate(DocumentEvent arg0) {
if(arg0.getLength()==0) return;
validate();
}

@Override
public void removeUpdate(DocumentEvent arg0) {
if(arg0.getLength()==0) return;
validate();
}

public void validate() {
try {
double newValue = SimpleParser.evaluate(field.getText());
field.setForeground(UIManager.getColor("Textfield.foreground"));
field.setToolTipText(null);

if(timer!=null) timer.cancel();
timer = new Timer("Delayed response");
timer.schedule(new TimerTask() {
public void run() {
consumer.accept(newValue);
}
}, 100L); // brief delay in case someone is typing fast
} catch (IllegalArgumentException e) {
field.setForeground(Color.RED);
field.setToolTipText(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.marginallyclever.donatello.select;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;


/**
Expand Down Expand Up @@ -35,46 +32,13 @@ public SelectDouble(String internalName,String labelKey, Locale locale, double d
field.setHorizontalAlignment(JTextField.RIGHT);
setValue(defaultValue);

field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent arg0) {
if(arg0.getLength()==0) return;
validate();
field.getDocument().addDocumentListener(new DelayedDocumentValidator(field,newValue-> {
if (value != newValue) {
double oldValue = value;
value = newValue;
fireSelectEvent(oldValue, newValue);
}

@Override
public void insertUpdate(DocumentEvent arg0) {
if(arg0.getLength()==0) return;
validate();
}

@Override
public void removeUpdate(DocumentEvent arg0) {
if(arg0.getLength()==0) return;
validate();
}

public void validate() {
try {
double newValue = Float.parseFloat(field.getText());
field.setForeground(UIManager.getColor("Textfield.foreground"));
if(value != newValue) {
double oldValue = value;
value = newValue;

if(timer!=null) timer.cancel();
timer = new Timer("Delayed response");
timer.schedule(new TimerTask() {
public void run() {
fireSelectEvent(oldValue,newValue);
}
}, 100L); // brief delay in case someone is typing fast
}
} catch (NumberFormatException e) {
field.setForeground(Color.RED);
}
}
});
}));

this.add(label, BorderLayout.LINE_START);
this.add(field, BorderLayout.LINE_END);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.marginallyclever.donatello.select;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.NumberFormatter;
import java.awt.*;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;


/**
Expand All @@ -20,7 +16,6 @@
public class SelectInteger extends Select {
private JFormattedTextField field;
private int value;
private Timer timer=null;

public SelectInteger(String internalName,String labelKey,Locale locale,int defaultValue) {
super(internalName);
Expand All @@ -37,44 +32,13 @@ public SelectInteger(String internalName,String labelKey,Locale locale,int defau
field.setMinimumSize(d);
field.setValue(defaultValue);
field.setHorizontalAlignment(JTextField.RIGHT);
field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent arg0) {
validate();
field.getDocument().addDocumentListener(new DelayedDocumentValidator(field,newValue-> {
if (value != newValue) {
double oldValue = value;
value = newValue.intValue();
fireSelectEvent(oldValue, newValue);
}

@Override
public void insertUpdate(DocumentEvent arg0) {
validate();
}

@Override
public void removeUpdate(DocumentEvent arg0) {
validate();
}

public void validate() {
try {
int newNumber = Integer.parseInt(field.getText());
field.setForeground(UIManager.getColor("Textfield.foreground"));
if(value != newNumber) {
int oldValue = value;
value = newNumber;

if(timer!=null) timer.cancel();
timer = new Timer("Delayed response");
timer.schedule(new TimerTask() {
public void run() {
fireSelectEvent(oldValue,newNumber);
}
}, 100L); // brief delay in case someone is typing fast
}
} catch(NumberFormatException e1) {
field.setForeground(Color.RED);
return;
}
}
});
}));

this.add(label,BorderLayout.LINE_START);
this.add(field,BorderLayout.LINE_END);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.marginallyclever.donatello.simpleparser;

import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import net.objecthunter.exp4j.function.Function;

/**
* A simple parser that evaluates a mathematical expression. Supports the use of the constant PI and the hypot function.
*/
public class SimpleParser {
private static final Function hypot = new Function("hypot",2) {
@Override
public double apply(double... doubles) {
return Math.hypot(doubles[0], doubles[1]);
}
};

public static double evaluate(String expression) {
Expression e = new ExpressionBuilder(expression)
.variables("PI") // Declare the variable
.function(hypot)
.build()
.setVariable("PI", Math.PI); // Assign value to variable
return e.evaluate();
}
}
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
requires org.reflections;
requires com.formdev.flatlaf;
requires java.prefs;
requires exp4j;

exports com.marginallyclever.donatello;
exports com.marginallyclever.donatello.actions.undoable;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.marginallyclever.donatello.simplerparser;

import com.marginallyclever.donatello.simpleparser.SimpleParser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class SimpleParserTest {
@Test
public void test() {
Assertions.assertEquals(-12,SimpleParser.evaluate("3 + 5 * (2 - 8) / 2.0"));
Assertions.assertEquals(1,SimpleParser.evaluate("sin(PI / 2)")); // 1.0
Assertions.assertEquals(1,SimpleParser.evaluate("cos(0)")); // 1.0
Assertions.assertEquals(0.999999,SimpleParser.evaluate("tan(PI / 4)"),1e-6);
Assertions.assertEquals(0.7615941559557649,SimpleParser.evaluate("tanh(1)"),1e-6); // Hyperbolic tangent
Assertions.assertEquals(5,SimpleParser.evaluate("hypot(3, 4)")); // Pythagorean theorem
Assertions.assertEquals(5,SimpleParser.evaluate("sqrt(25)"));
Assertions.assertEquals(-2,SimpleParser.evaluate("-5 + 3"));
Assertions.assertEquals(1,SimpleParser.evaluate("10 % 3"));
Assertions.assertEquals(57.29577951308232,SimpleParser.evaluate("(180 / PI)"),1e-6); // Convert to degrees
Assertions.assertEquals(0.017453292519943295,SimpleParser.evaluate("(PI / 180)"),1e-6); // Convert to radians
Assertions.assertEquals(3,SimpleParser.evaluate("floor(PI)"));
Assertions.assertEquals(4,SimpleParser.evaluate("ceil(PI)"));
Assertions.assertThrows(IllegalArgumentException.class,()->SimpleParser.evaluate("random"));
}
}

0 comments on commit 2281244

Please sign in to comment.