Add tweed5-naming-format

This commit is contained in:
2024-07-22 18:00:01 +02:00
parent 1b3bf0ca96
commit 37d64502ad
8 changed files with 514 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.namingformat.api;
public interface NamingFormat {
String[] splitIntoWords(String name);
String joinToName(String[] words);
String name();
}

View File

@@ -0,0 +1,45 @@
package de.siphalor.tweed5.namingformat.api;
import de.siphalor.tweed5.namingformat.impl.NamingFormatImpls;
public class NamingFormats {
public static String convert(String text, NamingFormat sourceFormat, NamingFormat targetFormat) {
return targetFormat.joinToName(sourceFormat.splitIntoWords(text));
}
public static NamingFormat camelCase() {
return NamingFormatImpls.CAMEL_CASE;
}
public static NamingFormat pascalCase() {
return NamingFormatImpls.PASCAL_CASE;
}
public static NamingFormat kebabCase() {
return NamingFormatImpls.KEBAB_CASE;
}
public static NamingFormat upperKebabCase() {
return NamingFormatImpls.UPPER_KEBAB_CASE;
}
public static NamingFormat snakeCase() {
return NamingFormatImpls.SNAKE_CASE;
}
public static NamingFormat upperSnakeCase() {
return NamingFormatImpls.UPPER_SNAKE_CASE;
}
public static NamingFormat spaceCase() {
return NamingFormatImpls.SPACE_CASE;
}
public static NamingFormat upperSpaceCase() {
return NamingFormatImpls.UPPER_SPACE_CASE;
}
public static NamingFormat titleCase() {
return NamingFormatImpls.TITLE_CASE;
}
}

View File

@@ -0,0 +1,42 @@
package de.siphalor.tweed5.namingformat.impl;
import lombok.RequiredArgsConstructor;
import java.util.PrimitiveIterator;
@RequiredArgsConstructor
public class CodePointReader {
int EMPTY_PEEK = Integer.MIN_VALUE;
private final PrimitiveIterator.OfInt codePointIterator;
private int peek = EMPTY_PEEK;
public static CodePointReader ofString(CharSequence input) {
return new CodePointReader(input.codePoints().iterator());
}
public boolean hasNext() {
if (peek != EMPTY_PEEK) {
return true;
} else {
return codePointIterator.hasNext();
}
}
public int next() {
if (peek != EMPTY_PEEK) {
int codepoint = peek;
peek = EMPTY_PEEK;
return codepoint;
} else {
return codePointIterator.nextInt();
}
}
public int peek() {
if (peek == EMPTY_PEEK) {
peek = codePointIterator.nextInt();
}
return peek;
}
}

View File

@@ -0,0 +1,287 @@
package de.siphalor.tweed5.namingformat.impl;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import java.util.ArrayList;
import java.util.PrimitiveIterator;
public class NamingFormatImpls {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
public static final NamingFormat CAMEL_CASE = new NamingFormat() {
@Override
public String name() {
return "camelCase";
}
@Override
public String[] splitIntoWords(String name) {
return splitAtUpperCase(name);
}
@Override
public String joinToName(String[] words) {
if (words.length == 0) {
return "";
}
int totalLength = countTotalWordsLength(words);
StringBuilder stringBuilder = new StringBuilder(totalLength);
appendAllLower(stringBuilder, words[0]);
for (int i = 1; i < words.length; i++) {
appendCapitalized(stringBuilder, words[i]);
}
return stringBuilder.toString();
}
@Override
public String toString() {
return name();
}
};
public static final NamingFormat PASCAL_CASE = new NamingFormat() {
@Override
public String name() {
return "PascalCase";
}
@Override
public String[] splitIntoWords(String name) {
return splitAtUpperCase(name);
}
@Override
public String joinToName(String[] words) {
if (words.length == 0) {
return "";
}
int totalLength = countTotalWordsLength(words);
StringBuilder stringBuilder = new StringBuilder(totalLength);
for (String word : words) {
appendCapitalized(stringBuilder, word);
}
return stringBuilder.toString();
}
@Override
public String toString() {
return name();
}
};
public static final NamingFormat KEBAB_CASE = createLowerDelimitedFormat("kebab-case", '-');
public static final NamingFormat UPPER_KEBAB_CASE = createUpperDelimitedFormat("UPPER-KEBAB-CASE", '-');
public static final NamingFormat SNAKE_CASE = createLowerDelimitedFormat("snake_case", '_');
public static final NamingFormat UPPER_SNAKE_CASE = createUpperDelimitedFormat("UPPER_SNAKE_CASE", '_');
public static final NamingFormat SPACE_CASE = createLowerDelimitedFormat("space case", ' ');
public static final NamingFormat UPPER_SPACE_CASE = createUpperDelimitedFormat("UPPER SPACE CASE", ' ');
public static final NamingFormat TITLE_CASE = createCapitalizedDelimitedFormat("Title Case", ' ');
private static NamingFormat createLowerDelimitedFormat(String formatName, char delimiter) {
return new NamingFormat() {
@Override
public String name() {
return formatName;
}
@Override
public String[] splitIntoWords(String name) {
return splitAtCharacter(name, delimiter);
}
@Override
public String joinToName(String[] words) {
return joinAllLower(delimiter, words);
}
@Override
public String toString() {
return name();
}
};
}
private static NamingFormat createUpperDelimitedFormat(String formatName, char delimiter) {
return new NamingFormat() {
@Override
public String name() {
return formatName;
}
@Override
public String[] splitIntoWords(String name) {
return splitAtCharacter(name, delimiter);
}
@Override
public String joinToName(String[] words) {
return joinAllUpper(delimiter, words);
}
@Override
public String toString() {
return name();
}
};
}
private static NamingFormat createCapitalizedDelimitedFormat(String formatName, char delimiter) {
return new NamingFormat() {
@Override
public String name() {
return formatName;
}
@Override
public String[] splitIntoWords(String name) {
return splitAtCharacter(name, delimiter);
}
@Override
public String joinToName(String[] words) {
return joinCapitalized(delimiter, words);
}
@Override
public String toString() {
return name();
}
};
}
private static String[] splitAtUpperCase(String name) {
ArrayList<String> words = new ArrayList<>();
StringBuilder wordBuilder = new StringBuilder();
CodePointReader codePointReader = CodePointReader.ofString(name);
while (codePointReader.hasNext()) {
if (wordBuilder.length() == 0) {
wordBuilder.appendCodePoint(codePointReader.next());
} else if (Character.isUpperCase(codePointReader.peek())) {
words.add(wordBuilder.toString());
wordBuilder.setLength(0);
} else {
wordBuilder.appendCodePoint(codePointReader.next());
}
}
if (wordBuilder.length() > 0) {
words.add(wordBuilder.toString());
}
return words.toArray(EMPTY_STRING_ARRAY);
}
private static String[] splitAtCharacter(String text, char delimiter) {
ArrayList<String> words = new ArrayList<>();
int index = 0;
while (index < text.length()) {
int delimiterIndex = text.indexOf(delimiter, index);
if (delimiterIndex == -1) {
words.add(text.substring(index));
break;
}
words.add(text.substring(index, delimiterIndex));
index = delimiterIndex + 1;
}
return words.toArray(EMPTY_STRING_ARRAY);
}
private static String joinAllLower(char joiner, String[] words) {
if (words.length == 0) {
return "";
}
int totalLength = countTotalWordsLength(words) + words.length - 1;
StringBuilder stringBuilder = new StringBuilder(totalLength);
for (String word : words) {
appendAllLower(stringBuilder, word);
if (stringBuilder.length() < totalLength) {
stringBuilder.append(joiner);
}
}
return stringBuilder.toString();
}
private static String joinAllUpper(char joiner, String[] words) {
if (words.length == 0) {
return "";
}
int totalLength = countTotalWordsLength(words) + words.length - 1;
StringBuilder stringBuilder = new StringBuilder(totalLength);
for (String word : words) {
appendAllUpper(stringBuilder, word);
if (stringBuilder.length() < totalLength) {
stringBuilder.append(joiner);
}
}
return stringBuilder.toString();
}
private static String joinCapitalized(char joiner, String[] words) {
if (words.length == 0) {
return "";
}
int totalLength = countTotalWordsLength(words) + words.length - 1;
StringBuilder stringBuilder = new StringBuilder(totalLength);
for (String word : words) {
appendCapitalized(stringBuilder, word);
if (stringBuilder.length() < totalLength) {
stringBuilder.append(joiner);
}
}
return stringBuilder.toString();
}
private static int countTotalWordsLength(String[] words) {
if (words.length == 0) {
return 0;
}
int totalLength = 0;
for (String word : words) {
totalLength += word.length();
}
return totalLength;
}
private static void appendAllLower(StringBuilder target, CharSequence input) {
if (input.length() == 0) {
return;
}
PrimitiveIterator.OfInt codePointIterator = input.codePoints().iterator();
while (codePointIterator.hasNext()) {
target.appendCodePoint(Character.toLowerCase(codePointIterator.nextInt()));
}
}
private static void appendAllUpper(StringBuilder target, CharSequence input) {
if (input.length() == 0) {
return;
}
PrimitiveIterator.OfInt codePointIterator = input.codePoints().iterator();
while (codePointIterator.hasNext()) {
target.appendCodePoint(Character.toUpperCase(codePointIterator.nextInt()));
}
}
private static void appendCapitalized(StringBuilder target, CharSequence input) {
if (input.length() == 0) {
return;
}
PrimitiveIterator.OfInt codePointIterator = input.codePoints().iterator();
target.appendCodePoint(Character.toUpperCase(codePointIterator.nextInt()));
while (codePointIterator.hasNext()) {
target.appendCodePoint(Character.toLowerCase(codePointIterator.nextInt()));
}
}
}

View File

@@ -0,0 +1,5 @@
@ApiStatus.Internal
package de.siphalor.tweed5.namingformat.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,101 @@
package de.siphalor.tweed5.namingformat.api;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Arrays;
import java.util.Locale;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class NamingFormatsTest {
@ParameterizedTest
@MethodSource("allNamingFormats")
void splitIntoWordsEmpty(NamingFormat namingFormat) {
assertArrayEquals(new String[0], namingFormat.splitIntoWords(""));
}
@ParameterizedTest
@MethodSource("allNamingFormats")
void joinToNameEmpty(NamingFormat namingFormat) {
assertEquals("", namingFormat.joinToName(new String[0]));
}
@ParameterizedTest
@MethodSource("allNamingFormats")
void isFormatNameStableInOwnFormat(NamingFormat namingFormat) {
assertEquals(namingFormat.name(), NamingFormats.convert(namingFormat.name(), namingFormat, namingFormat));
}
@ParameterizedTest
@MethodSource("splitIntoWordsArgs")
void splitIntoWords(NamingFormat namingFormat, String name) {
String[] result = namingFormat.splitIntoWords(name);
// Expecting "not a URL" in any casing
assertEquals(3, result.length, "Words should be 3, but got: " + Arrays.toString(result));
assertEquals("not", result[0].toLowerCase(Locale.ROOT));
assertEquals("a", result[1].toLowerCase(Locale.ROOT));
assertEquals("url", result[2].toLowerCase(Locale.ROOT));
}
private static Stream<Arguments> splitIntoWordsArgs() {
return Stream.of(
Arguments.of(NamingFormats.camelCase(), "notAUrl"),
Arguments.of(NamingFormats.pascalCase(), "NotAUrl"),
Arguments.of(NamingFormats.kebabCase(), "not-a-url"),
Arguments.of(NamingFormats.upperKebabCase(), "NOT-A-URL"),
Arguments.of(NamingFormats.snakeCase(), "not_a_url"),
Arguments.of(NamingFormats.upperSnakeCase(), "NOT_A_URL"),
Arguments.of(NamingFormats.spaceCase(), "not a url"),
Arguments.of(NamingFormats.upperSpaceCase(), "NOT A URL"),
Arguments.of(NamingFormats.titleCase(), "Not A Url")
);
}
@ParameterizedTest
@MethodSource("joinToNameArgs")
void joinToName(NamingFormat namingFormat, String expectedName) {
assertEquals(expectedName, namingFormat.joinToName(new String[]{ "an", "INTERESTING", "über", "name" }));
}
@ParameterizedTest
@MethodSource("joinToNameArgs")
void joinToNameLocaleIndependent(NamingFormat namingFormat, String expectedName) {
Locale turkishLocale = Locale.of("tr", "TR");
Locale.setDefault(turkishLocale);
assertEquals("ı", "I".toLowerCase(), "Turkish locale works");
assertEquals(expectedName, namingFormat.joinToName(new String[]{ "an", "INTERESTING", "über", "name" }));
}
private static Stream<Arguments> joinToNameArgs() {
return Stream.of(
Arguments.of(NamingFormats.camelCase(), "anInterestingÜberName"),
Arguments.of(NamingFormats.pascalCase(), "AnInterestingÜberName"),
Arguments.of(NamingFormats.kebabCase(), "an-interesting-über-name"),
Arguments.of(NamingFormats.upperKebabCase(), "AN-INTERESTING-ÜBER-NAME"),
Arguments.of(NamingFormats.snakeCase(), "an_interesting_über_name"),
Arguments.of(NamingFormats.upperSnakeCase(), "AN_INTERESTING_ÜBER_NAME"),
Arguments.of(NamingFormats.spaceCase(), "an interesting über name"),
Arguments.of(NamingFormats.upperSpaceCase(), "AN INTERESTING ÜBER NAME"),
Arguments.of(NamingFormats.titleCase(), "An Interesting Über Name")
);
}
private static NamingFormat[] allNamingFormats() {
return new NamingFormat[] {
NamingFormats.camelCase(),
NamingFormats.pascalCase(),
NamingFormats.kebabCase(),
NamingFormats.upperKebabCase(),
NamingFormats.snakeCase(),
NamingFormats.upperSnakeCase(),
NamingFormats.spaceCase(),
NamingFormats.upperSpaceCase(),
NamingFormats.titleCase()
};
}
}

View File

@@ -0,0 +1,25 @@
package de.siphalor.tweed5.namingformat.impl;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CodePointReaderTest {
@Test
void ofString() {
CodePointReader reader = CodePointReader.ofString("Aüc");
assertTrue(reader.hasNext());
assertEquals('A', reader.next());
assertTrue(reader.hasNext());
assertEquals('ü', reader.peek());
assertEquals('ü', reader.peek());
assertTrue(reader.hasNext());
assertEquals('ü', reader.next());
assertTrue(reader.hasNext());
assertEquals('c', reader.next());
assertFalse(reader.hasNext());
assertThrows(Exception.class, reader::next);
}
}