PHP test

I'm studying for the Zend PHP Certification and to help with that I decided to create a couple of small tools to practice. My first target was operators, specifically their precedence, because with 21 levels of precedence it can be a bit hard to remember which ones are executed first in a given expression.

Hence, I put together some HTML5, a bit of Javascript with jQuery, some CSS using Twitter Bootstrap - and voila, plind.dk/php-quiz/ was born. The quizzes are very simple, yet I think they work quite fine in terms of practicing the knowledge.

Order the scrambled info

The first quiz is a simple list of all the operator levels - each level displays the operators on it. Start the quiz and the list gets scrambled and you then have to sort it. When done you check your results.

It's fascinating to me how easy it is to create a test like this. All you need is: your list, a button for interaction (start, stop), a bit of help from jQuery (detaching the list elements, sorting them using Math.random(), reattaching them, allowing for list rearrangement with jQuery.sortable(), and of course checking the result with a loop through the elements) and some layouting/design, which is made easy with Twitter Bootstrap.

Scrambling the list:

items = this.list.children().css('background-color', 'transparent').detach();
items.sort(function(a, b) {
  return Math.random() > 0.5 ? 1 : 0;
});

this.list.append(items);

Checking the sorted list:

this.list.children().each(function(idx, item) {
  var self = $(this);
  if (idx == parseInt(self.data('index'), 10) - 1) {
    self.css('background-color', '#afa');
  } else {
    self.css('background-color', '#faa');
    failed = true;
  }
});

Apart from that, there's just a bit of bookkeeping for the scoreboard and some setup - nothing else. It took less than an hour to write the test itself - and now I have something that I can easily use to train my memory of operator precedence.

Pick the higher precedence

The second test I wrote was as easy or easier still to make. Create two boxes with content, user has to choose the box representing the higher precedence. At first I set an entire operator precedence level as content but then thought better of it and just set it to a single operator - so you're comparing two operators against each other. They will always be of different precedence which makes it slightly easier - but also means I don't have to do extra checks on associativity.

The javascript for setting up the test and for checking it is slightly more complex than for the first test. Setup:

var a = Math.floor(Math.random() * 20),
  b,
  temp;

  do {
    b = Math.floor(Math.random() * 20);
  } while (a == b);

  temp = this.operators[a].split(/, /);
  temp = temp.length > 1 ? temp[Math.floor(Math.random() * temp.length)] : temp[0];

  this.test_a.html(temp)
    .attr('data-index', a);

  temp = this.operators[b].split(/, /);
  temp = temp.length > 1 ? temp[Math.floor(Math.random() * temp.length)] : temp[0];

  this.test_b.html(temp)
    .attr('data-index', b);

  this.current_winner = a < b ? a : b;

The code for checking the result:

  window.setTimeout(function() {
    that.reset();
  }, 1500);

  if (parseInt(element.attr('data-index'), 10) == this.current_winner) {
    element.css('background-color', '#afa');
    this.times_correct++;
  } else {
    element.css('background-color', '#faa');

    if (element.prop('id') == 'test_a') {
      this.test_b.css('background-color', '#afa');
    } else {
      this.test_a.css('background-color', '#afa');
    }
  }

As should be very obvious, there's really very little involved in making this. Yet at the same time it's a dynamic quiz: I can keep going over it and I won't know in advance exactly what I will be asked. In that sense it works a whole lot better than a static multiple-choice quiz.

Calculate the expression

The latest test I've put in place so far is about reasoning through an expression with different operators. The code for it is again more complex - this time mainly because sticking an expression together that is valid is more problematic than comparing different operator precedence levels.

Putting together the expression looks like:

        setCalculation: function() {
            var limit = this.operator_limit,
                operator,
                last_operator,
                expression = Math.floor(Math.random() * 10),
                last_seen_unary,
                use_ternary = false,
                can_use_non_ranged_comparison = true,
                can_use_ranged_comparison = true,
                operators = operator_test_module.expression_generator_operators;

            while (limit) {
                operator = operators[Math.floor(Math.random() * operators.length)];

                if (!this.use_bool && operator_test_module.isBool(operator)) {
                    continue;
                }

                if (last_operator && operator_test_module.isNonAssociative(last_operator) && operator_test_module.isNonAssociative(operator)) {
                    continue;
                }

                if (operator_test_module.isUnary(operator)) {
                    last_seen_unary = operator;
                    continue;
                }

                if (operator === '? :') {
                    use_ternary = true;
                    continue;
                }

                if (operator_test_module.isNonRangedComparison(operator)) {
                    if (!can_use_non_ranged_comparison) {
                        continue;
                    } else {
                        can_use_non_ranged_comparison = false;
                    }
                } else if (operator_test_module.isRangedComparison(operator)) {
                    if (!can_use_ranged_comparison) {
                        continue;
                    } else {
                        can_use_ranged_comparison = false;
                    }
                }

                expression += ' ' + operator + ' ';

                if (last_seen_unary) {
                    expression += last_seen_unary + ' ';
                    last_seen_unary = null;
                }

                expression += Math.floor(Math.random() * 10);

                limit--;
            }

            this.current_expression = expression;
            this.expression.text(this.current_expression);
        },

Problem one is that some of the operators don't play well together - you can only have one ranged comparison operator in an expression, and only one non-ranged comparison operator. Also, sticking binary operators in an expression is easy - just put one between literals. But you also need unary operators which should be prefixed to the literals. Then, obviously, there's the ternary operator - which is not yet part of the above code, mainly because of the added complexity.

Checking it is yet again another story. You could likely piece together an equivalent expression for javascript, using parentheses to get the correct precedence. That, however, is way too much work, when you can just send the expression to a php script and have it evaluated against the answer given. This, of course, results in a security problem: you need to make sure that only proper expressions can be sent to the testing script. And that was what led to this regex:

#^\\d+ ( |< |<= |>= |> |== |=== |!= |!== |<> |\\* |/ |% |\\+ |- |<< |>> |& |\\^ |\\| |&& |\\|\\| |and |xor |or |~ |\\(bool\\) |\\(int\\) |\\(string\\) |\\(float\\) |\\(unset\\) |! |\\d+)+$#

It essentially tests for the various possible operators and number literals - with no other input excepted, to make sure the eval() function of php is not exploited.

After testing the expression generator a bit, I also realized you need to be able to modify the expressions. There are two main factors: length and operator type. Obviously, longer expressions become harder to follow, however, the longer the expression the bigger the chance you'll see and 'and', 'or' or 'xor' expression in there - and since they run last, the result will be one of true or false. In fact, with a length of 5 literals, more than half the expressions evaluate to true or false - and in the long run, it's just not as interesting to just guess at true or false, as it is to see if you can follow the operators.

Conclusion

Writing these tests provide pretty awesome experience in and of itself. While I knew the operators before as well, they're a lot closer to heart (and memory) now. Also, it's really interesting to try out your skills at something so easy yet rewarding - you see quickly how it's possible to make something of value. It's one of the things I'm often missing from my language learning experiences: proper projects to make.

social