views:

188

answers:

5

Hi, recently I had a job interview. I had two tasks:

1) to refactor a JavaScript code

// The library 'jsUtil' has a public function that compares 2 arrays, returning true if
// they're the same. Refactor it so it's more robust, performs better and is easier to maintain.
/**
  @name jsUtil.arraysSame
  @description Compares 2 arrays, returns true if both are comprised of the same items, in the same order
  @param {Object[]} a Array to compare with
  @param {Object[]} b Array to compare to
  @returns {boolean} true if both contain the same items, otherwise false
  @example
  if ( jsUtil.arraysSame( [1, 2, 3], [1, 2, 3] ) ) {
    alert('Arrays are the same!');
  }
*/
// assume jsUtil is an object

jsUtil.arraysSame = function(a, b) {
  var r = 1;
  for (i in a) if ( a[i] != b[i] ) r = 0;
    else continue;
    return r;
}

2) To refactor a PHP function that checks for a leap year

<?php
/*
  The class 'DateUtil' defines a method that takes a date in the format DD/MM/YYYY, extracts the year
  and works out if it is a leap year. The code is poorly written. Refactor it so that it is more robust
  and easier to maintain in the future.

  Hint: a year is a leap year if it is evenly divisible by 4, unless it is also evenly
  divisible by 100 and not by 400.
*/

class DateUtil {
    function notLeapYear ($var) {
        $var = substr($var, 6, 4);
        if (! ($var % 100) && $var % 400) {
            return 1;
        }
        return $var % 4;
    }
}


$testDates = array('03/12/2000', '01/04/2001', '28/01/2004', '29/11/2200');

/* the expected result is
* 03/12/2000 falls in a leap year
* 01/04/2001 does not fall in a leap year
* 28/01/2004 falls in a leap year
* 29/11/2200 does not fall in a leap year
*/
?>

<? $dateUtil = new DateUtil(); ?>
<ul>
  <? foreach ($testDates as $date) { ?>
    <li><?= $date ?> <?= ($dateUtil->notLeapYear($date) ? 'does not fall' : 'falls') ?> in a leap year</li>
  <? } ?>
</ul>

I think I cope with the task but I am not quite sure, I still don't have an answer from them and it's been about a week. Could you give an example of your approach to this tasks? I'd really appreciate. Later I can post my solutions/code.

OK here are my answers to the questions.

<?php // Always use full/long openning tags not 

$start = microtime(true);

class DateUtil {

    /**
     * The date could be used in other 
     * class methods in the future.
     * Use just internally.
     **/
    var $_date; 

    /**
     * The constructor of the class takes
     * 1 argument, date, as a string and sets
     * the object parameter _date to be used
     * internally. This is compatable only in PHP5
     * for PHP4 should be replaced with function DateUtil(...)
     */
    public function __construct( $date = '' ) {
        $this->_date = $date;
    }

    /**
     * Setter for the date. Currently not used.
     * Also we could use _set magical function.
     * for PHP5.
    **/
    public function setDate( $date = '' ) {
        $this->_date = $date;
    }

    /**
     * Gettre of the date. Currently not used.
     * Also we could use _get magical function.
     * for PHP5.
    **/
    public function getDate() {
        return $this->_date;
    }

    public function isLeapYear( $year = '' ) {
        // all leap years can be divided through 4
        if (($year % 4) != 0) {
            return false;
        }

        // all leap years can be divided through 400
        if ($year % 400 == 0) {
            return true;
        } else if ($year % 100 == 0) {
            return false;
        }

        return true;
    }
}

$dates = array('03/12/2000', '01/04/2001', '30/01/2004', '29/11/2200');
$dateUtil = new DateUtil();

foreach($dates as $date) {
    /** 
     * This processing is not done in the class
     * because the date format could be different in 
     * other cases so we cannot assume this will allways 
     * be the format of the date
     * 
     * The php function strtotime() was not used due to 
     * a bug called 2K38, more specifically dates after and 2038
     * are not parsed correctly due to the format of the UNIX 
     * timestamp which is 32bit integer.
     * If the years we use are higher than 1970 and lower
     * than 2038 we can use date('L', strtotime($date));
    **/
    $year = explode('/', $date);
    $year = $year[2];
    $isLeap = $dateUtil->isLeapYear($year);

    echo '<pre>' . $date . ' - ';
    echo ($isLeap)? 'Is leap year': 'Is not leap year';
    echo '</pre>';
}

echo 'The execution took: ' . (microtime(true) - $start) . ' sec';
?>

JavaScript

/***************************************************/

jsUtil = new Object();

jsUtil.arraysSame = function(a, b) {


    if( typeof(a) != 'object') {
        // Check if tepes of 'a' is object
        return false;
    } else if(typeof(a) != typeof(b)) {
        // Check if tepes of 'a' and 'b' are same
        return false;
    } else if(a.length != b.length) {
        // Check if arrays have different length if yes return false
        return false;
    }

    for(var i in a) {
        // We compare each element not only by value
        // but also by type so 3 != '3'
        if(a[i] !== b[i]) {
            return false;
        }
    }

    return true;
}

// It will work with associative arrays too
var a = {a:1, b:2, c:3};
var b = {a:1, b:2, c:3};    // true
var c = {a:1, b:2, g:3};    // false
var d = {a:1, b:2, c:'3'};  // false

var output = '';

output += 'Arrays a==b is: ' + jsUtil.arraysSame( a, b );
output += '\n';
output += 'Arrays a==c is: ' + jsUtil.arraysSame( a, c );
output += '\n';
output += 'Arrays a==d is: ' + jsUtil.arraysSame( a, d );

alert(output);
A: 

For the first problem maybe I can help you with this:

var jsUtil = jsUtil || {};

jsUtil.arraysSame = function(a, b){
    //both must be arrays
    if (!a instanceof Array || !b instanceof Array) {
        return false;
    }
    //both must have the same size
    if (a.length !== b.length) {
        return false;
    }

    var isEquals = true;
    for (var i = 0, j = a.length; i < j; i++) {
        if (a[i] !== b[i]) {

            isEquals = false;
            i = j; //don't use break
        }
    }
    return isEquals;
}

I included type checking and made the things more clear.

madeinstefano
*sniff*...hmm, smells like Crockford.
MooGoo
Out of interest, why do you not just `return false` if `a[i] != b[i]` within your loop?
Steve Greatrex
Also why not use break?
Jake
@Jake maybe because of this? "@returns {boolean} true if both contain the same items, otherwise false"
orolo
@orolo I think we're talking about different things. If you use "return false" or break and then return depends on if you support the "one return point" 'rule' or not, I guess. I meant what is the benefit of using "i=j" instead of just "break"
Jake
@Jake; oops, I think I'm off here; I'm going to retract my comments. Hopefully madeinstefano will post.
orolo
@orolo haha, that means I have to retract mine so it doesnt look like Im talking to myself! No worries, I can see where I wasn't 100% clear what I meant.
Jake
'break', the name says all, is a break on your code and this is not what I want. In that case, I just wanted for end my looping for performance issues.
madeinstefano
"end my looping for performance issues" is exactly what break does.
Stephen P
Yeah, I dont see any benefit whatsoever of using "i=j" unless you're trying to avoid "break" for the sake of it, which isn't really a benefit at all...
Jake
Using `break` would theoretically yield better performance, as the final `i < j` comparison need not be performed. As I said, this stinks of Crockford.
MooGoo
@MooGoo: I thought as much and did a tiny test before you posted in order to double check. The gain is incredibly tiny but using break is indeed faster :P I think you're right on the Crockford front.
Jake
A: 

In my opinion using built-in predefined functions is always your best bet.

1) Use a function that converts the arrays into strings. There are many of these available and depending on which library you are already using you may want to use different ones. You can find one at Json.org

jsUtil.arraysSame = function(a, b) {

  return JSON.stringify(a) == JSON.stringify(b);
}

2) USe PHP's built in date function and strtotime

class DateUtil {
    function notLeapYear ($var) {
        return (date( 'L', strtotime( $var)) == "0");
    }
}
Jon Snyder
Not a good idea. Try `a = [{}]`, `b = [{}]`. They are not the same array but your function will say they are.
Tim Down
strtotime doesn't work. Try strtotime('29/11/2200');Y2K38 problem (UNIX timestamp is 32bits)
infinity
This is actually not the same as the original JS function, which returned true for arraySame([1],['1']) (which might or might not be wanted). More importantly, it won't handle arrays of objects well.
Tgr
Your JS solution shouldn't rely on non-native libraries.
mway
+3  A: 

For the PHP version:

class DateUtil {
    function LeapYear ($var) {
        $date = DateTime::CreateFromFormat($var, 'd/m/Y');
        return($date->format('L')); // returns 1 for leapyear, 0 otherwise
    }
    function notLeapYear($var) {
        return(!$this->LeapYear($var)) {
    }
}
Marc B
does this works with bigger years than 2038? (the Y2K38 bug for timestamp)
infinity
Nope. PHP dates are standard Unix time_t's internally, which are signed 32bit ints. Once libc moves to 64bit time_t's, then PHP should automatically follow as well. Of course, the original version wasn't Y10k compliant, as it assumed a 4 character year.
Marc B
@infinity @MarcB if in your above example, you mean `DateTime`, [this is incorrect](http://www.php.net/manual/en/intro.datetime.php): "The date and time information is internally stored as an 64-bit number so all imaginable dates (including negative years) are supported. The range is from about 292 billion years in the past to the same in the future."
Pekka
Ah. Phoo, I was going off http://php.net/manual/en/function.date.php, which says it's limited to 1901->2038. Didn't realize DateTime was a whole new beast, and not just an OOP front-end to the old date stuff.
Marc B
@Marc I've corrected the classname but the method name `notLeapYear` is still wrong if you are not negating the return value.
Gordon
@Gordon. Thanks. I *HATE* functions with implied negations in the name.
Marc B
+4  A: 

Iterate arrays using a for loop rather than for...in. If the arrays are different, you want to return as quickly as possible, so start with a length comparison and return immediately you come across an element that differs between the two arrays. Compare them using the strict inequality operator !==. Iterate backwards through the array for speed and to minimise the number of variables required by assigning a's length to i and reusing i as the iteration variable.

This code assumes that the parameters a and b are both supplied and are both Array objects. This seems to be implied by the question.

var jsUtil = jsUtil || {};

jsUtil.arraysSame = function(a, b) {
    var i = a.length;
    if (i != b.length) return false;
    while (i--) {
        if (a[i] !== b[i]) return false;
    }
    return true;
};
Tim Down
Downvoter: please explain. This code is correct and I've explained it.
Tim Down
+1; clear, succinct, minus the caveat of no typechecking. Not really sure why others are having an issue with this. Only thing I'd add is `if(!(a instanceof Array) || !(b instanceof Array)) return false;`.
mway
I believe the `a[i] !== b[i]` handles the typechecking. @Tim, the downvoter may have been me - I accidently hit the wrong arrow the first time, not sure if stackoverflow will alert you to that even after I've retracted it.
Jake
@Jake: I'd noticed the downvote has gone, so thanks for that :)
Tim Down
@mway: the trouble with type-checking is what exactly types mean in a cross-document environment, where each array may have its own different `Array` prototype. If you want to compare arrays that come from different documents you would need a more complicated check. Another JavaScript oddity: `undefined`. Is an explicit array of `[1, undefined, 3]` the same as an array `var a= []; a[0]= 1; a[2]= 3;`? They are detectably different using the `in` operator, but are they the “same”?
bobince
@bobince: Since you're effectively creating a span of indices with no value (undefined) by increasing the length of the array, I'd say that yes, those arrays would be the same. However, contextually speaking, I don't see how checking for `arg instanceof Array` would be an issue, since `Array` would be a reliable check, regardless of prototype, wouldn't it?
mway
@bobince: Just realized you said cross-document. That's probably true, though arguably less common, wouldn't you think?
mway
A: 
  • check inputs (type, range - keep in mind that very old dates used a different calendar); you might use PHP date functions to parse date (more flexibility on one hand, limited to relatively recent dates on the other)
  • never iterate with in in javascript, will fail horribly when prototypes of the standard types have been extended
  • you should clarify what the functions should do; e.g. should the array comparison be recursive? Should it use strict equivalence?
  • you can stop iterating the arrays when the first difference is found. Also, you might want to check if the two refer to the same object before starting to iterate.
  • write unit tests
Tgr