Basically, I'm trying to make a Simplifier that can take inputs in the form of a String
and print out the solution step-by-step. Now that I've got it working, I think it has some cleanliness issues.
public static void test(String eqn) {
char[] order = { '^', '*', '/', '+'}; //order of precendece
int found, i, j;
eqn = eqn.replace("-", "+-"); //Subtracting = Adding (-)ve number.
eqn = eqn.replace(" ", ""); //Spaces can cause a lot of trouble.
String eqnC, left, right;
System.out.println("=>\t"+eqn);
for(char op : order) {
while ((found = eqn.indexOf(op)) > -1) {
left = eqn.substring(0, found);
right = eqn.substring(found + 1);
//keep 'walking' up and down the String till a non-number encoutered.
for( i = left.length() - 1; i >= 0; i--)
if(!isPONumber(left.charAt(i)))
break;
for( j = 0; j < right.length(); j++)
if(!isPONumber(right.charAt(j)))
break;
double lV = Double.parseDouble(left.substring(++i)); //left operand
double rV = Double.parseDouble(right.substring(0, j)); //right operand
eqn = left.substring(0, i) + putV(lV, rV, op) + right.substring(j);
System.out.println("=\t" + eqn);
}
}
}
private static String putV(double a, double b, char op) {
switch(op) {
case '^': return Math.pow(a, b) + "";
case '/': return (a / b) + "";
case '*': return (a * b) + "";
case '+': return (a + b) + "";
}
return null;
}
/*
* Limit (for now) : decimal support.
*/
private static boolean isPONumber(char c) {
return Character.isDigit(c) || c == '.' || c == '-';
}
Here are some outputs:
=> 2*84/6+7^2
= 2*84/6+49.0
= 168.0/6+49.0
= 28.0+49.0
= 77.0
=> 3^3/3^2
= 27.0/3^2
= 27.0/9.0
= 3.0
=> 400^0.5
= 20.0
I'd be grateful if anyone would comment and criticize and help me to clean-up my code. I also invite suggestions for making it better, improvising it and so on.
-
\$\begingroup\$ @Jamal Thanks for the edit... (I see that you had edited my previous post as well, thanks for that too) :-) \$\endgroup\$Hungry Blue Dev– Hungry Blue Dev2014年03月19日 05:33:50 +00:00Commented Mar 19, 2014 at 5:33
-
3\$\begingroup\$ No problem! Feel free to change something back if an edit introduces misleading information (I don't think that's the case here). \$\endgroup\$Jamal– Jamal2014年03月19日 05:36:40 +00:00Commented Mar 19, 2014 at 5:36
2 Answers 2
I love the replacement of -
with +-
. It's simple and effectful. I'd like to see a similar use of *
and /
by introducing an operator for the reciprocal, e.g. ~
. This would of course require using a custom function for parsing the numbers instead of parseDouble
(or the introduction of real unary operators), so that may cancel out any advantages.
EDIT: Just like a - b
is equal to a + -b
, a / b
is equal a * 1/b
. 1/b
is called the reciprocal of b
. You could define an operator (such as ~
or any other character, that you aren't using) to represent the reciprocal, and replace /
with *~
, so that 4/2
becomes 4*~2
. That requires you to add ~
as a valid character to isPONumber
, and replace parseDouble
since it doesn't know what ~
is (I just invented it). See next edit.
I'm not really a fan of using parseDouble
directly anyway. I'd at least replace it with a custom function that possibly uses parseDouble
internally.
EDIT: By using parseDouble
you are saying "my programm uses the same syntax for number literals as Java". That is not a problem, until you realize that the Java syntax may not support every thing you need, for example because you just decided to support the reciprocal operator, or you want the program to be international and some countries use a comma instead of a period for the decimal mark, e.g. 1,2
instead of 1.2
, but parseDouble
doesn't support this. And instead of needing to search your whole code and replace each occurance of parseDouble
, you should have one function
static public double parseNumber(String number) {
return Double.parseDouble(number);
}
Now if you need to change the syntax of your numbers, then you only have one defined place where you need to do it. For example to support the reciprocal:
static public double parseNumber(String number) {
if (number.charAt(0) == '~') {
return 1 / parseNumber(number.substring(1));
}
return Double.parseDouble(number);
}
(END EDIT)
I also don't like the use of the variable names i
and j
here, since they are used outside the for
loop. Something like leftOffset
/rightOffset
may be more appropriate.
You could drop the variables left
and right
and search the original string with i
and j
starting at found
/found + 1
.
The use of a enum
for the operators would also be useful. Something like:
interface DoubleOperation {
public double calc(double a, double b);
}
enum Operator {
POWER("^", new DoubleOperation() { public double calc(double a, double b) { return Math.pow(a, b); } }),
DIVISION("/", new DoubleOperation() { public double calc(double a, double b) { return a / b; } });
// ...
public String op; // Use private and a getter in real code
public DoubleOperation calc;
Operator(String op, DoubleOperation calc) {
this.op = op;
this.calc = calc;
}
}
(Look into the just released JDK 8 for lamba expressions to simplify this.)
-
\$\begingroup\$ And what do you mean by similar use of * and / by introducing an operator for the reciprocal? Could you please elaborate? \$\endgroup\$Hungry Blue Dev– Hungry Blue Dev2014年03月19日 14:04:05 +00:00Commented Mar 19, 2014 at 14:04
-
1\$\begingroup\$ Don't dismiss rofl's answer. All his remarks are correct too (except regarding the scope of
i
/j
). My remarks are just additions. I'll do some edits to expand on your comments. \$\endgroup\$RoToRa– RoToRa2014年03月19日 14:19:10 +00:00Commented Mar 19, 2014 at 14:19 -
\$\begingroup\$ No, I didn't dismiss off @rofl's answer, I applied his
ArrayList<>
approach. So I guess it's Bye bye offset! But how do I apply theenum
? Should I use an enhancedfor-loop
for this? \$\endgroup\$Hungry Blue Dev– Hungry Blue Dev2014年03月21日 03:25:42 +00:00Commented Mar 21, 2014 at 3:25 -
\$\begingroup\$ Also (after reading your approach about reciprocals) is it really efficient? I mean, we'll have to divide it anyway... why add [extra] overhead? \$\endgroup\$Hungry Blue Dev– Hungry Blue Dev2014年03月21日 03:28:48 +00:00Commented Mar 21, 2014 at 3:28
-
\$\begingroup\$ No. Forget the reciprocals. It was just a wild idea that jumped into my head :-) \$\endgroup\$RoToRa– RoToRa2014年03月21日 12:45:01 +00:00Commented Mar 21, 2014 at 12:45
Algorithm
The algorithm works well enough. You have captured the essence of the problem in your code, and reading your code it is apparent what it does, and why.
This is a good thing. It is not very often that the intent of the code is so easy to discern.
The draw-back of your algorithm is that it is not going to be able to support the more esoteric operations like parenthesis e.g. ( 123 - 45 ) ^ 2
.... That sort of support will require a preprocessing step.
Keeping the equation as a string is convenient for some things, but i would have preferred to see the process broken down in to components, for example, a parse step could break the input down to List<String> eqnParts = parse(eqn)
. at the same time, it could be trimming the white-space. Then, you just need to scan the List<String>
and find 3 items separated by the highest precedence operator, and swap all three with the resulting value.
Code Style
You should read through the Code Style Guidelines for Java.
This code, for example, has a number of faults:
for( i = left.length() - 1; i >= 0; i--) if(!isPONumber(left.charAt(i))) break; for( j = 0; j < right.length(); j++) if(!isPONumber(right.charAt(j))) break;
It should rather look something like:
for (int i = left.length() - 1; i >= 0; i--) {
if(!isPONumber(left.charAt(i))) {
break;
}
}
for (int j = 0; j < right.length(); j++) {
if(!isPONumber(right.charAt(j))) {
break;
}
}
Notes:
- declare
int i
andint j
inside the loop. - space after the keyword
for
. - braces even for '1-liner' conditional statements.
Double.parseDouble
Double.parseDouble()
throws the unchecked exception NumberFormatException
.
The exception thrown does not give great deals of information about why the number is not valid.
I always recommend wrapping Double.parseDouble (and Integer.parseInteger()
, etc.) in a try-catch that reports what value was parsed, as well as the exception that was thrown. Otherwise debugging is a pain.
I often have a helper function like:
public static final double parseDouble(String value) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException("Unable to Double.parseDouble(" + value + ")", nfe);
}
}
-
\$\begingroup\$ This is only a part of the real thing... I'll add the ability to detect brackets later. See this to understand what I mean. \$\endgroup\$Hungry Blue Dev– Hungry Blue Dev2014年03月19日 07:00:27 +00:00Commented Mar 19, 2014 at 7:00
-
\$\begingroup\$ I forgot to say thanks... As for the
NumberFormatException
, should I define a customException
likeEquationError
or something? \$\endgroup\$Hungry Blue Dev– Hungry Blue Dev2014年03月19日 07:04:03 +00:00Commented Mar 19, 2014 at 7:04 -
\$\begingroup\$ @ambigram_maker Normally what I have is a helper function. I edited my answer to give an example. \$\endgroup\$rolfl– rolfl2014年03月19日 07:10:40 +00:00Commented Mar 19, 2014 at 7:10
-
3\$\begingroup\$ @rolfl If he declares
i, j
in the for loop the program won't compile. He uses them in the next two lines. \$\endgroup\$abuzittin gillifirca– abuzittin gillifirca2014年03月19日 07:27:55 +00:00Commented Mar 19, 2014 at 7:27 -
1\$\begingroup\$ @ambigram_maker why not add a throws new Exception("more information") in the catch block, then? \$\endgroup\$Vogel612– Vogel6122014年03月19日 08:12:40 +00:00Commented Mar 19, 2014 at 8:12