
Fast, deterministic integer permutations producing keyed, reversible bijections over ranges. Memory-efficient, invertible shuffles with cycle-walking, xor-shift-multiply mixing, seeded determinism, and full/unsigned domain support.
Fast, seeded, reversible permutations over integer domains of any size. Use it to obfuscate IDs, shuffle lazily, or sample without collisions. Not cryptographic.
implementation("com.eignex:kpermute:1.3.0")Obfuscate an ID:
val idPerm = longPermutation(seed = 1L)
val longId = 49102490812045L
val encoded = idPerm.encode(longId)
println("encoded: $encoded (always prints 3631103739497407856)")Shuffle a list lazily:
val largeList = object : AbstractList<Int>() {
override val size: Int get() = Int.MAX_VALUE
override fun get(index: Int) = index
}
val perm = intPermutation(largeList.size)
val shuffled = largeList.permuted(perm)
println("shuffled: ${shuffled.take(20)}")
val unshuffled = shuffled.unpermuted(perm)
println("unshuffled: ${unshuffled.take(20)}")Permute a range:
val rangePerm = intPermutation(-100..199)
println("encode(-50): ${rangePerm.encode(-50)}")
println("decode(...): ${rangePerm.decode(rangePerm.encode(-50))}")Permute the full 32-bit domain:
val fullPerm = intPermutation(-1, seed = 1L)
println(fullPerm.encode(0)) // 1789640036
println(fullPerm.encode(1)) // -299069089The size selects the domain 0 until size. A size of -1 means every 32- or 64-bit value, and other negative sizes are read as unsigned.
Sizes up to 4096 use a shuffled lookup table. Larger sizes mix within the enclosing power-of-two block and use cycle-walking, repeating until the output is in range. The full 32- or 64-bit domain mixes directly. Range factories wrap any of these in a view over a non-empty range.
Each mixing round multiplies by an odd constant, adds or xors a per-round key, and xor-shifts the value right, in the style of Jenkins' integer hashes. Every step is invertible.
Fast, seeded, reversible permutations over integer domains of any size. Use it to obfuscate IDs, shuffle lazily, or sample without collisions. Not cryptographic.
implementation("com.eignex:kpermute:1.3.0")Obfuscate an ID:
val idPerm = longPermutation(seed = 1L)
val longId = 49102490812045L
val encoded = idPerm.encode(longId)
println("encoded: $encoded (always prints 3631103739497407856)")Shuffle a list lazily:
val largeList = object : AbstractList<Int>() {
override val size: Int get() = Int.MAX_VALUE
override fun get(index: Int) = index
}
val perm = intPermutation(largeList.size)
val shuffled = largeList.permuted(perm)
println("shuffled: ${shuffled.take(20)}")
val unshuffled = shuffled.unpermuted(perm)
println("unshuffled: ${unshuffled.take(20)}")Permute a range:
val rangePerm = intPermutation(-100..199)
println("encode(-50): ${rangePerm.encode(-50)}")
println("decode(...): ${rangePerm.decode(rangePerm.encode(-50))}")Permute the full 32-bit domain:
val fullPerm = intPermutation(-1, seed = 1L)
println(fullPerm.encode(0)) // 1789640036
println(fullPerm.encode(1)) // -299069089The size selects the domain 0 until size. A size of -1 means every 32- or 64-bit value, and other negative sizes are read as unsigned.
Sizes up to 4096 use a shuffled lookup table. Larger sizes mix within the enclosing power-of-two block and use cycle-walking, repeating until the output is in range. The full 32- or 64-bit domain mixes directly. Range factories wrap any of these in a view over a non-empty range.
Each mixing round multiplies by an odd constant, adds or xors a per-round key, and xor-shifts the value right, in the style of Jenkins' integer hashes. Every step is invertible.