Tuesday, 5 July 2016

Web Tutorial: QBasic Bubble Sort Animation

Sorting data is one of the cornerstones of information technology. And the numero uno method, not in terms of ingenuity but in terms of sheer simplicity, is the Bubble Sort. How it works is simple.

Traverse a list of numbers, like the one below.
93,12,777,90

Each time you encounter a number, compare it with the number after that. If the first number is larger than the second number, swap their positions in the list. The numbers swapped are in red for clarity.

93,12,777,90
12,93,777,90

Carry on till you reach the end of the list, then begin anew at the beginning of the list. Do this till you have traversed the entire list, from first to last number, with no swaps taking place.

12,93,777,90
12,93,90,777
12,93,90,777
12,90,93,777
12,90,93,777

And thus, the list is sorted. Now, just for the heck of it, let's do this in QBasic! Here, we have a variable called sortlistsize which defines the number of numbers you wish to sort. This will be input by the user.
PRINT "T___T's BUBBLE SORT"
INPUT "What is the size of your list"; sortlistsize

After that, we use the number to define the size of the array sortlist, then use a For loop to ask the user to input all those numbers.
PRINT "T___T's BUBBLE SORT"
INPUT "What is the size of your list";

DIM sortlist(sortlistsize) 

FOR i = 0 TO (sortlistsize - 1) STEP 1 
    INPUT "Please enter Number: ", sortlist(i) 
NEXT i

If you run the code now, this is what you'll get.
 

After this, we define the variables col and row.
DIM sortlist(sortlistsize)

 FOR i = 0 TO (sortlistsize - 1) STEP 1
    INPUT "Please enter Number: ", sortlist(i)
NEXT i

DIM col, row 

col = 2 
row = 2

We're going to define four colors next - for the current numbers being sorted, and those not being sorted. For each category, we have a color for foreground and background. To find out what the values mean, look up the QBasic color chart.
DIM col, row

col = 2
row = 2

LOCATE row, col

DIM current_fc, current_bc
current_fc = 15
current_bc = 4

DIM unsorted_fc, unsorted_bc
unsorted_fc = 0
unsorted_bc = 14

Next, we define more variables. sorted is either 0 or 1, and is 1 only when the entire list has been sorted.  
passes keeps track of the number of passes so far.  
swaps keeps track of the number of swaps during the current pass. This value is reset in the next pass. currentpos keeps track of the cursor's current position in the sortlist array.
temp is a variable for holding the current value of the number to be swapped.
totalswaps, just for fun, shows us how many swaps it took for your list to get sorted.

DIM current_fc, current_bc
current_fc = 15
current_bc = 4

DIM unsorted_fc, unsorted_bc
unsorted_fc = 0
unsorted_bc = 14

DIM sorted, passes, swaps, currentpos, temp, totalswaps
sorted = 0
passes = 0
swaps = 0
totalswaps = 0
currentpos = 0

Here, we set the color to black and clear the screen using that color.
DIM sorted, passes, swaps, currentpos, temp, totalswaps
sorted = 0
passes = 0
swaps = 0
totalswaps = 0
currentpos = 0

COLOR 0, 0
CLS

This next While loop effectively means that the program continues execution till sorted becomes 1, which in turn means that the sortlist array has been completely sorted. Do not run your code at this time - you're only going to get an infinite loop because there's no exit clause.
COLOR 0, 0
CLS

WHILE sorted = 0

WEND

This introduces a slight pause that begins with every new pass. The cursor is then moved to the position on screen specified by row and col, which is, at the start of the loop, near the top left.
WHILE sorted = 0
    SLEEP 2
    LOCATE row, col

WEND

We're going to draw the entire series of numbers in the sortlist array on screen, in the appropriate colors. To do this, we use a For loop.
WHILE sorted = 0
    SLEEP 2
    LOCATE row, col

    FOR i = 0 TO (sortlistsize - 1) STEP 1

    NEXT i

WEND

Now, this next conditional checks if the variable i is at the current position of the cursor, or the next one. If so, it uses the current colors; otherwise it uses the default colors. The value is then drawn out on screen.
WHILE sorted = 0
    SLEEP 2
    LOCATE row, col

    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);

    NEXT i
WEND

The color is changed back to black, and a space is printed as a separator.
WHILE sorted = 0
    SLEEP 2
    LOCATE row, col

    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);
        COLOR 0, 0
        PRINT " ";

    NEXT i
WEND

After the entire list of numbers has been drawn on screen, we start sorting. Here, we provide an If conditional. If the number represented at the current position of the list represented by the variable currentpos is greater than the next...
    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);
        COLOR 0, 0
        PRINT " ";
    NEXT i

    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN

    END IF

WEND

...we swap their places with the use of the variable temp.
    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN
        temp = sortlist(currentpos)
        sortlist(currentpos) = sortlist(currentpos + 1)
        sortlist(currentpos + 1) = temp

    END IF
WEND

We then increment the variable swaps, and tell the processor to pause for 1 second, just for effect.
    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN
        temp = sortlist(currentpos)
        sortlist(currentpos) = sortlist(currentpos + 1)
        sortlist(currentpos + 1) = temp

        swaps = swaps + 1

        SLEEP 1

    END IF
WEND

We then go to the next line using the LOCATE command, and print out some information regarding the swap.
    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN
        temp = sortlist(currentpos)
        sortlist(currentpos) = sortlist(currentpos + 1)
        sortlist(currentpos + 1) = temp

        swaps = swaps + 1

        SLEEP 1

        LOCATE row + 1, col
        COLOR 14, 0
        PRINT "Swapping ";
        PRINT temp;
        PRINT " with ";
        PRINT sortlist(currentpos);
        PRINT "...";

    END IF
WEND

And if no sort takes place, we clear the screen.
    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN
        temp = sortlist(currentpos)
        sortlist(currentpos) = sortlist(currentpos + 1)
        sortlist(currentpos + 1) = temp

        swaps = swaps + 1

        SLEEP 1

        LOCATE row + 1, col
        COLOR 14, 0
        PRINT "Swapping ";
        PRINT temp;
        PRINT " with ";
        PRINT sortlist(currentpos);
        PRINT "...";
    ELSE
        CLS

    END IF
WEND

After that, we repeat the code block which was drawing the list of numbers. Yes I know, good programming practices dictate that I should write this as a function and avoid repeating myself. And I should, for the purposes of code efficiency, check if there are any swaps before redrawing. But honestly? This is pretty much throwaway code, so to hell with that.
    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);
        COLOR 0, 0
        PRINT " ";
    NEXT i

    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN
        temp = sortlist(currentpos)
        sortlist(currentpos) = sortlist(currentpos + 1)
        sortlist(currentpos + 1) = temp

        swaps = swaps + 1

        SLEEP 1

        LOCATE row + 1, col
        COLOR 14, 0
        PRINT "Swapping ";
        PRINT temp;
        PRINT " with ";
        PRINT sortlist(currentpos);
        PRINT "...";
    ELSE
    CLS
    END IF

    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);
        COLOR 0, 0
        PRINT " ";
    NEXT i

WEND

The next block of code moves two lines down and prints stats.
    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);
        COLOR 0, 0
        PRINT " ";
    NEXT i

    IF sortlist(currentpos) > sortlist(currentpos + 1) THEN
        temp = sortlist(currentpos)
        sortlist(currentpos) = sortlist(currentpos + 1)
        sortlist(currentpos + 1) = temp

        swaps = swaps + 1

        SLEEP 1

        LOCATE row + 1, col
        COLOR 14, 0
        PRINT "Swapping ";
        PRINT temp;
        PRINT " with ";
        PRINT sortlist(currentpos);
        PRINT "...";
    ELSE
        CLS
    END IF

    FOR i = 0 TO (sortlistsize - 1) STEP 1
        IF i = currentpos OR currentpos + 1 = i THEN
            COLOR current_fc, current_bc
        ELSE
            COLOR unsorted_fc, unsorted_bc
        END IF

        PRINT sortlist(i);
        COLOR 0, 0
        PRINT " ";
    NEXT i

    LOCATE row + 2, col
    COLOR 14, 0
    PRINT "Passes: ";
    PRINT passes;
    PRINT "Swaps: ";
    PRINT swaps;

WEND

And after printing out the stats, we increment the variable currentpos and then check if it has reached the end of the list. If so, we start tracking stats. The variable passes is incremented because getting to the end of the list counts as one pass completed. If no swaps have taken place during the entire pass, this means the list is sorted and the variable sorted can be set to 1. Otherwise, the number of swaps in this current pass is added to the running total.
    LOCATE row + 2, col
    COLOR 14, 0
    PRINT "Passes: ";
    PRINT passes;
    PRINT "Swaps: ";
    PRINT swaps;

    currentpos = currentpos + 1

    IF currentpos = sortlistsize - 1 THEN
        currentpos = 0
        passes = passes + 1

        IF swaps = 0 THEN
            sorted = 1
        ELSE
            totalswaps = totalswaps + swaps
        END IF

        swaps = 0
    END IF

WEND

So now we finally have a condition where the While loop can be exited. The next segment of code clears the screen in black and prints out the entire sort list, sans current marker.
WEND

COLOR 0, 0
CLS
LOCATE row, col

FOR i = 0 TO (sortlistsize - 1) STEP 1
    COLOR unsorted_fc, unsorted_bc
    PRINT sortlist(i);
    COLOR 0, 0
    PRINT " ";
NEXT i

And then we print out more stats to show how many passes and swaps it took to sort the list.
FOR i = 0 TO (sortlistsize - 1) STEP 1
    COLOR unsorted_fc, unsorted_bc
    PRINT sortlist(i);
    COLOR 0, 0
    PRINT " ";
NEXT i

LOCATE row + 2, col
COLOR 14, 0
PRINT "SORTED WITH PASSES: ";
PRINT passes;
LOCATE row + 3, col
PRINT "TOTAL SWAPS: ";
PRINT totalswaps; s

Enjoy your sorting! Run your code. Enter in the size of your list followed by the numbers, then watch the magic unfold.




Note! 

List sizes of 1 and 0 are automatically considered sorted. However, this has not been handled in the code, so if you try it, you're likely to break something. I just want to point out that this isn't a great practice as far as programming goes. I figure we can get away with it today because we're just messing about with QBasic and a Bubble Sort. Very trivial stuff.

Thanks, guys. I had a great time. Sort of.
T___T

Tuesday, 28 June 2016

Spot The Bug: SQL weirdness

It's time for Spot The Bug again, so get your game face on.

I'm looking at you,
buster.

I was working on a very bare-bones login procedure. Quick, dirty, throw-away code. The front-end was in HTML, back-end in PHP and interfacing with a MySQL database. Upon clicking the Login button, the login.php script would fire off and query the database. The query returns the number of records that match the email address and password. If there is exactly one match, you're logged in. And if not, you have to try again.

Sounds simple enough. I must've done it a million times. But somehow, I just could not get a match. I always got the message "No such user found.". No syntax errors were in my PHP script.

But then I discovered something really strange with the query. This is the original code from login.php.
<?php
include "inc/config.php";

$login = $POST["txtLogin"];
$password = $POST["txtPassword"];

if (db_connect()->connect_errno)
{
    die("Database connection error.");
}
else
{
    $DBConn=db_connect();

    $strsql="SELECT COUNT(user_id) FROM tb_users WHERE user_login LIKE ? AND  user_password=MD5(?)";

    $sqlresult = $DBConn->prepare($strsql);

    if (!$sqlresult)
    {
        die("Database query error.");
    }
    else
    {
        $sqlresult->bind_param("ss",$login,$password);

        $sqlresult->execute();
        $sqlresult->bind_result($num);
        $sqlresult->fetch();

        if ($num==1)
        {
             //Logged in
        }
        else
        {
             die("No such user found.");
        }
    }

    $DBConn->close();
}
?>

When I changed it, like so, I got a match!
<?php
include "inc/config.php";

$login = $POST["txtLogin"];
$password = $POST["txtPassword"];

if (db_connect()->connect_errno)
{
    die("Database connection error.");
}
else
{
    $DBConn=db_connect();

    $strsql="SELECT COUNT(user_id) FROM tb_users WHERE user_login = ? AND  user_password=MD5(?)";

    $sqlresult = $DBConn->prepare($strsql);

    if (!$sqlresult)
    {
        die("Database query error.");
    }
    else
    {
        $sqlresult->bind_param("ss",$login,$password);

        $sqlresult->execute();
        $sqlresult->bind_result($num);
        $sqlresult->fetch();

        if ($num==1)
        {
              //Logged in
        }
        else
        {
              die("No such user found.");
        }
    }

    $DBConn->close();
}
?>

But it is not enough that things work; sometimes it is just as important to know why they are working. A SQL LIKE is supposed to be more flexible than the "=" operator. So why was the "=" operator matching while the LIKE wasn't?

What went wrong

The problem wasn't in my output. It was in my input. Apparently, while entering the email address in my HTML form, I had accidentally included a trailing space. Instead of "teochewthunder@gmail.com", the input was "teochewthunder@gmail.com ". And this had resulted in the SQL query returning a false positive. The Microsoft Knowledge Base provides a decent explanation. Apparently SQL Server suffers from the same quirk as MySQL. (https://support.microsoft.com/en-us/kb/316626).


How I fixed it

The problem arose because, unlike all the other times I had built this seemingly simple module, I had not written a utility to sanitize the input first. Which included eliminating leading and trailing spaces! So after I added this to my PHP script, it ran like a charm. It was a basic sanitization function using PHP's trim() function to remove leading and trailing spaces.
<?php
include "inc/config.php";

$login = sanitize($POST["txtLogin"]);
$password = sanitize($POST["txtPassword"]);

if (db_connect()->connect_errno)
{
    die("Database connection error.");
}
else
{
    $DBConn=db_connect();

    $strsql="SELECT COUNT(user_id) FROM tb_users WHERE user_login LIKE ? AND  user_password=MD5(?)";

    $sqlresult = $DBConn->prepare($strsql);

    if (!$sqlresult)
    {
        die("Database query error.");
    }
    else
    {
        $sqlresult->bind_param("ss",$login,$password);

        $sqlresult->execute();
        $sqlresult->bind_result($num);
        $sqlresult->fetch();

        if ($num==1)
        {
              //Logged in
        }
        else
        {
              die("No such user found.");
        }
    }

    $DBConn->close();
}

function sanitize($input)
{
    $temp=$input;
    $temp=trim($temp," ");

    return $temp;
}

?>


Moral of the story

Two things to take away from this.

1) Always, always sanitize user input. If I, the developer, can make such an elementary mistake on my own bloody form, who knows what the typical clueless end-user is capable of?

2) Beware of the SQL LIKE and "=" operator. They don't always function the way you'd expect.

That was, like, totally bogus, dudes.
T___T

Thursday, 23 June 2016

Time You're Paying For

It was early days in my career when I wound up working for a friend. He was a good buddy, and we'd had great times together. People warned me that working for a friend was not the best idea. It wasn't that I didn't listen - while I acknowledged the possible pitfalls, I had simply decided to take my chances, and let the dice fall where they may.

Working for him was a mixed bag - we still hung out, we had great synergy where generating ideas was concerned, and we genuinely enjoyed each other's company. There were moments of friction, of course, but nothing to worry about.

Until one time, on a Sunday. He had asked another buddy and I to help him fix some Ikea DIY furniture in the office, as a personal favor. But he had forgotten to bring the tools, so there was a bit of delay while we journeyed to his home to grab them.

Me: We're wasting time here...

Him: I'm wasting his time. Yours, I'm paying for. So it doesn't matter for you.

Notwithstanding that it was a Sunday, I let that one go. What was a bit of ribbing between friends?

Then it happened again.

There was an argument that resulted because I had been holding on to one available set of keys to the office when I went out for a quick late night bite while putting in some extra hours. He had been counting on the office being open when he arrived, and was annoyed that he had to drive down to where I was, to get those keys. Next day, he brought the issue up.

Me: You could have saved yourself a lot of time and trouble by bringing your own keys, you know.

Him: I'm paying for your time, and you're not paying for mine. Therefore, my time's more important than yours.

Again, he meant it as a cheeky remark, but it stung. And it was probably at this moment that I found myself not wanting to work for this guy anymore.

Paying for time.

Sure, we were friends. And as friends who happened to share an employer-employee relationship, there were things we said to each other on a daily basis that would be anathema to most other relationships of this ilk. But he had, perhaps unwittingly, crossed a line for me. I am a professional. People pay for my time, my service and my expertise. And if you do not respect my time, you do not respect me. And if you are paying for my time and fail to respect my time, on some level, you fail to respect yourself. It was a flippant and irresponsible remark to make.

Years later, I was working for someone else. I was on holiday in Malaysia, high up in the misty mountains of Genting. My companion (old flame, long story) had gone to try her luck in her casino and I was chilling (quite literally, brrr) at the Starbucks cafe taking advantage of free WiFi over a steaming mug of hazelnut coffee, when I decided to log on to MSN Messenger and update my boss over the status of some project.

Me: So the interface is done. The new features they're asking for are also done, but I haven't activated them yet. So if they want it right away, all you need to do is set that flag in the database.

Him: Yep. Aren't you supposed to be on holiday?

Me: Yes.

Him: Then I don't want to hear from you till you get back.

That contrast in attitude really hit home for me. This boss wasn't my friend. But he knew to respect the time of his employees. It was then I knew my choice was justified.

Employees have lives outside of the office. And any employer who fails to appreciate that, isn't worth doggy-doo.

Thanks for your time,
T___T


Monday, 20 June 2016

No Internet for Public Sector?

Come July 2017, all public service computers will be detached from the internet.

Much has been made of the hubbub surrounding Infocomm Development Authority of Singapore (IDA)'s controversial decision last week. Shots have been fired, and a new waves of memes circulated.

Courtesy of SGAG

I have refrained from commenting thus far because it is way too easy to get caught up in criticizing the Government (not that they don't deserve it) before one has all the facts. And because - bear with me here - this might not be as crazy as it sounds.

Public servants still get to use the Internet - through use of their personal handheld devices and specially designated workstations. The aim is to keep the Internet separate from the government's Intranet, which doubtlessly contains all manner of data no mere mortal should ever access.

The technique in question here is called "air-gapping", and it is not in the least new. Singapore's certainly not the first nation in the world to do this.

The public fallout was fairly predictable. There were cries of derision pointing to Prime Minister Lee Hsien Loong's speech about a Smart Nation last year.  There were concerns about what such a drastic gesture might mean for those in Public Service. And noticeable silence from pro-Government fanboys who probably quietly worried if their idols were going bonkers. Laymen were, for the most part, appalled.

Reactions of those in the IT Sector were somewhat mixed, with some lambasting the Government for "stupidity", others reacting with approval. But from those in tech security, I have seen little comment. Were they, like myself, reserving judgment? Or did they know something the rest of us don't?

This brought to mind an episode I recently had with a hacker buddy. I had set up a website and was boasting about the security features I had set in place, and the cheeky fellow had bet me fifty bucks that he could bypass them all to hack my database. I upped the ante to a hundred bucks, and it was on. Within a couple hours, my friend admitted defeat and asked me what database I was using.

"None," was my innocent response. "This is a static website."

The murderous look on his face is something I remember fondly.

But isn't that the essence of all zen-like chop-cocky martial arts flicks? Meet an incoming force, not with resistance, but with emptiness. The Empty City Strategem. And all that crap.

Feel that emptiness yet?

Long story short - if you know it's a game you can't win, make sure you don't ever need to play that game. Which is why aging footballers leave the Premier League and go to MLS - physically, they can't compete on stamina and strength with their younger counterparts any more. Which is why I never negotiate during a job interview - not only is haggling beneath me, I cannot hope to out-haggle professionals whose main skillset is haggling. Play the game you're good at. Don't play the game you will lose.

Because cyber-security is a game one is doomed to lose at some point or other. You can only defend against an existing and known threat. Which means the threat will always exist before the solution does. Which, in turn, means that you're always playing catch-up. Bear in mind that it is possible for a threat to exist long before it makes itself known - probably by compromising some poor sod we hopefully don't care about.

How about other measures?

Of course, there are a multitude of other measures the Government could take - firewalls, domain whitelists and blacklists, user training, to name a few.

But no one is suggesting that these not be in place. By themselves, all these security measures come with their own limitations. They should be in place as complementary measures, rather than primary ones.

What's your position on this?

Whoops. Rambled on a bit there, didn't I?

So, I think the Government might have something there. And the fact that they will be taking at least one year to fully implement it, shows that this decision was not made in haste.

Not that this will stop people from making fun of the Government, mind you. Neither should they. What use is a Democracy if you can't mock the people in charge, whether or not it's deserved? Jeer away. Have fun doing it! But don't imagine for a moment that buying an anti-virus software license once every year makes you some kind of expert on the matter. That is exactly the kind of mentality that gets systems compromised.

The Million Dollar Question

This is an extreme move, which will no doubt cause loads of inconvenience to employees in the Public sector. Why now? What threats has the Government encountered, to even contemplate such a move and deem it necessary?

I would prefer to trust that the Government knows what it's doing. Because the alternative is just too awful to contemplate.


Stay safe,
T___T

Wednesday, 15 June 2016

Web Tutorial: The Scrooge Expense Tracker Mobile App (Part 4/4)

Welcome to the final installment of this web tutorial. In this, we'll be putting up the final screen of the Scrooge app.

This screen is not on featured any of the links in the footers of the previous screens. In fact, it's accessible only via the "entries" screen we set up in the previous part of this web tutorial. For this, we'll be copying the HTML template for the "settings" page, and renaming it "dailyentry".

        <div data-role="page" id="settings" data-theme="b">
            <div data-role="header" data-position="fixed">
            SETTINGS
            </div>

            <div data-role="main" class="ui-content">
                <div data-role="fieldcontainer">
                <label for="txtA">Category A: </label>
                <input id="txtA_name" name="txtA" placeholder="Enter title...">
            </div>
            <div data-role="fieldcontainer">
                <label for="txtB">Category B: </label>
                <input id="txtB_name" name="txtB" placeholder="Enter title...">
            </div>
            <div data-role="fieldcontainer">
                <label for="txtC">Category C: </label>
                <input id="txtC_name" name="txtC" placeholder="Enter title...">
            </div>
            <div data-role="fieldcontainer">
                <label for="txtD">Category D: </label>
                <input id="txtD_name" name="txtD" placeholder="Enter title...">
                </div>
            <div data-role="fieldcontainer">
                <label for="txtD">Monthly Budget: </label>
                <input id="txtBudget" name="txtD" placeholder="Enter amount..." onchange="fixinput(this,2);">
                </div>
            <div data-role="fieldcontainer">
                <label for="txtD">Interval (days): </label>
                <input id="txtInterval" name="txtD" placeholder="Enter days..." onchange="fixinput(this,0);">
                </div>
            <a href="#settings" data-role="button" data-icon="edit" onclick="update_settings();">Edit</a>
            </div>

            <div data-role="footer" data-position="fixed">
                <a href="#selectdate" data-icon="info">Select Month</a>
                <a href="#entries" data-icon="grid">Entries</a>
                <a href="#settings" data-icon="gear">Settings</a>
            </div>
        </div>

        <div data-role="page" id="dailyentry" data-theme="b">
            <div data-role="header" data-position="fixed">
               
            </div>

            <div data-role="main" class="ui-content">               

            </div>

            <div data-role="footer" data-position="fixed">
                <a href="#selectdate" data-icon="info">Select Month</a>
                <a href="#entries" data-icon="grid">Entries</a>
                <a href="#settings" data-icon="gear">Settings</a>
            </div>
        </div>


Now fill it in with the following code. You'll notice it's pretty similar to the code we did for the "settings" page, except that for categories A, B, C and D, we'll be entering in numbers instead of text. Hence each of these fields will trigger the fixinput() function when changed. When the edit button is clicked, the update_dailyentry() function is fired off. The pnlDay span element is used to display the current day the user is editing.
        <div data-role="page" id="dailyentry" data-theme="b">
            <div data-role="header" data-position="fixed">
                DAILY ENTRY for <span id="pnlDay"></span>
            </div>

            <div data-role="main" class="ui-content">               
                <div data-role="fieldcontainer">
                    <label for="txtA" id="lblA">Category A: </label>
                    <input id="txtA_amount" name="txtA" placeholder="Enter amount..." onchange="fixinput(this,2);">
                </div>
                <div data-role="fieldcontainer">
                    <label for="txtB" id="lblB">Category B: </label>
                    <input id="txtB_amount" name="txtB" placeholder="Enter amount..." onchange="fixinput(this,2);">
                </div>
                <div data-role="fieldcontainer">
                    <label for="txtC" id="lblC">Category C: </label>
                    <input id="txtC_amount" name="txtC" placeholder="Enter amount..." onchange="fixinput(this,2);">
                </div>
                <div data-role="fieldcontainer">
                    <label for="txtD" id="lblD">Category D: </label>
                    <input id="txtD_amount" name="txtD" placeholder="Enter amount..." onchange="fixinput(this,2);">
                </div>
                <div data-role="fieldcontainer">
                    <label for="txtD" id="lblD">Comments: </label>
                    <textarea id="txtComments" name="txtComments" placeholder="Enter comment..."></textarea>
                </div>
                <a href="#dailyentry" data-role="button" data-icon="edit" onclick="update_dailyentry();">Edit</a>
            </div>
           
            <div data-role="footer" data-position="fixed">
                <a href="#selectdate" data-icon="info">Select Month</a>
                <a href="#entries" data-icon="grid">Entries</a>
                <a href="#settings" data-icon="gear">Settings</a>
            </div>
        </div>


So let's see what the screen looks like. You will access this page from the table links in the "entries" page. Looks all right, except that data needs to be pre-filled in.


This is what we'll be doing next. Set up the code below. Firstly, the pnlDay span element is populated with the date of the day that data is being entered for. This is derived from the glb_currentyear, glb_currentmonth and glb_currentday global variables. The glb_currentday variabe was set in the goto_dailyentry() function prior to redirection to the "dailyentry" screen.
                $(document).on("pagebeforeshow", "#settings", function(event, data)
                {
                    $("#txtA_name").val(glb_settings[0].name);
                    $("#txtB_name").val(glb_settings[1].name);
                    $("#txtC_name").val(glb_settings[2].name);
                    $("#txtD_name").val(glb_settings[3].name);
                    $("#txtBudget").val(glb_settings[4].budget);
                    $("#txtInterval").val(glb_settings[5].interval);
                });

                $(document).on("pagebeforeshow", "#dailyentry", function(event, data) 
                {
                    var showdate=new Date(glb_currentyear,glb_currentmonth-1,glb_currentday,0,0,0,0);
                    $("#pnlDay").html(showdate.toDateString());
                });

                function fixinput(varobj,vardec)
                {
                    if (isNaN(varobj.value)||varobj.value.trim()=="")
                    {
                        varobj.value=(0).toFixed(vardec);
                    }
                    else
                    {
                        varobj.value=parseFloat(varobj.value).toFixed(vardec);
                    }
                }


OK, so take a look now. The day has been filled in nicely.


Next, we take the values of the glb_settings global array and apply them to the labels lblA, lblB, lblC and lblD. So instead of saying "Category A" and so on, the screen will display all the values you entered in the "settings" screen.
                $(document).on("pagebeforeshow", "#dailyentry", function(event, data)
                {
                    var showdate=new Date(glb_currentyear,glb_currentmonth-1,glb_currentday,0,0,0,0);
                    $("#pnlDay").html(showdate.toDateString());

                    $("#lblA").html(glb_settings[0].name);
                    $("#lblB").html(glb_settings[1].name);
                    $("#lblC").html(glb_settings[2].name);
                    $("#lblD").html(glb_settings[3].name);
                });


Awesome, right?


Next, we populate the textboxes using the values found in the archive for this particular date. Since you haven't entered anything, all of the values will be "0.00".
                $(document).on("pagebeforeshow", "#dailyentry", function(event, data)
                {
                    var showdate=new Date(glb_currentyear,glb_currentmonth-1,glb_currentday,0,0,0,0);
                    $("#pnlDay").html(showdate.toDateString());

                    $("#lblA").html(glb_settings[0].name);
                    $("#lblB").html(glb_settings[1].name);
                    $("#lblC").html(glb_settings[2].name);
                    $("#lblD").html(glb_settings[3].name);

                    dayindex=glb_currentday-1;

                    $("#txtA_amount").val(glb_archive[dayindex].A);
                    $("#txtB_amount").val(glb_archive[dayindex].B);
                    $("#txtC_amount").val(glb_archive[dayindex].C);
                    $("#txtD_amount").val(glb_archive[dayindex].D);
                    $("#txtComments").val(glb_archive[dayindex].comments);
                });


This is what you should see now.


Finally, it's time to create the update_dailyentry() function.
        function update_dailyentry()
        {

        }

        function update_settings()
        {
            glb_settings[0].name=$("#txtA_name").val();
            glb_settings[1].name=$("#txtB_name").val();
            glb_settings[2].name=$("#txtC_name").val();
            glb_settings[3].name=$("#txtD_name").val();
            glb_settings[4].budget=parseFloat($("#txtBudget").val()).toFixed(2);
            glb_settings[5].interval=$("#txtInterval").val();

            localStorage.setItem("scrooge_settings", JSON.stringify(glb_settings));

            alert ("Settings saved.");
        }


Not too much to explain here. We take the values of each of the textboxes, cast them into floating point values with two decimal places, and save them into the glb_archive array where we got the values from in the first place. The txtComments textbox too, of course. Then we turn the array into a JSON string using the stringify() method and overwrite the corresponding localStorage value using the setItem() method. And of course, a nice popup tells the user that the data is saved.
        function update_dailyentry()
        {
            dayindex=glb_currentday-1;

            glb_archive[dayindex].A=parseFloat($("#txtA_amount").val()).toFixed(2);
            glb_archive[dayindex].B=parseFloat($("#txtB_amount").val()).toFixed(2);
            glb_archive[dayindex].C=parseFloat($("#txtC_amount").val()).toFixed(2);
            glb_archive[dayindex].D=parseFloat($("#txtD_amount").val()).toFixed(2);
            glb_archive[dayindex].comments=$("#txtComments").val();

            localStorage.setItem("archive_" + glb_currentyear + "_" + glb_currentmonth, JSON.stringify(glb_archive));

            alert ("Daily entry saved.");
        }


Try it!


Check out the changes in your localStorage.



Now, go to your "entries" page. What do you see?


Now keep filling in values for the other days. Then go back to the "entries" page. Do your numbers add up? Now it's time to test the Delete button on your "selectmonth" screen. Click it. Does the archive in the localStorage disappear? Now go back to your "entries" page. Is everything back to "0.00"?

The Scrooge app is complete!

Congratulations, you have a web app, which you can now export into a hybrid mobile app by following these instructions.

Post mortem

This is my virgin attempt at explaining how to create a HTML5-based app for mobile. As such, I'd like to explain a few design decisions I made.

The use of global variables - normally this is frowned on and not really considered "good" programming. Well, it was either use global variables, or install a plug-in to pass variables from one screen to another. The latter seemed totally unnecessary because this app technically consists of one single file. Also, I wanted to keep things as simple as possible for a web tutorial, and including a plug-in seemed counter-intuitive to that.

Lack of structure and coherence - unlike earlier web tutorials where almost every question was addressed right away, there were controls and functions on many screens that had to be explained much later while we dealt with more pressing issues. This is because it's not a single program - it's an app with many moving parts integrated together. And as such, I had to choose what to explain first and what to explain later. It could be because I suck - if that's the case, I can only get better with practice.

Some of the screenshots, especially in the latter parts of this web tutorial, look different. That's because this web tutorial took a while to complete. In between the intervals, the web browser I use for testing and screenshots - Google Chrome - got updated.

This app will be useful. I'd put money on it!
T___T