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   1049 use strict;
  17         56  
  17         523  
11 17     17   99 use warnings;
  17         57  
  17         473  
12 17     17   326 use 5.016001;
  17         82  
13              
14 17     17   116 use List::Util qw(any);
  17         47  
  17         1252  
15 17     17   13035 use List::Compare::Functional qw(is_LequivalentR is_LsubsetR);
  17         181600  
  17         1381  
16              
17 17     17   154 use Exporter;
  17         45  
  17         25000  
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.00;
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 539     539 1 1031 my($x, $y) = @_;
78              
79 539         3230 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 2921 my($mask, @terms) = @_;
94 231         331 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         910 (my $m0 = $mask) =~ s/[^01]/0/g;
102 231         728 (my $m1 = $mask) =~ s/[^01]/1/g;
103 231         504 $m0 = oct "0b" . $m0;
104 231         374 $m1 = oct "0b" . $m1;
105              
106 231         433 for my $x (@terms)
107             {
108 3368         5068 my $b = oct "0b" . $x;
109 3368 100 100     8662 push @t, $x if ((($m0 & $b) == $m0) && (($m1 & $b) == $b));
110             }
111              
112 231         896 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 1027     1027 1 2087 my($mask, @terms) = @_;
127 1027         1452 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 1027         3170 (my $m0 = $mask) =~ s/[^01]/0/g;
135 1027         2535 (my $m1 = $mask) =~ s/[^01]/1/g;
136 1027         1947 $m0 = oct "0b" . $m0;
137 1027         1516 $m1 = oct "0b" . $m1;
138              
139 1027         1973 for my $j (0 .. $#terms)
140             {
141 2224         3532 my $b = oct "0b" . $terms[$j];
142 2224 100 100     5490 push @p, $j if ((($m0 & $b) == $m0) && (($m1 & $b) == $b));
143             }
144              
145 1027         2081 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 150     150 1 306 my($primes) = @_;
159              
160 150         252 my %essentials;
161             my %bterms;
162              
163             #
164             # Invert the hash-of-arrays
165             #
166 150         244 while (my($k, $v) = each %{$primes})
  677         1801  
167             {
168 527         753 for my $term (@{ $v })
  527         853  
169             {
170 984 100       2170 $bterms{$term} = [] unless (exists $bterms{$term});
171 984         1384 push @{$bterms{$term}}, $k;
  984         2196  
172             }
173             }
174              
175             #
176             # Find the term that can be covered by only one bit term. Those
177             # terms are essentials.
178             #
179 150         415 for my $k (keys %bterms)
180             {
181 502 100       701 if (scalar @{ $bterms{$k}} == 1)
  502         1063  
182             {
183 174         275 my @bt = @{ $bterms{$k}};
  174         357  
184 174         256 $essentials{ ${ $bterms{$k}}[0]} = 1;
  174         442  
185             }
186             }
187              
188 150         1042 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 201     201 1 2964 my($primes, $dominant_rows) = @_;
213 201         482 my @kp = keys %$primes;
214 201         313 my %unique_rows;
215              
216 201   50     447 $dominant_rows //= 0;
217              
218 201         372 for my $row1 (@kp)
219             {
220 543         121910 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 4407 100 100     1096002 !is_LsubsetR([ $primes->{$row1}, $primes->{$row2} ]));
      100        
232              
233 157 100       52379 $unique_rows{(($dominant_rows)? $row1: $row2)} = 1;
234             }
235             }
236              
237 201         680 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 34     34 1 72 my($primes) = @_;
252 34         70 my(@covers);
253              
254             #
255             # Collect the bit terms that are within the hash's arrays.
256             #
257             my %bterms;
258 34         58 $bterms{$_} += 1 for (map {@$_} values %{$primes});
  108         315  
  34         89  
259 34         111 my @t = keys %bterms;
260              
261             #print STDERR "bit terms hash:\n";
262             #for my $j (@t)
263             #{
264             # print STDERR "\t$j => " . $bterms{$j} . "\n";
265             #}
266             #print STDERR "\n";
267              
268             #
269             # Find out which keys in the primes hash
270             # cover each term (that is, have the term
271             # in each primes' arrays).
272             #
273 34         83 my @pkeys = keys %$primes;
274 34         71 my $count = 1 + scalar @pkeys;
275 34         72 my $term = "";
276              
277             #
278             # Now find a term with the lowest number of covers.
279             #
280 34         84 for my $o (@t)
281             {
282 63         99 my $c = $bterms{$o};
283 63 100       151 if ($c < $count)
284             {
285 35         64 $term = $o;
286 35         80 $count = $c;
287             }
288             }
289              
290 34         73 for my $p (@pkeys)
291             {
292 108 100   129   316 push @covers, $p if any { $_ eq $term } @{ $primes->{$p} };
  129         425  
  108         300  
293             }
294              
295             #print STDERR "covered_least() returns term ($term) and covers (" . join(", ", @covers) . ")\n";
296              
297 34         187 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 166     166 1 367 my($primes, @ess) = @_;
312              
313 166 100 66     735 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 112         261 delete ${$primes}{$_} for @ess;
  177         403  
320              
321 112         287 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 210     210 1 439 my ($href, @els) = @_;
342 210         358 my $rems = 0;
343 210         462 my @kp = keys %$href;
344              
345 210         394 for my $el (@els)
346             {
347 221         385 for my $k (@kp)
348             {
349 1027         1522 my @pos = maskedmatchindexes($el, @{$href->{$k}});
  1027         2012  
350 1027         1646 $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 1027 100       1466 if (scalar @pos == scalar @{$href->{$k}})
  1027         2101  
358             {
359 87         203 delete $href->{$k};
360             }
361             else
362             {
363 940         2121 splice(@{$href->{$k}}, $_, 1) for (reverse sort @pos);
  179         510  
364             }
365             }
366             }
367              
368 210         527 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 34     34 1 63 my %h;
383 34 50       88 return map { $h{ join(",", @{$_}) }++ == 0 ? $_ : () } @_;
  110         173  
  110         472  
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 98     98 1 217 my($table) = @_;
397 98         185 my(%r90, %hoh);
398              
399             #
400             # Set up a hash-of-hashes, inverting the
401             # key to array-of-values relationship.
402             #
403 98         162 for my $r (keys %{$table})
  98         278  
404             {
405 241         356 $hoh{$_}{$r} = 1 for (@{$table->{$r}});
  241         843  
406             }
407              
408             #
409             # For each key collect those sub-hash keys into arrays.
410             #
411 98         295 %r90 = map{ ($_ , [ keys %{$hoh{$_}} ]) } keys %hoh;
  214         369  
  214         833  
412              
413 98         460 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 19451 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       42825 return -1 unless(scalar(() = $v=~ m/[^0]/g) == 1);
442              
443 1472         3699 $v =~ m/[^0]/g;
444 1472         3662 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