Convert an alphabetic string into digits and recursively sum them so they're below a certain max

StackOverflow https://stackoverflow.com/questions/22509726

سؤال

Here's an arithmetical puzzler for you StackOverflowers. I'm working on a JS game that'll allow someone to enter their name and will generate another name based on what they enter. Sort of like this. Basically every time someone enters a particular string, I want to generate the same other string by taking items from a list.

So I have an array of words, numbering 20 in this example

"nouns": [
    "Nitwit",
    "Cretin",
    "Village Idiot"
    // ..
]

When the user inputs their name, I convert each ASCII alphabetic character to a digit. I'm going to add up all the resulting digits and use the total to select one of the words from my array.

// Convert alpha chars to numerical equivalents
function str2num (mystr) {

    mystr = mystr.toUpperCase();
    var conv = [],
        l = mystr.length,
        regex = /^[A-Za-z]+$/;

    for (var i = 0; i < l; i++) {
        if (regex.test(mystr.charAt(i))) {
            conv.push(mystr.charCodeAt(i) - 64);
        }
    }

    var c = conv.length,
        sum = 0;
    for (var j = 0; j < c; j++) {
        sum += conv[j];
    }
    return sumDigits(sum);

}

Since there are only 20 elements in the word array, I always want the sum to be less than 20. So if it's equal or greater than 20, I want to add up its digits again. This is how I'm currently doing it.

// Recursively add digits of number together
// till the total is less than length of word list
function sumDigits (number) {

    var listLength = adjectives.length;
    var sum = number % listLength;

    if (number > listLength) {
        var remainder = Math.floor(number / 10);
        sum += sumDigits(remainder);
    }

    if (sum > listLength) {
        sum = sumDigits(sum);
    }

    return sum;
}

And when I've got a result below 20, I return the value of nouns[sum] to the user. This code pretty much works - the resulting sum is always below the maximum allowed. But the result isn't very random - it seems that I'm getting a disproportionate number of sums in the lower end of the 0 to 20 range. I don't want users to keep seeing words from the beginning of my list. Is there any change I can make to sumDigits that will ensure an even spread of results? Or is this already correct? (I've done this JSFiddle to demo what I'm talking about.)

هل كانت مفيدة؟

المحلول

I would make it dependent on the charcode of the characters in the name:

function getValue(name) {
    var letters = name.toLowerCase().split(''), 
        value = 0,
        i = 0;
    for(; i < letters.length; i ++) {
        value += letters[i].charCodeAt(0);
    }
    return value % 20;
}

نصائح أخرى

The output sum in your current implementation is indeed not uniformly distributed.

As an example, consider all the numbers with one or two digits (1 - 99):

1 of them is  summed up to  1 ( 1)
2 of them are summed up to  2 ( 2, 20)
3 of them are summed up to  3 ( 3, 30, 21)
4 of them are summed up to  4 ( 4, 40, 31, 22)
5 of them are summed up to  5 ( 5, 50, 41, 32, 23)
6 of them are summed up to  6 ( 6, 60, 51, 42, 33, 24)
7 of them are summed up to  7 ( 7, 70, 61, 52, 43, 34, 25)
8 of them are summed up to  8 ( 8, 80, 71, 62, 53, 44, 35, 26)
9 of them are summed up to  9 ( 9, 90, 81, 72, 63, 54, 45, 36, 27)
9 of them are summed up to 10 (10, 91, 82, 73, 64, 55, 46, 37, 28)
9 of them are summed up to 11 (11, 92, 83, 74, 65, 56, 47, 38, 29)
8 of them are summed up to 12 (12, 93, 84, 75, 66, 57, 48, 39)
7 of them are summed up to 13 (13, 94, 85, 76, 67, 58, 49)
6 of them are summed up to 14 (14, 95, 86, 77, 68, 59)
5 of them are summed up to 15 (15, 96, 87, 78, 69)
4 of them are summed up to 16 (16, 97, 88, 79)
3 of them are summed up to 17 (17, 98, 89)
2 of them are summed up to 18 (18, 99)
1 of them is  summed up to 19 (19)

This is probably closer to Normal Distribution than to Uniform Distribution.

In order to achieve the latter, simply return sum % c instead of sumDigits(sum).

I think sumDigits function returns a biased number. Do not return sumDigits(sum). Instead you can return sum % 20.

fzzle's answer is a good one.

Alternatively, you could do a sum reduction with a while:

var reduced = sum;
while(reduced > 20){
    reduced -= name.length;
}

This is a more complete example:

var sum = 0;
var name = 'John Smith'.split('');

for(var k in name){
    sum += name[k].charCodeAt(0);
}

var key = sum;
while(key > 20){
    key -= 'John Smith'.length;
}

If you test it you'll see it produced a different results to sumDigits % 20. Mind you, I don't know how this method will behave with unusually long names. Let me test it.

Confirmed. XD I tried with John Smith John Smith John Smith John Smithxx and broke it. Don't think this qualifies as an answer now. :(

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top