1

I'm generating a number based on a fixed character set.

function generator()
{
     var text = "";
     var char_list = "LEDGJR", number_list = "0123456789";
     for(var i=0; i < 2; i++ )
     {  
          text += char_list.charAt(Math.floor(Math.random() * char_list.length));
     }

     for(var j=0; j < 2; j++ )
     {  
          text += number_list.charAt(Math.floor(Math.random() *             
                              number_list.length));
     }

     return text;
}

Result : RE39, JE12 etc...

Once all the permutation related to the above sequence is done, then the generator should generate string as RE391, JE125 means adding one more number to the complete number.

How can I get the permutation count of sequence?

The JOKER
  • 453
  • 8
  • 21
  • If you want 3 numbers instead of 2, can't you change `j < 2` to `j < 3`? – Ivar Sep 30 '20 at 11:29
  • Yes i can, but its not just about changing that. I would change j<2 to j<3 only when all the permutations are utilised. – The JOKER Sep 30 '20 at 11:30
  • So the return value should be random but unique, used return value should not be return again, is that correct? – hackape Sep 30 '20 at 11:32
  • should be random and unique is right, but should be from the char_list and number_list defined. – The JOKER Sep 30 '20 at 11:34
  • 1
    That’s kinda challenging…lemme think about it. – hackape Sep 30 '20 at 11:35
  • Is `EE00` a valid result? Telling from your code it is, but I just wanna make sure. – hackape Sep 30 '20 at 11:39
  • @hackape yes it is! – The JOKER Sep 30 '20 at 11:41
  • What’s the usage context of this generator thing? Can help better understand the problem. Care to share the background why would you want such a generator? – hackape Sep 30 '20 at 11:41
  • this generated string will be saved on a .save db operation with a certain data. There will be different departments, so each department will have their unique set of char_list for identification of certain things about saved data. – The JOKER Sep 30 '20 at 11:43
  • char_list like this "LEDGJR", "INVO", "DEBT" so the expected string for INVO would be VO48 – The JOKER Sep 30 '20 at 11:45
  • It sounds like a URL shortener to me. Might wanna check this post https://stackoverflow.com/questions/742013/how-do-i-create-a-url-shortener – hackape Sep 30 '20 at 11:50
  • As a code challenge this is valid probelm. But as a practical one I’m not sure why the random part is an requirement? Why require uniqueness AND randomness? Doesn’t uniqueness alone solve the problem? – hackape Sep 30 '20 at 11:54
  • The unique string has to be generated using the charList and number list, For example char_list = "LEDGJR", random behavior would give more permutations. – The JOKER Sep 30 '20 at 11:57
  • 1
    OK i have something on my mind. Will type it down once get home, I’m on my phone now it’s tedious to type. – hackape Sep 30 '20 at 12:06
  • how does this code know what's already in the database? sounds like two separate problems to me... first part is the code you've included, i.e. generating valid identifiers, the second part is rejecting duplicates that already exist in the database. arranging these two suitably seems very domain specific (e.g. start with two digits, then repeatedly try ~50 random identifiers before adding another digit) – Sam Mason Sep 30 '20 at 12:18
  • for checking if already exists in DB, I will either separately check the DB before saving, or can set the field to be unique in DB which will throw error and then will handle. No matter what algorithm/library I use, there will always be the necessity to check DB of the string don't already exists. – The JOKER Sep 30 '20 at 12:22
  • OK finally done. Check my full answer, quite a pleasant code challenge. This implementation ensure both uniqueness and pseudo-randomness. And it's very space efficient. – hackape Sep 30 '20 at 15:36
  • For demo purpose I maintain state in closure. But if this is a web service, you only need to persist `firstIndex`, `currentIndex` and `currentRowOfNums` in order to restore the whole internal state when service reboot. – hackape Sep 30 '20 at 15:39
  • "random behavior would give more permutations" no it will not... all possible permutations are determined once `char_list` and `number_list` and length of sequence are decided. Randomness can not increase nor decrease that number. – hackape Sep 30 '20 at 16:16

1 Answers1

1

For simplicity consider the case where:

chars = "AB"
nums = "123";

and we want to generate a 4-digit sequence of two chars and two numbers.

We define these variables

rows = [chars, chars, nums, nums]
rowSizes = rows.map(row => row.length) // [2, 2, 3, 3]

It’s easy to see the set size of all possible permuations equals:

spaceSize = rowSizes.reduce((m, n) => m * n, 1) // 2*2*3*3 = 36

And we define two set of utility functions, usage of which I'll explain in detail later.

  1. decodeIndex() which gives us uniqueness
function euclideanDivision(a, b) {
  const remainder = a % b;
  const quotient =  (a - remainder) / b;
  return [quotient, remainder]
}

function decodeIndex(index, rowSizes) {
  const rowIndexes = []
  let dividend = index
  for (let i = 0; i < rowSizes.length; i++) {
    const [quotient, remainder] = euclideanDivision(dividend, rowSizes[i])
    rowIndexes[i] = remainder
    dividend = quotient
  }
  return rowIndexes
}
  1. getNextIndex() which gives us pseudo-randomness
function isPrime(n) {
  if (n <= 1) return false;
  if (n <= 3) return true;

  if (n % 2 == 0 || n % 3 == 0) return false;

  for (let i = 5; i * i <= n; i = i + 6) {
    if (n % i == 0 || n % (i + 2) == 0) return false;
  }
  return true;
}

function findNextPrime(n) {
  if (n <= 1) return 2;
  let prime = n;

  while (true) {
    prime++;
    if (isPrime(prime)) return prime;
  }
}

function getIndexGeneratorParams(spaceSize) {
  const N = spaceSize;
  const Q = findNextPrime(Math.floor(2 * N / (1 + Math.sqrt(5))))
  const firstIndex = Math.floor(Math.random() * spaceSize);
  
  return [firstIndex, N, Q]
}

function getNextIndex(prevIndex, N, Q) {
  return (prevIndex + Q) % N
}

Uniqueness

Like mentioned above, spaceSize is the number of all possible permutations, thus each index in range(0, spaceSize) uniquely maps to one permutation. decodeIndex helps with this mapping, you can get the corresponding permutation to an index by:

function getSequenceAtIndex(index) {
  const tuple = decodeIndex(index, rowSizes)
  return rows.map((row, i) => row[tuple[i]]).join('')
}

Pseudo-Randomness

(Credit to this question. I just port that code into JS.)

We get pseudo-randomness by polling a "full cycle iterator". The idea is simple:

  1. have the indexes 0..35 layout in a circle, denote upperbound as N=36
  2. decide a step size, denoted as Q (Q=23 in this case) given by this formula
    Q = findNextPrime(Math.floor(2 * N / (1 + Math.sqrt(5))))
  3. randomly decide a starting point, e.g. number 5
  4. start generating seemingly random nextIndex from prevIndex, by
    nextIndex = (prevIndex + Q) % N

So if we put 5 in we get (5 + 23) % 36 == 28. Put 28 in we get (28 + 23) % 36 == 15.

This process will go through every number in circle (jump back and forth among points on the circle), it will pick each number only once, without repeating. When we get back to our starting point 5, we know we've reach the end.

†: I'm not sure about this term, just quoting from this answer
‡: This formula only gives a nice step size that will make things look more "random", the only requirement for Q is it must be coprime to N

Full Solution

Now let's put all the pieces together. Run the snippet to see result.

I've also includes the a counter before each log. For your case with char_list="LEDGJR", number_list="0123456789", the spaceSize for 4-digit sequence should be 6*6*10*10 = 3600

You'll observe the log bump to 5-digit sequence at 3601

function euclideanDivision(a, b) {
  const remainder = a % b;
  const quotient = (a - remainder) / b;
  return [quotient, remainder];
}

function decodeIndex(index, rowSizes) {
  const rowIndexes = [];
  let divident = index;
  for (let i = 0; i < rowSizes.length; i++) {
    const [quotient, remainder] = euclideanDivision(divident, rowSizes[i]);
    rowIndexes[i] = remainder;
    divident = quotient;
  }
  return rowIndexes;
}

function isPrime(n) {
  if (n <= 1) return false;
  if (n <= 3) return true;

  if (n % 2 == 0 || n % 3 == 0) return false;

  for (let i = 5; i * i <= n; i = i + 6) {
    if (n % i == 0 || n % (i + 2) == 0) return false;
  }
  return true;
}

function findNextPrime(n) {
  if (n <= 1) return 2;
  let prime = n;

  while (true) {
    prime++;
    if (isPrime(prime)) return prime;
  }
}

function getIndexGeneratorParams(spaceSize) {
  const N = spaceSize;
  const Q = findNextPrime(Math.floor((2 * N) / (1 + Math.sqrt(5))));
  const firstIndex = Math.floor(Math.random() * spaceSize);

  return [firstIndex, N, Q];
}

function getNextIndex(prevIndex, N, Q) {
  return (prevIndex + Q) % N;
}

function generatorFactory(rows) {
  const rowSizes = rows.map((row) => row.length);

  function getSequenceAtIndex(index) {
    const tuple = decodeIndex(index, rowSizes);
    return rows.map((row, i) => row[tuple[i]]).join("");
  }

  const spaceSize = rowSizes.reduce((m, n) => m * n, 1);

  const [firstIndex, N, Q] = getIndexGeneratorParams(spaceSize);

  let currentIndex = firstIndex;
  let exhausted = false;

  function generator() {
    if (exhausted) return null;
    const sequence = getSequenceAtIndex(currentIndex);
    currentIndex = getNextIndex(currentIndex, N, Q);
    if (currentIndex === firstIndex) exhausted = true;
    return sequence;
  }

  return generator;
}

function getRows(chars, nums, rowsOfChars, rowsOfNums) {
  const rows = [];
  while (rowsOfChars--) {
    rows.push(chars);
  }
  while (rowsOfNums--) {
    rows.push(nums);
  }
  return rows;
}

function autoRenewGeneratorFactory(chars, nums, initRowsOfChars, initRowsOfNums) {
  let realGenerator;
  let currentRowOfNums = initRowsOfNums;

  function createRealGenerator() {
    const rows = getRows(chars, nums, initRowsOfChars, currentRowOfNums);
    const generator = generatorFactory(rows);
    currentRowOfNums++;
    return generator;
  }

  realGenerator = createRealGenerator();

  function proxyGenerator() {
    const sequence = realGenerator();
    if (sequence === null) {
      realGenerator = createRealGenerator();
      return realGenerator();
    } else {
      return sequence;
    }
  }

  return proxyGenerator;
}

function main() {
  const char_list = "LEDGJR"
  const number_list = "0123456789";
  const generator = autoRenewGeneratorFactory(char_list, number_list, 2, 2);
  let couter = 0
  setInterval(() => {
    console.log(++couter, generator())
  }, 10);
}

main();
hackape
  • 18,643
  • 2
  • 29
  • 57
  • For demo purpose I maintain state in closure. But if this is a web service, you only need to persist `firstIndex`, `currentIndex` and `currentRowOfNums` in order to restore the whole internal state when service reboot. – hackape Sep 30 '20 at 16:43
  • The algorithm itself ensures uniqueness of ID, if this generator is the globally single source of ID generation, then checking against DB for collision is not necessary. – hackape Sep 30 '20 at 16:44