Reputation: 993
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
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:
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.
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.
[Edit] Working demo: https://jsfiddle.net/hmcwf0p5/1/
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.
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:
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.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
.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.
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
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