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   2102 use 5.006002;
  2         7  
4              
5 2     2   15 use strict;
  2         4  
  2         58  
6 2     2   12 use warnings;
  2         5  
  2         63  
7              
8 2     2   11 use Carp;
  2         3  
  2         154  
9 2     2   1110 use IO::File ();
  2         19294  
  2         126  
10 2     2   1498 use POSIX qw{ floor };
  2         14763  
  2         14  
11 2     2   3554 use Scalar::Util qw{ blessed openhandle };
  2         5  
  2         138  
12 2     2   1481 use Time::Local;
  2         5359  
  2         181  
13              
14 2     2   31 use constant FULL_CIRCLE => 4294967296; # 2 ** 32;
  2         4  
  2         150  
15 2     2   22 use constant PI => atan2( 0, -1 );
  2         6  
  2         109  
16 2     2   13 use constant TWO_PI => 2 * PI;
  2         4  
  2         95  
17 2     2   24 use constant SPEED_OF_LIGHT => 299792.458; # Km/sec, per U.S. NIST
  2         7  
  2         132  
18              
19 2     2   14 use constant ARRAY_REF => ref [];
  2         4  
  2         120  
20 2     2   11 use constant CODE_REF => ref sub {};
  2         4  
  2         4616  
21              
22             our $VERSION = '0.011';
23              
24             sub new {
25 90     90 1 17633 my $class = shift;
26 90   66     330 unshift @_, ref $class || $class;
27 90         264 goto &clone;
28             }
29              
30             sub azimuth {
31 4     4 1 14 splice @_, 1, 0, azimuth => TWO_PI;
32 4         16 goto &_bash_angle;
33             }
34              
35             sub clone {
36 92     92 1 644 my ( $self, @args ) = @_;
37 92         127 my ( $class, $clone );
38 92 100       205 if ( $class = ref $self ) {
39 2         6 $clone = {};
40 2         5 while ( my ( $name, $value ) = each %{ $self } ) {
  55         123  
41 53         91 $clone->{$name} = $value;
42             }
43             } else {
44 90         182 $clone = { _static() };
45 90         293 $class = $self;
46             }
47 92         152 bless $clone, $class;
48 92         221 while ( @args ) {
49 99         203 my ( $name, $value ) = splice @args, 0, 2;
50 99 100       595 my $code = $clone->can( $name )
51             or croak "Method $name() not found";
52 98         231 $code->( $clone, $value );
53             }
54 91         389 return $clone;
55             }
56              
57             sub data_interval {
58 6     6 1 16 my ( $self, @args ) = @_;
59 6 100       17 if ( @args ) {
60 3         7 my $interval = $args[0];
61 3 100       13 if ( $interval <= 0 ) {
    100          
62 1         106 croak "Negative data interval invalid";
63             } elsif ( $interval < 1 ) {
64 1         3 $interval = 1 / $interval;
65 1         3 $interval = ( ~ $interval & 0x07ff ) + 1;
66             }
67 2         5 $self->{tracker_type_and_data_rate} &= ~ 0x7ff;
68 2         39 $self->{tracker_type_and_data_rate} |= $interval & 0x07ff;
69 2         39 return $self;
70             } else {
71 3         10 my $interval = $self->{tracker_type_and_data_rate} & 0x07ff;
72 3 100       29 $interval & 0x0400 or return $interval;
73 1         3 $interval = ( ~ $interval & 0x07ff ) + 1;
74 1         5 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 26 my ( $self, $method, @args ) = @_;
185 12 100       47 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       40 ARRAY_REF eq $type
190             and return $dcdr->[ $self->$method( @args ) ];
191 2 50       11 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 45 splice @_, 1, 0, doppler_count => 1, 'is_doppler_valid';
199 13         39 goto &_bash_6_bytes;
200             }
201              
202             sub doppler_shift {
203 7     7 1 19 my ( $self, @args ) = @_;
204 7 100       97 @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       24 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     11 $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         12 my $count = $self->doppler_count() - $prior->doppler_count();
217 4         16 my $deltat = $self->measurement_time() - $prior->measurement_time();
218 4 100       13 if ( $deltat < 0 ) {
219 1         4 $deltat = - $deltat;
220 1         2 $count = - $count;
221             }
222 4 50       11 $count < 0 and $count += 2 << 48;
223 4         19 return ( $count / $deltat - 240_000_000 ) / $self->factor_M();
224             }
225              
226             sub elevation {
227 4     4 1 16 splice @_, 1, 0, elevation => PI;
228 4         14 goto &_bash_angle;
229             }
230              
231             sub enforce_validity {
232 34     34 1 438 my ( $self, @args ) = @_;
233 34 100       69 if ( @args ) {
234 5         14 $self->{enforce_validity} = shift @args;
235 5         15 return $self;
236             } else {
237 29         200 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 17 my ( $self, @args ) = @_;
247 6 100       19 if ( @args ) {
248 2         5 $self->{factor_K} = $args[0];
249 2         6 return $self;
250             } else {
251             return ( defined $self->{factor_K} ? $self->{factor_K} :
252             ( $self->{factor_K} =
253 4 50       20 $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 26 my ( $self, @args ) = @_;
264 9 100       17 if ( @args ) {
265 2         5 $self->{factor_M} = $args[0];
266 2         4 return $self;
267             } else {
268             return ( defined $self->{factor_M} ? $self->{factor_M} :
269             ( $self->{factor_M} =
270 7 50       33 $self->transmit_frequency() >= 12_000_000_000 ?
    100          
271             100 : 1000 ) );
272             }
273             }
274              
275             sub frequency_band {
276 4     4 1 16 splice @_, 1, 0, frequency_band_and_transmission_type => 1;
277 4         13 goto &_bash_nybble;
278             }
279              
280             sub hex_record {
281 1     1 1 4 my ( $self, @args ) = @_;
282 1 50       5 if ( @args ) {
283 0         0 return $self->raw_record( pack 'H*', $args[0] );
284             } else {
285 1         5 return unpack 'H*', $self->raw_record();
286             }
287             }
288              
289             sub is_angle_corrected_for_misalignment {
290 3     3 1 12 splice @_, 1, 0, data_validity => 3;
291 3         9 goto &_bash_bit;
292             }
293              
294             sub is_angle_corrected_for_refraction {
295 3     3 1 11 splice @_, 1, 0, data_validity => 4;
296 3         9 goto &_bash_bit;
297             }
298              
299             sub is_angle_valid {
300 5     5 1 16 splice @_, 1, 0, data_validity => 2;
301 5         17 goto &_bash_bit;
302             }
303              
304             sub is_destruct_doppler {
305 3     3 1 10 splice @_, 1, 0, data_validity => 6;
306 3         9 goto &_bash_bit;
307             }
308              
309             sub is_doppler_valid {
310 13     13 1 36 splice @_, 1, 0, data_validity => 1;
311 13         34 goto &_bash_bit;
312             }
313              
314             sub is_range_valid {
315 7     7 1 21 splice @_, 1, 0, data_validity => 0;
316 7         19 goto &_bash_bit;
317             }
318              
319             sub is_range_corrected_for_refraction {
320 3     3 1 9 splice @_, 1, 0, data_validity => 5;
321 3         9 goto &_bash_bit;
322             }
323              
324             sub is_side_lobe {
325 3     3 1 11 splice @_, 1, 0, data_validity => 7;
326 3         11 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         9 goto &_bash_bit;
332             }
333              
334             sub measurement_time {
335 12     12 1 27 my ( $self, @args ) = @_;
336 12 100       26 if ( @args ) {
337 1         10 my $time = floor( $args[0] );
338 1         6 my $microseconds = floor( ( $args[0] - $time ) * 1_000_000 + 0.5);
339 1         8 my @cald = gmtime $time;
340 1         4 my $year = $cald[5] % 100;
341 1         8 my $seconds = $time - timegm( 0, 0, 0, 1, 0, $cald[5] );
342 1         56 return $self->year( $year )->seconds_of_year( $seconds
343             )->microseconds_of_year( $microseconds );
344             } else {
345 11         36 my $yr = $self->year();
346 11 50       34 $yr < 70 and $yr += 100;
347 11         51 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 29 my ( $self, @args ) = @_;
354 13 100       33 if ( @args ) {
355 7         14 my $prior = shift @args;
356 7 100 100     30 defined $prior and not __PACKAGE__->_instance( $prior )
357             and croak 'Prior record must be undef or an ', __PACKAGE__,
358             ' object';
359 6         15 $self->{prior_record} = $prior;
360 6         15 return $self;
361             } else {
362 6         30 return $self->{prior_record};
363             }
364             }
365              
366             sub range {
367 3     3 1 12 my ( $self, @args ) = @_;
368 3 100       9 if ( @args ) {
369 1         74 croak "range() may not be used as a mutator";
370             } else {
371 2 100       10 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 22 splice @_, 1, 0, range_delay => 256, 'is_range_valid';
380 6         21 goto &_bash_6_bytes;
381             }
382              
383             sub range_rate {
384 3     3 1 9 my ( $self, @args ) = @_;
385 3 100       93 @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       10 if ( defined ( my $shift = $self->doppler_shift() ) ) {
389             return (
390 1         5 - 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         3 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 181 my ( $self, @args ) = @_;
425 89 100       164 if ( @args ) {
426 46         71 my $raw_record = shift @args;
427 46 100       210 length $raw_record == 75
428             or croak "Invalid raw record: length not 75 bytes";
429 2     2   1879 use bytes;
  2         35  
  2         13  
430 45         451 @$self{ @utdf_fields } = unpack $utdf_template, $raw_record;
431 45         200 return $self;
432             } else {
433 2     2   151 use bytes;
  2         15  
  2         9  
434 43         474 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   148 return wantarray ? %{ $static } : $static;
  90         1075  
449             }
450             }
451              
452             sub receive_antenna_diameter_code {
453 4     4 1 15 splice @_, 1, 0, receive_antenna_type => 1;
454 4         16 goto &_bash_nybble;
455             }
456              
457             sub receive_antenna_geometry_code {
458 4     4 1 15 splice @_, 1, 0, receive_antenna_type => 0;
459 4         12 goto &_bash_nybble;
460             }
461              
462             {
463              
464             my %my_arg = map { $_ => 1 } qw{ file };
465              
466             sub slurp {
467 5     5 1 1082 my ( undef, @in_args ) = @_; # Invocant unused
468              
469 5 100       29 @in_args % 2 and unshift @in_args, 'file';
470 5         11 my ( %arg, @attrib );
471 5         15 while ( @in_args ) {
472 6         20 my ( $name, $value ) = splice @in_args, 0, 2;
473 6 100       19 if ( $my_arg{$name} ) {
474 4         31 $arg{$name} = $value;
475             } else {
476 2         6 push @attrib, $name, $value;
477             }
478             }
479 5 100       103 $arg{file} or croak "File not specified";
480              
481 4         8 my $fh = $arg{file};
482 4         7 my $fn;
483 4 50       22 if ( ! openhandle( $fh ) ) {
484 4         7 $fn = $fh;
485 4 100       309 -e $fn or croak "$fn not found";
486 3 100       119 -f _ or croak "$fn not a normal file";
487 2 50       27 $fh = IO::File->new( $fn, '<' )
488             or croak "Unable to open $fn: $!";
489             }
490 2         318 binmode $fh;
491              
492 2         5 my @rslt;
493 2         3 my ( $buffer, $count );
494 2         68 while ( $count = read $fh, $buffer, 75 ) {
495 4 100       81 push @rslt, __PACKAGE__->new(
496             raw_record => $buffer,
497             prior_record => ( @rslt ? $rslt[-1] : undef ),
498             @attrib,
499             );
500             }
501 2         33 close $fh;
502 2         20 return @rslt;
503             }
504              
505             }
506              
507             sub tracker_type {
508 3     3 1 13 splice @_, 1, 0, tracker_type_and_data_rate => 3;
509 3         12 goto &_bash_nybble;
510             }
511              
512             sub tracking_mode {
513 4     4 1 14 my ( $self, @args ) = @_;
514             # This would be a delegation to _bash_quarter if there were more
515             # than one two-bit field.
516 4         8 my $attr = 'mode';
517 4         8 my $shift = 2;
518 4         12 my $mask = 0x03 << $shift;
519 4 100       16 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         17 return ( $self->{mode} & $mask ) >> $shift;
525             }
526             }
527              
528             sub transmission_type {
529 4     4 1 12 splice @_, 1, 0, frequency_band_and_transmission_type => 0;
530 4         12 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 15 splice @_, 1, 0, transmit_antenna_type => 0;
540 4         13 goto &_bash_nybble;
541             }
542              
543             sub transmit_frequency {
544 11     11 1 29 my ( $self, @args ) = @_;
545 11 100       28 if ( @args ) {
546 2         15 $self->{transmit_frequency} = floor( ( $args[0] + 5 ) / 10 );
547 2         8 return $self;
548             } else {
549 9         45 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   1725 no strict qw{ refs };
  2         4  
  2         1394  
574             *$attribute = sub {
575 91     91   525 my ( $self, @args ) = @_;
576 91 100       183 if ( @args ) {
577 19         45 $self->{$attribute} = shift @args;
578 19         109 return $self;
579             } else {
580 72         232 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   58 my ( $self, $attr, $factor, $validator, @args ) = @_;
593 19 100       43 if ( @args ) {
594 3         12 my $value = $factor * shift @args;
595 3         21 my $value_hi = floor( $value / 65536 );
596 3         9 my $value_lo = $value - $value_hi * 65536;
597 3         13 $self->{ $attr . '_hi' } = $value_hi | 0; # Force integer
598 3         12 $self->{ $attr . '_lo' } = $value_lo | 0; # Force integer
599 3         13 return $self;
600             } else {
601 16 100 66     67 $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         83 $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   26 my ( $self, $attr, $upper, @args ) = @_;
620 8 100       21 if ( @args ) {
621 2         10 my $angle = $args[0] / TWO_PI;
622 2         12 $angle -= floor( $angle );
623 2         10 $self->{$attr} = floor( $angle * FULL_CIRCLE + 0.5 );
624 2         9 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         20 my $angle = $self->{$attr} / FULL_CIRCLE * TWO_PI;
630 4 50       13 $angle >= $upper and $angle -= TWO_PI;
631 4         12 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   94 my ( $self, $attr, $bit, @args ) = @_;
642 43         84 my $mask = 0x01 << $bit;
643 43 100       85 if ( @args ) {
644 12 100       32 if ( $args[0] ) {
645 6         11 $self->{$attr} |= $mask;
646             } else {
647 6         16 $self->{$attr} &= ~ $mask;
648             }
649 12         42 return $self;
650             } else {
651 31 100       161 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   72 my ( $self, $attr, $bit, @args ) = @_;
662 27         48 my $shift = 4 * $bit;
663 27         57 my $mask = 0x0f << $shift;
664 27 100       61 if ( @args ) {
665 7         17 $self->{$attr} &= ~ $mask;
666 7         17 $self->{$attr} |= ( $args[0] & 0x0f ) << $shift;
667 7         24 return $self;
668             } else {
669 20         85 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   10 my ( $class, $obj ) = @_;
679 4 50       10 ref $class
680             and $class = ref $class;
681 4 100       143 ref $obj
682             or return;
683 3 50       19 blessed( $obj )
684             or return;
685 3 50       34 $obj->isa( $class )
686             or return;
687 3         13 return $class;
688             }
689              
690             1;
691              
692             __END__