ControlBreak
view release on metacpan or search on metacpan
lib/ControlBreak.pm view on Meta::CPAN
USA,California,Los Angeles,3919973
USA,California,San Jose,1026700
USA,Illinois,Chicago,2756546
USA,New York,New York City,8930002
USA,New York,Buffalo,281757
USA,Pennsylvania,Philadelphia,1619355
USA,Texas,Houston,2345606
=head1 DESCRIPTION
The B<ControlBreak> module provides a class that is used to detect
control breaks; i.e. when a value changes.
Typically, the data being retrieved or iterated over is ordered and
there may be more than one value that is of interest. For example
consider a table of population data with columns for country,
district and city, sorted by country and district. With this module
you can create an object that will detect changes in the district or
country, considered level 1 and level 2 respectively. The calling
program can take action, such as printing subtotals, whenever level
changes are detected.
Ordered data is not a requirement. An example using unordered data
would be counting consecutive numbers within a data stream; e.g. 0 0
1 1 1 1 0 1 1. Using ControlBreak you can detect each change and
count the consecutive values, yielding two zeros, four 1's, one zero,
and two 1's.
Note that ControlBreak cannot detect the end of your data stream.
The B<test()> method is normally called within a loop to detect changes
in control variables, but once the last iteration is processed there
are no further calls to B<test()> as the loop ends. It may be necessary,
therefore, to do additional processing after the loop in order to
handle the very last data group; e.g. to print a final set of subtotals.
To simplify this situation, method B<test_and_do()> can be used in
place of B<test()> and B<continue()>.
=cut
########################################################################
# Libraries and Features
########################################################################
use strict;
use warnings;
use v5.26; # minimum perl necessary for Object::Pad 0.66
use Object::Pad 0.66 qw( :experimental(init_expr) );
package ControlBreak;
class ControlBreak;
# althouth Object::Pad allows a version argument on the class statement
# we can't use it because we want Dist::Zilla to set it from the dist.ini
# version -- and that requires it to be an 'our' statement.
our $VERSION = 'v0.22.244';
use Carp qw(croak);
# public attributes
field $iteration :reader { 0 }; # [0] counts iterations
field @level_names :reader; # [1] list of level names
# private attributes
field $_num_levels; # [2] the number of control levels
field %_levname { }; # [3] map of levidx to levname
field %_levidx { }; # [4] map of lenname to levidx
field %_comp_op; # [5] comparison operators
field %_fcomp; # [6] comparison functions
field $_test_levelnum { 0 }; # [7] last level returned by test()
field $_test_levelname { '' }; # [8] last level returned by test()
field @_test_values; # [9] the values of the current test()
field @_last_values; # [10] the values from the previous test()
field $_continue_count { 0 }; # [11] the number of types continue was called
=head1 FIELDS
=head2 iteration
A readonly field that provides the current iteration number.
This can be useful if you are doing an final processing after an
iteration loop has ended. In the event that the data stream is empty
and there were no iterations, then you can condition your final
processing on iteration > 0.
Note that the B<interation> field is incremented by B<test()> (or
B<test_and_do()>). Therefore, when called within a loop it is
effectively zero-based if referenced within the iteration block
before B<test()> is invoked, and then one-based after B<test()>.
=head2 level_names
A readonly field that provides a list of the level names that were
provided as arguments to B<new()>.
=cut
######################################################################
# Constructor (a.k.a. the new() method)
######################################################################
=head1 METHODS
=head2 new ( $level_name> [, $level_name> ]... )
Create a new ControlBreak object.
Arguments are user-defined names for each level, in minor to major
order. The set of names must be unique, and they must each start
with a letter or underscore, followed by any number of letters,
numbers or underscores.
A level name can also begin with a '+', which denotes that a numeric
comparison will be used for the values processed at this level.
The number of arguments to B<new()> determines the number of control
levels that will be monitored. The variables provided to method
test() must match in number and datatype to these operators.
The order of the arguments corresponds to a hierarchical level of
control, from lowest to highest; i.e. the first argument corresponds
to level 1, the second to level 2, etc. This also corresponds to
sort order, from minor to major, when iterating through a data stream.
=cut
BUILD {
croak '*E* at least one argument is required'
if @_ == 0;
foreach my $arg (@_) {
croak '*E* invalid level name'
unless $arg =~ m{ \A [+]? [[:alpha:]_]\w+ }xms;
}
$_num_levels = @_;
my %lev_count;
foreach my $arg (@_) {
$lev_count{$arg}++;
croak '*E* duplicate level name: ' . $arg
if $lev_count{$arg} > 1;
lib/ControlBreak.pm view on Meta::CPAN
=cut
method levelname () {
return $_test_levelname;
}
=head2 levelnum
Return the level number for the most recent invocation of the
B<test()> method.
=cut
method levelnum () {
return $_test_levelnum;
}
=head2 level_numbers
Return a list of level numbers corresponding to the levels defined
in B<new()>. This can be useful, for example, when you want to
set up some lexical variables for use as indexes into a list you
might use to accumulate subtotals.
my $cb = ControlBreak->new( qw( L1 L2 EOD ) );
my @totals;
my ($L1, $L2, $EOD) = $cb->level_numbers;
foreach my $sublist (@list_of_lists) {
my ($control1, $control2, $number) = $sublist->@*;
...
my $sub_totals = sub {
if ($cb->break('L1')) {
# report the L1 subtotal here
$totals[$L1] = 0; # clear the subtotal
}
...
# accumulate subtotals
map { $totals[$_] += $number } $cb->level_numbers;
};
$cb->test_and_do(
$control1,
$control2,
$cb->iteration == $list_of_lists - 1,
$sub_totals
);
}
=cut
method level_numbers () {
return 1 .. $_num_levels;
}
=head2 reset
Resets the state of the object so it can be used again for another
set of iterations using the same number and type of controls
establish when the object was instantiated with B<new()>. Any
comparisons that were subsequently modified are retained.
=cut
method reset () { ## no critic [ProhibitParensWithBuiltins]
$iteration = 0;
$_continue_count = 0;
$_test_levelnum = 0;
$_test_levelname = 0;
@_test_values = ();
@_last_values = ( undef ) x $_num_levels;
}
=head2 test ( $var1 [, $var2 ]... )
Submits the control variables for testing against the values from the
previous iteration.
Testing is done in reverse order, from highest to lowest (major to
minor) and stops once a change is detected. Where it stops determines
the control break level. For example, if $var2 changed, method
levelnum will return 2. If $var2 did not change, but $var1 did, then
method B<levelnum()> will return 1. If nothing changes, then
B<levelnum()> will return 0.
Note that the level numbers set by B<test(...)> are true if there was
a level change, and false if there wasn't. So, they can be used as a
simple boolean test of whether there was a change. Or you can use
the B<break()> method to determine whether any control break has
occurred.
Because level numbers correspond to the hierarchical data order, they
can be use to trigger multiple actions; e.g. B<levelnum()> >= 1 could
be used to print subtotals for levels 1 whenever a control break
occurred for level 1, 2 or 3. It is usually the case that higher
control breaks are meant to cascade to lower control levels and this
can be achieved in this fashion. The B<break()> method simplifies
this.
Note that method B<continue()> must be called at the end of each
iteration in order to save the values of the iteration for the next
iteration. If not, the next B<test(...)> invocation will croak.
=cut
method test (@args) {
croak '*E* number of arguments to test() must match those given in new()'
if @args != $_num_levels;
croak '*E* continue() must be called after test()'
unless $iteration == $_continue_count;
@_test_values = @args;
$iteration++;
my $is_break;
my $lev_idx = 0;
( run in 1.690 second using v1.01-cache-2.11-cpan-96521ef73a4 )