File Coverage

blib/lib/Math/SlideRule.pm
Criterion Covered Total %
statement 111 112 99.1
branch 42 44 95.4
condition 13 13 100.0
subroutine 12 12 100.0
pod 4 4 100.0
total 182 185 98.3


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # slide rule virtualization for Perl
4              
5             package Math::SlideRule;
6              
7 2     2   100362 use 5.010000;
  2         7  
8              
9 2     2   452 use Moo;
  2         14596  
  2         9  
10 2     2   2823 use namespace::clean;
  2         14221  
  2         19  
11 2     2   705 use Scalar::Util qw/looks_like_number/;
  2         4  
  2         2658  
12              
13             our $VERSION = '1.11';
14              
15             ########################################################################
16             #
17             # ATTRIBUTES
18              
19             # these are taken from common scale names on a slide rule; see code for
20             # how they are populated
21             has A => ( is => 'lazy', );
22             has C => ( is => 'lazy', );
23              
24 1     1   15 sub _build_A { $_[0]->_range_exp_weighted( 1, 100 ) }
25 1     1   15 sub _build_C { $_[0]->_range_exp_weighted( 1, 10 ) }
26              
27             # increased precision comes at the cost of additional memory use
28             #
29             # NOTE changing the precision after A, C and so forth have been
30             # generated will do nothing to those values. instead, construct a new
31             # object with a different precision set, if necessary
32             has precision => ( is => 'rw', default => sub { 10_000 } );
33              
34             ########################################################################
35             #
36             # METHODS
37              
38             # builds two arrays, one of values (1, 2, 3...), another of distances
39             # based on the log of those values. these arrays returned in a hash
40             # reference. slide rule lookups obtain the index of a value, then use
41             # that to find the distance of that value, then uses other distances
42             # to figure out some new location, that a new value can be worked back
43             # out from
44             #
45             # NOTE that these scales are not calibrated directly to one another
46             # as they would be on a slide rule
47             sub _range_exp_weighted {
48 2     2   7 my ( $self, $min, $max ) = @_;
49              
50 2         15 my @range = map log, $min, $max;
51 2         4 my ( @values, @distances );
52              
53 2         11 my $slope = ( $range[1] - $range[0] ) / $self->precision;
54              
55 2         8 for my $d ( 0 .. $self->precision ) {
56             # via slope equation; y = mx + b and m = (y2-y1)/(x2-x1) with
57             # assumption that precision 0..$mp and @range[min,max]
58 20002         24991 push @distances, $slope * $d + $range[0];
59 20002         25203 push @values, exp $distances[-1];
60             }
61              
62 2         80 return { value => \@values, dist => \@distances };
63             }
64              
65             # binary search an array of values for a given value, returning index of
66             # the closest match. used to lookup values and their corresponding
67             # distances from the various A, C, etc. attribute tables. NOTE this
68             # routine assumes that the given value has been normalized e.g. via
69             # standard_form to lie somewhere on or between the minimum and maximum
70             # values in the given array reference
71             sub _rank {
72 95     95   786 my ( $self, $value, $ref ) = @_;
73              
74 95         104 my $lo = 0;
75 95         115 my $hi = $#$ref;
76              
77 95         164 while ( $lo <= $hi ) {
78 999         1175 my $mid = int( $lo + ( $hi - $lo ) / 2 );
79 999 100       1500 if ( $ref->[$mid] > $value ) {
    100          
80 477         637 $hi = $mid - 1;
81             } elsif ( $ref->[$mid] < $value ) {
82 475         657 $lo = $mid + 1;
83             } else {
84 47         85 return $mid;
85             }
86             }
87              
88             # no exact match; return index of value closest to the numeral supplied
89 48 50       79 if ( $lo > $#$ref ) {
90 0         0 return $hi;
91             } else {
92 48 100       345 if ( abs( $ref->[$lo] - $value ) >= abs( $ref->[$hi] - $value ) ) {
93 26         52 return $hi;
94             } else {
95 22         53 return $lo;
96             }
97             }
98             }
99              
100             # division is just multiplication done backwards on a slide rule, as the
101             # same physical distances are involved. there are also "CF" and "CI" (C
102             # scale, folded, or inverse) and so forth scales to assist with such
103             # operations, though these mostly just help avoid excess motions on the
104             # slide rule
105             #
106             # NOTE cannot just pass m*(1/n) to multiply() because that looses
107             # precision: .82 for 75/92 while can get .815 on pocket slide rule
108             sub divide {
109 10     10 1 569 my $self = shift;
110 10         14 my $n = shift;
111 10         14 my $i = 0;
112              
113 10 100       33 die "need at least two numbers\n" if @_ < 1;
114 9 100 100     56 die "argument index $i not a number\n"
115             unless defined $n and looks_like_number($n);
116              
117 7         22 my ( $n_coe, $n_exp, $neg_count ) = $self->standard_form($n);
118              
119 7         122 my $n_idx = $self->_rank( $n_coe, $self->C->{value} );
120 7         123 my $distance = $self->C->{dist}[$n_idx];
121 7         35 my $exponent = $n_exp;
122              
123 7         14 for my $m (@_) {
124 10         13 $i++;
125 10 100       39 die "argument index $i not a number\n" if !looks_like_number($m);
126              
127 8 100       16 $neg_count++ if $m < 0;
128              
129 8         17 my ( $m_coe, $m_exp, undef ) = $self->standard_form($m);
130 8         78 my $m_idx = $self->_rank( $m_coe, $self->C->{value} );
131              
132 8         92 $distance -= $self->C->{dist}[$m_idx];
133 8         33 $exponent -= $m_exp;
134              
135 8 50       78 if ( $distance < $self->C->{dist}[0] ) {
136 8         93 $distance = $self->C->{dist}[-1] + $distance;
137 8         32 $exponent--;
138             }
139             }
140              
141 5         45 my $d_idx = $self->_rank( $distance, $self->C->{dist} );
142 5         59 my $product = $self->C->{value}[$d_idx];
143              
144 5         30 $product *= 10**$exponent;
145 5 100       16 $product *= -1 if $neg_count % 2 == 1;
146              
147 5         77 return $product;
148             }
149              
150             sub multiply {
151 21     21 1 158 my $self = shift;
152 21         25 my $n = shift;
153 21         28 my $i = 0;
154              
155 21 100       54 die "need at least two numbers\n" if @_ < 1;
156 20 100 100     87 die "argument index $i not a number\n"
157             unless defined $n and looks_like_number($n);
158              
159 18         39 my ( $n_coe, $n_exp, $neg_count ) = $self->standard_form($n);
160              
161             # chain method has first lookup on D and then subsequent done by
162             # moving C on slider and keeping tabs with the hairline, then reading
163             # back on D for the final result. (plus incrementing the exponent
164             # count when a reverse slide is necessary, for example for 3.4*4.1, as
165             # that jumps to the next magnitude)
166             #
167             # one can also do the multiplication on the A and B scales, which is
168             # handy if you then need to pull the square root off of D. but this
169             # implementation ignores such alternatives
170 18         252 my $n_idx = $self->_rank( $n_coe, $self->C->{value} );
171 18         161 my $distance = $self->C->{dist}[$n_idx];
172 18         66 my $exponent = $n_exp;
173              
174 18         28 for my $m (@_) {
175 26         42 $i++;
176 26 100       61 die "argument index $i not a number\n" if !looks_like_number($m);
177              
178 24 100       39 $neg_count++ if $m < 0;
179              
180 24         39 my ( $m_coe, $m_exp, undef ) = $self->standard_form($m);
181 24         195 my $m_idx = $self->_rank( $m_coe, $self->C->{value} );
182              
183 24         188 $distance += $self->C->{dist}[$m_idx];
184 24         80 $exponent += $m_exp;
185              
186             # order of magnitude change, adjust back to bounds (these are
187             # notable on a slide rule by having to index from the opposite
188             # direction than usual for the C and D scales (though one could
189             # also obtain the value with the A and B or the CI and DI
190             # scales, but those would then need some rule to track the
191             # exponent change))
192 24 100       202 if ( $distance > $self->C->{dist}[-1] ) {
193 8         74 $distance -= $self->C->{dist}[-1];
194 8         31 $exponent++;
195             }
196             }
197              
198 16         139 my $d_idx = $self->_rank( $distance, $self->C->{dist} );
199 16         118 my $product = $self->C->{value}[$d_idx];
200              
201 16         68 $product *= 10**$exponent;
202 16 100       31 $product *= -1 if $neg_count % 2 == 1;
203              
204 16         144 return $product;
205             }
206              
207             # relies on conversion from A to C scales (and that the distances in
208             # said scales are linked to one another)
209             sub sqrt {
210 9     9 1 108 my ( $self, $n ) = @_;
211 9 100 100     56 die "argument not a number\n" unless defined $n and looks_like_number($n);
212 7 100       22 die "Can't take sqrt of $n\n" if $n < 0;
213              
214 6         13 my ( $n_coe, $n_exp, undef ) = $self->standard_form($n);
215              
216 6 100       17 if ( $n_exp % 2 == 1 ) {
217 3         4 $n_coe *= 10;
218 3         4 $n_exp--;
219             }
220              
221 6         120 my $n_idx = $self->_rank( $n_coe, $self->A->{value} );
222              
223             # NOTE division is due to A and C scale distances not being calibrated
224             # directly with one another
225 6         122 my $distance = $self->A->{dist}[$n_idx] / 2;
226              
227 6         112 my $d_idx = $self->_rank( $distance, $self->C->{dist} );
228 6         81 my $sqrt = $self->C->{value}[$d_idx];
229              
230 6         40 $sqrt *= 10**( $n_exp / 2 );
231              
232 6         72 return $sqrt;
233             }
234              
235             # converts numbers to standard form (scientific notation) or otherwise
236             # between a particular range of numbers (to support A/B "double
237             # decade" scales)
238             sub standard_form {
239 76     76 1 129 my ( $self, $val, $min, $max ) = @_;
240              
241 76   100     222 $min //= 1;
242 76   100     178 $max //= 10;
243              
244 76 100       140 my $is_neg = $val < 0 ? 1 : 0;
245              
246 76         87 $val = abs $val;
247 76         76 my $exp = 0;
248              
249 76 100       182 if ( $val < $min ) {
    100          
250 9         21 while ( $val < $min ) {
251 17         33 $val *= 10;
252 17         27 $exp--;
253             }
254             } elsif ( $val >= $max ) {
255 45         78 while ( $val >= $max ) {
256 59         83 $val /= 10;
257 59         92 $exp++;
258             }
259             }
260              
261 76         197 return $val, $exp, $is_neg;
262             }
263              
264             1;
265             __END__