diff --git a/core/src/main/java/org/quicktheories/generators/Generate.java b/core/src/main/java/org/quicktheories/generators/Generate.java index a992d59..2a08607 100644 --- a/core/src/main/java/org/quicktheories/generators/Generate.java +++ b/core/src/main/java/org/quicktheories/generators/Generate.java @@ -337,7 +337,45 @@ public static Gen characters(int startInclusive, int endInclusive) { return CodePoints.codePoints(startInclusive, endInclusive, '!') .map(l -> (char) l.intValue()); } - + + /** + * One dimensional char arrays + * @param sizes Gen of sizes for the arrays + * @param contents Gen of contents + * @return A Gen of char[] + */ + public static Gen charArrays(Gen sizes, Gen contents) { + Gen gen = td -> { + int size = sizes.generate(td); + char[] is = new char[size]; + for (int i = 0; i != size; i++) { + is[i] = contents.generate(td); + } + return is; + }; + return gen.describedAs(Arrays::toString); + } + + /** + * One dimensional char arrays + * @param sizes Gen of sizes for the arrays + * @param domain of content + * @return A Gen of char[] + */ + public static Gen charArrays(Gen sizes, char[] domain) { + Constraint constraints = Constraint.between(0, domain.length - 1).withNoShrinkPoint(); + Gen gen = td -> { + int size = sizes.generate(td); + char[] is = new char[size]; + for (int i = 0; i != size; i++) { + int idx = (int) td.next(constraints); + is[i] = domain[idx]; + } + return is; + }; + return gen.describedAs(Arrays::toString); + } + /** * One dimensional int arrays * @param sizes Gen of sizes for the arrays diff --git a/core/src/main/java/org/quicktheories/generators/StringsDSL.java b/core/src/main/java/org/quicktheories/generators/StringsDSL.java index bf67bc7..d695957 100644 --- a/core/src/main/java/org/quicktheories/generators/StringsDSL.java +++ b/core/src/main/java/org/quicktheories/generators/StringsDSL.java @@ -13,6 +13,16 @@ public class StringsDSL { private static final int BASIC_LATIN_FIRST_CODEPOINT = 0x0020; private static final int ASCII_LAST_CODEPOINT = 0x007F; private static final int LARGEST_DEFINED_BMP_CODEPOINT = 65533; + // https://en.wikipedia.org/wiki/Geohash + private static final char[] GEOHASH = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' + }; + // https://tools.ietf.org/html/rfc4648 + private static final char[] BASE_32 = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7' + }; /** * Generates integers as Strings, and shrinks towards "0". @@ -94,6 +104,26 @@ public StringGeneratorBuilder betweenCodePoints(int minInclusive, int maxInclusi return new StringGeneratorBuilder(minInclusive, maxInclusive); } + + /** + * Constructs a EncodingStringGeneratorBuilder which will build Strings composed from + * base32 encoding + * + * @return a EncodingStringGeneratorBuilder + */ + public EncodingStringGeneratorBuilder base32() { + return new EncodingStringGeneratorBuilder(BASE_32); + } + + /** + * Constructs a EncodingStringGeneratorBuilder which will build Strings composed from + * geohash encoding + * + * @return a EncodingStringGeneratorBuilder + */ + public EncodingStringGeneratorBuilder geohash() { + return new EncodingStringGeneratorBuilder(GEOHASH); + } public static class StringGeneratorBuilder { @@ -154,4 +184,43 @@ public Gen ofLengthBetween(int minLength, int maxLength) { } + public static class EncodingStringGeneratorBuilder { + private final char[] domain; + + private EncodingStringGeneratorBuilder(char[] domain) { + this.domain = domain; + } + + /** + * Generates Strings of a fixed length. + * + * @param fixedLength + * - the fixed length for the Strings + * @return a Source of type String + */ + public Gen ofLength(int fixedLength) { + return ofLengthBetween(fixedLength, fixedLength); + } + + /** + * Generates Strings of length bounded between minLength and maxLength + * inclusively. + * + * @param minLength + * - minimum inclusive length of String + * @param maxLength + * - maximum inclusive length of String + * @return a Source of type String + */ + public Gen ofLengthBetween(int minLength, int maxLength) { + ArgumentAssertions.checkArguments(minLength <= maxLength, + "The minLength (%s) is longer than the maxLength(%s)", + minLength, maxLength); + ArgumentAssertions.checkArguments(minLength >= 0, + "The length of a String cannot be negative; %s is not an accepted argument", + minLength); + return Generate.charArrays(Generate.range(minLength, maxLength), domain).map(a -> new String(a)); + } + } + } diff --git a/core/src/test/java/org/quicktheories/generators/ArraysTest.java b/core/src/test/java/org/quicktheories/generators/ArraysTest.java index 37c2be9..87ef621 100644 --- a/core/src/test/java/org/quicktheories/generators/ArraysTest.java +++ b/core/src/test/java/org/quicktheories/generators/ArraysTest.java @@ -8,6 +8,8 @@ import org.quicktheories.core.Gen; import org.quicktheories.generators.Generate; +import java.util.Arrays; + public class ArraysTest { private static final int ASCII_LAST_CODEPOINT = 0x007F; @@ -31,7 +33,22 @@ public void shrinksTowardsEmptyArrayWhenZeroLengthsAllowed() { assertThatGenerator(testee).shrinksTowards(new Character[0]); } - + @Test + public void shouldGenerateAllPossibleCharArraysWithinSmallDomain() { + Gen testee = Generate.charArrays(Generate.range(1, 2), Generate.pick(Arrays.asList('a', 'b'))); + assertThatGenerator(testee).generatesAllOf( + new char[] { 'a', 'a' }, new char[] { 'a', 'b' }, new char[] { 'b', 'a' }, + new char[] { 'b', 'b' }); + } + + @Test + public void shouldGenerateAllPossibleCharArraysWithinSmallDomainNoBoxing() { + Gen testee = Generate.charArrays(Generate.range(1, 2), new char[] {'a', 'b'}); + assertThatGenerator(testee).generatesAllOf( + new char[] { 'a', 'a' }, new char[] { 'a', 'b' }, new char[] { 'b', 'a' }, + new char[] { 'b', 'b' }); + } + @Test public void shrinksTowardsSmallestAllowedArrayWithSmallestContents() { Gen testee = Generate diff --git a/core/src/test/java/org/quicktheories/generators/StringsDSLTest.java b/core/src/test/java/org/quicktheories/generators/StringsDSLTest.java index ec238aa..888e7c3 100644 --- a/core/src/test/java/org/quicktheories/generators/StringsDSLTest.java +++ b/core/src/test/java/org/quicktheories/generators/StringsDSLTest.java @@ -29,5 +29,44 @@ public void fixedLengthStringsAreFixedLength() { .forAll(strings().allPossible().ofLength(100)) .check(s -> s.length() == 100); } - + + @Test + public void boundedLengthBase32() { + Gen testee = strings().base32().ofLengthBetween(3, 200); + qt() + .withExamples(100000) + .forAll(testee) + .check( s -> s.length() <= 200 && s.length() >= 3 && isValidBase32(s)); + } + + @Test + public void boundedLengthGeohash() { + Gen testee = strings().geohash().ofLengthBetween(3, 200); + qt() + .withExamples(100000) + .forAll(testee) + .check( s -> s.length() <= 200 && s.length() >= 3 && isValidGeohash(s)); + } + + private static boolean isValidBase32(final String str) { + for (int i = 0, len = str.length(); i < len; i++) { + char c = str.charAt(i); + if (!(('A' <= c && c <= 'Z') || ('2' <= c & c <= '7'))) { + return false; + } + } + return true; + } + + private static boolean isValidGeohash(final String str) { + for (int i = 0, len = str.length(); i < len; i++) { + char c = str.charAt(i); + // geohash has gaps in the alphabet, see https://en.wikipedia.org/wiki/Geohash for range + if (!(('0' <= c & c <= '9') || ('b' <= c && c <= 'h') || ('j' <= c && c <= 'k') || ('m' <= c && c <= 'n') || ('p' <= c && c <= 'z'))) { + return false; + } + } + return true; + } + }