Skip to content

Commit 50956be

Browse files
committed
Improve
1 parent 27aecf7 commit 50956be

8 files changed

Lines changed: 197 additions & 24 deletions

File tree

.idea/vcs.xml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ group = "de.no3x"
88
version = "1.0.0"
99

1010
dependencies {
11+
val junitVersion = "5.14.1"
12+
implementation(platform("org.junit:junit-bom:$junitVersion"))
1113
testImplementation("com.github.No3x:junit-locale-extension:1.0.0")
12-
testImplementation(kotlin("test"))
14+
testImplementation("com.willowtreeapps.assertk:assertk:0.28.1")
15+
testImplementation(group = "org.junit.jupiter", name = "junit-jupiter-engine")
16+
testImplementation(group = "org.junit.jupiter", name = "junit-jupiter-params")
17+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
1318
}
1419

1520
java {

src/main/kotlin/de/no3x/kbytes/Types.kt

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package de.no3x.kbytes
22

33
import kotlin.math.abs
4+
import java.math.BigDecimal
45

56
@JvmInline
6-
value class StorageSize private constructor(val bytes: Long) : Comparable<StorageSize> {
7+
value class StorageSize(val bytes: Long) : Comparable<StorageSize> {
78

89
companion object {
910
fun ofBytes(bytes: Long) = StorageSize(bytes)
@@ -13,6 +14,19 @@ value class StorageSize private constructor(val bytes: Long) : Comparable<Storag
1314

1415
fun of(value: Long, unit: DecimalUnit): StorageSize =
1516
StorageSize(value * unit.bytes)
17+
18+
fun of(value: Double, unit: BinaryUnit): StorageSize =
19+
fromExactDouble(value, unit.bytes)
20+
21+
fun of(value: Double, unit: DecimalUnit): StorageSize =
22+
fromExactDouble(value, unit.bytes)
23+
24+
private fun fromExactDouble(value: Double, unitBytes: Long): StorageSize {
25+
val bytes = BigDecimal.valueOf(value)
26+
.multiply(BigDecimal.valueOf(unitBytes))
27+
.longValueExact() // fail fast if fractional bytes would be required
28+
return StorageSize(bytes)
29+
}
1630
}
1731

1832
// Arithmetic (mirrors add/subtract/multiply/divide)
@@ -74,6 +88,15 @@ fun mebibyte(value: Long): StorageSize =
7488
fun gibibyte(value: Long): StorageSize =
7589
StorageSize.of(value, BinaryUnit.GIBIBYTE)
7690

91+
fun tebibyte(value: Long): StorageSize =
92+
StorageSize.of(value, BinaryUnit.TEBIBYTE)
93+
94+
fun pebibyte(value: Long): StorageSize =
95+
StorageSize.of(value, BinaryUnit.PEBIBYTE)
96+
97+
fun exbibyte(value: Long): StorageSize =
98+
StorageSize.of(value, BinaryUnit.EXBIBYTE)
99+
77100
fun kilobyte(value: Long): StorageSize =
78101
StorageSize.of(value, DecimalUnit.KILOBYTE)
79102

@@ -83,20 +106,41 @@ fun megabyte(value: Long): StorageSize =
83106
fun gigabyte(value: Long): StorageSize =
84107
StorageSize.of(value, DecimalUnit.GIGABYTE)
85108

109+
fun terabyte(value: Long): StorageSize =
110+
StorageSize.of(value, DecimalUnit.TERABYTE)
111+
112+
fun petabyte(value: Long): StorageSize =
113+
StorageSize.of(value, DecimalUnit.PETABYTE)
114+
115+
fun exabyte(value: Long): StorageSize =
116+
StorageSize.of(value, DecimalUnit.EXABYTE)
117+
86118
// --- Nice Kotlin DSL-style extensions ---------------------------------------
87119

88120
val Int.KiB: StorageSize get() = kibibyte(this.toLong())
89121
val Int.MiB: StorageSize get() = mebibyte(this.toLong())
90122
val Int.GiB: StorageSize get() = gibibyte(this.toLong())
123+
val Int.TiB: StorageSize get() = tebibyte(this.toLong())
124+
val Int.PiB: StorageSize get() = pebibyte(this.toLong())
125+
val Int.EiB: StorageSize get() = exbibyte(this.toLong())
91126

92127
val Int.KB: StorageSize get() = kilobyte(this.toLong())
93128
val Int.MB: StorageSize get() = megabyte(this.toLong())
94129
val Int.GB: StorageSize get() = gigabyte(this.toLong())
130+
val Int.TB: StorageSize get() = terabyte(this.toLong())
131+
val Int.PB: StorageSize get() = petabyte(this.toLong())
132+
val Int.EB: StorageSize get() = exabyte(this.toLong())
95133

96134
val Long.KiB: StorageSize get() = kibibyte(this)
97135
val Long.MiB: StorageSize get() = mebibyte(this)
98136
val Long.GiB: StorageSize get() = gibibyte(this)
137+
val Long.TiB: StorageSize get() = tebibyte(this)
138+
val Long.PiB: StorageSize get() = pebibyte(this)
139+
val Long.EiB: StorageSize get() = exbibyte(this)
99140

100141
val Long.KB: StorageSize get() = kilobyte(this)
101142
val Long.MB: StorageSize get() = megabyte(this)
102-
val Long.GB: StorageSize get() = gigabyte(this)
143+
val Long.GB: StorageSize get() = gigabyte(this)
144+
val Long.TB: StorageSize get() = terabyte(this.toLong())
145+
val Long.PB: StorageSize get() = petabyte(this.toLong())
146+
val Long.EB: StorageSize get() = exabyte(this.toLong())

src/main/kotlin/de/no3x/kbytes/Unit.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@ enum class DecimalUnit(val symbol: String, val bytes: Long) {
1818
GIGABYTE("GB", 1_000_000_000L),
1919
TERABYTE("TB", 1_000_000_000_000L),
2020
PETABYTE("PB", 1_000_000_000_000_000L),
21-
EXABYTE("EB", 1_000_000_000_000_000_000L),
21+
EXABYTE("EB", 1_000_000_000_000_000_000L), ;
22+
}
23+
24+
public fun BinaryUnit.nextLargerUnit(): BinaryUnit {
25+
val values = enumValues<BinaryUnit>()
26+
val nextOrdinal = (ordinal + 1)
27+
return values[nextOrdinal]
28+
}
29+
30+
public fun DecimalUnit.nextLargerUnit(): DecimalUnit {
31+
val values = enumValues<DecimalUnit>()
32+
val nextOrdinal = (ordinal + 1)
33+
return values[nextOrdinal]
2234
}
2335

2436
// convenience wrapper for "value + unit"
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package de.no3x.kbytes
22

3+
import assertk.assertThat
4+
import assertk.assertions.isGreaterThan
5+
import assertk.assertions.isNotNull
6+
import org.junit.jupiter.api.Test
37
import java.nio.file.Path
48
import kotlin.io.path.writeText
5-
import kotlin.test.Test
6-
import kotlin.test.assertNotNull
7-
import kotlin.test.assertTrue
89
import org.junit.jupiter.api.io.TempDir
910

1011
/**
@@ -19,10 +20,10 @@ class PathExtensionSampleTest {
1920

2021
val storageSize = tempFile.toStorageSize()
2122

22-
assertTrue(storageSize.inBytes() > 0)
23-
assertTrue(storageSize > 0.KiB)
24-
assertNotNull(storageSize.bestBinaryUnit())
25-
assertNotNull(storageSize.bestDecimalUnit())
23+
assertThat(storageSize.inBytes()).isGreaterThan(0)
24+
assertThat(storageSize).isGreaterThan(0.KiB)
25+
assertThat(storageSize.bestBinaryUnit()).isNotNull()
26+
assertThat(storageSize.bestDecimalUnit()).isNotNull()
2627
}
2728

2829
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package de.no3x.kbytes
2+
3+
import assertk.assertThat
4+
import assertk.assertions.isEqualTo
5+
import org.junit.jupiter.api.Test
6+
7+
class StorageSizeIdentityTest {
8+
9+
@Test
10+
fun `value class equality works`() {
11+
val a = StorageSize(42)
12+
val b = StorageSize(42)
13+
assertThat(a).isEqualTo(b)
14+
assertThat(a.bytes).isEqualTo(b.bytes)
15+
}
16+
}
Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package de.no3x.kbytes
22

3+
import assertk.assertThat
4+
import assertk.assertions.isCloseTo
5+
import assertk.assertions.isEqualTo
36
import de.no3x.junit.extension.locale.WithLocale
7+
import org.junit.jupiter.api.Test
48
import org.junit.jupiter.api.TestTemplate
5-
import java.util.Locale
6-
import kotlin.test.Test
7-
import kotlin.test.assertEquals
8-
import kotlin.test.assertTrue
9+
import java.util.*
910

1011
/**
1112
* Usage-style tests that also double as executable documentation for the tiny DSL.
@@ -14,16 +15,33 @@ class StorageSizeSampleTest {
1415

1516
@Test
1617
fun `dsl extensions create expected byte counts`() {
17-
assertEquals(1_024L, 1.KiB.inBytes())
18-
assertEquals(1_000_000L, 1L.MB.inBytes())
19-
assertEquals(3_000_000_000L, (3.GB).inBytes())
18+
assertThat(1.KiB.inBytes()).isEqualTo(1_024L);
19+
assertThat(1L.MB.inBytes()).isEqualTo(1_000_000L);
20+
assertThat(3.GB.inBytes()).isEqualTo(3_000_000_000L);
2021
}
2122

2223
@Test
2324
fun `arithmetic with storage sizes keeps byte precision`() {
2425
val total = 2.MiB + 512.KiB - 256.KiB
2526
val expectedBytes = (2L shl 20) + (512L shl 10) - (256L shl 10)
26-
assertEquals(expectedBytes, total.inBytes())
27+
assertThat(total.inBytes()).isEqualTo(expectedBytes)
28+
}
29+
30+
@Test
31+
fun `test inBytes`() {
32+
assertThat(1.KB.inBytes()).isEqualTo(1_000)
33+
assertThat(1.MB.inBytes()).isEqualTo(1_000_000)
34+
assertThat(1.GB.inBytes()).isEqualTo(1_000_000_000)
35+
assertThat(1.TB.inBytes()).isEqualTo(1_000_000_000_000)
36+
assertThat(1.PB.inBytes()).isEqualTo(1_000_000_000_000_000)
37+
assertThat(1.EB.inBytes()).isEqualTo(1_000_000_000_000_000_000)
38+
39+
assertThat(1.KiB.inBytes()).isEqualTo(1024)
40+
assertThat(1.MiB.inBytes()).isEqualTo(1048576)
41+
assertThat(1.GiB.inBytes()).isEqualTo(1073741824)
42+
assertThat(1.TiB.inBytes()).isEqualTo(1099511627776)
43+
assertThat(1.PiB.inBytes()).isEqualTo(1125899906842624)
44+
assertThat(1.EiB.inBytes()).isEqualTo(1152921504606846976)
2745
}
2846

2947
@TestTemplate
@@ -36,18 +54,18 @@ class StorageSizeSampleTest {
3654
"ja-JP" -> "2.36 MB"
3755
else -> "2.36 MB" // en-US and fallback
3856
}
39-
assertEquals(expected, total.toString())
57+
assertThat(total.toString()).isEqualTo(expected)
4058
}
4159

4260
@Test
4361
fun `best unit picks the largest readable unit`() {
4462
val oneAndHalfKiB = StorageSize.ofBytes(1_536)
4563
val bestBinary = oneAndHalfKiB.bestBinaryUnit()
46-
assertEquals(BinaryUnit.KIBIBYTE, bestBinary.unit)
47-
assertTrue(bestBinary.value in 1.49..1.51, "value is roughly 1.5 KiB")
64+
assertThat(bestBinary.unit).isEqualTo(BinaryUnit.KIBIBYTE)
65+
assertThat(bestBinary.value).isCloseTo(1.50, 0.01)
4866
val twoPointThreeGb = 2_300_000_000L.toStorageSize()
4967
val bestDecimal = twoPointThreeGb.bestDecimalUnit()
50-
assertEquals(DecimalUnit.GIGABYTE, bestDecimal.unit)
51-
assertTrue(bestDecimal.value in 2.29..2.31, "value is roughly 2.30 GB")
68+
assertThat(bestDecimal.unit).isEqualTo(DecimalUnit.GIGABYTE)
69+
assertThat(bestDecimal.value).isCloseTo(2.30, 0.01)
5270
}
5371
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package de.no3x.kbytes
2+
3+
import assertk.assertAll
4+
import assertk.assertThat
5+
import assertk.assertions.isEqualTo
6+
import org.junit.jupiter.api.Assertions.assertEquals
7+
import org.junit.jupiter.api.Test
8+
import org.junit.jupiter.params.ParameterizedTest
9+
import org.junit.jupiter.params.provider.EnumSource
10+
11+
class UnitConversionParameterizedTest {
12+
13+
@ParameterizedTest
14+
@EnumSource(BinaryUnit::class)
15+
fun `binary units convert from and to bytes`(unit: BinaryUnit) {
16+
val size = StorageSize.of(1, unit)
17+
18+
assertEquals(unit.bytes, size.inBytes())
19+
assertEquals(1.0, size.toBinary(unit))
20+
}
21+
22+
@ParameterizedTest
23+
@EnumSource(DecimalUnit::class)
24+
fun `decimal units convert from and to bytes`(unit: DecimalUnit) {
25+
val size = StorageSize.of(1, unit)
26+
27+
assertEquals(unit.bytes, size.inBytes())
28+
assertEquals(1.0, size.toDecimal(unit))
29+
}
30+
31+
@ParameterizedTest
32+
@EnumSource(
33+
DecimalUnit::class,
34+
names = ["EXABYTE"], // exclude last unit because it has no larger next unit,
35+
mode = EnumSource.Mode.EXCLUDE
36+
)
37+
fun `decimal units convert from and to bytes into next larger unit as well`(unit: DecimalUnit) {
38+
val nextLargerUnit = unit.nextLargerUnit()
39+
val size = StorageSize.of(1, unit)
40+
41+
assertEquals(unit.bytes, size.inBytes())
42+
assertEquals(0.001, size.toDecimal(nextLargerUnit))
43+
}
44+
45+
@ParameterizedTest
46+
@EnumSource(
47+
BinaryUnit::class,
48+
names = ["EXBIBYTE"], // exclude last unit because it has no larger next unit,
49+
mode = EnumSource.Mode.EXCLUDE
50+
)
51+
fun `binary units convert from and to bytes into next larger unit as well`(unit: BinaryUnit) {
52+
val nextLargerUnit = unit.nextLargerUnit()
53+
val size = StorageSize.of(1, unit)
54+
val expectedRatio = unit.bytes.toDouble() / nextLargerUnit.bytes.toDouble()
55+
56+
assertEquals(unit.bytes, size.inBytes())
57+
assertEquals(expectedRatio, size.toBinary(nextLargerUnit))
58+
}
59+
60+
@Test
61+
fun formatUnitWithDefaults() {
62+
assertAll {
63+
val binary = (900.KB + 123.KB).bestBinaryUnit()
64+
assertThat(binary.unit).isEqualTo(BinaryUnit.KIBIBYTE)
65+
assertThat(binary.value).isEqualTo(999.0234375)
66+
67+
val decimal = (900.KB + 123.KB).bestDecimalUnit()
68+
assertThat(decimal.unit).isEqualTo(DecimalUnit.MEGABYTE)
69+
assertThat(decimal.value).isEqualTo(1.023)
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)