Skip to content

Commit 10b4fe1

Browse files
committed
feat: Notations for Java packages and constants
fix: also handle Java keywords
1 parent 017e3a8 commit 10b4fe1

File tree

6 files changed

+253
-130
lines changed

6 files changed

+253
-130
lines changed

src/main/kotlin/BaseStringNotation.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
package de.joshuagleitze.stringnotation
22

3-
import java.util.*
4-
53
/**
64
* Base class for implementing string notations.
75
*
86
* @constructor Creates a string notation that will use the provided regular expression to [split][String.split] parts when parsing.
97
*/
108
abstract class BaseStringNotation(private val splitAt: Regex): StringNotation {
119
/**
12-
* Transforms a parsed part after it has been read. The default implementation is to convert the part to lowercase to discard possibly
13-
* wrong case information.
10+
* Transforms a parsed part after it has been read. The default implementation does not change the part.
1411
*/
15-
protected open fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
12+
protected open fun transformPartAfterParse(index: Int, part: String) = part
1613

17-
override fun parse(sourceString: String): Word = Word(sourceString.split(splitAt).asSequence().mapIndexed(::transformPartAfterParse))
14+
override fun parse(sourceString: String): Word =
15+
Word(sourceString.split(splitAt).asSequence().filter(String::isNotBlank).mapIndexed(::transformPartAfterParse))
1816

1917
/**
2018
* Allows to transform a part before it is being printed. The default implementation does not modify the part in any way.
@@ -34,7 +32,7 @@ abstract class BaseStringNotation(private val splitAt: Regex): StringNotation {
3432

3533
override fun print(word: Word) = word.parts
3634
.mapIndexed(::transformPartToPrint)
37-
.foldIndexed(StringBuffer()) { index, left, right -> left.append(printBeforePart(index, right)).append(right) }
35+
.foldIndexed(StringBuffer()) { index, existing, part -> existing.append(printBeforePart(index, part)).append(part) }
3836
.toString()
3937

4038
override fun toString() = this::class.java.simpleName!!

src/main/kotlin/JavaNotations.kt

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package de.joshuagleitze.stringnotation
2+
3+
import java.util.*
4+
import java.util.stream.IntStream
5+
import javax.lang.model.SourceVersion
6+
7+
/**
8+
* A notation for Java type names. This notation is like [UpperCamelCase], but when [printing][StringNotation.print], it will drop any
9+
* character that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it.
10+
*
11+
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
12+
* using [SourceVersion.isKeyword].
13+
*/
14+
object JavaTypeName: StringNotation by UpperCamelCase {
15+
override fun print(word: Word) = UpperCamelCase.print(word).makeValidJavaIdentifier()
16+
17+
override fun toString() = this::class.java.simpleName!!
18+
}
19+
20+
/**
21+
* A notation for java member names. This notation is like [LowerCamelCase], but when [printing][StringNotation.print], it will drop any
22+
* character that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it.
23+
*
24+
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
25+
* using [SourceVersion.isKeyword].
26+
*/
27+
object JavaMemberName: BaseStringNotation(camelCaseSplitRegex) {
28+
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
29+
30+
override fun print(word: Word) = word.parts
31+
.foldIndexed(StringBuffer()) { index, left, right ->
32+
val rightPart =
33+
if (left.contains(Regex("[a-zA-Z]"))) right.toFirstUpperOtherLowerCase()
34+
else right.toLowerCase()
35+
left.append(printBeforePart(index, rightPart)).append(rightPart)
36+
}.toString().makeValidJavaIdentifier()
37+
}
38+
39+
/**
40+
* A notation for java package parts. When [printing][StringNotation.print], it simply concatenates all word parts and drops any character
41+
* that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it. When
42+
* [parsing][StringNotation.parse], the notation will recognise word parts both in the [LowerCamelCase] and the [SnakeCase] notation.
43+
* However, neither notation is conventional and parsing will usually yield only one word part on real-world inputs.
44+
*
45+
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
46+
* using [SourceVersion.isKeyword].
47+
*/
48+
object JavaPackagePart: BaseStringNotation(Regex("_|${camelCaseSplitRegex.pattern}")) {
49+
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
50+
51+
override fun transformPartToPrint(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
52+
53+
override fun print(word: Word) = super.print(word).makeValidJavaIdentifier()
54+
}
55+
56+
/**
57+
* A notation for whole java packages. When [printing][StringNotation.print] parts, it will drop any character that is not allowed in a Java
58+
* identifier. If the result is a Java keyword, `_` will be appended to it.
59+
*
60+
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
61+
* using [SourceVersion.isKeyword].
62+
*/
63+
object JavaPackageName: BaseStringNotation(Regex("\\.")) {
64+
override fun transformPartToPrint(index: Int, part: String) = part.toLowerCase(Locale.ROOT).makeValidJavaIdentifier()
65+
66+
override fun printBeforeInnerPart(index: Int, part: String) = "."
67+
}
68+
69+
/**
70+
* A notation for `static final` fields in Java. This notation is like [ScreamingSnakeCase], but when [printing][StringNotation.print], it
71+
* will drop any character that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it.
72+
*
73+
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
74+
* using [SourceVersion.isKeyword].
75+
*/
76+
object JavaConstantName: StringNotation by ScreamingSnakeCase {
77+
override fun print(word: Word) = ScreamingSnakeCase.print(word).makeValidJavaIdentifier()
78+
79+
override fun toString() = this::class.java.simpleName!!
80+
}
81+
82+
private fun String.makeValidJavaIdentifier() = this.keepOnlyJavaIdentifierChars().neutralizeJavaReservedKeywords().ifEmpty { "__" }
83+
84+
private fun String.keepOnlyJavaIdentifierChars() = this.chars()
85+
.skipWhile { !Character.isJavaIdentifierStart(it) }
86+
.filter { Character.isJavaIdentifierPart(it) }
87+
.collect({ StringBuilder() }, { left, right -> left.appendCodePoint(right) }, { left, right -> left.append(right) })
88+
.toString()
89+
90+
private fun String.neutralizeJavaReservedKeywords() = if (SourceVersion.isKeyword(this)) this + "_" else this
91+
92+
private inline fun IntStream.skipWhile(crossinline condition: (Int) -> Boolean): IntStream {
93+
var found = false
94+
return this.filter {
95+
if (!found) {
96+
found = !condition(it)
97+
}
98+
found
99+
}
100+
}
101+

src/main/kotlin/Notations.kt

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package de.joshuagleitze.stringnotation
22

3-
import java.util.stream.IntStream
3+
import java.util.*
44

5-
private val camelCaseSplitRegex = Regex("(?<=.)(?=\\p{Lu})")
5+
internal val camelCaseSplitRegex = Regex("(?<=.)(?=\\p{Lu})")
66

77
/**
88
* The `UpperCamelCase` notation.
99
*
1010
* @see JavaTypeName
1111
*/
1212
object UpperCamelCase: BaseStringNotation(camelCaseSplitRegex) {
13+
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
14+
1315
public override fun transformPartToPrint(index: Int, part: String) = part.toFirstUpperOtherLowerCase()
1416
}
1517

@@ -19,13 +21,17 @@ object UpperCamelCase: BaseStringNotation(camelCaseSplitRegex) {
1921
* @see JavaMemberName
2022
*/
2123
object LowerCamelCase: BaseStringNotation(camelCaseSplitRegex) {
24+
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
25+
2226
override fun transformPartToPrint(index: Int, part: String) = if (index == 0) part.toLowerCase() else part.toFirstUpperOtherLowerCase()
2327
}
2428

2529
/**
2630
* The `SCREAMING_SNAKE_CASE` notation.
2731
*/
2832
object ScreamingSnakeCase: BaseStringNotation(Regex("_")) {
33+
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT)
34+
2935
override fun printBeforeInnerPart(index: Int, part: String) = "_"
3036

3137
override fun transformPartToPrint(index: Int, part: String) = part.toUpperCase()
@@ -35,63 +41,17 @@ object ScreamingSnakeCase: BaseStringNotation(Regex("_")) {
3541
* The `snake_case` notation.
3642
*/
3743
object SnakeCase: BaseStringNotation(Regex("_")) {
38-
override fun transformPartAfterParse(index: Int, part: String) = part
3944
override fun printBeforeInnerPart(index: Int, part: String) = "_"
4045
}
4146

42-
/**
43-
* A notation for java type names. This notation is like [UpperCamelCase], but will drop any character that is not allowed in a Java
44-
* identifier when [printing][StringNotation.print].
45-
*
46-
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart].
47-
*/
48-
object JavaTypeName: BaseStringNotation(camelCaseSplitRegex) {
49-
override fun transformPartToPrint(index: Int, part: String) = part.toFirstUpperOtherLowerCase()
50-
override fun print(word: Word) = super.print(word).keepOnlyJavaIdentifierChars()
51-
}
52-
53-
/**
54-
* A notation for java member names. This notation is like [LowerCamelCase], but will drop any character that is not allowed in a Java
55-
* identifier when [printing][StringNotation.print].
56-
*
57-
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart].
58-
*/
59-
object JavaMemberName: BaseStringNotation(camelCaseSplitRegex) {
60-
override fun print(word: Word) = word.parts
61-
.foldIndexed(StringBuffer()) { index, left, right ->
62-
val rightPart =
63-
if (left.contains(Regex("[a-zA-Z]"))) right.toFirstUpperOtherLowerCase()
64-
else right.toLowerCase()
65-
left.append(printBeforePart(index, rightPart)).append(rightPart)
66-
}.toString().keepOnlyJavaIdentifierChars()
67-
68-
}
69-
7047
/**
7148
* Notation for words written like in normal language. [Parsing][StringNotation.parse] will recognise all substrings that are separated by
7249
* one or more characters of whitespace as a [part][Word.parts]. [Printing][StringNotation.print] will print the parts separated by one
7350
* space.
7451
*/
7552
object NormalWords: BaseStringNotation(Regex("[\\s]+")) {
76-
override fun transformPartAfterParse(index: Int, part: String) = part
7753
override fun printBeforeInnerPart(index: Int, part: String) = " "
7854
}
7955

8056
internal fun String.toFirstUpperOtherLowerCase() = if (isNotEmpty()) this[0].toUpperCase() + substring(1).toLowerCase() else this
8157

82-
fun String.keepOnlyJavaIdentifierChars() = this.chars()
83-
.skipWhile { !Character.isJavaIdentifierStart(it) }
84-
.filter { Character.isJavaIdentifierPart(it) }
85-
.collect({ StringBuilder() }, { left, right -> left.appendCodePoint(right) }, { left, right -> left.append(right) })
86-
.toString()
87-
88-
internal inline fun IntStream.skipWhile(crossinline condition: (Int) -> Boolean): IntStream {
89-
var found = false
90-
return this.filter {
91-
if (!found) {
92-
found = !condition(it)
93-
}
94-
found
95-
}
96-
}
97-
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package de.joshuagleitze.stringnotation
2+
3+
import ch.tutteli.atrium.api.fluent.en_GB.feature
4+
import ch.tutteli.atrium.api.fluent.en_GB.toBe
5+
import ch.tutteli.atrium.api.verbs.expect
6+
import org.junit.jupiter.api.TestInstance
7+
import org.junit.jupiter.params.ParameterizedTest
8+
import org.junit.jupiter.params.provider.Arguments
9+
import org.junit.jupiter.params.provider.MethodSource
10+
11+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
12+
abstract class BaseNotationTest(
13+
private val notation: StringNotation,
14+
private val unchangedWords: List<Pair<String, Word>>,
15+
private val parseOnlyWords: List<Pair<String, Word>> = emptyList(),
16+
private val printOnlyWords: List<Pair<Word, String>> = emptyList()
17+
) {
18+
@ParameterizedTest(name = "\"{0}\" -> {1}")
19+
@MethodSource("parseWords")
20+
fun `parses words correctly`(input: String, expectedWord: Word) {
21+
expect(input.fromNotation(notation)) {
22+
feature(Word::partsList).toBe(expectedWord.partsList)
23+
}
24+
}
25+
26+
@ParameterizedTest(name = "{1} -> \"{0}\"")
27+
@MethodSource("printWords")
28+
fun `prints words correctly`(sourceWord: Word, expectedResult: String) {
29+
expect(sourceWord) {
30+
feature(Word::toNotation, notation).toBe(expectedResult)
31+
}
32+
}
33+
34+
@ParameterizedTest(name = "\"{0}\"")
35+
@MethodSource("unchangedWords")
36+
fun `parsing and printing a word written in this notation does not change the word`(word: String) {
37+
expect(word) {
38+
feature(String::fromNotation, notation) {
39+
feature(Word::toNotation, notation).toBe(word)
40+
}
41+
}
42+
}
43+
44+
private fun parseWords() = asArguments(unchangedWords + parseOnlyWords)
45+
private fun printWords() = asArguments(unchangedWords.map { it.swap() } + printOnlyWords)
46+
private fun unchangedWords() = asArguments(unchangedWords)
47+
48+
private fun asArguments(pairs: List<Pair<*, *>>) = pairs.map {
49+
Arguments.arguments(
50+
it.first,
51+
it.second
52+
)
53+
}
54+
55+
private fun <First, Second> Pair<First, Second>.swap() = Pair(this.second, this.first)
56+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package de.joshuagleitze.stringnotation
2+
3+
class JavaTypeNameTest: BaseNotationTest(
4+
notation = JavaTypeName,
5+
unchangedWords = listOf("ImInJavaTypeNotation" to Word("im", "in", "java", "type", "notation")),
6+
printOnlyWords = listOf(
7+
Word("I’m using", "Bad", "chaRacters!") to "ImusingBadCharacters",
8+
Word("1", "type", "name", "4", "you") to "TypeName4You",
9+
Word("removes", "upperCase") to "RemovesUppercase",
10+
Word("") to "__",
11+
Word("1") to "__",
12+
Word("8if") to "if_",
13+
Word("_") to "__"
14+
)
15+
)
16+
17+
class JavaMemberNameTest: BaseNotationTest(
18+
notation = JavaMemberName,
19+
unchangedWords = listOf("imInJavaMemberNotation" to Word("im", "in", "java", "member", "notation")),
20+
printOnlyWords = listOf(
21+
Word("I’m using", "Bad", "chaRacters!") to "imusingBadCharacters",
22+
Word("1", "Member", "name", "4", "you") to "memberName4You",
23+
Word("_", "underscore", "start") to "_underscoreStart",
24+
Word("$", "dollar", "start") to "\$dollarStart",
25+
Word("a", "letter", "start") to "aLetterStart",
26+
Word("removes", "upperCase") to "removesUppercase",
27+
Word("") to "__",
28+
Word("1") to "__",
29+
Word("8if") to "if_",
30+
Word("_") to "__"
31+
)
32+
)
33+
34+
class JavaPackagePartTest: BaseNotationTest(
35+
notation = JavaPackagePart,
36+
unchangedWords = listOf(
37+
"imapackagename" to Word("imapackagename")
38+
),
39+
parseOnlyWords = listOf(
40+
"withCamelCase" to Word("with", "camel", "case"),
41+
"with_snake_case" to Word("with", "snake", "case"),
42+
"withCamelAnd_snake_case" to Word("with", "camel", "and", "snake", "case"),
43+
"if" to Word("if")
44+
),
45+
printOnlyWords = listOf(
46+
Word("") to "__",
47+
Word("1") to "__",
48+
Word("8if") to "if_",
49+
Word("_") to "__"
50+
)
51+
)
52+
53+
class JavaPackageNameTest: BaseNotationTest(
54+
notation = JavaPackageName,
55+
unchangedWords = listOf("i.am.a.packagename" to Word("i", "am", "a", "packagename")),
56+
parseOnlyWords = listOf(
57+
"wIth.CAPITALS" to Word("wIth", "CAPITALS"),
58+
"if.true" to Word("if", "true")
59+
),
60+
printOnlyWords = listOf(
61+
Word("if", "", "cApitAls") to "if_.__.capitals",
62+
Word("_") to "__"
63+
)
64+
65+
)
66+
67+
class JavaConstantNameTest: BaseNotationTest(
68+
notation = JavaConstantName,
69+
unchangedWords = listOf(
70+
"I_AM_A_CONSTANT" to Word("i", "am", "a", "constant")
71+
),
72+
parseOnlyWords = listOf(
73+
"if" to Word("if")
74+
),
75+
printOnlyWords = listOf(
76+
Word("") to "__",
77+
Word("1") to "__",
78+
Word("8if") to "IF",
79+
Word("_") to "__"
80+
)
81+
)

0 commit comments

Comments
 (0)