The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Number::AnyBase - Converts decimals to and from any alphabet of any size (for shortening IDs, URLs etc.)

VERSION

version 1.00000

SYNOPSIS

    use strict;
    use warnings;
    
    use Number::AnyBase;
    
    # 62 symbols alphabet
    my @alphabet = (0..9, 'A'..'Z', 'a'..'z');
    my $conv = Number::AnyBase->new(\@alphabet);
    my $base62_num = $conv->to_base(123456);     # W7E
    my $dec_num    = $conv->to_dec($base62_num); # back to 123456
    
    use feature 'say';
    
    # URI unreserved characters alphabet
    my $uri_conv = Number::AnyBase->new_urisafe;
    say $uri_conv->to_base(1234567890); # ~2Bn4
    say $uri_conv->to_dec( '~2Bn4' );   # 1234567890
    
    # ASCII printable characters alphabet
    my $ascii_conv = Number::AnyBase->new_ascii;
    say $ascii_conv->to_base(199_000_000_000); # >Z8X<8
    say $ascii_conv->to_dec( '>Z8X<8' );       # 199000000000
    
    # Hexadecimal base
    my $hex_conv = Number::AnyBase->new( 0..9, 'A'..'F' );
    say $hex_conv->to_base(2047);   # 7FF
    say $hex_conv->to_dec( '7FF' ); # 2047
    
    # Morse-like alphabet :-)
    my $morse_conv = Number::AnyBase->new( '_.' );
    say $morse_conv->to_base(99);         # ..___..
    say $morse_conv->to_dec( '..___..' ); # 99
    
    {
        # Unicode alphabet (webdings font);
        use utf8;
        binmode STDOUT, ':utf8';
        my $webdings_conv = Number::AnyBase->new(
            '♣♤♥♦☭☹☺☻✈✪✫✭✰✵✶✻❖♩♧♪♫♬⚓⚒⛔✼✾❁❂❄❅❊☿⚡⚢⚣⚤⚥⚦⛀⛁⛦⛨'
        );
        say $webdings_conv->to_base(1000000000); # ☺⚢♬♬⚥⛦
        say $webdings_conv->to_dec( '☺⚢♬♬⚥⛦' ); # 1000000000
    }
    
    # Fast native unary increment/decrement
    my $sequence = Number::AnyBase->fastnew(['A'..'Z']);
    say $sequence->next('ZZZ');  # BAAA
    say $sequence->prev('BAAA'); # ZZZ

DESCRIPTION

First the intended usage scenario: this module has been conceived to shorten ids, URLs etc., like the most popular URL shortening services do (think of TinyURL and similar).

Then a bit of theory: an id is (or can anyway be mapped to) just a number, therefore it can be represented in any base. The longer is the alphabet of the base, the shorter the number representation will be (in terms of symbols of the said alphabet). This module converts any non-negative decimal integer (including Math::BigInt-compatible objects) to any given base/alphabet and vice versa, thus giving the shortest possible representation for the original number/id (at least for a collision-free transformation).

The suggested workflow to shorten your ids is therefore the following:

  1. when storing an item in your data store, generate a decimal id for it (for example through the SEQUENCE field type offered by many DBMSs);

  2. shorten the said decimal id through the "to_base" method explained below;

  3. publish the shortened id rather than the (longer) original decimal id.

When receiving a request for a certain item through its corresponding shortened id you've published:

  1. obtain the corresponding original decimal id through the "to_dec" method explained below;

  2. retrieve the requested item in your data store through its original decimal id you've obtained at the previous step;

  3. serve the requested item.

Of course one can also save the shortened id along with the item in the data store, thus saving the to_dec conversion at the step 1 above (using the shortened id rather than the decimal one in the subsequent step 2).

Through the fast native unary increment/decrement offered by the "next" and "prev" methods, it is even possible to skip the decimal ids generation and the conversion steps altogether.

A couple of similar modules were already present on CPAN, but for one reason or another I did not find them completely satisfactory: for a detailed explanation, please see the "COMPARISON" section below.

METHODS

Constructors

new

  • Number::AnyBase->new( @alphabet )

  • Number::AnyBase->new( \@alphabet )

  • Number::AnyBase->new( $alphabet )

This is the constructor method, which initializes and returns the converter object. It requires an alphabet, that is the set of symbols to represent the converted numbers (the size of the base is the number of symbols of the provided alphabet).

An exception is thrown if no alphabet is passed to new.

The alphabet can be passed as a list or as a listref of characters, or packed into a string (in which case the alphabet is obtained by splitting the string into its individual characters).

For example the following three invocations return exactly the same object:

    $conv = Number::AnyBase->new( '0'..'9', 'a'..'z' );
    
    # Same as above
    $conv = Number::AnyBase->new( ['0'..'9', 'a'..'z'] );
    
    # The same through a string
    $conv = Number::AnyBase->new( '0123456789abcdefghijklmnopqrstuvwxyz' );

An alphabet must have at least two symbols (that is, at least two distinct characters), otherwise an excpetion is thrown. Any duplicate character is automatically removed, so for example:

    $conv = Number::AnyBase->new( 'a'..'z', '0'..'9' );
    
    # Exactly the same as above
    $conv = Number::AnyBase->new( 'a'..'z', '0'..'9', qw/a b c d z z z/ );
    
    # Error: an alphabet with a single symbol has been passed
    $conv = Number::AnyBase->new( 'aaaaaaaaaaaaaaaa' );

As a single symbol alphabet is not admissible, when new is called with a single (string) parameter, it is interpreted as a string containing the whole alphabet and not as a list containing a single (multichar) symbol. In other words, if you want to pass the alphabet as a list, it must contain at least two elements.

The alphabet can't contain symbols longer than one character, otherwise an exception is thrown. Note that this can happen only when the alphabet is passed as a list or a listref, since when a (single) string is given to new, the alphabet is obtained by splitting the string into its individual characters (and the possible duplicate characters are removed), so no multichar symbols are ever created in this case:

    # Error: the last symbol in the provided alphabet (as a list) is two characters long
    Number::AnyBase->new( qw/z z z aa/ );
    
    # This is instead correct since the alphabet will be: 'z', 'a'
    Number::AnyBase->new( 'zzzaa' );

fastnew

  • Number::AnyBase->fastnew( \@alphabet )

This is an alternative, faster constructor, which skips all of the checks performed by new (if an illegal alphabet is passed, the behavior is currently indeterminate).

It only accepts a listref.

Specialized constructors

Several constructors with ready-made alphabets are offered as well.

new_urisafe

It builds and returns a converter to/from an alphabet made by the unreserved URI characters, as per the RFC3986. More precisely, it is the same as:

    Number::AnyBase->fastnew( ['-', '.', '0'..'9', 'A'..'Z', '_', 'a'..'z', '~'] );

new_base36

The same as:

    Number::AnyBase->fastnew( ['0'..'9', 'A'..'Z'] );

new_base62

The same as:

    Number::AnyBase->fastnew( ['0'..'9', 'A'..'Z', 'a'..'z'] );

new_base64

The same as:

    Number::AnyBase->fastnew( ['A'..'Z', 'a'..'z', '0'..'9', '+', '/'] );

new_base64url

The same as:

    Number::AnyBase->fastnew( ['A'..'Z', 'a'..'z', '0'..'9', '-', '_'] );

new_bin

It builds a binary converter. The same as:

    Number::AnyBase->fastnew( ['0', '1'] );

new_oct

It builds an octal converter. The same as:

    Number::AnyBase->fastnew( ['0'..'7'] )

new_hex

It builds an hexadecimal converter. The same as:

    Number::AnyBase->fastnew( ['0'..'9', 'A'..'F'] );

new_ascii

It builds and returns a converter to/from an alphabet composed of all the printable ASCII characters except the space. More precisely, it is the same as:

    Number::AnyBase->fastnew([
        '!', '"' , '#', '$', '%', '&', "'", '(', ')', '*', '+', '-', '.', '/',
        '0'..'9' , ':', ';', '<', '=', '>', '?', '@', 'A'..'Z',
        '[', '\\', ']', '^', '_', '`', 'a'..'z', '{', '|', '}', '~'
    ]);

to_base

  • $string = $converter->to_base( $decimal )

This is the method which transforms the given decimal number into its representation in the new base, as shown in the "SYNOPSIS" above.

It works only on decimal non-negative integers (including 0). For speed reasons, no check is performed on the given number: in case it is illegal, the behavior is currently indeterminate.

It works transparently also on Math::BigInt-compatible objects (that is, any object which overloads the arithmetic operators like Math::BigInt does): just pass any such big number and you will get the correct result:

    use Math::BigInt; # Or use Math::GMP;
    Math::BigInt->accuracy(60); # For example
    
    my $bignum = Math::BigInt->new( '123456789012345678901234567890123456789012345678901234567890' ); # Or Math::GMP->new(...)
    
    my $conv = Number::AnyBase->new_base62;
    
    my $base_num = $conv->to_base( $bignum ); # sK0FUywPQsEhMwNhdPBZJcA9KumP0WpD0

This permits to freely choose any Math::BigInt option (the accuracy, as shown above, or the backend library etc.), or to use any other compatible class, such as, for example, Math::GMP.

to_dec

  • $decimal_number = $converter->to_base( $base_num )

  • $decimal_bignumber = $converter->to_base( $base_num, $bigint_obj )

This is the method which converts the transformed number (or rather string) back to its decimal representation, as exemplified in the "SYNOPSIS" above.

For speed reasons, no check is performed on the given string, which could be inconsistent (for example because it contains characters not present in the current alphabet): in this case the behavior is currently indeterminate.

It accepts a second optional parameter, which should be a Math::BigInt-compatible object (it does not matter if it is initialized or not), which tells to_base that a bignum result is requested. It is necessary only when the result is too large to be held by a native perl integer (though, other than slowing down the conversion, it does not harm anyway).

The passed bignum object is used for the internal calculations so, though unusual, this interface permits to have the maximum flexibility, as it completely decouples the bignum library, permitting the user to freely choose any Math::BigInt option as well as any (faster) Math::BigInt-compatible alternative (such as Math::GMP):

    use Math::BigInt; # Or use Math::GMP;
    Math::BigInt->accuracy(60); # For example
    
    my $conv = Number::AnyBase->new_base62;
    
    my $big_dec_num = $conv->to_dec( 'sK0FUywPQsEhMwNhdPBZJcA9KumP0WpD0', Math::BigInt->new ); # Or Math::GMP->new
    # $big_dec_num is now a Math::BigInt object which stringifies to:
    # 123456789012345678901234567890123456789012345678901234567890

next

  • $string = $converter->next( $base_num )

This method performs an optimized native unary increment on the given converted number/string, returning the next number/string in the current base (see also the "SYNOPSYS" above):

    $next_base_num = $converter->next($base_num);

It is over 2x faster than the conversion roundtrip:

    $next_base_num = $converter->to_base( $converter->to_dec($base_num) + 1 );

(see the benchmark/native_sequence.pl benchmark included in the distribution). It therefore offers an efficient way to get the next id from the last (converted) id stored in a db, for example.

prev

  • $string = $converter->prev($base_num)

This method performs an optimized native unary decrement on the given converted number/string, returning the previous number/string in the current base (see also the "SYNOPSYS" above):

    $prev_base_num = $converter->prev($base_num);

It is over 2x faster than the conversion roundtrip:

    $prev_base_num = $converter->to_base( $converter->to_dec($base_num) - 1 );

When called on the zero of the base, it returns undef.

alphabet

  • $listref = $converter->alphabet

Read-only method which returns the alphabet of the current target base, as a listref.

COOKBOOK

Security

This module focuses only on converting numbers from decimals to any base/alphabet and vice versa, therefore it has nothing to do with security, that is, given a number/string and the alphabet it is represented on, the next (through an unary increment) number/string is guessable. If you want your (converted) id sequence not to be guessable, the solution is however simple: just randomize your decimal numbers upfront, leaving large random gaps in the set. Then feed the randomized decimals to this module to have them shortened.

Sorting

Characters ordering in the given alphabet does matter: if it is desidered that converting a sorted sequence of decimals produces a sorted sequence of strings (when properly padded of course), the characters in the provided alphabet must be sorted as well.

An alphabet with unsorted characters can be used to make the converted numbers somewhat harder to guess.

Note that the predefined constructors always use sorted alphabets.

Speed

For maximum speed, as a constructor use fastnew or any of the predefined constructors, resorting to new only when it is necessary to perform the extra checks.

Conversion speed maximization does not require any trick: as long as big numbers are not used, the calculations are performed at the full perl native integers speed.

Big numbers of course slow down the conversions but, as shown above, performances can be fine-tuned, for example by properly setting the Math::BigInt precision and accuracy, by choosing a faster back-end library, or by using Math::GMP directly in place of Math::BigInt (advised).

As already said, the optimized native unary increment [decrement] provided by next [prev] is over 2x faster than the to_dec/to_base conversion rountrip. However, if a sequence of converted numbers must be generated, and such sequence is large enough so that the first to_dec() call can be amortized, using to_base() (only) is marginally faster than using next:

    use Number::AnyBase;
    
    use constant SEQ_LENGTH => 10_000;
    
    my $conv = Number::AnyBase->new( 0..9, 'A'..'Z', 'a'..'z' );
    my (@seq1, @seq2);
    my $base_num = 'zzzzzz';
    
    # @seq1 construction through native increment:
    my $next = $base_num;
    push @seq1, $next = $conv->next($next) for 1..SEQ_LENGTH;
    
    # @seq2 construction through to_base is marginally faster than @seq1:
    my $dec_num = $conv->to_dec($base_num);
    push @seq2, $conv->to_base( $dec_num + $_ ) for 1..SEQ_LENGTH;

See the benchmark/native_sequence.pl benchmark script included in the distribution.

COMPARISON

Here is a brief and completely biased comparison with Math::BaseCalc, Math::BaseConvert and Math::Base::Convert, which are similar CPAN modules.

For the performance claims, please see the benchmark/other_cpan_modules.pl benchmark script included in the distribution. Also note that the coversion speed gaps tend to increase with the numbers size.

  • vs Math::BaseCalc

    • Pros

      • Number::AnyBase is faster: decimal->base conversion is about 2x (100%) faster, base->decimal conversion is about the same, fastnew is about 20% faster than Math::BaseCalc::new.

      • Base->decimal conversion in Number::AnyBase can return Math::BigInt (or similar) objects upon request, while Math::BaseCalc only returns native perl integers, thus producing wrong results when the decimal number is too large.

      • Math::BaseCalc lacks the fast native unary increment/decrement offered by Number::Anybase, which permits an additional 2x speedup.

    • Cons

      • Math::BaseCalc::new converts also negative integers, while Number::AnyBase only converts non-negative integers (this feature has been considered not particularly important and therefore traded for speed in Number::AnyBase).

  • vs Math::BaseConvert

    • Pros

      • With native perl integers, Number::AnyBase is hugely faster: something like 200x faster in decimal->base conversion and 130x faster in base->decimal conversion (using Math::BaseConvert::cnv).

      • With big integers (60 digits), Number::AnyBase (using Math::GMP) is still faster: over 13x faster in both decimal->base conversion and base->decimal conversion; though much less, it's faster even using Math::BigInt with its pure-perl backend.

      • Math::BaseConvert has a weird API: first it has a functional interface, which is not ideal for code which has to maintain its internal state. Then, though a custom alphabet can be set (through a state-changing function called dig), every time cnv is called, the target alphabet size must anyway be given.

      • Math::BaseConvert doesn't permit to use a bignum library other than Math::BigInt, nor it permits to set any Math::BigInt option.

      • Math::BaseConvert lacks the fast native unary increment/decrement offered by Number::Anybase, which permits an additional 2x speedup.

    • Cons

      • Math::BaseConvert manages big numbers transparently (but this makes it extremely slow and does not permit to use a library other than Math::BigInt, as already said).

      • Math::BaseConvert can convert numbers between two arbitrary bases with a single call.

      • Math::BaseConvert converts also negative integers.

  • vs Math::Base::Convert

    • Pros

      • With native perl integers, Number::AnyBase is largely faster: something like over 15x faster in decimal->base conversion and over 22x faster in base->decimal conversion (using the Math::Base::Convert object API, which is the recommended one for speed); fastnew is over 70% faster than Math::Base::Convert::new

      • With big integers (60 digits), Number::AnyBase (using Math::GMP) is still faster: about 15% faster in decimal->base conversion and about 100% faster in base->decimal conversion.

      • Though generally better, Math::Base::Convert preserves some of the Math::BaseConvert API shortcomings: to convert numbers bidirectionally between base 10 to/from another given base, two different objects must be istantiated (or the bases must passed each time through the functional API).

      • Math::Base::Convert lacks the fast native unary increment/decrement offered by Number::Anybase, which permits an additional 2x speedup.

      • Possible minor glitch: some of the predefined alphabets offered by Math::Base::Convert are not sorted.

    • Cons

      • Math::Base::Convert manages big numbers transparently and natively, i.e. without resorting to Math::BigInt or similar modules (but, though not as pathologically slow as Math::BaseConvert, this makes Math::Base::Convert massively slow as well, when native perl integers can be used).

      • On big integers, if Number::AnyBase uses Math::BigInt with its pure-perl engine, Math::Base::Convert is faster: about 11x in decimal->base conversion and about 6x in in base->decimal conversion (as already said, Number::AnyBase can however use Math::GMP and be faster even with big integers).

      • Math::Base::Convert can convert numbers between two arbitrary bases with a single call.

      • Math::Base::Convert converts also negative integers.

All of the reviewed modules are pure-perled, though the Math::GMP module that Number::AnyBase can (optionally) use to maximize its speed with big numbers it's not. Note however that the Number::AnyBase fast native unary increment/decrement work even on big numbers without any external module.

SEE ALSO

BUGS

No known bugs.

Please report any bugs or feature requests to bug-number-AnyBase at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Number-AnyBase. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Number::AnyBase

You can also look for information at:

ACKNOWLEDGEMENTS

Many thanks to the IPW (Italian Perl Workshop) organizers and sponsors: they run a fascinating an inspiring event.

AUTHOR

Emanuele Zeppieri <emazep@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Emanuele Zeppieri.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.