2

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.)

And Finally
  • 5,602
  • 14
  • 70
  • 110
  • What about using a [Bag-of-words model](http://en.wikipedia.org/wiki/Bag-of-words_model) using a bag of letters instead of words for each name. Turn their name into a 26 element vector of letter counts and see which name their name is closest to. – Felix Castor Mar 19 '14 at 15:12

4 Answers4

1

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;
}
fzzle
  • 1,466
  • 5
  • 23
  • 28
  • since array runs from 0 to 19 why the +1? – jing3142 Mar 19 '14 at 15:21
  • 1
    Thanks fzzle, nice solution! I'm already converting the letters using charCodeAt. Just using modulus instead of sumDigits(sum) does seem to produce a better spread of results. – And Finally Mar 19 '14 at 15:41
  • Are there potential overflow problems here? just wondering if really long names would sum up to be too big before modulusing them. Solvable by modulusing each letter before adding it which will make no difference to the result. – Chris Mar 19 '14 at 15:51
  • 1
    @Chris I've just tested it with a string with the length of 98234. There were no problems.. And btw. I don't think any name length is anywhere that long. – fzzle Mar 19 '14 at 15:53
  • 1
    @fzzle: Yeah, I don't think I did any realism testing in my head here. I was just thinking about summing and modulus problems. And no names that long? Chris is just a short form of my name, my real name is.... ;-) – Chris Mar 19 '14 at 16:01
1

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).

barak manos
  • 29,648
  • 10
  • 62
  • 114
  • Thanks barak, fzzle got in with the modulus answer first, but your pyramid of numbers is so beautiful I've given yours an upvote! – And Finally Mar 19 '14 at 15:43
0

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

serdar
  • 1,564
  • 1
  • 20
  • 30
0

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. :(

SparoHawk
  • 557
  • 2
  • 10