Skip to content

Commit

Permalink
Fix PlainTextStringFormatter
Browse files Browse the repository at this point in the history
  • Loading branch information
ogesaku committed Jan 6, 2024
1 parent 1f2993f commit 97bdfd1
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ Add dependency to `build.gradle.kts`:

```kts
dependencies {
implementation("com.coditory.klog:klog:0.0.1")
implementation("com.coditory.klog:klog:$version")
}
```
1 change: 1 addition & 0 deletions klog/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# Klog

11 changes: 11 additions & 0 deletions klog/src/main/kotlin/com/coditory/klog/format/BytesFormat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.coditory.klog.format

object BytesFormat {
fun formatBytesSI(bytes: Long): String {
return QuantityFormat.formatQuantitySIWithSuffix(bytes, 'B')
}

fun formatBytesBin(bytes: Long): String {
return QuantityFormat.formatQuantityBinWithSuffix(bytes, 'B')
}
}
65 changes: 65 additions & 0 deletions klog/src/main/kotlin/com/coditory/klog/format/QuantityFormat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.coditory.klog.format

import java.lang.Long.signum
import kotlin.math.abs

object QuantityFormat {
fun formatQuantitySI(bytes: Long): String {
return formatQuantitySIWithSuffix(bytes, suffix = null)
}

fun formatQuantityBin(bytes: Long): String {
return formatQuantityBinWithSuffix(bytes, suffix = null)
}

internal fun formatQuantitySIWithSuffix(
bytes: Long,
suffix: Char?,
): String {
if (-1000 < bytes && bytes < 1000) {
return if (suffix != null) "$bytes $suffix" else "$bytes"
}
var significant = bytes
val x = "kMGTPE"
var xi = 0
while (xi < x.length - 1 && (significant <= -999_950 || significant >= 999_950)) {
significant /= 1000
xi++
}
return if (suffix != null) {
String.format("%.1f %c$suffix", significant / 1000.0, x[xi])
} else {
String.format("%.1f %c", significant / 1000.0, x[xi])
}
}

internal fun formatQuantityBinWithSuffix(
bytes: Long,
suffix: Char?,
): String {
val absB =
if (bytes == Long.MIN_VALUE) {
Long.MAX_VALUE
} else {
abs(bytes.toDouble()).toLong()
}
if (absB < 1024) {
return if (suffix != null) "$bytes $suffix" else "$bytes"
}
var value = absB
val x = "KMGTPE"
var xi = 0
var i = 40
while (xi < x.length - 1 && (i >= 0 && absB > 0xfffccccccccccccL shr i)) {
value = value shr 10
xi++
i -= 10
}
value *= signum(bytes).toLong()
return if (suffix != null) {
String.format("%.1f %ci$suffix", value / 1024.0, x[xi])
} else {
String.format("%.1f %ci", value / 1024.0, x[xi])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class PlainTextStringFormatterBuilder internal constructor() {
return this
}

fun pasRight(pad: Char = ' '): PlainTextStringFormatterBuilder {
fun padRight(pad: Char = ' '): PlainTextStringFormatterBuilder {
this.padding = Padding.right(pad)
return this
}
Expand Down Expand Up @@ -68,7 +68,7 @@ class PlainTextStringFormatterBuilder internal constructor() {
return this
}

fun mapped(mapper: (String) -> String?): PlainTextStringFormatterBuilder {
fun mapper(mapper: (String) -> String?): PlainTextStringFormatterBuilder {
this.mapper = mapper
return this
}
Expand Down Expand Up @@ -102,6 +102,7 @@ class PlainTextStringFormatterBuilder internal constructor() {
CompactedPlainTextStringFormatter(
separator = separator,
maxLength = maxLength,
maxLengthMarker = maxLengthMarker,
padding = padding,
prefix = prefix,
postfix = postfix,
Expand Down Expand Up @@ -149,21 +150,24 @@ internal class ConfigurablePlainTextStringFormatter(
text: String,
appendable: Appendable,
) {
if (text.isEmpty()) return
if (text.isEmpty()) {
if (pad != null) appendable.append(pad)
return
}
prefix.format(appendable)
if (padding?.left() == true && text.length < maxLength) {
appendable.append(pad, 0, text.length - maxLength)
appendable.append(pad, 0, maxLength - text.length)
}
appendable.append(style.prefix)
if (text.length < maxLength) {
if (text.length <= maxLength) {
appendable.append(text)
} else {
appendable.append(text, 0, maxLength)
appendable.append(maxLengthMarker)
}
appendable.append(style.postfix)
if (padding?.right() == true && text.length < maxLength) {
appendable.append(pad, 0, text.length - maxLength)
appendable.append(pad, 0, maxLength - text.length)
}
postfix.format(appendable)
}
Expand All @@ -172,6 +176,7 @@ internal class ConfigurablePlainTextStringFormatter(
internal class CompactedPlainTextStringFormatter(
private val separator: Char,
private val maxLength: Int,
private val maxLengthMarker: String,
private val padding: Padding?,
private val prefix: StyledText,
private val postfix: StyledText,
Expand All @@ -186,32 +191,59 @@ internal class CompactedPlainTextStringFormatter(
text: String,
appendable: Appendable,
) {
if (text.isEmpty()) return
if (text.isEmpty()) {
if (pad != null) appendable.append(pad)
return
}
val chunks = text.split(separator).filter { it.isNotEmpty() }
val last = chunks.last()
prefix.format(appendable)
if (last.length + 1 > maxLength) {
if (chunks.size == 1 || last.length >= maxLength - 1) {
if (padding?.left() == true && text.length < maxLength) {
appendable.append(pad, 0, maxLength - text.length)
}
appendable.append(style.prefix)
appendable.append(last.substring(0, maxLength))
if (last.length > maxLength) {
appendable.append(last.substring(0, maxLength))
appendable.append(maxLengthMarker)
} else {
appendable.append(last)
}
appendable.append(style.postfix)
if (padding?.right() == true && text.length < maxLength) {
appendable.append(pad, 0, maxLength - text.length)
}
postfix.format(appendable)
return
}
val length = (chunks.size * 2) + last.length
if (padding?.left() == true && length < maxLength) {
appendable.append(pad, 0, length - maxLength)
}
val prefixMaxLength = maxLength - last.length
val start = max(0, chunks.size - (prefixMaxLength / 2))
if (padding?.left() == true) {
if (length < maxLength) {
appendable.append(pad, 0, maxLength - length)
} else if (prefixMaxLength % 2 == 1) {
appendable.append(pad, 0, 1)
}
}
val start = max(0, chunks.size - 1 - (prefixMaxLength / 2))
appendable.append(style.prefix)
for (i in start..<chunks.size) {
for (i in start..<chunks.size - 1) {
appendable.append(chunks[i][0])
appendable.append(separator)
}
appendable.append(last)
if (last.length <= maxLength) {
appendable.append(last)
} else {
appendable.append(last, 0, maxLength)
appendable.append(maxLengthMarker)
}
appendable.append(style.postfix)
if (padding?.right() == true && length < maxLength) {
appendable.append(pad, 0, length - maxLength)
if (padding?.right() == true) {
if (length < maxLength) {
appendable.append(pad, 0, maxLength - length)
} else if (prefixMaxLength % 2 == 1) {
appendable.append(pad, 0, 1)
}
}
postfix.format(appendable)
}
Expand Down
43 changes: 43 additions & 0 deletions klog/src/test/kotlin/com/coditory/klog/format/BytesFormatSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.coditory.klog.format

import io.kotest.core.spec.style.FunSpec
import io.kotest.core.tuple
import io.kotest.matchers.shouldBe

class BytesFormatSpec : FunSpec({
listOf(
tuple(1L, "1 B"),
tuple(123L, "123 B"),
tuple(999L, "999 B"),
tuple(1000L, "1.0 kB"),
tuple(1001L, "1.0 kB"),
tuple(123456L, "123.5 kB"),
tuple(123456L * 1000, "123.5 MB"),
tuple(123456L * 1000 * 1000, "123.5 GB"),
tuple(123456L * 1000 * 1000 * 1000, "123.5 TB"),
tuple(123456L * 1000 * 1000 * 1000 * 1000, "123.5 PB"),
tuple(123456L * 1000 * 1000 * 1000 * 1000 * 10, "1.2 EB"),
).forEach {
test("format SI: ${it.a} -> \"${it.b}\"") {
BytesFormat.formatBytesSI(it.a) shouldBe it.b
}
}

listOf(
tuple(1L, "1 B"),
tuple(123L, "123 B"),
tuple(1023L, "1023 B"),
tuple(1024L, "1.0 KiB"),
tuple(1025L, "1.0 KiB"),
tuple(123456L, "120.6 KiB"),
tuple(123456L * 1024, "120.6 MiB"),
tuple(123456L * 1024 * 1024, "120.6 GiB"),
tuple(123456L * 1024 * 1024 * 1024, "120.6 TiB"),
tuple(123456L * 1024 * 1024 * 1024 * 1024, "120.6 PiB"),
tuple(123456L * 1024 * 1024 * 1024 * 1024 * 10, "1.2 EiB"),
).forEach {
test("format bin: ${it.a} -> \"${it.b}\"") {
BytesFormat.formatBytesBin(it.a) shouldBe it.b
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.coditory.klog.format

import com.coditory.klog.format.QuantityFormat.formatQuantityBin
import com.coditory.klog.format.QuantityFormat.formatQuantitySI
import io.kotest.core.spec.style.FunSpec
import io.kotest.core.tuple
import io.kotest.matchers.shouldBe

class QuantityFormatSpec : FunSpec({
listOf(
tuple(1L, "1"),
tuple(123L, "123"),
tuple(999L, "999"),
tuple(1000L, "1.0 k"),
tuple(1001L, "1.0 k"),
tuple(123456L, "123.5 k"),
tuple(123456L * 1000, "123.5 M"),
tuple(123456L * 1000 * 1000, "123.5 G"),
tuple(123456L * 1000 * 1000 * 1000, "123.5 T"),
tuple(123456L * 1000 * 1000 * 1000 * 1000, "123.5 P"),
tuple(123456L * 1000 * 1000 * 1000 * 1000 * 10, "1.2 E"),
).forEach {
test("format SI: ${it.a} -> \"${it.b}\"") {
formatQuantitySI(it.a) shouldBe it.b
}
}

listOf(
tuple(1L, "1"),
tuple(123L, "123"),
tuple(1023L, "1023"),
tuple(1024L, "1.0 Ki"),
tuple(1025L, "1.0 Ki"),
tuple(123456L, "120.6 Ki"),
tuple(123456L * 1024, "120.6 Mi"),
tuple(123456L * 1024 * 1024, "120.6 Gi"),
tuple(123456L * 1024 * 1024 * 1024, "120.6 Ti"),
tuple(123456L * 1024 * 1024 * 1024 * 1024, "120.6 Pi"),
tuple(123456L * 1024 * 1024 * 1024 * 1024 * 10, "1.2 Ei"),
).forEach {
test("format bin: ${it.a} -> \"${it.b}\"") {
formatQuantityBin(it.a) shouldBe it.b
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.coditory.klog.shared

import com.coditory.klog.text.plain.PlainTextStringFormatter

fun PlainTextStringFormatter.formatToString(text: String): String {
val sb = StringBuilder()
this.format(text, sb)
return sb.toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.coditory.klog.text.plain

import com.coditory.klog.shared.formatToString
import io.kotest.core.spec.style.FunSpec
import io.kotest.core.tuple
import io.kotest.matchers.shouldBe

class CompactedPlainTextStringFormatterTest : FunSpec({
listOf(
tuple("", " "),
tuple("abcdef", " abcdef"),
tuple("abcdefghi", "abcdefghi"),
tuple("abcdefghijkl", "abcdefghi..."),
tuple("x123.y123.z123.abcdef", " z.abcdef"),
tuple("x123.y123.z123.abcde", "y.z.abcde"),
tuple("x123.y123.z123.abcd", " y.z.abcd"),
tuple("x123.y123.z123.abc", "x.y.z.abc"),
tuple("x123.y123.z123.abcdefghijkl", "abcdefghi..."),
).forEach {
test("should pad left to 9 chars: \"${it.a}\" -> \"${it.b}\"") {
val formatted =
PlainTextStringFormatter.builder()
.compactSections()
.padLeft()
.maxLength(9)
.maxLengthMarker("...")
.build()
.formatToString(it.a)
formatted shouldBe it.b
}
}

listOf(
tuple("", " "),
tuple("abcdef", "abcdef "),
tuple("abcdefghi", "abcdefghi"),
tuple("abcdefghijkl", "abcdefghi..."),
tuple("x123.y123.z123.abcdef", "z.abcdef "),
tuple("x123.y123.z123.abcde", "y.z.abcde"),
tuple("x123.y123.z123.abcd", "y.z.abcd "),
tuple("x123.y123.z123.abc", "x.y.z.abc"),
tuple("x123.y123.z123.abcdefghijkl", "abcdefghi..."),
).forEach {
test("should pad right to 9 chars: \"${it.a}\" -> \"${it.b}\"") {
val formatted =
PlainTextStringFormatter.builder()
.compactSections()
.padRight()
.maxLength(9)
.maxLengthMarker("...")
.build()
.formatToString(it.a)
formatted shouldBe it.b
}
}
})
Loading

0 comments on commit 97bdfd1

Please sign in to comment.