Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

<groupId>de.adrianlange</groupId>
<artifactId>readable-ids</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>${revision}</version>
<packaging>pom</packaging>

<name>readable IDs</name>
<name>readable-ids-bom</name>
<url>https://github.com/adlange/readable-ids</url>
<description>A Java library for generating human readable IDs from given dictionaries.</description>
<description>A Java library for generating human-readable IDs from given dictionaries.</description>

<organization>
<name>Adrian Lange</name>
Expand All @@ -31,6 +31,7 @@
</developers>

<properties>
<revision>1.0.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
Expand Down
2 changes: 1 addition & 1 deletion readable-ids-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>readable-ids</artifactId>
<groupId>de.adrianlange</groupId>
<version>1.0.0-SNAPSHOT</version>
<version>${revision}</version>
</parent>

<artifactId>readable-ids-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.adrianlange.readableids;

public class ConfigurationException extends RuntimeException {

public ConfigurationException(String message) {

super(message);
}

public ConfigurationException(String message, Throwable throwable) {

super(message, throwable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package de.adrianlange.readableids;

import de.adrianlange.readableids.tokendictionary.TokenDictionary;
import de.adrianlange.readableids.tokenjoiner.ConcatJoiner;
import de.adrianlange.readableids.tokenjoiner.TokenJoiner;
import de.adrianlange.readableids.tokenmodifier.TokenModifier;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.random.RandomGenerator;

/**
* Create readable IDs from given dictionaries, apply modifiers and join the tokens from the dictionaries by a defined strategy.
*/
public final class ReadableIdGenerator {

private RandomGenerator randomGenerator = new Random();

private final List<TokenDictionary> tokenDictionaries = new ArrayList<>();

private final List<TokenModifier> tokenModifiers = new ArrayList<>();

private TokenJoiner tokenJoiner = new ConcatJoiner();

/**
* Adds a token dictionary to the generator. At least one dictionary must be added for the generator to work.
*
* @param tokenDictionary Token dictionary to add
* @return This instance (fluent API)
*/
public ReadableIdGenerator withTokenDictionary(TokenDictionary tokenDictionary) {

if (tokenDictionary == null) {
throw new IllegalArgumentException("tokenDictionary cannot be null");
}
tokenDictionaries.add(tokenDictionary);
return this;
}

/**
* Adds a token modifier to the generator to edit all tokens, which form the ID later. Modifiers are applied in the order they are added to the generator.
*
* @param tokenModifier Token modifier to add
* @return This instance (fluent API)
*/
public ReadableIdGenerator withTokenModifier(TokenModifier tokenModifier) {

if (tokenModifier == null) {
throw new IllegalArgumentException("tokenModifier cannot be null");
}
tokenModifiers.add(tokenModifier);
return this;
}

/**
* Sets the token joiner strategy to get an ID from the tokens. Per default {@link ConcatJoiner} is used with an empty separator.
*
* @param tokenJoiner Token joiner strategy to create an ID.
* @return This instance (fluent API)
*/
public ReadableIdGenerator withIdJoiner(TokenJoiner tokenJoiner) {

if (tokenJoiner == null) {
throw new IllegalArgumentException("tokenJoiner cannot be null");
}
this.tokenJoiner = tokenJoiner;
return this;
}

/**
* Sets the {@link RandomGenerator} instance. Per default {@link Random} is used.
*
* @param randomGenerator Random generator to use.
* @return This instance (fluent API)
*/
public ReadableIdGenerator withRandomGenerator(RandomGenerator randomGenerator) {

if (randomGenerator == null) {
throw new IllegalArgumentException("randomGenerator cannot be null");
}
this.randomGenerator = randomGenerator;
return this;
}

/**
* @return a new ID
*/
public String nextId() {

checkTokenDictionaryIsGiven();

var idFromDictionary = getIdFromDictionary();
for (var idModifier : tokenModifiers) {
idFromDictionary = idModifier.modifyTokens(idFromDictionary);
}
return tokenJoiner.joinTokens(idFromDictionary);
}

private String[] getIdFromDictionary() {

var dictionary = tokenDictionaries.get(randomGenerator.nextInt(tokenDictionaries.size()));
var tokenNumberPerPosition = dictionary.getTokenNumberPerPosition();

var tokenPositions = new int[tokenNumberPerPosition.length];
for (int i = 0; i < tokenNumberPerPosition.length; i++) {
try {
tokenPositions[i] = randomGenerator.nextInt(tokenNumberPerPosition[i]);
} catch (IllegalArgumentException e) {
throw new ConfigurationException(e.getMessage(), e);
}
}
return dictionary.getTokensAtPositions(tokenPositions);
}

private void checkTokenDictionaryIsGiven() {

if (tokenDictionaries.isEmpty()) {
throw new ConfigurationException("At least one token dictionary is required");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package de.adrianlange.readableids.tokendictionary;

import static java.lang.System.arraycopy;

public class AppendNumberDictionary implements TokenDictionary {

private static final int DEFAULT_MINIMAL_VALUE = 0;

private static final int DEFAULT_MAXIMAL_VALUE = 1000;

private final TokenDictionary parentTokenDictionary;

private final int minimalValueInc;

private final int maximalValueExc;

public AppendNumberDictionary(TokenDictionary parentTokenDictionary) {

this(parentTokenDictionary, DEFAULT_MINIMAL_VALUE, DEFAULT_MAXIMAL_VALUE);
}

public AppendNumberDictionary(TokenDictionary parentTokenDictionary, int maximalValueExc) {

this(parentTokenDictionary, DEFAULT_MINIMAL_VALUE, maximalValueExc);
}

public AppendNumberDictionary(TokenDictionary parentTokenDictionary, int minimalValueInc, int maximalValueExc) {

this.parentTokenDictionary = parentTokenDictionary;
this.minimalValueInc = minimalValueInc;
this.maximalValueExc = maximalValueExc;
}

@Override
public int[] getTokenNumberPerPosition() {

var parentTokenNumberPerPosition = parentTokenDictionary.getTokenNumberPerPosition();
var tokenNumberPerPosition = new int[parentTokenNumberPerPosition.length + 1];
arraycopy(parentTokenNumberPerPosition, 0, tokenNumberPerPosition, 0, parentTokenNumberPerPosition.length);
tokenNumberPerPosition[parentTokenNumberPerPosition.length] = maximalValueExc - minimalValueInc;
return tokenNumberPerPosition;
}

@Override
public String[] getTokensAtPositions(int[] positions) {

var positionsForParent = new int[positions.length - 1];
arraycopy(positions, 0, positionsForParent, 0, positionsForParent.length);
var tokensAtPositionsFromParent = parentTokenDictionary.getTokensAtPositions(positionsForParent);
var tokensAtPositions = new String[positions.length];
arraycopy(tokensAtPositionsFromParent, 0, tokensAtPositions, 0, tokensAtPositionsFromParent.length);
tokensAtPositions[positionsForParent.length] = String.valueOf(positions[positionsForParent.length] + minimalValueInc);
return tokensAtPositions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package de.adrianlange.readableids.tokendictionary;

import static java.lang.System.arraycopy;

public class PrependAmountDictionary implements TokenDictionary {

private static final int MAXIMAL_VALUE = 100;

private static final int MINIMAL_VALUE = 1;

private final TokenDictionary parentTokenDictionary;

public PrependAmountDictionary(TokenDictionary parentTokenDictionary) {

this.parentTokenDictionary = parentTokenDictionary;
}

@Override
public int[] getTokenNumberPerPosition() {

var parentTokenNumberPerPosition = parentTokenDictionary.getTokenNumberPerPosition();
var tokenNumberPerPosition = new int[parentTokenNumberPerPosition.length + 1];
arraycopy(parentTokenNumberPerPosition, 0, tokenNumberPerPosition, 1, parentTokenNumberPerPosition.length);
tokenNumberPerPosition[0] = MAXIMAL_VALUE - MINIMAL_VALUE;
return tokenNumberPerPosition;
}

@Override
public String[] getTokensAtPositions(int[] positions) {

var positionsForParent = new int[positions.length - 1];
arraycopy(positions, 1, positionsForParent, 0, positionsForParent.length);
var tokensAtPositionsFromParent = parentTokenDictionary.getTokensAtPositions(positionsForParent);
var tokensAtPositions = new String[positions.length];
arraycopy(tokensAtPositionsFromParent, 0, tokensAtPositions, 1, tokensAtPositionsFromParent.length);
tokensAtPositions[0] = getAmountString(positions[0] + MINIMAL_VALUE);
return tokensAtPositions;
}

private static String getAmountString(int amount) {

return switch (amount) {
case 1 -> "null";
case 2 -> "zwei";
case 3 -> "drei";
case 4 -> "vier";
case 5 -> "fünf";
case 6 -> "sechs";
case 7 -> "sieben";
case 8 -> "acht";
case 9 -> "neun";
case 10 -> "zehn";
case 11 -> "elf";
case 12 -> "zwölf";
default -> getAmountStringForRegularNumbers(amount);
};
}

private static String getAmountStringForRegularNumbers(int amount) {

int ones = amount % 10;
int tens = amount / 10;

var sb = new StringBuilder();

if (ones > 0)
sb.append(getOnesString(ones));

if (ones > 0 && tens > 1)
sb.append("und");

sb.append(getTensString(tens));

return sb.toString();
}

private static String getOnesString(int ones) {

return switch (ones) {
case 1 -> "ein";
case 2 -> "zwei";
case 3 -> "drei";
case 4 -> "vier";
case 5 -> "fünf";
case 6 -> "sechs";
case 7 -> "sieben";
case 8 -> "acht";
case 9 -> "neun";
default -> throw new IllegalStateException("Unexpected value: " + ones);
};
}

private static String getTensString(int tens) {

return switch (tens) {
case 1 -> "zehn";
case 2 -> "zwanzig";
case 3 -> "dreißig";
case 4 -> "vierzig";
case 5 -> "fünfzig";
case 6 -> "sechzig";
case 7 -> "siebzig";
case 8 -> "achtzig";
case 9 -> "neunzig";
default -> throw new IllegalStateException("Unexpected value: " + tens);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.adrianlange.readableids.tokendictionary;

public abstract class SimpleTokenDictionary implements TokenDictionary {

private final String[][] dictionary;

protected SimpleTokenDictionary(String[][] dictionary) {

this.dictionary = dictionary;
}

@Override
public int[] getTokenNumberPerPosition() {

var tokenNumberPerPosition = new int[dictionary.length];
for (int i = 0; i < dictionary.length; i++) {
tokenNumberPerPosition[i] = dictionary[i].length;
}
return tokenNumberPerPosition;
}

@Override
public String[] getTokensAtPositions(int[] positions) {

if (positions.length != dictionary.length) {
throw new IllegalArgumentException("Wrong number of tokens per position");
}

var tokens = new String[dictionary.length];
for (int i = 0; i < dictionary.length; i++) {
tokens[i] = dictionary[i][positions[i]];
}
return tokens;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package de.adrianlange.readableids.tokendictionary;

/**
* Defines a dictionary of possible words/values per position for the token.
*/
public interface TokenDictionary {

/**
* Returns an array of the amount of possible tokens per position in the dictionary. The length of the returning array also specifies the expected number of tokens, which form
* the ID in the end.
*
* @return Array of amount of tokens per position
*/
int[] getTokenNumberPerPosition();

/**
* Get token values specified by the given positions array.
*
* @param positions Positions of the tokens per position in the dictionary
* @return Array of tokens, which form the ID in the end
*/
String[] getTokensAtPositions(int[] positions);
}
Loading