Thank you, Javascript



  • I was having a very, very difficult afternoon.  After over a year of pondering, we finally had an excuse to wedge a date widget into our web-based intranet app.  Unfortunately, the Javascript library that we chose doesn't exactly have a plethora of widget options unless you're also using the underlying platform the library likes.  Yes, I'm talking to you, Prototype.  Stop that, it's annoying.

    The date widget we picked is hardly feature-packed, but it gets the job done well.  Well, rather, it gets the job it was designed for done well.  It wasn't designed to be a real date widget.  It's just a calendar with decent support for doing whatever you want with it, and it even favors YYYY-MM-DD like we already use throughout our app.

    Something it didn't do was automatically change its own date when the user manually edits the date field while the widget is active.  It was an annoying problem with an easy and straightforward fix.  But then I started testing.  Whenever I picked a date in August or September, the widget wouldn't update.  Every other month worked fine.  Deeply confused, I dug in more.  The 8th and 9th of the month also wouldn't update the widgets.  Confused, I dove back into the code.

            var date_bits = element.value.match(/^(\d{4})\-(\d{1,2})\-(\d{1,2})$/);
            var new_date = null;
            if(date_bits && date_bits.length == 4 && parseInt(date_bits[2]) > 0 && parseInt(date_bits[3]) > 0)
                new_date = new Date(parseInt(date_bits[1]), parseInt(date_bits[2]) - 1, parseInt(date_bits[3]));

    '2008-09-01' was coming out of that regex as [ '2008-09-01', '2008', '09', '01' ], so no problem there. 

    parseInt('2008') == 2008, good. 

    parseInt('09') == ...

     If you said 9, you fail.  No, it's zero.  See, Javascript supports octal numbers.  Any number starting with a zero is octal, even if it can't be an actual octal number.  In certain languages, like Perl, trying to use a non-octal number as an octal number results in an error.  In other languages, like Javascript, it silently fails.  

     So, thank you Javascript, for teaching me something I didn't know about you... and making me hate your quirks all the more.



  • Yeah.  parseInt takes a radix as the second argument, so parseInt(date_bits[2], 10) should work as expected, as will Number(date_bits[2])

    I sometimes wonder how a numbering system only commonly used for file mode bits made its way into javascript, but whenever I start to wonder why javascript is the way it is, I remember that the language was designed and implemented in 3 weeks.



  •  This is a well known 'feature' of javascript. It's a WTF, but any lint too would have caught that for you.



  • Wow, what a coincidence! The same thing happened to me, like, two days ago.

    The fix is, like it's been said, to pass the radix to parseInt.



  • Funny, I had a colleague give me the stern "RTFM" comment six or seven years ago over this exact example. The behaviour seems odd if you expect a decimal treatment, but goes to show the value of not assuming anything



  •  And that goes for the assumption that parseInt would return NaN for an invalid input, too.



  • Well now I know.

    Although I kind of like the fact that JavaScript infers that "0xFF" is a hexadecimal number.



  • @Charles Capps said:

    If you said 9, you fail.  No, it's zero.  See, Javascript supports octal numbers.  Any number starting with a zero is octal, even if it can't be an actual octal number.  In certain languages, like Perl, trying to use a non-octal number as an octal number results in an error.  In other languages, like Javascript, it silently fails.
     

    Somewhere, the principle of least surprise weeps silently. 



  •  PHP supports the same notation for integer constants -- but not when casting strings to ints.

     

    e.g.:

     

    lappyx:~ merreborn$ php -r "var_dump(010, (int)'010');"
    int(8)
    int(10)
    lappyx:~ merreborn$ 
    


  • @alunharford said:

     This is a well known 'feature' of javascript. It's a WTF, but any lint too would have caught that for you.

    I know lint is quite powerful, but I want to know how it could have caught a quirk that depends on an untyped variable containing processed user input. Keep in mind that the '09' was nowhere in the code, now was anything that could have turned into a '09'. The only way I can imagine a lint could have caught this one is if it generally showed a warning for every parseInt() call ever used - which would make the function quite useless.

    @nixen said:

    The behaviour seems odd if you expect a decimal treatment, but goes to show the value of not assuming anything.

     

    The irony being of course that the whole WTF only exists because the authors of the language assumed too much
    <rant>
    That's what I don't like about the high-praised "magic" in Perl, Prototype, Ruby on Rails and others. They're full of little tidbits and special rules that are said to make programming easier, because they make the language "know what you mean". The downside is of course that there are so many of them that you can encounter really obscure errors (like this one) if you one time think outside the frame of the language authors.
    The main developer of Ruby on Rails even goes one step further and declares this a feature called "opinionated programming". I shudder.
    Of course I don't want everyone going back to the bloat of Java and XML, but why is it so hard to design a language that is both concise and consequent?
    </rant> 



  • @Zecc said:

     And that goes for the assumption that parseInt would return NaN for an invalid input, too.
     

    parseInt() does that. Not sure what your point is.



  • @dhromed said:

    @Zecc said:

     And that goes for the assumption that parseInt would return NaN for an invalid input, too.
     

    parseInt() does that. Not sure what your point is.

     

    My point is: parseInt('12monkeys') == 12; parseInt('01239asdew') == 83 (0123 octal).

    parseInt is "smart" as parses as many characters it can and ignores the rest (pretty much like atoi in C, btw.)



  •  Cool.

    But "dfkjhbvgd" is invalid input and returns NaN.

    I don't see a real problem -- other than the default radix being whatever the hell JS feels it should be, which is a definite wtf. It should be 10, of course.



  • @Zecc said:

     And that goes for the assumption that parseInt would return NaN for an invalid input, too.

    Ugh, tell me about it.  Another part of the application does some rather complex twiddling, and a full quarter of my debugigng effort was making sure that things would fail gracefully if the user entered bogus numbers.  Having the entire page grind to a halt because a user entered "lol" into a quantity field is bad.

    Also, thanks for the collective reminder about the radix paramater -- I'd forgotten all about it.  I now remember using it to deal with hex numbers a while back to do some HTML color twiddling.  That reminds me of another story that I'll post another time...


  • JavaScript's [i]parseInt[/i] merely defers to [i]strtol[/i] which has a well-known behavior when not provided a radix (ANSI C, POSIX 2001). It's supposed to be a convienence.

    The real WTF is the [b]defined[/b] behavior in the corner case where the leading-zero and/or 0x logic is allowed to 'look ahead' one character; if it finds anything but 0-7 after that first zero it thinks you mean DECIMAL zero followed by a letter , with the exception of 0x which returns NaN (because the hex string following it was empty). It's a bug in the logic -- 0a, 0b and so forth should return 0 as it looks like a zero butted up against something that's string so it's probably safe to interpret it as a zero.  0x and 0X put it in hex mode, and 0[0-7] tell it octal, but they "forget" 8 and 9 and lump them in with the alphabet letters and treat it as a 0 instead of just leaving it in decimal and continuing the interpretation.

    To paraphrase the standards docs:

    the radix is decimal unless the first character is zero, in which case it is octal, unless the second character is an X or x, in which case it is hexadecimal, else return 0 and optionally set EINVAL

     

    You should use the default string->number coercion along with rounding like Math.round(Number("string")) if you don't want it to accidentally read the prefix.

      



  • @Charles Capps said:

    Unfortunately, the Javascript library that we chose
     

     Those words always lead to severe pain in the genitals



  • @PSWorx said:

    The only way I can imagine a lint could have caught this one is if it generally showed a warning for every parseInt() call ever used - which would make the function quite useless.

    Show a warning for every parseInt() call that fails to specify a radix.



  • @kirchhoff said:

    You should use the default string->number coercion along with rounding like Math.round(Number("string")) if you don't want it to accidentally read the prefix.
    parseFloat doesn't take a radix argument and therefore doesn't have the prefix problem, but using Number is better.

    Number('0123go') returns NaN, while parseFloat('0123go') returns 123.

    Thanks for sharing that thought. 



  • Let's add a bit  more to the confusion, shall we?

     

    '12monkeys' * 1 == 1 * '12monkeys' == NaN

    '0090' * 1 == 1 * '0090' == 90

    '0xbeef' * 1 == 1 * '0xbeef' == 0xbeef == 48879

    '0xbeefcake' * 1 == 1 * '0xbeefcake' == NaN

    '007' * '009' == 63

    '0xDeadBeef' * ('1' + '.0') == '0xDeadBeef' * 1.0 == 0xdeadbeef == 3735928559

    ('0x' + 'Dead' + 'Beef') * ('1' + 0) == '0xDeadBeef' * '10' == 37359285590

    '1' + 0 == 1 + '0' == '10'    !=   1 + 0

    typeof NaN == 'number' 

     

    (Tested in Firefox) 



  • You're using == in javascript.

    You  *deserve* pain.



  • No, what I mean is javascript may unexpected conversions behind your back if you're not careful with what you are coding. Or perhaps you may assume too much.

    You may be doing a string concatenation instead of adding two numbers - or the other way around - and not even notice, if the end result seems minimally plausible.

    This is why multiplying by 1 in an expression may seem redundant, but it isn't (I've been reminded of this recently). 

    var val1 = document.getElementById('input_element_1').value;    // '24'

    var val2 = document.getElementById('input_element_2').value;   // '87'

    var total = val1 + val2; // Guess what this does... 

    if (total > 100) alert('Congratulations! You've been bit in the ass!');



  • @Zecc said:

    typeof NaN == 'number' 
     

    This is extremely nice behaviour that allows you to use  Number's methods even if it's not a number, so you don't need to write another silly isNaN check.

    @Zecc said:

    This is why multiplying by 1 in an expression may seem redundant, but it isn't (I've been reminded of this recently). 

    Don't multiply by 1. ParseInt or cast it. It's just as bad as concatenating an empty string to window.location to "coerce" it to a string (it has a href property: RTFM).

    Your code snippet displays completely predictable behaviour. string + string = bigger string.



  • @alunharford said:

    You're using == in javascript.

    You  *deserve* pain.

    What? 


Log in to reply