Sunday, 29 September 2019

Web Tutorial: Ada Lovelace Day Generator

We celebrate Ada Lovelace Day next month!

Ada Lovelace Day is a day dedicated by computer geeks and mathematicians worldwide, to a woman who is commonly recognized as the mother of modern computing, Ada Lovelace. It falls on the second Tuesday of October, and while it's nice and easy to remember, sometimes we just want the exact date for any given year.

That's where today's web tutorial comes in! Using ReactJS, we'll generate the exact date for Ada Lovelace Day from any user input year.

We begin with a HTML/ReactJS layout. Stuff will generally be in a div with an id of appContainer.
<!DOCTYPE html>
<html>
    <head>
        <title>Ada Lovelace Day</title>

        <style>

        </style>

        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
    </head>

    <body>
        <div id="appContainer">

        </div>

        <script type="text/babel">

        </script>
    </body>
</html>


Now, in appContainer, we'll insert two more divs, with ids of adaContainer and genContainer, respectively.
        <div id="appContainer">
            <div id="adaContainer">

            </div>

            <div id="genContainer">

            </div>   
        </div>


genContainer will hold the components, but adaContainer will hold this lovely little image I found.

ada.png

So let's start styling. Set all divs to have a red outline. Then give appContainer this height and width, and align it to the middle of the screen.
        <style>
            div {outline: 1px solid #FF0000;}
           
            #appContainer
            {   
                height: 500px;           
                width: 450px;
                margin: 5% auto 0 auto;
            }   
        </style>


OK so far? Great.


Now, make sure both adaContainer and genContainer have 100% width. They'll have different heights. adaContainer will have a deep grey bottom border while genContainer will have a thicker light grey top border.
        <style>
            div {outline: 1px solid #FF0000;}
           
            #appContainer
            {   
                height: 500px;           
                width: 450px;
                margin: 5% auto 0 auto;
            }

            #adaContainer
            {   
                height: 300px;           
                width: 100%;
                border-bottom: 1px solid #444444;
            }

            #genContainer
            {   
                border-top: 2px solid #CCCCCC;
                height: 200px;           
                width: 100%;
            }       
        </style>


The special effect isn't all that apparent due to the red outline, but never mind for now.


Now use the image as adaContainer's background. Set the background-size property to contain. The image should be aligned bottom.
            #adaContainer
            {   
                height: 300px;           
                width: 100%;
                border-bottom: 1px solid #444444;
                background: url(ada.png) center bottom no-repeat;
                background-size: contain;
            }


Oh, we're off to a great start!


Now for some serious business!

In the script tag, declare the class AdaLovelaceDay with a constructor method, constructor(). In it, call the super() function using props as an argument (so that we can use this to set state), then set state with two properties - year (default 0) and adaLovelaceDay (default empty string). The class should also have a componentDidMount() method, because we want stuff to happen on page load. And of course, we'll have a render() method which returns the component markup.
        <script type="text/babel">
            class AdaLovelaceDay extends React.Component
            {
                constructor(props)
                {
                      super(props);
                      this.state =
                      {
                          "year": 0,
                          "adaLovelaceDay": ""
                      };
                  }

                componentDidMount()
                {

                }

                render()
                {
                    return (

                    );               
                }
            }

            ReactDOM.render(<AdaLovelaceDay />, document.getElementById("genContainer"));
        </script>


Bind the showAdaLovelaceDayForYear() method to the constructor, then declare the method.
                constructor(props)
                {
                      super(props);
                      this.state =
                      {
                          "year": 0,
                          "adaLovelaceDay": ""
                      };
                      this.showAdaLovelaceDayForYear = this.showAdaLovelaceDayForYear.bind(this);
                  }

                showAdaLovelaceDayForYear(renderYear)
                {

                }

                componentDidMount()
                {

                }


What the showAdaLovelaceDayForYear() method does is accept the parameter renderYear, which is a four-digit number representing the year for which we want to find the second Tuesday of October. So we start by declaring a variable, day, which we set to 1. And the variable tuesdays, which we set to 0.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;
                }


Use a While loop, set to continue only if tuesdays is less than 2.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {

                    }
                }


Declare a variable, october. Set it to a date using the Date() function, and passing in the arguments renderYear, 9 and day. Since day is 1, you've basically set october to the first day of October (we use 9 because the count starts at 0, so the 10th month, October, is represented by 9) in the year renderYear.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {
                        var october = new Date(renderYear, 9, day);
                    }
                }


Use the getDay() method on october. If the result is 2, which means it's a Tuesday, increment tuesdays.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {
                        var october = new Date(renderYear, 9, day);
                       
                        if (october.getDay() == 2) tuesdays++;
                    }
                }


If tuesdays is still less than 2 at this point, increment day. So this continues until you reach the second Tuesday of October, and by that time, day would be the... well, you get the idea.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {
                        var october = new Date(renderYear, 9, day);
                       
                        if (october.getDay() == 2) tuesdays++;

                        if (tuesdays < 2) day++;
                    }
                }


The rest is fluff. Declare the variable adaLovelaceDay and set it to the value of day, then depending on what day of the month it is, append either "st", "nd" or "th" to it. It's not absolutely necessary, but it's pretty, so let's do it!
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {
                        var october = new Date(renderYear, 9, day);
                       
                        if (october.getDay() == 2) tuesdays++;

                        if (tuesdays < 2) day++;
                    }

                    var adaLovelaceDay = day;
                    if (day == 1 || day == 21 || day == 31)
                    {
                        adaLovelaceDay += "st";
                    }
                    else if (day == 2 || day == 12)
                    {
                        adaLovelaceDay += "nd";
                    }
                    else
                    {
                        adaLovelaceDay += "th";
                    }
                }


Finally set state. Use the setState() method to modify the properties year to renderYear and adaLovelaceDay to adaLovelaceDay.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {
                        var october = new Date(renderYear, 9, day);
                       
                        if (october.getDay() == 2) tuesdays++;

                        if (tuesdays < 2) day++;
                    }

                    var adaLovelaceDay = day;
                    if (day == 1 || day == 21 || day == 31)
                    {
                        adaLovelaceDay += "st";
                    }
                    else if (day == 2 || day == 12)
                    {
                        adaLovelaceDay += "nd";
                    }
                    else
                    {
                        adaLovelaceDay += "th";
                    }

                    this.setState({"year": renderYear, "adaLovelaceDay" : adaLovelaceDay});
                }


In the componentDidMount() method, run the showAdaLovelaceDayForYear() method, passing in the current year as an argument. So upon page load, the first thing this script does is get Ada Lovelace Day for the current year.
                componentDidMount()
                {
                    this.showAdaLovelaceDayForYear((new Date()).getFullYear());
                }


That's all well and good, but...

Yeah, we're getting there. Since we've set the state and all, we'll need to display it. That's where render() comes in! It will return this div, id adaText, with the following template string.
                render()
                {
                    return (
                            <div className="adaText">
                                Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
                            </div>
                    );               
                }


Style adaText. Just do what looks nice for you.
            #genContainer
            {   
                border-top: 2px solid #CCCCCC;
                height: 200px;           
                width: 100%;
            }   

            .adaText
            {
                height: 1.5em;
                width: 80%;
                margin: 0 auto 0 auto;
                padding-top: 1em;
                font-size: 16px;
                font-family: arial;
                text-align: center;
            }


And here we go! Ada Lovelace Day for 2019 is... 8th October!


Hold on, we're not done!

What's the point if we only get to forecast the current year? This would be a lot more useful if we included controls in the interface to input other years. We'll make the numbers of each four-digit year mutable via the interface.

In the render() method, declare an empty array, digitGens. Use a For loop to run 4 times.
                render()
                {
                    var digitGens = [];

                    for (var i = 0; i <= 3; i++)
                    {

                    }

                    return (
                            <div className="adaText">
                                Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
                            </div>
                    );               
                }


On every iteration, push a new element into the digitGens array. It will be an object containing the properties number and id. number is set to each digit of the four digit year stored in the state. We'll first need to convert it to a string, then extract the character at the appropriate position. id is set to the current value of i in the For loop.
                render()
                {
                    var digitGens = [];

                    for (var i = 0; i <= 3; i++)
                    {
                        digitGens.push
                        (
                            {
                                "number": this.state.year.toString().charAt(i),
                                "id": i
                            }
                        );
                    }

                    return (
                            <div className="adaText">
                                Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
                            </div>
                    );               
                }


While we're here, give each element in digitGens a inc() and dec() method. That method will call this component's changeYear() method, passing in the argument true for incrementing, and false for decrementing. (I know, we haven't created the changeYear() method yet. Soon!) Also, since this would be out of scope inside that For loop, we first declare the variable functionParent and set it to the value of this, and then use functionParent in place of this in the For loop.
                render()
                {
                    var digitGens = [];
                    var functionParent = this;

                    for (var i = 0; i <= 3; i++)
                    {
                        digitGens.push
                        (
                            {
                                "number": this.state.year.toString().charAt(i),
                                "id": i,
                                "inc": function() {functionParent.changeYear(true);},
                                "dec": function() {functionParent.changeYear(false);}
                            }
                        );
                    }

                    return (
                            <div className="adaText">
                                Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
                            </div>
                    );               
                }


Now, declare the variable digitGen (note the singular) and set it to the value of a map() method run on the digitGens array.
                render()
                {
                    var digitGens = [];
                    var functionParent = this;

                    for (var i = 0; i <= 3; i++)
                    {
                        digitGens.push
                        (
                            {
                                "number": this.state.year.toString().charAt(i),
                                "id": i,
                                "inc": function() {functionParent.changeYear(true);},
                                "dec": function() {functionParent.changeYear(false);}
                            }
                        );
                    }

                    var digitGen = digitGens.map(
                        (item, key) =>
                        {
                            return (

                            );
                        }
                    );

                    return (
                            <div className="adaText">
                                Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
                            </div>
                    );               
                }


We're going to use the digitGens array to generate a series of four number-scrollers mini-components. So, what happens is that a div with class of yearGen (note that we use className in order to avoid confusing the Babel compiler) contains two divs. The first div is styled using genDigit and contains the number.
                    var digitGen = digitGens.map(
                        (item, key) =>
                        {
                            return (
                                <div className="yearGen">                                   
                                    <div className="genDigit">
                                        {item.number}
                                    </div>   
                                    <div>
                   
                                    </div>
                                </div>
                            );
                        }
                    );


The second div is styled using buttons and has an id of digit plus the current element's id property. It contains two buttons. The buttons have data attributes, namely, i. Set that value to the id property of the current element. The onClick event handlers in each button will call the current item's inc() and dec() methods respectively. The button's text are HTML symbols ▲ and ▼.
                    var digitGen = digitGens.map(
                        (item, key) =>
                        {
                            return (
                                <div className="yearGen">                                   
                                    <div className="genDigit">
                                        {item.number}
                                    </div>   
                                    <div id="digit{item.id}" className="buttons">
                                        <button data-i={item.id} onClick={item.inc}>&#9650;</button>
                                        <button data-i={item.id} onClick={item.dec}>&#9660;</button>                 
                                    </div>
                                </div>
                            );
                        }
                    );


Next, we enclose the returned div in an outer div, and style the outer div using genContent. Put the entire digitGen component in it, just before the div.
                    return (
                            <div className="genContent">
                                {digitGen}

                                <div className="adaText">
                                    Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
                                </div>
                            </div>
                    ); 


Don't forget to create the changeYear() method! Leave it blank, for now.
                showAdaLovelaceDayForYear(renderYear)
                {
                    var day = 1;
                    var tuesdays = 0;

                    while (tuesdays < 2)
                    {
                        var october = new Date(renderYear, 9, day);
                       
                        if (october.getDay() == 2) tuesdays++;

                        if (tuesdays < 2) day++;
                    }

                    var adaLovelaceDay = day;
                    if (day == 1 || day == 21 || day == 31)
                    {
                        adaLovelaceDay += "st";
                    }
                    else if (day == 2 || day == 12)
                    {
                        adaLovelaceDay += "nd";
                    }
                    else
                    {
                        adaLovelaceDay += "th";
                    }

                    this.setState({"year": renderYear, "adaLovelaceDay" : adaLovelaceDay});
                }

                changeYear(inc)
                {

                }

                componentDidMount()
                {
                    this.showAdaLovelaceDayForYear((new Date()).getFullYear());
                }


Yikes. This looks like a horrible, horrible mess. Let's clean this up.


First, ensure that genContent occupies the full height and width of its parent.
            #genContainer
            {   
                border-top: 2px solid #CCCCCC;
                height: 200px;           
                width: 100%;
            }   

            .genContent
            {   
                height: 100%;           
                width: 100%;
            }

            .adaText
            {
                height: 1.5em;
                width: 80%;
                margin: 0 auto 0 auto;
                padding-top: 1em;
                font-size: 16px;
                font-family: arial;
                text-align: center;
            }


Each div styled using yearGen should be 150 pixels tall. They should take up 25% of their parent's width, because there are four of these suckers, and four of 25% makes up... 100%! Float them all left. genDigit basically determines how each number looks. Use personal taste here. But do center the text using the text-align property.
            .adaText
            {
                height: 1.5em;
                width: 80%;
                margin: 0 auto 0 auto;
                padding-top: 1em;
                font-size: 16px;
                font-family: arial;
                text-align: center;
            }   

            .yearGen
            {
                height: 150px;
                width: 25%;
                float: left;
            }   

            .genDigit
            {
                width: 100%;
                height: 100px;
                font-size: 80px;
                font-family: verdana;
                font-weight: bold;
                text-align: center;
            }


Much better.


Now for the buttons. The container, buttons, takes up full width and text content is centered. The button tags are styled to be small squares with rounded corners, and a little margin.
            .genDigit
            {
                width: 100%;
                height: 100px;
                font-size: 80px;
                font-family: verdana;
                font-weight: bold;
                text-align: center;
            }   

            .buttons
            {
                width: 100%;
                text-align: center;
            }   

            button
            {
                margin: 0.25em;
                width: 2.5em;
                height: 2.5em;
                text-align: center;
                border-radius: 20%;
            }


Excellent!


To prevent the text from overlapping, set the clear property to both.
            .adaText
            {
                clear: both;
                height: 1.5em;
                width: 80%;
                margin: 0 auto 0 auto;
                padding-top: 1em;
                font-size: 16px;
                font-family: arial;
                text-align: center;
            }   


Yep...


And hey, while you're at it, remove the red lines.
div {outline: 0px solid #FF0000;}


Great!


Now let's deal with the changeYear() method. Declare yearString and set it to the state object's year property, converted to a string. Then declare digits, which will be an array of yearString's numbers.
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                }


Finally, declare index, which will be the element's dataset's custom attribute i. Remember setting that while creating the sub-elements? This will tell you which number's buttons are being clicked.
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                    var index = parseInt(event.target.dataset["i"]);
                }


Now, use the map() method and the parseInt() function to convert all elements of the digits array back to numbers.
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                    var index = parseInt(event.target.dataset["i"]);

                    digits = digits.map(function(x) {return parseInt(x)});
                }


inc is true if incrementing, and false if decrementing. So if inc is true, check if the digit to be incremented is less than 9, and increment the number if so. This is to set a limit for the number... because in a year string you obviously can't go above 9.
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                    var index = parseInt(event.target.dataset["i"]);

                    digits = digits.map(function(x) {return parseInt(x)});

                    if (inc)
                    {
                        if (digits[index] < 9) digits[index]++;
                    }
                    else
                    {
                    
                    }
                }


If it's decrementing, more checks are needed. We don't want the first digit to be a 0 ever, so first check if it's the first digit (i.e. index is 0), then decrement only if the number is greater than 1. That means you get a 1, minimum.
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                    var index = parseInt(event.target.dataset["i"]);

                    digits = digits.map(function(x) {return parseInt(x)});

                    if (inc)
                    {
                        if (digits[index] < 9) digits[index]++;
                    }
                    else
                    {
                        if (index == 0)
                        {
                            if (digits[index] > 1)
                            {
                                digits[index]--;
                            }   
                        }
                        else
                        {
                       
                        }                      
                    }
                }


If it's any number other than the first, check if the number is more than 0 before decrementing. That means all other numbers can be decremented all the way to 0.
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                    var index = parseInt(event.target.dataset["i"]);

                    digits = digits.map(function(x) {return parseInt(x)});

                    if (inc)
                    {
                        if (digits[index] < 9) digits[index]++;
                    }
                    else
                    {
                        if (index == 0)
                        {
                            if (digits[index] > 1)
                            {
                                digits[index]--;
                            }   
                        }
                        else
                        {
                            if (digits[index] > 0)
                            {
                                digits[index]--;
                            }                           
                        }                      
                    }
                }


Next, set yearString to be the joined string of all the numbers in the digits array. Then call the showAdaLovelaceDayForYear() method, passing in yearString converted back to an integer, as an argument. Remember that showAdaLovelaceDayForYear() changes the state of the component, updating the year and adaLovelaceDay properties?
                changeYear(inc)
                {
                    var yearString = this.state.year.toString();
                    var digits = yearString.split("");
                    var index = parseInt(event.target.dataset["i"]);

                    digits = digits.map(function(x) {return parseInt(x)});

                    if (inc)
                    {
                        if (digits[index] < 9) digits[index]++;
                    }
                    else
                    {
                        if (index == 0)
                        {
                            if (digits[index] > 1)
                            {
                                digits[index]--;
                            }   
                        }
                        else
                        {
                            if (digits[index] > 0)
                            {
                                digits[index]--;
                            }                           
                        }                      
                    }

                    yearString = digits.join("");

                    this.showAdaLovelaceDayForYear(parseInt(yearString));
                }


Well, now click on the buttons, increment and decrement as you wish, and the generated Ada Lovelace Day should change! That's the power of ReactJS binding, right there. Try changing the year to 2018. Does Ada Lovelace Day change to 9th October?


Wishing you all a happy and productive Ada Lovelace Day!

Decades after her death, look how far we've come. It's nice to remember the amazing woman who got us started down the road towards computing as we know it today.

All my Love(lace),
T___T


No comments:

Post a Comment