@@ -18,30 +18,74 @@ object Base62 : BaseRadix(BASE_62)
1818object Base36 : BaseRadix(BASE_62 .take(36))
1919
2020/* *
21- * Generic base-N encoder/decoder for binary data using arbitrary alphabets and block processing.
21+ * Maps indices to characters (encoding) and characters back to indices (decoding).
22+ * [indexOf] returns -1 for characters not in the alphabet.
2223 */
23- open class BaseRadix (private val alphabet : String , val blockSize : Int = 32 ) :
24- ByteEncoding {
24+ interface Alphabet {
25+ val size: Int
26+ operator fun get (index : Int ): Char
27+ fun indexOf (c : Char ): Int
28+ }
2529
30+ /* *
31+ * Alphabet backed by an explicit string of characters.
32+ */
33+ class CharAlphabet (private val chars : String ) : Alphabet {
34+ init {
35+ require(chars.length > 1 ) { " Alphabet must contain at least 2 characters." }
36+ require(chars.toSet().size == chars.length) { " Alphabet must not contain duplicate characters." }
37+ }
38+
39+ override val size: Int = chars.length
40+ private val maxCode: Int = chars.maxOf { it.code }
41+ private val inverse: IntArray = IntArray (maxCode + 1 ) { - 1 }.also { lookup ->
42+ chars.forEachIndexed { index, c -> lookup[c.code] = index }
43+ }
44+
45+ override fun get (index : Int ): Char = chars[index]
46+ override fun indexOf (c : Char ): Int = if (c.code <= maxCode) inverse[c.code] else - 1
47+ }
48+
49+ /* *
50+ * Alphabet backed by a contiguous Unicode range starting at [start].
51+ * Defaults to U+0020 – U+D7FF (55,264 characters), the largest BMP range
52+ * that avoids surrogate code points.
53+ */
54+ class UnicodeRangeAlphabet (
55+ private val start : Int = 0x0020 ,
56+ override val size : Int = 0xD800 - 0x0020
57+ ) : Alphabet {
2658 init {
27- require(alphabet.length > 1 ) { " Alphabet must contain at least 2 characters." }
28- require(alphabet.toSet().size == alphabet.length) { " Alphabet must not contain duplicate characters." }
59+ require(size > 1 ) { " Alphabet must contain at least 2 characters." }
60+ require(start >= 0 ) { " Unicode range start must be non-negative." }
61+ require(start + size <= 0xD800 || start >= 0xE000 ) {
62+ " Unicode range must not overlap surrogate block (U+D800–U+DFFF)."
63+ }
64+ require(start + size <= 0x110000 ) { " Unicode range out of bounds." }
2965 }
3066
31- private val alphabetSize: Int = alphabet.length
32- private val base: BigInteger = BigInteger .fromLong(alphabetSize.toLong())
33- private val logBase: Double = log2(alphabetSize.toDouble())
67+ override fun get (index : Int ): Char = (start + index).toChar()
68+ override fun indexOf (c : Char ): Int {
69+ val offset = c.code - start
70+ return if (offset in 0 until size) offset else - 1
71+ }
72+ }
73+
74+ /* *
75+ * Generic base-N encoder/decoder for binary data using arbitrary alphabets and block processing.
76+ */
77+ open class BaseRadix (private val alphabet : Alphabet , val blockSize : Int = 32 ) :
78+ ByteEncoding {
79+
80+ constructor (chars: String , blockSize: Int = 32 ) : this (CharAlphabet (chars), blockSize)
81+
82+ private val base: BigInteger = BigInteger .fromLong(alphabet.size.toLong())
83+ private val logBase: Double = log2(alphabet.size.toDouble())
3484
3585 private val bigZero = BigInteger .ZERO
3686 private val bigFF = BigInteger .fromLong(0xFFL )
3787 private val zeroChar: Char get() = alphabet[0 ]
3888
39- private val maxAlphabetChar: Int = alphabet.maxOf { it.code }
40- private val inverseAlphabet: IntArray =
41- IntArray (maxAlphabetChar + 1 ) { - 1 }.also { lookup ->
42- alphabet.forEachIndexed { index, c -> lookup[c.code] = index }
43- }
44-
4589 private val lengths: IntArray = IntArray (blockSize) { blockIndex ->
4690 val bytesCount = blockIndex + 1
4791 ceil((bytesCount * 8 ) / logBase).toInt()
@@ -136,8 +180,7 @@ open class BaseRadix(private val alphabet: String, val blockSize: Int = 32) :
136180 var writeIndex = output.length - 1
137181 while (n > bigZero && writeIndex >= startPos) {
138182 val remainder = n.rem(base)
139- output[writeIndex-- ] =
140- alphabet[remainder.intValue(exactRequired = false )]
183+ output[writeIndex-- ] = alphabet[remainder.intValue(exactRequired = false )]
141184 n / = base
142185 }
143186 return output
@@ -153,9 +196,7 @@ open class BaseRadix(private val alphabet: String, val blockSize: Int = 32) :
153196 ): ByteArray {
154197 var n = bigZero
155198 for (i in inPos until (inPos + inLen)) {
156- val code = input[i].code
157- val index =
158- if (code < inverseAlphabet.size) inverseAlphabet[code] else - 1
199+ val index = alphabet.indexOf(input[i])
159200 require(index >= 0 ) { " Not an encoding char: '${input[i]} '" }
160201 n = n * base + BigInteger .fromLong(index.toLong())
161202 }
0 commit comments