File Coverage

blib/lib/Lingua/Abbreviate/Hierarchy.pm
Criterion Covered Total %
statement 115 117 98.2
branch 38 50 76.0
condition 10 17 58.8
subroutine 22 22 100.0
pod 4 4 100.0
total 189 210 90.0


line stmt bran cond sub pod time code
1             package Lingua::Abbreviate::Hierarchy;
2              
3 3     3   21425 use warnings;
  3         6  
  3         82  
4 3     3   14 use strict;
  3         5  
  3         84  
5              
6 3     3   15 use Carp qw( croak );
  3         10  
  3         186  
7 3     3   16 use List::Util qw( min max );
  3         3  
  3         5280  
8              
9             =head1 NAME
10              
11             Lingua::Abbreviate::Hierarchy - Shorten verbose namespaces
12              
13             =head1 VERSION
14              
15             This document describes Lingua::Abbreviate::Hierarchy version 0.04
16              
17             =cut
18              
19             our $VERSION = '0.04';
20              
21             =head1 SYNOPSIS
22              
23             use Lingua::Abbreviate::Hierarchy;
24             my $abr = Lingua::Abbreviate::Hierarchy->new( keep => 1 );
25              
26             $abr->add_namespace(qw(
27             comp.lang.perl.misc
28             comp.lang.perl.advocacy
29             ));
30              
31             # gets 'c.l.p.misc'
32             my $clpm = $abr->ab('comp.lang.perl.misc');
33              
34             # abbreviate an array
35             my @ab = $abr->ab(qw(
36             comp.lang.perl.misc
37             comp.lang.perl.advocacy
38             ));
39              
40             =head1 DESCRIPTION
41              
42             It's a common practice to abbreviate the elements of namespaces
43             like this:
44              
45             comp.lang.perl.misc -> c.l.p.misc
46             comp.lang.perl.advocacy -> c.l.p.advocacy
47              
48             This module performs such abbreviation. It guarantees that generated
49             abbreviations are long enough to be unique within the current namespace.
50              
51             =head1 INTERFACE
52              
53             To abbreviate names within a namespace use the module:
54              
55             use Lingua::Ab::H; # use abbreviated name
56              
57             Create a new abbreviator:
58              
59             my $abr = Lingua::Ab::H->new( keep => 1 );
60              
61             Set up the namespace:
62              
63             $abr->add_namespace(qw(
64             comp.lang.perl.misc
65             comp.lang.perl.advocacy
66             ));
67              
68             Get your abbreviations:
69              
70             # gets 'c.l.p.misc'
71             my $clpm = $abr->ab('comp.lang.perl.misc');
72              
73             # abbreviate an array
74             my @ab = $abr->ab(qw(
75             comp.lang.perl.misc
76             comp.lang.perl.advocacy
77             ));
78              
79             Often the namespace will be larger; for example if you wanted to
80             generate abbreviations that would be unique within the entire
81             comp.lang.* hierarchy you would add all the terms in that space to the
82             abbreviator.
83              
84             =head2 C<< new >>
85              
86             Create a new abbreviator. Options may be passed as key, value pairs:
87              
88             my $abr = Lingua::Ab::H->new(
89             keep => 1,
90             sep => '::'
91             );
92              
93             The following options are recognised:
94              
95             =over
96              
97             =item C<< sep => >> I
98              
99             The string that separates components in the namespace. For example '.'
100             for domain names or '::' for Perl package names;
101              
102             =item C<< only => >> I
103              
104             Abbreviate only the initial I elements in the name.
105              
106             =item C<< keep => >> I
107              
108             Leave I elements at the end of the name unabbreviated.
109              
110             =item C<< max => >> I
111              
112             Abbreviate from the left until the generated abbreviation contains I
113             or fewer characters. If C is specified then at least that many
114             elements will be abbreviated. If C is specified that many trailing
115             elements will be unabbreviated.
116              
117             May return more than I characters if the fully abbreviated name is
118             still too long.
119              
120             =item C<< trunc => >> I
121              
122             A truncation string (which may be empty). When C is supplied the
123             generated abbreviation will always be <= C characters and will be
124             prefixed by the truncation string.
125              
126             =item C<< flip => >> I
127              
128             Normally we consider the namespace to be rooted at the left (like a
129             filename or package name). Set C to true to process right-rooted
130             namespaces (like domain names).
131              
132             =item C<< ns => >> I
133              
134             Supply a reference to an array containing namespace terms. See
135             C for more details.
136              
137             =back
138              
139             =cut
140              
141             {
142             my %DEFAULT = (
143             sep => '.',
144             only => undef,
145             keep => undef,
146             max => undef,
147             trunc => undef,
148             flip => 0,
149             );
150              
151             sub new {
152 11     11 1 8320 my ( $class, %options ) = @_;
153              
154 11         31 my $ns = delete $options{ns};
155 11         33 my @unk = grep { !exists $DEFAULT{$_} } keys %options;
  10         35  
156 11 50       37 croak "Unknown option(s): ", join ', ', sort @unk if @unk;
157 11         92 my $self = bless { %DEFAULT, %options, ns => {} }, $class;
158 11         38 @{$self}{ 'flipa', 'flips' }
159             = $self->{flip}
160 73     18   369 ? ( sub { reverse @_ }, sub { $_[0] } )
  0         0  
161 11 100   484   106 : ( sub { @_ }, sub { scalar reverse $_[0] } );
  484         2190  
  6         77  
162 11 50       64 $self->add_namespace( $ns ) if defined $ns;
163 11         61 return $self;
164             }
165             }
166              
167             =head2 C<< add_namespace >>
168              
169             Add terms to the abbreviator's namespace:
170              
171             $abr->add_namespace( 'foo.com', 'bar.com' );
172              
173             When abbreviating a term only those elements of the term that fall
174             within the namespace will be abbreviated. Elements outside the namespace
175             will be untouched.
176              
177             =cut
178              
179             sub add_namespace {
180 11     11 1 16 my $self = shift;
181 11 50       32 croak "Can't add to namespace after calling ab()"
182             if $self->{cache};
183 11         28 my $sepp = quotemeta $self->{sep};
184 11 50       22 for my $term ( map { 'ARRAY' eq ref $_ ? @$_ : $_ } @_ ) {
  11         82  
185 116         396 my @path = $self->{flipa}( split /$sepp/o, $term );
186 116         330 $self->{ns} = $self->_add_node( $self->{ns}, @path );
187             }
188             }
189              
190             sub _add_node {
191 398     398   764 my ( $self, $nd, $wd, @path ) = @_;
192 398   100     902 $nd ||= {};
193 398   100     1140 $nd->{$wd} ||= {};
194 398 100       663 if ( @path ) {
195 282         674 $nd->{$wd}{k} = $self->_add_node( $nd->{$wd}{k}, @path );
196             }
197             else {
198 116         247 $nd->{$wd}{t} = 1;
199             }
200 398         1075 return $nd;
201             }
202              
203             =head2 C<< ab >>
204              
205             Abbreviate one or more terms:
206              
207             my $short = $abr->ab( 'this.is.a.long.name' );
208              
209             Or with an array:
210              
211             my @short = $abr->ab( @long );
212              
213             =cut
214              
215             sub ab {
216 11     11 1 45 my $self = shift;
217 11 50       85 $self->_init unless $self->{cache};
218 11   33     20 my @ab = map { $self->{cache}{$_} ||= $self->_abb( $_ ) } @_;
  116         549  
219 11 50       107 return wantarray ? @ab : $ab[0];
220             }
221              
222             sub _abb {
223 116     116   157 my ( $self, $term ) = @_;
224              
225 116         182 my $sepp = quotemeta $self->{sep};
226 116         487 my @path = $self->{flipa}( split /$sepp/, $term );
227 116         278 my $join = $self->_join;
228             my $abc = sub {
229 215     215   462 my ( $cnt, @path ) = @_;
230 215         482 join $join,
231             $self->{flipa}( $self->_ab( $self->{ns}, $cnt, @path ) );
232 116         410 };
233              
234 116 100       250 if ( defined( my $max = $self->{max} ) ) {
235 33   50     119 my $from = $self->{only} || 0;
236 33   50     117 my $to = scalar( @path ) - ( $self->{keep} || 0 );
237 33         40 my $ab = $term;
238 33         56 for my $cnt ( $from .. $to ) {
239 132         216 $ab = $abc->( $cnt, @path );
240 132 100       594 return $ab if length $ab <= $max;
241             }
242 2 50       9 if ( defined( my $trunc = $self->{trunc} ) ) {
243 2         3 my $flp = $self->{flips};
244             my $trc = sub {
245 2     2   6 my ( $tr, $a ) = @_;
246 2 50       11 return substr $tr, 0, $max if length $tr > $max;
247 2         9 return substr( $a, 0, $max - length $tr ) . $tr;
248 2         10 };
249 2         6 return $flp->( $trc->( $flp->( $trunc ), $flp->( $ab ) ) );
250             }
251 0         0 return $ab;
252             }
253             else {
254 83         95 my $lt = scalar @path;
255 83 100       218 $lt = max( $lt - $self->{keep}, 0 ) if defined $self->{keep};
256 83 100       191 $lt = min( $lt, $self->{only} ) if defined $self->{only};
257 83         162 return $abc->( $lt, @path );
258             }
259             }
260              
261             sub _ab {
262 752     752   1484 my ( $self, $nd, $limit, $word, @path ) = @_;
263 752 100       1909 return $word, @path if $limit-- <= 0;
264 597 100 66     2554 return $word, @path unless $nd && $nd->{$word};
265 571 100       2061 return ( $nd->{$word}{a},
266             @path ? $self->_ab( $nd->{$word}{k}, $limit, @path ) : () );
267             }
268              
269             =head2 C
270              
271             Expand an abbreviation created by calling C. When applied to
272             abbreviations created in the current namespace C will reliably
273             expand arbitrary abbreviated terms. It will also pass through
274             non-abbreviated terms unmolested.
275              
276             If the namespace for expansion is not identical to the namespace for
277             abbreviation then the results are unpredictable.
278              
279             my @ab = $abr->ab( @terms ); # Abbreviate terms...
280             my @ex = $abr->ex( @ab ); # ...and get them back
281              
282             =cut
283              
284             sub ex {
285 5     5 1 31 my $self = shift;
286 5 50       29 $self->_init_rev unless $self->{rev};
287 5   33     10 my @ex = map { $self->{rcache}{$_} ||= $self->_exx( $_ ) } @_;
  55         282  
288 5 50       50 return wantarray ? @ex : $ex[0];
289             }
290              
291             sub _join {
292 171     171   192 my $self = shift;
293 171 50       493 return defined $self->{join} ? $self->{join} : $self->{sep};
294             }
295              
296             sub _exx {
297 55     55   81 my ( $self, $term ) = @_;
298 55         94 my $sepp = quotemeta $self->_join;
299 55         212 my @path = $self->{flipa}( split /$sepp/, $term );
300 55         178 return join $self->{sep},
301             $self->{flipa}( $self->_ab( $self->{rev}, scalar @path, @path ) );
302             }
303              
304             sub _rev {
305 51     51   63 my ( $self, $nd ) = @_;
306 51         64 my $ond = {};
307 51         130 while ( my ( $k, $v ) = each %$nd ) {
308 93         341 my $nnd = { %$v, a => $k };
309 93 100       270 $nnd->{k} = $self->_rev( $nnd->{k} ) if $nnd->{k};
310 93         349 $ond->{ $v->{a} } = $nnd;
311             }
312 51         94 return $ond;
313             }
314              
315             sub _init_rev {
316 5     5   8 my $self = shift;
317 5 50       13 $self->_init unless $self->{cache};
318 5         25 $self->{rev} = $self->_rev( $self->{ns} );
319             }
320              
321             sub _init {
322 11     11   18 my $self = shift;
323 11         39 $self->_make_ab( $self->{ns} );
324 11         25 $self->{cache} = {};
325             }
326              
327             # Given a list of unique terms return a hash mapping each term onto an
328             # equally unique abbreviation.
329             sub _ab_list {
330 84     84   159 my ( $self, @w ) = @_;
331              
332 84         100 my %a = ();
333 84         86 my $len = 1;
334 84         128 my @bad = @w;
335              
336 84         84 while () {
337 107 100       587 $a{$_} = $len < length $_ ? substr $_, 0, $len : $_ for @bad;
338 107         128 $len++;
339 107         134 my %cc = ();
340 107         519 $cc{ $a{$_} }++ for keys %a;
341 107         207 @bad = grep { $cc{ $a{$_} } > 1 } keys %a;
  291         632  
342 107 100       415 return \%a unless @bad;
343             }
344             }
345              
346             # Traverse the namespace tree making abbreviations for each node.
347             sub _make_ab {
348 84     84   124 my ( $self, $nd ) = @_;
349 84         230 my @kk = keys %$nd;
350 84         186 my $ab = $self->_ab_list( @kk );
351 84         128 for my $k ( @kk ) {
352 171         309 $nd->{$k}{a} = $ab->{$k};
353 171 100       642 $self->_make_ab( $nd->{$k}{k} ) if $nd->{$k}{k};
354             }
355              
356             }
357              
358             "Ceci n'est pas 'Modern Perl'";
359              
360             __END__