File Coverage

blib/lib/Number/Tolerant.pm
Criterion Covered Total %
statement 116 116 100.0
branch 85 88 96.5
condition 68 72 94.4
subroutine 31 31 100.0
pod 10 10 100.0
total 310 317 97.7


line stmt bran cond sub pod time code
1 25     25   3014060 use strict;
  25         55  
  25         1060  
2 25     25   245 use warnings;
  25         57  
  25         2064  
3             package Number::Tolerant 1.711; # TRIAL
4             # ABSTRACT: tolerance ranges for inexact numbers
5              
6 25     25   12775 use Sub::Exporter::Util;
  25         520035  
  25         215  
7 25         144 use Sub::Exporter 0.950 -setup => {
8             exports => { tolerance => Sub::Exporter::Util::curry_class('new'), },
9             groups => { default => [ qw(tolerance) ] },
10 25     25   6631 };
  25         459  
11              
12 25     25   13564 use Carp ();
  25         60  
  25         471  
13 25     25   138 use Scalar::Util ();
  25         47  
  25         55673  
14              
15             #pod =head1 SYNOPSIS
16             #pod
17             #pod use Number::Tolerant;
18             #pod
19             #pod my $range = tolerance(10 => to => 12);
20             #pod my $random = 10 + rand(2);
21             #pod
22             #pod die "I shouldn't die" unless $random == $range;
23             #pod
24             #pod print "This line will always print.\n";
25             #pod
26             #pod =head1 DESCRIPTION
27             #pod
28             #pod Number::Tolerant creates a number-like object whose value refers to a range of
29             #pod possible values, each equally acceptable. It overloads comparison operations
30             #pod to reflect this.
31             #pod
32             #pod I use this module to simplify the comparison of measurement results to
33             #pod specified tolerances.
34             #pod
35             #pod reject $product unless $measurement == $specification;
36             #pod
37             #pod =head1 METHODS
38             #pod
39             #pod =head2 Instantiation
40             #pod
41             #pod =head3 new
42             #pod
43             #pod =head3 tolerance
44             #pod
45             #pod There is a C method on the Number::Tolerant class, but it also exports a
46             #pod simple function, C, which will return an object of the
47             #pod Number::Tolerant class. Both use the same syntax:
48             #pod
49             #pod my $range = Number::Tolerant->new( $x => $method => $y);
50             #pod
51             #pod my $range = tolerance( $x => $method => $y);
52             #pod
53             #pod The meaning of C<$x> and C<$y> are dependent on the value of C<$method>, which
54             #pod describes the nature of the tolerance. Tolerances can be defined in five ways,
55             #pod at present:
56             #pod
57             #pod method range
58             #pod -------------------+------------------
59             #pod plus_or_minus | x +/- y
60             #pod plus_or_minus_pct | x +/- (y% of x)
61             #pod or_more | x to Inf
62             #pod or_less | x to -Inf
63             #pod more_than | x to Inf, not x
64             #pod less_than | x to -Inf, not x
65             #pod to | x to y
66             #pod infinite | -Inf to Inf
67             #pod offset | (x + y1) to (x + y2)
68             #pod
69             #pod For C and C, C<$y> is ignored if passed. For C,
70             #pod neither C<$x> nor C<$y> is used; "infinite" should be the sole argument. The
71             #pod first two arguments can be reversed for C and C, to be
72             #pod more English-like.
73             #pod
74             #pod Offset tolerances are slightly unusual. Here is an example:
75             #pod
76             #pod my $offset_tolerance = tolerance(10 => offset => (-3, 5));
77             #pod # stringifies to: 10 (-3 +5)
78             #pod
79             #pod An offset is very much like a C tolerance, but its center value
80             #pod is not necessarily the midpoint between its extremes. This is significant for
81             #pod comparisons and numifications of the tolerance. Given the following two
82             #pod tolerances:
83             #pod
84             #pod my $pm_dice = tolerance(10.5 => plus_or_minus => 7.5);
85             #pod my $os_dice = tolerance(11 => offset => (-8, 7));
86             #pod
87             #pod The first will sort as numerically less than the second.
88             #pod
89             #pod If the given arguments can't be formed into a tolerance, an exception will be
90             #pod raised.
91             #pod
92             #pod =cut
93              
94             # these are the default plugins
95             my %_plugins;
96              
97             sub _plugins {
98 137     137   703 keys %_plugins
99             }
100              
101             sub disable_plugin {
102 2     2 1 6 my ($class, $plugin) = @_;
103 2         9 $class->_boot_up;
104 2         7 delete $_plugins{ $plugin };
105 2         5 return;
106             }
107              
108             sub enable_plugin {
109 255     255 1 835725 my ($class, $plugin) = @_;
110 255         802 $class->_boot_up;
111              
112             # XXX: there has to be a better test to use here -- rjbs, 2006-01-27
113 255 100       426 unless (eval { $plugin->can('construct') }) {
  255         2007  
114 252 100       17749 eval "require $plugin" or die $@;
115             }
116              
117 254 100       1205 unless (eval { $class->validate_plugin($plugin); }) {
  254         1709  
118 1         187 Carp::croak "class $plugin is not a valid Number::Tolerant plugin: $@";
119             }
120              
121 253         861 $_plugins{ $plugin } = undef;
122 253         3586 return;
123             }
124              
125             sub validate_plugin {
126 254     254 1 741 my ($class, $plugin) = @_;
127 254         680 for (qw(parse valid_args construct)) {
128 760 100       5087 die "can't $_" unless $plugin->can($_);
129             }
130 253         874 return 1;
131             }
132              
133             my $booted;
134             sub _boot_up {
135 429 100   429   1293 return if $booted;
136 25         57 $booted = 1;
137             my @_default_plugins =
138 25         85 map { "Number::Tolerant::Type::$_" }
  250         617  
139             qw(
140             constant infinite less_than
141             more_than offset or_less
142             or_more plus_or_minus plus_or_minus_pct
143             to
144             );
145              
146 25         173 __PACKAGE__->enable_plugin($_) for @_default_plugins;
147             }
148              
149             sub new {
150 138     138 1 7789608 my $class = shift;
151 138         549 $class->_boot_up;
152 138 100       459 return unless @_;
153 137         289 my $self;
154              
155 137         491 for my $type ($class->_plugins) {
156 833 100       3092 next unless my @args = $type->valid_args(@_);
157 114         441 my $guts = $type->construct(@args);
158              
159 114 100 66     2604 return $guts unless ref $guts and not Scalar::Util::blessed($guts);
160              
161 110 100 100     750 if (
      100        
      100        
162             defined $guts->{min} and defined $guts->{max} and
163             $guts->{min} == $guts->{max} and
164             not $guts->{constant}
165             ) {
166 3         22 @_ = ($class, $guts->{min});
167 3         23 goto &new;
168             }
169 107         680 $self = { method => $type, %$guts };
170 107         460 last;
171             }
172              
173 130 100       5533 Carp::confess("couldn't form tolerance from given args") unless $self;
174 107         587 bless $self => $self->{method};
175             }
176              
177             #pod =head3 from_string
178             #pod
179             #pod A new tolerance can be instantiated from the stringification of an old
180             #pod tolerance. For example:
181             #pod
182             #pod my $range = Number::Tolerant->from_string("10 to 12");
183             #pod
184             #pod die "Everything's OK!" if 11 == $range; # program dies of joy
185             #pod
186             #pod This will I yet parse stringified unions, but that will be implemented in
187             #pod the future. (I just don't need it yet.)
188             #pod
189             #pod If a string can't be parsed, an exception is raised.
190             #pod
191             #pod =cut
192              
193             sub from_string {
194 34     34 1 614965 my ($class, $string) = @_;
195 34         165 $class->_boot_up;
196 34 100       304 Carp::croak "from_string is a class method" if ref $class;
197 33         215 for my $type (keys %_plugins) {
198 217 100       1252 if (defined(my $tolerance = $type->parse($string, $class))) {
199 29         158 return $tolerance;
200             }
201             }
202              
203 4         739 Carp::confess("couldn't form tolerance from given string");
204             }
205              
206             sub stringify {
207 53     53 1 32135 my ($self) = @_;
208              
209 53 100 100     598 return 'any number' unless (defined $self->{min} || defined $self->{max});
210              
211 49         144 my $string = '';
212              
213 49 100       154 if (defined $self->{min}) {
214 37 100       205 $string .= "$self->{min} <" . ($self->{exclude_min} ? q{} : '=') . q{ };
215             }
216              
217 49         141 $string .= 'x';
218              
219 49 100       133 if (defined $self->{max}) {
220 33 100       124 $string .= ' <' . ($self->{exclude_max} ? q{} : '=') . " $self->{max}";
221             }
222              
223 49         638 return $string;
224             }
225              
226             #pod =head2 stringify_as
227             #pod
228             #pod my $string = $tolerance->stringify_as($type);
229             #pod
230             #pod This method does nothing! Someday, it will stringify the given tolerance as a
231             #pod different type, if possible. "10 +/- 1" will
232             #pod C to "10 +/- 10%" for example.
233             #pod
234             #pod =cut
235              
236       2 1   sub stringify_as { }
237              
238             #pod =head2 numify
239             #pod
240             #pod my $n = $tolerance->numify;
241             #pod
242             #pod This returns the numeric form of a tolerance. If a tolerance has both a
243             #pod minimum and a maximum, and they are the same, then that is the numification.
244             #pod Otherwise, numify returns undef.
245             #pod
246             #pod =cut
247              
248             sub numify {
249             # if a tolerance has equal min and max, it numifies to that number
250             return $_[0]{min}
251 3 50 100 3 1 32 if $_[0]{min} and $_[0]{max} and $_[0]{min} == $_[0]{max};
      66        
252             ## no critic (ReturnUndef)
253 3         16 return undef;
254             }
255              
256 449   100 449   2573 sub _num_eq { not( _num_gt($_[0],$_[1]) or _num_lt($_[0],$_[1]) ) }
257              
258 86     86   1539 sub _num_ne { not _num_eq(@_) }
259              
260 667 100   667   2707 sub _num_gt { $_[2] ? goto &_num_lt_canonical : goto &_num_gt_canonical }
261              
262 552 100   552   2620 sub _num_lt { $_[2] ? goto &_num_gt_canonical : goto &_num_lt_canonical }
263              
264 50 100   50   194 sub _num_gte { $_[1] == $_[0] ? 1 : goto &_num_gt; }
265              
266 50 100   50   178 sub _num_lte { $_[1] == $_[0] ? 1 : goto &_num_lt; }
267              
268             sub _num_gt_canonical {
269 658 100 100 658   2328 return 1 if $_[0]{exclude_min} and $_[0]{min} == $_[1];
270             defined $_[0]->{min} ? $_[1] < $_[0]->{min} : undef
271 630 100       3288 }
272              
273             sub _num_lt_canonical {
274 561 100 100 561   1727 return 1 if $_[0]{exclude_max} and $_[0]{max} == $_[1];
275             defined $_[0]->{max} ? $_[1] > $_[0]->{max} : undef
276 547 100       3588 }
277              
278 8     8   4728 sub _union { $_[0]->union($_[1]); }
279              
280             sub union {
281 8     8 1 496 require Number::Tolerant::Union;
282 8         34 return Number::Tolerant::Union->new($_[0],$_[1]);
283             }
284              
285 24     24   18001 sub _intersection { $_[0]->intersection($_[1]); }
286              
287             sub intersection {
288 24 100   24 1 86 if (! ref $_[1]) {
289 6 100       18 return $_[1] if $_[0] == $_[1];
290 1         6 Carp::confess "no valid intersection of ($_[0]) and ($_[1])";
291             }
292              
293 18         53 my ($min, $max);
294 18         0 my ($exclude_min, $exclude_max);
295              
296 18 100 100     259 if (defined $_[0]->{min} and defined $_[1]->{min}) {
297 8         37 ($min) = sort {$b<=>$a} ($_[0]->{min}, $_[1]->{min});
  8         27  
298             } else {
299 10 100       109 $min = defined $_[0]->{min} ? $_[0]->{min} : $_[1]->{min};
300             }
301              
302             $exclude_min = 1
303             if ($_[0]{min} and $min == $_[0]{min} and $_[0]{exclude_min})
304 18 100 100     224 or ($_[1]{min} and $min == $_[1]{min} and $_[1]{exclude_min});
      100        
      100        
      100        
      100        
305              
306 18 100 100     130 if (defined $_[0]->{max} and defined $_[1]->{max}) {
307 8         34 ($max) = sort {$a<=>$b} ($_[0]->{max}, $_[1]->{max});
  8         28  
308             } else {
309 10 100       30 $max = defined $_[0]->{max} ? $_[0]->{max} : $_[1]->{max};
310             }
311              
312             $exclude_max = 1
313             if ($_[0]{max} and $max == $_[0]{max} and $_[0]{exclude_max})
314 18 100 100     234 or ($_[1]{max} and $max == $_[1]{max} and $_[1]{exclude_max});
      100        
      100        
      100        
      100        
315              
316 18 100 100     68 return $_[0]->new('infinite') unless defined $min || defined $max;
317              
318 17 100       65 return $_[0]->new($min => ($exclude_min ? 'more_than' : 'or_more'))
    100          
319             unless defined $max;
320              
321 13 100       46 return $_[0]->new($max => ($exclude_max ? 'less_than' : 'or_less'))
    100          
322             unless defined $min;
323              
324 10 50 33     39 Carp::confess "no valid intersection of ($_[0]) and ($_[1])"
325             if $max < $min or $min > $max;
326              
327 10         74 bless {
328             max => $max,
329             min => $min,
330             exclude_max => $exclude_max,
331             exclude_min => $exclude_min
332             } => 'Number::Tolerant::Type::to';
333             }
334              
335             #pod =head2 Overloading
336             #pod
337             #pod Tolerances overload a few operations, mostly comparisons.
338             #pod
339             #pod =over
340             #pod
341             #pod =item boolean
342             #pod
343             #pod Tolerances are always true.
344             #pod
345             #pod =item numify
346             #pod
347             #pod Most tolerances numify to undef; see C>.
348             #pod
349             #pod =item stringify
350             #pod
351             #pod A tolerance stringifies to a short description of itself, generally something
352             #pod like "m < x < n"
353             #pod
354             #pod infinite - "any number"
355             #pod to - "m <= x <= n"
356             #pod or_more - "m <= x"
357             #pod or_less - "x <= n"
358             #pod more_than - "m < x"
359             #pod less_than - "x < n"
360             #pod offset - "x (-y1 +y2)"
361             #pod constant - "x"
362             #pod plus_or_minus - "x +/- y"
363             #pod plus_or_minus_pct - "x +/- y%"
364             #pod
365             #pod =item equality
366             #pod
367             #pod A number is equal to a tolerance if it is neither less than nor greater than
368             #pod it. (See below).
369             #pod
370             #pod =item comparison
371             #pod
372             #pod A number is greater than a tolerance if it is greater than its maximum value.
373             #pod
374             #pod A number is less than a tolerance if it is less than its minimum value.
375             #pod
376             #pod No number is greater than an "or_more" tolerance or less than an "or_less"
377             #pod tolerance.
378             #pod
379             #pod "...or equal to" comparisons include the min/max values in the permissible
380             #pod range, as common sense suggests.
381             #pod
382             #pod =item tolerance intersection
383             #pod
384             #pod A tolerance C<&> a tolerance or number is the intersection of the two ranges.
385             #pod Intersections allow you to quickly narrow down a set of tolerances to the most
386             #pod stringent intersection of values.
387             #pod
388             #pod tolerance(5 => to => 6) & tolerance(5.5 => to => 6.5);
389             #pod # this yields: tolerance(5.5 => to => 6)
390             #pod
391             #pod If the given values have no intersection, C<()> is returned.
392             #pod
393             #pod An intersection with a normal number will yield that number, if it is within
394             #pod the tolerance.
395             #pod
396             #pod =item tolerance union
397             #pod
398             #pod A tolerance C<|> a tolerance or number is the union of the two. Unions allow
399             #pod multiple tolerances, whether they intersect or not, to be treated as one. See
400             #pod L for more information.
401             #pod
402             #pod =cut
403              
404             use overload
405             fallback => 1,
406 10     10   3177 'bool' => sub { 1 },
407             '0+' => 'numify',
408             '<=>' => sub {
409 90 50   90   372 my $rv = $_[0] == $_[1] ? 0
    100          
    100          
410             : $_[0] < $_[1] ? -1
411             : $_[0] > $_[1] ? 1
412             : die "impossible";
413 90 100       321 $rv *= -1 if $_[2];
414 90         442 return $rv;
415             },
416 25         472 '""' => 'stringify',
417             '==' => '_num_eq',
418             '!=' => '_num_ne',
419             '>' => '_num_gt',
420             '<' => '_num_lt',
421             '>=' => '_num_gte',
422             '<=' => '_num_lte',
423             '|' => '_union',
424             '&' => '_intersection',
425 25     25   239 ;
  25         106  
426              
427             #pod =back
428             #pod
429             #pod =head1 EXTENDING
430             #pod
431             #pod This feature is slighly experimental, but it's here.
432             #pod
433             #pod New tolerance types may be written as subclasses of L,
434             #pod providing the interface described in its documentation. They can then be
435             #pod enabled or disabled with the following methods:
436             #pod
437             #pod =head2 C< enable_plugin >
438             #pod
439             #pod Number::Tolerant->enable_plugin($class_name);
440             #pod
441             #pod This method enables the named class, so that attempts to create new tolerances
442             #pod will check against this class. Classes are checked against
443             #pod C> before being enabled. An exception is thrown if the
444             #pod class does not appear to provide the Number::Tolerant::Type interface.
445             #pod
446             #pod =head2 C< disable_plugin >
447             #pod
448             #pod Number::Tolerant->disable_plugin($class_name);
449             #pod
450             #pod This method will disable the named class, so that future attempts to create new
451             #pod tolerances will not check against this class.
452             #pod
453             #pod =head2 C< validate_plugin >
454             #pod
455             #pod Number::Tolerant->validate_plugin($class_name);
456             #pod
457             #pod This method checks (naively) that the given class provides the interface
458             #pod defined in Number::Tolerant::Type. If it does not, an exception is thrown.
459             #pod
460             #pod =head1 TODO
461             #pod
462             #pod =over 4
463             #pod
464             #pod =item * Extend C to cover unions.
465             #pod
466             #pod =item * Extend C to include Number::Range-type specifications.
467             #pod
468             #pod =item * Allow translation into forms not originally used:
469             #pod
470             #pod my $range = tolerance(9 => to => 17);
471             #pod my $range_pm = $range->convert_to('plus_minus');
472             #pod $range->stringify_as('plus_minus_pct');
473             #pod
474             #pod =item * Create a factory so that you can simultaneously work with two sets of plugins.
475             #pod
476             #pod This one is very near completion. There will now be two classes that should be
477             #pod used: Number::Tolerant::Factory, which produces tolerances, and
478             #pod Number::Tolerant::Tolerance, which is a tolerance. Both will inherit from
479             #pod N::T, for supporting old code, and N::T will dispatch construction methods to a
480             #pod default factory.
481             #pod
482             #pod =back
483             #pod
484             #pod =head1 SEE ALSO
485             #pod
486             #pod The module L provides another way to deal with ranges of
487             #pod numbers. The major differences are: N::R is set-like, not range-like; N::R
488             #pod does not overload any operators. Number::Tolerant will not (like N::R) attempt
489             #pod to parse a textual range specification like "1..2,5,7..10" unless specifically
490             #pod instructed to. (The valid formats for strings passed to C does
491             #pod not match Number::Range exactly. See TODO.)
492             #pod
493             #pod The C code:
494             #pod
495             #pod $range = Number::Range->new("10..15","20..25");
496             #pod
497             #pod Is equivalent to the C code:
498             #pod
499             #pod $range = Number::Tolerant::Union->new(10..15,20..25);
500             #pod
501             #pod ...while the following code expresses an actual range:
502             #pod
503             #pod $range = tolerance(10 => to => 15) | tolerance(20 => to => 25);
504             #pod
505             #pod =head1 THANKS
506             #pod
507             #pod Thanks to Yuval Kogman and #perl-qa for helping find the bizarre bug that drove
508             #pod the minimum required perl up to 5.8
509             #pod
510             #pod Thanks to Tom Freedman, who reminded me that this code was fun to work on, and
511             #pod also provided the initial implementation for the offset type.
512             #pod
513             #pod =cut
514              
515             "1 +/- 0";
516              
517             __END__