Setting up the Javascript Experiment part 3: the Search

Where ya been Nellie?

I had the code working and written up for this part of the blog for a bit now, but wanted it incorporated into the finished app so the readers can see it in action before looking over the code.

Image: screenshot of Favorite Fonts Chingu prework project.  Much like google fonts catalog page, it will allow you to surf through, search through, and play with different fonts available through the google fonts API.



The full github repo:
https://github.com/nelliesnoodles/Favorite_Fonts_Chingu_V15_prework

Try it out!
https://nelliesnoodles.github.io/Favorite_Fonts_Chingu_V15_prework/

The reason:

Once I had the search algorithms worked out, there wasn't a whole lot of modification that needed to be done to work it into the javascript file for my favorite fonts project.  And since I had played with it and worked it thoroughly in the Experiment, I could see where different elements of the project needed to be included and modified to make the search work with the larger google font name list. 

Some programmers can just write code, and do large projects at scale without this step, and I'll admit, I've done a bit of that at times, but I find this method to really help it move along more smoothly and with a greater understanding of what needs to be done once the bigger picture gets set up.

Focusing on the smaller bits and bobs helps to refine it in such a way that taking the small functioning parts into the larger machine is made easier in my opinion.

For some of us older folks, putting a broken carburetor in the engine and expecting it to run is not the way to get the pistons firing.  You may just end up catching the whole thing on fire.

So, lets look at the functions for the search in chunks, and I'll post the whole JS file and HTML at the end of the post.  Feel free to scroll down to 'Gimme the code!' if you don't want to stick around for this bit.


The Functions:

Search for the first letter matches to limit the list we'll do a more refined search through.  Remember I'm keeping in mind that the google font name list is going to be massive, so shrinking the list I need to search through is going to make things faster and smoother in the long run.

The search functions will fire once the HTML element input 'search' has a keyup event.  So once someone types something into that search input, it will start in motion our searches.  This is what the call_keydown() function does.

I place functions in a bottom up structure.  So if a function needs to call another function,  the function it calls should be above it in the file.  So since call_keydown() will call from within it's boundaries all our search functions, it is below all of the functions it will use.  This has proven effective for me, but I can't fully explain why, other than that's how it's done in python, and I like it that way.  If someone can explain further, I'd love to include it in the post.

search_for uses two parameters, the astring, which is .... a string.  And the alist, which is a list.  Or in JavaScript an array.  The terminology works for me, but feel free to make it what works for you, keeping in mind that having something like search_for(x, y) will leave your code open for misinterpretation, and won't help future readers or onlookers understand your intentions.
After going through a for loop and comparing the first character of the search value and the items in our array/list, it will push them to a new list.  This new list will be the foundation for our next search,  *search_more(newstring)*.

Bobs and bits #1


--Start code block--



function search_for(astring, alist) {
    newlist = [];
    size = alist.length;
    i = 0
    
    for (j = 0; j < size; j++) {
        max = astring.length
        word = alist[j];
        first_in_word = word.charAt(i)
        first_in_string = astring.charAt(i)
        if (first_in_word == first_in_string) {
            newlist.push(word)
        }   
    };

    if (newlist.length > 0) {
        //console.log(newlist)
        //set this list that matches the first letter as the list we will
        //search through into the window under a global SEARCHLIST
        //If our list in a search is huge, we don't want to be searching through
        //it every time a letter changes... lets make it smaller, and mutable.
        window.SEARCHLIST = newlist
        //console.log(window.SEARCHLIST)
    }
    else {
        console.log("No match found")
    }

}

function call_keydown() {
        //in html, an onkeyup="call_keydown" will trigger this function.
        element = document.getElementById("search")
        newstring = element.value
        //Retrieve the first letter match list
        //This is only to match the first letter
        element_length = newstring.length

        /* ADDITION --- PART 3---
         * we're going to put this into an if block.  We only want this ran 
         * when it is the first letter of the string.  the function will save
         * the initial list of first letter matches into the window, and from
         * there we will manipulate that list into the closest matches for the user
         * we do not want to do a search if the element length is 0. 
         */
        if (element_length == 1 && element_length != 0) {
            search_for(newstring, search_array)
            console.log("first letter is entered, setting the window.SEARCHLIST")

        }
        else if (element_length > 1) {
            //Check that there is a list stored in the window:
            if (window.SEARCHLIST) {
                //lets make sure this bit is working with a console.log()
                //I went back into our search_for()  and removed the console.log of the newlist.
                // x = window.SEARCHLIST
                //console.log("window.SEARCHLIST exists")
                //console.log(x)
                search_more(newstring)
            }

        }
        else {
            // our element_length is not 1, or greater than 1, so it is less than 1.  We want
            //the window.SEARCHLIST emptied so if they are starting a new search string, we have a fresh list.
            window.SEARCHLIST = [];
        }


    }





--End code block--


The search_more(newstring) will be active from the else if clause of our call_keydown() when the user has more than one character in the search input, and the window.SEARCHLIST has been set.  I did not know you could make these 'global' like variables until recently, and the person I was coding with used them abundantly, so I think for our purposes, they are fine to use.  Again, if anyone has further input, I'd be happy to update this and add their further explanation.

In the experiment the sorted lists are only logged through the console. The parts where the lists are used to display the results to the user are in the bigger project.  Knowing the items are being searched through properly is all I want to focus on in this.  Get the small parts working, then get the big machine running.

Bits and Bobs #2

--Start code block--

function search_more(search_value) {
    //use the stored window.SEARCHLIST in our search for matching/similar strings
    var search_list = window.SEARCHLIST
    var search_list_size = search_list.length
    //console.log("***************")
    //console.log(search_list)
    //console.log("***************")
    var percentage_ranked_values = []

    if (search_list_size > 1 && search_list_size > 0) {
        //grab the astring and see what percentage of letters match the search.value
        //we'll set up the search keydown to only run this search if the value length is greater than 1.
        //percent match will be matched-letters divided by number of letters in the user string
        //the astring and search value need to be compared.

        for (i = 0; i < search_list_size; i++) {
            length_of_user_word = search_value.length
            word = search_list[i]
            total = 0
            //console.log("loop number: ", i)
            //console.log(search_list_size)

            if (word.length >= search_value.length) {
                for (n = 0; n <= length_of_user_word; n++) {
                    x = word.charAt(n)
                    y = search_value.charAt(n)
                    if (x == y) {
                        total += 1;
                    }
                };
            }
            // change the second for loop to reflect that the search value is longer than the word from the list
            // an index error will happen if we're trying to get a charAt() that has a greater index than the string has characters.
            else {
                length_of_search_value = search_value.length
                for (b = 0; b <= word.length; b++) {
                    x = word.charAt(b)
                    y = search_value.charAt(b)
                    if (x == y) {
                        total += 1;
                    }
                };
            }
            if (total > 0) {
                percentage = parseInt(total) / parseInt(search_value.length)
                fixed_percentage = percentage.toFixed(3)
                tuple = [word, fixed_percentage]
                percentage_ranked_values.push(tuple)
                //console.log(tuple)
            }
            else {
                percentage = parseInt(0)
                tuple = [word, percentage]
                percentage_ranked_values.push(tuple)
            }

        }

    };

    //console.log("The created tuple list is:")
    //console.log(percentage_ranked_values)
    sorted = sort_tuples(percentage_ranked_values)
    console.log(sorted)

        
       
}


--End code block--


Now the sort_tuples(tuple_array) was a bit of inspiration from Python.  JavaScript has no way to handle tuples, { Python docs: tuples },  but I was sure there had to be a way to accomplish the same idea.  Reference items in a set by it's index...  I had no idea this would actually work until I typed it in and tried it.  The sorting function is something built into javascript, and I just needed to modify it a bit to sort my list by the percentages instead of by the first item, word.
So you'll see in the sort function, I asked it to assign a = a[1] and b = b[1]  I want that second item to be the sort variable instead of the first.  I also think it would break if it a and b were arrays instead of single values.  I didn't test it, but feel free to!

Now a little more googling had me understanding that the javascript sort will not sort by the integer value, but by it's character value,  think unicode, ascii, all that.  In those character values, 0 comes before 1, comes before 2,  so the closer an item is to the head of the character table, the more value it has.  Our list is going to come out sorted lowest integer value, to highest.  This was easy to 'reverse()'.

--Start code block--


function sort_tuples(tuple_array) {
    /* from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
     * var numbers = [4, 2, 5, 1, 3];
       numbers.sort(function(a, b) {
       return a - b;
        });
     */
   
    
    tuple_array.sort(function (a, b) {
        a = a[1]
        b = b[1]
        return a - b
    })

    //reverse it, because the numbers are sorted by string code, not numeric value
    reversed = tuple_array.reverse()
    return reversed
}





--End code block--


Lets put it all together.

Gimme the code!

--start code block JAVASCRIPT--

// JavaScript source code
// Experiment 101, design a users search 

let deck;
let results_card;
let search; //<---  added id="search" to the html search <input>
/* added for the test */
let test;


/* We can make the array constant, because we do not want the original to change.
 * We'll make a duplicate to sort and rearrange for the pages search results.
 * Then if there's some catastrophic error, or original array is untouched by the sorting
 */
const search_array = ['ant', 'attic', 'anticipate', 'bore', 'boredom', 'cat', 'borealus', 'dog', 'emu', 'frog', 'giant', 'gargantuan', 'hardly', 'handy', 'snippet', 'snapper', 'match', 'matrix', 'mutton', 'mutton chop'];



/*  create a function to set the dom elements after the page has loaded.
 *  If the javascript tries to read an element before the page has finished loading,
 *  it will return that it is undefined.  Then making it unmodifiable to us.
 */

function set_DOM() {
    /* The elements at the top are the ones I know I'm going to manipulate so far.  
     * I can add elements as I go to the top, and in this function. 
     * I can name the elements what they are named(id=) in the HTML,  these variables will not
     * conflict with each other in the javascript and it makes it easier to track all the items.   
     */
    deck = document.getElementById("deck");
    results_card = document.getElementById("results_card")
    search = document.getElementById("search")
    test = document.getElementById("test")
}

/* Lets make sure everything is working by adding a function that will change something in the 
 * HTML after the load is complete,  we can just take this bit out when we're done.
 */
function test_function() {
/*Change the inner HTML of an element I've added for the test*/
    /*this element is set up in the set_DOM(), we can now manipulate it freely within functions*/
    test.innerHTML = "THIS TEST ELEMENT IS LOADED AND READY."

}

//------------- SEARCH THE LIST OF WORDS------------
/* We will be searching every time a person enters a character,
 * This could prove to run this function many many times, so is there
 * A way to shorten the process? 
 * A user might start with one letter, backspace to start a new first letter,
 * So we can't rely on the string we're searching for remaining constant.
 */
function search_for(astring, alist) {
    newlist = [];
    size = alist.length;
    i = 0
    
    for (j = 0; j < size; j++) {
        max = astring.length
        word = alist[j];
        first_in_word = word.charAt(i)
        first_in_string = astring.charAt(i)
        if (first_in_word == first_in_string) {
            newlist.push(word)
        }   
    };

    if (newlist.length > 0) {
        //console.log(newlist)
        //set this list that matches the first letter as the list we will
        //search through into the window under a global SEARCHLIST
        //If our list in a search is huge, we don't want to be searching through
        //it every time a letter changes... lets make it smaller, and mutable.
        window.SEARCHLIST = newlist
        //console.log(window.SEARCHLIST)
    }
    else {
        console.log("No match found")
    }

}

//---- Search through strings longer than one letter: ---

function search_more(search_value) {
    //use the stored window.SEARCHLIST in our search for matching/similar strings
    var search_list = window.SEARCHLIST
    var search_list_size = search_list.length
    //console.log("***************")
    //console.log(search_list)
    //console.log("***************")
    var percentage_ranked_values = []

    if (search_list_size > 1 && search_list_size > 0) {
        //grab the astring and see what percentage of letters match the search.value
        //we'll set up the search keydown to only run this search if the value length is greater than 1.
        //percent match will be matched-letters divided by number of letters in the user string
        //the astring and search value need to be compared.

        for (i = 0; i < search_list_size; i++) {
            length_of_user_word = search_value.length
            word = search_list[i]
            total = 0
            //console.log("loop number: ", i)
            //console.log(search_list_size)

            if (word.length >= search_value.length) {
                for (n = 0; n <= length_of_user_word; n++) {
                    x = word.charAt(n)
                    y = search_value.charAt(n)
                    if (x == y) {
                        total += 1;
                    }
                };
            }
            // change the second for loop to reflect that the search value is longer than the word from the list
            // an index error will happen if we're trying to get a charAt() that has a greater index than the string has characters.
            else {
                length_of_search_value = search_value.length
                for (b = 0; b <= word.length; b++) {
                    x = word.charAt(b)
                    y = search_value.charAt(b)
                    if (x == y) {
                        total += 1;
                    }
                };
            }
            if (total > 0) {
                percentage = parseInt(total) / parseInt(search_value.length)
                fixed_percentage = percentage.toFixed(3)
                tuple = [word, fixed_percentage]
                percentage_ranked_values.push(tuple)
                //console.log(tuple)
            }
            else {
                percentage = parseInt(0)
                tuple = [word, percentage]
                percentage_ranked_values.push(tuple)
            }

        }

    };

    //console.log("The created tuple list is:")
    //console.log(percentage_ranked_values)
    sorted = sort_tuples(percentage_ranked_values)
    console.log(sorted)

        
       
}

function sort_tuples(tuple_array) {
    /* from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
     * var numbers = [4, 2, 5, 1, 3];
       numbers.sort(function(a, b) {
       return a - b;
        });
     */
   
    
    tuple_array.sort(function (a, b) {
        a = a[1]
        b = b[1]
        return a - b
    })

    //reverse it, because the numbers are sorted by string code, not numeric value
    reversed = tuple_array.reverse()
    return reversed
}


    //----------- Conduct a search on keydown in the Search input-------------
    function call_keydown() {
        //in html, an onkeyup="call_keydown" will trigger this function.
        element = document.getElementById("search")
        newstring = element.value
        //Retrieve the first letter match list
        //This is only to match the first letter
        element_length = newstring.length

        /* ADDITION --- PART 3---
         * we're going to put this into an if block.  We only want this ran 
         * when it is the first letter of the string.  the function will save
         * the initial list of first letter matches into the window, and from
         * there we will manipulate that list into the closest matches for the user
         * we do not want to do a search if the element length is 0. 
         */
        if (element_length == 1 && element_length != 0) {
            search_for(newstring, search_array)
            console.log("first letter is entered, setting the window.SEARCHLIST")

        }
        else if (element_length > 1) {
            //Check that there is a list stored in the window:
            if (window.SEARCHLIST) {
                //lets make sure this bit is working with a console.log()
                //I went back into our search_for()  and removed the console.log of the newlist.
                // x = window.SEARCHLIST
                //console.log("window.SEARCHLIST exists")
                //console.log(x)
                search_more(newstring)
            }

        }
        else {
            // our element_length is not 1, or greater than 1, so it is less than 1.  We want
            //the window.SEARCHLIST emptied so if they are starting a new search string, we have a fresh list.
            window.SEARCHLIST = [];
        }


    }


/* The html will read the javascript file, because it is linked in the <head> first, before the html is fully loaded.
 * For this reason, we will tell it to set the DOM elements in this javascript file, after the page is done.
 * Otherwise we'll get elements that are undefined, because well, the page hasn't loaded them yet.
 */

window.onload = function load() {
    /* We'll set up all the things we want done after the window is loaded,
        * and that we want the javascript file to be able to access and use after load here.
        */
    set_DOM();
    /* event listeners will also need to be active after the page is fully loaded */
    /* elements are loaded, we can run our test.*/
    test_function();
}


--end code block---------------------

--start code block HTML---

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>
    <meta charset="utf-8" />
    <!--  viewport for mobile users -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- links to our css(optional) and javascript file-->
    <link rel="stylesheet" href="custom_js_experiment101.css" />
    <!--  THE ERROR script looked like this:

        <script type="text/javascript" src="js_experiment101.js" />

        Because the closing tag was missing, when I ran the HTML, the body was empty.
        If something has completely dissappeared from your page, HTML is probably ignoring it for some reason.
        -->
    <script type="text/javascript" src="js_experiment101.js" /></script>
    <!-- title for the page-->
    <title>javascript search</title>

</head>

<body>
    <h1> Search in an array for a close or matching string set:</h1>
    <!--  ADDED FOR TEST -->
    <h3 id="test"></h3>
    <h4> const search_array = ['ant', 'attic', 'anticipate', 'bore', 'boredom', 'cat', 'borealus', 'dog', 'emu', 'frog', 'giant', 'gargantuan', 'hardly', 'handy', 'snippet', 'snapper', 'match', 'matrix', 'mutton', 'mutton chop'];</h4>
    <main>
        <!--  a search input for user interaction -->
        <form>
            <!--    ADDED  -->
            <input placeholder="Search here" id="search" onkeyup="call_keydown()"/> <!-- Function call HERE -->
            <!-- A reset button to reset search parameters-->
            <input type="reset" value="reset" />

        </form>

        <div class="deck" id="deck">
            <div class="results_card" id="results_card" style="border: solid 2px red;">

                <!-- ADDED -->
                <h3> The array word </h3>

            </div>
        </div>
    </main>
    
</body>
</html>


--end code block HTML---


In conclusion:

NOTE: I have tons of blog entries and maybe 2 valid comments in a sea of 200 posts.  I welcome any comments, and if there is a way to better explain this, or for it to be helpful, I would actually be grateful for feedback.

For me, and I think maybe for other's like me, the experimentation route may be a way to shrink down the proportions of a problem and set focus on the details that need to be worked out before being applied to a larger engine.  Because I did these bits of codes first, got them working and verified it's what I wanted to happen, placing the functions into the bigger application was not nearly as scary or stressful.  I knew it worked on a small scale, I knew my logic was sound, and it was just a matter of a few tweeks to get the whole thing working together.

If you encounter a problem that seems vast and insurmountable, try tackling small bits.  Set up an experiment and tool around.  I've done this numerous times with python to much success, and now with building web applications, it has really helped me tackle things when I was way out of my depth.  I just swim the shallows for a little bit, build, build, break it, build it up again, and pretty soon I'm doing what I thought I wasn't capable.

As always, take it, break it, modify it, throw it away, and may the spam be ever in your flavor!



 

Comments

Popular posts from this blog

Statistics, Python, Making a Stem Plot

Pandas Python and a little probability

JavaScript Ascii animation with while loops and console.log