Perl: the Markov chain saw PerlMonks

### Perl Number range lookup question

by jamesbutler (Initiate)
 on Jun 18, 2012 at 00:53 UTC Need Help??
jamesbutler has asked for the wisdom of the Perl Monks concerning the following question:

I have working code but not a fan of this
``` my \$x = 40;

if ( \$x ~~ [0..9] || \$x ~~ [50..59] ) {
print 0;
} elsif ( \$x ~~ [10..19] || \$x ~~ [60..69] ) {
print 1;
} elsif ( \$x ~~ [20..29] || \$x ~~ [70..79] ) {
print 2;
} elsif ( \$x ~~ [30..39] || \$x ~~ [80..89] ) {
print 3;
} elsif ( \$x ~~ [40..49] || \$x ~~ [90..99] ) {
print 4;
}
was wondering if there is a more elegant solution was thinking hashes but cant get desired behavior, I was thinking something like
```
my %tiers = (
0..9 => 0,
10..19 => 1,
);

Replies are listed 'Best First'.
Re: Perl Number range lookup question
by jwkrahn (Monsignor) on Jun 18, 2012 at 02:52 UTC
```my \$x = 40;

print int( ( \$x % 50 ) / 10 );

./bow Many thanks :-D

This exactly was the first thing I wrote, but I decided I'd not do it that way because "tiers" are usually things that get changed sooner or later and I thought the OP code might just be a contrived example.

-sauoq
"My two cents aren't worth a dime.";
Re: Perl Number range lookup question
by AnomalousMonk (Chancellor) on Jun 18, 2012 at 04:01 UTC

jwkrahn's approach is pretty good all on its own, but if you want a hash-based solution (calculations done just once):

```>perl -wMstrict -le
"my %tiers = map { \$_ => int(\$_ % 50 / 10) } 0 .. 99;
;;
for my \$n (-1, 0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 100, 101) {
print qq{tier \$n },
exists \$tiers{\$n} ? qq{== \$tiers{\$n}} : 'out of range';
}
"
tier -1 out of range
tier 0 == 0
tier 11 == 1
tier 22 == 2
tier 33 == 3
tier 44 == 4
tier 55 == 0
tier 66 == 1
tier 77 == 2
tier 88 == 3
tier 99 == 4
tier 100 out of range
tier 101 out of range
I'd definitely benchmark this vs. calculating the tier on the fly each time (i.e., in this case, run it against print int( ( \$x % 50 ) / 10 );). Especially with a simple tier calculation, I would expect the hash lookup to take longer than (re)calculating on demand.

Using the precalculated hash would also be slower if the number of lookups isn't substantially larger than the number of precalculated values. If fewer lookups would be done, but the calculation is slower than a hash lookup, the better option would be to do no precalculation and instead do your lookups with

```my \$tier = \$tiers{\$n} //= calculate_tier(\$n);
which will calculate the tier for a value the first time it's requested, then return the calculated value from the hash on subsequent requests for that value. (Note that you'll need to use ||= instead of //= if your perl is older than 5.10.)

```my \$tier = \$tiers{\$n} //= calculate_tier(\$n);

That's a rather elegant memoization technique for functions in general (provided the function parameters can't be references). If your function took two parameters, you could even do:

```my \$tier = \$tiers{\$n, \$m} //= calculate_tier(\$n, \$m);

... taking advantage of Perl's rarely used multi-dimensional array fakery.

perl -E'sub Monkey::do{say\$_,for@_,do{(\$monkey=[caller(0)]->[3])=~s{::}{ }and\$monkey}}"Monkey say"->Monkey::do'
Re: Perl Number range lookup question
by tobyink (Abbot) on Jun 18, 2012 at 07:33 UTC

Several different solutions coming up...

```use 5.010;

my \$x = 40;

given (\$x)
{
when ([0..9,   50..59])   { say 0 }
when ([10..19, 60..69])   { say 1 }
when ([20..29, 70..79])   { say 2 }
when ([30..39, 80..89])   { say 3 }
when ([40..49, 90..99])   { say 4 }
}

As of Perl 5.14 (which is incidentally the oldest officially supported version of Perl, so if you've not upgraded yet, why not?), you can also use when as a statement modifier...

```use 5.014;

my \$x = 40;

given (\$x)
{
say 0 when [0..9,   50..59];
say 1 when [10..19, 60..69];
say 2 when [20..29, 70..79];
say 3 when [30..39, 80..89];
say 4 when [40..49, 90..99];
}

Or smart match with no topicalising...

```use 5.010;

my \$x = 40;

say 0 if \$x ~~ [0..9,   50..59];
say 1 if \$x ~~ [10..19, 60..69];
say 2 if \$x ~~ [20..29, 70..79];
say 3 if \$x ~~ [30..39, 80..89];
say 4 if \$x ~~ [40..49, 90..99];

And here's another technique using Smart::Dispatch...

```use Smart::Dispatch;

my \$x = 40;

my \$tiers = dispatcher {
match [0..9,   50..59], dispatch { 0 };
match [10..19, 60..69], dispatch { 1 };
match [20..29, 70..79], dispatch { 2 };
match [30..39, 80..89], dispatch { 3 };
match [40..49, 90..99], dispatch { 4 };
};

say \$tiers->(\$x);

And a last one using a regular hash, which has the advantage that it should work with even really ancient Perl 5.x releases.

```my \$x = 40;

my %tiers = (
(map { \$_ => 0 }  0..9,   50..59),
(map { \$_ => 1 }  10..19, 60..69),
(map { \$_ => 2 }  20..29, 70..79),
(map { \$_ => 3 }  30..39, 80..89),
(map { \$_ => 4 }  40..49, 90..99),
);

print \$tiers{\$x} . "\n";

What are the advantages of each solution? Well, a hash is nice in that it can be stored in a variable and passed around easily; it can be inspected, modified, etc. However, it can only deal with finite sets of keys. If you need to be able to match, say, all floating point numbers between 2.1 and 2.3, then you don't want to list them all in a hash!

The aim of Smart::Dispatch is to retain the benefits of hash-based dispatch tables (the ability to pass a reference to them around; inspect them; modify them) but without needing to enumerate all the hash keys.

The others, using Perl's built-in decision syntax (i.e. if, given, when, etc) will likely run a little faster (though may compile a little slower than a hash), but passing around references to them is more awkward (you'd need to write a wrapper sub and pass around references to that), and inspecting/modifying them at run-time is close to impossible. If you've got a reference to a sub wrapper for your decision tree, you can fake modifying it by installing additional wrappers around it. For example:

```use 5.010;

my \$standard_say = sub {
say 0 if \$x ~~ [0..9,   50..59];
say 1 if \$x ~~ [10..19, 60..69];
say 2 if \$x ~~ [20..29, 70..79];
say 3 if \$x ~~ [30..39, 80..89];
say 4 if \$x ~~ [40..49, 90..99];
};

\$standard_say->(40);

# Now imagine we decide to special-case 39...
my \$modified_say = sub {
if (\$_[0] == 39) {
say "yay!";
return;
}
\$standard_say->(@_);
};

\$modified_say->(40);
\$modified_say->(39);

Doing something like that with Smart::Dispatch is a little cleaner...

```use Smart::Dispatch;

my \$standard_say = dispatcher {
match [0..9,   50..59], dispatch { say 0 };
match [10..19, 60..69], dispatch { say 1 };
match [20..29, 70..79], dispatch { say 2 };
match [30..39, 80..89], dispatch { say 3 };
match [40..49, 90..99], dispatch { say 4 };
};

\$standard_say->(40);

my \$modified_say = dispatcher { match 39, dispatch { say "yay!" } } .
+\$standard_say;

\$modified_say->(40);
\$modified_say->(39);
perl -E'sub Monkey::do{say\$_,for@_,do{(\$monkey=[caller(0)]->[3])=~s{::}{ }and\$monkey}}"Monkey say"->Monkey::do'
Re: Perl Number range lookup question
by sauoq (Abbot) on Jun 18, 2012 at 01:23 UTC
```#!/usr/bin/perl

my \$x = 40;
my \$tier;

sub get_tier {
my \$x = shift;
my \$t = 0;
\$t += (\$x > \$_) for (@_);
return \$t;
}

\$tier = get_tier(\$x, 9,19,29,39) if (\$x >= 0 and \$x < 50);
\$tier = get_tier(\$x, 59,69,79,89) if (\$x >= 50 and \$x < 100);

print \$tier, \$/;

-sauoq
"My two cents aren't worth a dime.";

Create A New User
Node Status?
node history
Node Type: perlquestion [id://976708]
Approved by sauoq
help
Chatterbox?
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others perusing the Monastery: (3)
As of 2017-08-19 17:32 GMT
Sections?
Information?
Find Nodes?
Leftovers?
Voting Booth?
Who is your favorite scientist and why?

Results (312 votes). Check out past polls.

Notices?