0

The following function naturally enters the same loop over and over again. What I want to do is start counting down from 25 seconds, when it's finished, start counting down from 10 seconds, then go back to 25 seconds. But because of the condition I wrote in the else part, it always counts backwards from 10 seconds. How can I fix this?

        var interval = 25000;
        var interval1 = 10000;

        function millisToMinutesAndSeconds(millis) {
            var seconds = ((millis % 60000) / 1000).toFixed(0);
            return (seconds < 10 ? "0" : "") + seconds;
        }
        function tensecond() {
            localStorage.endTime = +new Date() + interval1;
        }

        function reset() {
            localStorage.endTime = +new Date() + interval;
        }

        setInterval(function () {
            var remaining = localStorage.endTime - new Date();
            if (remaining >= 0) {
                document.getElementById("timer").innerText =
                    millisToMinutesAndSeconds(remaining);
            } else {
                tensecond();
            }
        }, 100);
  • Why do you use the `localStorage` object to store your global time? – trincot Sep 14 '22 at 10:16
  • In general, an interactive [debugger](/q/25385173/90527) is your most powerful tool for troubleshooting unexpected behavior. It's trickier to use when time comes into play, but still possible. You can also add a bit of scaffolding in the form of printing debug information (e.g. call `console.debug`) to display the state of interesting variables at key points. – outis Sep 14 '22 at 10:29
  • Learning to debug in your head is also an important skill. Ask yourself questions like "What are the initial values of the variables?" and "What are the transition points for the variables?" – outis Sep 14 '22 at 10:30

3 Answers3

1

Some comments:

  • Don't use the localStorage object to store your own properties. This has nothing to do with the purpose of localStorage. Just use a global variable (if you need local storage, then use its getItem and setItem methods)

  • Don't use toFixed(0) to round a number to an integer. Moreover, the comparison of that string with 10 will make a character-based comparison, not a numerical comparison. Instead use Math.round, or more appropriate here: Math.floor.

  • Don't use new Date() when you want a number of milliseconds instead of a Date object. Use Date.now() instead.

  • Don't do arithmetic on values that are not initialised. Initialise endTime before starting any logic on it. So call reset() before calling setInterval()

As to your question:

One way to get this to work is to make a cycle that covers both intervals added together. Then at each tick check whether the remaining time falls inside the first or second interval. Adjust the displayed remaining time accordingly.

Here is how that looks:

var interval = 25000;
var interval1 = 10000;
var endTime;

function millisToMinutesAndSeconds(millis) {
    // Use floor instead of toFixed
    var seconds = Math.floor((millis % 60000) / 1000);
    return (seconds < 10 ? "0" : "") + seconds;
}

function reset() {
    // Use Date.now() instead of +new Date()
    // And create a cycle length that covers both intervals
    endTime = Date.now() + interval + interval1;
}

reset();
setInterval(function () {
    var remaining = endTime - Date.now();
    if (remaining >= 0) {
        // Adjust the time to display 
        // depending on where in the total interval we are:
        if (remaining >= interval1) remaining -= interval1;
        document.getElementById("timer").innerText =
            millisToMinutesAndSeconds(remaining);
    } else {
        reset()
    }
}, 100);
<div id="timer"></div>
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Thank you for your reply, I will take your advice into consideration. I have a question: What do I need to do so that every user who enters the page sees the same value in that counter? How do I make everyone see what the value of the counter is at that moment, rather than a counter counting down from 25? @trincot – kennedyforf Sep 17 '22 at 13:10
  • If you have multiple users, then you need a server application that tells all its clients what the counter is. – trincot Sep 17 '22 at 13:12
  • Well, if I use a structure like this, is it possible to achieve what I want? setInterval(() => { const timerMs = 25000 - (Date.now() % 25000); document.querySelector('body').innerHTML = millisToSeconds(timerMs); }, 100); function millisToSeconds(millis) { const seconds = ((millis % 60000) / 1000).toFixed(0); return (seconds < 10 ? "0" : "") + seconds; } – kennedyforf Sep 17 '22 at 13:15
  • Sure, with a server application. You cannot trust that the local time is set correctly, so `Date.now()` is not guaranteed to give a correct time. – trincot Sep 17 '22 at 13:15
  • If it is for a non-critical application, you can of course decide to take that potential inaccuracy, but then you must take the risk that not all users see the same count at the same time. – trincot Sep 17 '22 at 13:21
  • There will be a prize draw at the end of every 35 second period, so it's critical. Everyone should see the same time, the expiry of the time, and the gift that came out of the lottery at the same time. Normally, if there was an expiration time, I would save it in the database and subtract it from the current time to show how much time is left. But how can I set up a build for the counter that repeats every 35 seconds? @trincot – kennedyforf Sep 17 '22 at 13:25
  • As you have a database, it sounds like you have an application server. The browser applications should get the counter status from the server, just as they get the prize selection from the server. So the JavaScript should pull regularly (maybe every 10 seconds) to sync their counter with the server, and get the prize draw info when it becomes available. In such critical applications it is important that the server is always the master. The master has the truth. – trincot Sep 17 '22 at 13:27
  • Thanks, got it. I couldn't come up with that logic. Do I have to keep the value 25 in a single column in the database and then decrement it, so how can I keep the seconds in the column in the database? @trincot – kennedyforf Sep 17 '22 at 13:39
  • Only keep the total number of seconds at the server side. JavaScript can convert that to minutes and seconds -- no problem there. – trincot Sep 17 '22 at 14:06
0

These are the facts:

  • The first time the (anonymous) interval function runs, localStorage.endTime isn't initialized, so has value undefined.
  • Any arithmetic operations on undefined result in NaN1, 2, 3, so remaining is initialized to NaN.
  • Any comparisons to NaN (other than != and !==) are false4, 5, 6, so the first time the interval function runs, it calls tensecond.
  • Thereafter, the interval function counts down. When the timer runs out, it again calls tensecond.

Short version: reset is never called.

ECMAScript, 13th Ed references

  1. § 13.15.3 ApplyStringOrNumericBinaryOperator
  2. § 7.1.4 ToNumber
  3. § 6.1.6.1.7 Number::add ( x, y )
  4. § 13.11.1 Runtime Semantics: Evaluation
  5. § 7.2.15 IsLooselyEqual ( x, y )
  6. 6.1.6.1.13 Number::equal
outis
  • 75,655
  • 22
  • 151
  • 221
0

There's no need to incorporate specific datetimes or local storage if you just need an alternating countdown timer. A simpler technique is to just keep track of the number of remaining seconds and do updates after a repeated 1s delay, subtracting a second from the total each time.

Here's an example of that (and it also displays each second rounded up instead of rounded down — so it starts with 25 (or 10) and resets at the exact moment that 0 is reached rather than displaying 0 for an entire second):

const timerElement = document.getElementById('timer');

function updateTimerElement (seconds) {
  timerElement.textContent = String(seconds).padStart(2, '0');
}

function delay (ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function countdown (seconds) {
  while (seconds > 0) {
    updateTimerElement(seconds);
    await delay(1e3); // 1e3 is 1000 (1s)
    seconds -= 1;
  }
  // You might want to update the timer one final time in order to show 0
  // if you ever stop looping the countdowns:
  // updateTimerElement(seconds);
}

async function main () {
  // Store the total number of seconds for each countdown in order:
  const secondsList = [25, 10];
  // Keep track of the current one:
  let listIndex = 0;

  while (true) {
    // Get the current number of seconds from the list:
    const seconds = secondsList[listIndex];
    // Run the countdown timer:
    await countdown(seconds);
    // Update the index to the next number of seconds in the list:
    listIndex = (listIndex + 1) % secondsList.length;
  }
}

main();
body { font-family: sans-serif; font-size: 4rem; }
<div id="timer"></div>

Finally, take care to note that JavaScript timers are not precise timing tools. See more info at: Reasons for delays longer than specified - setTimeout() - Web APIs | MDN

jsejcksn
  • 27,667
  • 4
  • 38
  • 62