4
\$\begingroup\$

I was asked by a friend to write a simple page for selecting members to various tasks. The members are listed in a textfile, along with how much they are willing to contribute (Usually a number between 0 and 1). Bellow is an example of such a list:

name, mebershipdegree
Noen, 4
John Doe, 1
The rock, 0.5
Ally MacBeal, 0.121212021

Now my goal was to create a generator that picked one of the names from the list above relative to how much they wanted to contribute. So the leader should show up much more than the other names. My code is working as intended, the ratios seems to work properly. There are however a few things that bugs me

  • Is the JavaScript modern, succint and understandable?
  • I added a failsaife if the memberlist is not found, is the way to handle this error ok?
  • How could the JavaScript file be improved?
  • In firefox the site flashes when one reloads the page. This might be because the JavaScript fires before the CSS is loaded.
  • Is there a better method to do the fetch part? Now I have to do it twice to be able to reload the page using spacebar (not yet implemented in the live version).

Live version Hvem (Who). GitHub

HTML: index.html

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>Hvem</title>
    <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png">
    <link rel="manifest" href="./site.webmanifest">
    <link rel="mask-icon" href="./safari-pinned-tab.svg" color="#5bbad5">
    <meta name="msapplication-TileColor" content="#da532c">
    <meta name="theme-color" content="#ffffff">
    <link rel="stylesheet" href="index.css">
</head>

<body>
    <div class="center">
        <h1 id="Noen"></h1>
    </div>
    <script type="text/javascript" src="hvem.js"></script>
</body>

</html>

CSS: main.css

html,
body {
    margin: 0;
    height: 100%;
    overflow: hidden;
    overflow-y: hidden;
}

.center {
    height: 100%;
    position: relative;
}

.center h1 {
    text-align: center;
    font-size: 12vw;
    margin: 0;
    position: absolute;
    top: 50%;
    left: 50%;
    -ms-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
}

Javascript: hvem.js

document.addEventListener('keyup', function(e){
  if(e.keyCode == 32)
    // Reloads name on spacebar
    fetch("medlemsgrad.txt")
    .then(handleErrors)
    .then(response =>
          response.text()
          .then(text =>
                document.getElementById("Noen").innerHTML = hvem(text))
         )
    .catch(error => console.log(error));
});

document.addEventListener("DOMContentLoaded", function(event) {
    // Uses the built in fetch to read the textfile
    fetch("medlemsgrad.txt")
        .then(handleErrors)
        .then(response =>
            response.text()
            .then(text =>
                document.getElementById("Noen").innerHTML = hvem(text))
        )
        .catch(error => console.log(error));
});

function handleErrors(response) {
  //If medlemsgrad.text not found set hvem to "Noen" and throw an error
    if (!response.ok) {
        document.getElementById("Noen").innerHTML = "Noen";
        throw "medelmsgrad.txt is missing!";
    }
    return response;
}

function hvem(text) {
    let lines = text.split(/\r\n|\n/);
    let members = getMembers(lines);
    let accumulativeMembers = getAccumulutiveMembers(members);
    return getHvem(accumulativeMembers);
}

function getMembers(lines) {
  // members = [[John Doe, 0.13], [Jane Roe, 0,23]]
    let members = [];
    lines.forEach(line => { 
        let data = line.split(',');
        let membershipDegree = parseFloat(data[1]);
        // This is to avoid the headers (navn, medlemsgrad),
        // if membershipdegree is not a number skip
        if (!isNaN(membershipDegree)) {
            let name = data[0].trim();
            members.push([name, membershipDegree]);
        }
    });
    return members;
}

function getAccumulutiveMembers(members) {
    // The next function normalizes the membershipDegree to 1 and order the
    // members accumulatively. Example: Let
    //
    // [noen: 4, a: 1, b, 1]
    //
    // then the accumululative list looks like
    //
    // [noen: 4/6, a: 4/6 + 1/6, b: 4/6 + 1/6 + 1/6]
    //
    // [noen: 4/6, a: 5/6, b: 1]
    //
    // As it is sorted in ascending order
    let totalMembershipDegree = 0;
    members.forEach(member => {
        totalMembershipDegree += member[1]
    });

    let accumulative = 0;
    let accumulativeMemberlist = [];
    members.forEach(member => {
        name = member[0];
        membershipDegree = member[1];
        activity = parseFloat(membershipDegree) / totalMembershipDegree;
        accumulative += activity;
        accumulativeMemberlist.push([name, accumulative]);
    });

    // Sorts the accumulative list in ascending order (low to high)
    accumulativeMemberlist.sort((a, b) => {
        return a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0
    });

  // Sets the last member to 1, as the accumulative total should be 1
  // it is not one due to slight round off errors
    accumulativeMemberlist[accumulativeMemberlist.length - 1][1] = 1;

    return accumulativeMemberlist;
}

function getHvem(accumulativeMembers) {
    // Sets hvem as the default name. Tries 100 times to randomly pick someone
    // from the accumulatively membership list (including noen)
    // Example:
    //
    // [noen: 4/6, a: 5/6, b: 1]
    //
    // We then pick a random integer in the range [0, 1]
    // If this random number is less than or equal to 4/6 then Noen is choosen.
    // If the random number is between 4/6 and 5/6, a is choosen
    // If the random number is between 5/6 and 1, b is choosen.
    // This means that the chance of picking Noen is 4 times as great as b or a
    // which is what we wanted.
    console.log(accumulativeMembers);
    let randInt = Math.random();
    for (const member of accumulativeMembers) {
        var name = member[0];
        let number = member[1];

        if (randInt <= number) {
            return name;
        }
    };
    return name;
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Hey, long time no see! A̶l̶s̶o̶ ̶i̶s̶ ̶t̶h̶e̶ ̶'̶l̶i̶v̶e̶ ̶v̶e̶r̶s̶i̶o̶n̶'̶ ̶m̶e̶a̶n̶t̶ ̶t̶o̶ ̶s̶h̶o̶w̶ ̶m̶o̶r̶e̶ ̶t̶h̶a̶n̶ ̶j̶u̶s̶t̶ ̶"̶N̶o̶e̶n̶"̶?̶ I just realized that's the result. \$\endgroup\$
    – Peilonrayz
    Commented Feb 22, 2019 at 18:19
  • \$\begingroup\$ Thanks! In the live version updating the page with spacebar is not yet implemented. However, you can get another result by reloading the page manually (F5 or a similar hotkey). \$\endgroup\$ Commented Feb 22, 2019 at 18:28

1 Answer 1

1
\$\begingroup\$

Is the JavaScript modern, succint and understandable?

Provided you can use ES6, you could cut off a lot with, for example, arrow functions and argument destructuring.

How could the JavaScript file be improved?

I understood you are doing weighted random selection? If so, I did some searching and found another way which simplifies the code a lot; see below.

In firefox the site flashes when one reloads the page. This might be because the JavaScript fires before the CSS is loaded.

One often seen method is to put the javascript code (external or otherwise) just before the closing </body> tag, but for some reason it feels wrong to me. Another option would be to use the load event instead of DOMContentLoaded, but again, that feels wrong to me.

Is there a better method to do the fetch part? Now I have to do it twice to be able to reload the page using spacebar (not yet implemented in the live version).

You can give a name to the function body, and give a reference to it for the event listeners (i.e. fun = evt => {...}; el.addEventListener('whatever', fun)

So here’s what I put together from my findings and some of the above suggestions:

const asMembers = txt =>
  txt.split(/\r\n|\n/)
  .slice(1, -1) // drop the header line and the last (empty) split
  .map(line => {
    let [name, weight] = line.split(/, ?/)
    return [name.trim(), Number(weight)]
  }) // you could `.filter(([, n]) => !Number.isNaN(n))` to drop weightless

const randomBetween = (min, max) =>
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#Examples
  // NOTE: => [min, max)
  Math.random() * (max - min) + min

const weightedRandom = ary => {
  // https://medium.com/@peterkellyonline/weighted-random-selection-3ff222917eb6
  let randomWeight = randomBetween(1, ary.reduce((acc, [, n]) => acc + n, 0))
  for(let [name, weight] of ary) {
    randomWeight -= weight
    if(randomWeight <= 0)
      return name
  }
}

and then the calls would go something like fetch(...).then(asMembers).then(weightedRandom).

\$\endgroup\$
2
  • \$\begingroup\$ Interesting solution. How would this work when none of my weights are integers? \$\endgroup\$ Commented Feb 26, 2019 at 11:57
  • \$\begingroup\$ @N3buchadnezzar with a quick times = (n, retval = {}) => { let ppl = asMembers(fileContentsAsString); while(n-- > 0) { let cur = weightedRandom(ppl); retval[cur] = retval[cur] ? retval[cur] + 1 : 1 } return retval } and running times(10000), I get {Noen: 6437, 'John Doe': 2169, 'The rock': 1121, 'Ally MacBeal': 273}. \$\endgroup\$
    – morbusg
    Commented Feb 26, 2019 at 17:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.