0

I've got a page that shows a countdown timer. To continue the countdown if the page is reloaded, it stores the time that the timer was started when the page was first loaded (or if the timer runs down) in a cookie named "timer". On page load, it checks for this cookie and calculates how much time is remaining in the countdown: it subtracts "timer" from the current time to get the elapsed time, then subtracts this from the total time for the countdown (1 day). The remaining time is passed to the function that starts the timer, startTimer, as duration.

startTimer records when it was called in start. To calculate the current value for the countdown, the timer function (timer) subtracts start from the current time to get the time elapsed for the current page load, then subtracts this from the remaining time duration.

The following snippet shows the countdown, but cannot truly show the page load behavior, due to security restrictions (cookies are blocked):

function startTimer(duration, display) {
    var start = Date.now(),
        diff,
        hours,
        minutes,
        seconds;

    function timer() {
        // get the number of seconds that have elapsed since
        // startTimer() was called
        diff = duration - (((Date.now() - start) / 1000) | 0);

        // does the same job as parseInt truncates the float
        hours   = ((diff / 3600) % 24) | 0;
        minutes = ((diff / 60) % 60) | 0;
        seconds = (diff % 60) | 0;

        hours = hours < 10 ? "0" + hours : hours;
        minutes = minutes < 10 ? "0" + minutes : minutes;
        seconds = seconds < 10 ? "0" + seconds : seconds;

        display.textContent = hours + ":" + minutes + ":" + seconds;

        if (diff <= 0) {

            start = Date.now() + 1000;
        }
    }

    timer();
    setInterval(timer, 1000);
}

/* document.cookie isn't accessible in SO snippets,
 * so a try/catch block and use of cookieStore 
 * was added to the cookie functions (not present 
 * in production code)
 */
window.cookieStore ||= {};
function getCookie(name) {
    try {
        var match = document.cookie.match(RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
        return match ? match[1] : null;
    } catch (err) {
        return window.cookieStore[name];
    }
}

function setCookie(name, value) {
    try {
        document.cookie = name + "=" + value + "; max-age=" + 24 * 60 * 60;
    } catch (err) {
        window.cookieStore[name] = value;
        return;
    }
}

if (! getCookie('timer')) {
    setCookie('timer', Date.now());
} else if ((-1 * (getCookie('timer') - Date.now()) / 3600 / 24) >= (60 * 24)) {
    setCookie('timer', Date.now());
}

window.onload = function () {
    var timing = 60 * (60 * 24 -(Date.now() - getCookie('timer')) / 3600 / 24),
        display = document.querySelector('#time');
    startTimer(timing, display);
};
<div id="time"></div>

When I load the page, the countdown display 23:59:59, this is fine.

I wait a couple of seconds, then I press refresh. The countdown value is higher than expected by a few seconds or minutes. For example, I refreshed the page at 23:59:32, it showed 23:59:37.

I compared the countdown rate to a watch and it's not going too fast or too slow that I could tell.

So I presume, the value set or get from the cookie for the difference duration is wrong calculated, but I don't get it.

outis
  • 75,655
  • 22
  • 151
  • 221
zeflex
  • 1,487
  • 1
  • 14
  • 29
  • "different […] than expected" is vague. Always state explicitly exactly what you expect/want, and how it differs from what you get. – outis Jul 05 '22 at 19:21
  • Also, the code could use a prefatory explanation as to how it's supposed to work (as per [ask]). – outis Jul 05 '22 at 19:34
  • @outis I edit the message to says what I'm getting, what expecting. The the code itself, I didn't develop it, I am not a js expert but this code is in a couple of places on the web. I only added the part with the cookie in order to keep consistent value on refresh. – zeflex Jul 05 '22 at 19:54
  • This would likely be a lot simpler if instead of storing the start time and duration, calculate the end time by adding the total duration to the current time (when the timer first starts) and store that in the cookie. This way every time the page reloads you just need to subtract the end time from the current time to get your remaining time. – Jesse Jul 06 '22 at 21:58

1 Answers1

0

Instead of magic numbers, name your constants. For example:

const minutes_per_day = 24 * 60,
      seconds_per_day =  minutes_per_day * 60,
      seconds_per_hour = 60 * 60;

Use those in your formulae and some errors will pop out. Moreover, using named constants from the start will help avoid errors in the first place.

You should also go through the formulae and simplify them as much as possible. For example, since -1 * (a - b) == b - a, you can simplify -1 * (getCookie('timer') - Date.now()).

Part of the issue is time calculations aren't quite consistent with each other, itself partially a consequence of repeated code (every repetition is an opportunity for a bug). To avoid this, write functions/methods to handle relevant time calculations. For example:

function elapsed(since) {
    return (Date.now() - since) / 1000;
}

The whole approach could be simplified. Rather than storing the start time, calculating the elapsed time, passing that to the timer (which stores another start time) and calculating another elapsed time, store the start time in a cookie and pass that to the timer. The timer would then calculated the total time elapsed, and subtract that from the countdown duration to get the time remaining.

function startTimer(start, display) {
    var remaining = (seconds_per_day - elapsed(start)) || 0;
    // …
}

window.onload = function () {
    var start = getCookie('timer-start') || Date.now(),
        display = document.querySelector('#time');
    startTimer(start, display);
};

Other Notes

The x | 0 expressions in timer produce the intended result (defaulting to 0 if the calculations produce an invalid result, i.e. NaN), but may not have been the intended meaning. | is a bitwise operator; typically, the logical operator || is used to set a fallback. Binary bitwise operators will convert their arguments to integers, and NaN is converted to 0, so the result of NaN | 0 is 0. However, this won't work for other fallback values; for example, x | 1 will always result in an odd number if x is valid as an integer. Better to use ||.

Note window.onload only allows for one event handler. If more than one load handler is needed, use window.addEventListener.

The two branches that lead to setting the timer start cookie could be merged into one, so as not to repeat code.

var start = getCookie('timer-start');
if (   ! start
    || elapsed(start) >= seconds_per_day)
{
    setCookie('timer', Date.now());
}

As an alternative to concatenating strings with +, you can use template literals for string interpolation:

`${hours}:${minutes}:${seconds}`
// …
document.cookie = `${name}=${value}; max-age=${seconds_per_day}`;

Sometimes, one is more readable than the other; other times, it's a matter of personal preference.

You can use String.padStart to add leading zeroes, which is more readable than a ternary expression:

hours = hours.padStart(2, '0');

Since the server doesn't need the timer-start value, you could use localStorage instead of cookies. It doesn't directly support expiration, as cookies do, but is simpler to use (no need to parse the cookie string).

outis
  • 75,655
  • 22
  • 151
  • 221
  • looks like the issue is coming from the timing value I send when I get the value from the cookie. – zeflex Jul 06 '22 at 14:50