diff --git a/pom.xml b/pom.xml index c82350f..cc4c0bc 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.fross rpncalc - 5.0.14 + 5.1.0 jar rpncalc diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a7ff268..be880e4 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: rpncalc -version: '5.0.14' +version: '5.1.0' summary: The command line Reverse Polish Notation (RPN) calculator description: | RPNCalc is an easy to use command line based Reverse Polish diff --git a/src/main/java/org/fross/rpncalc/CommandParser.java b/src/main/java/org/fross/rpncalc/CommandParser.java index e0b7543..9e8afdd 100644 --- a/src/main/java/org/fross/rpncalc/CommandParser.java +++ b/src/main/java/org/fross/rpncalc/CommandParser.java @@ -206,6 +206,7 @@ public static void Parse(StackObj calcStack, StackObj calcStack2, String cmdInpu case "frac": case "fraction": String[] outString = StackConversions.cmdFraction(calcStack, cmdInputParam); + // If there wasn't an error (which would return an empty string), display the results if (!outString[0].isEmpty()) { Output.printColorln(Ansi.Color.YELLOW, outString[0]); Output.printColorln(Ansi.Color.WHITE, outString[1]); @@ -450,10 +451,17 @@ public static void Parse(StackObj calcStack, StackObj calcStack2, String cmdInpu BigDecimal fracTop = new BigDecimal(cmdInputParam.substring(0, cmdInputParam.indexOf('/'))); BigDecimal fracBottom = new BigDecimal(cmdInputParam.substring(cmdInputParam.indexOf('/') + 1)); + Output.debugPrintln("Fraction Top:\t"+fracTop); + Output.debugPrintln("Fraction Bot:\t"+fracBottom); + // Divide the fraction and get a decimal equivalent fracDecimalEquiv = fracTop.divide(fracBottom, MathContext.DECIMAL128); // Overall decimal equivalent (integer + decimal) + // If integer is negative, make the decimal negative so we can add them + if (fracInteger.signum() < 0) { + fracDecimalEquiv = fracDecimalEquiv.multiply(new BigDecimal("-1")); + } BigDecimal endResult = fracInteger.add(fracDecimalEquiv); // Simply convert the fraction to a decimal and add it to the stack diff --git a/src/main/java/org/fross/rpncalc/StackCommands.java b/src/main/java/org/fross/rpncalc/StackCommands.java index 43d49d8..f8e328d 100644 --- a/src/main/java/org/fross/rpncalc/StackCommands.java +++ b/src/main/java/org/fross/rpncalc/StackCommands.java @@ -739,8 +739,8 @@ public static void cmdRound(StackObj calcStack, String arg) { try { decimalPlaces = Integer.parseInt(arg); // Ensure a negative number is not provided for decimal points to round - if (decimalPlaces <= 0) { - Output.printColorln(Ansi.Color.RED, "ERROR: '" + arg + "' number of decimal places must be > 0"); + if (decimalPlaces < 0) { + Output.printColorln(Ansi.Color.RED, "ERROR: '" + arg + "' number of decimal places must be >= 0"); return; } diff --git a/src/main/java/org/fross/rpncalc/StackConversions.java b/src/main/java/org/fross/rpncalc/StackConversions.java index e29d9c1..c74cb40 100644 --- a/src/main/java/org/fross/rpncalc/StackConversions.java +++ b/src/main/java/org/fross/rpncalc/StackConversions.java @@ -94,7 +94,7 @@ public static String[] cmdFraction(StackObj calcStack, String param) { BigDecimal startingNumber = calcStack.peek(); // If starting number is negative, set a variable then remove the negative sign - if (startingNumber.compareTo(BigDecimal.ZERO) < 0) { + if (startingNumber.signum() < 0) { negativeNumber = true; startingNumber = startingNumber.abs(); } @@ -111,16 +111,35 @@ public static String[] cmdFraction(StackObj calcStack, String param) { return outputString; } + // Round the decimal number to the right denominator + // IF MY_DIM != ((FLOOR((MY_DIM * 16) + 0.5)) / 16) + // Reference: https://community.ptc.com/t5/3D-Part-Assembly-Design/Rounding-decimals-to-the-nearest-fraction/td-p/657420 + BigInteger roundedNumberTemp = BigInteger.ZERO; + BigDecimal roundedNumber = BigDecimal.ZERO; + try { + roundedNumberTemp = startingNumber.multiply(new BigDecimal(denominator)).add(new BigDecimal("0.5")).toBigInteger(); + roundedNumber = new BigDecimal(roundedNumberTemp).divide(new BigDecimal(denominator)); + } catch (ArithmeticException ex) { + // Ignore error where division doesn't equate to the exact value - we're estimating here... + } + // Determine the integer portion of the number - BigInteger integerPart = startingNumber.toBigInteger(); + BigInteger integerPart = roundedNumber.toBigInteger(); - // Determine the fractional portion as an double - BigDecimal decimalPart = startingNumber.subtract(new BigDecimal(integerPart)); + // Need to make the decimal a fraction my multiplying by the 10ths. + // Determine the number of decimals places and multiply by 10 + int scaleMultiplier = 10; + for (int i = 1; i < roundedNumber.scale(); i++) { + scaleMultiplier *= 10; + } - // Convert to a fraction with provided base - // This will round to the nearest integer by adding 1/2 to the number and getting it's integer value - BigDecimal numeratorNotRounded = decimalPart.multiply(new BigDecimal(denominator)); - BigInteger numerator = numeratorNotRounded.add(new BigDecimal(".5")).toBigInteger(); + // Numerator = decimal portion * scale so we have an integer + // Decimal = the scale. + // Example: 0.25 becomes 25/100 | 0.123 becomes 123/1000 + BigDecimal numeratorTemp = roundedNumber.subtract(new BigDecimal(integerPart)); + numeratorTemp = numeratorTemp.multiply(BigDecimal.valueOf(scaleMultiplier)); + BigInteger numerator = numeratorTemp.toBigInteger(); + denominator = BigInteger.valueOf(scaleMultiplier); // Get the Greatest Common Divisor so we can simply the fraction BigInteger gcd = Math.GreatestCommonDivisor(numerator, denominator); @@ -138,16 +157,17 @@ public static String[] cmdFraction(StackObj calcStack, String param) { // Output the fractional display // If there is no fractional result, remove it so we don't see '0/1' - String stackHeader = "-Fraction (Granularity: 1/" + (denominator.multiply(gcd)) + ")"; + String stackHeader = "-Fraction (Granularity: 1/" + (param) + ")"; + String result = integerPart + " " + ((numerator.compareTo(BigInteger.ZERO) == 0) ? "" : numerator.toString() + "/" + denominator); + + // Header Top outputString[0] = "\n" + stackHeader + "-".repeat(Main.configProgramWidth - stackHeader.length()); - if (numerator.compareTo(BigInteger.ZERO) != 0) { - outputString[1] = " " + calcStack.peek().setScale(8, RoundingMode.HALF_UP) + " is approximately '" + integerPart + " " + numerator + "/" - + denominator + "'"; - } else { - outputString[1] = " " + calcStack.peek() + " does not have a fractional component with a base of " + (denominator.multiply(gcd)); - } + // Results + outputString[1] = " " + calcStack.peek().setScale(5, RoundingMode.HALF_UP) + " is approximately '" + result.trim() + "'"; + // Header Bottom outputString[2] = "-".repeat(Main.configProgramWidth) + "\n"; - outputString[3] = integerPart + " " + numerator + "/" + denominator; + // Results for testing + outputString[3] = result.trim(); return outputString; } diff --git a/src/main/java/org/fross/rpncalc/StackObj.java b/src/main/java/org/fross/rpncalc/StackObj.java index f2267c1..d388873 100644 --- a/src/main/java/org/fross/rpncalc/StackObj.java +++ b/src/main/java/org/fross/rpncalc/StackObj.java @@ -152,7 +152,11 @@ public void push(BigDecimal item) { * @param item */ public void push(String item) { - calcStack.push(new BigDecimal(item, this.mc)); + try { + this.calcStack.push(new BigDecimal(item, this.mc)); + } catch (Exception ex) { + Output.printColorln(Ansi.Color.RED, "Error: " + ex.getMessage()); + } } /** diff --git a/src/test/java/org/fross/rpncalc/CommandParserTest.java b/src/test/java/org/fross/rpncalc/CommandParserTest.java index 9caa0c1..98b6c89 100644 --- a/src/test/java/org/fross/rpncalc/CommandParserTest.java +++ b/src/test/java/org/fross/rpncalc/CommandParserTest.java @@ -102,7 +102,7 @@ void testFractionInput() { CommandParser.Parse(stk1, stk2, "-4 1/64", "-4", "1/64"); StackCommands.cmdRound(stk1, "4"); - assertEquals(-3.9844, stk1.peek().doubleValue()); + assertEquals(-4.0156, stk1.peek().doubleValue()); assertEquals(4, stk1.size()); // Test scientific notation diff --git a/src/test/java/org/fross/rpncalc/StackCommandsTest.java b/src/test/java/org/fross/rpncalc/StackCommandsTest.java index 1c27b5d..efaad3d 100644 --- a/src/test/java/org/fross/rpncalc/StackCommandsTest.java +++ b/src/test/java/org/fross/rpncalc/StackCommandsTest.java @@ -1068,6 +1068,10 @@ void testCmdRound() { stk.push(-65.4329); StackCommands.cmdRound(stk, "12"); assertEquals(-65.4329, stk.pop().doubleValue()); + + stk.push(0.1); + StackCommands.cmdRound(stk, "0"); + assertEquals(0, stk.pop().doubleValue()); // Scientific Notation stk.push(1.23456789e19); diff --git a/src/test/java/org/fross/rpncalc/StackConversionsTest.java b/src/test/java/org/fross/rpncalc/StackConversionsTest.java index a0be65a..4ddd5ed 100644 --- a/src/test/java/org/fross/rpncalc/StackConversionsTest.java +++ b/src/test/java/org/fross/rpncalc/StackConversionsTest.java @@ -122,34 +122,226 @@ void testCmdDeg2Rad() { void testCmdFraction() { StackObj stk = new StackObj(); - // Test positive numbers with different bases + // Test #1 + stk.clear(); stk.push(71.046875); assertEquals(71.046875, stk.peek().doubleValue()); - - String[] result = StackConversions.cmdFraction(stk, ""); + // 64 + String[] result = StackConversions.cmdFraction(stk, "64"); assertEquals("71 3/64", result[3]); - + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("71 1/16", result[3]); + // 16 result = StackConversions.cmdFraction(stk, "16"); assertEquals("71 1/16", result[3]); - + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("71", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("71", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("71", result[3]); + + // Test #2 + stk.clear(); + stk.push(4.99); + assertEquals(4.99, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("4 63/64", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("5", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("5", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("5", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("5", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("5", result[3]); + + // Test #3 + stk.clear(); + stk.push(23.212121); + assertEquals(23.212121, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("23 7/32", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("23 7/32", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("23 3/16", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("23 1/4", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("23 1/4", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("23", result[3]); + + // Test #3 + stk.clear(); + stk.push(71.7272); + assertEquals(71.7272, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("71 47/64", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("71 23/32", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("71 3/4", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("71 3/4", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("71 3/4", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("71 1/2", result[3]); + + // Test with negative numbers + // Test #-1 + stk.clear(); + stk.push(-71.046875); + assertEquals(-71.046875, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("-71 3/64", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("-71 1/16", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("-71 1/16", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("-71", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("-71", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("-71", result[3]); + + // Test #-2 + stk.clear(); + stk.push(-4.99); + assertEquals(-4.99, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("-4 63/64", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("-5", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("-5", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("-5", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("-5", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("-5", result[3]); + + // Test #3 + stk.clear(); + stk.push(-23.212121); + assertEquals(-23.212121, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("-23 7/32", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("-23 7/32", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("-23 3/16", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("-23 1/4", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("-23 1/4", result[3]); + // 2 + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("-23", result[3]); + + // Test #3 + stk.clear(); + stk.push(-71.7272); + assertEquals(-71.7272, stk.peek().doubleValue()); + // 64 + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("-71 47/64", result[3]); + // 32 + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("-71 23/32", result[3]); + // 16 + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("-71 3/4", result[3]); + // 8 + result = StackConversions.cmdFraction(stk, "8"); + assertEquals("-71 3/4", result[3]); + // 4 + result = StackConversions.cmdFraction(stk, "4"); + assertEquals("-71 3/4", result[3]); + // 2 result = StackConversions.cmdFraction(stk, "2"); - assertEquals("71 0/1", result[3]); + assertEquals("-71 1/2", result[3]); + + + // Misc + stk.clear(); + stk.push(71.1); + result = StackConversions.cmdFraction(stk, "16"); + assertEquals("71 1/8", result[3]); + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("71", result[3]); + + stk.clear(); + stk.push(14.77); + result = StackConversions.cmdFraction(stk, "32"); + assertEquals("14 25/32", result[3]); // Test negative numbers with different bases + stk.clear(); stk.push(-123.456); - assertEquals(-123.456, stk.peek().doubleValue()); + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("-123 1/2", result[3]); + stk.push(-123.456); result = StackConversions.cmdFraction(stk, ""); assertEquals("-123 29/64", result[3]); - result = StackConversions.cmdFraction(stk, "44"); - assertEquals("-123 5/11", result[3]); - - result = StackConversions.cmdFraction(stk, "32"); - assertEquals("-123 15/32", result[3]); + // -41 33/34 + stk.push(-41.970588235); + result = StackConversions.cmdFraction(stk, "2"); + assertEquals("-42", result[3]); + // -12.123123123123123 + stk.push(-12.123123123123123); + result = StackConversions.cmdFraction(stk, "64"); + assertEquals("-12 1/8", result[3]); result = StackConversions.cmdFraction(stk, "8"); - assertEquals("-123 1/2", result[3]); + assertEquals("-12 1/8", result[3]); stk.clear(); stk.push(1.2e12 / 2.2e14); diff --git a/tools/FractionTestResults.xlsx b/tools/FractionTestResults.xlsx new file mode 100644 index 0000000..ebf9bba Binary files /dev/null and b/tools/FractionTestResults.xlsx differ