It's a Time for Tolerance
Secret Origins!
Once upon a time, I was called upon to write a database for complex semiconductor growth specifications. Very often, the specification would state that a given measurement had to be within a certain tolerance, rather than a specific value. For bizarre semi-political reasons, we couldn't just provide outer limits and shoot for a center point. We had to track, through all parts of the system, the specific way in which the specification was... specified. That might be like any of the following:
5
> 5
≥ 5
< 5
≥ 5
5 ± 1
5 ± 20%
4 .. 6
5 (-1 to +2)
At the time, the need to stick to a given format was frustrating, but it drove me to write a library that has proven itself useful again and again -- mostly in weird situations, but I'm still happy to have a tool I only use once in a while.
Tolerance in Form and Action
A tolerance is an object that acts like a number for the sake of comparison.
Rather than struggle to contrive a generic example, I'll talk about how this code was originally used. Our customer is going to come to us with a specification for a product, and we want to be able to record that specification, overlay our own specifications, and then test results against it. The customer might give us the following set of requirements:
1: | use Number::Tolerant; |
Internally, we know what our production equipment's capabilities are:
1: | $spec{machine} = { |
...and engineering doesn't want to use anything too heavy, and have done some final design work, so they tack on:
1: | $spec{engineering} = { |
Now we have a bunch of different, separate specification documents, and we can have a quick review of them all:
1: | sub tol_string { |
...to get...
CUSTOMER
depth 182 +/- 5%
hue 630 <= x <= 740
weight x < 10
width 10 +/- 2
ENGINEERING
weight x < 2
width 9
MACHINE
depth x < 184
hue 675
weight x < 50
width 7 <= x <= 10
Composing these together is easy:
1: | my $final_spec = {}; |
...and we get:
depth 172.9 <= x < 184
hue 675
weight x < 2
width 9
What just happened? Well, tolerances can be joined together logically with intersections and unions. When we want to produce the intersection of a bunch of tolerances, we just use the &
operator. Or, if you want to avoid weird overloading, you can use the union
method. When unions intersect, the result is either a new tolerance (like the unified depth specification above) or a constant giving the only permissible value. When a union would produce an impossible tolerance, an exception is thrown. I accidentally caused one of those when writing the above code. I'd picked a bad hue
for the machine spec and got:
No valid intersection of (630 <= x <= 740) and (775) at...
Once we've gotten our final product, it's easy to test against the spec:
1: | my $final_product = get_final_product; |
...which might produce an error like:
weight (2.2) outside of specification (x < 2)
Testing with Tolerance
Tolerances can also be handy for running automated tests. An output file's size should fall into a given range, or we should process no more than a certain number of records, and so on. Using Number::Tolerant with tests can be useful when tolerances must be combined -- but it's often just easier to use than Test::More's cmp_ok
, which has pretty gross semantics. For example, compare:
1: | use Test::More; |
to:
1: | use Test::Tolerant; |
Test::Tolerant is part of the Number-Tolerant distribution.