I'm not a PHP-guy but hopefully a JavaScript code with no advanced magic could be our common ground. So first here is some code:
const defaultChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
function indexToPermutation(index, targetLength, chars) {
if (arguments.length < 3)
chars = defaultChars;
const charsLen = chars.length;
let combinationChars = [];
for (let i = 0; i < targetLength; i++) {
let ch = chars[index % charsLen];
combinationChars.push(ch);
index = Math.floor(index / charsLen); //integer division
}
return combinationChars.reverse().join("")
}
function permutationToIndex(combination, chars) {
if (arguments.length < 2)
chars = defaultChars;
const charsLen = chars.length;
let index = 0;
let combinationChars = combination.split("");
for (let i = 0; i < combination.length; i++) {
let digit = chars.indexOf(combinationChars[i]);
index = index * charsLen + digit;
}
return index;
}
function indexToCombination(index, targetLength, chars) {
if (arguments.length < 3)
chars = defaultChars;
let base = chars.length - targetLength + 1;
let combinationIndices = [];
for (let i = 0; i < targetLength; i++) {
let digit = index % base;
combinationIndices.push(digit);
index = Math.floor(index / base); //integer division
base++;
}
let combinationChars = [];
for (let i = targetLength - 1; i >= 0; i--) {
let ch = chars[combinationIndices[i]];
combinationChars.push(ch);
// here it is important that chars is only local copy rather than global variable
chars = chars.slice(0, combinationIndices[i]) + chars.slice(combinationIndices[i] + 1); // effectively chars.removeAt(combinationIndices[i])
}
return combinationChars.join("")
}
function combinationToIndex(combination, chars) {
if (arguments.length < 2)
chars = defaultChars;
const charLength = chars.length; // full length before removing!
let combinationChars = combination.split("");
let digits = [];
for (let i = 0; i < combination.length; i++) {
let digit = chars.indexOf(combinationChars[i]);
// here it is important that chars is only local copy rather than global variable
chars = chars.slice(0, digit) + chars.slice(digit + 1); // effectively chars.removeAt(digit)
digits.push(digit);
}
let index = 0;
let base = charLength;
for (let i = 0; i < combination.length; i++) {
index = index * base + digits[i];
base--;
}
return index;
}
and here are some results
indexToPermutation(0, 6) => "000000"
indexToPermutation(1, 6) => "000001"
indexToPermutation(123, 6) => "00003F"
permutationToIndex("00003F") => 123
permutationToIndex("000123") => 1371
indexToCombination(0,6) => "012345"
indexToCombination(1,6) => "012346"
indexToCombination(123,6) => "01237Z"
combinationToIndex("01237Z") => 123
combinationToIndex("543210") => 199331519
Obviously if order of digits and letters is different for you, you can change it by either passing explicitly chars
(last argument) or changing defaultChars
.
Some math behind it
For permutations you may see a string as a number written in a 36
-base numeral system (36
= 10 digits + 26 letters) similar to how hexadecimal works. So converting index <=> permutation is actually a simple job of converting to and from that numeral system.
For combinations idea is similar but the numeral system is what I call a "variable-base" numeral system with base changing from 36
to 36-n+1
(where n
is the target length of combination). An example of a more familiar "variable-base" numeral system is clock. If you want to convert milliseconds into time, you first divide by 1000 milliseconds, then by 60 seconds, then by 60 minutes, and then by 24 hours. The only really trick part here is that which "digits" are allowed for each position depends on which digits are already used by the previous positions (size of the digits set is always the same nevertheless). This tricky part is the reason chars
argument is modified to remove one char after each iteration in both combinationToIndex
and indexToCombination
.