In our application, we have a need for a robust countdown timer. When the timer counts down to zero, we move the user on to the next page.
There are a LOT of questions on here about making a countdown timer in JavaScript. I've weeded through a lot of them, looked at a lot of the code samples, tried a lot of plugins, and all of them fail for this one test case: If you adjust your computers system clock, all of these timers mess up (gain time, lose time, or just freeze). The reason is that most of them base their logic on saving some initial start time and then at some interval (either via setTimeout or setInterval) getting the current time and doing a diff between it and the start time. When you adjust your system time, the "current" time can be an hour or days before your start time, or hours or days later.
So, I've come up with the following code to try to handle these system change scenarios. The main thing I do is check if the "diff" between my start and current is negative (or greater than roughly one hour to handle an automatic daylight savings adjustment) to just use the tick interval as an approximation. Also, reset the start time to be current time on each tick, so that elapsed time is the sum of the time between differences, not just the diff between an initial start time and current time. Anyone see any flaws with this approach?
var timeLeft = 60000, // time left in ms, 1 minute for this example
startTime,
totalElapsedTime = 0,
TICK_INTERVAL = 500; // the setTimeout interval
var timerTick = function () {
var now = new Date(),
elapsedTime = now.getTime() - startTime.getTime();
// if the elapsed time was positive and less than approximately an hour, then add the elapsed time to the total
// otherwise, add the TICK_INTERVAL, which we know is the theoretical minimum between timerTick() calls
totalElapsedTime = (elapsedTime > 0 && elapsedTime < 3590000) ? totalElapsedTime + elapsedTime : totalElapsedTime + TICK_INTERVAL;
// reset start time
startTime = now;
// there is time left, so set up another tick
if (totalElapsedTime < timeLeft) {
// update the html that is displaying remaining time
setTimeout(timerTick, TICK_INTERVAL);
}
else {
// time ran out, so do whatever you do when it runs out
}
}
// start the timer
startTime = new Date();
setTimeout(timerTick, TICK_INTERVAL);
Why does the following not work for you?
var timeleft = 60000,
tick = 1000,
intervalID = window.setInterval(function () {
timeleft = timeleft - tick;
if (timeleft <= 0) {
// clear the interval
clearInterval(intervalID);
// do something
}
}, tick);
Update
Using setTimeout (this will drift, but probably be ok unless you are using it for animation):
var timeleft = 60000,
tick = 1000;
window.setTimeout(function timerFn() {
timeleft = timeleft - tick;
if (timeleft > 0) {
// set the next timeout
setTimeout(timerFn, tick);
} else {
// do something
}
}, tick);
Related
I am trying to update information from a weather service on my page. The info should be updated every hour on the hour. How exactly do I go about calling a function on the hour every hour?
I kind of had an idea but I'm not sure of how to actually refine it so it works...
What I had in mind was something like creating an if statement, such as: (pseudo code)
//get the mins of the current time
var mins = datetime.mins();
if(mins == "00"){
function();
}
You want to check out setInterval: https://developer.mozilla.org/en-US/docs/Web/API/Window.setInterval
It's a little hard to tell what you're trying to call with your code, but it would be something in the form of:
function callEveryHour() {
setInterval(yourFunction, 1000 * 60 * 60);
}
If you want it every hour, try something like:
var nextDate = new Date();
if (nextDate.getMinutes() === 0) { // You can check for seconds here too
callEveryHour()
} else {
nextDate.setHours(nextDate.getHours() + 1);
nextDate.setMinutes(0);
nextDate.setSeconds(0);// I wouldn't do milliseconds too ;)
var difference = nextDate - new Date();
setTimeout(callEveryHour, difference);
}
Now, this implementation checks the time once, sets the delay (or calls the function immediately), and then relies on setInterval to keep track after that. An alternative approach may be to poll the time every x many seconds/minutes, and fire it .getMinutes() == 0 instead (similar to the first part of the if-statement), which may sacrifice (marginal) performance for (marginal) accuracy. Depending on your exact needs, I would play around with both solutions.
Here is what should work (JSFiddle):
function tick() {
//get the mins of the current time
var mins = new Date().getMinutes();
if (mins == "00") {
alert('Do stuff');
}
console.log('Tick ' + mins);
}
setInterval(tick, 1000);
What you probably want is something like that:
var now = new Date();
var delay = 60 * 60 * 1000; // 1 hour in msec
var start = delay - (now.getMinutes() * 60 + now.getSeconds()) * 1000 + now.getMilliseconds();
setTimeout(function doSomething() {
// do the operation
// ... your code here...
// schedule the next tick
setTimeout(doSomething, delay);
}, start);
So basically the first time the user get the access, you need to know what is the delay in millisecond to the next "hour". So, if the user access to the page at 8:54 (with 56 seconds and 123 milliseconds), you have to schedule the first execution after around 3 minutes: after the first one is done, you can call it every "hour" (60 * 60 * 1000).
Repeat at specific minute past the hour
This counter is a little bit more versatile; it allows to perform a task repeatedly always at the same minute past the hour (e.g. 37 minutes past the hour), and this with up to millisecond precision.
The precision of this timer is derived from its recursion.
At every recursion, the millisecond time to the next minute gets recalculated. This prevents time lag over long periods.
The % sign refers to the modulo operator.
function minuteCount(minutesAfterHour) {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const milliseconds = now.getMilliseconds();
waitUntilNextMinute = setTimeout(minuteCount, 60000 - seconds * 1000 - milliseconds);
if(minutes % 60 === minutesAfterHour) {
doSomethingHourly();
}
}
minuteCount(37);
Finally, timers are best kept away from the main thread. They are best run from within a web worker, as explained here.
This works perfectly with unfocused tabs in desktop browsers.
However, dedicated web workers on Chrome for Android are put to sleep about 5 minutes after moving the main client to the background.
EDIT: Oops, I didn't see the " o' clock" things, so I edit my answer :
var last_execution = new Date().getTime();
function doSomething(force){
var current_time = new Date().getTime();
if (force || (current_time.getMinutes() == 0)
{
last_execution = current_time;
// something
// ...
}
setTimeout(doSomething(false), 1000);
}
// force the first time
doSomething(true);
// ... call your func now
let intervalId;
let timeoutId = setTimeout(() => {
// ... call your func on end of current hour
intervalId = setInterval(() => {
// ... call your func on end of each next hours
}, 3600000);
}, ((60 − moment().minutes()) × 60 × 1000) - (moment().second() * 1000));
Here is my pair of setIntervalWithDelay and clearIntervalWithDelay that one can use like this:
let descriptor = setIntervalWithDelay(callback, 60 * 60 * 1000, nextHourDelay)
And when you are done with it:
clearIntervalWithDelay(descriptor)
Here is my implementation of the functions:
const setIntervalWithDelay = (callback, interval, delay = 0) => {
let descriptor = {}
descriptor.timeoutId = setTimeout(() => {
if(!descriptor.timeoutId){
return
}
descriptor.timeoutId = null
callback()
descriptor.intervalId = setInterval(callback, interval)
}, delay)
return descriptor
}
export const clearIntervalWithDelay = (descriptor) => {
if(!isObject(descriptor) || (!descriptor.timeoutId && !descriptor.intervalId)){
console.warn("clearIntervalWithDelay: Incorrect descriptor. Please pass an object returned by setIntervalWithDelay. Skipping this call.")
return
}
if(descriptor.timeoutId){
clearTimeout(descriptor.timeoutId)
descriptor.timeoutId = null
console.log("clearIntervalWithDelay: stopped during delay.")
}
if(descriptor.intervalId){
clearInterval(descriptor.intervalId)
descriptor.intervalId = null
console.log("clearIntervalWithDelay: stopped during interval repeat.")
}
}
One example of using dayjs to get the delay for the next hour:
let nextHour = dayjs().second(0).millisecond(0).add(1, "hour")
let nextHourDelay = nextHour.diff(dayjs())
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);
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>
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
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
§ 13.15.3 ApplyStringOrNumericBinaryOperator
§ 7.1.4 ToNumber
§ 6.1.6.1.7 Number::add ( x, y )
§ 13.11.1 Runtime Semantics: Evaluation
§ 7.2.15 IsLooselyEqual ( x, y )
6.1.6.1.13 Number::equal
i am trying to create a program that will run for X amount of minutes.
minutes was always set to 1 in testing
var minutes = $('#minutes').val();
var runtime = minutes*60; // gets the seconds
var secondsEpoch = new Date() / 1000; // Epoch time
var End = secondsEpoch + runtime; // add the minutes to the current epoch
if (secondsEpoch < End) {
window.setInterval(RunClock, 1000/10);
} else {
clearInterval(RunClock);
}
function RunClock() {
console.log(new Date() / 1000);
//my code
}
The script runs for infinity and i'm confused on why ???
When alerting variable secondsEpoch and End i always end up with a time difference of 1 minute?
Alerted the start and finish times and got
Start 1395022190.621
Finish 1395022250.621
Total difference of 60 which = 1 minute
but the console log at this minute is
1395022456.657
which is obviously greater than
1395022250.621
and the scrip is still running and not stopping
You are not clearing your interval correctly. This way it will work:
var interval;
if (secondsEpoch < End) {
interval = setInterval(RunClock, 1000/10);
} else {
clearInterval(interval);
}
let's suppose I have a game loop like this
function update(){
myTime = Date.now() - myTime
// use myTime as you wish...
// reset myTime
myTime = Date.now()
}
stuff happens there, ideally at 60fps but not always. Using Date.now() I need to know how many milliseconds happens between one frame and the other. Here's why:
I have a variable X, this variable should lineary increase so that every 1 second it is increased of 480 units. This increasing should be as smooth as possible so, I think, it should be of 0,48 every millisecond.
How can I do that considering that I just draw to the screen every 16,6 milliseconds (60fps)? Do I need to compensate that by adding 0,48 * 16,6 ? Or... what kind of forumla? I'm getting crazy on this, thanks in advance!
I'm not sure I get the question, but maybe this will help?
To just get the time since the function last ran, you could do:
var lastTime = Date.now();
function update(){
//do stuff here
var myTime = Date.now(),
timePassed = myTime - lastTime; //time passed since last time
lastTime = Date.now(); //set for next time
}
To see how many times the function runs each second you could do:
var lastTime = Date.now(), framerate=1;
function update(){
//do stuff
framerate++;
if ((Date.now()-1000)>lastTime) { //if a second or more has passed
console.log(framerate); //log number of times function has ran
framerate=0; //and reset values
lastTime = Date.now();
}
}
To join those functions, do:
var lastTimeEach = Date.now(), lastTimeSecond = Date.now(), framerate=1, x=480;
function update(){
//do game logic
var myTime = Date.now(), //time now
timePassed = myTime - lastTimeEach; //time passed since last iteration
lastTimeEach = Date.now(); //reset for next iteration
framerate++;
if ((myTime-1000)>lastTimeSecond) { //one second has passed
x += 480; //add 480 to x
framerate=0; //reset framerate
lastTimeSecond = Date.now(); //reset for next second
}
}
Here's a DEMONSTRATION
I have a simple system that refreshes a div every few seconds:
$(document).ready(function() {
$("#nownext").load("response.php");
var refreshId = setInterval(function() {
$("#nownext").load('response.php?randval='+ Math.random());
}, 20000);
});
Now, because of what the content is, it is more likely to update on the hour, or at half past. (Though not always). What I'd like to do it make the system refresh MORE often between a few minutes before and after the hour (and half past), just to make it more precise.
Is this possible/how would I do it without stressing out the client's computer too much?
Use setTimeout instead of setInterval so you can dynamically alter the timing of the next interval. I'm not sure what the performance implications of creating and checking the Date() object ever millisec in the "Fast" period would be, but you could always tune that frequency up closer to every second if its an issue.
start_timer = function(timing) {
var timer, d = new Date(), min = d.getMinutes(),
timeout = 20000; /* slow timeout */
if((min >= 28 && min <= 30) || min >= 58) {
timeout = 100; /* fast timeout */
}
timer = setTimeout(start_timer, timeout);
// Call your code here.
};
$(document).ready(function() {
start_timer();
});
Since the interval itself is going to be dynamic, you're going to have to use setTimeout instead.
Something like this (untested):
$(document).ready(function() {
$("#nownext").load('response.php?randval='+ Math.random());
var minutes = new Date().getMinutes(), interval = 60*10*1000; // default 10 mins
if (minutes <= 10 || (minutes > 30 && minutes < 35) || minutes > 50) {
// update once-per-minute if within the first/last 10mins of the hour, or 5mins within the half hour
interval = 60*1000;
}
setTimeout(arguments.callee, interval);
});