File Coverage

blib/lib/Algorithm/QuineMcCluskey/Util.pm
Criterion Covered Total %
statement 121 121 100.0
branch 23 24 95.8
condition 15 17 88.2
subroutine 18 18 100.0
pod 11 11 100.0
total 188 191 98.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Algorithm::QuineMcCluskey::Util - provide utility functions to
4             Algorithm::QuineMcCluskey
5              
6             =cut
7              
8             package Algorithm::QuineMcCluskey::Util;
9              
10 17     17   1080 use strict;
  17         56  
  17         520  
11 17     17   119 use warnings;
  17         39  
  17         423  
12 17     17   362 use 5.016001;
  17         67  
13              
14 17     17   126 use List::Util qw(any);
  17         44  
  17         1297  
15 17     17   13366 use List::Compare::Functional qw(is_LequivalentR is_LsubsetR);
  17         180480  
  17         1318  
16              
17 17     17   146 use Exporter;
  17         43  
  17         24592  
18             our @ISA = qw(Exporter);
19              
20             our %EXPORT_TAGS = (
21             all => [ qw(
22             covered_least
23             find_essentials
24             hammingd1pos
25             maskedmatch
26             matchcount
27             purge_elements
28             remels
29             row_dominance
30             transpose
31             uniqels
32             ) ],
33             );
34              
35             our @EXPORT_OK = (
36             @{ $EXPORT_TAGS{all} }
37             );
38              
39             our $VERSION = 1.01;
40              
41             =head1 DESCRIPTION
42              
43             This module provides various utilities designed for (but not limited to) use in
44             Algorithm::QuineMcCluskey.
45              
46             The prime implicant and essentials "tables" are in the form of a hash of
47             array refs, and are manipulated with the functions find_essentials(),
48             least_covered(), purge_elements(), remels(), row_dominance(), transpose(),
49             and uniqels().
50              
51             =cut
52              
53             =head2 FUNCTIONS
54              
55             =head3 matchcount()
56              
57             Returns the count of a search string Y found in the source string X.
58              
59             E.g.:
60              
61             my $str = "d10d11d1d";
62             matchcount($str, "d"); # returns 4
63             matchcount($str, "d1"); # returns 3
64              
65             To search for only the string without a regular expression accidentally
66             interfering, enclose the search string between '\Q' and '\E'. E.g.:
67              
68             #
69             # We don't know what's in $looking, so de-magic it.
70             #
71             matchcount($str, '\E' . $looking . '\Q]);
72              
73             =cut
74              
75             sub matchcount
76             {
77 124     124 1 247 my($x, $y) = @_;
78              
79 124         1075 return scalar(() = $x=~ m/$y/g);
80             }
81              
82             =head3 maskedmatch()
83              
84             Returns the terms that match a mask made up of zeros, ones, and don't-care
85             characters.
86              
87             my @rterms = maskedmatch("010-0", @terms);
88              
89             =cut
90              
91             sub maskedmatch
92             {
93 231     231 1 2719 my($mask, @terms) = @_;
94 231         334 my @t;
95              
96             #
97             # Make two patterns based on the don't-care characters
98             # in the mask (assumed to be the character that's not
99             # a zero or a one, an assumption enforced in BUILD.)
100             #
101 231         889 (my $m0 = $mask) =~ s/[^01]/0/g;
102 231         721 (my $m1 = $mask) =~ s/[^01]/1/g;
103 231         509 $m0 = oct "0b" . $m0;
104 231         415 $m1 = oct "0b" . $m1;
105              
106 231         408 for my $x (@terms)
107             {
108 3368         5200 my $b = oct "0b" . $x;
109 3368 100 100     8628 push @t, $x if ((($m0 & $b) == $m0) && (($m1 & $b) == $b));
110             }
111              
112 231         849 return @t;
113             }
114              
115             =head3 maskedmatchindexes()
116              
117             Returns the indexes of the terms that match a mask made up of zeros,
118             ones, and don't-care characters.
119              
120             my @pos = maskedmatchindexes("010-0", @terms);
121              
122             =cut
123              
124             sub maskedmatchindexes
125             {
126 1048     1048 1 2106 my($mask, @terms) = @_;
127 1048         1408 my @p;
128              
129             #
130             # Make two patterns based on the don't-care characters
131             # in the mask (assumed to be the character that's not
132             # a zero or a one, an assumption enforced in BUILD.)
133             #
134 1048         3194 (my $m0 = $mask) =~ s/[^01]/0/g;
135 1048         2635 (my $m1 = $mask) =~ s/[^01]/1/g;
136 1048         2038 $m0 = oct "0b" . $m0;
137 1048         1607 $m1 = oct "0b" . $m1;
138              
139 1048         2167 for my $j (0 .. $#terms)
140             {
141 2193         3611 my $b = oct "0b" . $terms[$j];
142 2193 100 100     5451 push @p, $j if ((($m0 & $b) == $m0) && (($m1 & $b) == $b));
143             }
144              
145 1048         2074 return @p;
146             }
147              
148             =head3 find_essentials()
149              
150             Find the essential prime implicants in a primes table.
151              
152             my @essentials = find_essentials(\%primes);
153              
154             =cut
155              
156             sub find_essentials
157             {
158 153     153 1 306 my($primes) = @_;
159              
160 153         271 my %essentials;
161             my %bterms;
162              
163             #
164             # Invert the hash-of-arrays
165             #
166 153         253 while (my($k, $v) = each %{$primes})
  693         1802  
167             {
168 540         741 for my $term (@{ $v })
  540         923  
169             {
170 995 100       2507 $bterms{$term} = [] unless (exists $bterms{$term});
171 995         1394 push @{$bterms{$term}}, $k;
  995         2175  
172             }
173             }
174              
175             #
176             # Find the term that can be covered by only one bit term. Those
177             # terms are essentials.
178             #
179 153         379 for my $k (keys %bterms)
180             {
181 503 100       691 if (scalar @{ $bterms{$k}} == 1)
  503         1101  
182             {
183 171         228 my @bt = @{ $bterms{$k}};
  171         316  
184 171         279 $essentials{ ${ $bterms{$k}}[0]} = 1;
  171         384  
185             }
186             }
187              
188 153         983 return keys %essentials;
189             }
190              
191             =head3 row_dominance()
192              
193             Row dominance checking.
194              
195             @dominated_rows = row_dominance(\%primes, 0);
196              
197             @dominant_rows = row_dominance(\%primes, 1);
198              
199             A row I<i> of a PI chart dominates row I<j> if row I<i> contains an x in each
200             column dominated by it.
201              
202             A column I<p> of a PI chart dominates column I<q> if column I<p> contains an x
203             in each row dominated by it.
204              
205             Return those rows (columns are handled by rotating the primes hash before
206             calling this function).
207              
208             =cut
209              
210             sub row_dominance
211             {
212 203     203 1 2888 my($primes, $dominant_rows) = @_;
213 203         462 my @kp = keys %$primes;
214 203         325 my %unique_rows;
215              
216 203   50     518 $dominant_rows //= 0;
217              
218 203         370 for my $row1 (@kp)
219             {
220 551         125295 for my $row2 (@kp)
221             {
222             #
223             # Skip if
224             # 1) the same row, or
225             # 2) the the rows have identical content, or
226             # 3) row1's list isn't a subset of row2 (which means
227             # it isn't dominated by row2).
228             #
229             next if ($row1 eq $row2 or
230             is_LequivalentR([ $primes->{$row1}, $primes->{$row2} ]) or
231 4445 100 100     1097497 !is_LsubsetR([ $primes->{$row1}, $primes->{$row2} ]));
      100        
232              
233 153 100       52253 $unique_rows{(($dominant_rows)? $row1: $row2)} = 1;
234             }
235             }
236              
237 203         683 return keys %unique_rows;
238             }
239              
240             =head3 covered_least()
241              
242             Find the term with the fewest implicant covers, along with a list of
243             those covers.
244              
245             my($term, @covers) = covered_least(\%primes);
246              
247             =cut
248              
249             sub covered_least
250             {
251 36     36 1 85 my($primes) = @_;
252 36         66 my(@covers);
253              
254             #
255             # Collect the bit terms that are within the hash's arrays.
256             #
257             my %bterms;
258 36         62 $bterms{$_} += 1 for (map {@$_} values %{$primes});
  117         333  
  36         96  
259              
260             #
261             # Find out which keys in the primes hash
262             # cover each term (that is, have the term
263             # in each primes' arrays).
264             #
265 36         114 my @t = keys %bterms;
266 36         91 my @pkeys = keys %$primes;
267 36         78 my $count = 1 + scalar @pkeys;
268 36         71 my $term = "";
269              
270             #print STDERR "bit terms hash:\n";
271             #for my $j (@t)
272             #{
273             # print STDERR "\t$j => " . $bterms{$j} . "\n";
274             #}
275             #print STDERR "\n";
276              
277             #
278             # Now find a term with the lowest number of covers.
279             #
280 36         72 for my $o (@t)
281             {
282 66         105 my $c = $bterms{$o};
283 66 100       153 if ($c < $count)
284             {
285 36         56 $term = $o;
286 36         75 $count = $c;
287             }
288             }
289              
290 36         72 for my $p (@pkeys)
291             {
292 117 100   139   343 push @covers, $p if any { $_ eq $term } @{ $primes->{$p} };
  139         454  
  117         314  
293             }
294              
295             #print STDERR "covered_least() returns term ($term) and covers (" . join(", ", @covers) . ")\n";
296              
297 36         167 return ($term, @covers);
298             }
299              
300             =head3 purge_elements()
301              
302             purge_elements(\%prime_implicants, @essentials);
303              
304             Given a table of prime implicants, delete the list of elements (usually
305             the essential prime implicants) from the table, both row-wise and column-wise.
306              
307             =cut
308              
309             sub purge_elements
310             {
311 173     173 1 382 my($primes, @ess) = @_;
312              
313 173 100 66     760 return 0 if (scalar @ess == 0 or scalar keys %$primes == 0);
314              
315             #
316             # Delete the rows of each element,
317             # then delete the columns associated with each element.
318             #
319 117         270 delete ${$primes}{$_} for @ess;
  182         395  
320              
321 117         274 return remels($primes, @ess);
322             }
323              
324             =head3 remels()
325              
326             Given a reference to a hash of arrayrefs and a reference to an array of
327             values, remove the values from the individual arrayrefs if the values
328             matches their masks.
329              
330             Deletes the entire arrayref from the hash if the last element of the
331             array is removed.
332              
333             remels(\%primes, @elements);
334              
335             Returns the number of removals made.
336              
337             =cut
338              
339             sub remels
340             {
341 216     216 1 459 my ($href, @els) = @_;
342 216         399 my $rems = 0;
343 216         494 my @kp = keys %$href;
344              
345 216         407 for my $el (@els)
346             {
347 227         375 for my $k (@kp)
348             {
349 1048         1504 my @pos = maskedmatchindexes($el, @{$href->{$k}});
  1048         1979  
350 1048         1645 $rems += scalar @pos;
351              
352             #
353             # If it turns out that all the elements in the array
354             # are to be removed, then just delete the entire
355             # array reference.
356             #
357 1048 100       1515 if (scalar @pos == scalar @{$href->{$k}})
  1048         2098  
358             {
359 110         268 delete $href->{$k};
360             }
361             else
362             {
363 938         2090 splice(@{$href->{$k}}, $_, 1) for (reverse sort @pos);
  177         470  
364             }
365             }
366             }
367              
368 216         603 return $rems;
369             }
370              
371             =head3 uniqels()
372              
373             Returns the unique arrays from an array of arrays (i.e., we're
374             ensuring non-duplicate answers).
375              
376             my @uels = uniqels(@els);
377              
378             =cut
379              
380             sub uniqels
381             {
382 36     36 1 59 my %h;
383 36 50       80 return map { $h{ join(",", @{$_}) }++ == 0 ? $_ : () } @_;
  124         195  
  124         540  
384             }
385              
386             =head3 transpose()
387              
388             Transposes a hash-of-arrays structure of the type used for %primes.
389              
390             my %table90 = transpose(\%table)
391              
392             =cut
393              
394             sub transpose
395             {
396 99     99 1 189 my($table) = @_;
397 99         172 my(%r90, %hoh);
398              
399             #
400             # Set up a hash-of-hashes, inverting the
401             # key to array-of-values relationship.
402             #
403 99         164 for my $r (keys %{$table})
  99         305  
404             {
405 251         375 $hoh{$_}{$r} = 1 for (@{$table->{$r}});
  251         869  
406             }
407              
408             #
409             # For each key collect those sub-hash keys into arrays.
410             #
411 99         272 %r90 = map{ ($_ , [ keys %{$hoh{$_}} ]) } keys %hoh;
  216         357  
  216         819  
412              
413 99         448 return %r90;
414             }
415              
416             =head3 hammingd1pos()
417              
418             Very specialized Hamming distance and position function.
419              
420             Our calling code is only interested in Hamming distances of 1.
421             In those cases return the string position where the two values differ.
422             In all the other cases where the distance isn't one, return a -1.
423              
424             $idx = hammingd1pos($val1, $val2);
425              
426             =cut
427              
428             sub hammingd1pos
429             {
430             #
431             # Xor the strings. The result will be a string in the
432             # non-printing range (in fact equal characters will result
433             # in a null character), so to each character Or a '0'.
434             #
435 9080     9080 1 19371 my $v = ($_[0] ^ $_[1]) | (qq(\x30) x length $_[0]);
436              
437             #
438             # Strings that don't have a Hamming distance of one are of no
439             # interest. Otherwise, return that character position.
440             #
441 9080 100       41901 return -1 unless(scalar(() = $v=~ m/[^0]/g) == 1);
442              
443 1472         3635 $v =~ m/[^0]/g;
444 1472         3607 return pos($v) - 1;
445             }
446              
447             =head1 SEE ALSO
448              
449             L<Algorithm::QuineMcCluskey>
450              
451             =head1 AUTHOR
452              
453             Darren M. Kulp C<< <darren@kulp.ch> >>
454              
455             John M. Gamble B<jgamble@cpan.org> (current maintainer)
456              
457             =head1 LICENSE AND COPYRIGHT
458              
459             Copyright (c) 2006 Darren Kulp. All rights reserved. This program is
460             free software; you can redistribute it and/or modify it under the same
461             terms as Perl itself.
462              
463             See L<http://dev.perl.org/licenses/> for more information.
464              
465             =cut
466              
467             1;
468              
469             __END__
470