File Coverage

blib/lib/Number/AnyBase.pm
Criterion Covered Total %
statement 76 88 86.3
branch 22 24 91.6
condition 3 3 100.0
subroutine 13 24 54.1
pod 19 19 100.0
total 133 158 84.1


line stmt bran cond sub pod time code
1             # ABSTRACT: Converts decimals to and from any alphabet of any size (for shortening IDs, URLs etc.)
2              
3             # no critic
4             package Number::AnyBase;
5             {
6             $Number::AnyBase::VERSION = '1.60000';
7             }
8             ## use critic
9              
10 6     6   200428 use strict;
  6         16  
  6         249  
11 6     6   33 use warnings;
  6         11  
  6         198  
12              
13 6     6   31 use base 'Class::Accessor::Faster';
  6         12  
  6         6063  
14              
15 6     6   40127 use Carp qw(croak);
  6         14  
  6         7854  
16              
17             Number::AnyBase->mk_ro_accessors( qw/
18             alphabet
19             _inverted_alphabet
20             /);
21              
22             sub new {
23 11     11 1 10474 my ($class, @in_alphabet) = @_;
24              
25 11 50       40 croak 'No alphabet passed to Number::AnyBase->new()'
26             unless @in_alphabet;
27              
28 11         26 my $type = ref $in_alphabet[0];
29              
30 11 100       98 my $tmp_alphabet
    100          
31             = $type eq 'ARRAY'
32             ? $in_alphabet[0]
33             : scalar @in_alphabet == 1
34             ? [ split '', $in_alphabet[0] ]
35             : \@in_alphabet;
36              
37 11         28 my %seen;
38             my @normalized_alphabet;
39 11         28 for ( @$tmp_alphabet ) {
40 335 100       846 push @normalized_alphabet, $_ unless $seen{$_}++
41             }
42              
43 11 100       482 croak 'The given alphabet must have at least two symbols'
44             if @normalized_alphabet < 2;
45              
46 9         19 for (@normalized_alphabet) {
47 310 100       723 croak 'Symbols in the given alphabet cannot be more than one character long'
48             if length > 1
49             }
50              
51 8         37 $class->fastnew(\@normalized_alphabet)
52             }
53              
54             sub fastnew {
55 10     10 1 20 my ($class, $alphabet) = @_;
56              
57 10         14 my %inverted_alphabet;
58 10         25 @inverted_alphabet{ @$alphabet } = 0 .. $#{ $alphabet };
  10         256  
59              
60 10         124 $class->SUPER::new({
61             alphabet => $alphabet,
62             _inverted_alphabet => \%inverted_alphabet
63             })
64             }
65              
66             sub new_bin {
67 0     0 1 0 shift->fastnew( ['0', '1'] )
68             }
69              
70             sub new_oct {
71 0     0 1 0 shift->fastnew( ['0'..'7'] )
72             }
73              
74             sub new_hex {
75 0     0 1 0 shift->fastnew( ['0'..'9', 'A'..'F'] )
76             }
77              
78             sub new_hex_lc {
79 0     0 1 0 shift->fastnew( ['0'..'9', 'a'..'f'] )
80             }
81              
82             sub new_base36 {
83 0     0 1 0 shift->fastnew( ['0'..'9', 'A'..'Z'] )
84             }
85              
86             sub new_base62 {
87 0     0 1 0 shift->fastnew( ['0'..'9', 'A'..'Z', 'a'..'z'] )
88             }
89              
90             sub new_base64 {
91 0     0 1 0 shift->fastnew( ['+', '/', '0'..'9', 'A'..'Z', '_', 'a'..'z'] )
92             }
93              
94             sub new_base64url {
95 0     0 1 0 shift->fastnew( ['-', '0'..'9', 'A'..'Z', '_', 'a'..'z'] )
96             }
97              
98             sub new_urisafe {
99 1     1 1 403 shift->fastnew( ['-', '.', '0'..'9', 'A'..'Z', '_', 'a'..'z', '~'] )
100             }
101              
102             sub new_dna {
103 0     0 1 0 shift->fastnew( ['A', 'C', 'G', 'T'] )
104             }
105              
106             sub new_dna_lc {
107 0     0 1 0 shift->fastnew( ['a', 'c', 'g', 't'] )
108             }
109              
110             sub new_ascii {
111             shift->fastnew([
112 1     1 1 663 '!', '"' , '#', '$', '%', '&', "'", '(', ')', '*', '+', '-', '.', '/',
113             '0'..'9' , ':', ';', '<', '=', '>', '?', '@', 'A'..'Z',
114             '[', '\\', ']', '^', '_', '`', 'a'..'z', '{', '|', '}', '~'
115             ])
116             }
117              
118             sub new_bytes {
119 0     0 1 0 shift->fastnew( [ map {chr} 0..255 ] )
  0         0  
120             }
121              
122             sub to_base {
123 61     61 1 43051 my ($self, $dec_num) = @_;
124              
125 61         189 my $alphabet = $self->alphabet;
126 61         371 my $alphabet_size = @{ $alphabet };
  61         101  
127              
128 61         90 my $base_num = '';
129 6     6   6533 use integer;
  6         66  
  6         32  
130 61         76 do { $base_num .= $alphabet->[ $dec_num % $alphabet_size ] }
  549         2391  
131             while $dec_num /= $alphabet_size;
132              
133 61         204 return scalar reverse $base_num
134             }
135              
136             sub to_dec {
137 60     60 1 214 my $self = shift;
138 60         100 my $reversed_base_num = reverse shift;
139              
140 60         159 my $inverted_alphabet = $self->_inverted_alphabet;
141 60         223 my $alphabet_size = @{ $self->alphabet };
  60         138  
142              
143             # Make $dec_num a bignum upon request.
144 60 50       276 my $dec_num = defined $_[0] ? $_[0] * 0 : 0;
145              
146             #$base_num = reverse $base_num;
147 60         846 $dec_num
148             = $dec_num * $alphabet_size
149             + $inverted_alphabet->{ chop $reversed_base_num }
150             while length $reversed_base_num;
151              
152 60         313 return $dec_num
153             }
154              
155             sub next {
156 100000     100000 1 362680 my ($self, $curr) = @_;
157              
158 100000         260416 my $alphabet = $self->alphabet;
159 100000         400158 my $alphabet_size = @{ $alphabet };
  100000         175012  
160 100000         281352 my $inverted_alphabet = $self->_inverted_alphabet;
161              
162 100000         375951 my $next_char_value;
163 100000         135614 my $next_num = '';
164 100000         206358 while ( length $curr ) {
165 107686 100       261950 if (
166             ( $next_char_value = $inverted_alphabet->{chop $curr} + 1 )
167             < $alphabet_size
168             ) {
169 99996         131197 $next_num .= $alphabet->[$next_char_value];
170             last
171 99996         115522 } else {
172 7690         25380 $next_num .= $alphabet->[0]
173             }
174             }
175              
176 100000 100       192947 $next_num .= $alphabet->[1] if $next_char_value >= $alphabet_size;
177              
178 100000         457275 return $curr . reverse $next_num
179             }
180              
181             sub prev {
182 100000     100000 1 1542664 my ($self, $curr) = @_;
183              
184 100000         309334 my $alphabet = $self->alphabet;
185 100000 100       616003 return if $curr eq $alphabet->[0];
186              
187 99999         313036 my $inverted_alphabet = $self->_inverted_alphabet;
188              
189 99999         431427 my $prev_char_value;
190 99999         144283 my $prev_num = '';
191 99999         253719 while ( length $curr ) {
192 107689 100       249697 if (
193             ( $prev_char_value = $inverted_alphabet->{chop $curr} ) == 0
194             ) {
195 7690         20421 $prev_num .= $alphabet->[-1]
196             } else {
197 99999         170351 $prev_num .= $alphabet->[$prev_char_value - 1];
198             last
199 99999         140124 }
200             }
201              
202 99999 100 100     290257 chop $prev_num if $prev_char_value == 1 && !length($curr);
203              
204 99999         488543 return $curr . reverse $prev_num
205             }
206              
207             1;
208              
209             __END__