File Coverage

blib/lib/Number/Nary.pm
Criterion Covered Total %
statement 100 101 99.0
branch 31 34 91.1
condition 2 3 66.6
subroutine 21 21 100.0
pod 3 3 100.0
total 157 162 96.9


line stmt bran cond sub pod time code
1 5     5   120531 use 5.006;
  5         20  
  5         188  
2 5     5   28 use warnings;
  5         9  
  5         153  
3 5     5   26 use strict;
  5         10  
  5         347  
4             package Number::Nary;
5             {
6             $Number::Nary::VERSION = '1.100312';
7             }
8             # ABSTRACT: encode and decode numbers as n-ary strings
9              
10 5     5   27 use Carp qw(croak);
  5         7  
  5         421  
11 5     5   31 use Scalar::Util 0.90 qw(reftype);
  5         167  
  5         542  
12 5     5   4375 use List::MoreUtils 0.09 qw(uniq);
  5         5757  
  5         419  
13 5     5   3776 use UDCode ();
  5         3115  
  5         237  
14              
15 5         82 use Sub::Exporter -setup => {
16             exports => [ qw(n_codec n_encode n_decode) ],
17             groups => {
18             default => [ qw(n_codec) ],
19             codec_pair => \&_generate_codec_pair,
20             }
21 5     5   4762 };
  5         71937  
22              
23             sub _generate_codec_pair {
24 2     2   253 my (undef, undef, $arg, undef) = @_;
25              
26 2         7 my $local_arg = {%$arg};
27 2         5 my $digits = delete $local_arg->{digits};
28              
29 2         2 my %pair;
30 2         8 @pair{qw(encode decode)} = n_codec($digits, $local_arg);
31 2         7 return \%pair;
32             }
33              
34              
35             sub _split_len_iterator {
36 10     10   24 my ($length) = @_;
37              
38             return sub {
39 20     20   67 my ($string, $callback) = @_;
40              
41 20         46 my $places = length($string) / $length;
42              
43 20 100       179 croak "string length is not a multiple of digit length"
44             unless $places == int $places;
45              
46 19         46 for my $position (1 .. $places) {
47 71         127 my $digit = substr $string, (-$length * $position), $length;
48 71         118 $callback->($digit, $position);
49             }
50             }
51 10         72 }
52              
53             sub _split_digit_iterator {
54 2     2   4 my ($digits) = @_;
55              
56             sub {
57 1     1   3 my ($string, $callback) = @_;
58 1         2 my @digits;
59 1         5 ITER: while (length $string) {
60 4         10 for (@$digits) {
61 14 100       35 if (index($string, $_) == 0) {
62 4         13 push @digits, substr($string, 0, length $_, '');
63 4         14 next ITER;
64             }
65             }
66 0         0 croak "could not decompose string '$string'";
67             }
68              
69 1         5 for (1 .. @digits) {
70 4         11 $callback->($digits[-$_], $_);
71             }
72             }
73 2         16 }
74              
75             sub _set_iterator {
76 14     14   23 my ($digits, $length_ref) = @_;
77              
78 14 50       44 croak "digit set is empty" unless @$digits;
79             croak "digit set contains zero-length digit"
80 5 50   5   3863 if do { no warnings 'uninitialized'; grep { ! length $_ } @$digits };
  5         14  
  5         3258  
  14         18  
  14         30  
  160         268  
81 14 100       370 croak "digit set contains repeated digits" if @$digits != uniq @$digits;
82              
83 13         52 my @lengths = uniq map { length } @$digits;
  154         272  
84              
85 13 100       85 return _split_len_iterator($lengths[0]) if @lengths == 1;
86              
87 3 100       14 croak "digit set may be ambiguous" if ! UDCode::is_udcode(@$digits);
88              
89 2         255 return _split_digit_iterator($digits);
90             }
91              
92             sub n_codec {
93 14     14 1 1578 my ($digit_set, $arg) = @_;
94              
95 14         21 my @digits;
96              
97 14 100       45 if (ref $digit_set) {
98 6 50       39 croak "digit set must be a string or arrayref"
99             unless reftype $digit_set eq 'ARRAY';
100 6         29 @digits = @$digit_set;
101             } else {
102 8         57 @digits = split //, $digit_set;
103             }
104              
105 14         43 my $iterator = _set_iterator(\@digits);
106              
107             my $encode_sub = sub {
108 22     22   3794 my ($value) = @_;
109              
110 22 100 66     7326 croak "value isn't an non-negative integer"
111             if not defined $value
112             or $value !~ /\A\d+\z/;
113              
114 14         26 my $string = '';
115              
116 14 100       40 if (@digits == 1) {
117 2         7 $string = $digits[0] x $value;
118             } else {
119 12         58 while (1) {
120 32         91 my $digit = $value % @digits;
121 32         71 $value = int($value / @digits);
122 32         60 $string = "$digits[$digit]$string";
123 32 100       83 last unless $value;
124             }
125             }
126              
127 14 100       47 $string = $arg->{postencode}->($string) if $arg->{postencode};
128 14         75 return $string;
129 12         52 };
130              
131 12         19 my %digit_value = do { my $i = 0; map { $_ => $i++ } @digits; };
  12         18  
  12         25  
  151         293  
132              
133             my $decode_sub = sub {
134 22     22   11629 my ($string) = @_;
135 22 100       61 return unless defined $string;
136              
137 21 100       69 $string = $arg->{predecode}->($string) if $arg->{predecode};
138              
139 21         53 my $value = 0;
140              
141             $iterator->($string, sub {
142 75     26   101 my ($digit, $position) = @_;
143 75 100       437 croak "string to decode contains invalid digits"
144             unless exists $digit_value{$digit};
145              
146             # Stupid hack, but I'm just cramming unary support in here at the moment.
147             # It can be polished up later, if needed. -- rjbs, 2009-11-22
148 73 100       264 $value += @digits == 1
149             ? 1
150             : ($digit_value{$digit} * @digits ** ($position++ - 1));
151 21         116 });
152              
153 18         132 return $value;
154 12         69 };
155              
156 12         62 return ($encode_sub, $decode_sub);
157             }
158              
159              
160             # If you really can't stand using n_codec, you could memoize these.
161 1     1 1 314 sub n_encode { (n_codec($_[1]))[0]->($_[0]) }
162 3     3 1 773 sub n_decode { (n_codec($_[1]))[1]->($_[0]) }
163              
164              
165             1; # my ($encode_sub, $decode_sub) = n_codec('8675309'); # jennynary
166              
167             __END__