Sunday, October 23, 2011

Strange jQuery HTML5 Data Attribute rounding error

Snappy title, I know. I hit upon a strange rounding error the other day. Here's what happened.

Like plenty of other blogs, I usually promote my posts by tweeting about them. But rather than simply tweeting the post's link as a way of 'advertising' the post, [Loading tweet...]. Because you don't often see tweets on their own, you forget that they're addressable, first-class citizens of the web. The tweet should be linked to its home on the web, its canonical URL, and displayed inline with all its links (hashtags, mentions and urls) preserved. To that end, I've written a service that fetches a tweet given its id (or status as Twitter calls it).

If you look at this page from the Guardian's live blog from New Corporation's AGM, you'll see an example of an embedded tweet halfway down the page. But it's only superficially embedded. The real thing as it appears at its address has a couple of hashtags and of course the user's (BorowitReport) account, plus plenty of other metadata. Here's an example of what I mean, using the real tweet:
[Loading tweet...].
So, now you know what I mean. Anyway, I want to change the way I've implemented this for one main reason: it offends the God of unobtrusiveness. For each tweet that I want to embed, I need two things: where to put it, and what to put. But at he moment, I'm also calling my Javascript function (Twitter.showTweet("127447427488817152", "tweet_1")) obtrusively, which is just embarrassing really. That needs to go.
<span id="tweet_1">[Loading tweet...]</span>
    ... more html ...
<script type="text/javascript">
    Twitter.showTweet("127447427488817152", "tweet_1")
</script>
So, I've been working on an alternative version, one that only tells the page where (to place the tweet) and what (tweet to show). The how is always the same. Let the <span> housing each tweet have a class - yes, "tweet" - and let the tweet's status/id be carried by a HTML5 Data Annotations, in this case "data-tweet-id". Now, using the unobtrusiveness engine that is jQuery, I can simply get all the tweets on the page like this:
$(function () {
        $(".tweet").each(function () { // finds each tweet
            var tweetStatus = $(this).data("tweet-id"); // note the jQuery selector for data attributes
            var tweetHTML = Twitter.GetTweet(tweetStatus); // gets the tweet from the service. Trust me
            $(this).html(tweetHTML); 
        });
    });
    ....
    <span class="tweet" data-tweet-id="107974190249947137">[Loading tweet]</span>
But it didn't work. In fact, it failed. The reason it failed was quite strange, at least to someone not familiar - yes, I confess - with the inner workings of the jQuery .data() selector. The each() iterator was finding the
$(this).data("tweet-id")
value alright. The problem was it was turning
107974190249947137
into
107974190249947140
Further investigation revealed that I was 2 orders of magnitude out of luck - a number 2 digits shorter would work. It would also work if I just give the span the id of the tweet's status:
<!-- <span class="tweet" data-tweet-id="107974190249947137">[Loading tweet]</span> -->
    <span class="tweet" id="107974190249947137">[Loading tweet]</span>
But that seems like a step backwards, a blow against HTML5 semantic modernity. I was loath to let it go. Further investigation revealed I could revert to my custom data annotation if I swapped jQuery selectors slightly.
// won't work in this case
        // var tweetStatus = $(this).data("tweet-id");

        // works, but jQuery treating my cool new HTML5 data annotation as any old arbitrary attribute :-(
        var tweetStatus = $(this).attr("data-tweet-id");
I'm happy with that. My markup checks out as valid HTML5, with only a minor change in my jQuery. I don't know why numbers above about 10 squillion get rounded for $(this).data("tweet-id") but not for $(this).attr("data-tweet-id"). I admit it's not very hacker of me to try and work out why but because I don't have to compromise my markup and the two jQuery methods are semantically practically identical, I can move on. I'm pretty sure that as a programmer I don't have to fight every bug head on: if I can deflect the blow and continue in the direction I was going, so much the better. Not to get too carried away with such a small matter, but therein is the path to true wisdom.

As a final note though, I couldn't help but think of the disaster that would have ensued on my blog if I hadn't noticed that error, and if twitter had sequential status integer values. My service might have returned random tweets, close in chronological order to the one I wanted, but random in terms of the content, yielding weird and wonderful juxtapositions like this:
Lorem ipsum ad nauseam, and here's a supporting tweet: [Loading tweet...].

Dude, your HTML5 data-widget is obfuscating my MVC3 route unobtrusively, and it says so on twitter right here: [Loading tweet...].

Highly-important business sentence predicting tremendous growth in Q3, and it's obviously true because our CEO tweeted it from the conference at Dubai: [Loading tweet...].

These are random tweets carefully selected by me for their comic value, and also to illustrate the rather obvious dangers in calling a service with only one value and no checking value, like the user id. So for God's sake don't use $(this).data("[custom attribute]") when you intend to have integer values that go above about 10000000000000000000. Use the good old-fashioned but robust attribute selector instead. Spread the word.

Update 26 Oct.

I found an explanation in the jQuery documentation which addresses what I was talking about. Under the heading 'HTML 5 data- Attributes', it says "Every attempt is made to convert the string to a JavaScript value (this includes booleans, numbers, objects, arrays, and null) otherwise it is left as a string. To retrieve the value's attribute as a string without any attempt to convert it, use the attr() method." A twitter status like 107974190249947137, while it looks like a number, is too big to be considered a number for the jQuery parser.