I am have an issue with a JS script I am trying to put together. I have an HTML table with somewhere in the neighborhood of 300 rows in it. I have made a sort function that will make the table headers clickable and launch my sort function. I would like to integrate a progress bar because in larger tables (500 - 1000 rows) after a header is clicked the table takes a bit of time to sort (IE is a big offender). The progress bar would tell them how much time remains before the sort is complete. The method I had in mind was a div element that I would resize based on the progression of the sort loop. The problem is that I can't seem to figure out how to integrate such a routine into my loop.
I've researched the issue and taken note of this: How to change progress bar in loop?
and this: Using setTimeout to update progress bar when looping over multiple variables
The second topic has a few demos that do essentially what I would like to do as far as the progress bar goes. However, anytime I try to implement the solutions shown in those two posts I either:
A - Crash the browser
B - Progress bar appears to work, but goes from 0 - 100% instantly, not progressively.
I am hoping someone can lead me in the right direction on what to do. This table sort progress indicator must be done using native JS because the contents must be available offline, hence I can't include any jQuery libraries via CDN and bloating the document with the entire jQuery library isn't desired.
I've created a JS fiddle with my code in it. I've stripped out what I had for progress bar code because I kept crashing the browser so all that is there as far as scripts go is the sorting-related code. jsfiddle
Here is the JS itself:
//Change this variable to match the "id" attribute of the
//table that is going to be operated on.
var tableID = "sortable";
/**
* Attach click events to all the <th> elements in a table to
* call the tableSort() function. This function assumes that cells
* in the first row in a table are <th> headers tags and that cells
* in the remaining rows are <td> data tags.
*
* #param table The table element to work with.
* #return void
*/
function initHeaders(table) {
//Get the table element
table = document.getElementById(table);
//Get the number of cells in the header row
var l = table.rows[0].cells.length;
//Loop through the header cells and attach the events
for(var i = 0; i < l; i++) {
if(table.rows[0].cells[i].addEventListener) { //For modern browsers
table.rows[0].cells[i].addEventListener("click", tableSort, false);
} else if(table.rows[0].cells[i].attachEvent) { //IE specific method
table.rows[0].cells[i].attachEvent("onclick", tableSort);
}
}
}
/**
* Compares values in a column of a table and then sorts the table rows.
* Subsequent calls to this function will toggle a row between ascending
* and descending sort directions.
*
* #param e The click event passed in from the browser.
* #return mixed No return value if operation completes successfully, FALSE on error.
*/
function tableSort(e) {
/**
* Checks to see if a value is numeric.
*
* #param n The incoming value to check.
* #return bool TRUE if value is numeric, FALSE otherwise.
*/
tableSort.isNumeric = function (n) {
var num = false;
if(!isNaN(n) && isFinite(n)) {
num = true;
}
return num;
}
//Get the element from the click event that was passed.
if(e.currentTarget) { //For modern browsers
e = e.currentTarget;
} else if(window.event.srcElement) { //IE specific method
e = window.event.srcElement;
} else {
console.log("Unable to determine source event. Terminating....");
return false;
}
//Get the index of the cell, will be needed later
var ndx = e.cellIndex;
//Toggle between "asc" and "desc" depending on element's id attribute
if(e.id == "asc") {
e.id = "desc";
} else {
e.id = "asc";
}
//Move up from the <th> that was clicked and find the parent table element.
var parent = e.parentElement;
var s = parent.tagName;
while(s.toLowerCase() != "table") {
parent = parent.parentElement;
s = parent.tagName;
}
/*
Executes two different loops. A "for" loop to control how many
times the table rows are passed looking for values to sort and a
"while" loop that does the actual comparing of values. The "for"
loop also controls how many times the embedded "while" loop will
run since each iteration with force at least one table row into
the correct position.
*/
//var interval = setInterval( function () { progress.updateProgress() } , 100);
var rows = parent.tBodies[0].rows.length; //Isolate and count rows only in the <tbody> element.
if(rows > 1) { //Make sure there are enough rows to bother with sorting
var v1; //Value 1 placeholder
var v2; //Value 2 placeholder
var tbody = parent.tBodies[0]; //Table body to manipulate
//Start the for loop (controls amount of table passes)
for(i = 0; i < rows; i++) {
var j = 0; //Counter for swapping routine
var offset = rows - i - 1; //Stops next loop from overchecking
// WANT TO UPDATE PROGRESS BAR HERE
//Start the while loop (controls number of comparisons to make)
while(j < offset) {
//Check to make sure values can be extracted before proceeding
if(typeof tbody.rows[j].cells[ndx].innerHTML !== undefined && typeof tbody.rows[j + 1].cells[ndx].innerHTML !== undefined) {
//Get cell values and compare
v1 = tbody.rows[j].cells[ndx].innerHTML;
v2 = tbody.rows[j + 1].cells[ndx].innerHTML;
if(tableSort.isNumeric(v1) && tableSort.isNumeric(v2)) {
//Dealing with two numbers
v1 = new Number(v1);
v2 = new Number(v2);
if(v1 > v2) {
if(e.id == "asc") { //v1 moves down
tbody.insertBefore(tbody.rows[j + 1], tbody.rows[j]);
}
} else {
if(e.id == "desc") { //v1 moves down
tbody.insertBefore(tbody.rows[j + 1], tbody.rows[j]);
}
}
} else if(tableSort.isNumeric(v1) && !tableSort.isNumeric(v2)) {
//v2 is a string, v1 is a number and automatically wins
if(e.id == "asc") { //v1 moves down
tbody.insertBefore(tbody.rows[j + 1], tbody.rows[j]);
}
} else if(!tableSort.isNumeric(v1) && tableSort.isNumeric(v2)) {
//v1 is a string, v2 is a number and automatically wins
if(e.id == "desc") { //v1 moves down
tbody.insertBefore(tbody.rows[j + 1], tbody.rows[j]);
}
} else {
//Both v1 and v2 are strings, use localeCompare()
if(v1.localeCompare(v2) > 0) {
if(e.id == "asc") { //v1 moves down
tbody.insertBefore(tbody.rows[j + 1], tbody.rows[j]);
}
} else {
if(e.id == "desc") { //v1 moves down
tbody.insertBefore(tbody.rows[j + 1], tbody.rows[j]);
}
}
}
j++;
} else {
console.log("One of the values turned up undefined");
}
}
}
}
}
//Wait until DOM is ready and then initialize the table headers.
window.onload = function () {
initHeaders(tableID);
}
Thanks in advance to anyone who can point me in the right direction.
-----
EDIT:
----- Okay so after reading the answers here and making some hefty modifications to how I was going about things I have come up with a much better solution. The progress bar isn't exactly what I wanted, but it is close. (Although I believe that it is showing up on the page just as the sorting is getting ready to finish up.)
I modified my loop to do a O(n log n) quick sort and instead of modifying the DOM directly I instead isolate the table rows to sort and copy them into an array. I then do the sort directly on the array and once it is finished I rebuild the rows and then remove the old rows and append the new ones in. The sort time has been reduced significantly.
Have a look: http://jsfiddle.net/jnBmp/5/
And here is the new JS code:
//Change this variable to match the "id" attribute of the
//table that is going to be operated on.
var tableID = "sortable";
/**
* Attach click events to all the <th> elements in a table to
* call the tableSort() function. This function assumes that cells
* in the first row in a table are <th> headers tags and that cells
* in the remaining rows are <td> data tags.
*
* #param table The table element to work with.
* #return void
*/
function initHeaders(table) {
//Get the table element
table = document.getElementById(table);
//Get the number of cells in the header row
var l = table.rows[0].cells.length;
//Loop through the header cells and attach the events
for(var i = 0; i < l; i++) {
if(table.rows[0].cells[i].addEventListener) { //For modern browsers
table.rows[0].cells[i].addEventListener("click", tableSort, false);
} else if(table.rows[0].cells[i].attachEvent) { //IE specific method
table.rows[0].cells[i].attachEvent("onclick", tableSort);
}
}
}
function tableSort(e) {
var runs = 0;
var pix = 0;
var ndx = 0;
var dir = "right";
var interval = false;
//Get the element from the click event that was passed.
if(e.currentTarget) { //For modern browsers
e = e.currentTarget;
} else if(window.event.srcElement) { //IE specific method
e = window.event.srcElement;
} else {
console.log("Unable to determine source event. Terminating....");
return false;
}
//Get the index of the cell, will be needed later
ndx = e.cellIndex;
//Toggle between "asc" and "desc" depending on element's id attribute
if(e.id == "asc") {
e.id = "desc";
} else {
e.id = "asc";
}
//Move up from the <th> that was clicked and find the parent table element.
var parent = e.parentElement;
var s = parent.tagName;
while(s.toLowerCase() != "table") {
parent = parent.parentElement;
s = parent.tagName;
}
//Get the rows to operate on as an array
var rows = document.getElementById("replace").rows;
var a = new Array();
for(i = 0; i < rows.length; i++) {
a.push(rows[i]);
}
//Show progress bar ticker
document.getElementById("progress").style.display = "block";
/**
* Show the progress bar ticker animation
*
* #param pix The current pixel count to set the <div> margin at.
*/
function updateTicker(pix) {
var tick = document.getElementById("progressTicker");
document.getElementById("progressText").style.display = "block";
document.getElementById("progressText").innerHTML = "Sorting table...please wait";
if(dir == "right") {
if(pix < 170) {
pix += 5;
tick.style.marginLeft = pix + "px";
} else {
dir = "left";
}
} else {
if(pix > 0) {
pix -= 5;
tick.style.marginLeft = pix + "px";
} else {
dir = "left";
}
}
interval = window.setTimeout( function () { updateTicker(pix); }, 25);
}
updateTicker(pix);
/**
* Checks to see if a value is numeric.
*
* #param n The incoming value to check.
* #return bool TRUE if value is numeric, FALSE otherwise.
*/
isNumeric = function (n) {
var num = false;
if(!isNaN(n) && isFinite(n)) {
num = true;
}
return num;
}
/**
* Compares two values and determines which one is "bigger".
*
* #param x A reference value to check against.
* #param y The value to be determined bigger or smaller than the reference.
* #return TRUE if y is greater or equal to x, FALSE otherwise
*/
function compare(x, y) {
var bigger = false;
x = x.cells[ndx].textContent;
y = y.cells[ndx].textContent;
//console.log(e.id);
if(isNumeric(x) && isNumeric(y)) {
if(y >= x) {
bigger = (e.id == "asc") ? true : false;
} else {
bigger = (e.id == "desc") ? true : false;
}
} else {
if(y.localeCompare(x) >= 0) {
bigger = (e.id == "asc") ? true : false;
} else {
bigger = (e.id == "desc") ? true : false;
}
}
return bigger;
}
/**
* Performs a quicksort O(n log n) on an array.
*
* #param array The array that needs sorting
* #return array The sorted array.
*/
function nlognSort(array) {
runs++
if(array.length > 1) {
var big = new Array();
var small = new Array();
var pivot = array.pop();
var l = array.length;
for(i = 0; i < l; i++) {
if(compare(pivot,array[i])) {
big.push(array[i]);
} else {
small.push(array[i]);
}
}
return Array.prototype.concat(nlognSort(small), pivot, nlognSort(big));
} else {
return array;
}
}
//Run sort routine
b = nlognSort(a);
//Rebuild <tbody> and replace new with the old
var tbody = document.createElement("tbody");
var l = b.length;
for(i = 0; i < l; i++) {
tbody.appendChild(b.shift());
}
parent.removeChild(document.getElementById("replace"));
parent.appendChild(tbody);
tbody.setAttribute("id","replace");
setTimeout(function () {
document.getElementById("progress").style.display = "none";
document.getElementById("progressText").style.display = "none";
clearTimeout(interval);
},1500);
}
window.onload = function() {
initHeaders(tableID);
}
Thanks again everyone!!
Take a look at the following:
http://jsfiddle.net/6JxQk/
The idea here is to replace your for loop with an asynchronous loop that uses setTimeout(), so you would go from the following:
for (var i = 0; i < rows; i++) {
// do stuff
}
... to this:
var i = 0;
(function doSort() {
// update progress
// do stuff
i++;
if (i < rows) {
setTimeout(doSort, 0);
}
})();
Although as you can see, this significantly slows down your sorting routine because in addition to updating the progress bar, this will reorder the rows of your table. With this in mind I think you are better off just using a built-in sort rather than your own implementation, and dropping the progress bar.
It may not be exactly what you are looking for - IMHO a progress bar must be used when you have an estimate of how much time a particular operation is going to take or how many bytes need to be transferred. In other non-deterministic cases you must be showing a spinner :-)
Related
I have an array that is constantly being updated, and needs to display the items in the array 5 at a time. Sometimes there are more than 5 elements in the array, sometimes there are less. If there are more than 5 elements in the array, then I need to cycle them 5 at a time. For example, if there are 10 elements, I want to fade in 1-5, then fade out 1-5, then fade in 5-10. I have this working, and updating, however, if there are only 4 news articles available after the data update, it still fades in and out 1-4, over and over. I need to always fade in the first articles, and if there are less than the numberToShow, don't fade out, just update.
I have tried clearInterval, but that stops updating. I tried .stop().fadeOut(); but then the fade in keeps occurring. I tried .stop().fadeOut(); with .stop().fadeIn(); but the data never fades in. Should I pass the array in to display it, and cycle in there?
For testing, this is simulated with using the date. Every 8 seconds it should update the the data with an updated number. If there are 4 articles, fade in, and update the Date.now() number, but never fade out. If there are 10 articles, fade in and update each cycle.
var numberToShow = 5;
var newsArray = [];
var startRow = 0;
var endRow = 0;
function getData() {
// Simulate the data changing using date.
newsArray = [Date.now(), "News article 1", "News article 2", "News article 3", "News article 4",
"News article 5", "News article 6", "News article 7", "News article 8", "News article 9"];
showNews(numberToShow);
}
// Fade out the results for the next cycle
setInterval(function() {
$("span.text").fadeOut({
duration: 800
});
setTimeout(
function() {
getData();
},
(800)
);
}, 8000);
// Update the data
function updateData() {
getData();
setTimeout(updateData, 6000);
}
// Display the results
function showNews() {
if (endRow >= newsArray.length) {
startRow = 0;
}
endRow = startRow + numberToShow;
if (endRow >= newsArray.length) {
endRow = newsArray.length;
}
var results = "";
for (var k = startRow; k < endRow; k++) {
results += "<span class='text' style='display:none;'>" + newsArray[k] + "</span><br>";
}
startRow = startRow + numberToShow;
document.getElementById('showResults').innerHTML = results;
$("span.text").fadeIn({
duration: 800
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="showResults"></div>
While previous answer works fine - this one could be more like the case in your description...
Short description of the idea:
show list or the part of it.
if list is longer - repeat (go to step 1 in a couple of secs to show another part of the list)
when update comes - anytime - start again with new array
And working example (removed unneeded code and added button to help with tests):
var
numberToShow = 5,
newsArray = [],
startRow = 0,
endRow = 0,
$results = $("#showResults"),
timer;
function getData() {
// Simulate the data changing
newsArray = [Date.now()];
// add random number of items
var j = Math.floor(Math.random()*7)+1;
for(var i=0; i<j; i++){
newsArray.push('News article '+i);
}
// add one more item named "last"
newsArray.push('Last News article');
startCycle();
}
function startCycle() {
startRow = 0;
endRow = 0;
$results.fadeOut(800, function(){
renderList();
});
}
function renderList() {
if (endRow >= newsArray.length) {
startRow = 0;
}
endRow = startRow + numberToShow;
if (endRow > newsArray.length) {
endRow = newsArray.length;
}
var results = "";
for (var k = startRow; k < endRow; k++) {
results += "<span class='text'>" + newsArray[k] + "</span><br>";
}
startRow = startRow + numberToShow;
$results.html(results);
$results.fadeIn(800, function(){
nextCycle();
});
}
function nextCycle() {
// start cycling only if there is more results to be shown
if(newsArray.length > numberToShow){
timer = setTimeout(function(){
$results.fadeOut(800, function(){
renderList();
});
}, 4000);
}
}
// update on request
function updateData() {
clearTimeout(timer);
$results.stop();
getData();
}
// add button for tests
$results.before(
$('<button/>').text('Update now').click(function(){
updateData();
})
)
getData();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="showResults"></div>
Ok - not sure what exactly you tried to do - but you will easily change my code into anything you need
...lets start with clear algorithm:
getData
fade out if list is visible ...then continue to step 3.
render next portion of items from array
fade in if list is not visible ...then go to step 5.
wait a while...
check if all items has been shown (if not - show next portion with step 2, if so - update data with step 1)
hope the the code will give you a chance to adopt it to your needs:
var
numberToShow = 5,
newsArray = [],
startRow = 0,
endRow = 0,
$results = $("#showResults"),
visible = false,
timer;
function fadeInIfNeeded(callback) {
var is_visible = visible;
visible = true;
if(is_visible){
callback();
}else{
$results.fadeIn(800, callback);
}
}
function fadeOutIfNeeded(callback) {
var is_visible = visible;
visible = false;
if(is_visible){
$results.fadeOut(800, callback);
}else{
callback();
}
}
function getData() {
// Simulate the data changing
newsArray = [Date.now()];
// add random number of items
var j = Math.floor(Math.random()*6)+2;
for(var i=1; i<j; i++){
newsArray.push('News article '+i);
}
// add one more item named "last"
newsArray.push('Last News article');
startCycle();
}
function startCycle() {
startRow = 0;
endRow = 0;
fadeOutIfNeeded(function(){
renderList();
});
}
function renderList() {
if (endRow >= newsArray.length) {
startRow = 0;
}
endRow = startRow + numberToShow;
if (endRow > newsArray.length) {
endRow = newsArray.length;
}
var results = "";
for (var k = startRow; k < endRow; k++) {
results += "<span class='text'>" + newsArray[k] + "</span><br>";
}
startRow = startRow + numberToShow;
$results.html(results);
fadeInIfNeeded(function(){
nextCycle();
});
}
function nextCycle() {
// every portion of data will be seen for 6 + 0.8 + 0.8 = 7.6 sec
timer = setTimeout(function(){
if(startRow >= newsArray.length){
// if all items has been shown - get new data (update)
getData();
}else{
// if there is more to show - fade out and render
fadeOutIfNeeded(function(){
renderList();
});
}
}, 6000);
}
getData();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="showResults"></div>
Here is a modern view of how to implement your use case. Please note there
is not a single global variable and all DOM changes (side effects) occur in a single function.
You describe a constantly changing array. This code produces that array of integers
with one remarkable difference. After the data is set/retrieved, a Custom Event
is produced and fired. That event carries the changed/updated array.
function newData(data) { // Random size of array (between 1-15)
const previousLength = data.length;
// ~50% of the time the array grows, otherwise it shrinks
data.length = (Math.random() > .5)?
data.length + Math.ceil(Math.random() * (7 - 1) + 1) :
Math.ceil(Math.random() * (4 - 1) + 1);
for (let i = previousLength; i < data.length; i++) {
data[i] = Math.ceil(Math.random() * (15 - 1) + 1);
}
let dataEvent = new CustomEvent('gotData', { detail: data});
document.getElementById('showResults').dispatchEvent(dataEvent);
}
Your question describes what is the need for a custom iterator that provides no more than 5
array elements for each iteration. The following code provides a Generator function that follows
JavaScript's iterator protocol by returning a result with a .value property
containing an array of no more than 5 elements and a .done property containing a Boolean indicating if there
is no futher data. The yield statement returns data or the return statement results in
.done being set to true to indicate there is no further data. (see the MDN articles for details)
function* nextSet(data = [], numberToShow = 5) {
let current = [];
let currentStart = 0;
while (true) {
[current] = [data.slice(currentStart, currentStart + numberToShow)];
if (currentStart < data.length) {
yield current;
} else {
return;
}
currentStart += numberToShow;
}
}
With the data and iterator in place this code starts off the procession. Set up
an event listener for the custom event, then get some mock data (starting with an
empty array):
document.getElementById('showResults').addEventListener('gotData', doDOM);
newData([]);
All the DOM work is done in the event callback function below (doDOM()).
First create an iterator from the Generator Function.
Then start the interval timer so that we can repeatedly call .next() on the iterator.
Please note how dead-simple the animation actually is with
a bit of rethinking the approach to the entire problem. If result is undefined
then cancel the interval timer, mock more data and repeat the process with the updated array.
function doDOM(event) {
const data = event.detail;
const iterator = nextSet(data); // create iterator from Generator
let text = '';
let page = 0;
let interval = setInterval(()=>{
page++;
let result = iterator.next().value;
if(result) {
text = `Array size: ${data.length} (Page ${page}) -- ${JSON.stringify(result)}`;
// Dead simple animations...
$(event.target).fadeOut(1000, () => {
event.target.innerText = text;
$(event.target).fadeIn(1000);
});
} else {
event.target.innerText += " ----> getting more data..."
// all done, so kill this one
clearInterval(interval);
// Mock new data arrival
newData(data);
return;
}
}, 5000);
}
I do realize this seems to be a mile off from your question. But this answer addresses the whole
puzzle rather than just one bit.
/**
* A Generator function to produce number of data elements
*/
function* nextSet(data = [], numberToShow = 5) {
let current = [];
let currentStart = 0;
while (true) {
[current] = [data.slice(currentStart, currentStart + numberToShow)];
if (currentStart < data.length) {
yield current;
} else {
return;
}
currentStart += numberToShow;
}
}
/**
* CustomEvent Handler - fired when new data is received
*
* DOM manipulations
* All side effects are contained within one function
* #parm Event - contains detail with data
*/
function doDOM(event) {
const data = event.detail;
const iterator = nextSet(data); // create iterator from Generator
let text = '';
let page = 0;
let interval = setInterval(() => {
page++;
let result = iterator.next().value;
if (result) {
text = `Array size: ${data.length} (Page ${page}) -- ${JSON.stringify(result)}`;
// Dead simple animations...
$(event.target).fadeOut(1000, () => {
event.target.innerText = text;
$(event.target).fadeIn(1000);
});
} else {
event.target.innerText += " ----> That's it! Getting more data..."
// all done, so kill this one
clearInterval(interval);
// Mock new data arrival
newData(data);
return;
}
}, 5000);
}
// Array that is either growing or changing on each call
function newData(data) { // Random size of array (between 1-15)
const previousLength = data.length;
// ~50% of the time the array grows, otherwise it shrinks
data.length = (Math.random() > .5) ?
data.length + Math.ceil(Math.random() * (7 - 1) + 1) :
Math.ceil(Math.random() * (4 - 1) + 1);
for (let i = previousLength; i < data.length; i++) {
data[i] = Math.ceil(Math.random() * (15 - 1) + 1);
}
let dataEvent = new CustomEvent('gotData', {
detail: data
});
document.getElementById('showResults').dispatchEvent(dataEvent);
}
document.getElementById('showResults').addEventListener('gotData', doDOM);
newData([]);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script defer src="index.js"></script>
</head>
<body>
<main id="showResults">Welcome! ----> waiting for data...</main>
</body>
</html>
Expand the code snippet to see this work.
I'm creating a game where the computer tries to guess the user's number based on user feedback like too high or too low. I'm using a binary search. The functions work properly, however, every time the buttons are pressed, the code resets to make the original list from 1 to 100 making the guess 50 instead of remembering the new list and guess defined inside my functions.
var list = new Array();
for (i = 0; i <= 100; i++) {
list.push(i)
}
//console.log(list)
// List is intially an empty array (list). The
// for loop generates integers from
// 0 to 100 and pushes them into the array.
var guess = list[Math.floor((list.length / 2))];
console.log(guess);
var toolow = function(guess) {
while (list.includes(guess) == true) {
list.shift()
};
var guess = list[Math.floor((list.length / 2) - 1)];
console.log(list);
console.log(guess)
}
// toolow(guess)
var toohigh = function(guess) {
var last = parseInt(list.length);
while (list.includes(guess) == true) {
list.pop()
};
var guess = list[Math.round(list.length / 2)];
console.log(list);
console.log(guess)
}
// toohigh(guess)
<h1> Guess Your Number </h1>
<button id="TooLow" onclick="toolow(guess);"> Too Low</button>
<button id="TooHigh" onclick="toohigh(guess);">Too High</button>
your over use of the variable guess is causing all sorts of issues
no need to pass guess from onclick to the function
don't declare a var guess inside the functions
et voila - your code works now
var list = new Array();
for (i = 0; i <= 100; i++) {
list.push(i)
}
//console.log(list)
// List is intially an empty array (list). The
// for loop generates integers from
// 0 to 100 and pushes them into the array.
var guess = list[Math.floor((list.length / 2))];
console.log(guess);
var toolow = function() {
while (list.includes(guess) == true) {
list.shift()
};
guess = list[Math.floor((list.length / 2) - 1)];
console.log(list);
console.log(guess)
}
// toolow(guess)
var toohigh = function() {
var last = parseInt(list.length);
while (list.includes(guess) == true) {
list.pop()
};
guess = list[Math.round(list.length / 2)];
console.log(list);
console.log(guess)
}
// toohigh(guess)
<h1> Guess Your Number </h1>
<button id="TooLow" onclick="toolow();"> Too Low</button>
<button id="TooHigh" onclick="toohigh();">Too High</button>
I wrote a kind of N-Queens algorithm, handling only the vertical and horizontal threats detection. Thus, it's rather a N-Towers solutions finder.
To do this, I use recursion. It's a well-known algorithm. For each square of the chessboard, I place a tower. For each placed tower, I try to place another tower (this is the recursive call). If there is not any remaining tower to place, it means the program has found a solution and the recursive level has to return. If all the chessboard has been crossed with remaining tower(s) to place, it means the program didn't find a solution and the recursive level has to return.
My recursive function has two parameters : the number of towers which have to be placed and the chessboard (an array of array of string ; the string equals "T" to indicate a tower has been placed in this chessboard's square and "-" to indicate the square is empty).
The problem
My algorithm seems to find all the solutions and displays them as chessboards, using the "-" (and, if it worked well, "T") notation. This notation is explained above.
However, even if the number of solutions seems to be correct, the displayed solutions/chessboards contain only "-".
I think I don't pass my array of array (ie. : the chessboard) correctly in my recursive call.
Illustration of this problem
For 2 towers and a chessboard of 2*2 squares, two solutions are found and it's normal. But there are only "-" and no "T" appears... That's the problem. Indeed :
--
--
Code : focus on my recursive function
/**
* RECURSIVE FUNCTION. If there are still towers to place, this function tries to place them. If not, it means a
* solution has been found : it's stored in an array (external to this function).
* If this function can't place a tower, nothing happens.
* Else, it places it and makes the recursive call.
* Each recursion level does this for each next (to the placed tower) chessboard's squares.
* #param number_of_left_towers how many remaining towers to place there are (if 0, it means a solution has been
* found)
* #param array_array_chessboard the chessboard
* #returns {Number} the return is not important
*/
function placeTower(number_of_left_towers, array_array_chessboard) {
if (number_of_left_towers == 0) {
return solutions.push(array_array_chessboard);
}
for (var current_x = 0; current_x < number_of_lines; current_x++) {
for (var current_y = 0; current_y < number_of_columns; current_y++) {
if (array_array_chessboard[current_x][current_y] == "-" && canBePlaced(array_array_chessboard, current_x, current_y)) {
array_array_chessboard[current_x][current_y] = "T";
placeTower(number_of_left_towers - 1, array_array_chessboard);
array_array_chessboard[current_x][current_y] = "-";
}
}
}
}
Code : JSFiddle with all the source
https://jsfiddle.net/btcj6uzp/
You can also find the same code below :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Recursive algorithm of the N-Towers</title>
</head>
<body>
<script type="text/javascript">
/**
* Finds all the solutions to the N-Towers algorithm.
*
* #param number_of_towers number of towers to try to place in the chessboard
* #param number_of_lines chessboard's ones
* #param number_of_columns chessboard's ones
* #returns {nTowersSolutions} array containing all the solutions
*/
function nTowersSolutions(number_of_towers, number_of_lines, number_of_columns) {
/*
NB
"T" = "Tower" = presence of a tower in this square of the chessboard
"-" = "nothing" = no tower in this square of the chessboard
(used for both solutions displaying and finding)
*/
var solutions = [];
var array_array_chessboard = []; // Represents the chessboard
for(var i = 0; i < number_of_lines; i++) {
array_array_chessboard[i] = new Array(number_of_columns);
for(var j = 0; j < number_of_columns; j++) {
array_array_chessboard[i][j] = "-"; // We initialize the chessboard with "-"
}
}
/**
* Uses HTML to display the found solutions, in the Web page
*/
this.displaySolutions = function() {
var body = document.body;
solutions.forEach((array_array_chessboard) => {
array_array_chessboard.forEach(function(array_chessboard) {
array_chessboard.forEach((square) => {
body.innerHTML += square; // New cell
});
body.innerHTML += "<br />"; // New line
});
body.innerHTML += "<br /><br />"; // New solution
});
};
/**
* RECURSIVE FUNCTION. If there are still towers to place, this function tries to place them. If not, it means a
* solution has been found : it's stored in an array (external to this function).
* If this function can't place a tower, nothing happens.
* Else, it places it and makes the recursive call.
* Each recursion level does this for each next (to the placed tower) chessboard's squares.
* #param number_of_left_towers how many remaining towers to place there are (if 0, it means a solution has been
* found)
* #param array_array_chessboard the chessboard
* #returns {Number} the return is not important
*/
function placeTower(number_of_left_towers, array_array_chessboard) {
if (number_of_left_towers == 0) {
return solutions.push(array_array_chessboard);
}
for (var current_x = 0; current_x < number_of_lines; current_x++) {
for (var current_y = 0; current_y < number_of_columns; current_y++) {
if (array_array_chessboard[current_x][current_y] == "-" && canBePlaced(array_array_chessboard, current_x, current_y)) {
array_array_chessboard[current_x][current_y] = "T";
placeTower(number_of_left_towers - 1, array_array_chessboard);
array_array_chessboard[current_x][current_y] = "-";
}
}
}
}
/**
* Can this tower be placed ?
* #param array_array_chessboard
* #param new_x
* #param new_y
* #returns {boolean}
*/
function canBePlaced(array_array_chessboard, new_x, new_y) {
for(var i = 0; i < array_array_chessboard.length; i++) {
for(var z = 0; z < array_array_chessboard[i].length; z++) {
if(array_array_chessboard[i][z] == "T"
&& (
new_x == z || new_y == i // Horizontal and vertical checks
)
) {
return false;
}
}
}
return true;
}
placeTower(number_of_towers, array_array_chessboard);
return this;
}
// <!-- CHANGE THESE PARAMETERS' VALUE TO TEST -->
nTowersSolutions(2, 2, 2).displaySolutions();
</script>
</body>
</html>
Your problem is most likely that there is only one (two dimentional) array, which is global, so in the end your solutions are all pointing to the same array which will be the last state it had before our recursive function completely returned.
array_array_chessboard[current_x][current_y] = "T";
placeTower(number_of_left_towers - 1, array_array_chessboard);
array_array_chessboard[current_x][current_y] = "-";
If I understand the above correctly, you are (looping over all positions, ish)
1) assigning T to a location
2) solving all boards with T in that location
3) assigning "-" to the previous location
so in the end you have an array full of "-", and all solutions point to this same array
Try replacing
return solutions.push(array_array_chessboard);
by
return solutions.push(JSON.decode(JSON.encode(array_array_chessboard)));
The above will make a deep copy of your solution, and while it might not be the utmost efficient way to make a deep copy it is a simple one. If your algorithm needs to be really fast you might want to look in a faster way to clone your solution.
Though I can't guarantee that this will work since I can't run your fiddle
(also for readability I suggest you write your return like so:)
solutions.push(JSON.parse(JSON.stringify(array_array_chessboard)));
return;
EDIT: why to use JSON.parse+stringify over Array::from:
if you simply do
solutions.push(Array.from(array_array_chessboard));
The second dimention will still reference the same arrays, and that is where your string data is stored after all.
to demonstrate (note that you need to polyfill the Array.from in IE, or simply try on a different browser):
var arr1 = ["a"];
var arr2 = ["b"];
var metaArr = [arr1, arr2];
console.log(metaArr[0][0], metaArr[1][0]); // "a b"
var metaArrClone = Array.from(metaArr);
var metaArrClone[0][0] = "c";
console.log(metaArrClone[0][0]); // "c"
console.log(metaArr[0][0]); // "c"
var metaArrClone2 = JSON.parse(JSON.stringify(metaArr));
console.log(metaArrClone2[0][0]); // "c"
metaArrClone2[0][0] = "d";
console.log(metaArrClone2[0][0]); // "d"
console.log(metaArr[0][0]); // "c"
You do not need to keep the solutions outside your recursive function. Could be better if you keep the solutions inside your recursive function and than return all the solutions, so you do not need to worry about the state outside the function.
Of course if you have to use the finded solutions before that the recursive function returns (maybe you have a big cheesboard) your solution could be better. Or you could use a generator.
Could be also good keep this kind of logic separate from the ui so first focus on the solution and then try to draw the result in the browser or where you want, or do the opposite.
You can start from the code below, but please check if it actually finds all the solutions before use it.
'use strict'
/* Finds all the solutions to the N-Towers algorithm.
*
* #param number_of_towers number of towers to try to place in the chessboard
* #param number_of_lines chessboard's ones
* #param number_of_columns chessboard's ones
* #returns {nTowersSolutions} array containing all the solutions
* "Tower" = presence of a tower in this square of the chessboard
* "Nothing" = no tower in this square of the chessboard
* "Blocked" = the cell is blocked
*/
function nTowersSolutions(number_of_towers, number_of_lines, number_of_columns) {
var chessboard = _initChessboard(number_of_lines, number_of_columns);
var solutions = _findAllSolution(chessboard, number_of_towers);
return solutions;
}
// nuber, * -> array
var _newArrFromLenAndElement = function(length, element) {
return Array.apply(null, Array(length)).map(function(){ return element; });
};
// number, number -> cheesboard
var _initChessboard = function(number_of_lines, number_of_columns) {
var oneColumnChessboard = _newArrFromLenAndElement(number_of_lines, []);
var chessboard = oneColumnChessboard.map(function() {
var line = _newArrFromLenAndElement(number_of_columns, 'Nothing');
return line;
});
return chessboard;
};
// chessboard, line_index, column_index -> chessboard
var _placeTower = function(chessboard, line_index, column_index) {
var ff = chessboard.map(function(line, index) {
if (index === line_index) {
return line.map(function() { return 'Blocked'; });
}
else {
return line.map(function(x, index){
if(index===column_index){return'Blocked';}
else{return x;}
});
}
});
ff[line_index][column_index] = 'Tower';
return ff;
};
// array[][] -> array[]
var _flatten = function(arr) {
return [].concat.apply([], arr);
};
// *, array -> boolean
var _isInArray = function(value, array) {
return array.indexOf(value) > -1;
};
// cheesboard, numberm number -> array
// this could be a bruteforce recursive solution at your problem ( actually you have to check if
// it is correct )
// we need _lines_done for don't look for solutions already finded (if you have tried all the
// pattern with a tower in the first line you don't want try to place a tower in the first line)
var _findAllSolution = function(chessboard, number_of_towers, _lines_done, _deep) {
_lines_done = _lines_done || [];
_deep = _deep || 0;
if (number_of_towers === 0){
return chessboard;
}
//for all the cells in the ceesboard try to place a tower
var solutions = chessboard.map(function(line, line_index) {
var solution = line.map(function(cell, column_index) {
if (_isInArray(line_index, _lines_done)) {
return 'alreadyVisitedLine';
}
else if (cell === 'Nothing') {
var fulfilledChessboard = _placeTower(chessboard, line_index, column_index);
if (line_index > 0 && _deep === 0 && !(_isInArray(line_index - 1, _lines_done))) {
_lines_done.push(line_index - 1);
}
return _findAllSolution(fulfilledChessboard, number_of_towers -1, _lines_done, _deep + 1);
}
else {
return 'DeadEnd!';
}
});
return _flatten(solution);
});
var flatSolutions = _flatten(solutions);
//you should .filter the solutions
return _flatten(solutions);
};
var h = nTowersSolutions(2,2,2)
console.log(h)
So I created an implicit binary tree, and a simple way of navigating through that tree that reacts when, for example, you push the left arrow, right arrow, or up arrow:
var items = [ .... ];
function newItem() {
var me = this;
this.item_location = 1;
this.leftItem = function() {
var left = items[(me.item_location * 2)];
if(left){
me.theItem = //create element, insert item from 'left' var, display item
me.item_location *= 2;
} else {}
}
this.rightItem = function() {
var right = items[(me.item_location * 2) + 1];
if(right){
me.theItem = //create element, insert item from 'right' var, display item
me.item_location = (me.item_location * 2) + 1;
} else {}
this.upItem = function() {
var up = items[Math.floor(me.item_location / 2)];
if(up){
me.theItem = //as above
me.item_location = Math.floor(me.item_location / 2);
} else {
// abandon setting this item, return to a different context/menu
}
}
My issue is that I'm finding 'duplicates' that aren't actually there, so there must be a bug in either this tree navigation code or in the code I'm using to check for duplicates:
function findItem(theItem) {
var itemLocations = [];
for(var i = items.length - 1; i >= 0; i--) {
if(items[i] === theItem) {
itemLocations.push(i);
}
}
for(var i = itemLocations.length - 1; i >= 0; i--) {
console.log(itemLocations[i]);
}
console.log(itemLocations);
return itemLocations;
}
What is wrong with this? It's consistently ending up with certain entries showing up twice, but the findItem function consistently finds only one entry.
There's nothing more to the code that could possibly cause this, so I feel like I've missed something super obvious.
I am coding a simple JavaScript version of the classic boardgame "Mastermind".
I have some (for sure fundamental) problems, 99% with JavaScript arrays and referencing their values or elements. These issues am I "solving" currently for quite long time, so I decided to ask.
Facts:
my game pegs, and the game board at all is made in HTML table, pegs are implemented like this(a row contains 4 pegs and a td containing the results image):
<td>
<a href="javascript:void(0)"
onClick="changePegColor('0','0'); return false"
onFocus="this.blur()">
<img src="img/void.png" width=22 height=22 name="peg_0_0">
</a>
</td>
my default array declaration looks this (showing both variants tried, none of them worked for me):
var pegsAI = ['pegAI_0', 'pegAI_1', 'pegAI_2', 'pegAI_3'];
var pegsAI = new Array('pegAI_0', 'pegAI_1', 'pegAI_2', 'pegAI_3');
Setting AI's pegs, which is the player going to guess works this way (this is working, no problem with array):
pegsAI[position] = Math.floor((Math.random() * possibleColorsNumber));
And here are my issues:
At the moment of clicking Submit button, there is a check if every peg in a row has a colour this way (this does neither work, nor throws an error in chrome F12):
...
for (var position = 0; position <= 3; position++) {
if (document["peg_" + currentRow + "_" + position].src === "img/void.png") {
alert('Finish color picking!');
return false;
}
}
...
After this check, there is function that should convert players pegs to numbers and save it to an array and there is probably a problem, because it doesn't work (array got undefined values in result):
function convertToNumbers() {
for (var position = 0; position <= 3; position++) { //4 pegs in row, var 'position' declares peg's position
if (document["peg_" + currentRow + "_" + position].src === possibleColors[index] ) { //if a peg has an color (his img's src is equal to an element of array 'possibleColors', which contains possible img's src's), then ->
pegsPlayer[position] = index; // -> then index of this color saves to pegsPlayer[] array
}
}
}
///added for explanation
my function for calculating score:
var goodPegPlayer = [false, false, false, false];
var goodPegAI = [false, false, false, false];
function calcSkore() {
convertToNumbers();
alert("array values" + pegsPlayer[0] + "_" + pegsPlayer[1] + "_" + pegsPlayer[2] + "_" + pegsPlayer[3]);
for (var position = 0; position <= 3; position++) {
goodPegPlayer[position] = false;
goodPegAI[position] = false;
if (pegsPlayer[position] === pegsAI[position]) {
skoreBlack++;
goodPegPlayer[position] = true;
goodPegAI[position] = true;
}
}
for (var position = 0; position <= 3; position++) {
if (goodPegPlayer[position] === false) {
for (var positionAI = 0; positionAI <= 3; positionAI++) {
if ((position !== positionAI) && (goodPegPlayer[position] === false) && (goodPegAI[positionAI] === false)) {
if (pegsPlayer[position] === pegsAI[positionAI]) {
skoreWhite++;
goodPegPlayer[position] = true;
goodPegAI[positionAI] = true;
}
}
}
}
}
resultsSubmit();
}
!! right after using converToNumber() function in this function, an alert() is used to check if the values are correct.
You're not accessing the DOM correctly. Try using the id attribute instead of name to identify your images and update your JavaScript as follows:
for (var position = 0; position <= 3; position++) {
var id = "peg_" + currentRow + "_" + position;
if (document.getElementById(id).src === "img/void.png") {
alert('Finish color picking!');
return false;
}
}
I wrote my own version of Mastermind back in 2004.
The code is a bit outdated, but it still works in modern browsers. I'm not sure if it's helpful to you, but feel free to take a look!
The code is MIT licensed, which means you can freely use it (or parts of it) anywhere you want!
Resources
Demo
Github repository