/ src / radixes.ts
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  }