radixes.ts
1 const zero = '0' 2 const base9 = '123456789' 3 const baseMinus9 = '➊➋➌➍➎➏➐➑➒' 4 // const baseMinus9 = '⑨⑧⑦⑥⑤④③②①' 5 const base26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 6 const baseMinus26 = '🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩' 7 // const baseMinus26 = 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ' 8 // const base32cz = 'ABCČDĎEFGHIJKLMNŇOPQRŘSŠTŤUVXYZŽ' 9 // const base36cz = 'AÁBCČDĎEÉFGHIÍJKLMNŇOÓPQRŘSŠTŤUÚVXZŽ' 10 // const base42 = [ 'A', 'Á', 'B', 'C', 'Č', 'D', 'Ď', 'E', 'É', 'Ě', 'F', 'G', 'H', 'Ch', 'I', 'Í', 'J', 'K', 'L', 'M', 'N', 'Ň', 'O', 'Ó', 'P', 'Q', 'R', 'Ř', 'S', 'Š', 'T', 'Ť', 'U', 'Ú', 'Ů', 'V', 'W', 'X', 'Y', 'Ý', 'Z', 'Ž' ] 11 12 const defaultCharsArray: string[] = [ 13 ...Array.from(baseMinus26).reverse(), 14 ...Array.from(baseMinus9).reverse(), 15 zero, 16 ...Array.from(base9), 17 ...Array.from(base26), 18 ] 19 20 export const defaultChars: string = defaultCharsArray.join('') 21 22 export type Radix = { 23 name: string, 24 radix: bigint, 25 system: 'standard' | 'bijective' | 'balanced' | 'clock' | 'sum' | 'balsum', 26 chars: string, 27 enabled: boolean, 28 values: Map<string, bigint>, 29 reversedValues: Map<bigint, string>, 30 low: number, 31 high: number, 32 } 33 34 export function createRadixes(chars?: string): Radix[] { 35 return [ ...Array<undefined>(35) ].flatMap((_, i) => { 36 const radix = i + 2 37 const ret = [ createRadix(radix, 'standard', chars) ] 38 if (radix < 36) ret.push(createRadix(radix, 'bijective', chars)) 39 if (radix & 1) ret.push(createRadix(radix, 'balanced', chars)) 40 if (radix <= 26) ret.push(createRadix(radix, 'sum', chars)) 41 // if (radix <= 27 && radix & 1) ret.push(createRadix(radix, 'balsum', charsArray)) 42 // if ((radix & 1) === 0) ret.push(createRadix(radix, 'clock', chars)) 43 return ret 44 }) 45 } 46 47 export function createRadix(radix: number, system: Radix['system'] = 'standard', chars = defaultChars, enabled?: boolean, name?: string, allChars = true): Radix { 48 if (allChars) { 49 if (chars !== defaultChars && chars.length < defaultChars.length) throw new Error(`chars must have at least ${defaultChars.length} characters, ${chars.length} provided`) 50 if ((chars.length & 1) === 0) throw new Error('chars must have odd number of characters') 51 } 52 if (Array.from(chars).length !== new Set(chars).size) { 53 throw new Error('chars must be unique') 54 } 55 56 let ret: Radix 57 const charsArray = Array.from(chars) 58 59 switch (system) { 60 case 'standard': 61 ret = createStandardRadix(radix, charsArray, enabled, name, allChars) 62 break 63 case 'bijective': 64 ret = createBijectiveRadix(radix, charsArray, enabled, name, allChars) 65 break 66 case 'balanced': 67 ret = createBalancedRadix(radix, charsArray, enabled, name, allChars) 68 break 69 case 'sum': 70 ret = createSumRadix(radix, charsArray, enabled, name, allChars) 71 break 72 case 'balsum': 73 ret = createBalsumRadix(radix, charsArray, enabled, name, allChars) 74 break 75 case 'clock': 76 ret = createClockRadix(radix, charsArray, enabled, name, allChars) 77 break 78 default: 79 throw new Error(`createRadix: Unknown system: "${String(system)}"`) 80 } 81 82 return ret 83 } 84 85 function createStandardRadix(radix: number, chars = defaultCharsArray, enabled?: boolean, name = `${radix}`, allChars = true): Radix { 86 if (allChars) { 87 const zeroAt = (chars.length - 1) / 2 88 chars = radix === 26 ? chars.slice(zeroAt + 10, zeroAt + 10 + radix) : chars.slice(zeroAt, zeroAt + radix) 89 } else if (chars.length !== radix) throw invalidNumberOfCharacters(name, radix, chars.length) 90 91 const values = chars.map((c, i) => [ c, BigInt(i) ] as [ string, bigint ]) 92 93 return { 94 name, 95 system: 'standard', 96 radix: BigInt(radix), 97 chars: chars.join(''), 98 values: new Map(values), 99 reversedValues: new Map(values.map(([ c, v ]) => [ v, c ])), 100 low: 0, 101 high: radix - 1, 102 enabled: enabled ?? [ 2, 10, 12, 26 ].includes(radix), 103 } 104 } 105 106 function createBijectiveRadix(radix: number, chars = defaultCharsArray, enabled?: boolean, name = `bij-${radix}`, allChars = true): Radix { 107 if (allChars) { 108 const zeroAt = (chars.length - 1) / 2 109 chars = radix === 26 ? chars.slice(zeroAt, zeroAt + radix + 10).toSpliced(1, 9) : chars.slice(zeroAt, zeroAt + radix + 1) 110 } else if (chars.length !== radix + 1) throw invalidNumberOfCharacters(name, radix + 1, chars.length) 111 112 const values = chars.map((c, i) => [ c, BigInt(i) ] as [ string, bigint ]) 113 114 return { 115 name, 116 system: 'bijective', 117 radix: BigInt(radix), 118 chars: chars.join(''), 119 values: new Map(values), 120 reversedValues: new Map(values.map(([ c, v ]) => [ v, c ])), 121 low: 1, 122 high: radix, 123 enabled: enabled ?? [ 26 ].includes(radix), 124 } 125 } 126 127 function createBalancedRadix(radix: number, chars = defaultCharsArray, enabled?: boolean, name = `bal-${radix}`, allChars = true): Radix { 128 if ((radix & 1) === 0) throw new Error(`createRadix: Radix(balanced) must be odd: ${radix}`) 129 if (!allChars && chars.length !== radix) throw invalidNumberOfCharacters(name, radix, chars.length) 130 131 const zeroAt = (chars.length - 1) / 2 132 const half = (radix - 1) / 2 133 const zeroChar = chars[zeroAt] 134 chars = radix === 27 && allChars ? chars.slice(zeroAt + 10, zeroAt + 36).toSpliced(13, 0, zeroChar) : chars.slice(zeroAt - half, zeroAt + half + 1) 135 const values = chars.map((c, i) => [ c, BigInt(-half + i) ] as [ string, bigint ]) 136 137 return { 138 name, 139 system: 'balanced', 140 radix: BigInt(radix), 141 chars: chars.join(''), 142 values: new Map(values), 143 reversedValues: new Map(values.map(([ c, v ]) => [ v, c ])), 144 low: -half, 145 high: half, 146 enabled: enabled ?? [ 3, 13, 19, 27 ].includes(radix), 147 } 148 } 149 150 function createSumRadix(radix: number, chars = defaultCharsArray, enabled?: boolean, name = `sum-${radix}`, allChars = true): Radix { 151 if (allChars) { 152 chars = chars.slice((chars.length - 1) / 2).toSpliced(1, 9) 153 } 154 if (chars.length < radix) throw invalidNumberOfCharacters(name, radix, chars.length, true) 155 156 const r = radix - 1 157 let order = 1 158 const values = [ 159 [ chars[0], 0n ], 160 ...chars.slice(1).map((c, i) => { 161 if (i % r === 0 && i > 0) order *= r + 1 162 return [ c, BigInt(((i % r) + 1) * order) ] as [ string, bigint ] 163 }), 164 ] as [ string, bigint ][] 165 166 return { 167 name, 168 system: 'sum', 169 radix: BigInt(radix), 170 chars: chars.join(''), 171 values: new Map(values), 172 reversedValues: new Map(values.reverse().map(([ c, v ]) => [ v, c ])), 173 low: 1, 174 high: radix, 175 enabled: enabled ?? [ 2, 10 ].includes(radix), 176 } 177 } 178 179 function createBalsumRadix(radix: number, chars = defaultCharsArray, enabled?: boolean, name = `balsum-${radix}`, allChars = true): Radix { 180 let half = (chars.length - 1) / 2 181 if (allChars) { 182 chars = chars.slice(half + 10).toSpliced((half - 9) / 2, 0, chars[half]) 183 half = (chars.length - 1) / 2 184 } 185 if (chars.length < radix) throw invalidNumberOfCharacters(name, radix, chars.length, true) 186 187 const high = (radix - 1) / 2 188 let createValues: (order: number) => (c: string, i: number) => [string, bigint] 189 if (radix === 3) { 190 createValues = (order: number) => (c: string, i: number) => [ c, BigInt(3 ** i * order) ] 191 } else { 192 createValues = (order: number) => (c: string, i: number) => { 193 if (i > 0 && i % high === 0) order *= radix 194 return [ c, BigInt(((i % high) + 1) * order) ] 195 } 196 } 197 198 const plusValues = chars.slice(half + 1).map(createValues(1)) 199 const minusValues = chars.slice(0, half).toReversed().map(createValues(-1)) 200 const values = [ ...minusValues.toReversed(), [ chars[half], 0n ], ...plusValues ] as [ string, bigint ][] 201 202 return { 203 name, 204 system: 'balsum', 205 radix: BigInt(radix), 206 chars: chars.join(''), 207 values: new Map(values), 208 reversedValues: new Map(values.map(([ c, v ]) => [ v, c ])), 209 low: -high, 210 high, 211 enabled: enabled ?? [ 3, 27 ].includes(radix), 212 } 213 } 214 215 function createClockRadix(radix: number, chars = defaultCharsArray, enabled?: boolean, name = `clock-${radix}`, allChars = true): Radix { 216 if (radix & 1) throw new Error(`createRadix: Radix(clock) must be even: ${radix}`) 217 let zeroAt: number 218 if (allChars) { 219 zeroAt = (chars.length - 1) / 2 220 } else { 221 if (chars.length !== radix) throw invalidNumberOfCharacters(name, radix, chars.length) 222 zeroAt = chars.length / 2 - 1 223 } 224 225 const half = radix / 2 226 chars = chars.slice(zeroAt - half + 1, zeroAt + half + 1) 227 const values = chars.map((c, i) => [ c, BigInt(-half + 1 + i) ] as [string, bigint]) 228 229 return { 230 name, 231 system: 'clock', 232 radix: BigInt(radix), 233 chars: chars.join(''), 234 values: new Map(values), 235 reversedValues: new Map(values.map(([ c, v ]) => [ v, c ])), 236 low: -half + 1, 237 high: half, 238 enabled: enabled ?? [ 2, 4, 6, 8, 10, 12, 16, 18, 20, 22, 24, 30, 36 ].includes(radix), 239 } 240 } 241 242 // const num2strCache = new Map<Radix, Map<bigint, string>>() 243 export function num2str(num: bigint, radix: Radix, maxLength = 28): string { 244 if (num === 0n) return radix.reversedValues.get(0n)! 245 // const str = num2strCache.get(radix)?.get(num) 246 // if (str != undefined) { 247 // return str 248 // } 249 250 const sum = radix.system === 'sum' 251 const bal = radix.system === 'balanced' 252 const balsum = radix.system === 'balsum' 253 const clock = radix.system === 'clock' 254 255 const ret: string[] = [] 256 257 const neg = num < 0n 258 let n = neg ? -num : num 259 260 if (sum) { 261 let i: number 262 let v: string | undefined 263 const { reversedValues } = radix 264 for (const value of reversedValues.keys()) { 265 i = 0 266 if (value) { 267 while (n >= value) { 268 if ((v = reversedValues.get(value))) ret.push(v) 269 if (++i === maxLength) { 270 ret.unshift('…') 271 n %= value 272 } else { 273 n -= value 274 } 275 } 276 } 277 } 278 // } else if (balsum) { 279 // let i 280 // const max = 5 281 // let v: string | undefined 282 // const values = radix.reversedValues 283 // for (const value of neg ? values.keys() : Array(values.keys()).reverse()) { 284 // if (!value) continue 285 // i = 0 286 // while (neg ? num <= value : num >= value) { 287 // if (v = values.get(value)) ret.push(v) 288 // if (++i === max) { 289 // ret.unshift('…') 290 // num = neg ? -(num % value) : num % value 291 // } else { 292 // num = neg ? num + value : num - value 293 // } 294 // } 295 // } 296 } else { 297 const { radix: rad, system, reversedValues } = radix 298 299 const bij = system === 'bijective' 300 const high = BigInt(radix.high) 301 302 // let d: bigint 303 // let q: bigint 304 // let i = 1n 305 for (let d: bigint, q: bigint, i = 1n; n > 0n; i *= rad) { 306 d = n % rad 307 if (bij) { 308 q = d === 0n ? n / rad - 1n : n / rad 309 d = n - q * rad 310 n = q 311 } else { 312 if (bal || clock || balsum) { 313 if (d > high || clock && neg && d === high) { 314 d -= rad 315 n += high 316 } 317 if (neg) d = -d 318 } 319 n /= rad 320 } 321 if (balsum) { 322 if (d) ret.unshift(reversedValues.get(d * i)!) 323 } else { 324 ret.unshift(reversedValues.get(d)!) 325 } 326 } 327 } 328 329 if (neg && !(bal || clock || balsum)) ret.unshift('-') 330 331 const str = ret.join('') 332 // let rc = num2strCache.get(radix) 333 // if (!rc) { 334 // num2strCache.set(radix, rc = new Map()) 335 // } 336 // rc.set(num, str) 337 return str 338 } 339 340 export function str2num(str: string, radix: Radix): bigint { 341 if (str === radix.reversedValues.get(0n)) return 0n 342 343 const neg = str.startsWith('-') 344 const s = neg ? str.slice(1) : str 345 346 const { radix: rad, values } = radix 347 const sum = radix.system === 'sum' 348 const balsum = radix.system === 'balsum' 349 const ret = Iterator.from(s).reduce((acc, c) => { 350 if (c === '…') return acc 351 const v = values.get(c) 352 if (v == undefined) throw new Error(`Non-Base character encountered: "${c}". ${allowedCharaters(radix)}`) 353 return sum || balsum ? acc + v : acc * rad + v 354 }, 0n) 355 356 return neg ? -ret : ret 357 } 358 359 function invalidNumberOfCharacters(name: string, requiredLength: number, providedLength: number, atLeast = false) { 360 return new Error(`radix ${name} needs${atLeast ? ' at least' : ''} ${requiredLength} characarters, ${providedLength} provided`) 361 } 362 363 export function allowedCharaters(radix: Radix): string { 364 const chars = radix.system === 'bijective' ? radix.chars.slice(1) : radix.chars 365 return `Allowed characters are "${radix.system === 'balanced' ? chars : `-${radix.chars}`}".` 366 }