...
 
Commits (29)
......@@ -239,10 +239,22 @@ By default, SI and IEC prefixes are supported.
Not just plain values but whole expressions are supported.
a a = 5
b b = 10
a a=5 = 5
b b=10 = 10
c c=a*sqrt(b) meter to inch = 622.4956...
Also combined units are being supported.
#1 5km/h to in/sec = 10.9361...
#2 12 l/sec/m^2 in l/hour/sqin = 27.8709...
There is also an automatic conversion from a unit with a prefix to a unit
without one, if no target unit is specified.
#1 1km = 1000
#2 1km/h = 1000
#### Custom units files
Additional units can be specified in a file and passed to jMathPaper with the
......@@ -369,12 +381,17 @@ be entered instead of expressions:
:bd
copy Copies the current paper to copy, cp, y
the clipboard.
the clipboard by default. Accepts
a type and a range.
new Creates a new paper. new, :new
next Switches to the next paper in next, right,
the list, if there is any. :bnext, :bn
note Allows to edit the current note. note
Accepts the operation and parameters.
open Opens a paper from the given path. open, :e
The path to open the files is expected
as parameter to this command. If
......@@ -411,6 +428,66 @@ Parameters are to be provided space separated and can be quoted, some examples:
open ./relative\ path\ with\ spaces/file.jmathpaper
open a.jmathpaper b.jmathpaper c.jmathpaper
#### copy
The copy command accepts additional parameters, namely a part and a range. If
no parameters are provided the whole paper will be copied, if parameters are
provided only the set parts of the given lines will be copied.
copy [part] [range]
The types available are:
Part Description Aliases
-----------------------------------------------------------------------
expression The expression of the selected lines. expression, exp
id The ID of the selected lines. id
line The whole lines. line
paper The whole paper. paper
result The result of the selected lines. result, res
The range can either be nothing (for all), a single ID or index (starting at 1),
a comma separated list of ID's or indexes or a range specified with two dots.
copy abc
copy 4
copy expression #3,abc,#7
copy result 3..abc
#### note
The note command accepts additional parameters, namely the action and
the associated parameters.
note action [index] line
The actions are:
Action Description Aliases
-----------------------------------------------------------------------
add Adds the given value as separate line add, append
to the notes.
clear Clears the whole note. clear, clr, cls,
reset
delete Deletes the line with the given 1-based delete, del,
index. remove, rem
insert Inserts the line at the given 1-based insert, ins
index.
Some examples.
note add New line.
note insert 1 Inserted before the first line.
note delete 3
note clear
### Options
There are various options available to change how jMathPaper behaves, they can
......
......@@ -21,8 +21,8 @@ package org.bonsaimind.jmathpaper;
import java.nio.file.Path;
import org.bonsaimind.jmathpaper.core.ConfigurationProcessor;
import org.bonsaimind.jmathpaper.core.configuration.Configuration;
import org.bonsaimind.jmathpaper.core.configuration.ConfigurationProcessor;
import org.bonsaimind.jmathpaper.core.configuration.Definitions;
import org.bonsaimind.jmathpaper.core.resources.ResourceLoader;
import org.bonsaimind.jmathpaper.core.ui.Ui;
......
......@@ -76,7 +76,7 @@ public class Paper {
}
public void evaluateFromText(String text) throws InvalidExpressionException {
evaluateLines(Arrays.asList(text.split("\\n")));
evaluateLines(Arrays.asList(text.split("\\r?\\n")));
}
public void evaluateLines(List<String> lines) throws InvalidExpressionException {
......@@ -142,7 +142,7 @@ public class Paper {
}
public int getPrecision() {
return evaluator.getMathContext().getPrecision();
return evaluator.getResultMathContext().getPrecision();
}
public int getResultColumnSize() {
......@@ -150,7 +150,7 @@ public class Paper {
}
public RoundingMode getRoundingMode() {
return evaluator.getMathContext().getRoundingMode();
return evaluator.getResultMathContext().getRoundingMode();
}
public boolean isChanged() {
......@@ -195,7 +195,8 @@ public class Paper {
}
Evaluator newEvaluator = new Evaluator(evaluator);
newEvaluator.setMathContext(evaluator.getMathContext());
newEvaluator.setCalculationMathContext(evaluator.getCalculationMathContext());
newEvaluator.setResultMathContext(evaluator.getResultMathContext());
List<EvaluatedExpression> newEvaluatedExpressions = new ArrayList<>();
......@@ -247,7 +248,7 @@ public class Paper {
if (format.contains("?")) {
StringBuilder builder = new StringBuilder();
for (int counter = 0; counter < evaluator.getMathContext().getPrecision(); counter++) {
for (int counter = 0; counter < evaluator.getResultMathContext().getPrecision(); counter++) {
builder.append("#");
}
......@@ -255,23 +256,35 @@ public class Paper {
}
numberFormat = new DecimalFormat(format);
numberFormat.setRoundingMode(evaluator.getMathContext().getRoundingMode());
numberFormat.setRoundingMode(evaluator.getResultMathContext().getRoundingMode());
}
public void setPrecision(int precision) {
evaluator.setMathContext(new MathContext(
if (precision <= 0) {
evaluator.setCalculationMathContext(new MathContext(
precision,
evaluator.getCalculationMathContext().getRoundingMode()));
} else {
evaluator.setCalculationMathContext(new MathContext(
Math.max(precision * 2, 4),
evaluator.getCalculationMathContext().getRoundingMode()));
}
evaluator.setResultMathContext(new MathContext(
precision,
evaluator.getMathContext().getRoundingMode()));
evaluator.getResultMathContext().getRoundingMode()));
setNumberFormat(originalNumberFormat);
}
public void setRoundingMode(RoundingMode roundingMode) {
evaluator.setMathContext(new MathContext(
evaluator.getMathContext().getPrecision(),
evaluator.setCalculationMathContext(new MathContext(
evaluator.getCalculationMathContext().getPrecision(),
roundingMode));
evaluator.setResultMathContext(new MathContext(
evaluator.getResultMathContext().getPrecision(),
roundingMode));
numberFormat.setRoundingMode(evaluator.getMathContext().getRoundingMode());
numberFormat.setRoundingMode(evaluator.getResultMathContext().getRoundingMode());
}
@Override
......
......@@ -17,7 +17,7 @@
* Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.bonsaimind.jmathpaper.core;
package org.bonsaimind.jmathpaper.core.configuration;
import java.io.BufferedReader;
import java.io.FileInputStream;
......
......@@ -19,10 +19,13 @@
package org.bonsaimind.jmathpaper.core.resources;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import org.bonsaimind.jmathpaper.core.ConfigurationProcessor;
import org.bonsaimind.jmathpaper.core.configuration.ConfigurationProcessor;
/**
* {@link ResourceLoader} is a static utility for loading embedded resources.
......@@ -66,6 +69,28 @@ public final class ResourceLoader {
processResource(relativePath, lineProcessor, null);
}
public static final String readResource(String relativePath) {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
ResourceLoader.class.getResourceAsStream(relativePath)))) {
String line = reader.readLine();
while (line != null) {
content.append(line);
content.append("\n");
line = reader.readLine();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return content.toString();
}
/**
* Iterates over each line of the given resource.
* <p>
......
Basics
======
When you start jMathPaper you are greeted with a (currently empty) list of previous expressions and the input box to enter a new one. You can enter any expression you want to evaluate. Additionally, there is a larger input are to the right which allows you to take notes.
You can save and load papers from the menu, they are stored in a very simple cleartext format, so it is easy to create and edit them without jMathPaper. Examples can be found in the examples directory.
Let us start with something simple and enter a few simple expressions:
ID Expression Result
--------------------------------------------------
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
One of the big features of jMathPaper is that you can easily reference
a previous result by using its ID, like this:
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
#4 #2*25 = 625
We can also define variables which we can reference:
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
#4 #2*25 = 625
length length=10 = 10
width width=5 = 5
area area=length*width = 50
Now if we made a mistake, for example the width really is 7, we can simply press
up and down to scroll through the history and execute the corrected expression
again. The previously executed expressions do not change, so we need to evaluate
them again:
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
#4 #2*25 = 625
length length=10 = 10
width width=5 = 5
area area=length*width = 50
width width=7 = 7
area area=length*width = 70
We can also use mathematical functions, like `sin`:
#1 sin(70) = 0.9396926
See the documentation of [EvalEx](https://github.com/uklimaschewski/EvalEx/blob/master/README.md) to see what functions are supported.
jMathPaper
==========
This is a simple calculator application which aims to provide a similar experience like solving equations by taking notes on a sheet of paper. By coincidence it also works similar to spreadsheet applications, but with being limited to a mere list.
You can access further help topics with the `help` command, for example by typing
help basics
into the input field.
main.markdown
basics/main.markdown
\ No newline at end of file
......@@ -15,19 +15,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Regular Expression for finding a function definition and expression.
# Regular Expression for finding the separation index of an expression and
# the unit.
#
# Matched samples:
# 1+1*4 m to in
^
(?<EXPRESSION>
.*?
)
( )*
(?<FROM>
([a-zA-Z]+)
(\^[0-9]+)?
)
$ # End of string.
\ No newline at end of file
(
[0-9)] *[a-zA-Z]
|[0-9] +1
|[a-zA-Z)] +[a-zA-Z]
|[a-zA-Z)] *1
)
\ No newline at end of file
#
# Copyright 2018, Robert 'Bobby' Zenz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Regular Expression for finding a function definition and expression.
#
# Matched samples:
# 1+1*4 m to in
^
(?<EXPRESSION>
.*?
)
( )*
(?<FROM>
([a-zA-Z]+|[a-zA-Z]*1)
(\^[0-9]+)?
)
(
( to )
|( in )
|( as )
|( )
)
(?<TO>
([a-zA-Z]+|[a-zA-Z]*1)
(\^[0-9]+)?
)
$ # End of string.
\ No newline at end of file
......@@ -49,7 +49,7 @@ rood 10890sqft
acre 43560sqft
# Imperial volume, https://en.wikipedia.org/wiki/Imperial_units#Volume
fluidounce 28.4130625ml
fluidounce 28.4130625mlitre
gill 5floz
pint 20floz
quart 40floz
......@@ -62,6 +62,14 @@ ounce 28.349523125g
# Imperial pressure
poundspersquareinch 6.894757kPa
# US volume, https://en.wikipedia.org/wiki/Fluid_ounce
usfluidounce 29.5735295625mlitre
usgill 4usfloz
uscup 8usfloz
uspint 16usfloz
usquart 32usfloz
usgallon 128usfloz
# Astronomy length
earthradius 6371km
lunardistance 384402km
......
......@@ -70,6 +70,14 @@ span 1
cubit 1
ell 1
# US volume, https://en.wikipedia.org/wiki/Fluid_ounce
usfluidounce 3 usfloz
usgill 3 usgi
uscup 3 uscp
uspint 3 uspt
usquart 3 usqt
usgallon 3 usgal
# Astronomy length
earthradius 1 RE
lunardistance 1 LD
......@@ -131,10 +139,10 @@ reaumur 1 Re,°Re
romer 1 Ro,°Ro
# Time
second 1 sec
second 1 s,sec
minute 1 min
hour 1
day 1
hour 1 h
day 1 d
week 1
month 1
year 1
......
package org.bonsaimind.jmathpaper.core.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bonsaimind.jmathpaper.core.resources.ResourceLoader;
public class Topic {
protected String content = null;
protected String name = null;
protected Topic parent = null;
protected String title = null;
protected List<Topic> topics = new ArrayList<>();
protected Map<String, Topic> topicsByName = new HashMap<>();
private List<Topic> topicsReadonly = null;
public Topic(String name, String content) {
super();
this.name = name;
this.content = content;
updateTitle();
}
public static final Topic buildFrom(String relativeResourcePackage) {
Topic rootNode = new Topic("", "");
ResourceLoader.processResource(relativeResourcePackage + "/topics.index", (line) -> {
String path = line;
if (path.endsWith(".markdown")) {
path = path.substring(0, path.length() - 9);
}
if (path.endsWith("/main")) {
path = path.substring(0, path.length() - 5);
} else {
path = "";
}
rootNode.addTopic(path, ResourceLoader.readResource(relativeResourcePackage + "/" + line));
});
return rootNode;
}
public void addTopic(String path, String content) {
Topic topic = getTopic(path);
if (topic == this) {
this.content = content;
updateTitle();
} else if (topic != null) {
topic.content = content;
topic.updateTitle();
} else {
String targetPath = "";
String name = path;
if (name.endsWith("/")) {
name = name.substring(0, name.length() - 1);
}
int lastSlashIndex = name.lastIndexOf("/");
if (lastSlashIndex >= 0) {
targetPath = name.substring(0, lastSlashIndex);
name = name.substring(lastSlashIndex + 1);
}
addTopic(targetPath, new Topic(name, content));
}
}
public void addTopic(String path, Topic topic) {
Topic parent = getTopic(path);
if (parent == null) {
parent = this;
for (String name : path.split("/")) {
if (!name.isEmpty()) {
Topic subTopic = parent.getTopic(name);
if (subTopic == null) {
subTopic = new Topic(name, "");
parent.addTopic(subTopic);
}
parent = subTopic;
}
}
}
Topic alreadyExistingTopic = parent.getTopic(topic.getName());
if (alreadyExistingTopic != null) {
topic.addTopics(alreadyExistingTopic.getTopics());
}
parent.addTopic(topic);
}
public void addTopic(Topic topic) {
if (topic == null) {
return;
}
topics.add(topic);
Topic previousTopic = topicsByName.put(topic.getName(), topic);
topic.setParent(this);
if (previousTopic != null) {
previousTopic.setParent(null);
topics.remove(previousTopic);
}
}
public void addTopics(Iterable<Topic> topics) {
if (topics == null) {
return;
}
for (Topic topic : topics) {
addTopic(topic);
}
}
public String getContent() {
return content;
}
public String getName() {
return name;
}
public Topic getParent() {
return parent;
}
public Topic getRoot() {
Topic ancestor = this;
while (ancestor.getParent() != null) {
ancestor = ancestor.getParent();
}
return ancestor;
}
public String getTitle() {
return title;
}
public Topic getTopic(String pathOrName) {
if (pathOrName == null || pathOrName.isEmpty()) {
return this;
}
Topic topic = topicsByName.get(pathOrName);
if (topic == null && pathOrName.contains("/")) {
topic = this;
for (String name : pathOrName.split("/")) {
if (!name.isEmpty()) {
topic = topic.getTopic(name);
if (topic == null) {
return null;
}
}
}
}
return topic;
}
public List<Topic> getTopics() {
if (topicsReadonly == null) {
topicsReadonly = Collections.unmodifiableList(topics);
}
return topicsReadonly;
}
@Override
public String toString() {
return name;
}
protected void setParent(Topic topic) {
parent = topic;
}
protected void updateTitle() {
title = name;
if (content != null) {
int newlineIndex = content.indexOf("\n");
if (newlineIndex > 0) {
title = content.substring(0, newlineIndex);
} else if (newlineIndex < 0) {
title = content;
}
}
}
}
......@@ -38,18 +38,20 @@ public enum Command {
/** All papers will be closed. */
CLOSEALL("closeall"),
/**
* The current paper will be copied to the clipboard in its text
* representation.
*/
/** The specified part will be copied to the clipboard. */
COPY("copy", "cp", "y"),
HELP("help", "man", "manual"),
/** Start a new paper. */
NEW("new", ":new"),
/** Switches to the next paper, if there is any. */
NEXT("next", "right", ":bnext", ":bn"),
/** Edits the note. */
NOTE("note"),
/** Opens the given paper. */
OPEN("open", ":e"),
......
/*
* Copyright 2018, Robert 'Bobby' Zenz
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>
* or write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.bonsaimind.jmathpaper.core.ui;
/**
* Defines an action being taken on a note.
*/
public enum NoteAction {
/** Adds a line to the end of the note. */
ADD("add", "append"),
/** Clears the whole note. */
CLEAR("clear", "clr", "cls", "reset"),
/** Deletes the line with the given index. */
DELETE("delete", "del", "remove", "rem"),
/** Inserts a line at the given index. */
INSERT("insert", "ins");
private String[] aliases = null;
private NoteAction(String... aliases) {
this.aliases = aliases;
}
public static NoteAction getNoteAction(String name) {
if (name == null || name.length() == 0) {
return null;
}
for (NoteAction paperPart : values()) {
for (String alias : paperPart.aliases) {
if (name.equalsIgnoreCase(alias)) {
return paperPart;
}
}
}
return null;
}
}
/*
* Copyright 2018, Robert 'Bobby' Zenz
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>
* or write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.bonsaimind.jmathpaper.core.ui;
/**
* Defines the part of a paper.
*/
public enum PaperPart {
/** The expression of a line/expression. */
EXPRESSION("expression", "exp"),
/** The ID of a line/expression. */
ID("id"),
/** The whole line-expression. */
LINE("line"),
/** The whole paper. */
PAPER("paper"),
/** The result of a line/expression. */
RESULT("result", "res");
private String[] aliases = null;
private PaperPart(String... aliases) {
this.aliases = aliases;
}
public static PaperPart getPaperPart(String name) {
if (name == null || name.length() == 0) {
return null;
}
for (PaperPart paperPart : values()) {
for (String alias : paperPart.aliases) {
if (name.equalsIgnoreCase(alias)) {
return paperPart;
}
}
}
return null;
}
}
/*
* Copyright 2018, Robert 'Bobby' Zenz
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>
* or write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.bonsaimind.jmathpaper.core.units;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link CompoundUnit} is a combination of two or more unit of measurements.
* For example all units of speed are a compound unit of length and time.
*/
public class CompoundUnit {
protected List<Token> tokens = new ArrayList<>();
private String cachedStringValue = null;
private List<Token> readonlyTokens = null;
/**
* Creates a new instance of {@link CompoundUnit}.
*
* @param tokens The {@link List} of {@link Token}s which makes up this
* {@link CompoundUnit}.
*/
public CompoundUnit(List<Token> tokens) {
super();
this.tokens.addAll(tokens);
}
/**
* Returns a {@link CompoundUnit} derived from this one with all units at
* their base, meaning with the {@link Prefix#BASE}.
*
* @return A {@link CompoundUnit} derived from this one wtih all units at
* their base.
*/
public CompoundUnit atBase() {
List<Token> tokensAtBase = new ArrayList<>();
for (Token token : tokens) {
if (token.getTokenType() == TokenType.UNIT) {
tokensAtBase.add(new Token(
token.getValue(),
token.getTokenType(),
token.getUnit().atBase()));
} else {
tokensAtBase.add(token);
}
}
return new CompoundUnit(tokensAtBase);
}
/**
* Gets the {@link List} of {@link Token}s which makes up this
* {@link CompoundUnit}.
*
* @return The {@link List} of {@link Token}s which makes up this
* {@link CompoundUnit}.
*/
public List<Token> getTokens() {
if (readonlyTokens == null) {
readonlyTokens = Collections.unmodifiableList(tokens);
}
return readonlyTokens;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
if (cachedStringValue == null) {
StringBuilder stringValue = new StringBuilder();
for (Token token : tokens) {
if (token.getTokenType() == TokenType.UNIT) {
stringValue.append(token.getUnit().toString());
} else {
stringValue.append(token.getValue());
}
}
cachedStringValue = stringValue.toString();
}
return cachedStringValue;
}
/**
* The {@link Token} represents a single part of a compound unit.
*/
public static class Token {
protected TokenType tokenType = null;
protected PrefixedUnit unit = null;
protected String value = null;
/**
* Creates a new instance of {@link Token}.
*
* @param value The original {@link String} value.
* @param tokenType The {@link TokenType}.
* @param unit The {@link Unit} this {@link Token} represents, if any.
*/
public Token(String value, TokenType tokenType, PrefixedUnit unit) {
super();
this.value = value;
this.tokenType = tokenType;
this.unit = unit;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Token other = (Token)obj;
if (tokenType != other.tokenType) {
return false;
}
if (unit == null) {
if (other.unit != null) {
return false;
}
} else if (!unit.equals(other.unit)) {
return false;
}
if (value == null) {
if (other.value != null) {
return false;
}
} else if (!value.equals(other.value)) {
return false;
}
return true;
}
/**
* Gets the {@link TokenType}.
*
* @return The {@link TokenType}
*/
public TokenType getTokenType() {
return tokenType;
}
/**
* Gets the {@link PrefixedUnit}.
*
* @return The {@link PrefixedUnit}
*/
public PrefixedUnit getUnit() {
return unit;
}
/**
* Gets the actual {@link String} value.
*
* @return The actual {@link String} value.
*/
public String getValue() {
return value;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((tokenType == null) ? 0 : tokenType.hashCode());
result = prime * result + ((unit == null) ? 0 : unit.hashCode());
result = prime * result + ((value == null) ? 0 : value.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
if (tokenType == TokenType.OPERATOR) {
return tokenType.name() + ": " + value;
} else {
return tokenType.name() + ": " + value + " (" + unit + ")";
}
}
}
/**
* The {@link TokenType} represents the type of a single token.
*/
public static enum TokenType {
/** The token is an operator, for example a dash "{@code /}". */
OPERATOR,
/** The token represents a unit. */
UNIT;
}
}
......@@ -56,6 +56,17 @@ public class PrefixedUnit {
this.unit = unit;
}
/**
* A {@link PrefixedUnit} derived from this one with the {@link Prefix#BASE}
* .
*
* @return A {@link PrefixedUnit} derived from this one with the
* {@link Prefix#BASE}.
*/
public PrefixedUnit atBase() {
return new PrefixedUnit(Prefix.BASE, unit);
}
/**
* {@inheritDoc}
*/
......
package org.bonsaimind.jmathpaper.uis.swt;
import org.bonsaimind.jmathpaper.core.support.Topic;
import org.bonsaimind.jmathpaper.uis.swt.events.ForwardingSelectionListener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
public class HelpComponent extends SashForm {
private StyledText contentText = null;
private Tree topicsTree = null;
public HelpComponent(Composite parent, int style) {
super(parent, style);
topicsTree = new Tree(this, SWT.BORDER);
topicsTree.addSelectionListener(new ForwardingSelectionListener(this::onTopicsTreeSelectionChanged));
contentText = new StyledText(this, SWT.BORDER | SWT.H_SCROLL | SWT.MULTI | SWT.V_SCROLL | SWT.WRAP);
contentText.setAlwaysShowScrollBars(false);
contentText.setEditable(false);
contentText.setWordWrap(true);
setWeights(new int[] { 1, 2 });
}
public void showHelp(Topic topic) {
rebuildTree(topic.getRoot());
if (topicsTree.getItemCount() > 0) {
TreeItem treeItem = topicsTree.getItem(0);
if (treeItem.getData() != topic) {
treeItem = findTreeItem(topic, topicsTree.getItem(0));
}
if (treeItem != null) {
topicsTree.select(treeItem);
}
}
onTopicsTreeSelectionChanged();
}
private void attachTopics(Topic topic, TreeItem parentTreeItem) {
if (topic == null) {
return;
}
TreeItem treeItem = setupTreeItem(new TreeItem(parentTreeItem, SWT.NONE), topic);
for (Topic subStopic : topic.getTopics()) {
attachTopics(subStopic, treeItem);
}
treeItem.setExpanded(true);
}
private TreeItem findTreeItem(Topic topic, TreeItem currentTreeItem) {
for (TreeItem treeItem : currentTreeItem.getItems()) {
if (treeItem.getData() == topic) {
return treeItem;
} else {
TreeItem foundTreeItem = findTreeItem(topic, treeItem);
if (foundTreeItem != null) {
return foundTreeItem;
}
}
}
return null;
}
private void onTopicsTreeSelectionChanged() {
if (topicsTree.getSelectionCount() > 0) {
TreeItem selectedTreeItem = topicsTree.getSelection()[0];
Topic selectedTopic = (Topic)selectedTreeItem.getData();
contentText.setText(selectedTopic.getContent());
}
}
private void rebuildTree(Topic topic) {
topicsTree.removeAll();
TreeItem treeItem = setupTreeItem(new TreeItem(topicsTree, SWT.NONE), topic);
for (Topic subStopic : topic.getTopics()) {
attachTopics(subStopic, treeItem);
}
treeItem.setExpanded(true);
}
private TreeItem setupTreeItem(TreeItem treeItem, Topic topic) {
treeItem.setData(topic);
treeItem.setText(topic.getTitle());
return treeItem;
}
}
......@@ -27,6 +27,7 @@ import java.nio.file.Paths;
import org.bonsaimind.jmathpaper.Version;
import org.bonsaimind.jmathpaper.core.InvalidExpressionException;
import org.bonsaimind.jmathpaper.core.Paper;
import org.bonsaimind.jmathpaper.core.support.Topic;
import org.bonsaimind.jmathpaper.core.ui.AbstractPapersUi;
import org.bonsaimind.jmathpaper.core.ui.UiParameters;
import org.bonsaimind.jmathpaper.uis.swt.events.EventForwarder;
......@@ -47,6 +48,7 @@ import org.eclipse.swt.widgets.Shell;
public class Swt extends AbstractPapersUi {
protected Display display = null;
protected HelpComponent helpComponent = null;
protected Shell shell = null;
private MenuItem clearMenuItem = null;
private MenuItem closeAllMenuItem = null;
......@@ -55,6 +57,7 @@ public class Swt extends AbstractPapersUi {
private Label errorLabel = null;
private FileDialog fileOpenDialog = null;
private FileDialog fileSaveDialog = null;
private Shell helpShell = null;
private MenuItem nextPaperMenuItem = null;
private MenuItem openMenuItem = null;
private int paperCounter = 0;
......@@ -311,6 +314,22 @@ public class Swt extends AbstractPapersUi {
return null;
}
@Override
protected void showHelp(Topic topic) {
if (helpShell == null || helpShell.isDisposed()) {
helpShell = new Shell();
helpShell.setLayout(new GridLayout(1, true));
helpShell.setSize(720, 480);
helpShell.setText("jMathPaper " + Version.CURRENT + " - Help");
helpComponent = new HelpComponent(helpShell, SWT.NONE);
helpComponent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
}
helpComponent.showHelp(topic);
helpShell.setVisible(true);
}
protected void updateCurrentPaper() {
if (cTabFolder.getSelection() != null) {
setPaper(getCurrentPaperComponent().getPaper());
......
......@@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.List;
import org.bonsaimind.jmathpaper.core.evaluatedexpressions.BooleanEvaluatedExpression;
import org.bonsaimind.jmathpaper.core.resources.ResourceLoader;
import org.bonsaimind.jmathpaper.core.ui.AbstractPapersUi;
import org.bonsaimind.jmathpaper.core.ui.CommandExecutionException;
import org.bonsaimind.jmathpaper.core.ui.UiParameters;
......@@ -33,6 +34,7 @@ import org.junit.Before;
import org.junit.Test;
public class TestAbstractPapersUi extends AbstractPapersUi {
private String clipboard = "";
private volatile boolean quitCalled = false;
public TestAbstractPapersUi() {
......@@ -80,6 +82,77 @@ public class TestAbstractPapersUi extends AbstractPapersUi {
assertLastResult(false);
}
@Test
public void testCommandCopy() throws CommandExecutionException, InvalidExpressionException {
process("1+1");
process("2+2");
process("3+3");
process("4+4");
process("5+5");
// Lines by ID
process("copy #1");
assertClipboard("#1 1+1 = 2");
process("copy #1, #2, #3");
assertClipboard("#1 1+1 = 2\n#2 2+2 = 4\n#3 3+3 = 6");
process("copy #1..#3");
assertClipboard("#1 1+1 = 2\n#2 2+2 = 4\n#3 3+3 = 6");
// Lines by index.
process("copy 1");
assertClipboard("#1 1+1 = 2");
process("copy -1");
assertClipboard("#5 5+5 = 10");
process("copy 1, 2, 3");
assertClipboard("#1 1+1 = 2\n#2 2+2 = 4\n#3 3+3 = 6");
process("copy 1..3");
assertClipboard("#1 1+1 = 2\n#2 2+2 = 4\n#3 3+3 = 6");
process("copy 1..-3");
assertClipboard("#1 1+1 = 2\n#2 2+2 = 4\n#3 3+3 = 6");
// IDs
process("copy ID 1..-3");
assertClipboard("#1\n#2\n#3");
// Expression
process("copy exp 1..-3");
assertClipboard("1+1\n2+2\n3+3");
// Result
process("copy res 1..-3");
assertClipboard("2\n4\n6");
// Copy everything
process("copy exp");
assertClipboard("1+1\n2+2\n3+3\n4+4\n5+5");
}
@Test
public void testCommandNote() throws CommandExecutionException, InvalidExpressionException {
process("note add 1");
process("note add 2");
process("note add 3");
process("note add 4");
assertNote("1\n2\n3\n4\n");
process("note insert 1 First");
assertNote("First\n1\n2\n3\n4\n");
process("note delete 3");
assertNote("First\n1\n3\n4\n");
process("note clear");
assertNote("");
}
@Test
public void testCommandQuit() throws CommandExecutionException, InvalidExpressionException {
process("quit");
......@@ -87,6 +160,33 @@ public class TestAbstractPapersUi extends AbstractPapersUi {
Assert.assertTrue(quitCalled);
}
@Test
public void testCompoundUnitConversions() throws CommandExecutionException, InvalidExpressionException {
// Load the defaults
ResourceLoader.processResource("units/iec.prefixes", getPaper().getEvaluator().getUnitConverter()::loadPrefix);
ResourceLoader.processResource("units/si.prefixes", getPaper().getEvaluator().getUnitConverter()::loadPrefix);
ResourceLoader.processResource("units/default.units", getPaper().getEvaluator().getUnitConverter()::loadUnit);
ResourceLoader.processResource("units/default.conversions", getPaper().getEvaluator().getUnitConverter()::loadConversion);
process("1m/sec to km/h");
assertLastResult("3.6");
process("1l/sec/m^2 to usfloz/hour/in^2");
assertLastResult("78.535637590755700991921464362409");
process("1 km/h m/h");
assertLastResult("1000");
process("2 km/h m/h");
assertLastResult("2000");
process("1+1 * 8 / 2 + 10km/h m/h");
assertLastResult("15000");
process("km/h");
assertLastResult("1000");
}
@Test
public void testSplitParameters() {
assertSplitParameters(new String[] {}, null);
......@@ -128,6 +228,42 @@ public class TestAbstractPapersUi extends AbstractPapersUi {
}, " command value ; 1+1; command \"some ; value 1+1\" \\; 2+2");
}
@Test
public void testUnitConversions() throws CommandExecutionException, InvalidExpressionException {
// Load the defaults
ResourceLoader.processResource("units/iec.prefixes", getPaper().getEvaluator().getUnitConverter()::loadPrefix);
ResourceLoader.processResource("units/si.prefixes", getPaper().getEvaluator().getUnitConverter()::loadPrefix);
ResourceLoader.processResource("units/default.units", getPaper().getEvaluator().getUnitConverter()::loadUnit);
ResourceLoader.processResource("units/default.conversions", getPaper().getEvaluator().getUnitConverter()::loadConversion);
process("1km to m");
assertLastResult("1000");
process("1km in m");
assertLastResult("1000");
process("1km as m");
assertLastResult("1000");
process("1km m");
assertLastResult("1000");
process("1km");
assertLastResult("1000");
process("km");
assertLastResult("1000");
}
@Override
protected void copyToClipboard(String value) {
clipboard = value;
}
private final void assertClipboard(String expected) {
Assert.assertEquals(expected, clipboard);
}
private final void assertLastResult(boolean expected) {
EvaluatedExpression lastEvaluatedExpression = paper.evaluatedExpressions.get(paper.evaluatedExpressions.size() - 1);
......@@ -154,6 +290,10 @@ public class TestAbstractPapersUi extends AbstractPapersUi {
}
}
private final void assertNote(String expected) {
Assert.assertEquals(expected, paper.getNotes());
}
private final void assertSplitParameters(String[] expected, String input) {
assertLists(expected, splitParameters(input));
}
......
......@@ -22,6 +22,7 @@ package org.bonsaimind.jmathpaper.core;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import org.bonsaimind.jmathpaper.core.configuration.ConfigurationProcessor;
import org.junit.Assert;
import org.junit.Test;
......
......@@ -64,6 +64,38 @@ public class TestEvaluator extends AbstractExpressionTest {
assertResult("2", "1 + /* Nested // */ 1", new Evaluator());
}
@Test
public void testCompoundUnitConversions() throws InvalidExpressionException {
Evaluator evaluator = new Evaluator();
// Load the defaults
ResourceLoader.processResource("units/iec.prefixes", evaluator.getUnitConverter()::loadPrefix);
ResourceLoader.processResource("units/si.prefixes", evaluator.getUnitConverter()::loadPrefix);
ResourceLoader.processResource("units/default.units", evaluator.getUnitConverter()::loadUnit);
ResourceLoader.processResource("units/default.conversions", evaluator.getUnitConverter()::loadConversion);
ResourceLoader.processResource("other/default.aliases", evaluator::loadAlias);
ResourceLoader.processResource("other/default.context", evaluator::loadContextExpression);
// Basic support
assertResult("2236.9362920544022906227630637079", "1km/sec to ml/h", evaluator);
assertResult("2236.9362920544022906227630637079", "1km/sec in ml/h", evaluator);
assertResult("2236.9362920544022906227630637079", "1km/sec as ml/h", evaluator);
assertResult("2236.9362920544022906227630637079", "1km/sec ml/h", evaluator);
// Variables
evaluator.evaluate("a1=5");
assertResult("5", "a1", evaluator);
assertResult("61", "a1+7 * 8", evaluator);
assertResult("20", "a1 + a1 + a1 + a1", evaluator);
assertResult("20", "a1 + a1 + a1 + a1 1", evaluator);
assertResult("20", "a1 + a1 + a1 + a1 to 1", evaluator);
assertResult("20000", "a1 + a1 + a1 + a1 km to m", evaluator);
assertResult("20000", "a1 + a1 + a1 + a1 km m", evaluator);
// No value
assertResult("43166.4685056", "l/hour/sqm l/minute/sqml", evaluator);
}
@Test
public void testContextExpressions() throws InvalidExpressionException {
Evaluator evaluator = new Evaluator();
......@@ -141,6 +173,8 @@ public class TestEvaluator extends AbstractExpressionTest {
assertResult("123456790", "123456789+1", new Evaluator());
assertResult("123456789123456790", "123456789123456789+1", new Evaluator());
assertResult("1.000000001", "1.000000+0.000000001", new Evaluator());
assertResult("60", "1/(1/60)", new Evaluator());
}
@Test
......@@ -159,6 +193,7 @@ public class TestEvaluator extends AbstractExpressionTest {
assertResult("2.54", "1inch to centimeter", evaluator);
assertResult("2.54", "1in to cm", evaluator);
assertResult("2.54", "1in in cm", evaluator);
assertResult("2.54", "1in as cm", evaluator);
assertResult("2.54", "1in cm", evaluator);
assertResult("1", "1 m to m", evaluator);
......
package org.bonsaimind.jmathpaper.core.help;
import org.bonsaimind.jmathpaper.core.support.Topic;
import org.junit.Assert;
import org.junit.Test;
public class TestHelp {
@Test
public void testInsertAndRetrieve() {
Topic topic = new Topic("", "");
topic.addTopic("/a/b/c", new Topic("d1", "d1"));