Display of Multiple JavaScript Countdowns - javascript

I found a way to display multiple countdowns, using javascript.
Having multiple countdown timer events intervals in javascript
I am trying to create a "warmer" display, in a grid. I opted for an HTML table. I have a draft here
I can't get the countdowns to display in the table cells, with separated time components. I just have the entire SPAN tag in the "Days" column.

I'd take a different approach. First of all, instead of creating the table in your HTML, I would store the data about the countdown timers in an array of objects in your JavaScript code and generate the table using JS. Doing this will make it cleaner and more maintainable; to add new items you can just add an object to the array instead of mucking about with HTML.
Secondly, instead of starting an interval for each timer, I would create a single interval that updates all of the timers. Using a single interval means your DOM updates will be batched together and will minimize page reflow.
This code also recalculates the time left each time it updates. If you calculate it once and then just subtract each round, it could introduce drift into your counter. This is because setInterval only guarantees that it will wait at least as many milliseconds as you specify in the delay parameter, it could be more. It probably wouldn't matter much unless your timer was running continuously for a very long time, but over time it would be come inaccurate.
// data is an array of objects, each representing one of your categories.
// Each category has a .title to store its title and a .counters that
// stores an object for each counter in that category.
var data = [
{
title: 'ASTRONOMY',
counters: [
// Each counter has a .title and a .date that is parsed by new Date()
{
title: 'Total Solar Eclipse - (NE US)',
date: 'August 21, 2017'
},
{
title: 'Next Supermoon - Full',
date: 'December 3, 2017'
}
]
},
{
title: 'POLITICS',
counters: [
{
title: 'Next Presidential Election',
date: 'November 3, 2020'
}
]
},
{
title: 'TEST',
counters: [
{
title: '30 seconds from page load',
date: (new Date()).getTime() + (30 * 1000)
},
{
title: 'Unix Epoch',
date: 'January 1, 1970'
}
]
}
];
// this reduce generates the table
let table = data.reduce((acc, category, categoryIndex) => {
return acc + `<tr><td colspan="6" class="category">${category.title}</td></tr>` +
category.counters.reduce((acc, counter, index) => {
return acc + `<tr id="counter-${categoryIndex}-${index}">
<td>${counter.title}</td>
<td>${counter.date}</td>
<td class="days"></td>
<td class="hours"></td>
<td class="minutes"></td>
<td class="seconds"></td>
</tr>`;
}, '');
}, '<table class="countdown"><tr><th>Event</th><th>Date</th><th>Days</th><th>Hours</th><th>Minutes</th><th>Seconds</th></tr>');
table += '</table>';
// insert the table after the noscript tag
document.getElementById('countdown').insertAdjacentHTML('afterend', table);
// generate a flat list of counters
let counters = data.reduce((acc, category, categoryIndex) => {
return acc.concat(category.counters.reduce((counterAcc, counter, index) => {
return counterAcc.concat([{
// counters will be an array of the objects we generate here.
// node contains a reference to the tr element for this counter
node: document.getElementById(`counter-${categoryIndex}-${index}`),
// date is the date for this counter parsed by Date and then converted
// into a timestamp
date: (new Date(counter.date)).getTime()
}]);
}, []));
}, []);
const msSecond = 1000,
msMinute = msSecond * 60,
msHour = msMinute * 60,
msDay = msHour * 24;
let intervalId;
function updateCounters () {
counters.forEach((counter, counterIndex) => {
let remaining = counter.date - Date.now(),
node = counter.node;
let setText = (selector, text) => node.querySelector(selector).textContent = text;
if (remaining > 0) {
setText('.days', Math.floor(remaining / msDay));
remaining %= msDay;
setText('.hours', Math.floor(remaining / msHour));
remaining %= msHour;
setText('.minutes', Math.floor(remaining / msMinute));
remaining %= msMinute;
setText('.seconds', Math.floor(remaining / msSecond));
} else {
// make sure we don't accidentally display negative numbers if a timer
// firing late returns a past timestamp (or the data contains a past date)
setText('.days', 0);
setText('.hours', 0);
setText('.minutes', 0);
setText('.seconds', 0);
// This countdown has reached 0 seconds, stop updating it.
counters.splice(counterIndex, 1);
// no more counters? Stop the timer
if (counters.length === 0) {
clearInterval(intervalId);
}
}
});
}
// display counters right away without waiting a second
updateCounters();
intervalId = setInterval(updateCounters, 1000);
table {
border-collapse: collapse;
}
tr:nth-child(even) {
background-color: #edf;
}
.category {
font-weight: bold;
}
td, th {
padding: .5em;
}
.days, .hours, .minutes, .seconds {
text-align: right;
}
<noscript id="countdown">Sorry, you need JavaScript enabled to view the count
downs</noscript>
More Reading
Creating Accurate Timers in JavaScript
Critical rendering path
Repaints and Reflows: Manipulating the DOM responsibly

If you are ok to use any javascript library why not check FlipClock.js out.
As per the text provided on their site, the following are the logical requirements that were considered when creating the API.
Use as a clock
Use as a timer
Use as a countdown
Themeable using pure CSS
Clean & Dry Syntax
Abstract everything into reusable objects
Full-Featured Developer API to create custom “Clock Faces”
And if you are not ok to use any library here is what you can learn from about how to create a countdown timer using javascript
https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_countdown

Related

Finding objects that are within a time range of each other

I am using an external API to fetch a list of events happening between two dates. I have then used array.reduce to group the events happening on the same day into one array.
const time = events && events.reduce((acc, item) => {
if (!acc[item.fixture.date.split('T')[1]]) {
acc[item.fixture.date.split('T')[1]] = [];
}
acc[item.fixture.date.split('T')[1]].push(item);
return acc;
}, {})
They are labelled by the time in which the event occurs. If I console.log time then you can see in the image below how the data is returned for one day.
Example of returned data
I am trying to work out how to loop through these objects and find the ones that are within 30 minutes of each other. For example: It would find 16:05:00+01:00 and 16:30:00+01:00 and place these into a new array called Interval together.
What would be the easiest way to achieve this?
const datesInRange = (date1, date2, range) => //range = difference in minutes
(date1 - date2) / 1000 / 60 <= range
? true : false

how to change style of table row depending on cell timestamp value

I have a table that contains a date column. This column has dates from the past to the future including the current time with intervals of approximately 7 min (it may change).
I need to color the row which has present value with green and to move green to the next row when the next time is reached.
Is it possible to listen to the time value and change style over the rows?
I could color the present value but when time goes on, the style doesn't move to the next row since the code executed once on the page load.
Here is my code attempt:
I am using vue so in the HTML table part:
<tbody v-for='row in rows' :key='row.id'>
<tr :style="{'background-color': dateFormat(row.date) ?'green' :'' }">
...
</tr>
</tbody>
and in the methods:
dateFormat(d) {
const time = new Date(d);
const cc = new Date();
if (time.getMinutes() === cc.getMinutes()) return true;
return false;
}
Any help will be appreciated!
Thanks in advance.
There's not too much information of what kind of a table you want to integrate the action to. You can apply the following instructions to achieve what you want.
Make a "model" of the task, at first, read the dates when the active row should change to a JS array (data in the example code). It's handy to also include the table rows in that array, that saves some time when you don't have to query the DOM to find a row to highlight. Then create some variables to store some information of the state of the task (current, next). This information is used to control the state. Finally, create a timer, which runs when ever there's a next date to await. Calculate the delay based on the values you've stored in the model. Something like this:
// Fill the dates (for the example only)
const rows = Array.from(document.querySelector('#traced').rows);
fillDates(new Date(), 5); // constant 5 = increase time [second]
// A date can be passed to Date constructor as an acceptable string too
// Creates the timer
(function() {
const tbody = document.querySelector('#traced'),
rows = Array.from(tbody.rows),
data = rows.map(row => {
const time = new Date(row.cells[1].textContent).getTime();
// The constant 1 above is the date column number of the table
return {row, time};
});
let now = Date.now(),
last = data.length - 1,
next = data.findIndex(row => row.time > now),
current = Math.max(-1, next - 1);
if (now > data[last].time) {
// All the dates are in the past, no need for a timer
data[last].row.classList.add('active');
return;
}
// Updates row highlighting and counters, the timed function
function activateRow() {
// Update highlights
if (current > 0) {
// Remove the current highlight
data[current - 1].row.classList.remove('active');
}
if (current > -1 && next) {
// Highlight the current row
data[current].row.classList.add('active');
}
// Set the timer if needed
if (next > last) {return;} // Quit, no more dates to await
const delay = data[next].time - Date.now();
window.setTimeout(activateRow, delay);
// Update counters
current += 1;
next += 1;
}
activateRow();
}());
// Emulates the server-side dynamic table filling (for the example only)
function fillDates(base, gap = 15000) {
if (gap < 1000) {
gap *= 1000;
}
gap += Math.floor(Math.random() * 3000);
const zone = new Date().getTimezoneOffset(),
date = new Date(base).getTime() - zone * 60000;
rows.forEach((row, index) => {
const dte = new Date(date + gap * index).toISOString(),
end = dte.length - 5;
row.lastElementChild.textContent = dte.substring(0, end);
});
}
.active {
background: green;
}
<table>
<tbody id="traced">
<tr><td>Date 1</td><td></td></tr>
<tr><td>Date 2</td><td></td></tr>
<tr><td>Date 3</td><td></td></tr>
<tr><td>Date 4</td><td></td></tr>
<tr><td>Date 5</td><td></td></tr>
<tr><td>Date 6</td><td></td></tr>
</tbody>
</table>
When you're integrating the example to your own code, include only the IIFE to your JS code, the other parts of the snippet are there to make it possible to run the code reasonably in StackSnippet nevertheless visitor's timezone. Of course you've to define active class in your CSS too. You should also not include the style of the active row on the server, the JS snippet takes care of the highlighting.
Depending on the date format in the table, you might also need to edit the code according to the used format, and even make changes to the actual dates, because depending on visitor's timezone, the dates you add on the server could be badly off on some other timezone, and the automatic highlighter won't work.
There's also a jsFiddle to play with.

Why are some of the objects not getting removed in this .splice() and some are?

I'm using .splice() to remove objects from an array of objects containing a timestamp, based on whether the userDates contains either a matching timestamp or a timestamp within a range of 45 minutes before or after the object's timestamp. Essentially removing all objects with overlapping date values with the userDates array date values.
When you run this code, you'll notice that some of the objects get removed, but others don't.
JSFiddle: https://jsfiddle.net/jb2t3Lr9/1/ and code to reproduce the problem:
let userDates = ["2020-11-20T22:00:00.000Z","2020-11-20T23:00:00.000Z","2020-11-21T00:00:00.000Z","2020-11-21T01:00:00.000Z","2020-11-22T02:15:00.000Z","2020-11-22T03:15:00.000Z","2020-11-22T01:00:00.000Z","2020-11-22T00:00:00.000Z","2020-11-21T23:00:00.000Z","2020-12-13T22:00:00.000Z","2020-12-14T22:00:00.000Z","2020-12-15T22:00:00.000Z","2020-12-16T22:00:00.000Z","2020-12-13T23:00:00.000Z","2020-12-14T23:00:00.000Z","2020-11-21T20:00:00.000Z","2020-11-22T20:00:00.000Z","2020-11-22T19:00:00.000Z","2020-11-21T19:00:00.000Z"];
let datesToUpdate = [
{ sessionInterval: 50, dateTime: '2020-11-22T20:00:00.000Z' },
{ sessionInterval: 50, dateTime: '2020-11-21T20:00:00.000Z' },
{ sessionInterval: 50, dateTime: '2020-11-21T19:00:00.000Z' },
{ sessionInterval: 50, dateTime: '2020-11-22T19:00:00.000Z' },
{ sessionInterval: 50, dateTime: '2020-11-22T17:30:00.000Z' },
{ sessionInterval: 50, dateTime: '2020-11-21T17:00:00.000Z' }
];
function removeOverlappingDates(userDates, datesToUpdate) {
const FIFTEEN_MINUTES = 15 * 60 * 1000; // milliseconds
datesToUpdate.forEach((toUpdate, index) => {
userDates.forEach((date) => {
let dateInMS = new Date("" + date).valueOf();
const fifteenBefore = dateInMS - FIFTEEN_MINUTES;
const thirtyBefore = dateInMS - FIFTEEN_MINUTES * 2;
const fortyFiveBefore = dateInMS - FIFTEEN_MINUTES * 3;
const fifteenAfter = dateInMS + FIFTEEN_MINUTES;
const thirtyAfter = dateInMS + FIFTEEN_MINUTES * 2;
const fortyFiveAfter = dateInMS + FIFTEEN_MINUTES * 3;
let toUpdateInMS = new Date("" + toUpdate.dateTime).valueOf();
if (
toUpdateInMS == fifteenBefore ||
toUpdateInMS == thirtyBefore ||
toUpdateInMS == fortyFiveBefore ||
toUpdateInMS == fifteenAfter ||
toUpdateInMS == thirtyAfter ||
toUpdateInMS == fortyFiveAfter ||
toUpdateInMS == dateInMS
) {
datesToUpdate.splice(index, 1);
}
});
});
return datesToUpdate;
}
console.log("datesToUpdate 1", datesToUpdate);
datesToUpdate = removeOverlappingDates(userDates, datesToUpdate);
console.log("datesToUpdate 2", datesToUpdate);
What's even stranger to me is that if I just compare to arrays of datetime values against each other (with the same datetime values as the array of objects contains), then everything gets removed properly. Fiddle: https://jsfiddle.net/rc1mvzLq/
I'll try to write this with minimal new methods, but as mentioned in the comments, filter is ideal for this use case.
First and foremost, never change the array you're iterating over. That causes behaviours that are hard to debug. However, a copy of the array in the beginning would not help you in this case because you're using splice to remove an element of the array in the middle of it which causes a bunch of elements to reindex.
For example if you have [A,B,C,D,E] and you remove the element at index 1, you now have [A,C,D,E,F] and (since the iteration for that index is done) your index got incremented and is now 2 and you will never test value C in your logic. A few more situations like this one and you can see how multiple elements get left unchecked. The only way this can work is if two elements that need to get deleted are never next to one another.
To keep this simple and as close to your original logic as possible, what I advise is that you count how many elements you've deleted and offset the index by that amount. However also copy the array so that you don't mutate the one you're iterating over (NOTE: a shallow copy is enough, and you can simply use the spread syntax to create a new array with the same objects).
function removeOverlappingDates(userDates, datesToUpdate) {
const FIFTEEN_MINUTES = 15 * 60 * 1000; // milliseconds
const newDatesToUpdate = [...datesToUpdate];
let deletedElementsCount = 0;
datesToUpdate.forEach((toUpdate, index) => {
// same code as before
if (
// check if this condition should maybe be toUpdateInMS >= fortyFiveBefore && toUpdateInMS <= fortyFiveAfter
) {
const newIndex = index - deletedElementsCount;
newDatesToUpdate.splice(newIndex, 1);
deletedElementsCount = deletedElementsCount + 1;
}
});
});
return newDatesToUpdate;
}
As a side note, there is a reason why the copy is the one I'm changing and that's because datesToUpdate is passed as a function argument. If the datesToUpdate array gets changed inside the function, it will remain changed after it. You shouldn't change argument objects (this includes arrays) inside functions unless you want it to remain like that. A function that does this it is considered to have side effects and this can also be hard to debug if not used cautiously. In your case there is no need to do side effects because you're returning the result.

How to filter last day in an array?

I have an array of objects like this:
[
{
created: "2019-08-14T13:24:36Z",
email: "test1#gmail.com"
},
{
created: "2019-08-15T13:24:36Z",
email: "test2#gmail.com"
},
{
created: "2019-08-16T13:24:36Z",
email: "test1#gmail.com"
},
{
created: "2019-08-22T13:24:36Z",
email: "test4#gmail.com"
},
{
created: "2019-08-22T15:29:66Z",
email: "test1#gmail.com"
}
]
The array is sorted by created. I want to filter those records which are on the last day, irrespective of the time on that day. I added the timestamp using moment.js. Something on these lines:
router.get('/GetLastDayRecords', (req, res) => {
res.json(allRecords.filter(record => record.created.max()));
});
Split the task: first get the maximum date which you'll find at the end of the sorted array (just getting the "YYYY-MM-DD" part of it is enough) and then launch the filter:
let max = allRecords.length ? allRecords[allRecords.length-1].created.slice(0,10) : "";
res.json(allRecords.filter(({created}) => created >= max));
First you need to figure out which day is the last day. If you can assume the records are already sorted, then this is pretty simple:
// Assuming your records are stored in the variable "records"
var lastDay = records[records.length - 1].created;
Now here's where your specific answer may differ based on how you want to handle time zones. Suppose one event happened at 11 PM EST (3 AM GMT) and another event happened at 1 AM EST (5 AM GMT). Are these the same day? In Europe they are, but in America they aren't!
What you need to do is create some cipher from the date+time listed to a "day". This way you can compare two "days" to see if they're the same:
lastDay = new Date(lastDay);
// Setting hours, minutes, and seconds to 0 will give you just the "day" without the time, but by default will use the system timezone
lastDay.setHours(0);
lastDay.setMinutes(0);
lastDay.setSeconds(0);
Once you know which day was the last, it's a simple filter:
// Using a for loop
var results = []
for (var i = 0; i < records.length; i++)
{
if (records[i].created > lastDay) {
results.push(records[i]);
}
}
// Using .filter
var results = records.filter(x => x.created > lastDay);
Alternatively, since we know it's already sorted, we can do it a bit more efficiently by binary searching for the first record on the last day, then grabbing all records after that:
var test = records.length / 2;
var step = records.length / 4;
var found = false;
while (!found) {
if (records[test].created < lastDay) {
test += step;
step /= 2;
}
else if (records[test].created > lastDay) {
if (step == 1) {
// We found the exact cut-off
found = true;
}
else {
test -= step;
step /= 2;
}
}
}
var results = records.slice(test);
Because you're only interested in the "last" day, the logic is a bit simpler. If you wanted the "third" day, you would need to check if created was after the start of the third day and before the end of the third day. We can just check if it's after the start of the last day.
I would create a function to turn your created properties into data be easily compared.
I would also avoid trying to do the entire filter operation in one or two lines as it will difficult to read by other developers.
const dateToInt = date => parseInt( date.split('T').shift().replace(/-/g, '') );
The above will:
Split your created property into an array of date and time.
Select the first element, which happens to be the date.
Remove the dashes in the date.
Coerce the value into a number.
With this you can find the maximum value and filter based on that value.
const nums = foo.map( ({ created }) => dateToInt(created) )
First get a list of numbers from the dataset.
const max = Math.max( ...nums )
Get the biggest number in the list.
const lastDays = foo.filter( ({ created }) => dateToInt(created) === max )
With all that setup, getting the max date is very easy and readable.
Of course, since the list is already sorted. You could have just done this as well.
const last = foo[foo.length -1].created;
const lastDays = foo.filter( ({ created }) => created === last )
I wrote a solution using reduce and filter:
const lastDay = arr.reduce((acc, el) => {
const date = el.created.substr(0,10);
const oldDate = new Date(acc);
const nextDate = new Date(date);
if(oldDate.getTime() > nextDate.getTime()) {
return oldDate;
} else {
return nextDate;
}
}, '1900-01-01');
const lastDayArr = arr.filter(el => {
const date = el.created.substr(0,10);
const oldDate = new Date(lastDay);
const nextDate = new Date(date);
return (oldDate.getTime() === nextDate.getTime());
});
First, you find the most recent date, reducing the original array by comparing which date is the most recent, for this you drop the part of the created string that specifies the hours/minutes/seconds.
You can use a very distant in time date as initial value, or you can set it to null and add another validation in your callback function.
As a second step, you use filter, using the same technique of dropping the hours/minutes/seconds of the created string.
The end result is an array of the elements with the most recent date in your original array.
If you can assume the array is sorted, you can skip the reduce method and just do:
const lastDay = arr[arr.length - 1].created.substr(0,10);
This should work:
allRecords.filter( record => {
let last_date = allRecords[ allRecords.length - 1].created
return last_date.slice(0, 10) === record.created.slice(0, 10)
})
Basically, you are getting the last element from your array and slicing its created value down to its date. Then you are slicing your current record's created value down to its date and comparing if they are the same.
Assuming that the array is already ASC ordered:
const onLastDay = values.filter( v => {
const last = moment(values[ values.length - 1 ].created)
const differenceInDays = last.diff(moment(v.created), 'days')
return differenceInDays < 1
})
console.log(onLastDay)
NOTE: If you try with the reported array you get an error due the fact that the last date is not valid! There are 66 seconds!

Query for average scores of all game results in a collection?

My app stores game results that can have a final score in the range of -30 to +30 in a field called score. How do I query for the overall average of all the game results?
Simple Solution
If you know the number of game results being written will be at most once per second, you can use Cloud Functions to update a separate document average/score. For each game result addition, if the document didn't exist, set a field called count to 1 and a field called score to the game score. If the document did exist, add 1 to the field called count and add the score to the field called score.
Now, to query for the average score, simply read average/score and divide score by count.
Scalable Solution
If you suspect or know the number of game results being written will exceed once per second, you will need to apply a distributed counter style of the Simple Solution.
Your data model for the average document will use subcollections and look like:
// average/score
{
"num_shards": NUM_SHARDS,
"shards": [subcollection]
}
// average/score/shards/${NUM}
{
"count": 115,
"score": 1472
}
To make your update code more streamlined, you can initialize these shards first with:
// ref points to db.collection('average').doc('score')
function createAverageAggregate(ref, num_shards) {
var batch = db.batch();
// Initialize the counter document
batch.set(ref, { num_shards: num_shards });
// Initialize each shard with count=0
for (let i = 0; i < num_shards; i++) {
let shardRef = ref.collection('shards').doc(i.toString());
batch.set(shardRef, { count: 0, count: 0 });
}
// Commit the write batch
return batch.commit();
}
Updating the average aggregate in Cloud Functions is now as easy as:
// ref points to db.collection('average').doc('score')
function updateAverage(db, ref, num_shards) {
// Select a shard of the counter at random
const shard_id = Math.floor(Math.random() * num_shards).toString();
const shard_ref = ref.collection('shards').doc(shard_id);
// Update count in a transaction
return db.runTransaction(t => {
return t.get(shard_ref).then(doc => {
const new_count = doc.data().count + 1;
const new_score = doc.data().score + 1;
t.update(shard_ref, { count: new_count, score: new_score });
});
});
}
Getting the average can then be done with:
// ref points to db.collection('average').doc('score')
function getAverage(ref) {
// Sum the count and sum the score of each shard in the subcollection
return ref.collection('shards').get().then(snapshot => {
let total_count = 0;
let total_score = 0;
snapshot.forEach(doc => {
total_count += doc.data().count;
total_score += doc.data().score;
});
return total_score / total_count;
});
}
The write rate you can achieve in this system is NUM_SHARDS per second, so plan accordingly. Note: You can start out small and increase the number of shards easily. Simply create a new version of createAverageAggregate to increase the number of shards by first initializing the new ones, then updating the num_shards setting to match. This should be automatically picked up by your updateAverage and getAverage functions.

Categories

Resources