Stücke
Stücke

Reputation: 993

Timing of nested loops output

I just started with JavaScript and I am trying to write a small open source application for fingerboard training. I nested some time loops to account for the intervals between hangtime and breaks:

<html>

<body>
  Sets: <input type="number" id="setsIN" value="5">
  <br>
  Rounds: <input type="number" id="roundsIN" value="6">
  <br>
  Workout: <input type="number" id="hangIN" value="7">
  <br>
  Short Break: <input type="number" id="shortbreakIN" value="3">
  <br>
  Long Break: <input type="number" id="longbreakIN" value="180">
  <br>

  <hr>

  <script>
    // Import values
    var setsNUMBER = parseInt(document.getElementById("setsIN").value);
    var roundsNUMBER = parseInt(document.getElementById("roundsIN").value);
    var hangTIME = parseInt(document.getElementById("hangIN").value);
    var shortbreakTIME = parseInt(document.getElementById("shortbreakIN").value);
    var longbreakTIME = parseInt(document.getElementById("longbreakIN").value);
    console.log("Sets: " + setsNUMBER)
    console.log("Rounds: " + roundsNUMBER)
    console.log("Hang: " + hangTIME)
    console.log("Short breaks: " + shortbreakTIME)
    console.log("Long breaks: " + longbreakTIME)
    // calculate duration
    var duration = ((hangTIME + shortbreakTIME) * roundsNUMBER + longbreakTIME) * setsNUMBER
    console.log("Duration (minutes): " + duration/60)
    // Counter
    var setsCOUNT = 1; // Start counting at 1
    var roundsCOUNT = 1; // Start counting at 1
    var hangCOUNT = 1; // Start counting at 1
    var shortbreakCOUNT = 1; // Start counting at 1
    var longbreakCOUNT = 1; // Start counting at 1
/////////////////////////////////////////////////////////////////

  // Sets
  while (setsCOUNT < setsNUMBER+1) {
    console.log("Set: "+ setsCOUNT)
    setsCOUNT++;
    roundsCOUNT = 1;
    longbreakCOUNT = 1;
    // Rounds
      while (roundsCOUNT  < roundsNUMBER+1) {
        console.log("Round: "+ roundsCOUNT)
        roundsCOUNT++;
        hangCOUNT = 1;
        shortbreakCOUNT = 1;
        // HAngtime
          while (hangCOUNT  < hangTIME+1) {
            console.log("WorkOutTime: "+ hangCOUNT)
            hangCOUNT++;
          }
        // Pausetime
         while (shortbreakCOUNT  < shortbreakTIME+1) {
           console.log("ShortBreak: "+ shortbreakCOUNT)
           shortbreakCOUNT++;
         }
          }
          // LongBreak
           while (longbreakCOUNT  < longbreakTIME+1) {
             //console.log("longBreak: "+ longbreakCOUNT)
             longbreakCOUNT++;
      }
  }

  </script>

</html>

The sequence of the training is as follows: - 7 seconds workout - 3 seconds break Repeat the above six times (=60 seconds) Rest for 180 seconds Repeat all steps above five times (=5*4 minutes)

It seems like I got the sequence of the output right. The console.log() is returned in the right order. Currently however, whenever I run the script all log lines are returned immediately right after I load the page. How can I print one line every second? I experimentd with setTimeout() but couldn't get it running.

Upvotes: 1

Views: 120

Answers (1)

The Witness
The Witness

Reputation: 920

Loops should not be used to organize time. Loop is a series of operations happening in no time, one after another.

What I would do is to use some time function: either setTimeout* or requestAnimationFrame, albeit in the latter you will have to track time in each frame yourself. Doable and probably more reliable* but for the sake of example I used setTimeout. It’s proof of concept, anyway.

What you want to do is:

  • Start set, and call Start round or Finish
  • Start round, and call Start Workout or Start the next set
  • Repeat 6 times:
    • Work out, and 7 seconds later call Short break or Long break
    • Take a short break, and 3 seconds later call Work out
  • Take a long break, and 180 seconds later call Start set

This is series of steps that are called in x amount of times (except for new set and new round, which are just data updates) with certain pauses in between.

Code

To not come empty handed, this is working proof of concept:

<html>
<body>
  Sets: <input type="number" id="setsIN" value="5">
  <br>
  Rounds: <input type="number" id="roundsIN" value="6">
  <br>
  Workout: <input type="number" id="hangIN" value="7">
  <br>
  Short Break: <input type="number" id="shortbreakIN" value="3">
  <br>
  Long Break: <input type="number" id="longbreakIN" value="180">
  <br>

  <hr>

  <script>
    const setsNUMBER = 'setsNUMBER';
    const roundsNUMBER = 'roundsNUMBER';
    const hangNUMBER = 'hangNUMBER';
    const hangTIME = 'hangTIME';
    const shortbreakTIME = 'shortbreakTIME';
    const longbreakTIME = 'longbreakTIME';

    const setsCOUNT = 'setsCOUNT';
    const roundsCOUNT = 'roundsCOUNT';
    const hangCOUNT = 'hangCOUNT';
    const shortbreakCOUNT = 'shortbreakCOUNT';
    const longbreakCOUNT = 'longbreakCOUNT';

    function training({ config, data, next: current }) {
        switch (current) {
            case setsCOUNT: {
                if (data[setsCOUNT] < config[setsNUMBER]) {
                    const updatedSetsCOUNT = data[setsCOUNT] + 1;
                    console.log(`Set: ${updatedSetsCOUNT}`);

                    training({
                        config,
                        data: {
                            ...data,
                            [setsCOUNT]: updatedSetsCOUNT,
                            [roundsCOUNT]: 0,
                        },
                        next: roundsCOUNT,
                    });
                } else {
                    console.log('The end. It was a good workout, bro!');
                }
                break;
            }

            case roundsCOUNT: {
                if (data.roundsCOUNT < config.roundsNUMBER) {
                    const updatedRoundsCOUNT = data.roundsCOUNT + 1;
                    console.log(`Round: ${updatedRoundsCOUNT}`);

                    training({
                        config,
                        data: {
                            ...data,
                            [roundsCOUNT]: updatedRoundsCOUNT,
                            [hangCOUNT]: 0,
                            [shortbreakCOUNT]: 0,
                        },
                        next: hangCOUNT,
                    });
                } else {
                    console.log('New set');

                    training({
                        config,
                        data: {
                            ...data,
                            roundsCOUNT: 0,
                        },
                        next: setsCOUNT,
                    });
                }
                break;
            }

            case hangCOUNT: {
                if (data[hangCOUNT] < config[hangNUMBER]) {
                    const updatedHangCOUNT = data[hangCOUNT] + 1;
                    console.log(`WorkOutTime: ${updatedHangCOUNT}`);

                    setTimeout(training, config[hangTIME] * 1000, {
                        config,
                        data: {
                            ...data,
                            [hangCOUNT]: updatedHangCOUNT,
                        },
                        next: shortbreakTIME,
                    });
                } else {
                    training({
                        config,
                        data,
                        next: longbreakCOUNT,
                    });
                }
                break;
            }

            case shortbreakTIME: {
                const updatedShortBreakCOUNT = data[shortbreakCOUNT] + 1;
                console.log(`Short break: ${updatedShortBreakCOUNT}`);

                setTimeout(training, config[shortbreakTIME] * 1000, {
                    config,
                    data: {
                        ...data,
                        [shortbreakCOUNT]: updatedShortBreakCOUNT,
                    },
                    next: hangCOUNT,
                });
                break;
            }

            case longbreakCOUNT: {
                // this update is probably obsolete as setsCOUNT stage is keeping track
                const updatedLongbreakCOUNT = data[longbreakCOUNT] + 1;
                console.log(`LongBreak: ${updatedLongbreakCOUNT}`);

                setTimeout(training, config[longbreakTIME] * 1000, {
                    config,
                    data: {
                        ...data,
                        [longbreakCOUNT]: updatedLongbreakCOUNT,
                    },
                    next: roundsCOUNT,
                });
                break;
            }
        }
    }

    const config = {
        [setsNUMBER]: parseInt(document.getElementById("setsIN").value),
        [roundsNUMBER]: parseInt(document.getElementById("roundsIN").value),
        [hangNUMBER]: 6,
        [hangTIME]: parseInt(document.getElementById("hangIN").value),
        [shortbreakTIME]: parseInt(document.getElementById("shortbreakIN").value),
        [longbreakTIME]: parseInt(document.getElementById("longbreakIN").value),
    };

    console.log("Sets: " + config.setsNUMBER);
    console.log("Rounds: " + config.roundsNUMBER);
    console.log("Workout time: " + config.hangTIME);
    console.log("Short break time: " + config.shortbreakTIME);
    console.log("Long break time: " + config.longbreakTIME);
    console.log("Duration (minutes): " + (
        (
            (
                (config.hangTIME + config.shortbreakTIME)
                * config.roundsNUMBER
                + config.longbreakTIME
            )
            * config.setsNUMBER
        )
        / 60)
    );

    const data = {
        [setsCOUNT]: 0,
        [roundsCOUNT]: 0,
        [hangCOUNT]: 0,
        [shortbreakCOUNT]: 0,
        [longbreakCOUNT]: 0,
    };

    training({ config, data, next: setsCOUNT });
  </script>
</body>
</html>

General concept is function training that takes arbitrary object with config, data, and next step, and calls itself again with new values until condition is met (in this case number of sets finished).

I recommend against using it as delivered.

  1. I would split it into small functions.
  2. If after short break comes long break then probably such short break can be skipped.
  3. Function above sometimes mutates object. This is unlikely to cause problems but maybe creating new objects when passing would be safer. Up to you.

[Edit] Working demo: https://jsfiddle.net/hmcwf0p5/1/

[Edit] Code 2

Then, I was asked if the code above can display seconds as they go. (This is a very good example why specs should be clear from the beginning; otherwise, it can result in very different implementations.)

In short, no.

In long, we could but due to nature of setTimeout this could not be accurate. I don’t even wanna go. Hence, I started thinking on different approach and came up with timeline.

Code: timeline

Idea: run timed function that does something in given interval. In our case this will be 1 second. Then, depending on our data, we should do something.

const prepareTimeline = (possibleTimeline) => {
    const sortedFrames = Object.keys(possibleTimeline)
        .map(Number)
        .filter((number) => !Number.isNaN(number))
        .sort((a, b) => a - b);

    return Object.assign(
        sortedFrames.reduce(
            (acc, number) => Object.assign(acc, { [number]: possibleTimeline[number] }),
            {}
        ),
        { last: Math.max(...sortedFrames) + 1 }
    );
}

const handleFrame = (data) => {
    const { second, frames, frames: { last }, timelineId } = data;

    if (second == last) {
        return clearInterval(timelineId);
    }

    if (frames[second]) {
        frames[second].forEach((message) => {
            console.log(message);
        });
    }

    data.second = second + 1;
};

const runTimeline = (frames) => {
    const timelineObject = {
        second: 0,
        frames: prepareTimeline(frames),
        timelineId: null,
    }

    const timelineId = setInterval(handleFrame, 1000, timelineObject);
    timelineObject.timelineId = timelineId;
}

What happens:

  1. runTimeline takes an object with frames. The key is second at which something’s gonna happen and the value is array with messages to be displayed.
  2. Inside prepareTimeline function removes all the non-numeric keys (passed as frames property) and adds last which is the biggest value plus 1 second, so we can kill setInterval.
  3. handleFrame is a function called each second. As param it takes object with timelineId (which we can pass to clearInterval), frames, second. Inside this function we mutate this object.
    • Here you can decide what you wanna do with your frame. Instead of console.logging it, you can setState of your app or whatever you find fit.
    • Also, in my example it’s object of arrays but it’s up to handleFrame what is accepted.

Notation setInterval(functionName, time, ...params) will call functionName(...params) every second.

Now, let’s run it:

runTimeline({
    1: ['Initial message'],
    5: ['Second message', 'And last'],
});

In console log, after 1 second first shows message

Initial message

Then 4 seconds later (at the same time):

Second message
And last

Workout timeline

So what’s left is to build workout frames. buildWorkoutTimeline takes params and runs a lot of nested loops (which was somewhat initial implementation). Probably could be written differently but I’ll leave it to you.

const buildWorkoutTimeline = ({
    sets = 3,
    rounds = 5,
    workouts = 6,
    workoutTime = 7,
    pauseTime = 3,
    restTime = 180,
} = {}) => {
    let seconds = 0;
    const workoutTimeline = { [seconds]: [] };

    for (let set = 1; set <= sets; set++) {
        workoutTimeline[seconds].push(`New set: ${set}`);
        for (let round = 1; round <= rounds; round++) {
            workoutTimeline[seconds].push(`Set ${set}’s new round: ${round}`);
            for (let workout = 1; workout <= workouts; workout++) {
                seconds += 1;
                workoutTimeline[seconds] = [`Set ${set}, round ${round}’s new workout: ${workout}`];
                for (let time = 0; time < workoutTime; time++) {
                    seconds += 1;
                    workoutTimeline[seconds] = [`Seconds: ${workoutTime - time}`];
                }

                if (workout < workouts) {
                    seconds += 1;
                    workoutTimeline[seconds] = ['Short break'];
                    for (let time = 0; time < pauseTime; time++) {
                        seconds += 1;
                        workoutTimeline[seconds] = [`Seconds: ${pauseTime - time}`];
                    }
                } else if (round < rounds) {
                    seconds += 1;
                    workoutTimeline[seconds] = ['Long break'];
                    for (let time = 0; time < restTime; time++) {
                        seconds += 1;
                        workoutTimeline[seconds] = [`Seconds: ${restTime - time}`];
                    }
                }
            }
        }
    }

    seconds += 1;
    workoutTimeline[seconds] = [
        'Workout finished',
        `Total time: ${(seconds / 60) | 0} minutes, ${seconds % 60} seconds`
    ];

    return workoutTimeline;
};

runTimeline(buildWorkoutTimeline());

Ta-dam.


* setTimeout(someFunction, 1000) will not execute 1000 milliseconds from the moment it was called but not earlier than 1000 milliseconds. So total workout might be slightly longer. requestAnimationFrame could give better results. Test it.

Upvotes: 2

Related Questions