In Perl, how can I distinguish between numeric and JSON encoded string hash values?
In the following code, I have a hash related class. In the function, FETCH
I print 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 am getting the following output:
{"key":"0"}
The result 0
is encoded as a string. Is there a way to encode it to a number instead?
print($exp->{0} . "\n"); # this should print {"key":0}
print($exp->{'0'} . "\n"); # this should print {"key":"0"}
source to share
Since Perl has no real concept of a string or number, but only a scalar, this is tricky. the JSON module tries to do this by looking at the last context whose value it encodes.
Plain Perl scanners (any scalar that is not a reference) are the most difficult objects to encode: this module will encode undefined-scanning scalars as null JSON values, scalars that were used in the past in a string context before being encoded as JSON strings, and everything else as numeric 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 FETCH
doesn't do this specifically. So it must be somewhere else.
I'm guessing Perl's auto-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 automatically processes elements within elements {}
for a hash (ref) as specified and they become strings. Because this is easily forgotten, there is a 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 numeric keys for the hashes. If there are number keys, you can order them and then you have an array.
You can get further evidence for this by calling yours FETCH
directly with the unquoted number as an argument.
Example::Tie->FETCH(1);
This will lead to
{"key":1}
So I 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 a number. There is an example in the JSON module documentation.
You can force a type to be a number by numbering 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.
source to share
Basically, @simbabque's answer is in place. When yours FETCH
gets the list of arguments, 0
in $exp->{0}
has already been compressed since hash keys are always strings.
Of course you will get in trouble if you add 0
to each argument to get indiscriminately. Below I am using Scalar :: Util :: looks_like_number to distinguish between numbers and strings, but of course it won't work if you try it with "0"
. This also converts 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';
source to share