Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Stop guessing if a variable named `fileSize` is in Bytes, Kilobytes, or Megabyte
## ✨ Key Features
* **Zero Allocation:** Built entirely on `@JvmInline value class` for zero garbage collection overhead at runtime.
* **Strict Standards:** Explicit support for both Metric/SI (powers of 1,000) and Binary/IEC (powers of 1,024) capacities.
* **Safe Math:** Add, subtract, multiply, and divide capacities with built-in zero-coercion to prevent negative byte bounds.
* **Safe Dimensional Math:** Add, subtract, multiply, and divide capacities safely. Dividing two capacities correctly yields a scalar ratio. Zero-coercion prevents negative byte bounds.
* **Memory Safe:** Safely allocate `ByteArray` and `UByteArray` buffers with built-in protections against `Int.MAX_VALUE` overflows.
* **Platform Native:** Read sizes instantly from Android `File`/`Path` and iOS `NSURL`/`NSData`.
* **Thread-Safe UI Formatting:** Localized, multiplatform string formatting using thread-safe `DecimalFormat` (Android) and `NSNumberFormatter` (iOS).

Expand All @@ -22,17 +23,17 @@ Kapacity is published to Maven Central. Add the following to your `build.gradle.
```kotlin
dependencies {
// Core library
implementation("io.github.developrofthings:kapacity:0.9.9-beta04")
implementation("io.github.developrofthings:kapacity:0.9.9-beta06")

// Optional: kotlinx-io extensions (Buffer and ByteString interoperability)
implementation("io.github.developrofthings:kapacity-io:0.9.9-beta04")
implementation("io.github.developrofthings:kapacity-io:0.9.9-beta06")
}
```

## 📖 Usage

### 1. Creating Capacities
Kapacity provides fluent extension properties for `Long`, `ULong`, `Double`, and `Float`.
Kapacity provides fluent extension properties for `Long`, `Int`, `Double`, `Float`, and their unsigned counterparts.
By default, standard properties (`.kilobyte`, `.megabyte`) use the **Metric (base-10)** standard. If you need exact memory measurements, use the **Binary (base-2)** equivalents.

``` kotlin
Expand All @@ -43,11 +44,15 @@ val downloadSize = 5.megabyte
val fractionalSize = 1.5.gigabyte

// Binary (1 MiB = 1,048,576 Bytes)
val ramCache = 512.binaryMegabyte
val bufferSize = 16.binaryKilobyte
val ramCache = 512.mebibyte
val bufferSize = 16.kibibyte

// Unsigned support
val rawBytes = 1024uL.byte

// Measure arrays and collections instantly
val myBuffer = ByteArray(2048)
val bufferCap = myBuffer.kapacity // 2.048 KB
```

### 2. Math & Operations
Expand All @@ -57,18 +62,36 @@ Because Kapacity is strictly typed, you can perform math operations directly on
val total = 1.gigabyte
val downloaded = 250.megabyte

// Standard math operators
val remaining = total - downloaded
val twiceAsLarge = total * 2

// Zero-coercion prevents negative data bounds natively!
// Dimensional Division: Dividing a capacity by a capacity returns a Long ratio!
val chunkCount = 10.megabyte / 2.megabyte // Returns 5L

// Zero-coercion prevents negative data bounds natively
val overSubtracted = 10.megabyte - 50.megabyte // Returns 0 Bytes
```

### 3. File System Integration (Android & iOS)
### 3. Safe Memory Allocation
Need to allocate a buffer based on a capacity? Kapacity provides safe builders that automatically truncate allocations to `Int.MAX_VALUE` (≈ 2.14 GB) to prevent fatal `OutOfMemoryError` or `IllegalArgumentException` crashes on the JVM.

```kotlin
val targetSize = 16.kilobyte

// Allocates an exactly sized, zero-initialized ByteArray or UByteArray
val primitiveBuffer = targetSize.toByteArray()
val unsignedBuffer = targetSize.toUByteArray()

// Safely caps out at Int.MAX_VALUE under the hood
val massiveBuffer = 5.gigabyte.toByteArray()
```

### 4. File System Integration (Android & iOS)
Kapacity includes platform-specific extensions to instantly measure file sizes directly from the disk or memory.

**On Android:**
``` kotlin
```kotlin
val myFile = File("/path/to/video.mp4")
val capacity = myFile.kapacity

Expand All @@ -85,7 +108,7 @@ val buffer = NSData.dataWithBytes(...)
val bufferCapacity = buffer.kapacity
```

### 4. Human-Readable UI Formatting
### 5. Human-Readable UI Formatting
Kapacity handles localized formatting out of the box. The `toString()` function safely formats the underlying bytes into a readable string (e.g., "1.5 MB"), utilizing localized decimal separators.

``` kotlin
Expand Down
3 changes: 1 addition & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ kotlin {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
// implementation(libs.kapacity.io)
implementation(project(":kapacity"))
implementation(libs.kapacity.io)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kapacity = "0.9.9-beta05"
kapacity = "0.9.9-beta06"
agp = "8.11.2"
android-compileSdk = "36"
android-minSdk = "24"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ value class Kapacity private constructor(val rawBytes: Long) : Comparable<Kapaci

override fun toString(): String = toString(unit = null, useMetric = true, useUnitSuffix = true)


//region `Long` operator overloads
/**
* Adds the specified number of bytes to this capacity.
* Ensures the resulting capacity is never negative.
*/
operator fun plus(bytes: Long): Kapacity = fromBytes(
bytes = (this.rawBytes + bytes).coerceAtLeast(minimumValue = 0L)
)

/**
* Subtracts the specified number of bytes from this capacity.
* Ensures the resulting capacity is never negative.
*/
operator fun minus(bytes: Long): Kapacity = fromBytes(
bytes = (this.rawBytes - bytes).coerceAtLeast(minimumValue = 0L)
)

/**
* Multiplies this capacity by a given scalar factor.
* Ensures the resulting capacity is never negative.
*/
operator fun times(factor: Long): Kapacity = fromBytes(
bytes = (this.rawBytes * factor).coerceAtLeast(minimumValue = 0L)
)

/**
* Divides this capacity by a given scalar divisor.
* Ensures the resulting capacity is never negative.
*/
operator fun div(divisor: Long): Kapacity = fromBytes(
bytes = (this.rawBytes / divisor).coerceAtLeast(minimumValue = 0L)
)
//endregion

//region `Int` operator overloads
/**
* Adds the specified number of bytes to this capacity.
* Ensures the resulting capacity is never negative.
Expand All @@ -59,46 +95,81 @@ value class Kapacity private constructor(val rawBytes: Long) : Comparable<Kapaci
* Multiplies this capacity by a given scalar factor.
* Ensures the resulting capacity is never negative.
*/
operator fun times(bytes: Int): Kapacity = times(bytes = bytes.toLong())
operator fun times(bytes: Int): Kapacity = times(factor = bytes.toLong())

/**
* Divides this capacity by a given scalar divisor.
* Ensures the resulting capacity is never negative.
*/
operator fun div(bytes: Int): Kapacity = div(bytes = bytes.toLong())
operator fun div(divisor: Int): Kapacity = div(divisor = divisor.toLong())
//endregion

//region `Double` operator overloads
/**
* Adds the specified number of bytes to this capacity.
* Ensures the resulting capacity is never negative.
*/
operator fun plus(bytes: Long): Kapacity = fromBytes(
bytes = (this.rawBytes + bytes).coerceAtLeast(minimumValue = 0L)
operator fun plus(bytes: Double): Kapacity = fromBytes(
bytes = (this.rawBytes + bytes).coerceAtLeast(minimumValue = 0.0).roundToLong()
)

/**
* Subtracts the specified number of bytes from this capacity.
* Ensures the resulting capacity is never negative.
*/
operator fun minus(bytes: Long): Kapacity = fromBytes(
bytes = (this.rawBytes - bytes).coerceAtLeast(minimumValue = 0L)
operator fun minus(bytes: Double): Kapacity = fromBytes(
bytes = (this.rawBytes - bytes).coerceAtLeast(minimumValue = 0.0).roundToLong()
)

/**
* Multiplies this capacity by a given scalar factor.
* Ensures the resulting capacity is never negative.
*/
operator fun times(bytes: Long): Kapacity = fromBytes(
bytes = (this.rawBytes * bytes).coerceAtLeast(minimumValue = 0L)
operator fun times(factor: Double): Kapacity = fromBytes(
bytes = (this.rawBytes * factor).coerceAtLeast(minimumValue = 0.0).roundToLong()
)

/**
* Divides this capacity by a given scalar divisor.
* Ensures the resulting capacity is never negative.
*/
operator fun div(bytes: Long): Kapacity = fromBytes(
bytes = (this.rawBytes / bytes).coerceAtLeast(minimumValue = 0L)
)
operator fun div(divisor: Double): Kapacity {
require(divisor != 0.0 && !divisor.isNaN() && !divisor.isInfinite()) {
"Cannot divide Kapacity by zero, NaN, or Infinity."
}
return fromBytes(
bytes = (this.rawBytes / divisor).coerceAtLeast(minimumValue = 0.0).roundToLong()
)
}
//endregion

//region `Float` operator overloads
/**
* Adds the specified number of bytes to this capacity.
* Ensures the resulting capacity is never negative.
*/
operator fun plus(bytes: Float): Kapacity = plus(bytes = bytes.toDouble())

/**
* Subtracts the specified number of bytes from this capacity.
* Ensures the resulting capacity is never negative.
*/
operator fun minus(bytes: Float): Kapacity = minus(bytes = bytes.toDouble())

/**
* Multiplies this capacity by a given scalar factor.
* Ensures the resulting capacity is never negative.
*/
operator fun times(factor: Float): Kapacity = times(factor = factor.toDouble())

/**
* Divides this capacity by a given scalar divisor.
* Ensures the resulting capacity is never negative.
*/
operator fun div(divisor: Float): Kapacity = div(divisor = divisor.toDouble())
//endregion

//region `Kapacity` operator overloads
/**
* Adds another [Kapacity] to this one.
* Ensures the resulting capacity is never negative.
Expand All @@ -117,6 +188,7 @@ value class Kapacity private constructor(val rawBytes: Long) : Comparable<Kapaci
* [Long] ratio representing how many times the [other] capacity fits into this one.
*/
operator fun div(other: Kapacity): Long = this.rawBytes / other.rawBytes
//endregion

override fun compareTo(other: Kapacity): Int = this.rawBytes.compareTo(other.rawBytes)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.github.developrofthings.kapacity

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class KapacityOperatorTest {

//region Double Division Tests
@Test
fun testDoubleDivisionHappyPath() {
val capacity = 10.megabyte
val result = capacity / 2.0

assertEquals(5.megabyte.rawBytes, result.rawBytes)
}

@Test
fun testDoubleDivisionByZeroThrowsException() {
val capacity = 10.megabyte

assertFailsWith<IllegalArgumentException> {
capacity / 0.0
}
}

@Test
fun testDoubleDivisionByNaNThrowsException() {
val capacity = 10.megabyte

assertFailsWith<IllegalArgumentException> {
capacity / Double.NaN
}
}

@Test
fun testDoubleDivisionByInfinityThrowsException() {
val capacity = 10.megabyte

assertFailsWith<IllegalArgumentException> {
capacity / Double.POSITIVE_INFINITY
}

assertFailsWith<IllegalArgumentException> {
capacity / Double.NEGATIVE_INFINITY
}
}
//endregion

//region Float Division Tests
@Test
fun testFloatDivisionHappyPath() {
val capacity = 10.megabyte
val result = capacity / 2.0f

assertEquals(5.megabyte.rawBytes, result.rawBytes)
}

@Test
fun testFloatDivisionByZeroThrowsException() {
val capacity = 10.megabyte

assertFailsWith<IllegalArgumentException> {
capacity / 0.0f
}
}

@Test
fun testFloatDivisionByNaNThrowsException() {
val capacity = 10.megabyte

assertFailsWith<IllegalArgumentException> {
capacity / Float.NaN
}
}
//endregion
}
Loading