It's a Time for Tolerance
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:
Internally, we know what our production equipment's capabilities are:
...and engineering doesn't want to use anything too heavy, and have done some final design work, so they tack on:
Now we have a bunch of different, separate specification documents, and we can have a quick review of them all:
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:
...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:
...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:
Test::Tolerant is part of the Number-Tolerant distribution.