A Kotlin Multiplatform library that generates lexicographically sortable keys for application-defined ordering (e.g., drag-and-drop lists).
Each key is a canonical variable-length byte sequence ending in 0x80. By generating new keys from neighboring items (before / after / between), most inserts can be handled without rewriting the entire list.
import dev.pon.fractionalindexing.FractionalIndex
import dev.pon.fractionalindexing.FractionalIndexGenerator
val center = FractionalIndex.default()
val left = FractionalIndexGenerator.before(center)
val right = FractionalIndexGenerator.after(center)
val mid = FractionalIndexGenerator
.between(left, right)
.getOrThrow()
check(left < mid && mid < right)
Example App
A Kotlin Multiplatform example module is available at example/.
Run the drag-and-drop list demo with ./gradlew :example:jvmRun.
import dev.pon.fractionalindexing.after
import dev.pon.fractionalindexing.before
import dev.pon.fractionalindexing.between
val center = FractionalIndex.default()
val left = center.before()
val right = center.after()
val mid = left.between(right).getOrThrow()
Parse and Encode
val indexFromHex = FractionalIndex.fromHexString("7f80").getOrThrow()
val indexFromSortableBase64 = FractionalIndex.fromSortableBase64String("Us-").getOrThrow()
val indexFromBase64 = FractionalIndex.fromBase64String("f4A=").getOrThrow()
val rawBytes = indexFromHex.toByteArray()
val indexFromBytes = FractionalIndex.fromByteArray(rawBytes).getOrThrow()
val hex = indexFromHex.toHexString() // "7f80"val sortableBase64 = indexFromSortableBase64.toSortableBase64String()
base64 = indexFromBase64.toBase64String()
fromByteArray / / / accept canonical library format only.
Ending with is necessary but not sufficient: the first byte is also a format tag.
Malformed or non-canonical keys (for example , , ) return failure.
The existing APIs (, , and ) remain available but are deprecated.
Changes to the key format or generation algorithm that break compatibility with previously generated keys are treated as breaking changes (major version bump).
Changes that only affect the exact canonical keys produced by future calls are not considered breaking as long as existing keys remain valid and continue to sort correctly.
Algorithmic output changes may still be called out in release notes.
Note: Upgrading to a new major version may require migrating your existing database records to maintain the correct sort order.
API Compatibility Check
CI runs ./gradlew :library:checkKotlinAbi --no-configuration-cache to detect binary-incompatible public API changes.
When intentionally changing public API, regenerate the baseline with ./gradlew :library:updateKotlinAbi --no-configuration-cache and commit the updated ABI dump files under library/api/.
Performance Regression Check
Deterministic key-size checks (FractionalIndexGeneratorBenchmarkRegressionTest) run in regular JVM tests.
FractionalIndex values are lexicographically comparable (Comparable<FractionalIndex>).
FractionalIndexGenerator.between(...) accepts bounds in either order.
FractionalIndexGenerator.rebalance(...) accepts either-side null endpoints for open-ended generation.
FractionalIndexGenerator.rebalance(count, null, null) starts from FractionalIndex.default().
FractionalIndexGenerator.rebalance(count, lowerEndpoint, upperEndpoint) includes non-null endpoints in the returned list.
When both endpoints are non-null, they must define a valid ascending sequence for count.
Raw bytes preserve FractionalIndex order only under unsigned-lexicographic comparison. ByteArray is not Comparable in Kotlin, and signed Byte comparison does not preserve this order; compare FractionalIndex values instead.
toString() is a debug representation. Use toByteArray(), toHexString(), toSortableBase64String(), or toBase64String() for serialization.
toSortableBase64String() is a library-specific encoding that preserves sort order. Not a standard — see SortableBase64 for the encoding specification.
toBase64String() uses standard Base64 (RFC 4648) but does not preserve sort order.
Wall-clock checks (FractionalIndexGeneratorPerformanceRegressionTest) are opt-in locally and remain merge-gating in CI.
CI runs functional JVM tests separately, then runs the wall-clock check. An initial wall-clock failure is confirmed
with one fresh measurement; the required job fails only when both measurements fail.
The scheduled/manual performance observation workflow (.github/workflows/perf-observation.yml) runs the strict
wall-clock budgets and archives the measured profile, memory observation, and test report.