BrowserUk has asked for the wisdom of the Perl Monks concerning the following question:

This has been a recurring dilemma down the years.

Given a contiguous input and a set of break points, find the highest breakpoint lower than the input value and return the associated value.

sub lookup { my( $v ) = shift; if( $v < 25000 ) return 2500; if( $v < 50000 ) return 5000; if( $v < 150000 ) return 12500; if( $v < 225000 ) return 25000; if( $v < 300000 ) return 37500; if( $v < 600000 ) return 60000; if( $v < 1200000 ) return 120000; if( $v < 3600000 ) return 300000; if( $v < 5400000 ) return 600000; if( $v < 10800000 ) return 900000; if( $v < 21600000 ) return 1800000; if( $v < 43200000 ) return 3600000; if( $v < 64800000 ) return 7200000; if( $v < 129600000 ) return 10800000; if( $v < 216000000 ) return 21600000; if( $v < 432000000 ) return 43200000; if( $v < 864000000 ) return 86400000; if( $v < 1728000000 ) return 172800000; if( $v < 3024000000 ) return 345600000; if( $v < 6048000000 ) return 604800000; if( $v < 12096000000 ) return 1209600000; if( $v < 31557600000 ) return 2629800000; if( $v < 63115200000 ) return 5259600000; if( $v < 78894000000 ) return 7889400000; if( $v < 157788000000 ) return 15778800000; return 31557600000; }

Simple. Efficient. Not very pretty. Is there a better way?

Then there's the search method. Most time the set isn't big enough to warrant coding a binary search in place of a linear one. Most times efficiency isn't a particular concern, but in this case, the routine is called as part of a redraw function when rotating stuff on screen, so it can be called many times a second.

Basically, there are several ways of doing it, but none of them are particularly satisfying, and I'm wondering if anyone has discovered a nice way that I haven't covered?

(The final twist is that this is destined for JavaScript; so if any JS guys know of a good method that language supports; I'd be happy to hear of it. Perhaps off-line.)


With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
"Science is about questioning the status quo. Questioning authority". I'm with torvalds on this
In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked

Replies are listed 'Best First'.
Re: A better way of lookup?
by Corion (Patriarch) on Apr 26, 2015 at 12:24 UTC

    Maybe instead of a parallel array or arrayreferences, just use a single array with the even offsets being the threshholds and the odd offsets being the clamped values?

    use strict; use vars qw(@lookup_values); @lookup_values= (qw( 25000 2500 50000 5000 150000 12500 225000 25000 300000 37500 600000 60000 1200000 120000 3600000 300000 5400000 600000 10800000 900000 21600000 1800000 43200000 3600000 64800000 7200000 129600000 10800000 216000000 21600000 432000000 43200000 864000000 86400000 1728000000 172800000 3024000000 345600000 6048000000 604800000 12096000000 1209600000 31557600000 2629800000 63115200000 5259600000 78894000000 7889400000 157788000000 15778800000 )); sub lookup { my $v= $_[0]; return 31557600000 if $v >= 157788000000; my $i; for( $i= 0; $i <= $#lookup_values-1 && $v >= $lookup_values[$i]; $ +i+=2 ) { # nothing in the loop body #print "$v: $i -> $lookup_values[$i]\n"; }; return $lookup_values[ $i+1 ]; }; for( 10,2500,2501, 157788000000, 157788000001, 78894000000 ) { print sprintf "%d => %d\n", $_, lookup($_); };
Re: A better way of lookup?
by hdb (Monsignor) on Apr 26, 2015 at 12:25 UTC

    I would go for parallel arrays and binary search or leave it as is. If you do not depend on the discrete values (piecewise constant function), a log-log-linear fit would work very well: $lookup=exp(-2.276+0.997*log($v));. If you want to be fancy you could add some rounding.

      $lookup = exp( -2.276 + 0.997 * log( $v ) );

      That's a pretty good fit(*) -- a lot closer than I got -- but, this is intended to replace the Mysterious Function, and on balance, I think those constants make it even more mysterious.

      I'm not particularly happy with my mysterious lookup table either, but every other attempt I've had screws up the results to a greater or lesser extent, whereas this works. I just need to come up with some documentation to explain it. (Not easy!).

      (* However, I would be very interested to know how you derived that equation?)


      With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
      Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
      "Science is about questioning the status quo. Questioning authority". I'm with torvalds on this
      In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked

        Paste the data into Excel as two columns, apply LN to the data, use SLOPE and INTERCEPT to get the constants. That gives you have a linear formula to predict the log of the desired value as a funtion of the log of the input, therefore take EXP on the result. If you want to do it in Perl, have a look at this: Re^5: Data range detection?. There is a coefficients function in that module.

        Update: On documentation, I remember that all these numbers are milliseconds, so translating into seconds, minutes, hours, days, and years should be helpful.

Re: A better way of lookup?
by flexvault (Monsignor) on Apr 26, 2015 at 15:46 UTC

    Hello BrowserUk,

    As usual, I'm late to the table, but I've found in my *nix environments that using 'elsif' does improve the performance and adding some minimal binary splitting also improved the performance of your subroutine. I benchmarked both your original and my re-work and results are at the bottom. Obviously if the inputs are better known you could improve the break points. Hope this spurs your imagination a little.

    use strict; use warnings; use Benchmark qw(:all); my @input = ( 25000, 50000, 150000, 225000, 300000, 600000, 1200000, 3 +600000, 5400000, 10800000, 21600000, 43200000, 64800000, 1296000 +00, 216000000, 432000000, 864000000, 1728000000, 3024000000, 6048000000, 12096000000, 31557600000, 63115200000, 78894 +000000, 157788000000, 315576000000, ); my @newinput = (); for ( 0 .. $#input ) { $newinput[$_] = $input[$_] - 11; # print $newinput[$_],"\ +n"; # Testing } # print $#newinput,"\n"; # size of input array ## Make sure both subroutines return same results! for my $no ( 0 .. $#newinput ) { my $r1 = &lookup_org ( $newinput[ $no ] ); my $r2 = &lookup_new ( $newinput[ $no ] ); if ( $r1 != $r2 ) { print "$no|$r1|$r2|\n"; exit(1); } # print "$no|$r1|$r2|\n"; # Testing } timethese ( -2 , { case1 => sub { &lookup1 }, case2 => sub { &lookup2 }, }, ); sub lookup1 { for ( 0 .. $#newinput ) { lookup_org ( $newinput[ $_ ] ); } } sub lookup2 { for ( 0 .. $#newinput ) { lookup_new ( $newinput[ $_ ] ); } } sub lookup_org { my $v = shift; if( $v < 25000 ) { return 2500; } if( $v < 50000 ) { return 5000; } if( $v < 150000 ) { return 12500; } if( $v < 225000 ) { return 25000; } if( $v < 300000 ) { return 37500; } if( $v < 600000 ) { return 60000; } if( $v < 1200000 ) { return 120000; } if( $v < 3600000 ) { return 300000; } if( $v < 5400000 ) { return 600000; } if( $v < 10800000 ) { return 900000; } if( $v < 21600000 ) { return 1800000; } if( $v < 43200000 ) { return 3600000; } if( $v < 64800000 ) { return 7200000; } if( $v < 129600000 ) { return 10800000; } if( $v < 216000000 ) { return 21600000; } if( $v < 432000000 ) { return 43200000; } if( $v < 864000000 ) { return 86400000; } if( $v < 1728000000 ) { return 172800000; } if( $v < 3024000000 ) { return 345600000; } if( $v < 6048000000 ) { return 604800000; } if( $v < 12096000000 ) { return 1209600000; } if( $v < 31557600000 ) { return 2629800000; } if( $v < 63115200000 ) { return 5259600000; } if( $v < 78894000000 ) { return 7889400000; } if( $v < 157788000000 ) { return 15778800000; } else { return 31557600000; } } sub lookup_new { my $v = shift; if ( $v >= 64800000 ) { if ( $v < 129600000 ) { return 10800000; } elsif( $v < 216000000 ) { return 21600000; } elsif( $v < 432000000 ) { return 43200000; } elsif( $v < 864000000 ) { return 86400000; } elsif( $v < 1728000000 ) { return 172800000; } elsif( $v < 3024000000 ) { return 345600000; } elsif( $v < 6048000000 ) { return 604800000; } elsif( $v < 12096000000 ) { return 1209600000; } elsif( $v < 31557600000 ) { return 2629800000; } elsif( $v < 63115200000 ) { return 5259600000; } elsif( $v < 78894000000 ) { return 7889400000; } elsif( $v < 157788000000 ) { return 15778800000; } else { return 31557600000; } } else { if ( $v < 25000 ) { return 2500; } elsif( $v < 50000 ) { return 5000; } elsif( $v < 150000 ) { return 12500; } elsif( $v < 225000 ) { return 25000; } elsif( $v < 300000 ) { return 37500; } elsif( $v < 600000 ) { return 60000; } elsif( $v < 1200000 ) { return 120000; } elsif( $v < 3600000 ) { return 300000; } elsif( $v < 5400000 ) { return 600000; } elsif( $v < 10800000 ) { return 900000; } elsif( $v < 21600000 ) { return 1800000; } elsif( $v < 43200000 ) { return 3600000; } elsif( $v < 64800000 ) { return 7200000; } } } __END__ Results: 1st is worst time, 2nd is best time Benchmark: running case1, case2 for at least 2 CPU seconds... case1: 2 wallclock secs ( 2.24 usr + 0.00 sys = 2.24 CPU) @ 4799 +9.55/s (n=107519) case2: 3 wallclock secs ( 2.06 usr + 0.00 sys = 2.06 CPU) @ 6088 +8.83/s (n=125431) Benchmark: running case1, case2 for at least 2 CPU seconds... case1: 3 wallclock secs ( 2.32 usr + 0.00 sys = 2.32 CPU) @ 486 +57.33/s (n=112885) case2: 2 wallclock secs ( 2.24 usr + 0.00 sys = 2.24 CPU) @ 610 +86.61/s (n=136834)

    Best Regards...Ed

    "Well done is better than well said." - Benjamin Franklin

      There's another possible micro optimization. You could change the last line of the second group to just return 7200000 as you already know v is less then 64800000. There's no point testing it again.

      # replace elsif( $v < 64800000 ) { return 7200000; } #with else { return 7200000; }

      BTW that's an interesting result that elsif is quicker, did you test them in isolation? What improvement did the partial binary chop make compared with the elsif?

        RichardK,

        Actually, both outer 'if / else' conditional statements could be reduced to just 'return ...' since each previous test did a 'return...' if the test was 'true'.

          BTW that's an interesting result that elsif is quicker, did you test them in isolation?
        Interestingly, use of the 'elsif' gave the most performance improvement! Someone on PerlMonks many years ago showed how it was better, all I did was remember and continue to use it in my scripts.

          What improvement did the partial binary chop make compared with the elsif?

        Not as much of an improvement as the 'elsif', but since BrowserUk may have more information about the frequency of the inputs, I left it in with the hope that it would help. Another consideration is that BrowserUk uses 64bit Windows and I use mostly 32bit *nix. That's why the benchmarking is critical, since the results may be different in his environment. Please note, that I had to add brackets around the 'return' in order to compile his original script in my environment. YMMV!

        Regards...Ed

        "Well done is better than well said." - Benjamin Franklin

      I must admit, like RichardK, I was skeptical about the benefits of using elsifs where the logic short-circuits with returns anyway, and immediately tried switching from your:if( cond ) { return x; } form to the statement modifier form: return x if cond;; and ... it made no difference. elsif still won out.

      So then I looked at -MO=Deparse,-x9 to see if if/elsif/else chains were perhaps being converted to some kind of jump table or switch.

      But no. They appear to be implemented as (the equivalent of) a chain of nested ternaries, but when I tried coding them that way, they were still slower than the if/elsif/else chain?

      I then turned my attention to your rather more interesting innovation; namely (partially) unrolling a binary search. I tried adding another level, and got greater benefit; then another and (reduced, but) still more; so then another; and the benefits ran out :(

      My (long and winding) benchmark and results are here:

      Many thanks for a) responding; b) teaching me something(if/elsif); c) stimulating my brain in a different direction (hardcoding a binary search).


      With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
      Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
      "Science is about questioning the status quo. Questioning authority". I'm with torvalds on this
      In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked
Re: A better way of lookup?
by davido (Cardinal) on Apr 26, 2015 at 15:37 UTC

    Here's a solution that uses List::BinarySearch::XS. You could just use List::BinarySearch, since installing it will pull in the XS version and use it automatically, but I included ::XS to be explicit.

    You would have to benchmark this in a tight loop. One optimization would be to not pass the $h array ref in to bsearch_lookup. And you could probably come up with a more efficient code block to pass in to the binsearch_pos function, but it's a start. I think that for large crossreference lookup sets, this will beat a linear search.

    use List::BinarySearch::XS 'binsearch_pos'; my @lookup_values = ( [ 25000, 2500 ], [ 50000, 5000 ], [ 150000, 12500 ], [ 225000, 25000 ], [ 300000, 37500 ], [ 600000, 60000 ], [ 1200000, 120000 ], [ 3600000, 300000 ], [ 5400000, 600000 ], [ 10800000, 900000 ], [ 21600000, 1800000 ], [ 43200000, 3600000 ], [ 64800000, 7200000 ], [ 129600000, 10800000 ], [ 216000000, 21600000 ], [ 432000000, 43200000 ], [ 864000000, 86400000 ], [ 1728000000, 172800000 ], [ 3024000000, 345600000 ], [ 6048000000, 604800000 ], [ 12096000000, 1209600000 ], [ 31557600000, 2629800000 ], [ 63115200000, 5259600000 ], [ 78894000000, 7889400000 ], [ 157788000000, 15778800000 ], ); sub bsearch_lookup { my( $n, $h ) = @_; my $pos = binsearch_pos { $a+1 <=> $b->[0] } $n, @$h; $pos > $#$h && return 31557600000; return $h->[$pos][1]; } my @tests = ( 100, 25000, 50000, ( map { int rand 160000000000 } 1 .. +10 ), 160000000000 ); # Make sure we have some edge cases foreach my $test ( @tests ) { my $bmatch = bsearch_lookup( $test, \@lookup_values ); print "Bsearch Found : $test => $bmatch\n\n";

    If you want to stick with a linear search approach you could probably use List::Utils first. That would also pull most of the work into an XS-based routine, but of course it would be a linear search.

    Update: For the small list of thresholds that you provided, it's hard to beat the simple search. I haven't tested with a larger list of thresholds, mostly because I'm not enthusiastic about generating the larger list. Here are results for three possible solutions given your list of 25 (well, really 26 if you count the default return value) thresholds:

    Rate first bsearch simple first 33.8/s -- -25% -41% bsearch 44.9/s 33% -- -21% simple 57.1/s 69% 27% --

    And here is the code that generated that report:

    For the worst case, both the 'simple' and 'first' lookups will need to do 25 threshold comparisons, whereas the 'bsearch' lookup will do about 5. That tradeoff isn't worthwhile for small lookup tables. If your lookup table were to grow to, for example, 1000, the 'simple' and 'first' lookups will require 1000 comparisons in the worst case, and the 'bsearch' method will require about 10 (you know all this, of course; I'm just thinking out loud). The advantage in that case would probably lean toward the 'bsearch' method. It would be interesting to know at what point that method becomes worthwhile.


    Dave

Re: A better way of lookup?
by Laurent_R (Canon) on Apr 26, 2015 at 15:15 UTC
    Hi BrowserUk,

    this is my attempt, using a hash for the breakpoint lookup and an array of sorted keys of that hash for a binary search. I also stored the calculation in an anonymous closure to avoid repeating the sorting of the keys and so on for each call (if this is not an option under JS, maybe you can use static variables or even global variables, or whatever, I haven't used JS for about 10 years).

    use strict; use warnings; my $lookup = create_lookup(); printf "%14d : %14d \n", $_, $lookup->($_) for (qw /24000 25000 51000 +85000000 670000 260000000 78893999998 157788000001/); sub create_lookup { my %lookup_h = qw / 25000 2500 50000 5000 150000 12500 225000 25000 300000 37500 600000 60000 1200000 120000 3600000 300000 5400000 600000 10800000 900000 21600000 1800000 43200000 3600000 64800000 7200000 129600000 10800000 216000000 21600000 432000000 43200000 864000000 86400000 1728000000 172800000 3024000000 345600000 6048000000 604800000 12096000000 1209600000 31557600000 2629800000 63115200000 5259600000 78894000000 7889400000 157788000000 15778800000 /; my @keys = sort {$a <=> $b} keys %lookup_h; my $size = scalar @keys; my $ceil = $keys[-1]; my $ceil_val = = $lookup_h{$ceil}; return sub { my $v = shift; return $ceil_val if $v >= $ceil; return $lookup_h{$keys[0]} if $v < $keys[0]; my ($min, $max) = (0, $size); while (1) { my $mid = int (($min + $max)/2); return $lookup_h{$keys[$max]} if $max - $min <= 1; $min = $mid and next if $v > $keys[$mid]; $max = $mid; } } }
    This prints (correctly, I think):
    $ perl lookup.pl 24000 : 2500 25000 : 5000 51000 : 12500 85000000 : 10800000 670000 : 120000 260000000 : 43200000 78893999998 : 7889400000 157788000001 : 15778800000
    but I have not thoroughly tested edge cases.

    Update: fixed a couple of errors, especially when the input value is smaller than the first value in the hash.

    Je suis Charlie.
Re: A better way of lookup?
by hdb (Monsignor) on Apr 26, 2015 at 17:29 UTC

    And here yet another idea, stealing from many prior proposals and guessing the index using linear regression:

    use strict; use warnings; sub lookup { my $v = shift; if( $v < 25000 ) { return 2500; } if( $v < 50000 ) { return 5000; } if( $v < 150000 ) { return 12500; } if( $v < 225000 ) { return 25000; } if( $v < 300000 ) { return 37500; } if( $v < 600000 ) { return 60000; } if( $v < 1200000 ) { return 120000; } if( $v < 3600000 ) { return 300000; } if( $v < 5400000 ) { return 600000; } if( $v < 10800000 ) { return 900000; } if( $v < 21600000 ) { return 1800000; } if( $v < 43200000 ) { return 3600000; } if( $v < 64800000 ) { return 7200000; } if( $v < 129600000 ) { return 10800000; } if( $v < 216000000 ) { return 21600000; } if( $v < 432000000 ) { return 43200000; } if( $v < 864000000 ) { return 86400000; } if( $v < 1728000000 ) { return 172800000; } if( $v < 3024000000 ) { return 345600000; } if( $v < 6048000000 ) { return 604800000; } if( $v < 12096000000 ) { return 1209600000; } if( $v < 31557600000 ) { return 2629800000; } if( $v < 63115200000 ) { return 5259600000; } if( $v < 78894000000 ) { return 7889400000; } if( $v < 157788000000 ) { return 15778800000; } else { return 31557600000; } } my @lookup_values = ( [ 25000, 2500 ], [ 50000, 5000 ], [ 150000, 12500 ], [ 225000, 25000 ], [ 300000, 37500 ], [ 600000, 60000 ], [ 1200000, 120000 ], [ 3600000, 300000 ], [ 5400000, 600000 ], [ 10800000, 900000 ], [ 21600000, 1800000 ], [ 43200000, 3600000 ], [ 64800000, 7200000 ], [ 129600000, 10800000 ], [ 216000000, 21600000 ], [ 432000000, 43200000 ], [ 864000000, 86400000 ], [ 1728000000, 172800000 ], [ 3024000000, 345600000 ], [ 6048000000, 604800000 ], [ 12096000000, 1209600000 ], [ 31557600000, 2629800000 ], [ 63115200000, 5259600000 ], [ 78894000000, 7889400000 ], [ 157788000000, 15778800000 ], ); sub guesslookup { my $v = shift; return 31557600000 if $v >= 157788000000; return 2500 if $v < 25000; my $guess = int( -15.35 + 1.535 * log $v ); # magic formula ;) have +fun documenting! $guess++ while $lookup_values[$guess]->[0] <= $v; return $lookup_values[$guess]->[1]; } for( my $test=0; $test<25; $test++ ) { for( my $offset=-1; $offset<=1; $offset++ ) { my $v = $lookup_values[$test]->[0] + $offset; my $org = lookup($v); my $new = guesslookup($v); print "Error: $test => $org : $new\n" if $org != $new; } }
Re: A better way of lookup? (Thanks and outcome.)
by BrowserUk (Patriarch) on Apr 27, 2015 at 11:11 UTC

    First, thanks to everyone who responded. I wasn't expecting much response to the question; and was surprised by the multitude of alternatives offered. Thank you all.

    Upshot: I coded up several of the more distinct variations in JS and plugged them into the JS code and made a (probably clumsy and naive) attempt to measure the results; and the outcome was that I couldn't discern any consistent benefit from any of them.

    Perhaps if I had more experience with JS; or if the table size was larger; but I suspect that JIT compilation and optimisation is playing a part here.

    So, for now, I'm sticking with the crudely simple version I posted in the OP for this purpose, and I'll re-visit this thread next time I have the same problem either in Perl; or with a larger table size.


    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority". I'm with torvalds on this
    In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked
      Probably the most sensible thing to do. It probably does not matter to the computer how you do it. What matters most is how you specify it to your colleagues. K.I.S.S. is usually the best-all-around approach ... unless and until ... you can prove "actual pain" (computer, or human) with regards to the approach that was taken.

        What I think is close to being the final version for this project, looks like this:

        const ms = 1, sec = 1000*ms, mn = 60*sec, hr = 60*mn, day = 24*hr, mth + = 30.4375*day, yr = 365.25*day; function _DateInterval( vv ) { if( vv < 25*sec ) return 2.5*sec; if( vv < 50*sec ) return + 5*sec; if( vv < 2.5*mn ) return 12.5*sec; if( vv < 3.75*mn ) return +25*sec; if( vv < 5*mn ) return 37.5*sec; if( vv < 10*mn ) return + 1*mn; if( vv < 20*mn ) return 2*mn; if( vv < 1*hr ) return + 5*mn; if( vv < 1.5*hr ) return 10*mn; if( vv < 3*hr ) return + 15*mn; if( vv < 6*hr ) return 30*mn; if( vv < 12*hr ) return + 1*hr; if( vv < 18*hr ) return 2*hr; if( vv < 1.5*day ) return + 3*hr; if( vv < 2.5*day ) return 6*hr; if( vv < 5*day ) return + 12*hr; if( vv < 10*day ) return 1*day; if( vv < 20*day ) return + 2*day; if( vv < 35*day ) return 4*day; if( vv < 70*day ) return + 7*day; if( vv < 140*day ) return 14*day; if( vv < 1*yr ) return + 1*mth; if( vv < 2*yr ) return 2*mth; if( vv < 2.5*yr ) return + 3*mth; if( vv < 5*yr ) return 6*mth; return 1*yr; }

        Suggestions -- especially re:JS -- welcomed.


        With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority". I'm with torvalds on this
        In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked
Re: A better way of lookup?
by davies (Monsignor) on Apr 26, 2015 at 13:40 UTC

    In my experience, the software that does the best job of lookups is the spreadsheet. I don't know enough about the languages involved in developing them even to have looked at the source code, but I'm pretty sure that things like LibreOffice and Gnumeric are open source. Is pinching the algorithm (if not the code) from one of them an option, or would understanding the code be more work than profit?

    Regards,

    John Davies

Re: A better way of lookup?
by Anonymous Monk on Apr 26, 2015 at 20:12 UTC
    Well, hdb is pretty close, except there's not really a need to guess or search through the array. The smallest step in the compared values is
    $ perl -we 'print log(78894000000/63115200000)'
    0.22314355131421
    

    Using a suitably small log base, one is assured that no more than one value step can occur between log()..log()+1. Here, int(4.5*log(...)) will do nicely.

    #! /usr/bin/perl -l use List::Util qw(first min max); use Data::Dumper; my @G = ( [ 25000, 2500 ], [ 50000, 5000 ], [ 150000, 12500 ], [ 225000, 25000 ], [ 300000, 37500 ], [ 600000, 60000 ], [ 1200000, 120000 ], [ 3600000, 300000 ], [ 5400000, 600000 ], [ 10800000, 900000 ], [ 21600000, 1800000 ], [ 43200000, 3600000 ], [ 64800000, 7200000 ], [ 129600000, 10800000 ], [ 216000000, 21600000 ], [ 432000000, 43200000 ], [ 864000000, 86400000 ], [ 1728000000, 172800000 ], [ 3024000000, 345600000 ], [ 6048000000, 604800000 ], [ 12096000000, 1209600000 ], [ 31557600000, 2629800000 ], [ 63115200000, 5259600000 ], [ 78894000000, 7889400000 ], [ 157788000000, 15778800000 ], ); # construct the lookup table. my @TT = map { my $i=$_; first { $i <= tti($_->[0]) } @G } 0..tti(~0); push @TT, [~0, 31557600000]; print Dumper \@TT; sub tti { -45 + int (4.5 * log(min(max(25000,shift),157788000000))) } sub v2r { my ($v, $i) = ($_[0], tti($_[0])); $TT[$i + ($v >= $TT[$i][0])][1]; } for (map { $_-1, $_, int 1.2*$_ } map $_->[0], @G) { print "v2r($_) == @{[v2r($_)]}"; }

    Split array and combined array variants ought be benched as well. In practice, computing the log might be more cumbersome than a small search.

Re: A better way of lookup?
by KurtSchwind (Chaplain) on Apr 28, 2015 at 11:38 UTC

    So in both perl and javascript you can use the same algorithm if you'd like.

    Start with an arrary with all of your breakpoint values. I'll abridge here.

    var breakpoints = [ 25000 , 50000 , 150000 ]; my @breakpoints = ( 25000 , 50000 , 150000 );

    Then push the new value in. For our example I'll push in 50123.

    breakpoints.push(50123); push(@breakpoints, 50123);

    Sort them. Do an index-of on your value - 1.

    breakpoints.sort(); var return_value = breakpoints[breakpoints.indexOf(50123) - 1]; @breakpoints = sort @breakpoints; # Use your favourite perl method for indexOf like above
    In javascript this is actually cleaner than perl. The sort in javascript doesn't have to do a copy at the end and it has the 'indexOf' operator built in. </code>
    --
    “For the Present is the point at which time touches eternity.” - CS Lewis