Evaluate expressions with significant figures

The number of significant figures that a number has is how many digits it has when written in scientific notation, including zeros at the end if a decimal point is present. For example, 1200 has 2 significant figures because it is 1.2*10^3 but 1200. has 4 significant figures and 1200.0 has 5 significant figures.

When adding two numbers, the result should be rounded to the same number of places as the number whose least significant digit is furthest to the left. For example, 1200 + 3 = 1200 (rounded to the hundreds place since 1200 is rounded to the hundreds place), 1200.01 + 3 = 1203, and 4.59 + 2.3 = 6.9. Note that 5 rounds up. This same rule applies to subtraction. 0 is rounded to the ones place. Note that adding and subtracting do not depend on the number of significant digits. For example, 999 + 2.00 = 1001 because 999 is rounded to the ones place and 2.00 is rounded to the hundredths place; the one rounded to fewer places is 999, so the result, 1001.00, should be rounded to the ones place as well. Similarly, 300 + 1 - 300 is exactly equal to 1, but 300 is rounded to the hundreds place, so the final result should also be rounded to the hundreds place, giving 0. 300. + 1 - 300. would equal 1 on the other hand.

When multiplying or dividing two numbers, round to the number of significant digits of the number with the least significant digits. For example, 3.839*4=20 because the exact value, 15.356, rounds to 20 since 4 has only one significant figure. Similarly, 100/4=30 since both numbers have one significant figure, but 100./4.00=25.0 since both numbers have 3 significant figures. 0 is defined to have 1 significant figure.

Expressions will only contain *, /, +, and -, (and parentheses). Order of operations should be followed and results should be rounded after every operation. If parentheses are left out in a string of additions or subtractions or a string of multiplications and divisions, then round after all operations are completed. For example, 6*0.4*2 = 5 (one significant figure), while 0.4*(2*6)=0.4*10=4 and (6*0.4)*2=2*2=4.

Input: A string, with an expression containing ()*/+- and digits. To simplify things, - will only be used as a subtraction operator, not to signify negative numbers; answers, however, could still be negative and would require - as a prefix.

Output: The result of the expression, evaluated and rounded to the correct number of digits. Note that 25 is incorrect for 25.0.

Test cases:

3 + 0.5 --> 4
25.01 - 0.01 --> 25.00
4*7*3 --> 80
(4*7)*3 --> 90
(8.0 + 0.5)/(2.36 - 0.8 - 0.02) --> 5.7
6.0 + 4.0 --> 10.0
5.0 * 2.0 --> 10.0
1/(2.0 * (3.0 + 5.0)) --> 0.06
0.0020 * 129 --> 0.26
300 + 1 - 300 --> 0
0 - 8.8 --> -9
3*5/2*2 --> 20

Edge case: Consider the problem of 501*2.0. The exact value is 1002. Printing 1002 gives too many significant figures (4, when we need 2) but 1000 gives too few (1, when we need 2). In this case, your program should print 1000 anyway.

This source explains significant digits as well: http://www.purplemath.com/modules/rounding2.htm

• What do you mean by "the same number of places"? Is that the same as "the same number of significant figures"? If you want an edge case for addition, 999 + 2.00. Aug 25 '16 at 7:14
• Surely 300 + 1 - 300 is a string of additions and subtractions, so doesn't need to be rounded until the end. (300 + 1) - 300 would be zero.
– Neil
Aug 25 '16 at 19:14

Java 11, 1325137913561336 1290 bytes

import java.math.*;String c(String s)throws Exception{String r="",T=r,a[],b[],z="\\.";int i=0,l,A[],M=0,m=s.length(),j,f=0,q=m;if(s.contains("(")){for(;i<m;){var c=s.charAt(i++);if(f<1){if(c==40){f=1;continue;}r+=c;}else{if(c==41&T.replaceAll("[^(]","").length()==T.replaceAll("[^)]","").length()){r+="x"+s.substring(i);break;}T+=c;}}return c(r.replace("x",c(T)));}else{for(a=s.split("[\\+\\-\\*/]"),A=new int[l=a.length];i<l;f=b.length>1&&(j=b.length())>f?j:f)M=(j=(b=a[i++].split(z)).length())>M?j:M;for(b=a.clone(),i=0;i<l;A[i]=b[i].contains(".")?j=b[i].length()-1:b[i].replaceAll("0*$","").length(),i++)for(q=(j=b[i].replace(".","").length())<q?j:q,j=a[i].split(z).length();j++<M;)b[i]=0+b[i];double R=new Double(new javax.script.ScriptEngineManager().getEngineByName("JS").eval(s)+""),p;for(int x:A)m=x<m?x:m;m=m==M&R%1==0&(int)R/10%10<1&(j=(r=R+"").split(z).length())>m?j-q>1?q:j:R>99?m:R%10==0?r.length()-1:m<1?1:m;R=new BigDecimal(R).round(new MathContext((R<0?-R:R)<1?m-1:m)).doubleValue();r=(m<M&(p=Math.pow(10,M-m))/10>R?(int)(R/p)*p:R)+"";l=r.length()-2;r=(r=f<1?r.replaceAll(z+"0$",""):r+"0".repeat(f)).substring(0,(j=r.length())<m?j:r.contains(".")?(j=r.replaceAll("^0\\.0+","").length())<m?m-~j:m+1:m);for(i=r.length();i++<l;)r+=0;return r.replaceAll(z+"$","");}} +54 bytes to fix the edge case 501*2.0 (gave result 1002 before, but now correct 1000). I now understand why this challenge was unanswered for almost two years.. >.> This challenge has more special cases than the Dutch language, which is saying something.. Java is certainly not the right language for these kind of challenges (or any codegolf challenge for that matter.. ;p), but it's the only language I know good enough to even attempt a difficult challenge like this. Input format as String without spaces (if that is not allowed, you can add s=s.replace(" ","") (+19 bytes) at the top of the method). Try it online. Explanation: Sorry for the long post. if(s.contains("(")){ for(;i<m;){ var c=s.charAt(i++); if(f<1){ if(c==40){ f=1; continue;} r+=c;} else{ if(c==41&T.replaceAll("[^(]","").length()==T.replaceAll("[^)]","").length()){ r+="x"+s.substring(i); break;} T+=c;}} return c(r.replace("x",c(T)));} This part is used for input containing parenthesis. It will get the separated parts and use recursive-calls. • 0.4*(2*6) becomes 0.4*A, where A is a recursive call to c(2*6) • (8.3*0.02)+(1.*(9*4)+2.2) becomes A+B, where A is a recursive call to c(8.3*0.02) and B a recursive call to c(1.*(9*4)+2.2) → which in turn becomes 1.*C+2.2, where C is a recursive call to c(9*4) for(a=s.split("[\\+\\-\\*/]"),A=new int[l=a.length]; i<l; f=b.length>1&&(j=b.length())>f?j:f) M=(j=(b=a[i++].split(z)).length())>M?j:M; This first loop is used to fill the values M and k, where M is the largest integer-length regarding significant figures and k the largest decimals-length. • 1200+3.0 becomes M=2, k=1 (12, .0) • 999+2.00 becomes M=3, k=2 (999, .00) • 300.+1-300. becomes M=3, k=0 (300, .) for(b=a.clone(),i=0; i<l; A[i]=b[i].contains(".")?j=b[i].length()-1:b[i].replaceAll("0*$","").length(),i++)
for(q=(j=b[i].replace(".","").length())<q?j:q,
j=a[i].split(z).length();
j++<M;)
b[i]=0+b[i];

This second loop is used to fill the arrays A and b as well as value q, where A is the amount of significant figures, b hold the integers with leading zeroes to match M, and q is the lowest length disregarding dots.

• 1200+3.0 becomes A=[2, 5] (12, 00030), b=[1200, 0003.0], and q=2 (30)
• 999+2.00 becomes A=[3, 5] (999, 00200), b=[999, 002.00], and q=3 (both 999 and 200)
• 300.+1-300. becomes A=[3, 3, 3] (300, 001, 300), b=[300., 001, 300.], and q=1 (1)
• 501*2.0 becomes A=[3, 4] (501, 0020), b=[501, 002.0], and q=2 (20)

double R=new Double(new javax.script.ScriptEngineManager().getEngineByName("JS").eval(s)+"")

Uses a JavaScript engine to eval the input, which will be saved in R as double.

• 1200+3.0 becomes R=1203.0
• 999+2.00 becomes R=1001.0
• 300.+1-300. becomes R=1.0

for(int x:A)
m=x<m?x:m;

This sets m to the smallest value in the array A.

• A=[2, 5] becomes m=2
• A=[3, 5] becomes m=3
• A=[3, 3, 3] becomes m=3

m=m==M                // If m equals M
&R%1==0             // and R has no decimal values (apart from 0)
&(int)R/10%10<1     // and floor(int(R)/10) modulo-10 is 0
&(j=(r=R+"").split(z).length())>m?
// and the integer-length of R is larger than m:
j-q>1?             //  If this integer-length of R minus q is 2 or larger:
q                 //   Set m to q instead
:                  //  Else:
j                 //  Set m to this integer-length of R
:R>99?              // Else-if R is 100 or larger:
m                  //  Leave m the same
:R%10==0?           // Else-if R modulo-10 is exactly 0:
r.length()-1       //  Set m to the total length of R (minus the dot)
:m<1?               // Else-if m is 0:
1                  //  Set m to 1
:                   // Else:
m;                 //  Leave m the same

This modifies m based on multiple factors.

• 999+2.00 = 1001.0 & m=3,q=3 becomes m=4 (because m==M (both 3) → R%1==0 (1001.0 has no decimal values) → (int)R/10%10<1 ((int)1001.0/10 becomes 100100%10<1) → "1001".length()>m (4>3) → "1001".length()-q<=1 (4-3<=1) → so m becomes the length of the integer-part "1001" (4))
• 3.839*4 = 15.356 & m=1,q=1 stays m=1 (because m==M (both 1) → R%1!=0 (15.356 has decimal values) → R<=99R%10!=0 (15.356%10==5.356) → m!=0 → so m stays the same (1))
• 4*7*3 = 84.0 & m=1,q=1 stays m=1 (because m==M (both 1) → R%1==0 (84.0 has no decimal values) → (int)R/10%10>=1 ((int)84/10 becomes 88%10>=1) → R<=99R%10!=0 (84%10==4) → m!=0 → so m stays the same (1))
• 6.0+4.0 = 10.0 & m=2,q=2 becomes m=3 (because m!=M (m=2, M=1) → R<=99R%10==0 (10%10==0) → so m becomes the length of the total R (minus the dot) "10.0".length()-1 (3))
• 0-8.8 = -8.8 & m=0,q=1 becomes m=1 (because m!=M (m=0, M=1) → R<=99R%10!=0 (-8.8%10==-8.8) → m<1 → so m becomes 1)
• 501*2.0 = 1001.0 & m=3,q=2 becomes m=2 (because m==M (both 3) → R%1==0 (1001.0 has no decimal values) → (int)R/10%10<1 ((int)1001.0/10 becomes 100100%10<1) → "1001".length()>m (4>3) → "1001".length()-q>1 (4-2>1) → so m becomes q (2))

R=new BigDecimal(R).round(new MathContext((R<0?-R:R)<1?m-1:m)).doubleValue();

Now R is rounded based on m.

• 1001.0 & m=4 becomes 1001.0
• 0.258 & m=3 becomes 0.26 (because abs(R)<1, m-1 (2) instead of m=3 is used inside MathContext)
• -8.8 & m=1 becomes -9.0
• 1002.0 & m=2 becomes 1000.0

m<M&(p=Math.pow(10,M-m))/10>R?(int)(R/p)*p:R;

This modifies the integer part of R if necessary.

• 300.+1-300. = 1.0 & m=3,M=3 stays 1.0 (because m>=M → so R stays the same (1.0))
• 0.4*10 = 4.0 & m=1,M=2 stays 4.0 (because m<M(10^(M-m))/10<=R ((10^1)/10<=4.010/10<=4.01<=4.0) → so R stays the same (4.0))
• 300+1-300 = 1.0 & m=1,M=3 becomes 0.0 (because m<M(10^(M-m))/10>R ((10^2)/10>1.0100/10>1.010>1.0) → so R becomes 0.0 because of int(R/(10^(M-m)))*(10^(M-m)) (int(1.0/(10^2))*(10^2)int(1.0/100)*1000*1000)

r=(...)+"";                  // Set R to r as String (... is the part explained above)
l=r.length()-2;              // Set l to the length of R minus 2
r=(r=k<1?                    // If k is 0 (no decimal values in any of the input-numbers)
r.replaceAll(z+"0$","") // Remove the .0 at the end : // Else: r+"0".repeat(f) // Append k zeroes after the current r ).substring(0, // Then take the substring from index 0 to: (j=r.length())<m? // If the total length of r is below m: j // Leave r the same :r.contains(".")? // Else-if r contains a dot (j=r.replaceAll("^0\\.0+","").length())<m? // And R is a decimal below 1, // and its rightmost decimal length is smaller than m m-~j // Take the substring from index 0 to m+j+1 // where j is this rightmost decimal length : // Else: m+1 // Take the substring from index 0 to m+1 : // Else: m); // Take the substring from index 0 to m This sets R to r as String, and modifies it based on multiple factors. • 1203.0 & m=4,k=2 becomes 1203. (because k>=1 → so r becomes 1001.000; r.length()>=m (8>=4) → r.contains(".")r.length()>=m (8>=4) → substring from index 0 to m+1 (5)) • 6.9 & m=2,k=2 stays 6.9 (because k>=1 → so r becomes 6.900; r.length()>=m (5>=2) → r.contains(".")r.length()>=m (5>=2) → substring from index 0 to m+1 (3)) • 1.0 & m=3,k=0 becomes 1 (because k<1 → so r becomes 1; r.length()<m (1<3) → substring from index 0 to r.length() (1)) • 25.0 & m=4,k=4 becomes 25.00 (because k>=1 → so r becomes 25.00000; r.length()>=m (8>=4) → r.contains(".")r.length()>+m (8>=4) → substring from index 0 to m+1 (5)) • 0 & m=1,k=0 stays 0 (because k<1 → so r stays 0; r.length()>=m (1>=1) → !r.contains(".") → substring from index 0 to m (1)) for(i=r.length();i++<l;) r+=0; This puts trailing zeroes back again to the integer part if necessary. • r="12" & R=1200.0 becomes r="1200" • r="1" & R=10.0 becomes r="10" • r="8" & R=80.0 becomes r="80" return r.replaceAll(z+"$","");

And finally we return the result, after we've removed any trailing dots.

• 1203. becomes 1203
• 5. becomes 5

Can definitely be golfed by a couple hundred bytes, but I'm just glad it's working now. It already took a while to understand each of the cases and what was being asked in the challenge. And then it took a lot of trial-and-error, testing and retesting to get to the result above. And while writing this explanation above I was able to remove another ±50 bytes of unused code..

• Upvoted. But the spec seems to require that 501*2.0 to output 1000 (you should output 1000 anyway, which I interpret as "still", not either way). Magnificent work anyway. Mar 3 '18 at 20:53
• @WeijunZhou Thanks for the feedback! I've given it some thought again and was able to fix the edge-case without breaking any other cases. :) Mar 3 '18 at 21:26