Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Perl, how can I distinguish between numeric and string hash values in JSON encoding?

Tags:

perl

In the following code, I have a class tied to a hash. In the FETCH function, I am printing the JSON encoding of the key:

package Example::Tie;
use JSON;
my $json = JSON->new();

sub TIEHASH {
    my ($pkg,@list) = @_;
    bless { @list }, $pkg;
}

sub FETCH {
    my ($tied,$key) = @_;
    return $json->encode({key => $key});
}

package Example;

sub new {
    my ($pkg,@list) = @_;
    my $self = {};
    tie %$self, 'Example::Tie', @list;
    bless $self, $pkg;
}

package main;
my $exp = Example->new();
print($exp->{0} . "\n");

I get the following output:

{"key":"0"}

This results in 0 being encoded as a string. Is there a way to encode it into a number instead?

print($exp->{0} . "\n"); # this should print {"key":0}
print($exp->{'0'} . "\n"); # this should print {"key":"0"}
like image 917
aoiee Avatar asked Dec 18 '22 05:12

aoiee


2 Answers

Since there is no real concept of string or number in Perl, but only the scalar, this is tricky. The JSON module tries to do it by looking at the last context the value it encodes was used in.

Simple Perl scalars (any scalar that is not a reference) are the most difficult objects to encode: this module will encode undefined scalars as JSON null values, scalars that have last been used in a string context before encoding as JSON strings, and anything else as number value:

# dump as number
encode_json [2]                      # yields [2]
encode_json [-3.0e17]                # yields [-3e+17]
my $value = 5; encode_json [$value]  # yields [5]

# used as string, so dump as string
print $value;
encode_json [$value]                 # yields ["5"]

# undef becomes null
encode_json [undef]                  # yields [null]

Your code in FETCH does not do this specifically. So it has to be somewhere else.

My guess is that Perl's automatic quoting for hash keys is the culprit here.

$exp->{0}; # this should print {"key":0}
$exp->{'0'}; # this should print {"key":"0"}

These two expressions are equivalent. Perl will automatically treat things inside of the {} for hash (ref) elements as quoted and they become strings. Because that's easily forgotten, there is best practice to always use single quotes ''.

Perldata says (emphasis mine):

Hashes are unordered collections of scalar values indexed by their associated string key.

The idea is that there are no numerical keys for hashes. If there are numerical keys, they can be ordered, and then you have an array.

You can get further proof for this by calling your FETCH directly with an unquoted number as the arg.

Example::Tie->FETCH(1);

This will result in

{"key":1}

I therefore conclude that using tie with the JSON module what you want to do is not possible, unless you explicitly try to force it back to be a number. There is an example in the JSON module's documentation.

You can force the type to be a number by numifying it:

my $x = "3"; # some variable containing a string
$x += 0;     # numify it, ensuring it will be dumped as a number
$x *= 1;     # same thing, the choice is yours.
like image 198
simbabque Avatar answered Dec 22 '22 00:12

simbabque


Basically, @simbabque's answer is spot on. By the time, your FETCH gets the argument list, the 0 in $exp->{0} has already been stringified because hash keys are always strings.

Of course, you are going to have issues if you add 0 to every argument to fetch indiscriminately. Below, I use Scalar::Util::looks_like_number to distinguish between numbers and strings, but, of course, this will not work if you try it with "0". That will also get converted to 0.

use strict; use warnings;

package Example::Tie;

use JSON;
use Scalar::Util qw( looks_like_number );

my $json = JSON->new;

sub TIEHASH {
    my $pkg = shift;
    bless { @_ } => $pkg;
}

sub FETCH {
    my $tied = shift;
    $json->encode({key => looks_like_number($_[0]) ? 0 + $_[0] : $_[0]})
}

package Example;

sub new {
    my $pkg = shift;
    my $self = {};
    tie %$self, 'Example::Tie', @_;
    bless $self => $pkg;
}

package main;

my $exp = Example->new;
print "$_\n" for map $exp->{$_}, 0, 'a';
like image 32
Sinan Ünür Avatar answered Dec 22 '22 00:12

Sinan Ünür