From d5faddbc8516b7ae9ea3b08ad0368d6725d28368 Mon Sep 17 00:00:00 2001 From: Omar Ahmed Hassan <98468609+omar-ahmed42@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:48:20 +0200 Subject: [PATCH] Enhancement: Enhance NFunction evaluation and support advanced NFunctions (#2577) # Description Enhance NFunction sanitization and support advanced functions: - Start page counting from 1 rather than 0 as PDFs are one based from the user's perspective, thus functions results would be affected by starting with "0" rather than "1". - Ignore out of bound results rather than stopping iterations to work with functions such as (n - 4) when page count is 10 as we would get positive values when n > 4. - Remove spaces to support expressions such as 2n + 1 rather just 2n+1. - Support advanced functions as follows: - Support expressions such as follows 5(n-1), n(n-1), expressions followed by opening rounded without '*' operator. - Support expressions such as follows (n-1)5, (n-1)n, expressions preceded closing rounded without '*' operator. - Support consecutive "n" expressions, examples: nnn, 2nn, nn*3, nnnn. Closes #(issue_number) ## Checklist - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have performed a self-review of my own code - [ ] I have attached images of the change if it is UI based - [x] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [x] My changes generate no new warnings - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) --- cucumber/features/general.feature | 12 ++-- .../software/SPDF/utils/GeneralUtils.java | 37 +++++++++--- .../software/SPDF/utils/GeneralUtilsTest.java | 59 ++++++++++++++++++- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/cucumber/features/general.feature b/cucumber/features/general.feature index 4255c89e7..3ac610669 100644 --- a/cucumber/features/general.feature +++ b/cucumber/features/general.feature @@ -1,7 +1,7 @@ @general Feature: API Validation - + @split-pdf-by-sections @positive Scenario Outline: split-pdf-by-sections with different parameters Given I generate a PDF file as "fileInput" @@ -66,7 +66,7 @@ Feature: API Validation | pageNumbers | file_count | | 1,3,5-9 | 8 | | all | 20 | - | 2n+1 | 11 | + | 2n+1 | 10 | | 3n | 7 | @@ -106,9 +106,9 @@ Feature: API Validation And the response ZIP should contain 2 files And the response file should have size greater than 0 And the response status code should be 200 - + Examples: - | format | - | png | + | format | + | png | | gif | - | jpeg | + | jpeg | diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index ac4cdca8f..799511452 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -13,6 +13,8 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.simpleyaml.configuration.file.YamlFile; import org.simpleyaml.configuration.file.YamlFileWrapper; @@ -220,32 +222,51 @@ public class GeneralUtils { throw new IllegalArgumentException("Invalid expression"); } - int n = 0; - while (true) { + for (int n = 1; n <= maxValue; n++) { // Replace 'n' with the current value of n, correctly handling numbers before // 'n' - String sanitizedExpression = insertMultiplicationBeforeN(expression, n); + String sanitizedExpression = sanitizeNFunction(expression, n); Double result = evaluator.evaluate(sanitizedExpression); // Check if the result is null or not within bounds - if (result == null || result <= 0 || result.intValue() > maxValue) { - if (n != 0) break; - } else { + if (result == null) + break; + + if (result.intValue() > 0 && result.intValue() <= maxValue) results.add(result.intValue()); - } - n++; } return results; } + private static String sanitizeNFunction(String expression, int nValue) { + String sanitizedExpression = expression.replace(" ", ""); + String multiplyByOpeningRoundBracketPattern = "([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2) + sanitizedExpression = sanitizedExpression.replaceAll(multiplyByOpeningRoundBracketPattern, "$1*("); + + String multiplyByClosingRoundBracketPattern = "\\)([0-9n)])"; // example: (n-1)n, (n-1)9, (n-1)(n-2) + sanitizedExpression = sanitizedExpression.replaceAll(multiplyByClosingRoundBracketPattern, ")*$1"); + + sanitizedExpression = insertMultiplicationBeforeN(sanitizedExpression, nValue); + return sanitizedExpression; + } + private static String insertMultiplicationBeforeN(String expression, int nValue) { // Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") String withMultiplication = expression.replaceAll("(\\d)n", "$1*n"); + withMultiplication = formatConsecutiveNsForNFunction(withMultiplication); // Now replace 'n' with its current value return withMultiplication.replace("n", String.valueOf(nValue)); } + private static String formatConsecutiveNsForNFunction(String expression) { + String text = expression; + while (text.matches(".*n{2,}.*")) { + text = text.replaceAll("(? handlePart(String part, int totalPages, int offset) { List partResult = new ArrayList<>(); diff --git a/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java b/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java index be63e8d36..18afcad62 100644 --- a/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java +++ b/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java @@ -52,14 +52,68 @@ public class GeneralUtilsTest { @Test void nFuncAdvanced3() { List result = GeneralUtils.parsePageList(new String[]{"4n+1"}, 9, true); - assertEquals(List.of(1, 5, 9), result, "'All' keyword should return all pages."); + assertEquals(List.of(5, 9), result, "'All' keyword should return all pages."); + } + + @Test + void nFunc_spaces() { + List result = GeneralUtils.parsePageList(new String[]{"n + 1"}, 9, true); + assertEquals(List.of(2, 3, 4, 5, 6, 7, 8, 9), result); + } + + @Test + void nFunc_consecutive_Ns_nnn() { + List result = GeneralUtils.parsePageList(new String[]{"nnn"}, 9, true); + assertEquals(List.of(1, 8), result); + } + + @Test + void nFunc_consecutive_Ns_nn() { + List result = GeneralUtils.parsePageList(new String[]{"nn"}, 9, true); + assertEquals(List.of(1, 4, 9), result); + } + + @Test + void nFunc_opening_closing_round_brackets() { + List result = GeneralUtils.parsePageList(new String[]{"(n-1)(n-2)"}, 9, true); + assertEquals(List.of(2, 6), result); + } + + @Test + void nFunc_opening_round_brackets() { + List result = GeneralUtils.parsePageList(new String[]{"2(n-1)"}, 9, true); + assertEquals(List.of(2, 4, 6, 8), result); + } + + @Test + void nFunc_opening_round_brackets_n() { + List result = GeneralUtils.parsePageList(new String[]{"n(n-1)"}, 9, true); + assertEquals(List.of(2, 6), result); + } + + @Test + void nFunc_closing_round_brackets() { + List result = GeneralUtils.parsePageList(new String[]{"(n-1)2"}, 9, true); + assertEquals(List.of(2, 4, 6, 8), result); + } + + @Test + void nFunc_closing_round_brackets_n() { + List result = GeneralUtils.parsePageList(new String[]{"(n-1)n"}, 9, true); + assertEquals(List.of(2, 6), result); + } + + @Test + void nFunc_function_surrounded_with_brackets() { + List result = GeneralUtils.parsePageList(new String[]{"(n-1)"}, 9, true); + assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8), result); } @Test void nFuncAdvanced4() { List result = GeneralUtils.parsePageList(new String[]{"3+2n"}, 9, true); - assertEquals(List.of(3, 5, 7, 9), result, "'All' keyword should return all pages."); + assertEquals(List.of(5, 7, 9), result, "'All' keyword should return all pages."); } @Test @@ -80,7 +134,6 @@ public class GeneralUtilsTest { assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly."); } - @Test void testParsePageListWithRangeZeroBaseOutput() { List result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, false);