Please note that this project is now part of the Kommons library.
Below you will find the description of the last release.
Kommons Debug is a Kotlin Multiplatform Library that adds:
- powerful print debugging functions:
- runtime information on the running program, its platform and a platform-independent stack trace
- easily accessible Unicode support
- string handling functions
- regex functions such as the possibility to use glob patterns
- facilitated time handling using
Now
,Yesterday
, andTomorrow
- factories to easily implement
of
/ofOrNull
,from
/fromOrNull
, andparse
/parseOrNull
- file handling features such as locating source files and accessing the class path with the NIO2 API
Kommons Debug is hosted on GitHub with releases provided on Maven Central.
-
Gradle
implementation("com.bkahlert.kommons:kommons-debug:0.14.0")
-
Maven
<dependency> <groupId>com.bkahlert.kommons</groupId> <artifactId>kommons-debug</artifactId> <version>0.14.0</version> </dependency>
Print tracing information and easily cleanup afterward using IntelliJ's code cleanup feature.
data class Foo(val bar: String = "baz") {
private val baz = 42.0
fun compute() // used to demonstrate that trace/inspect return their argument unchanged
}
Foo().trace.compute()
// output: (sample.kt:5) ⟨ Foo(bar=baz) ⟩
Foo().trace("caption").compute()
// output: (sample.kt:8) caption ⟨ Foo(bar=baz) ⟩
Foo().trace("details") { it.bar.reversed() }.compute()
// output: (sample.kt:11) details ⟨ Foo(bar=baz) ⟩ { "zab" }
Foo().inspect.compute()
// output: (sample.kt:14) ⟨ !Foo { baz: !Double 42.0, bar: !String "baz" } ⟩
Foo().inspect("caption").compute()
// output: (sample.kt:17) caption ⟨ !Foo { baz: !Double 42.0, bar: !String "baz" } ⟩
Foo().inspect("details") { it.bar.reversed() }.compute()
// output: (sample.kt:20) details ⟨ !Foo { baz: !Double 42.0, bar: !String "baz" } ⟩ { !String "zab" }
The examples above also work in browsers:
Renders any object's type
"string".renderType() // String
class Foo(val bar: Any = "baz")
foo().renderType() // Foo
val lambda: (String) -> Unit = {}
lambda.renderType() // (String)->Unit
Renders any object depending on whether its toString()
is overridden:
- If there is a custom
toString()
it's simply used. - if there is no custom
toString()
the object is serialized in the form structurally
"string".render() // string
class Foo(val bar: Any = "baz")
foo().render() // { bar: "baz" }
foo(foo()).render(typed = true) // Foo { bar: Foo { bar: "baz" } }
foo().asString() // { bar: "baz" }
foo(null).asString(excludeNullValues = false) // { }
Renders any object as an emoji.
null.asEmoji() // "❔"
true.asEmoji() // "✅"
false.asEmoji() // "❌"
Now.asEmoji() // "🕝"
Now.asEmoji(Floor) // "🕑"
"other".asEmoji() // "🔣"
Contains a map of the object's properties with each entry representing the name and value of a property.
"string".properties // { length: 6 }
class Foo(val bar: Any = "baz")
foo().properties // { bar: "baz" }
foo(foo()).properties // { bar: { bar: "baz" } }
Any URL
, URI
, Path
and File
can be opened locally using open
.
URL("file:///home/john/dev/project/src/jvm/kotlin/packages/source.kt").open()
In order to only open the directory containing an above-mentioned resource
locate
can be used.
URL("file:///home/john/dev/project/src/jvm/kotlin/packages/source.kt").locate()
Reflects the running program and provides:
Program.isDebugging
: Returns whether the program is running in debug mode.Program.isIntelliJ
: Returns whether the program is running in IntelliJ.Program.onExit
: Allows registering callbacks that are invoked when the program exits.
Reflects the platform the program runs on (e.g. Platform.JVM
)
and provides:
Platform.ansiSupport
: Returns to what extent ANSI escape codes are supported.Platform.fileSeparator
: Returns the separator used to separate path segments.
Access the current stack trace by a simple call to StackTrace.get()
or locate a specific caller using StackTrace.get().findLastKnownCallOrNull
.
fun foo(block: () -> StackTraceElement?) = block()
fun bar(block: () -> StackTraceElement?) = block()
foo { bar { StackTrace.findLastKnownCallOrNull("bar") } }?.function // "foo"
foo { bar { StackTrace.findLastKnownCallOrNull(::bar) } }?.function // "foo"
Handling user input requires functions to handle Unicode correctly, unless you're not afraid of the following:
"👨🏾🦱".substring(0, 3) // "👨?", skin tone gone, curly hair gone
"👩👩👦👦".substring(1, 7) // "?👩?", wife gone, kids gone
Decode any string to a sequence / list of code points using String.asCodePointSequence
/ String.toCodePointList
.
Decode any string to a sequence / list of graphemes using String.asGraphemeSequence
/ String.toGraphemeList
.
Transliterations and transforms can be done using String.transform
.
"a".asCodePoint().name // "LATIN SMALL LETTER A"
"a𝕓c̳🔤".toCharArray() // "a", "?", "?", "c", "̳", "?", "?"
"a𝕓c̳🔤".toCodePointList() // "a", "𝕓", "c", "̳", "🫠"
"a𝕓c̳🔤".toGraphemeList() // "a", "𝕓", "c̳", "🫠"
"a𝕓🫠🇩🇪👨🏾🦱👩👩👦👦".length // 27 (= number of Java chars)
"a𝕓🫠🇩🇪👨🏾🦱👩👩👦👦".asText(CodePoint).length // 16 (= number of Unicode code points)
"a𝕓🫠🇩🇪👨🏾🦱👩👩👦👦".asText(Grapheme).length // 6 (= visually perceivable units)
"a𝕓🫠🇩🇪👨🏾🦱👩👩👦👦".truncate(7.characters) // "a\uD835 … 👦"
"a𝕓🫠🇩🇪👨🏾🦱👩👩👦👦".truncate(7.codePoints) // "a𝕓 … 👦"
"a𝕓🫠🇩🇪👨🏾🦱👩👩👦👦".truncate(7.graphemes) // "a𝕓 … 👨🏾🦱👩👩👦👦"
"© А-З Ä-ö-ß".transform("de_DE", "de_DE-ASCII") // "(C) A-Z AE-oe-ss"
UTF-16 | Char (Java, JavaScript, Kotlin, ...) |
Unicode Code Point |
Unicode Grapheme Cluster |
---|---|---|---|
\u0061 | a (LATIN SMALL LETTER A) | a | a |
\uD835 \uDD53 |
𝕓 (MATHEMATICAL DOUBLE-STRUCK SMALL B) | 𝕓 | 𝕓 |
\uD83E \uDEE0 \uD83C \uDDE9 |
? (HIGH SURROGATES D83E) ? (LOW SURROGATES DEE0) |
🫠 (MELTING FACE EMOJI) | 🫠 |
\uD83C \uDDE9 |
? (HIGH SURROGATES D83C) ? (LOW SURROGATES DDE9) |
[D] (REGIONAL INDICATOR SYMBOL LETTER D) | 🇩🇪 |
\uD83C \uDDEA |
? (HIGH SURROGATES D83C) ? (LOW SURROGATES DDEA) |
[E] (REGIONAL INDICATOR SYMBOL LETTER E) | |
\uD83D \uDC68 |
? (HIGH SURROGATES D83D) ? (LOW SURROGATES DC68) |
👨 (MAN) | 👨🏾🦱 |
\uD83C \uDFFE |
? (HIGH SURROGATES D83C) ? (LOW SURROGATES DFFE) |
🏾 (EMOJI MODIFIER FITZPATRICK TYPE-5) | |
\u200D | [ZWJ] (ZERO WIDTH JOINER) | [ZWJ] (ZERO WIDTH JOINER) | |
\uD83E \uDDB1 |
? (HIGH SURROGATES D83E) ? (LOW SURROGATES DDB1) |
🦱 (EMOJI COMPONENT CURLY HAIR) | |
\uD83D \uDC69 |
? (HIGH SURROGATES D83D) ? (LOW SURROGATES DC69) |
👩 (WOMAN) | 👩👩👦👦 |
\u200D | [ZWJ] (ZERO WIDTH JOINER) | [ZWJ] (ZERO WIDTH JOINER) | |
\uD83D \uDC69 |
? (HIGH SURROGATES D83D) ? (LOW SURROGATES DC69) |
👩 (WOMAN) | |
\u200D | [ZWJ] (ZERO WIDTH JOINER) | [ZWJ] (ZERO WIDTH JOINER) | |
\uD83D \uDC66 |
? (HIGH SURROGATES D83D) ? (LOW SURROGATES DC66) |
👦 (BOY) | |
\u200D | [ZWJ] (ZERO WIDTH JOINER) | [ZWJ] (ZERO WIDTH JOINER) | |
\uD83D \uDC66 |
? (HIGH SURROGATES D83D) ? (LOW SURROGATES DC66) |
👦 (BOY) |
quoted
: quotes and escapes an existing stringansiRemoved
: removes ANSI escape sequencesspaced
/startSpaced
/endSpaced
: adds a space before and/or after a string if there isn't already onetruncate
/truncateStart
/truncateEnd
: truncates a string to a given lengthtoIdentifier
: create an identifier from any string that resembles itrandomString
: create a random string- LineSeparators: many extension functions to work with usual and exotic Unicode line breaks.
"string".quoted // "string"
"""{ bar: "baz" }""".quoted // "{ bar: \"baz\" }"
"""
line 1
"line 2"
""".quoted // "line1\n\"line2\""
"\u001B[1mbold \u001B[34mand blue\u001B[0m".ansiRemoved
// "bold and blue"
"\u001B[34m↗\u001B(B\u001B[m \u001B]8;;https://example.com\u001B\\link\u001B]8;;\u001B\\".ansiRemoved
// "↗ link"
"string".spaced // " string "
"bar".withPrefix("foo") // "foobar"
"foo bar".withPrefix("foo") // "foo bar"
"foo".withSuffix("bar") // "foobar"
"12345678901234567890".truncate() // "123456 … 567890"
"12345678901234567890".truncateStart() // " … 901234567890"
"12345678901234567890".truncateEnd() // "123456789012 … "
"1👋 xy-z".toIdentifier() // "i__xy-z3"
randomString()
// returns "Ax-212kss0-xTzy5" (16 characters by default)
Capitalize / decapitalize strings using capitalize
/decapitalize
or
manipulate the case style using toCasesString
or any of its specializations.
"fooBar".capitalize() // "FooBar"
"FooBar".decapitalize() // "fooBar"
"FooBar".toCamelCasedString() // "fooBar"
"FooBar".toPascalCasedString() // "FooBar"
"FooBar".toScreamingSnakeCasedString() // "FOO_BAR"
"FooBar".toKebabCasedString() // "foo-bar"
"FooBar".toTitleCasedString() // "Foo Bar"
enum class FooBar { FooBaz }
FooBar::class.simpleCamelCasedName // "fooBar"
FooBar::class.simplePascalCasedName // "FooBar"
FooBar::class.simpleScreamingSnakeCasedName // "FOO_BAR"
FooBar::class.simpleKebabCasedName // "foo-bar"
FooBar::class.simpleTitleCasedName // "Foo Bar"
FooBar.FooBaz.camelCasedName // "fooBaz"
FooBar.FooBaz.pascalCasedName // "FooBaz"
FooBar.FooBaz.screamingSnakeCasedName // "FOO_BAZ"
FooBar.FooBaz.kebabCasedName // "foo-baz"
FooBar.FooBaz.titleCasedName // "Foo Baz
Easily check edge-case with a fluent interface as does requireNotNull
does:
requireNotEmpty("abc") // passes and returns "abc"
requireNotBlank(" ") // throws IllegalArgumentException
checkNotEmpty("abc") // passes and returns "abc"
checkNotBlank(" ") // throws IllegalStateException
"abc".takeIfNotEmpty() // returns "abc"
" ".takeIfNotBlank() // returns null
"abc".takeUnlessEmpty() // returns "abc"
" ".takeUnlessBlank() // returns null
Regex
can be authored as follows:
Regex("foo") + Regex("bar") // Regex("foobar")
Regex("foo") + "bar" // Regex("foobar")
Regex("foo") or Regex("bar") // Regex("foo|bar")
Regex("foo") or "bar" // Regex("foo|bar")
Regex.fromLiteralAlternates( // Regex("\\[foo\\]|bar\\?")
"[foo]", "bar?"
)
Regex("foo").optional() // Regex("(?:foo)?")
Regex("foo").repeatAny() // Regex("(?:foo)*")
Regex("foo").repeatAtLeastOnce() // Regex("(?:foo)+")
Regex("foo").repeat(2, 5) // Regex("(?:foo){2,5}")
Regex("foo").group() // Regex("(?:foo)")
Regex("foo").group("name") // Regex("(?<name>foo)")
Find matches easier:
// get group by name
Regex("(?<name>ba.)")
.findAll("foo bar baz")
.mapNotNull { it.groups["name"]?.value } // "bar", "baz"
// get group value by name
Regex("(?<name>ba.)")
.findAll("foo bar baz")
.map { it.groupValue("name") } // "bar", "baz"
// find all values
Regex("(?<name>ba.)")
.findAllValues("foo bar baz") // "bar", "baz"
// match URLs / URIs
Regex.UrlRegex.findAll(/* ... */)
Regex.UriRegex.findAll(/* ... */)
Match multiline strings with simple glob patterns:
// matching within lines with wildcard
"foo.bar()".matchesGlob("foo.*") // ✅
// matching across lines with multiline wildcard
"""
foo
.bar()
.baz()
""".matchesGlob(
"""
foo
.**()
""".trimIndent() // ✅
)
"""
foo
.bar()
.baz()
""".matchesGlob(
"""
foo
.*()
""".trimIndent() // ❌ (* doesn't match across lines)
)
Alternatively, you can use matchesCurly
if you prefer SLF4J / Logback style
wildcards {}
and {{}}
.
Require or check emptiness of collections and arrays using requireNotEmpty
and checkNotEmpty
.
Iterate any type of closed ranges using asIterable
.
(-4.2..42.0)
.asIterable { it + 9 }
.map { it.toInt() } // [-4, 4, 13, 22, 31, 40]
Now + 2.seconds // 2 seconds in the future
Today - 3.days // 3 days in the past
Yesterday - 2.days // 3 days in the past
Tomorrow + 1.days // the day after tomorrow
Instant.parse("1910-06-22T13:00:00Z") + 5.minutes // 1910-06-22T12:05:00Z
LocalDate.parse("1910-06-22") - 2.days // 1910-06-20
SystemLocations.Temp.createTempFile().age // < 1ms
Now.toMomentString() // "now"
(Now - 12.hours).toMomentString() // "12h ago"
(Now + 3.days).toMomentString() // "in 3d"
(Today - 1.days).toMomentString() // "yesterday"
The extension functions
toHexadecimalString()
toOctalString()
toBinaryString()
... are provided for:
Byte
ByteArray
Int
Long
UByte
UByteArray
UInt
ULong
val byteArray = byteArrayOf(0x00, 0x7f, -0x80, -0x01)
val largeByteArrayOf = byteArrayOf(-0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01, -0x01)
val veryLargeByteArray = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
byteArray.map { it.toHexadecimalString() } // "00", "7f", "80", "ff"
byteArray.toHexadecimalString() // "007f80ff"
largeByteArrayOf.toHexadecimalString() // "ffffffffffffffffffffffffffffffff"
veryLargeByteArray.toHexadecimalString() // "0100000000000000000000000000000000"
byteArray.map { it.toOctalString() } // "000", "177", "200", "377"
byteArray.toOctalString() // "000177200377"
largeByteArrayOf.toOctalString() // "377377377377377377377377377377377377377377377377"
veryLargeByteArray.toOctalString() // "001000000000000000000000000000000000000000000000000"
byteArray.map { it.toBinaryString() } // "00000000", "01111111", "10000000", "11111111"
byteArray.toBinaryString() // "00000000011111111000000011111111"
largeByteArrayOf.toBinaryString() // "111111111111111111111111111...111111"
veryLargeByteArray.toBinaryString() // "00000001000000000000000000000000000...000000"
Further conversions:
Int.toByteArray()
Long.toByteArray()
UInt.toUByteArray()
ULong.toUByteArray()
Compute MD5
, SHA-1
, and SHA-256
checksums for arbitrary files.
val file = SystemLocations.Home / ".gitconfig"
file.md5Checksum()
file.sha1Checksum()
file.sha256Checksum()
The Factory interface provides
the factory builders creator
, converter
, and parser
to easily implement
the factory methods of
/ofOrNull
, from
/fromOrNull
, and parse
/parseOrNull
as shown in the following example:
data class Version(val major: Int, val minor: Int, val patch: Int) {
companion object : Parser<Version> by (parser { // The `parsing` method supports the following outcomes:
it.split('.').let { (major, minor, patch) -> // - return a `Version` instance in case of success
Version(major.toInt(), minor.toInt(), patch.toInt()) // - return `null` a generic ParsingException is thrown.
} // - If you throw an exception it will be wrapped in a ParsingException.
})
}
Version.parseOrNull("1.2.3") // returns Version(1, 2, 3)
Version.parse("invalid") // throws ParsingException: "Failed to parse "invalid" into an instance of Version"
Easily access your working directory with SystemLocations.Work
,
your home directory with SystemLocations.Home
and your system's
temporary directory with SystemLocations.Temp
.
Create files with contents in one call using
createTextFile
createBinaryFile
createTempTextFile
createTempBinaryFile
Safely read files with
useInputStream
, useBufferedInputStream
, useReader
, and useBufferedReader
,
and write files with
useOutputStream
, useBufferedOutputStream
, useWriter
, and useBufferedWriter
.
Find the class directory, the source directory or the source file itself of a class.
Foo::class.findClassesDirectoryOrNull() // /home/john/dev/project/build/classes/kotlin/jvm/test
Foo::class.findSourceDirectoryOrNull() // /home/john/dev/project/src/jvmTest/kotlin
Foo::class.findSourceFileOrNull() // /home/john/dev/project/src/jvmTest/kotlin/packages/source.kt
Access class path resources like any other NIO 2 path using the classpath
URI scheme.
Paths.get("classpath:dir/to/resource").readText()
Paths.get("classpath:dir/to/resource").readBytes()
Paths.get("classpath:dir/to/resource").copyToDirectory(SystemLocations.Temp)
Paths.get("classpath:dir/to/resource").useBufferedReader { it.readLine() }
0.5.scale(+0.5) // = +0.75 (0.5 scaled 50% closer to +1.0)
0.5.scale(-0.5) // = -0.25 (0.5 scaled 50% closer to -1.0)
4.0.scale(+0.5, -10.0..+10.0) // = +7.0 (4.0 scaled 50% closer to +10.0)
4.0.scale(-0.5, -10.0..+10.0) // = -4.0 (4.0 scaled 50% closer to -10.0)
Generic either type that can be used as a replacement for Result
,
i.e. in cases where the alternative value doesn't necessarily mean failure.
Available methods are:
getLeftOrThrow
/getRightOrThrow
getLeftOrElse
/getRightOrElse
getLeftOrDefault
/getRightOrDefault
fold
/mapLeft
/mapRight
onLeft
/onLeft
toResult
/Result.toEither
val foo: Either<Foo, Bar> = Left(Foo)
foo.getLeftOrThrow() // returns Foo
foo.getRighttOrThrow() // throws an exception
Want to contribute? Awesome! The most basic way to show your support is to star the project, or to raise issues. You can also support this project by making a PayPal donation to ensure this journey continues indefinitely!
Thanks again for your support, it is much appreciated! 🙏
MIT. See LICENSE for more details.