diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7550a8a..1b6b16b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,22 +15,22 @@ jobs:
timeout-minutes: 30
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Set up JDK
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v5
with:
java-version: 8
distribution: 'temurin'
- - uses: burrunan/gradle-cache-action@cbdf4342ff988d143aa7a5aeceedffafb8c74bcf #v1.10
+ - uses: burrunan/gradle-cache-action@663fbad34e03c8f12b27f4999ac46e3d90f87eca # v3.0
name: Build with Gradle
with:
arguments: build
- name: Upload Test Results
if: always()
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
name: Test Results Linux
path: '**/test-results/**/*.xml'
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..49996a8
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,48 @@
+name: Publish to Maven Central
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: 11
+ distribution: 'temurin'
+
+ - name: Publish to Maven Central
+ run: ./gradlew publish
+ env:
+ ORG_GRADLE_PROJECT_NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
+ ORG_GRADLE_PROJECT_NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
+ ORG_GRADLE_PROJECT_SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
+ ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
+ ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+
+ - name: Check version
+ id: version
+ run: |
+ version=$(grep "generexVersion" gradle.properties | cut -d'=' -f2 | tr -d ' ')
+ if [[ "$version" != *"SNAPSHOT"* ]]; then
+ echo "is_release=true" >> $GITHUB_OUTPUT
+ else
+ echo "is_release=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Notify Central Publisher Portal
+ if: steps.version.outputs.is_release == 'true'
+ run: |
+ token=$(echo -n "${{ secrets.NEXUS_USERNAME }}:${{ secrets.NEXUS_PASSWORD }}" | base64)
+ curl -X POST \
+ --max-time 30 \
+ --fail-with-body \
+ -H "Authorization: Bearer $token" \
+ "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.pkware.generex?publishing_type=automatic"
diff --git a/README.md b/README.md
index 9b5320f..0ea73c7 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ If you use [Maven](http://maven.apache.org) you can include this library to your
com.pkware.generex
generex
- 1.1.0
+ 1.2.0
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 9a1880c..79598a5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -71,6 +71,7 @@ publishing {
}
repositories {
maven {
+ name = "MavenCentral"
url = uri(if (version.toString().isReleaseBuild) releaseRepositoryUrl else snapshotRepositoryUrl)
credentials {
username = repositoryUsername
@@ -81,8 +82,15 @@ publishing {
}
signing {
- // Signing credentials are stored locally in the user's global gradle.properties file.
+ // Signing credentials are stored as secrets in GitHub.
// See https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials for more information.
+
+ useInMemoryPgpKeys(
+ signingKeyId,
+ signingKey,
+ signingPassword,
+ )
+
sign(publishing.publications["mavenJava"])
}
@@ -92,13 +100,13 @@ val String.isReleaseBuild
val Project.releaseRepositoryUrl: String
get() = properties.getOrDefault(
"RELEASE_REPOSITORY_URL",
- "https://oss.sonatype.org/service/local/staging/deploy/maven2"
+ "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2",
).toString()
val Project.snapshotRepositoryUrl: String
get() = properties.getOrDefault(
"SNAPSHOT_REPOSITORY_URL",
- "https://oss.sonatype.org/content/repositories/snapshots"
+ "https://central.sonatype.com/repository/maven-snapshots/",
).toString()
val Project.repositoryUsername: String
@@ -107,6 +115,15 @@ val Project.repositoryUsername: String
val Project.repositoryPassword: String
get() = properties.getOrDefault("NEXUS_PASSWORD", "").toString()
+val Project.signingKeyId: String
+ get() = properties.getOrDefault("SIGNING_KEY_ID", "").toString()
+
+val Project.signingKey: String
+ get() = properties.getOrDefault("SIGNING_KEY", "").toString()
+
+val Project.signingPassword: String
+ get() = properties.getOrDefault("SIGNING_PASSWORD", "").toString()
+
val Project.pomPackaging: String
get() = properties.getOrDefault("POM_PACKAGING", "jar").toString()
diff --git a/gradle.properties b/gradle.properties
index aea5f34..5ec4e56 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
kotlin.code.style=official
-generexVersion=1.1.0
+generexVersion=1.2.0
POM_ARTIFACT_ID=generex
POM_NAME=Generex
diff --git a/src/main/java/com/mifmif/common/regex/Generex.java b/src/main/java/com/pkware/generex/Generex.java
similarity index 78%
rename from src/main/java/com/mifmif/common/regex/Generex.java
rename to src/main/java/com/pkware/generex/Generex.java
index 239a4db..81925c3 100644
--- a/src/main/java/com/mifmif/common/regex/Generex.java
+++ b/src/main/java/com/pkware/generex/Generex.java
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-package com.mifmif.common.regex;
+package com.pkware.generex;
import dk.brics.automaton.Automaton;
import dk.brics.automaton.RegExp;
@@ -60,6 +60,13 @@ public class Generex implements Iterable {
private Node rootNode;
private boolean isTransactionNodeBuilt;
+ /**
+ * Determined possible minimum and maximum length of a regex by traversing the
+ * Automaton tree using depth first search.
+ */
+ private Integer cachedMinLength;
+ private Integer cachedMaxLength;
+
/**
* The maximum length a produced string for an infinite regex if {@link #random(int, int)} hasn't been given a max
* length other than {@link Integer#MAX_VALUE}.
@@ -317,7 +324,9 @@ public String random() {
* See {@link #random(int, int)}
*/
public String random(int minLength) {
- return random(minLength, automaton.isFinite() ? Integer.MAX_VALUE : DEFAULT_INFINITE_MAX_LENGTH);
+ calculateLengthBounds();
+ int actualMaxLength = isInfinite() ? DEFAULT_INFINITE_MAX_LENGTH : cachedMaxLength;
+ return random(minLength, actualMaxLength);
}
/**
@@ -342,8 +351,21 @@ public String random(int minLength) {
* given range. Otherwise, see the {@code minLength} and {@code maxLength} docs.
*/
public String random(int minLength, int maxLength) {
+ calculateLengthBounds();
+
+ // Calculate actual valid range by comparing the regex and the user defined bounds
+ int actualMinLength = Math.max(minLength, cachedMinLength);
+ int actualMaxLength = Math.min(maxLength, isInfinite() ? maxLength : cachedMaxLength);
+
+ // Pre-select target length uniformly from valid range
+ int targetLength;
+ if (actualMinLength > actualMaxLength) {
+ targetLength = actualMaxLength;
+ } else {
+ targetLength = actualMinLength + random.nextInt(actualMaxLength - actualMinLength + 1);
+ }
- String result = prepareRandom("", automaton.getInitialState(), minLength, maxLength);
+ String result = prepareRandom("", automaton.getInitialState(), minLength, maxLength, targetLength);
// Substring in case a length of 'maxLength + 1' is returned, which is possible if a smaller string can't be produced.
return result.substring(0, Math.min(maxLength, result.length()));
}
@@ -357,19 +379,26 @@ public String random(int minLength, int maxLength) {
* @param maxLength Maximum wanted length of produced string.
* @return A string built from the accumulation of previous transitions.
*/
- private String prepareRandom(String currentMatch, State state, int minLength, int maxLength) {
+ private String prepareRandom(String currentMatch, State state, int minLength, int maxLength, int targetLength) {
// Return a string of length 'maxLength + 1' to indicate a dead branch.
- if (currentMatch.length() > maxLength) return currentMatch;
+ if (currentMatch.length() > maxLength || state.getTransitions().isEmpty()) return currentMatch;
- if (state.isAccept() && shouldTerminate(currentMatch.length(), minLength, maxLength)) return currentMatch;
+ String returnValue = null;
+
+ if (state.isAccept()) {
+ // Set the current match to the value to return, just in case this would happen to be the cloest match to
+ // the target length.
+ returnValue = currentMatch;
+
+ if (currentMatch.length() == targetLength) return currentMatch;
+ }
// Make a copy so the original set is never modified.
Set possibleTransitions = new HashSet<>(state.getTransitions());
int totalWeightedTransitions = calculateTotalWeightedTransitions(possibleTransitions);
- String returnValue = currentMatch;
-
+ // Will never start as empty due to the initial if statement in the function.
while (!possibleTransitions.isEmpty()) {
Transition randomTransition = pickRandomWeightedTransition(possibleTransitions, totalWeightedTransitions);
@@ -378,39 +407,88 @@ private String prepareRandom(String currentMatch, State state, int minLength, in
possibleTransitions.remove(randomTransition);
char randomChar = (char) (random.nextInt(subTransitions) + randomTransition.getMin());
- String result = prepareRandom(currentMatch + randomChar, randomTransition.getDest(), minLength, maxLength);
+ String result = prepareRandom(currentMatch + randomChar, randomTransition.getDest(), minLength, maxLength, targetLength);
- // Greedily return the first valid result found.
- if (minLength <= result.length() && result.length() <= maxLength) return result;
+ // Greedily return the first valid result found that is of the wanted length..
+ if (result.length() == targetLength) return result;
- // Continue to search for a valid result if the result is greater than the max length, or if the result is
- // less than the minimum length. In the case a result never reaches the minimum length, return the longest
- // match found.
- if (returnValue.length() < result.length()) returnValue = result;
+ returnValue = getBestMatch(result, returnValue, minLength, maxLength, targetLength);
}
return returnValue;
}
/**
- * Attempts to randomly terminate regexes in a way where a uniform distribution of lengths is produced by initially
- * having a low probability of termination when close to the minimum length, and linearly increasing this
- * probability as a regex nears its maximum requested length.
- *
- * In practice this doesn't work well when an infinitely repeating part of the regex is located in the middle with a
- * non-repeating terminal ending, but still works better than a flat chance of termination regardless of the range
- * of lengths requested.
+ * Determines if the new generation is better than the current generation.
+ *
+ * The new generation is better if it is within the bounds and is closer to the target length than the current
+ * generation. Otherwise, the current generation is better.
+ *
+ * @param newMatch the new generation to compare against the current generation.
+ * @param currentMatch the current generation.
+ * @param min minimum length of the generated string.
+ * @param max maximum length of the generated string.
+ * @param target the target length of the generated string.
+ * @return the best match between the new generation and the current generation.
+ */
+ private String getBestMatch(String newMatch, String currentMatch, int min, int max, int target) {
+
+ if (currentMatch == null) return newMatch;
+ if (newMatch.length() > max && currentMatch.length() > min) return currentMatch;
+
+ boolean newInRange = newMatch.length() >= min;
+ boolean currentInRange = currentMatch.length() >= min && currentMatch.length() <= max;
+
+ if (newInRange && !currentInRange) return newMatch;
+ if (currentInRange && !newInRange) return currentMatch;
+
+ int currentTargetDistance = Math.abs(currentMatch.length() - target);
+ int newTargetDistance = Math.abs(newMatch.length() - target);
+
+ if (newTargetDistance < currentTargetDistance) return newMatch;
+ return currentMatch;
+ }
+
+ /**
+ * Calculate the possible bounds of the generated string by traversing the regex
+ */
+ private void calculateLengthBounds() {
+ if (cachedMinLength != null) return;
+
+ int[] bounds = dfsLengthBounds(automaton.getInitialState(), new HashSet<>());
+ cachedMinLength = bounds[0];
+ cachedMaxLength = bounds[1];
+ }
+
+ /**
+ * Uses a depth first search to calculate the minimum and maximum length of the regex by
+ * traversing through the automaton tree.
*
- * It is assumed `maxLength` is not an absurdly large value, as this could allow the regex to grow extremely long,
- * and that `maxLength` is greater than `minLength`
+ * We can use DFS because the automaton is finite (does not contain infinite loops) and
+ * we need to visit every state regardless to determine the longest length.
*
- * @param depth Size of the current string produced to match a regex.
- * @param minLength Minimum wanted length of the produced string.
- * @param maxLength Maximum wanted length of the produced string.
- * @return Whether the current string should be returned as a match for the regex.
+ * @param state the current state of the automaton.
+ * @param visited the set of visited states.
+ * @return an int array containing the minimum and maximum length of the regex.
*/
- private boolean shouldTerminate(int depth, int minLength, int maxLength) {
- return depth >= minLength && random.nextInt(maxLength - depth + 1) == 0;
+ private int[] dfsLengthBounds(State state, Set visited) {
+ if (visited.contains(state)) return new int[]{Integer.MAX_VALUE, 0};
+
+ int minLength = state.isAccept() ? 0 : Integer.MAX_VALUE;
+ int maxLength = 0;
+
+ visited.add(state);
+
+ for (Transition transition : state.getTransitions()) {
+ int[] bounds = dfsLengthBounds(transition.getDest(), visited);
+ if (bounds[0] != Integer.MAX_VALUE) {
+ minLength = Math.min(minLength, bounds[0] + 1);
+ }
+ maxLength = Math.max(maxLength, bounds[1] + 1);
+ }
+
+ visited.remove(state);
+ return new int[]{minLength, maxLength};
}
/**
diff --git a/src/main/java/com/mifmif/common/regex/GenerexIterator.java b/src/main/java/com/pkware/generex/GenerexIterator.java
similarity index 99%
rename from src/main/java/com/mifmif/common/regex/GenerexIterator.java
rename to src/main/java/com/pkware/generex/GenerexIterator.java
index d1ec1c2..c2322b7 100644
--- a/src/main/java/com/mifmif/common/regex/GenerexIterator.java
+++ b/src/main/java/com/pkware/generex/GenerexIterator.java
@@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.mifmif.common.regex;
+package com.pkware.generex;
import dk.brics.automaton.State;
import dk.brics.automaton.Transition;
diff --git a/src/main/java/com/mifmif/common/regex/Main.java b/src/main/java/com/pkware/generex/Main.java
similarity index 98%
rename from src/main/java/com/mifmif/common/regex/Main.java
rename to src/main/java/com/pkware/generex/Main.java
index 45d15b0..ace75e1 100644
--- a/src/main/java/com/mifmif/common/regex/Main.java
+++ b/src/main/java/com/pkware/generex/Main.java
@@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.mifmif.common.regex;
+package com.pkware.generex;
/**
* @author y.mifrah
diff --git a/src/main/java/com/mifmif/common/regex/Node.java b/src/main/java/com/pkware/generex/Node.java
similarity index 98%
rename from src/main/java/com/mifmif/common/regex/Node.java
rename to src/main/java/com/pkware/generex/Node.java
index daee4d7..d12a253 100644
--- a/src/main/java/com/mifmif/common/regex/Node.java
+++ b/src/main/java/com/pkware/generex/Node.java
@@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.mifmif.common.regex;
+package com.pkware.generex;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/test/java/com/mifmif/common/regex/GenerexIteratorTest.java b/src/test/java/com/pkware/generex/GenerexIteratorTest.java
similarity index 98%
rename from src/test/java/com/mifmif/common/regex/GenerexIteratorTest.java
rename to src/test/java/com/pkware/generex/GenerexIteratorTest.java
index 8dbc592..f5eee0f 100644
--- a/src/test/java/com/mifmif/common/regex/GenerexIteratorTest.java
+++ b/src/test/java/com/pkware/generex/GenerexIteratorTest.java
@@ -1,4 +1,4 @@
-package com.mifmif.common.regex;
+package com.pkware.generex;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
diff --git a/src/test/java/com/mifmif/common/regex/GenerexIteratorUnitTest.java b/src/test/java/com/pkware/generex/GenerexIteratorUnitTest.java
similarity index 99%
rename from src/test/java/com/mifmif/common/regex/GenerexIteratorUnitTest.java
rename to src/test/java/com/pkware/generex/GenerexIteratorUnitTest.java
index ba378fe..a49bb96 100644
--- a/src/test/java/com/mifmif/common/regex/GenerexIteratorUnitTest.java
+++ b/src/test/java/com/pkware/generex/GenerexIteratorUnitTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.mifmif.common.regex;
+package com.pkware.generex;
import dk.brics.automaton.Automaton;
import dk.brics.automaton.State;
diff --git a/src/test/java/com/mifmif/common/regex/GenerexRandomTest.java b/src/test/java/com/pkware/generex/GenerexRandomTest.java
similarity index 98%
rename from src/test/java/com/mifmif/common/regex/GenerexRandomTest.java
rename to src/test/java/com/pkware/generex/GenerexRandomTest.java
index ed514fc..1aee061 100644
--- a/src/test/java/com/mifmif/common/regex/GenerexRandomTest.java
+++ b/src/test/java/com/pkware/generex/GenerexRandomTest.java
@@ -1,4 +1,4 @@
-package com.mifmif.common.regex;
+package com.pkware.generex;
import kotlin.ranges.IntRange;
import org.junit.jupiter.params.ParameterizedTest;
diff --git a/src/test/java/com/mifmif/common/regex/GenerexTest.java b/src/test/java/com/pkware/generex/GenerexTest.java
similarity index 99%
rename from src/test/java/com/mifmif/common/regex/GenerexTest.java
rename to src/test/java/com/pkware/generex/GenerexTest.java
index a33c412..1b9d1e3 100644
--- a/src/test/java/com/mifmif/common/regex/GenerexTest.java
+++ b/src/test/java/com/pkware/generex/GenerexTest.java
@@ -1,4 +1,4 @@
-package com.mifmif.common.regex;
+package com.pkware.generex;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
diff --git a/src/test/java/com/mifmif/common/regex/GenerexUnitTest.java b/src/test/java/com/pkware/generex/GenerexUnitTest.java
similarity index 99%
rename from src/test/java/com/mifmif/common/regex/GenerexUnitTest.java
rename to src/test/java/com/pkware/generex/GenerexUnitTest.java
index 020f5f7..e7ffe8c 100644
--- a/src/test/java/com/mifmif/common/regex/GenerexUnitTest.java
+++ b/src/test/java/com/pkware/generex/GenerexUnitTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.mifmif.common.regex;
+package com.pkware.generex;
import dk.brics.automaton.Automaton;
import org.junit.jupiter.api.Test;
diff --git a/src/test/kotlin/com/mifmif/common/regex/KotlinTests.kt b/src/test/kotlin/com/pkware/generex/KotlinTests.kt
similarity index 55%
rename from src/test/kotlin/com/mifmif/common/regex/KotlinTests.kt
rename to src/test/kotlin/com/pkware/generex/KotlinTests.kt
index a1c7f2e..0d6abb7 100644
--- a/src/test/kotlin/com/mifmif/common/regex/KotlinTests.kt
+++ b/src/test/kotlin/com/pkware/generex/KotlinTests.kt
@@ -1,4 +1,4 @@
-package com.mifmif.common.regex
+package com.pkware.generex
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
@@ -7,6 +7,7 @@ import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.ValueSource
import java.util.stream.Stream
+import kotlin.collections.iterator
import kotlin.math.max
import kotlin.math.min
@@ -93,6 +94,100 @@ class KotlinTests {
assertThat(ratio).isLessThan(1.1)
}
+ @Test
+ fun `ranges of group generate with a semi-uniform distribution`() {
+ val regex = "(abcde){1,5}"
+
+ val generex = Generex(regex)
+ val instancesMap = HashMap()
+
+ repeat(100_000) {
+
+ val result = generex.random(5, 25)
+ instancesMap[result] = instancesMap.getOrDefault(result, 0) + 1
+
+ assertThat(result).matches(regex)
+ }
+
+ val sortedKeys = instancesMap.keys.sortedBy { it.length }
+
+
+ // Bounds are uniformly distributed
+
+ var maxInstances = 0
+ var minInstances = Int.MAX_VALUE
+
+ println("Bounds:")
+ println("\t${sortedKeys.first()}: ${instancesMap[sortedKeys.first()]}")
+ maxInstances = max(maxInstances, instancesMap[sortedKeys.first()]!!)
+ minInstances = min(minInstances, instancesMap[sortedKeys.first()]!!)
+ println("\t${sortedKeys.last()}: ${instancesMap[sortedKeys.last()]}")
+ maxInstances = max(maxInstances, instancesMap[sortedKeys.last()]!!)
+ minInstances = min(minInstances, instancesMap[sortedKeys.last()]!!)
+
+ var ratio = 1.0 * maxInstances / minInstances
+ assertThat(ratio).isLessThan(1.1)
+
+
+ // Middle range is uniformly distributed
+
+ maxInstances = 0
+ minInstances = Int.MAX_VALUE
+
+ println("Middle Ranges:")
+ for (key in sortedKeys.subList(1, 4)) {
+ println("\t$key: ${instancesMap[key]}")
+ maxInstances = max(maxInstances, instancesMap[key]!!)
+ minInstances = min(minInstances, instancesMap[key]!!)
+ }
+
+ ratio = 1.0 * maxInstances / minInstances
+ assertThat(ratio).isLessThan(1.1)
+
+
+
+ }
+
+ @ParameterizedTest
+ @MethodSource("rangeUniformDistributionArgs")
+ fun `range regexes generate with uniform distributions`(regex: String) {
+
+ val generex = Generex(regex)
+ val instancesMap = HashMap()
+
+ repeat(100_000) {
+
+ val result = generex.random()
+ instancesMap[result.length] = instancesMap.getOrDefault(result.length, 0) + 1
+
+ assertThat(result).matches(regex)
+ }
+
+ var maxInstances = 0
+ var minInstances = Int.MAX_VALUE
+
+ // Assumes all possible strings have actually been produced.
+ for ((key, instances) in instancesMap) {
+ println("$key: $instances")
+ maxInstances = max(maxInstances, instances)
+ minInstances = min(minInstances, instances)
+ }
+
+ val ratio = 1.0 * maxInstances / minInstances
+ assertThat(ratio).isLessThan(1.1)
+ }
+
+ @ParameterizedTest
+ @MethodSource("regexExceedsColumnValue")
+ fun `if regex must produce longer value, return value is trimmed`(
+ regex: String,
+ targetLength: Int,
+ ) {
+ val generated = Generex(regex).random(targetLength, targetLength)
+
+ assertThat(generated.length).isEqualTo(targetLength)
+ }
+
companion object {
@JvmStatic
@@ -130,6 +225,20 @@ class KotlinTests {
Arguments.of("[a-ce-gr-ux-z]", 1, 1),
Arguments.of("123a*", 1, 10),
Arguments.of("123a*", 5, 10),
+ Arguments.of("a*123", 5, 20),
+ )
+
+ @JvmStatic
+ fun rangeUniformDistributionArgs() = Stream.of(
+ Arguments.of("\\d{1,5}"),
+ Arguments.of("\\d{1,10}"),
+ )
+
+ @JvmStatic
+ fun regexExceedsColumnValue() = Stream.of(
+ Arguments.of("(hi){3,5}", 7),
+ Arguments.of("aaa", 2),
+ Arguments.of("a{5,10}", 2),
)
}
}