pax_global_header00006660000000000000000000000064131241732550014515gustar00rootroot0000000000000052 comment=915674e31bea860345c4980bef1dc6a955757cb4 st-1.1.4/000077500000000000000000000000001312417325500121465ustar00rootroot00000000000000st-1.1.4/.gitignore000066400000000000000000000002351312417325500141360ustar00rootroot00000000000000blib/ .build/ _build/ cover_db/ inc/ Build !Build/ Build.bat .last_cover_stats Makefile Makefile.old MANIFEST.bak META.yml MYMETA.yml nytprof.out pm_to_blib st-1.1.4/Changelog000066400000000000000000000024611312417325500137630ustar00rootroot00000000000000Revision history for st 1.1.4 Mon Jun 26 13:57:00 2017 +0200 Percentile between 0 and 100 1.1.3 Mon Jun 26 12:58:00 2017 +0200 Fixed --percentile and --quartile options 1.1.2 Wed Apr 1 18:43:12 2015 +0200 Bugfix: sorted data was not cached 1.1.1 Thu Oct 10 19:37:41 2013 +0200 Makefile.PL allows script renaming 1.1.0 Mon Sep 23 18:38:35 2013 +0200 Adopt "%g" as default output format s/transverse/transpose/g 1.0.10 Mon Sep 23 17:18:19 2013 +0200 Speed improvements 1.0.9 Mon Sep 23 16:37:15 2013 +0200 Speed improvements 1.0.8 Wed Sep 11 17:10:32 2013 +0200 Fixed --transverse-output 1.0.7 Wed Sep 11 14:28:36 2013 +0200 Renamed: bin/st -> script/st Changes 1.0.6 Tue Sep 10 19:27:21 2013 +0200 --sem (Standard error of mean) 1.0.5 Mon Sep 9 18:14:11 2013 +0200 Improved formatting Fixed --strict option 1.0.4 Mon Sep 9 14:04:46 2013 +0200 --strict 1.0.3 Fri Sep 6 17:35:31 2013 +0200 --transverse-output --quartile 1.0.2 Thu Sep 5 13:55:32 2013 +0200 --no-header --quiet 1.0.1 Wed Sep 4 17:53:40 2013 +0200 Fixed Makefile.PL 1.0 Wed Sep 4 16:13:48 2013 +0200 First version, released on an unsuspecting world. st-1.1.4/INSTALL000066400000000000000000000010221312417325500131720ustar00rootroot00000000000000Installation ============ perl Makefile.PL make make test sudo make install Renaming the script ------------------- Use the "--script-name" option to specify a different script name: perl Makefile.PL --script-name= make make test sudo make install (This option may be useful if the default script name ("st") conflicts with an application with the same name.) Making a distribution --------------------- perl Makefile.PL [--script-name=] make manifest make dist st-1.1.4/LICENSE000066400000000000000000000020701312417325500131520ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Nelson Ferraz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. st-1.1.4/Makefile.PL000066400000000000000000000030051312417325500141160ustar00rootroot00000000000000use 5.006; use strict; use warnings; use ExtUtils::MakeMaker; use Getopt::Long; use File::Copy; my %opt; GetOptions( \%opt, 'rename|script-name=s' ); my $DEFAULT_SCRIPT_NAME = "st"; my $script_name = $DEFAULT_SCRIPT_NAME; # rename script if necessary if ($opt{rename} and $opt{rename} =~ /^(\w+)$/) { my $new_script_name = $1; open my $in, '<', "script/$DEFAULT_SCRIPT_NAME" or die "Can't read 'script/$DEFAULT_SCRIPT_NAME': $!"; open my $out, '>', "script/$new_script_name" or die "Can't write to 'script/$new_script_name': $!"; my $found_pod; while (my $line = <$in>) { if ($line eq "__END__\n" or $found_pod) { # only after __END__ if ($line !~ m{http.+/$DEFAULT_SCRIPT_NAME}) { # only if not in url $line =~ s/\b$DEFAULT_SCRIPT_NAME\b/$new_script_name/g; } $found_pod = 1; } print $out $line; } close $out; close $in; $script_name = $new_script_name; } WriteMakefile( NAME => 'App::St', AUTHOR => q{Nelson Ferraz }, VERSION_FROM => 'lib/App/St.pm', ABSTRACT_FROM => 'lib/App/St.pm', LICENSE => 'MIT', EXE_FILES => [ "script/$script_name", ], PL_FILES => { }, MIN_PERL_VERSION => 5.006, CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => 0, }, BUILD_REQUIRES => { 'Test::More' => 0, }, PREREQ_PM => { }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'App-*' }, ); st-1.1.4/README.md000066400000000000000000000072351312417325500134340ustar00rootroot00000000000000st == simple statistics from the command line interface (CLI) ### Description Imagine you have this sample file: $ cat numbers.txt 1 2 3 4 5 6 7 8 9 10 How do you calculate the sum of the numbers? #### The traditional way If you ask around, you'll come up with suggestions like these: $ awk '{s+=$1} END {print s}' numbers.txt 55 $ perl -lne '$x += $_; END { print $x; }' numbers.txt 55 $ sum=0; while read num ; do sum=$(($sum + $num)); done < numbers.txt ; echo $sum 55 $ paste -sd+ numbers.txt | bc 55 Now imagine that you need to calculate the arithmetic mean, median, or standard deviation... #### Using st "st" is a command-line tool to calculate simple statistics from a file or standard input. Let's start with "sum": $ st --sum numbers.txt 55 That was easy! How about mean and standard deviation? $ st --mean --stddev numbers.txt mean stddev 5.5 3.02765 If you don't specify any options, you'll get this output: $ st numbers.txt N min max sum mean stddev 10 1 10 55 5.5 3.02765 You can switch rows and columns using the "--transpose-output" option: $ st --transpose-output numbers.txt N 10 min 1 max 10 sum 55 mean 5.5 stddev 3.02765 The "--summary" option will provide the five-number summary: $ st --summary numbers.txt min q1 median q3 max 1 3.5 5.5 7.5 10 And "--complete" will print a complete description: $ st --complete numbers.txt N min q1 median q3 max sum mean stddev stderr 10 1 3.5 5.5 7.5 10 55 5.5 3.02765 0.957427 #### How does it compare with R, Octave and other analytical tools? "R" and Octave are integrated suites for data manipulation, calculation and graphical display. They provide high-level interpreted languages, capabilities for the numerical solution of linear and nonlinear problems, and for performing other numerical experiments, including statistical tests, classification, clustering, etc. "st" is a simpler solution for simpler problems, focused on descriptive statistics for small datasets, handy when you need quick results without leaving the shell. ### Usage st [options] [file] #### Options ##### Functions --N|n|count --mean|avg|m --stddev|sd --stderr|sem|se --sum|s --var|variance --min --q1 --median --q3 --max --percentile=<0..1> --quartile=<1..4> If no functions are selected, "st" will print the default output: N min max sum mean stddev You can also use the following predefined sets of functions: --summary # five-number summary (min q1 median q3 max) --complete # everything ##### Formatting --format|fmt|f= # default: "%g" --delimiter|d= # default: "\t" --no-header|nh # don't display header --transpose-output|to # switch rows and columns Examples of valid formats ("--format" option): %d signed integer, in decimal %e floating-point number, in scientific notation %f floating-point number, in fixed decimal notation %g floating-point number, in %e or %f notation ##### Input validation By default, "st" skips invalid input with a warning. You can change this behavior with the following options: --strict # throws an error, interrupting process --quiet|q # no warning ### Author Nelson Ferraz <> ### Contribute Send comments, suggestions and bug reports to: https://github.com/nferraz/st/issues Or fork the code on github: https://github.com/nferraz/st st-1.1.4/lib/000077500000000000000000000000001312417325500127145ustar00rootroot00000000000000st-1.1.4/lib/App/000077500000000000000000000000001312417325500134345ustar00rootroot00000000000000st-1.1.4/lib/App/St.pm000066400000000000000000000130271312417325500143630ustar00rootroot00000000000000package App::St; use strict; use warnings; #use bignum; our $VERSION = '1.1.3'; sub new { my ($class, %opt) = @_; my $delimiter = $opt{'delimiter'} || "\t"; my $format = $opt{'format'} || '%.2f'; if ($delimiter =~ /^\\[a-z]$/) { $delimiter = $delimiter eq '\t' ? "\t" : $delimiter eq '\n' ? "\n" : die "Invalid delimiter: '$delimiter'\n"; } if ($format =~ m{( \s* \% [\s+-]? [0-9]*\.?[0-9]* [deEfgGi] \s* )}x) { $format = $1; } else { die "Invalid format: '$format'\n"; } return bless { %opt, N => 0, sum => 0, sum_square => 0, mean => 0, stddev => 0, stderr => 0, min => undef, q1 => 0, median => 0, q3 => 0, max => undef, M2 => 0, delimiter => $delimiter, format => $format, data => [], }, $class; } sub validate { my ($self, $num) = @_; return ($num =~ m{^ [+-]? (?: \. ? [0-9]+ | [0-9]+ \. [0-9]* | [0-9]* \. ? [0-9]+ [Ee] [+-]? [0-9]+ ) $}x); } sub process { my ($self, $num) = @_; die "Invalid input '$num'\n" if !$self->validate($num); $self->{N}++; $self->{sum} += $num; $self->{min} = $num if (!defined $self->{min} or $num < $self->{min}); $self->{max} = $num if (!defined $self->{max} or $num > $self->{max}); my $delta = $num - $self->{mean}; $self->{mean} += $delta / $self->{N}; $self->{M2} += $delta * ($num - $self->{mean}); push( @{ $self->{data} }, $num ) if $self->{keep_data}; return; } sub N { return $_[0]->{N}; } sub sum { return $_[0]->{sum}; } sub min { return $_[0]->{min}; } sub max { return $_[0]->{max}; } sub mean { my ($self,%opt) = @_; my $mean = $self->{mean}; return $opt{formatted} ? $self->_format($mean) : $mean; } sub quartile { my ($self,$q,%opt) = @_; if ($q !~ /^[01234]$/) { die "Invalid quartile '$q'\n"; } return $self->percentile($q / 4 * 100, %opt); } sub median { my ($self,%opt) = @_; return $self->percentile(50, %opt); } sub variance { my ($self,%opt) = @_; my $N = $self->{N}; my $M2 = $self->{M2}; my $variance = $N > 1 ? $M2 / ($N - 1) : undef; return $opt{formatted} ? $self->_format($variance) : $variance; } sub stddev { my ($self,%opt) = @_; my $variance = $self->variance(); my $stddev = defined $variance ? sqrt($variance) : undef; return $opt{formatted} ? $self->_format($stddev) : $stddev; } sub stderr { my ($self,%opt) = shift; my $stddev = $self->stddev(); my $N = $self->N(); my $stderr = defined $stddev ? $stddev/sqrt($N) : undef; return $opt{formatted} ? $self->_format($stderr) : $stderr; } sub percentile { my ($self, $p, %opt) = @_; my $data = $self->{data}; if (!$self->{keep_data} or scalar @{$data} == 0) { die "Can't get percentile from empty dataset\n"; } if ($p < 0 or $p > 100) { die "Invalid percentile '$p'\n"; } if (!$self->{_is_sorted_}) { $data = [ sort {$a <=> $b} @{ $data } ]; $self->{data} = $data; $self->{_is_sorted_} = 1; } my $N = $self->N(); my $idx = ($N - 1) * $p / 100; my $percentile = int($idx) == $idx ? $data->[$idx] : ($data->[$idx] + $data->[$idx+1]) / 2; return $opt{formatted} ? _format($percentile) : $percentile; } sub result { my $self = shift; my %result = ( N => $self->N(), sum => $self->sum(), mean => $self->mean(), stddev => $self->stddev(), stderr => $self->stderr(), min => $self->min(), max => $self->max(), variance => $self->variance(), ); if ($self->{keep_data}) { %result = (%result, ( q1 => $self->quartile(1), median => $self->median(), q3 => $self->quartile(3), ) ); } if (exists $self->{percentile}) { %result = ( %result, percentile => $self->percentile($self->{percentile}), ); } if (exists $self->{quartile}) { %result = ( %result, quartile => $self->quartile($self->{quartile}), ); } return %result; } sub _format { my ($self,$value,%opt) = @_; my $format = $self->{format}; return sprintf( $format, $value ); } 1; __END__ =head1 NAME App::St - Simple Statistics =head1 DESCRIPTION App::St provides the core functionality of the L application. =head1 SYNOPSIS use App::St; my $st = App::St->new(); while (<>) { chomp; next unless $st->validate($_); $st->process($_); } print $st->mean(); print $st->stddev(); print $st->sterr(); =head1 METHODS =head2 new(%options) =head2 validate($num) =head2 process($num) =head2 N =head2 sum =head2 mean =head2 stddev =head2 stderr =head2 percentile=<0..100> =head2 quartile=<0..4> =head2 min =head2 q1 =head2 median =head2 q3 =head2 max =head1 AUTHOR Nelson Ferraz L<> =head1 CONTRIBUTING Send comments, suggestions and bug reports to: https://github.com/nferraz/st/issues Or fork the code on github: https://github.com/nferraz/st =head1 COPYRIGHT Copyright (c) 2013 Nelson Ferraz. This program is free software; you can redistribute it and/or modify it under the MIT License (see LICENSE). st-1.1.4/script/000077500000000000000000000000001312417325500134525ustar00rootroot00000000000000st-1.1.4/script/st000077500000000000000000000133711312417325500140330ustar00rootroot00000000000000#!perl use strict; use warnings; #use bignum; use Data::Dumper; use Getopt::Long; use Pod::Usage; use App::St; my %opt; GetOptions( \%opt, # functions 'N|n|count', 'mean|avg|m', 'stddev|sd', 'stderr|sem|se', 'sum|s', 'variance|var', 'min|q0', 'q1', 'median|q2', 'q3', 'max|q4', 'percentile=f', 'quartile=i', # predefined output sets 'summary', 'complete|everything|all', 'default', # output control 'delimiter|d=s', 'format|fmt|f=s', 'no-header|nh', 'transpose-output|transverse-output|to', # error handling 'quiet|q', 'strict', 'help|h', ) or pod2usage(1); pod2usage(1) if $opt{help}; my %config = get_config(%opt); my @stats = statistical_options(%opt); if ( $opt{summary} or $opt{complete} or $opt{q1} or $opt{median} or $opt{q3} or defined $opt{percentile} or defined $opt{quartile} ) { $config{keep_data} = 1; } # special cases: percentile and quartile are not booleans my %special_parameters = map { $_ => $opt{$_} } grep { exists $opt{$_} } qw/percentile quartile/; my $st = App::St->new(%config, %special_parameters); my $n = 0; while (my $num = <>) { chomp $num; $n++; if (!$st->validate($num)) { my $err = "Invalid value '$num' on input line $.\n"; if ($opt{strict}) { die $err; } elsif (!$opt{quiet}) { warn $err; } next; } $st->process($num); } exit if $st->N() == 0; my %result = $st->result(); my @opt = grep { exists $result{$_} } statistical_options(%opt); if (scalar @opt == 1) { print sprintf( $config{format}, $result{$opt[0]} ), "\n"; exit; } if ($config{'transpose-output'}) { for my $opt (@opt) { print "$opt$config{delimiter}" unless $config{'no-header'}; print sprintf( $config{format}, $result{$opt} ), "\n"; } } else { print join($config{delimiter}, @opt), "\n" unless $config{'no-header'}; print join($config{delimiter}, map { sprintf ($config{format}, $result{$_}) } @opt), "\n"; } exit; ### sub get_config { my %opt = @_; my %config = map { $_ => $opt{$_} } grep { exists $opt{$_} } qw/delimiter format no-header transpose-output quiet strict/; my $delimiter = $opt{'delimiter'} || "\t"; my $format = $opt{'format'} || '%g'; if ($delimiter =~ /^\\[a-z]$/) { $delimiter = $delimiter eq '\t' ? "\t" : $delimiter eq '\n' ? "\n" : die "Invalid delimiter: '$delimiter'\n"; } if ($format =~ m{( \s* \% [\s+-]? [0-9]*\.?[0-9]* [deEfgGi] \s* )}x) { $format = $1; } else { die "Invalid format: '$format'\n"; } return (%config, delimiter => $delimiter, format => $format); } sub statistical_options { my %opt = @_; # predefined sets my %predefined = ( complete => [ qw/N min q1 median q3 max sum mean stddev stderr variance percentile quartile/ ], summary => [ qw/min q1 median q3 max/ ], default => [ qw/N min max sum mean stddev/ ], ); # selected options my %selected = map { $_ => 1 } grep { exists $opt{$_} } @{ $predefined{complete} }; # expand with predefined sets for my $set (keys %predefined) { if ($opt{$set}) { %selected = (%selected, map { $_ => 1 } @{ $predefined{$set} }); } } my @selected = %selected ? grep { exists $selected{$_} } @{ $predefined{complete} } : @{ $predefined{default} }; return @selected; } __END__ =head1 NAME st - simple statistics from the command line interface (CLI) =head1 DESCRIPTION C is a command-line tool to calculate simple statistics from a file or standard input. =head1 USAGE st [options] [input_file] =head2 OPTIONS =head3 FUNCTIONS --N|n|count # sample size --min # minimum --max # maximum --mean|average|avg|m # mean --stdev|sd # standard deviation --stderr|sem|se # standard error of mean --sum|s # sum of elements of the sample --variance|var # variance The following options require that the whole dataset is stored in memory, which can be problematic for huge datasets: --q1 # first quartile --median|q2 # second quartile, or median --q3 # third quartile --percentile=f # percentile=<0..100> --quartile=i # quartile=<1..4> If no functions are selected, C will print the default output: N min max sum mean stddev You can also use the following predefined sets of functions: --summary # five-number summary (min q1 median q3 max) --complete # everything =head3 FORMATTING --format|fmt|f= # default: "%g" Examples of valid formats: %d signed integer, in decimal %e floating-point number, in scientific notation %f floating-point number, in fixed decimal notation %g floating-point number, in %e or %f notation --delimiter|d= # default: "\t" --no-header|nh # don't display header --transpose-output|to # switch rows and columns =head3 INPUT VALIDATION By default, C skips invalid input with a warning. You can change this behavior with the following options: --strict # throws an error, interrupting process --quiet|q # no warning =head1 AUTHOR Nelson Ferraz L<> =head1 CONTRIBUTE Send comments, suggestions and bug reports to: https://github.com/nferraz/st/issues Or fork the code on github: https://github.com/nferraz/st =head2 THANKS imurray, who suggested a different algorithm for calculating variance. asgeirn, who suggested a input filter and helped to remove some warnings. gabeguz, who modified the script to make it more portable. =head1 COPYRIGHT Copyright (c) 2013 Nelson Ferraz. This program is free software; you can redistribute it and/or modify it under the MIT License (see LICENSE). st-1.1.4/t/000077500000000000000000000000001312417325500124115ustar00rootroot00000000000000st-1.1.4/t/01-use.t000066400000000000000000000001451312417325500136100ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; plan tests => 1; use App::St; pass(); st-1.1.4/t/02-new.t000066400000000000000000000001771312417325500136130ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; plan tests => 1; use App::St; my $st = App::St->new(); pass(); st-1.1.4/t/03-validate.t000066400000000000000000000015711312417325500146130ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; my %validation = ( # valid '123' => 1, '+123' => 1, '-123' => 1, '123.0' => 1, '+123.0' => 1, '-123.0' => 1, '1.23' => 1, '+1.23' => 1, '-1.23' => 1, '.123' => 1, '+.123' => 1, '-.123' => 1, '1e2' => 1, '+1e2' => 1, '+1e-2' => 1, '1.23E2' => 1, '1.23E+2' => 1, '1.23E-2' => 1, '+1.23e2' => 1, '-1.23e2' => 1, '-1.23e+2' => 1, '-1.23e-2' => 1, # invalid '' => 0, '123+' => 0, '+-123' => 0, '1+23' => 0, '1e+-23' => 0, '-+123' => 0, '1.2.3' => 0, 'x' => 0, '1X2' => 0, ); plan tests => scalar keys %validation; use App::St; my $st = App::St->new(); for my $num (keys %validation) { my $result = $validation{$num}; ok( $st->validate($num) == $result ); } st-1.1.4/t/04-process.t000066400000000000000000000003011312417325500144670ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; my @num = (1..10); plan tests => 1; use App::St; my $st = App::St->new(); for my $num (@num) { $st->process($num); } pass(); st-1.1.4/t/05-basic-stats.t000066400000000000000000000005261312417325500152400ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; plan tests => 6; use App::St; my $st = App::St->new(); # process numbers from 1 to 10 $st->process($_) for (1..10); is( $st->N(), 10 ); is( $st->min(), 1 ); is( $st->max(), 10 ); is( $st->sum(), 55 ); is( $st->mean(), 5.5 ); is( int($st->stddev()*100)/100, 3.02 ); st-1.1.4/t/05-format.t000066400000000000000000000007451312417325500143160ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; plan tests => 6; use App::St; my $st = App::St->new( format => '%.5f' ); # process numbers from 1 to 10 $st->process($_) for (1..10); # non-formatted cmp_ok( $st->N(), 'eq', '10' ); cmp_ok( $st->min(), 'eq', '1' ); cmp_ok( $st->max(), 'eq', '10' ); cmp_ok( $st->sum(), 'eq', '55' ); # formatted cmp_ok( $st->mean( formatted => 1 ), 'eq', '5.50000' ); cmp_ok( $st->stddev( formatted => 1), 'eq', '3.02765' ); st-1.1.4/t/06-percentile.t000066400000000000000000000005731312417325500151600ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; use App::St; my $st = App::St->new( keep_data => 1 ); for my $num (reverse 1..10) { $st->process($num); } my %percentiles = ( 0 => 1, 50 => 5.5, 90 => 9.5, 100 => 10, ); plan tests => scalar keys %percentiles; for my $p (keys %percentiles) { is($st->percentile($p), $percentiles{$p}); } st-1.1.4/t/06-quantiles.t000066400000000000000000000005571312417325500150350ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; use App::St; my $st = App::St->new( keep_data => 1 ); for my $num (1..10) { $st->process($num); } my %quartiles = ( 0 => 1, 1 => 3.5, 2 => 5.5, 3 => 7.5, 4 => 10, ); plan tests => scalar keys %quartiles; for my $q (keys %quartiles) { is($st->quartile($q), $quartiles{$q}); } st-1.1.4/t/07-result.t000066400000000000000000000007071312417325500143440ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; use App::St; my $st = App::St->new( keep_data => 1 ); # process numbers from 1 to 10 $st->process($_) for (1..10); my %result = $st->result(); my %expected = ( min => 1, q1 => 3.5, median => 5.5, q3 => 7.5, max => 10, mean => 5.5, ); plan tests => scalar keys %expected; for my $stat (keys %expected) { is($result{$stat}, $expected{$stat}); }