Skip to content

Commit e409558

Browse files
authored
Merge pull request #20 from jGleitz/feature/java-package-and-constants
feat: Notations for Java packages and constants
2 parents 017e3a8 + 3e784e4 commit e409558

File tree

7 files changed

+295
-131
lines changed

7 files changed

+295
-131
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import de.marcphilipp.gradle.nexus.NexusRepository
12
import org.jetbrains.dokka.gradle.DokkaTask
23
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3-
import de.marcphilipp.gradle.nexus.NexusRepository
44

55
plugins {
66
kotlin("jvm").version("1.3.61")

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

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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.Assumptions.assumeTrue
7+
import org.junit.jupiter.api.TestInstance
8+
import org.junit.jupiter.params.ParameterizedTest
9+
import org.junit.jupiter.params.provider.Arguments
10+
import org.junit.jupiter.params.provider.MethodSource
11+
12+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
13+
abstract class BaseNotationTest(
14+
private val notation: StringNotation,
15+
private val unchangedWords: List<NotationTestData>,
16+
private val parseOnlyWords: List<NotationTestData> = emptyList(),
17+
private val printOnlyWords: List<NotationTestData> = emptyList()
18+
) {
19+
@ParameterizedTest(name = "\"{1}\" -> {2}")
20+
@MethodSource("parseWords")
21+
fun `parses words correctly`(minimumJavaVersion: Int, input: String, expectedWord: Word) {
22+
assumeTrue(currentJavaVersion >= minimumJavaVersion, "Requires at least Java $minimumJavaVersion")
23+
expect(input.fromNotation(notation)) {
24+
feature(Word::partsList).toBe(expectedWord.partsList)
25+
}
26+
}
27+
28+
@ParameterizedTest(name = "{2} -> \"{1}\"")
29+
@MethodSource("printWords")
30+
fun `prints words correctly`(minimumJavaVersion: Int, expectedResult: String, sourceWord: Word) {
31+
assumeTrue(currentJavaVersion >= minimumJavaVersion, "Requires at least Java $minimumJavaVersion")
32+
expect(sourceWord) {
33+
feature(Word::toNotation, notation).toBe(expectedResult)
34+
}
35+
}
36+
37+
@ParameterizedTest(name = "\"{1}\"")
38+
@MethodSource("unchangedWords")
39+
fun `parsing and printing a word written in this notation does not change the word`(minimumJavaVersion: Int, word: String) {
40+
assumeTrue(currentJavaVersion >= minimumJavaVersion, "Requires at least Java $minimumJavaVersion")
41+
expect(word) {
42+
feature(String::fromNotation, notation) {
43+
feature(Word::toNotation, notation).toBe(word)
44+
}
45+
}
46+
}
47+
48+
private fun parseWords() = asArguments(unchangedWords + parseOnlyWords)
49+
private fun printWords() = asArguments(unchangedWords + printOnlyWords)
50+
private fun unchangedWords() = asArguments(unchangedWords)
51+
52+
private fun asArguments(pairs: List<NotationTestData>) = pairs.map {
53+
Arguments.arguments(
54+
it.minimumJavaVersion,
55+
it.string,
56+
it.word
57+
)
58+
}
59+
}
60+
61+
data class NotationTestData(val word: Word, val string: String, var minimumJavaVersion: Int = 0)
62+
63+
infix fun Word.to(string: String) = NotationTestData(this, string)
64+
infix fun String.to(word: Word) = NotationTestData(word, this)
65+
infix fun NotationTestData.ifJvmVersionIsAtLeast(minimumJavaVersion: Int) = this.apply { this.minimumJavaVersion = minimumJavaVersion }
66+
67+
val currentJavaVersion by lazy {
68+
System.getProperty("java.runtime.version")
69+
.split(".")
70+
.let { if (it[0] == "1") it.drop(1) else it }[0]
71+
.toInt()
72+
}

0 commit comments

Comments
 (0)