├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── maven.yml ├── .gitignore ├── src ├── main │ ├── resources │ │ └── META-INF │ │ │ └── MANIFEST.MF │ └── java │ │ └── com │ │ └── valkryst │ │ └── VNameGenerator │ │ └── generator │ │ ├── GrammarGenerator.java │ │ ├── ConsonantVowelGenerator.java │ │ ├── NameGenerator.java │ │ ├── CombinatorialGenerator.java │ │ └── MarkovGenerator.java └── test │ └── java │ └── com │ └── valkryst │ └── VNameGenerator │ ├── ConsonantVowelGeneratorTest.java │ └── CombinatorialGeneratorTest.java ├── jitpack.yml ├── pom.xml ├── README.md └── LICENSE.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Valkryst -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | out 4 | VNameGenerator.iml -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.valkryst.NameGenerator 3 | 4 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sdk update 3 | - sdk install maven 4 | - sdk install java 21.0.0-open 5 | - sdk use java 21.0.0-open -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | assignees: 8 | - "Valkryst" 9 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | # https://github.com/marketplace/actions/setup-java-jdk 13 | - name: Setup JDK 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: 'zulu' 17 | java-version: 17 18 | java-package: jdk 19 | architecture: x64 20 | 21 | - name: Cache Maven Packages 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.m2 25 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 26 | restore-keys: ${{ runner.os }}-m2 27 | 28 | - name: Build with Maven 29 | run: mvn --batch-mode --update-snapshots -Dmaven.javadoc.skip=true verify --file pom.xml -------------------------------------------------------------------------------- /src/main/java/com/valkryst/VNameGenerator/generator/GrammarGenerator.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator.generator; 2 | 3 | import com.valkryst.VParser_CFG.ContextFreeGrammar; 4 | import lombok.NonNull; 5 | 6 | import java.util.List; 7 | 8 | public final class GrammarGenerator extends NameGenerator{ 9 | private ContextFreeGrammar contextFreeGrammar; 10 | 11 | /** 12 | * Constructs a GrammarGenerator. 13 | * 14 | * @param rules A set of Context Free Grammar rules. 15 | */ 16 | public GrammarGenerator(final @NonNull List rules) { 17 | setRules(rules); 18 | } 19 | 20 | @Override 21 | public String generate(final int maxLength) { 22 | super.validateMaxLength(maxLength); 23 | 24 | final var sb = new StringBuilder(); 25 | sb.append(contextFreeGrammar.run()); 26 | sb.setLength(Math.min(sb.length(), maxLength)); 27 | return super.capitalize(super.clean(sb)); 28 | } 29 | 30 | /** 31 | * Set a new set of rules. 32 | * 33 | * @param rules A set of Context Free Grammar rules. 34 | */ 35 | public void setRules(final @NonNull List rules) { 36 | if (rules.size() == 0) { 37 | throw new IllegalArgumentException("The list of rules must have at least one rule. It is currently empty."); 38 | } 39 | 40 | contextFreeGrammar = new ContextFreeGrammar(rules.toArray(new String[0])); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/valkryst/VNameGenerator/ConsonantVowelGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator; 2 | 3 | import com.valkryst.VNameGenerator.generator.ConsonantVowelGenerator; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.concurrent.ThreadLocalRandom; 8 | 9 | public class ConsonantVowelGeneratorTest { 10 | private final ConsonantVowelGenerator generator = new ConsonantVowelGenerator(); 11 | 12 | @Test 13 | public void canGenerateName() { 14 | final var result = generator.generate(10).length(); 15 | Assertions.assertTrue(result <= 10); 16 | Assertions.assertTrue(result > 0); 17 | } 18 | 19 | @Test 20 | public void canGenerateNameWithArbitraryLength() { 21 | final var threadLocalRandom = ThreadLocalRandom.current(); 22 | 23 | for (int i = 0 ; i < 4 ; i++) { 24 | final int maxLength = threadLocalRandom.nextInt(1, 75); 25 | final var result = generator.generate(maxLength).length(); 26 | Assertions.assertTrue(result <= maxLength); 27 | Assertions.assertTrue(result > 0); 28 | } 29 | } 30 | 31 | @Test 32 | public void cannotGenerateNameWithZeroAsMaxLength() { 33 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 34 | generator.generate(0); 35 | }); 36 | } 37 | 38 | @Test 39 | public void canSetConsonants() throws NoSuchFieldException, IllegalAccessException { 40 | final String[] consonants = new String[] { "z" }; 41 | generator.setConsonants(consonants); 42 | 43 | final var field = generator.getClass().getDeclaredField("consonants"); 44 | field.setAccessible(true); 45 | 46 | Assertions.assertArrayEquals(consonants, (String[]) field.get(generator)); 47 | } 48 | 49 | @Test 50 | public void canSetVowels() throws NoSuchFieldException, IllegalAccessException { 51 | final String[] vowels = new String[] { "z" }; 52 | generator.setVowels(vowels); 53 | 54 | final var field = generator.getClass().getDeclaredField("vowels"); 55 | field.setAccessible(true); 56 | 57 | Assertions.assertArrayEquals(vowels, (String[]) field.get(generator)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/valkryst/VNameGenerator/generator/ConsonantVowelGenerator.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator.generator; 2 | 3 | import lombok.NonNull; 4 | 5 | public final class ConsonantVowelGenerator extends NameGenerator { 6 | /** A set of consonants. */ 7 | private String[] consonants; 8 | /** A set of vowels. */ 9 | private String[] vowels; 10 | 11 | /** Constructs a new ConsonantVowelGenerator. */ 12 | public ConsonantVowelGenerator() { 13 | setConsonants(new String[] { 14 | "al", "an", "ar", "as", "at", "ea", "ed", "en", "er", "es", "ha", "he", "hi", "in", "is", "it", 15 | "le", "me", "nd", "ne", "ng", "nt", "on", "or", "ou", "re", "se", "st", "te", "th", "ti", "to", 16 | "ve", "wa", "it" 17 | }); 18 | 19 | setVowels(new String[] { "a", "e", "i", "o", "u", "y" }); 20 | } 21 | 22 | @Override 23 | public String generate(int maxLength) { 24 | super.validateMaxLength(maxLength); 25 | maxLength = super.randomizeMaxLength(maxLength); 26 | 27 | final var stringBuilder = new StringBuilder(); 28 | 29 | String temp; 30 | while (stringBuilder.length() < maxLength) { 31 | if (maxLength % 2 == 0) { 32 | temp = super.randomArrayElement(vowels); 33 | } else { 34 | temp = super.randomArrayElement(consonants); 35 | } 36 | 37 | stringBuilder.append(temp); 38 | } 39 | 40 | while (stringBuilder.length() > maxLength) { 41 | stringBuilder.deleteCharAt(stringBuilder.length() - 1); 42 | } 43 | 44 | return super.capitalize(super.clean(stringBuilder)); 45 | } 46 | 47 | /** 48 | * Sets a new set of consonants. 49 | * 50 | * @param consonants A set of consonants. 51 | */ 52 | public void setConsonants(final @NonNull String[] consonants) { 53 | if (consonants.length == 0) { 54 | throw new IllegalArgumentException("The array of consonants must have at least one element. It is currently empty."); 55 | } 56 | 57 | super.lowercaseAllElements(consonants); 58 | 59 | this.consonants = consonants; 60 | } 61 | 62 | /** 63 | * Sets a new set of vowels. 64 | * 65 | * @param vowels A set of vowels. 66 | */ 67 | public void setVowels(final @NonNull String[] vowels) { 68 | if (consonants.length == 0) { 69 | throw new IllegalArgumentException("The array of vowels must have at least one element. It is currently empty."); 70 | } 71 | 72 | super.lowercaseAllElements(vowels); 73 | 74 | this.vowels = vowels; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/valkryst/VNameGenerator/generator/NameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator.generator; 2 | 3 | import lombok.NonNull; 4 | 5 | import java.util.Locale; 6 | import java.util.concurrent.ThreadLocalRandom; 7 | 8 | public abstract class NameGenerator { 9 | /** 10 | * Generates a name of at most {@code maxLength} characters. 11 | * 12 | * @param maxLength Maximum length of the generated name. 13 | * @return The name. 14 | */ 15 | abstract String generate(final int maxLength); 16 | 17 | /** 18 | * Determines the validity of a maximum length 19 | * 20 | * @param maxLength A maximum length. 21 | * @throws IllegalArgumentException If {@code maxLength} <= 0. 22 | */ 23 | protected final void validateMaxLength(final int maxLength) throws IllegalArgumentException { 24 | if (maxLength <= 0) { 25 | throw new IllegalArgumentException("The maximum length, which is currently " + maxLength + " must be at least 1."); 26 | } 27 | } 28 | 29 | protected final void lowercaseAllElements(final @NonNull String[] array) { 30 | for (int i = 0 ; i < array.length ; i++) { 31 | array[i] = array[i].toLowerCase(); 32 | } 33 | } 34 | 35 | /** 36 | * Capitalizes the string held by a {@link StringBuilder}. 37 | * 38 | * @param sb A {@link StringBuilder}. 39 | * @return The capitalized string. 40 | */ 41 | protected String capitalize(final StringBuilder sb) { 42 | return sb.substring(0, 1).toUpperCase(Locale.ROOT) + sb.substring(1); 43 | } 44 | 45 | /** 46 | * Cleans the string held by a {@link StringBuilder} by performing the 47 | * following operations: 48 | * 49 | * 57 | * 58 | * @param sb A {@link StringBuilder}. 59 | * @return The cleaned {@link StringBuilder}. 60 | */ 61 | protected StringBuilder clean(final StringBuilder sb) { 62 | int codePoint = sb.codePointAt(0); 63 | if (!Character.isAlphabetic(codePoint)) { 64 | sb.deleteCharAt(0); 65 | } 66 | 67 | codePoint = sb.codePointAt(sb.length() - 1); 68 | if (!Character.isAlphabetic(codePoint)) { 69 | sb.deleteCharAt(sb.length() - 1); 70 | } 71 | 72 | return sb; 73 | } 74 | 75 | /** 76 | * Returns a random element from the given array. 77 | * 78 | * @param array An array. 79 | * @return A random element from the given array. 80 | */ 81 | protected String randomArrayElement(final String[] array) { 82 | if (array.length == 0) { 83 | throw new ArrayIndexOutOfBoundsException("The array is empty."); 84 | } 85 | 86 | return array[ThreadLocalRandom.current().nextInt(array.length)]; 87 | } 88 | 89 | /** 90 | * Produces a random max length which is guaranteed to be between 91 | * (0.5 * maxLength) and maxLength. 92 | * 93 | * @param maxLength The initial max length. 94 | * @return The randomized max length. 95 | */ 96 | protected int randomizeMaxLength(final int maxLength) { 97 | return ThreadLocalRandom.current().nextInt( 98 | (int) (maxLength * 0.5), 99 | maxLength + 1 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.Valkryst 8 | VNameGenerator 9 | 2025.10.1-fix 10 | jar 11 | VNameGenerator 12 | 13 | 14 | 17 15 | UTF-8 16 | UTF-8 17 | 18 | 19 | 20 | 21 | jitpack.io 22 | https://jitpack.io 23 | 24 | 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.42 31 | provided 32 | 33 | 34 | 35 | com.github.Valkryst 36 | VParser_CFG 37 | 2025.10.2 38 | 39 | 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter-engine 44 | 6.0.1 45 | test 46 | 47 | 48 | org.junit.jupiter 49 | junit-jupiter-params 50 | 6.0.1 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-compiler-plugin 60 | 3.14.1 61 | 62 | ${maven.compiler.release} 63 | ${maven.compiler.release} 64 | 65 | 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-surefire-plugin 71 | 3.5.4 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-javadoc-plugin 78 | 3.12.0 79 | 88 | 89 | ${java.home}/bin/javadoc 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/com/valkryst/VNameGenerator/generator/CombinatorialGenerator.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator.generator; 2 | 3 | import lombok.NonNull; 4 | 5 | public final class CombinatorialGenerator extends NameGenerator { 6 | /** A set of name beginnings. */ 7 | private String[] beginnings; 8 | /** A set of name middles. */ 9 | private String[] middles; 10 | /** A set of name endings. */ 11 | private String[] endings; 12 | 13 | /** 14 | * Constructs a new CombinatorialGenerator. 15 | * 16 | * @param beginnings A set of name beginnings. 17 | * @param endings A set of name endings. 18 | */ 19 | public CombinatorialGenerator(final @NonNull String[] beginnings, final @NonNull String[] endings) { 20 | this(beginnings, null, endings); 21 | } 22 | 23 | /** 24 | * Constructs a new CombinatorialGenerator. 25 | * 26 | * @param beginnings A set of name beginnings. 27 | * @param middles A set of name middles. 28 | * @param endings A set of name endings. 29 | * 30 | * @throws IllegalArgumentException 31 | * If the lists of beginnings or endings are null or empty. 32 | */ 33 | public CombinatorialGenerator(final @NonNull String[] beginnings, final String[] middles, final @NonNull String[] endings) { 34 | if (beginnings.length == 0) { 35 | throw new IllegalArgumentException("The array of beginnings must have at least one element. It is currently empty."); 36 | } 37 | 38 | if (endings.length == 0) { 39 | throw new IllegalArgumentException("The array of endings must have at least one element. It is currently empty."); 40 | } 41 | 42 | setBeginnings(beginnings); 43 | setMiddles(middles); 44 | setEndings(endings); 45 | } 46 | 47 | @Override 48 | public String generate(int maxLength) { 49 | super.validateMaxLength(maxLength); 50 | maxLength = super.randomizeMaxLength(maxLength); 51 | 52 | final var stringBuilder = new StringBuilder(); 53 | 54 | final var beginning = super.randomArrayElement(beginnings); 55 | stringBuilder.append(beginning); 56 | 57 | if (middles.length != 0) { 58 | while (stringBuilder.length() < maxLength) { 59 | stringBuilder.append(super.randomArrayElement(middles)); 60 | } 61 | } 62 | 63 | if (maxLength > 1) { 64 | final var temp = super.randomArrayElement(endings); 65 | stringBuilder.replace(maxLength - temp.length(), maxLength, temp); 66 | } 67 | 68 | return super.capitalize(super.clean(stringBuilder)); 69 | } 70 | 71 | /** 72 | * Sets a new set of beginnings. 73 | * 74 | * @param beginnings A set of beginnings. 75 | */ 76 | public void setBeginnings(final @NonNull String[] beginnings) { 77 | if (beginnings.length == 0) { 78 | throw new IllegalArgumentException("The array of beginnings must have at least one element. It is currently empty."); 79 | } 80 | 81 | super.lowercaseAllElements(beginnings); 82 | 83 | this.beginnings = beginnings; 84 | } 85 | 86 | /** 87 | * Sets a new set of middles. 88 | * 89 | * @param middles A set of middles. 90 | */ 91 | public void setMiddles(final String[] middles) { 92 | if (middles == null || middles.length == 0) { 93 | this.middles = new String[0]; 94 | return; 95 | } 96 | 97 | super.lowercaseAllElements(middles); 98 | 99 | this.middles = middles; 100 | } 101 | 102 | /** 103 | * Sets a new set of endings. 104 | * 105 | * @param endings A set of endings. 106 | */ 107 | public void setEndings(final @NonNull String[] endings) { 108 | if (endings.length == 0) { 109 | throw new IllegalArgumentException("The array of endings must have at least one element. It is currently empty."); 110 | } 111 | 112 | super.lowercaseAllElements(endings); 113 | 114 | this.endings = endings; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/valkryst/VNameGenerator/generator/MarkovGenerator.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator.generator; 2 | 3 | import lombok.NonNull; 4 | 5 | import java.util.HashMap; 6 | import java.util.Locale; 7 | import java.util.Map; 8 | import java.util.concurrent.ThreadLocalRandom; 9 | 10 | public final class MarkovGenerator extends NameGenerator { 11 | private final Map entries = new HashMap<>(); 12 | 13 | /** 14 | * Constructs a MarkovGenerator and trains it with a set of names. 15 | * 16 | * @param names A set of names. 17 | */ 18 | public MarkovGenerator(final @NonNull String[] names) { 19 | if (names.length == 0) { 20 | throw new IllegalArgumentException("The array of training names must have at least one element. It is currently empty."); 21 | } 22 | 23 | final Map> builders = new HashMap<>(); 24 | 25 | for (String string : names) { 26 | if (string.length() < 2) { 27 | return; 28 | } 29 | 30 | string = string.toLowerCase(Locale.ROOT); 31 | 32 | for (int i = 2 ; i < string.length() ; i++) { 33 | final String charGroup = string.substring(i - 2, i); 34 | final char subsequentChar = string.charAt(i); 35 | 36 | builders.putIfAbsent(charGroup, new HashMap<>()); 37 | 38 | builders.get(charGroup).putIfAbsent(subsequentChar, 0); 39 | builders.get(charGroup).put(string.charAt(i), builders.get(charGroup).get(subsequentChar) + 1); 40 | } 41 | } 42 | 43 | for (final Map.Entry> entry : builders.entrySet()) { 44 | entries.put(entry.getKey(), new Entry(entry.getValue())); 45 | } 46 | } 47 | 48 | @Override 49 | public String generate(int maxLength) { 50 | super.validateMaxLength(maxLength); 51 | 52 | /* 53 | * Even a small Markov Chain is capable of generating long names, but 54 | * the names can be of a subjectively poor quality due to repetition 55 | * and length. 56 | * 57 | * e.g. Poor Quality Names 58 | * 59 | * Rlasaìdhagaidhairidh 60 | * Itirigiosaidhagsaìde 61 | * Nsaireabalaileall 62 | * Nanagaililiormailili 63 | * Unagaghreagagaghnait 64 | * 65 | * To generate names that are of a subjectively higher quality, we 66 | * randomize maxLength. This partially solves the issues of repetition 67 | * and length. 68 | * 69 | * We could allow maxLength randomization by users, but we want this 70 | * function to produce subjectively good results without needing to 71 | * know much about its internals. 72 | */ 73 | maxLength = super.randomizeMaxLength(maxLength); 74 | 75 | final var random = ThreadLocalRandom.current(); 76 | final var sb = new StringBuilder(); 77 | sb.append((String) entries.keySet().toArray()[random.nextInt(entries.size())]); 78 | 79 | for (int i = 2 ; i < maxLength ; i++) { 80 | final var substring = sb.substring(i - 2, i); 81 | 82 | final var entry = entries.get(substring); 83 | if (entry == null) { 84 | break; 85 | } 86 | 87 | sb.appendCodePoint(entry.randomCodePoint()); 88 | } 89 | 90 | return super.capitalize(super.clean(sb)); 91 | } 92 | 93 | private static final class Entry { 94 | private final int[] codePoints; 95 | private final float[] sums; 96 | 97 | public Entry(final @NonNull Map occurrences) { 98 | codePoints = new int[occurrences.size()]; 99 | sums = new float[occurrences.size()]; 100 | 101 | int index = 0; 102 | float cumulativeSum = 0; 103 | 104 | for (final var entry : occurrences.entrySet()) { 105 | codePoints[index] = entry.getKey(); 106 | 107 | cumulativeSum += entry.getValue(); 108 | sums[index] = cumulativeSum; 109 | 110 | index++; 111 | } 112 | } 113 | 114 | public int randomCodePoint() { 115 | /* 116 | * This algorithm was inspired by the following StackExchange 117 | * answer: 118 | * 119 | * https://softwareengineering.stackexchange.com/a/150618 120 | */ 121 | final var randomSum = ThreadLocalRandom.current().nextDouble(sums[sums.length - 1] + 0.01f); 122 | 123 | for (int i = 0 ; i < sums.length ; i++) { 124 | if (randomSum <= sums[i]) { 125 | return codePoints[i]; 126 | } 127 | } 128 | 129 | return 0; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/com/valkryst/VNameGenerator/CombinatorialGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.valkryst.VNameGenerator; 2 | 3 | import com.valkryst.VNameGenerator.generator.CombinatorialGenerator; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.concurrent.ThreadLocalRandom; 8 | 9 | public class CombinatorialGeneratorTest { 10 | private final static String[] BEGINNINGS = new String[] { "a", "b", "c" }; 11 | private final static String[] MIDDLES = new String[] { "d", "e", "f" }; 12 | private final static String[] ENDINGS = new String[] { "g", "h", "i" }; 13 | 14 | private final CombinatorialGenerator generator = new CombinatorialGenerator(BEGINNINGS, MIDDLES, ENDINGS); 15 | 16 | @Test 17 | public void canConstructWithBeginningsAndEndings() throws NoSuchFieldException, IllegalAccessException { 18 | final var generator = new CombinatorialGenerator(BEGINNINGS, ENDINGS); 19 | 20 | var field = generator.getClass().getDeclaredField("beginnings"); 21 | field.setAccessible(true); 22 | Assertions.assertArrayEquals(BEGINNINGS, (String[]) field.get(generator)); 23 | 24 | field = generator.getClass().getDeclaredField("endings"); 25 | field.setAccessible(true); 26 | Assertions.assertArrayEquals(ENDINGS, (String[]) field.get(generator)); 27 | } 28 | 29 | @Test 30 | public void canConstructWithBeginningsMiddlesAndEndings() throws NoSuchFieldException, IllegalAccessException { 31 | final var generator = new CombinatorialGenerator(BEGINNINGS, MIDDLES, ENDINGS); 32 | 33 | var field = generator.getClass().getDeclaredField("beginnings"); 34 | field.setAccessible(true); 35 | Assertions.assertArrayEquals(BEGINNINGS, (String[]) field.get(generator)); 36 | 37 | field = generator.getClass().getDeclaredField("middles"); 38 | field.setAccessible(true); 39 | Assertions.assertArrayEquals(MIDDLES, (String[]) field.get(generator)); 40 | 41 | field = generator.getClass().getDeclaredField("endings"); 42 | field.setAccessible(true); 43 | Assertions.assertArrayEquals(ENDINGS, (String[]) field.get(generator)); 44 | } 45 | 46 | @Test 47 | public void canConstructWithNullMiddles() throws NoSuchFieldException, IllegalAccessException { 48 | final var generator = new CombinatorialGenerator(BEGINNINGS, null, ENDINGS); 49 | 50 | var field = generator.getClass().getDeclaredField("beginnings"); 51 | field.setAccessible(true); 52 | Assertions.assertArrayEquals(BEGINNINGS, (String[]) field.get(generator)); 53 | 54 | field = generator.getClass().getDeclaredField("middles"); 55 | field.setAccessible(true); 56 | Assertions.assertEquals(0, ((String[]) field.get(generator)).length); 57 | 58 | field = generator.getClass().getDeclaredField("endings"); 59 | field.setAccessible(true); 60 | Assertions.assertArrayEquals(ENDINGS, (String[]) field.get(generator)); 61 | } 62 | 63 | @Test 64 | public void canGenerateName() { 65 | final var result = generator.generate(10).length(); 66 | Assertions.assertTrue(result <= 10); 67 | Assertions.assertTrue(result > 0); 68 | } 69 | 70 | @Test 71 | public void canGenerateNameWithArbitraryLength() { 72 | final var threadLocalRandom = ThreadLocalRandom.current(); 73 | 74 | for (int i = 0 ; i < 4 ; i++) { 75 | final int maxLength = threadLocalRandom.nextInt(1, 75); 76 | final var result = generator.generate(maxLength).length(); 77 | Assertions.assertTrue(result <= maxLength); 78 | Assertions.assertTrue(result > 0); 79 | } 80 | } 81 | 82 | @Test 83 | public void cannotGenerateNameWithZeroAsMaxLength() { 84 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 85 | generator.generate(0); 86 | }); 87 | } 88 | 89 | @Test 90 | public void canSetBeginnings() throws NoSuchFieldException, IllegalAccessException { 91 | final String[] beginnings = new String[] { "z" }; 92 | generator.setBeginnings(beginnings); 93 | 94 | final var field = generator.getClass().getDeclaredField("beginnings"); 95 | field.setAccessible(true); 96 | Assertions.assertArrayEquals(beginnings, (String[]) field.get(generator)); 97 | } 98 | 99 | @Test 100 | public void canSetMiddles() throws NoSuchFieldException, IllegalAccessException { 101 | final String[] middles = new String[] { "z" }; 102 | generator.setMiddles(middles); 103 | 104 | final var field = generator.getClass().getDeclaredField("middles"); 105 | field.setAccessible(true); 106 | Assertions.assertArrayEquals(middles, (String[]) field.get(generator)); 107 | } 108 | 109 | @Test 110 | public void canSetMiddlesWithNull() throws NoSuchFieldException, IllegalAccessException { 111 | final var field = generator.getClass().getDeclaredField("middles"); 112 | field.setAccessible(true); 113 | Assertions.assertNotEquals(0, ((String[]) field.get(generator)).length); 114 | 115 | generator.setMiddles(null); 116 | Assertions.assertEquals(0, ((String[]) field.get(generator)).length); 117 | } 118 | 119 | @Test 120 | public void canSetEndings() throws NoSuchFieldException, IllegalAccessException { 121 | final String[] endings = new String[] { "z" }; 122 | generator.setEndings(endings); 123 | 124 | final var field = generator.getClass().getDeclaredField("endings"); 125 | field.setAccessible(true); 126 | Assertions.assertArrayEquals(endings, (String[]) field.get(generator)); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | As of this project's inception, the included algorithms are the only ones my research has turned up. If you know of 2 | any other _unique_ algorithms for name generation, then please let me know. I would love to implement them. 3 | 4 | ## Table of Contents 5 | 6 | * [Installation](https://github.com/Valkryst/VNameGenerator#installation) 7 | * [Gradle](https://github.com/Valkryst/VNameGenerator#-gradle) 8 | * [Maven](https://github.com/Valkryst/VNameGenerator#-maven) 9 | * [sbt](https://github.com/Valkryst/VNameGenerator#-scala-sbt) 10 | * [Algorithms](https://github.com/Valkryst/VNameGenerator#algorithms) 11 | * [Combinatorial](https://github.com/Valkryst/VNameGenerator#combinatorial) 12 | * [Consonant Vowel](https://github.com/Valkryst/VNameGenerator#consonant-vowel) 13 | * [Context Free Grammar](https://github.com/Valkryst/VNameGenerator#context-free-grammar) 14 | * [Markov Chain](https://github.com/Valkryst/VNameGenerator#markov-chain) 15 | 16 | ## Installation 17 | 18 | VNameGenerator is hosted on the [JitPack package repository](https://jitpack.io/#Valkryst/VNameGenerator) 19 | which supports Gradle, Maven, and sbt. 20 | 21 | ### ![Gradle](https://i.imgur.com/qtc6bXq.png?1) Gradle 22 | 23 | Add JitPack to your `build.gradle` at the end of repositories. 24 | 25 | ``` 26 | allprojects { 27 | repositories { 28 | ... 29 | maven { url 'https://jitpack.io' } 30 | } 31 | } 32 | ``` 33 | 34 | Add VNameGenerator as a dependency. 35 | 36 | ``` 37 | dependencies { 38 | implementation 'com.github.Valkryst:VNameGenerator:2025.10.1-fix' 39 | } 40 | ``` 41 | 42 | ### ![Maven](https://i.imgur.com/2TZzobp.png?1) Maven 43 | 44 | Add JitPack as a repository. 45 | 46 | ``` xml 47 | 48 | 49 | jitpack.io 50 | https://jitpack.io 51 | 52 | 53 | ``` 54 | Add VNameGenerator as a dependency. 55 | 56 | ```xml 57 | 58 | com.github.Valkryst 59 | VNameGenerator 60 | 2025.10.1-fix 61 | 62 | ``` 63 | 64 | ### ![Scala SBT](https://i.imgur.com/Nqv3mVd.png?1) Scala SBT 65 | 66 | Add JitPack as a resolver. 67 | 68 | ``` 69 | resolvers += "jitpack" at "https://jitpack.io" 70 | ``` 71 | 72 | Add VNameGenerator as a dependency. 73 | 74 | ``` 75 | libraryDependencies += "com.github.Valkryst" % "VNameGenerator" % "2025.10.1-fix" 76 | ``` 77 | 78 | ## Algorithms 79 | 80 | The _Combinatorial_, _Consonant Vowel_, and _Markov Chain_ algorithms will 81 | generate names with a length of `(maxLength * 0.5)` to `maxLength`. This was 82 | done to improve the quality of the generated names. 83 | 84 | Names are guaranteed to begin with a capital character. 85 | 86 | ### Combinatorial 87 | 1. A beginning is chosen and added to the name. 88 | 2. While the name is less than the maximum length, middles are chosen and added 89 | to the name. 90 | 3. An ending is chosen and added to the end of the name, overwriting existing 91 | characters in order to fit within the maximum length. 92 | 93 | ```java 94 | public class Example { 95 | public static void main(final String[] args) { 96 | final var beginnings = new String[] { "th", "bo", "ja", "fu" }; 97 | final var middles = new String[] { "la", "su", "dhu", "li", "da", "zk", "fr"}; 98 | final var endings = new String[] { "r", "t", "gh", "or", "al", "ar", "is" }; 99 | 100 | final var generator = new CombinatorialGenerator(beginnings, middles, endings); 101 | 102 | for (int i = 0 ; i < 20 ; i++) { 103 | System.out.println(generator.generate(10)); 104 | } 105 | } 106 | } 107 | ``` 108 | ``` 109 | Thlifrdhgh 110 | Bolalar 111 | Fusuladagh 112 | Thlis 113 | Thlagh 114 | Thlilifis 115 | Jadasugh 116 | Fuzklr 117 | Jadhular 118 | Jadaal 119 | Bosulaligh 120 | Jafrgh 121 | Jadar 122 | Bodhugh 123 | Bolazkr 124 | Thlidadis 125 | Fudhr 126 | Thzklaar 127 | Jazklidgh 128 | Bozkr 129 | ``` 130 | 131 | ### Consonant Vowel 132 | 1. A consonant is chosen and added to the name. 133 | 2. A vowel is chosen and added to the name. 134 | 3. Repeat the previous steps until the name is equal to the maximum length. 135 | 136 | ```java 137 | public class Example { 138 | public static void main(final String[] args) { 139 | final var generator = new ConsonantVowelGenerator(); 140 | 141 | for (int i = 0 ; i < 20 ; i++) { 142 | System.out.println(generator.generate(10)); 143 | } 144 | } 145 | } 146 | ``` 147 | ``` 148 | Itleanas 149 | Netemete 150 | Auieie 151 | Stvetoerit 152 | Ataseander 153 | Ouitha 154 | Auyuyieyoe 155 | Arhaea 156 | Aauuiaui 157 | Thatatea 158 | Seenesor 159 | Itstisme 160 | Titire 161 | Eouuyuoo 162 | Leteisha 163 | Ayueea 164 | Waatanto 165 | Eoyouo 166 | Eeoyieuuui 167 | Haontiseal 168 | ``` 169 | 170 | ### Context Free Grammar 171 | Click [here](http://www.tutorialspoint.com/automata_theory/context_free_grammar_introduction.htm) 172 | to learn more about CFGs and how they work. 173 | 174 | I do not recommend using this method as it is difficult to create a set of rules 175 | that results in good quality names, and a large variety of names. 176 | 177 | ```java 178 | public class Example { 179 | public static void main(final String[] args) { 180 | /* 181 | * This set of rules was created using the following set of names. 182 | * 183 | * Balin, Bifur, Bofur, Bombur, Borin, Dain, Dis, Dori, Dwalin, Farin, 184 | * Fili, Floi, Frar, Frerin, Fror, Fundin, Gaiml, Gimli, Gloin, Groin, 185 | * Gror, Ibun, Khim, Kili, Loni, Mim, Nain, Nali, Nar, Narvi, Nori, Oin, 186 | * Ori, Telchar, Thorin, Thrain, Thror 187 | */ 188 | final List rules = new ArrayList<>(); 189 | rules.add("S B D F G I K L M N O T"); 190 | rules.add("A a aL aI aR"); 191 | rules.add("B b bA bI bO"); 192 | rules.add("C c"); 193 | rules.add("D d dA dI dO dW dU"); 194 | rules.add("E e eR eL"); 195 | rules.add("F f fA fI fL fR fU fO"); 196 | rules.add("G g gA gI gL gR"); 197 | rules.add("H h hI hA"); 198 | rules.add("I i"); 199 | rules.add("K k kH kI"); 200 | rules.add("L l lO"); 201 | rules.add("M m mI"); 202 | rules.add("N n nA nO"); 203 | rules.add("O o oI oR"); 204 | rules.add("P p"); 205 | rules.add("Q q"); 206 | rules.add("R r rI rO rV"); 207 | rules.add("S s"); 208 | rules.add("T t tE tH"); 209 | rules.add("U u uR uN"); 210 | rules.add("V v"); 211 | rules.add("W w wA"); 212 | rules.add("X x"); 213 | rules.add("Y y"); 214 | rules.add("Z z"); 215 | 216 | final var generator = new GrammarGenerator(rules); 217 | 218 | final int maxLength = 10; 219 | String temp; 220 | for (int i = 0 ; i < 20 ; i++) { 221 | do { 222 | temp = generator.generate(10); 223 | } while (temp.length() < (maxLength / 2)); 224 | 225 | System.out.println(temp); 226 | } 227 | } 228 | } 229 | ``` 230 | ``` 231 | Ororv 232 | Dwarv 233 | Naloro 234 | Ororo 235 | Grori 236 | Daloi 237 | Narori 238 | Nalorv 239 | Noroi 240 | Terororoi 241 | Dunai 242 | Flori 243 | Glori 244 | Thalo 245 | Funal 246 | Baloroi 247 | Khari 248 | Ororv 249 | Thalor 250 | Dwari 251 | ``` 252 | 253 | ### Markov Chain 254 | Click [here](https://en.wikipedia.org/wiki/Markov_chain) to learn more about 255 | Markov Chains and how they work. 256 | 257 | I recommend using this method with a large set of training names. Smaller sets 258 | will result in the generation of many similar names, whereas larger sets will 259 | result in more unique and varied names. 260 | 261 | ```java 262 | public class Example { 263 | public static void main(final String[] args) { 264 | final String[] trainingNames = new String[] { 265 | "ailios", "ailisl", "aimil", "aingealag", "anabla", "anna", 266 | "aoife", "barabal", "baraball", "barabla", "bearnas", "beasag", 267 | "beathag", "beileag", "beitidh", "beitiris", "beitris", 268 | "bhioctoria", "brighde", "brìde", "cairistiòna", "cairistìne", 269 | "cairistìona", "caitir", "caitlin", "caitrìona", "calaminag", 270 | "catrìona", "ceana", "ceit", "ceiteag", "ceitidh", "ciorsdan", 271 | "ciorstag", "ciorstaidh", "ciorstan", "cotrìona", "criosaidh", 272 | "curstag", "curstaidh", "deirdre", "deòiridh", "deònaidh", 273 | "dior-bhorgàil", "diorbhail", "doileag", "doilidh", "doirin", 274 | "dolag", "ealasaid", "eamhair", "eilidh", "eimhir", "eiric", 275 | "eithrig", "eubh", "eubha", "èibhlin", "fionnaghal", "fionnuala", 276 | "floireans", "flòraidh", "frangag", "giorsail", "giorsal", 277 | "gormall", "gormlaith", "isbeil", "iseabail", "iseabal", 278 | "leagsaidh", "leitis", "lili", "liùsaidh", "lucrais", "lìosa", 279 | "magaidh", "maighread", "mairead", "mairearad", "malamhìn", 280 | "malmhìn", "marsail", "marsaili", "marta", "milread", "moibeal", 281 | "moire", "moireach", "muire", "muireall", "màili", "màiri", 282 | "mòr", "mòrag", "nansaidh", "oighrig", "olibhia", "peanaidh", 283 | "peigi", "raghnaid", "raodhailt", "raonaid", "raonaild", "rut", 284 | "seasaìdh", "seonag", "seònaid", "simeag", "siubhan", "siùsaidh", 285 | "siùsan", "sorcha", "stineag", "sìle", "sìleas", "sìlis", "sìne", 286 | "sìneag", "sìonag", "teasag", "teàrlag", "ùna", "una" 287 | }; 288 | 289 | final MarkovGenerator generator = new MarkovGenerator(trainingNames); 290 | 291 | for (int i = 0 ; i < 20 ; i++) { 292 | System.out.println(generator.generate(10)); 293 | } 294 | } 295 | } 296 | ``` 297 | ``` 298 | Sorsag 299 | Iria 300 | Unabarst 301 | Nualasana 302 | Tirdreal 303 | Craoilisl 304 | Nearaidha 305 | Lrealairea 306 | Nuala 307 | Almhalamh 308 | Reabarnaig 309 | Ireag 310 | Geabl 311 | Abara 312 | Unaba 313 | Ighang 314 | Beitrìd 315 | Ciorcha 316 | Caimeabal 317 | Mhailil 318 | ``` 319 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | 178 | Copyright 2021 Valkryst --------------------------------------------------------------------------------