File Coverage

blib/lib/Astro/UTDF.pm
Criterion Covered Total %
statement 277 279 99.2
branch 102 116 87.9
condition 15 21 71.4
subroutine 61 61 100.0
pod 37 37 100.0
total 492 514 95.7


line stmt bran cond sub pod time code
1             package Astro::UTDF;
2              
3 2     2   2155 use 5.006002;
  2         8  
4              
5 2     2   13 use strict;
  2         4  
  2         59  
6 2     2   11 use warnings;
  2         4  
  2         81  
7              
8 2     2   11 use Carp;
  2         4  
  2         134  
9 2     2   1189 use IO::File ();
  2         19480  
  2         63  
10 2     2   1488 use POSIX qw{ floor };
  2         14990  
  2         14  
11 2     2   3157 use Scalar::Util qw{ blessed openhandle };
  2         4  
  2         145  
12 2     2   1328 use Time::Local;
  2         5574  
  2         145  
13              
14 2     2   18 use constant FULL_CIRCLE => 4294967296; # 2 ** 32;
  2         4  
  2         136  
15 2     2   14 use constant PI => atan2( 0, -1 );
  2         4  
  2         99  
16 2     2   12 use constant TWO_PI => 2 * PI;
  2         3  
  2         92  
17 2     2   12 use constant SPEED_OF_LIGHT => 299792.458; # Km/sec, per U.S. NIST
  2         5  
  2         103  
18              
19 2     2   12 use constant ARRAY_REF => ref [];
  2         4  
  2         129  
20 2     2   12 use constant CODE_REF => ref sub {};
  2         5  
  2         4771  
21              
22             our $VERSION = '0.011_01';
23              
24             sub new {
25 90     90 1 17424 my $class = shift;
26 90   66     318 unshift @_, ref $class || $class;
27 90         242 goto &clone;
28             }
29              
30             sub azimuth {
31 4     4 1 12 splice @_, 1, 0, azimuth => TWO_PI;
32 4         12 goto &_bash_angle;
33             }
34              
35             sub clone {
36 92     92 1 636 my ( $self, @args ) = @_;
37 92         138 my ( $class, $clone );
38 92 100       179 if ( $class = ref $self ) {
39 2         7 $clone = {};
40 2         5 while ( my ( $name, $value ) = each %{ $self } ) {
  55         119  
41 53         95 $clone->{$name} = $value;
42             }
43             } else {
44 90         162 $clone = { _static() };
45 90         325 $class = $self;
46             }
47 92         185 bless $clone, $class;
48 92         209 while ( @args ) {
49 99         210 my ( $name, $value ) = splice @args, 0, 2;
50 99 100       587 my $code = $clone->can( $name )
51             or croak "Method $name() not found";
52 98         203 $code->( $clone, $value );
53             }
54 91         381 return $clone;
55             }
56              
57             sub data_interval {
58 6     6 1 13 my ( $self, @args ) = @_;
59 6 100       15 if ( @args ) {
60 3         5 my $interval = $args[0];
61 3 100       15 if ( $interval <= 0 ) {
    100          
62 1         111 croak "Negative data interval invalid";
63             } elsif ( $interval < 1 ) {
64 1         2 $interval = 1 / $interval;
65 1         2 $interval = ( ~ $interval & 0x07ff ) + 1;
66             }
67 2         5 $self->{tracker_type_and_data_rate} &= ~ 0x7ff;
68 2         5 $self->{tracker_type_and_data_rate} |= $interval & 0x07ff;
69 2         20 return $self;
70             } else {
71 3         12 my $interval = $self->{tracker_type_and_data_rate} & 0x07ff;
72 3 100       23 $interval & 0x0400 or return $interval;
73 1         5 $interval = ( ~ $interval & 0x07ff ) + 1;
74 1         3 return 1 / $interval;
75             }
76             }
77              
78             {
79              
80             my @antenna_diameter = (
81             'less than 1 meter',
82             '3.9 meters',
83             '4.3 meters',
84             '9 meters',
85             '12 meters',
86             '26 meters',
87             'TDRSS ground antenna',
88             '6 meters',
89             '7.3 meters',
90             '8 meters',
91             'unused',
92             'unused',
93             'unused',
94             'unused',
95             'unused',
96             'unused',
97             );
98             my @antenna_geometry = (
99             'az-el',
100             'X-Y (+X south)',
101             'X-Y (+X east)',
102             'RA-DEC',
103             'HR-DEC',
104             'unused',
105             'unused',
106             'unused',
107             'unused',
108             'unused',
109             'unused',
110             'unused',
111             'unused',
112             'unused',
113             'unused',
114             'unused',
115             );
116              
117             my $hexify = sub {
118             my ( $self, $method ) = @_;
119             return unpack 'H*', $self->$method();
120             };
121              
122             my %decoder = (
123             data_validity => '0x%02x',
124             frequency_band => [
125             'unspecified',
126             'VHF',
127             'UHF',
128             'S-band',
129             'C-band',
130             'X-band',
131             'Ku-band',
132             'visible',
133             'S-band uplink/Ku-band downlink',
134             'unknown code 9',
135             'unknown code 10',
136             'unknown code 11',
137             'unknown code 12',
138             'unknown code 13',
139             'unknown code 14',
140             'unknown code 15',
141             ],
142             frequency_band_and_transmission_type => '0x%02x',
143             front => $hexify,
144             measurement_time => sub {
145             # Note that perldoc -f localtime says that the string
146             # returned in scalar context is _not_ locale-dependant.
147             return scalar gmtime $_[0]->measurement_time();
148             },
149             mode => '0x%04x',
150             raw_record => $hexify,
151             rear => $hexify,
152             receive_antenna_diameter_code => \@antenna_diameter,
153             receive_antenna_geometry_code => \@antenna_geometry,
154             tdrss_only => $hexify,
155             tracking_mode => [
156             'autotrack',
157             'program track',
158             'manual',
159             'slaved',
160             ],
161             transmission_type => [
162             'test',
163             'unused',
164             'simulated',
165             'resubmit',
166             'RT (real time)',
167             'PB (playback)',
168             'unused',
169             'unused',
170             'unused',
171             'unused',
172             'unused',
173             'unused',
174             'unused',
175             'unused',
176             'unused',
177             'unused',
178             ],
179             transmit_antenna_diameter_code => \@antenna_diameter,
180             transmit_antenna_geometry_code => \@antenna_geometry,
181             );
182              
183             sub decode {
184 12     12 1 24 my ( $self, $method, @args ) = @_;
185 12 100       41 my $dcdr = $decoder{$method}
186             or return $self->$method( @args );
187 11 100       30 my $type = ref $dcdr
188             or return sprintf $dcdr, $self->$method( @args );
189 9 100       34 ARRAY_REF eq $type
190             and return $dcdr->[ $self->$method( @args ) ];
191 2 50       8 CODE_REF eq $type
192             and return $dcdr->( $self, $method, @args );
193 0         0 confess "Programming error -- decoder for $method is $type";
194             }
195             }
196              
197             sub doppler_count {
198 13     13 1 43 splice @_, 1, 0, doppler_count => 1, 'is_doppler_valid';
199 13         31 goto &_bash_6_bytes;
200             }
201              
202             sub doppler_shift {
203 7     7 1 19 my ( $self, @args ) = @_;
204 7 100       86 @args and croak "doppler_shift() may not be used as a mutator";
205             # Note that this can never be a mutator, because it uses data from
206             # more than one record.
207 6 100       14 defined( my $prior = $self->prior_record() )
208             # If I simply returned as PBP would have me do, this method
209             # would behave differently in list context depending on whether
210             # prior_record() returned a defined value.
211             or return undef; ## no critic (ProhibitExplicitReturnUndef)
212 4 50 33     10 $self->enforce_validity()
      66        
213             and not ( $self->is_doppler_valid() &&
214             $prior->is_doppler_valid() )
215             and return undef; ## no critic (ProhibitExplicitReturnUndef)
216 4         13 my $count = $self->doppler_count() - $prior->doppler_count();
217 4         18 my $deltat = $self->measurement_time() - $prior->measurement_time();
218 4 100       33 if ( $deltat < 0 ) {
219 1         3 $deltat = - $deltat;
220 1         2 $count = - $count;
221             }
222 4 50       11 $count < 0 and $count += 2 << 48;
223 4         14 return ( $count / $deltat - 240_000_000 ) / $self->factor_M();
224             }
225              
226             sub elevation {
227 4     4 1 30 splice @_, 1, 0, elevation => PI;
228 4         11 goto &_bash_angle;
229             }
230              
231             sub enforce_validity {
232 34     34 1 459 my ( $self, @args ) = @_;
233 34 100       59 if ( @args ) {
234 5         17 $self->{enforce_validity} = shift @args;
235 5         16 return $self;
236             } else {
237 29         136 return $self->{enforce_validity};
238             }
239             }
240              
241             # Return the factor K, which is documented as 240/221 for S-band or 1
242             # for VHF. Since we know we can't count on the frequency_band, we
243             # compute this ourselves, making the break at the bottom of the S band.
244              
245             sub factor_K {
246 6     6 1 14 my ( $self, @args ) = @_;
247 6 100       14 if ( @args ) {
248 2         4 $self->{factor_K} = $args[0];
249 2         5 return $self;
250             } else {
251             return ( defined $self->{factor_K} ? $self->{factor_K} :
252             ( $self->{factor_K} =
253 4 50       16 $self->transmit_frequency() >= 2_000_000_000 ?
    100          
254             240 / 221 : 1 ) );
255             }
256             }
257              
258             # Return the factor M, which is documented as 1000 for S-band or 100 for
259             # K-band. Since we know we can't count on the frequency_band, we
260             # compute this ourselves, making the break at the bottom of the Ku band.
261              
262             sub factor_M {
263 9     9 1 16 my ( $self, @args ) = @_;
264 9 100       18 if ( @args ) {
265 2         5 $self->{factor_M} = $args[0];
266 2         5 return $self;
267             } else {
268             return ( defined $self->{factor_M} ? $self->{factor_M} :
269             ( $self->{factor_M} =
270 7 50       25 $self->transmit_frequency() >= 12_000_000_000 ?
    100          
271             100 : 1000 ) );
272             }
273             }
274              
275             sub frequency_band {
276 4     4 1 11 splice @_, 1, 0, frequency_band_and_transmission_type => 1;
277 4         10 goto &_bash_nybble;
278             }
279              
280             sub hex_record {
281 1     1 1 3 my ( $self, @args ) = @_;
282 1 50       4 if ( @args ) {
283 0         0 return $self->raw_record( pack 'H*', $args[0] );
284             } else {
285 1         3 return unpack 'H*', $self->raw_record();
286             }
287             }
288              
289             sub is_angle_corrected_for_misalignment {
290 3     3 1 19 splice @_, 1, 0, data_validity => 3;
291 3         8 goto &_bash_bit;
292             }
293              
294             sub is_angle_corrected_for_refraction {
295 3     3 1 9 splice @_, 1, 0, data_validity => 4;
296 3         8 goto &_bash_bit;
297             }
298              
299             sub is_angle_valid {
300 5     5 1 16 splice @_, 1, 0, data_validity => 2;
301 5         12 goto &_bash_bit;
302             }
303              
304             sub is_destruct_doppler {
305 3     3 1 9 splice @_, 1, 0, data_validity => 6;
306 3         8 goto &_bash_bit;
307             }
308              
309             sub is_doppler_valid {
310 13     13 1 39 splice @_, 1, 0, data_validity => 1;
311 13         32 goto &_bash_bit;
312             }
313              
314             sub is_range_valid {
315 7     7 1 15 splice @_, 1, 0, data_validity => 0;
316 7         18 goto &_bash_bit;
317             }
318              
319             sub is_range_corrected_for_refraction {
320 3     3 1 16 splice @_, 1, 0, data_validity => 5;
321 3         8 goto &_bash_bit;
322             }
323              
324             sub is_side_lobe {
325 3     3 1 9 splice @_, 1, 0, data_validity => 7;
326 3         7 goto &_bash_bit;
327             }
328              
329             sub is_last_frame {
330 3     3 1 11 splice @_, 1, 0, tracker_type_and_data_rate => 11;
331 3         8 goto &_bash_bit;
332             }
333              
334             sub measurement_time {
335 12     12 1 22 my ( $self, @args ) = @_;
336 12 100       25 if ( @args ) {
337 1         4 my $time = floor( $args[0] );
338 1         6 my $microseconds = floor( ( $args[0] - $time ) * 1_000_000 + 0.5);
339 1         6 my @cald = gmtime $time;
340 1         4 my $year = $cald[5] % 100;
341 1         5 my $seconds = $time - timegm( 0, 0, 0, 1, 0, $cald[5] );
342 1         57 return $self->year( $year )->seconds_of_year( $seconds
343             )->microseconds_of_year( $microseconds );
344             } else {
345 11         31 my $yr = $self->year();
346 11 50       31 $yr < 70 and $yr += 100;
347 11         33 return timegm( 0, 0, 0, 1, 0, $yr ) + $self->seconds_of_year() +
348             $self->microseconds_of_year() / 1_000_000;
349             }
350             }
351              
352             sub prior_record {
353 13     13 1 26 my ( $self, @args ) = @_;
354 13 100       25 if ( @args ) {
355 7         10 my $prior = shift @args;
356 7 100 100     27 defined $prior and not __PACKAGE__->_instance( $prior )
357             and croak 'Prior record must be undef or an ', __PACKAGE__,
358             ' object';
359 6         12 $self->{prior_record} = $prior;
360 6         15 return $self;
361             } else {
362 6         26 return $self->{prior_record};
363             }
364             }
365              
366             sub range {
367 3     3 1 10 my ( $self, @args ) = @_;
368 3 100       9 if ( @args ) {
369 1         75 croak "range() may not be used as a mutator";
370             } else {
371 2 100       15 defined( my $range_delay = $self->range_delay() )
372             or return undef; ## no critic (ProhibitExplicitReturnUndef)
373 1         4 return ( ( $range_delay - $self->transponder_latency() ) *
374             SPEED_OF_LIGHT / 2_000_000_000 );
375             }
376             }
377              
378             sub range_delay {
379 6     6 1 19 splice @_, 1, 0, range_delay => 256, 'is_range_valid';
380 6         29 goto &_bash_6_bytes;
381             }
382              
383             sub range_rate {
384 3     3 1 7 my ( $self, @args ) = @_;
385 3 100       81 @args and croak "range_rate() may not be used as a mutator";
386             # Note that this can never be a mutator because it uses
387             # doppler_shift() (q.v.)
388 2 100       6 if ( defined ( my $shift = $self->doppler_shift() ) ) {
389             return (
390 1         4 - SPEED_OF_LIGHT / ( 2 * $self->transmit_frequency() *
391             $self->factor_K() ) * $shift
392             );
393             } else {
394             # If I simply returned as PBP would have me do, this method
395             # would behave differently in list context depending on whether
396             # doppler_shift() returned a defined value.
397 1         2 return undef; ## no critic (ProhibitExplicitReturnUndef)
398             }
399             }
400              
401             {
402              
403             my $utdf_template = 'a3A2CnnNNNNNnNnnNCCCCnCCna18a3';
404             my @utdf_fields = qw{
405             front router year sic vid seconds_of_year
406             microseconds_of_year azimuth elevation
407             range_delay_hi range_delay_lo
408             doppler_count_hi doppler_count_lo
409             agc
410             transmit_frequency
411             transmit_antenna_type
412             transmit_antenna_padid
413             receive_antenna_type
414             receive_antenna_padid
415             mode
416             data_validity
417             frequency_band_and_transmission_type
418             tracker_type_and_data_rate
419             tdrss_only
420             rear
421             };
422              
423             sub raw_record {
424 89     89 1 156 my ( $self, @args ) = @_;
425 89 100       162 if ( @args ) {
426 46         73 my $raw_record = shift @args;
427 46 100       220 length $raw_record == 75
428             or croak "Invalid raw record: length not 75 bytes";
429 2     2   1621 use bytes;
  2         34  
  2         12  
430 45         444 @$self{ @utdf_fields } = unpack $utdf_template, $raw_record;
431 45         164 return $self;
432             } else {
433 2     2   150 use bytes;
  2         16  
  2         7  
434 43         441 return pack $utdf_template, @$self{ @utdf_fields };
435             }
436             }
437              
438             my $static = {};
439             @$static{ @utdf_fields } = ( 0 ) x scalar @utdf_fields;
440             $static->{front} = pack 'H*', '0d0a01';
441             $static->{router} = ' ';
442             $static->{tdrss_only} = pack( 'H*', '00' ) x 18;
443             $static->{rear} = pack 'H*', '040f0f';
444             $static->{transponder_latency} = 0;
445             bless $static, __PACKAGE__;
446              
447             sub _static {
448 90 50   90   165 return wantarray ? %{ $static } : $static;
  90         1060  
449             }
450             }
451              
452             sub receive_antenna_diameter_code {
453 4     4 1 12 splice @_, 1, 0, receive_antenna_type => 1;
454 4         10 goto &_bash_nybble;
455             }
456              
457             sub receive_antenna_geometry_code {
458 4     4 1 12 splice @_, 1, 0, receive_antenna_type => 0;
459 4         10 goto &_bash_nybble;
460             }
461              
462             {
463              
464             my %my_arg = map { $_ => 1 } qw{ file };
465              
466             sub slurp {
467 5     5 1 1094 my ( undef, @in_args ) = @_; # Invocant unused
468              
469 5 100       18 @in_args % 2 and unshift @in_args, 'file';
470 5         8 my ( %arg, @attrib );
471 5         13 while ( @in_args ) {
472 6         14 my ( $name, $value ) = splice @in_args, 0, 2;
473 6 100       16 if ( $my_arg{$name} ) {
474 4         13 $arg{$name} = $value;
475             } else {
476 2         5 push @attrib, $name, $value;
477             }
478             }
479 5 100       95 $arg{file} or croak "File not specified";
480              
481 4         7 my $fh = $arg{file};
482 4         6 my $fn;
483 4 50       15 if ( ! openhandle( $fh ) ) {
484 4         6 $fn = $fh;
485 4 100       224 -e $fn or croak "$fn not found";
486 3 100       103 -f _ or croak "$fn not a normal file";
487 2 50       15 $fh = IO::File->new( $fn, '<' )
488             or croak "Unable to open $fn: $!";
489             }
490 2         241 binmode $fh;
491              
492 2         3 my @rslt;
493 2         3 my ( $buffer, $count );
494 2         58 while ( $count = read $fh, $buffer, 75 ) {
495 4 100       24 push @rslt, __PACKAGE__->new(
496             raw_record => $buffer,
497             prior_record => ( @rslt ? $rslt[-1] : undef ),
498             @attrib,
499             );
500             }
501 2         23 close $fh;
502 2         18 return @rslt;
503             }
504              
505             }
506              
507             sub tracker_type {
508 3     3 1 9 splice @_, 1, 0, tracker_type_and_data_rate => 3;
509 3         9 goto &_bash_nybble;
510             }
511              
512             sub tracking_mode {
513 4     4 1 7 my ( $self, @args ) = @_;
514             # This would be a delegation to _bash_quarter if there were more
515             # than one two-bit field.
516 4         9 my $attr = 'mode';
517 4         5 my $shift = 2;
518 4         8 my $mask = 0x03 << $shift;
519 4 100       10 if ( @args ) {
520 1         3 $self->{$attr} &= ~ $mask;
521 1         3 $self->{$attr} |= ( $args[0] & 0x03 ) << $shift;
522 1         4 return $self;
523             } else {
524 3         18 return ( $self->{mode} & $mask ) >> $shift;
525             }
526             }
527              
528             sub transmission_type {
529 4     4 1 15 splice @_, 1, 0, frequency_band_and_transmission_type => 0;
530 4         11 goto &_bash_nybble;
531             }
532              
533             sub transmit_antenna_diameter_code {
534 4     4 1 15 splice @_, 1, 0, transmit_antenna_type => 1;
535 4         10 goto &_bash_nybble;
536             }
537              
538             sub transmit_antenna_geometry_code {
539 4     4 1 12 splice @_, 1, 0, transmit_antenna_type => 0;
540 4         10 goto &_bash_nybble;
541             }
542              
543             sub transmit_frequency {
544 11     11 1 19 my ( $self, @args ) = @_;
545 11 100       21 if ( @args ) {
546 2         9 $self->{transmit_frequency} = floor( ( $args[0] + 5 ) / 10 );
547 2         5 return $self;
548             } else {
549 9         35 return $self->{transmit_frequency} * 10;
550             }
551             }
552              
553             # Generate all the simple accessors. These just return the value of
554             # the correspondingly-named attribute, which is assumed to exist.
555              
556             foreach my $attribute ( qw{
557             front router year sic vid seconds_of_year
558             microseconds_of_year
559             agc
560             transmit_antenna_type
561             transmit_antenna_padid
562             receive_antenna_type
563             receive_antenna_padid
564             mode
565             data_validity
566             frequency_band_and_transmission_type
567             tracker_type_and_data_rate
568             tdrss_only
569             rear
570              
571             transponder_latency
572             } ) {
573 2     2   1687 no strict qw{ refs };
  2         5  
  2         1485  
574             *$attribute = sub {
575 91     91   426 my ( $self, @args ) = @_;
576 91 100       153 if ( @args ) {
577 19         39 $self->{$attribute} = shift @args;
578 19         56 return $self;
579             } else {
580 72         248 return $self->{$attribute};
581             }
582             };
583             }
584              
585             # Generic accessor/mutator for 6 byte values. The specific
586             # accessor/mutator splices the constant part of the attribute name (the
587             # part before '_hi' or '_lo', the factor (to multiply by for the
588             # mutator, or divide by for the accessor), and the name of the valid-bit
589             # routine (or undef if none) into the argument list right after the
590             # object, and co-routines to this.
591             sub _bash_6_bytes {
592 19     19   45 my ( $self, $attr, $factor, $validator, @args ) = @_;
593 19 100       36 if ( @args ) {
594 3         7 my $value = $factor * shift @args;
595 3         20 my $value_hi = floor( $value / 65536 );
596 3         7 my $value_lo = $value - $value_hi * 65536;
597 3         9 $self->{ $attr . '_hi' } = $value_hi | 0; # Force integer
598 3         7 $self->{ $attr . '_lo' } = $value_lo | 0; # Force integer
599 3         10 return $self;
600             } else {
601 16 100 66     44 $validator
      100        
602             and $self->enforce_validity()
603             and not $self->$validator()
604             and return undef; ## no critic (ProhibitExplicitReturnUndef)
605             return ( $self->{ $attr . '_hi' } * 65536 +
606 14         68 $self->{ $attr . '_lo' } ) / $factor;
607             }
608             }
609              
610             # Generic accessor/mutator for angles. The specific accessor/mutator
611             # splices the attribute name and the upper limit in radians onto the
612             # argument list after the object, and co-routines to this (or calls it
613             # returning whatever it returns). Note that the upper limit is used only
614             # to normalize the angle for the accessor; all angles are stored as
615             # positive fractions of a circle.
616             # WE ASSUME ANYTHING THAT GOES THROUGH THIS CODE IS SUBJECT TO
617             # is_angle_valid().
618             sub _bash_angle {
619 8     8   20 my ( $self, $attr, $upper, @args ) = @_;
620 8 100       17 if ( @args ) {
621 2         8 my $angle = $args[0] / TWO_PI;
622 2         9 $angle -= floor( $angle );
623 2         9 $self->{$attr} = floor( $angle * FULL_CIRCLE + 0.5 );
624 2         8 return $self;
625             } else {
626 6 100 66     23 $self->enforce_validity()
627             and not $self->is_angle_valid()
628             and return undef; ## no critic (ProhibitExplicitReturnUndef)
629 4         21 my $angle = $self->{$attr} / FULL_CIRCLE * TWO_PI;
630 4 50       9 $angle >= $upper and $angle -= TWO_PI;
631 4         13 return $angle;
632             }
633             }
634              
635             # Generic accessor/mutator for single bits. The specific
636             # accessor/mutator splices the attribute name and the bit number into
637             # the argument list after the object, and co-routines to this (or calls
638             # it returning whatever it returns). NOTE: I would love to use vec()
639             # here, but that works on strings.
640             sub _bash_bit {
641 43     43   90 my ( $self, $attr, $bit, @args ) = @_;
642 43         68 my $mask = 0x01 << $bit;
643 43 100       81 if ( @args ) {
644 12 100       20 if ( $args[0] ) {
645 6         14 $self->{$attr} |= $mask;
646             } else {
647 6         26 $self->{$attr} &= ~ $mask;
648             }
649 12         44 return $self;
650             } else {
651 31 100       146 return $self->{$attr} & $mask ? 1 : 0;
652             }
653             }
654              
655             # Generic accessor/mutator for nybbles. The specific accessor/mutator
656             # splices the attribute name and the nybble number into the argument
657             # list after the object, and co-routines to this (or calls it returning
658             # whatever it returns). NOTE: I would love to use vec() here, but that
659             # works on strings.
660             sub _bash_nybble {
661 27     27   60 my ( $self, $attr, $bit, @args ) = @_;
662 27         45 my $shift = 4 * $bit;
663 27         46 my $mask = 0x0f << $shift;
664 27 100       54 if ( @args ) {
665 7         14 $self->{$attr} &= ~ $mask;
666 7         14 $self->{$attr} |= ( $args[0] & 0x0f ) << $shift;
667 7         24 return $self;
668             } else {
669 20         80 return ( $self->{$attr} & $mask ) >> $shift;
670             }
671             }
672              
673             # $class->_instance( $obj )
674             # returns the class name of the invocant if the argument is an instance
675             # of the invocant's class, or one of its subclasses. Otherwise simply
676             # returns. Can be called as a static method.
677             sub _instance {
678 4     4   8 my ( $class, $obj ) = @_;
679 4 50       11 ref $class
680             and $class = ref $class;
681 4 100       98 ref $obj
682             or return;
683 3 50       13 blessed( $obj )
684             or return;
685 3 50       31 $obj->isa( $class )
686             or return;
687 3         11 return $class;
688             }
689              
690             1;
691              
692             __END__