File Coverage

blib/lib/Arithmetic/PaperAndPencil/Number.pm
Criterion Covered Total %
statement 60 174 34.4
branch 8 50 16.0
condition 2 15 13.3
subroutine 12 19 63.1
pod 8 8 100.0
total 90 266 33.8


line stmt bran cond sub pod time code
1             # -*- encoding: utf-8; indent-tabs-mode: nil -*-
2              
3              
4 3     3   41 use 5.38.0;
  3         12  
5 3     3   20 use utf8;
  3         5  
  3         19  
6 3     3   92 use strict;
  3         7  
  3         60  
7 3     3   15 use warnings;
  3         5  
  3         169  
8 3     3   17 use open ':encoding(UTF-8)';
  3         5  
  3         16  
9 3     3   378 use feature qw/class/;
  3         23  
  3         433  
10 3     3   20 use experimental qw/class/;
  3         5  
  3         19  
11              
12             class Arithmetic::PaperAndPencil::Number 0.01;
13              
14 3     3   441 use Carp;
  3         6  
  3         274  
15 3     3   20 use Exporter 'import';
  3         6  
  3         132  
16 3     3   1940 use POSIX qw/floor/;
  3         26134  
  3         21  
17              
18             our @EXPORT_OK = qw/max_unit adjust_sub/;
19              
20             field $value :param;
21             field $radix :param = 10;
22              
23             method value { $value }
24             method radix { $radix }
25              
26             my $digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
27             my @digits = split('', $digits);
28             my %digit_value;
29             my $val = 0;
30             for (@digits) {
31             $digit_value{$_} = $val++;
32             }
33              
34             ADJUST {
35             if ($radix < 2 or $radix > 36) {
36             croak("Radix $radix should be between 2 and 36");
37             }
38             my $valid_digits = substr($digits, 0, $radix);
39             unless ($value =~ /^[$valid_digits]+$/) {
40             croak("Invalid digit in value '$value' for radix $radix");
41             }
42             $value =~ s/^0*//g;
43             if ($value eq '') {
44             $value = '0';
45             }
46             }
47              
48             method chars {
49             return length($value);
50             }
51              
52             method is_odd {
53             my $even_digits = '02468ACEGIKMOQSUWY';
54             if ($radix % 2 == 0) {
55             my $last = substr($value, -1, 1);
56             my $pos = index($even_digits, $last);
57             return $pos == -1;
58             }
59             my $val = $value;
60             $val =~ tr/02468ACEGIKMOQSUWY//d;
61             return 1 == length($val) % 2;
62             }
63              
64             method unit($len = 1) {
65             if ($len > $self->chars) {
66             $len = $self->chars;
67             }
68             return Arithmetic::PaperAndPencil::Number->new(
69             radix => $radix
70             , value => substr($value, $self->chars - $len)
71             );
72             }
73              
74             method carry($len = 1) {
75             if ($len >= $self->chars) {
76             return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => '0');
77             }
78             else {
79             return Arithmetic::PaperAndPencil::Number->new(
80             radix => $radix
81             , value => substr($value, 0, $self->chars - $len)
82             );
83             }
84             }
85              
86 0     0 1 0 sub max_unit($radix) {
  0         0  
  0         0  
87 0 0 0     0 if ($radix < 2 or $radix > 36) {
88 0         0 croak("Radix $radix should be between 2 and 36");
89             }
90 0         0 return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $digits[$radix - 1]);
91             }
92              
93             method _native_int {
94             if ($self->chars > 2) {
95             croak("Conversion to native allowed only for 1-digit numbers or 2-digit numbers");
96             }
97             my $tens = $digit_value{$self->carry->value};
98             my $units = $digit_value{$self->unit ->value};
99             return $tens * $self->radix + $units;
100             }
101              
102 23     23 1 41 sub add($x, $y, $invert) {
  23         46  
  23         45  
  23         37  
  23         36  
103 23         77 my $radix = $x->radix;
104 23 50       55 if ($radix != $y->radix) {
105 0         0 croak("Addition not allowed with different bases: $radix @{[$y->radix]}");
  0         0  
106             }
107 23 50 66     61 if ($x->chars != 1 and $y->chars != 1) {
108 0         0 croak("Addition allowed only if at least one number has a single digit");
109             }
110              
111 23         46 my @long_op;
112             my $short_op;
113 23 100       54 if ($x->chars == 1) {
114 12         35 $short_op = $x->value;
115 12         29 @long_op = reverse(split '', $y->value);
116             }
117             else {
118 11         32 $short_op = $y->value;
119 11         26 @long_op = reverse(split '', $x->value);
120             }
121 23         56 my $digit_nine = $digits[$radix - 1]; # '9' for radix 10, 'F' for radix 16, and so on
122 23         58 my $a = $digit_value{$short_op};
123 23         48 my $b = $digit_value{$long_op[0]};
124              
125 23 100       63 if ($a + $b < $radix) {
126 13         29 $long_op[0] = $digits[$a + $b];
127 13         88 return Arithmetic::PaperAndPencil::Number->new(
128             radix => $radix
129             , value => join('', reverse(@long_op)));
130             }
131              
132 10         25 push @long_op, '0';
133 10         22 $long_op[0] = $digits[$a + $b - $radix];
134 10         101 for my $i (1 .. 0 + @long_op) {
135 11 100       30 if ($long_op[$i] ne $digit_nine) {
136 10         31 $long_op[$i] = $digits[1 + $digit_value{$long_op[$i]}];
137 10         22 last;
138             }
139 1         3 $long_op[$i] = '0';
140             }
141              
142 10         73 return Arithmetic::PaperAndPencil::Number->new(
143             radix => $radix
144             , value => join('', reverse(@long_op)));
145             }
146              
147 0     0 1   sub minus($x, $y, $invert) {
  0            
  0            
  0            
  0            
148 0 0         if ($invert) {
149 0           ($x, $y) = ($y, $x);
150             }
151 0           my $radix = $x->radix;
152 0 0         if ($radix != $y->radix) {
153 0           croak("Subtraction not allowed with different bases: $radix @{[$y->radix]}");
  0            
154             }
155 0 0 0       if ($x->chars != 1 or $y->chars != 1) {
156 0           croak("Subtraction allowed only for single-digit numbers");
157             }
158 0 0         if ($x->value lt $y->value) {
159 0           croak("The first number must be greater or equal to the second number");
160             }
161 0           my $x10 = $x->_native_int;
162 0           my $y10 = $y->_native_int;
163 0           my $z10 = $x10 - $y10;
164 0           return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $digits[$z10]);
165             }
166              
167 0     0 1   sub times($x, $y, $invert) {
  0            
  0            
  0            
  0            
168 0 0         if ($invert) {
169 0           ($x, $y) = ($y, $x);
170             }
171 0           my $radix = $x->radix;
172 0 0         if ($radix != $y->radix) {
173 0           croak("Multiplication not allowed with different bases: $radix @{[$y->radix]}");
  0            
174             }
175 0 0 0       if ($x->chars != 1 or $y->chars != 1) {
176 0           croak("Multiplication allowed only for single-digit numbers");
177             }
178 0           my $x10 = $x->_native_int;
179 0           my $y10 = $y->_native_int;
180 0           my $z10 = $x10 * $y10;
181 0           my $zu = $z10 % $radix;
182 0           my $zt = floor($z10 / $radix);
183 0           return Arithmetic::PaperAndPencil::Number->new(value => $digits[$zt] . $digits[$zu]
184             , radix => $radix);
185             }
186              
187 0     0 1   sub divide($x, $y, $invert) {
  0            
  0            
  0            
  0            
188 0 0         if ($invert) {
189 0           ($x, $y) = ($y, $x);
190             }
191 0           my $radix = $x->radix;
192 0 0         if ($radix != $y->radix) {
193 0           croak("Division not allowed with different bases: $radix @{[$y->radix]}");
  0            
194             }
195 0 0         if ($x->chars > 2) {
196 0           croak("The dividend must be a 1- or 2-digit number");
197             }
198 0 0         if ($y->chars > 1) {
199 0           croak("The divisor must be a single-digit number");
200             }
201 0 0         if ($y->value eq '0') {
202 0           croak("Division by 0 not allowed");
203             }
204 0           my $xx = $x->_native_int;
205 0           my $yy = $y->_native_int;
206 0           my $qq = floor($xx / $yy);
207 0 0         if ($qq >= $radix) {
208 0           my $q0 = $qq % $radix;
209 0           my $q1 = floor($qq / $radix);
210 0           return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $digits[$q1] . $digits[$q0]);
211             }
212             else {
213 0           return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $digits[$qq]);
214             }
215             }
216              
217 0     0 1   sub num_cmp($x, $y, $invert) {
  0            
  0            
  0            
  0            
218 0           my $radix = $x->radix;
219 0 0         if ($radix != $y->radix) {
220 0           croak("Comparison not allowed with different bases: $radix @{[$y->radix]}");
  0            
221             }
222 0   0       return $x->chars <=> $y->chars
223             ||
224             $x->value cmp $y->value;
225             }
226              
227 0     0 1   sub alpha_cmp($x, $y, $invert) {
  0            
  0            
  0            
  0            
228 0           my $radix = $x->radix;
229 0 0         if ($radix != $y->radix) {
230 0           croak("Comparison not allowed with different bases: $radix @{[$y->radix]}");
  0            
231             }
232 0           return $x->value cmp $y->value;
233             }
234              
235 3         24 use overload '+' => \&add
236             , '-' => \&minus
237             , '*' => \×
238             , '/' => \÷
239             , '<=>' => \&num_cmp
240             , 'cmp' => \&alpha_cmp
241 3     3   18168 ;
  3         5501  
242              
243             method complement($len) {
244             my $s = $value;
245             if (length($s) > $len) {
246             croak("Parameter length $len should be greater than or equal to number's length @{[length($s)]}");
247             }
248             my $before = substr($digits, 0, $radix);
249             my $after = reverse($before);
250             $_ = '0' x ($len - length($s)) . $s;
251             eval "tr/$before/$after/";
252             return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $_)
253             + Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => '1');
254             }
255              
256 0     0 1   sub adjust_sub ($high, $low) {
  0            
  0            
  0            
257 0           my $radix = $high->radix;
258 0 0         if ($low->radix != $radix) {
259 0           die "Subtraction not allowed with different bases: $radix @{[$low->radix]}";
  0            
260             }
261 0 0         if ($high->chars != 1) {
262 0           die "The high number must be a single-digit number";
263             }
264 0 0         if ($low->chars > 2) {
265 0           die "The low number must be a single-digit number or a 2-digit number";
266             }
267 0           my $adjusted_carry = Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $low->carry->value);
268 0           my $low_unit = $low->unit;
269 0           my $native_high = $high->_native_int;
270 0           my $native_low_unit = $low_unit->_native_int;
271 0 0         if ($high < $low_unit) {
272 0           $adjusted_carry += Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => "1");
273 0           $native_high += $radix;
274             }
275 0           my $adjusted_high = Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $adjusted_carry->value . $high->value);
276 0           my $result = Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $digits[$native_high - $native_low_unit]);
277 0           return ($adjusted_high, $result);
278             }
279              
280             method square_root {
281             if ($self->chars > 2) {
282             croak("The number must be a single-digit number or a 2-digit number");
283             }
284             my $root = floor(sqrt($self->_native_int));
285             return Arithmetic::PaperAndPencil::Number->new(radix => $radix, value => $digits[$root])
286             }
287             '355/113'; # End of Arithmetic::PaperAndPencil
288              
289             =encoding utf8
290              
291             =head1 NAME
292              
293             Arithmetic::PaperAndPencil::Number - integer, with elementary operations
294              
295             =head1 VERSION
296              
297             Version 0.01
298              
299             =head1 SYNOPSIS
300              
301             use Arithmetic::PaperAndPencil::Number;
302              
303             my $x = Arithmetic::PaperAndPencil::Number->new(radix => 10, value => '9');
304             my $y = Arithmetic::PaperAndPencil::Number->new(radix => 10, value => '6');
305             my $sum = $x + $y;
306             my $pdt = $x * $y;
307              
308             =head1 DESCRIPTION
309              
310             This class should not be used directly. It is meant to be a utility
311             module for C.
312              
313             C is a class storing integer
314             numbers and simulating elementary operations that a pupil learns at
315             school. The simulated operations are the operations an average human
316             being can do in his head, without outside help such as a paper and a
317             pencil.
318              
319             So, operations are implemented with only very simple numbers. For
320             example, when adding two numbers, at least one of them must have only
321             one digit. And when multiplying numbers, both numbers must have a
322             single digit. Attempting to multiply, or add, two numbers with
323             multiple digits triggers an exception.
324              
325             An important difference with the average human being: most humans can
326             compute in radix 10 only. Some gifted humans may add or subtract in
327             radix 8 and in radix 16, but they are very few. This module can
328             compute in any radix from 2 to 36.
329              
330             Another difference with normal human beings: a human can add a
331             single-digit number with a multi-digit number, provided the
332             multi-digit number is not too long. E.g. a human can compute C<15678 +
333             6> and get C<15684>, but when asked to compute C<18456957562365416378
334             + 6>, this human will fail to remember all necessary digits. The
335             module has no such limitations. Or rather, the module's limitations
336             are those of the Perl interpreter and of the host machine.
337              
338             =head1 METHODS
339              
340             =head2 new
341              
342             An instance of C is built by
343             calling method C with two parameters, C and C. If
344             omitted, C defaults to 10.
345              
346             =head2 radix
347              
348             The numerical base, or radix, in which the number is defined.
349              
350             =head2 value
351              
352             The digits of the number.
353              
354             =head2 chars
355              
356             The number of chars in the C attribute.
357              
358             =head2 unit
359              
360             Builds a number (instance of C),
361             using the last digit of the input number. For example, when applied to
362             number C<1234>, the C method gives C<4>.
363              
364             Extended usage: given a C<$len> parameter (positional, optional,
365             default 1), builds a number using the last C<$len> digits of the input
366             number. For example, when applied to number C<1234> with parameter
367             C<2>, the C method gives C<34>. When applied to number C<1234>
368             with parameter C<3>, the C method gives C<234>.
369              
370             =head2 carry
371              
372             Builds a number (instance of C),
373             using the input number without its last digit. For example, when
374             applied to number C<1234>, the C method gives C<123>.
375              
376             Extended usage: given a C<$len> parameter (positional, optional,
377             default 1), builds a number, using the input number without its last
378             C<$len> digits. For example, when applied to number C<1234> with
379             parameter C<2>, the C method gives C<12> by removing 2 digits,
380             C<34>. When applied to number C<1234> with parameter C<3>, the
381             C method gives C<1>.
382              
383             =head2 complement
384              
385             Returns the 10-complement, 2-complement, 16-complement, whatever, of
386             the number. Which complement is returned is determined by the number's
387             radix. The method requires another parameter, to choose the number of
388             digits in the computed complement. This length parameter is a
389             positional parameter.
390              
391             Example
392              
393             radix = 16 |
394             number = BABE | → complement = FFFF5652
395             length = 8 |
396              
397             =head2 square_root
398              
399             Returns the square root of the objet, rounded down to an integer.
400              
401             The object must be a single-digit or a double-digit instance of
402             C.
403              
404             =head2 is_odd
405              
406             Returns an integer used as a boolean, C<1> if the number is odd, C<0>
407             if the number is even.
408              
409             =head1 FUNCTIONS
410              
411             =head2 C
412              
413             The input parameter is the radix (positional). The function returns
414             the highest single-digit number for this radix. For example,
415             C returns C<9> and C returns C.
416              
417             The returned value is an instance of C).
418              
419             =head2 Addition C
420              
421             Adding two numbers with the same radix. At least one argument must be
422             a single-digit number. This function is used to overload C<+>.
423              
424             =head2 Subtraction C
425              
426             Subtracting two numbers with the same radix. Both arguments must be
427             single-digit numbers. This function is used to overload C<->.
428              
429             =head2 Subtraction C
430              
431             Actually, this is not the plain subtraction. This function receives a
432             1-digit high number and a 1- or 2-digit low number. It sends back an
433             adjusted high-number and a subtraction result. The adjusted
434             high-number is the first number greater than the low number and in
435             which the unit is the parameter high number.
436              
437             For example (radix 10):
438              
439             high = 1, low = 54 → adjusted-high = 61, result = 7
440             high = 8, low = 54 → adjusted-high = 58, result = 4
441              
442             The parameters are positional.
443              
444             =head2 Multiplication C
445              
446             Multiplying two numbers with the same radix. Both arguments must be
447             single-digit numbers. This function is used to overload C<*>.
448              
449             =head2 Division C
450              
451             Dividing two numbers with the same radix. The first argument must be a
452             single-digit or double-digit number and the second argument must be a
453             single-digit number (and greater than zero, of course).
454              
455             =head2 Numeric Comparison C
456              
457             This function interprets the arguments as numbers and returns the
458             3-way comparison of these numbers. This function overloads C<< <=> >>,
459             which means that all other numeric comparisons (C<==>, C<< < >>,
460             C<< <= >>, etc) are overloaded too.
461              
462             =head2 Alphabetic Comparison C
463              
464             This function interprets the arguments as strings and returns the
465             3-way comparison of these strings. This function overloads C,
466             which means that all other numeric comparisons (C, C, C,
467             etc) are overloaded too.
468              
469             =head1 EXPORT
470              
471             Functions C and C are exported.
472              
473             =head1 AUTHOR
474              
475             Jean Forget, C<< >>
476              
477             =head1 BUGS
478              
479             Please report any bugs or feature requests to C, or through
480             the web interface at L. I will be notified, and then you'll
481             automatically be notified of progress on your bug as I make changes.
482              
483             =head1 SUPPORT
484              
485             You can find documentation for this module with the perldoc command.
486              
487             perldoc Arithmetic::PaperAndPencil
488              
489             You can also look for information at:
490              
491             =over 4
492              
493             =item * RT: CPAN's request tracker (report bugs here)
494              
495             L
496              
497             =item * CPAN Ratings
498              
499             L
500              
501             =item * Search CPAN
502              
503             L
504              
505             =back
506              
507              
508             =head1 ACKNOWLEDGEMENTS
509              
510              
511             =head1 LICENSE AND COPYRIGHT
512              
513             This software is Copyright (c) 2024 by jforget.
514              
515             This is free software, licensed under:
516              
517             The Artistic License 2.0 (GPL Compatible)
518              
519              
520             =cut
521