File Coverage

blib/lib/SCUBA/Table/NoDeco.pm
Criterion Covered Total %
statement 116 118 98.3
branch 52 54 96.3
condition 21 24 87.5
subroutine 17 17 100.0
pod 11 11 100.0
total 217 224 96.8


line stmt bran cond sub pod time code
1             package SCUBA::Table::NoDeco;
2            
3 10     10   258805 use strict;
  10         25  
  10         424  
4 10     10   56 use Carp;
  10         15  
  10         2397  
5            
6             our $VERSION = "0.03";
7            
8             =head1 NAME
9            
10             SCUBA::Table::NoDeco - Calculate no-decompression dive times.
11            
12             =head1 SYNOPSIS
13            
14             use SCUBA::Table::NoDeco;
15            
16             my $table = SCUBA::Table::NoDeco->new();
17            
18             $table->dive(metres => 15, minutes => 30);
19            
20             print $table->group,"\n"; # Prints "E"
21            
22             $table->surface(minutes => 60);
23            
24             print $table->group,"\n"; # Prints "D"
25            
26             print $table->max_time(metres => 30),"\n"; # Prints 6
27            
28             =head1 WARNING AND DISCLAIMER
29            
30             Do B use this module as your sole source of no-decompression dive
31             times. This module is intended for use only as a guide to assist in
32             planning your dives. B calculate and verify your dive times
33             by hand once you have planned your dives. If you have a dive
34             computer, follow its instructions for use.
35            
36             SCUBA diving involves serious risks of injury or death, and should
37             only be performed by individuals in good health and with the appropriate
38             skills and training. Even when tables are used with proper safety
39             procedures, decompression sickness may still occur.
40            
41             The author provides ABSOLUTELY NO WARRANTY on this module, without
42             even the implied warranty of merchantability or fitness for a particular
43             purpose. Use entirely at your own risk.
44            
45             =head1 DESCRIPTION
46            
47             This module provides the ability to perform useful calculations
48             using dive-tables, including calculating dive groups and maximum
49             no-decompression times for repetitive dives. A selection of tables
50             are available. The module assumes that the diver is using air as
51             their breathing gas.
52            
53             =head1 METHODS
54            
55             The following methods are provided.
56            
57             =cut
58            
59             # There's really 0.3048 feet in a metre, but all the dive tables seem
60             # to assume a flat 1 ft = 30 cm. As such, we use the same constant here,
61             # otherwise we end up with incorrect results.
62            
63 10     10   63 use constant FEET2METRES => 0.3;
  10         21  
  10         34675  
64            
65             our %MIN_SURFACE_TIME = (
66             SSI => 10, # Less than 10 minutes is considered same dive.
67             PADI => 0, # PADI doesn't have a minimum surface time.
68             );
69            
70             our %LIMITS = (
71             SSI => {
72             3.0 => [60, 120, 210, 300],
73             4.5 => [35, 70, 110, 160, 225, 350],
74             6.0 => [25, 50, 75, 100, 135, 180, 240, 325],
75             7.5 => [20, 35, 55, 75, 100, 125, 160, 195, 245],
76             9.0 => [15, 30, 45, 60, 75, 95, 120, 145, 170, 205],
77             10.5 => [ 5, 15, 25, 40, 50, 60, 80, 100, 120, 140, 160],
78             12.0 => [ 5, 15, 25, 30, 40, 50, 70, 80, 100, 110, 130],
79             15.0 => [ 0, 10, 15, 25, 30, 40, 50, 60, 70],
80             18.0 => [ 0, 10, 15, 20, 25, 30, 40, 50],
81             21.0 => [ 0, 5, 10, 15, 20, 30, 35, 40],
82             24.0 => [ 0, 5, 10, 15, 20, 25, 30],
83             27.0 => [ 0, 5, 10, 12, 15, 20, 25],
84             30.0 => [ 0, 5, 7, 10, 15, 20],
85             33.0 => [ 0, 0, 5, 10, 13, 15],
86             36.0 => [ 0, 0, 5, 10],
87             39.0 => [ 0, 0, 5]
88             },
89             PADI => {
90             # 35 feet
91             10.5 => [ 10, 19, 25, 29, 32, 36, 40, 44, 48, 52, 57, 62,
92             67, 73, 79, 85, 92, 100, 108, 117, 127, 139, 152, 168,
93             188, 205],
94             # 40 feet
95             12.0 => [ 9, 16, 22, 25, 27, 31, 34, 37, 40, 44, 48, 51,
96             55, 60, 64, 69, 74, 79, 85, 91, 97, 104, 111, 120,
97             129, 140],
98             # 50 feet
99             15.0 => [ 7, 13, 17, 19, 21, 24, 26, 28, 31, 33, 36, 39,
100             41, 44, 47, 50, 53, 57, 60, 63, 67, 71, 75, 80],
101             # 60 feet
102             18.0 => [ 6, 11, 14, 16, 17, 19, 21, 23, 25, 27, 29, 31,
103             33, 35, 37, 39, 42, 44, 47, 49, 52, 54, 55],
104             # 70 feet
105             21.0 => [ 5, 9, 12, 13, 15, 16, 18, 19, 21, 22, 24, 26,
106             27, 29, 31, 33, 35, 36, 38, 40],
107             # 80 feet
108             24.0 => [ 4, 8, 10, 11, 13, 14, 15, 17, 18, 19, 21, 22,
109             23, 25, 26, 28, 29, 30],
110             # 90 feet
111             27.0 => [ 4, 7, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19,
112             21, 22, 23, 24, 25],
113             #100 feet
114             30.0 => [ 3, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
115             18, 19, 20],
116             #110 feet
117             33.0 => [ 3, 6, 7, 8, 9, 10, 11, 12, 13, 13, 14, 15,
118             16],
119             #120 feet
120             36.0 => [ 3, 5, 6, 7, 8, 9, 10, 11, 11, 12, 13],
121             #130 feet
122             39.0 => [ 3, 5, 6, 7, 7, 8, 9, 10],
123             #140 feet
124             42.0 => [ 0, 4, 5, 6, 7, 8],
125             },
126             );
127            
128             our %SURFACE = (
129             SSI => {
130             A => { 12*60+ 0 => "A" },
131             B => { 12*60+ 0 => "A", 3*60+20 => "B" },
132             C => { 12*60+ 0 => "A", 4*60+49 => "B", 1*60+39 => "C" },
133             D => { 12*60+ 0 => "A", 5*60+48 => "B", 2*60+38 => "C", 1*60+ 9 => "D"},
134             E => { 12*60+ 0 => "A", 6*60+34 => "B", 3*60+24 => "C", 1*60+57 => "D",
135             54+ 0 => "E" },
136             F => { 12*60+ 0 => "A", 7*60+ 5 => "B", 3*60+57 => "C", 2*60+28 => "D",
137             1*60+29 => "E", 0*60+45 => "F" },
138             G => { 12*60+ 0 => "A", 7*60+35 => "B", 4*60+25 => "C", 2*60+58 => "D",
139             1*60+50 => "E", 1*60+15 => "F", 0*60+40 => "G" },
140             H => { 12*60+ 0 => "A", 7*60+59 => "B", 4*60+49 => "C", 3*60+20 => "D",
141             2*60+23 => "E", 1*60+41 => "F", 1*60+06 => "G", 0*60+36 => "H"},
142             I => { 12*60+ 0 => "A", 8*60+21 => "B", 5*60+12 => "C", 3*60+43 => "D",
143             2*60+44 => "E", 2*60+02 => "F", 1*60+29 => "G", 0*60+59 => "H",
144             0*60+33 => "I" },
145             J => { 12*60+ 0 => "A", 8*60+50 => "B", 5*60+40 => "C", 4*60+02 => "D",
146             3*60+04 => "E", 2*60+20 => "F", 1*60+47 => "G", 1*60+19 => "H",
147             0*60+54 => "I", 0*60+31 => "J" },
148             K => { 12*60+ 0 => "A", 8*60+58 => "B", 5*60+48 => "C", 4*60+19 => "D",
149             3*60+21 => "E", 2*60+38 => "F", 2*60+38 => "G", 1*60+35 => "H",
150             1*60+11 => "I", 0*60+49 => "J", 0*60+28 => "K" },
151             },
152            
153             );
154            
155             # PADI tables seem to have a consistant transition between
156             # groups (it always takes 3 hours to get out of group A, always
157             # takes 47 minutes to get out of group B). An extra minute
158             # appears to be added to each group transition beyond the first.
159             # As such, we can dynamically generate the PADI surface table.
160            
161             # Unfortunately, this doesn't hold true for everything. Some particular
162             # entries have minor variations that then dissapear. For example, D is
163             # regular, E is not (there's a minute that's gained), and F
164             # is regular. Whether these are mistakes, intentional changes to
165             # perform some sort of fingerprinting, or intentional changes with
166             # a sound backing, I'm not sure.
167            
168             # In any case, it certainly makes testing more difficult. What's
169             # more correct, the table, or the precursor tables from which it appears
170             # to have been derived? I think for the moment we have to assume that
171             # the tables themselves are considered definitive.
172            
173             {
174             my @surface = ( 3*60, 47, 21, 8, (7) x 2, 6, (5) x 3, (4) x 3,
175             (3) x 6, (2) x 7 );
176            
177             foreach my $start (0..25) {
178             my $accum = 0;
179             my $end = $start;
180             while ($end >= 0) {
181             $accum += $surface[$end];
182             $SURFACE{PADI}
183             {chr(ord('A')+$start)}
184             {$accum + $start - $end} = chr(ord('A')+$end);
185             $end--;
186             }
187             }
188            
189             }
190            
191             # Per-row tweaks. 'E' gains a minute for moves to B and A.
192            
193             delete $SURFACE{PADI}{E}{86}; $SURFACE{PADI}{E}{87} = "B";
194             delete $SURFACE{PADI}{E}{267}; $SURFACE{PADI}{E}{268} = "A";
195            
196             # The 'O' and 'P' rows also does a funny thing at the end.
197             delete $SURFACE{PADI}{O}{142}; $SURFACE{PADI}{O}{143} = "B";
198             delete $SURFACE{PADI}{P}{146}; $SURFACE{PADI}{P}{147} = "B";
199            
200             # The PADI tables are highly consistant, in that the residual time
201             # for each group is equal to the dive times on table 1. This means
202             # that in our representation of these tables, they are exactly the same.
203             # Rather convenient.
204             #
205             # XXX - Consider what this means for repetitive dives with no surface
206             # interval. Does it work to treat these as separate dives? Yes, it
207             # appears it does.
208            
209             our %RESIDUAL = (
210             SSI => {
211             3 => [39, 88, 159, 279],
212             6 => [18, 39, 62, 88, 120, 159, 208, 279, 399],
213             9 => [12, 25, 39, 54, 70, 88, 109, 132, 159, 190],
214             12 => [ 7, 17, 25, 37, 49, 61, 73, 87, 101, 116],
215             15 => [ 6, 13, 21, 29, 38, 47, 56, 66],
216             18 => [ 5, 11, 17, 24, 30, 36, 44],
217             21 => [ 4, 9, 15, 20, 26, 31, 37],
218             24 => [ 4, 8, 13, 18, 23, 28],
219             27 => [ 3, 7, 11, 16, 20, 24],
220             30 => [ 3, 7, 10, 14, 18],
221             33 => [ 3, 6, 10, 13],
222             36 => [ 3, 6, 9],
223             39 => [ 3],
224             },
225             PADI => $LIMITS{PADI},
226             );
227            
228             # Which depths appear on our limit charts, in numerically ascending order.
229             our %LIMIT_DEPTHS = (
230             SSI => [sort {$a <=> $b} keys %{$LIMITS{SSI}}],
231             PADI => [sort {$a <=> $b} keys %{$LIMITS{PADI}}],
232             );
233            
234             # Same for residual depths. For SSI there are less depths on the residual
235             # table, so we must interpret some repetitve dives as deeper than they
236             # really are.
237             our %RESIDUAL_DEPTHS = (
238             SSI => [sort {$a <=> $b} keys %{$RESIDUAL{SSI}}],
239             PADI => [sort {$a <=> $b} keys %{$RESIDUAL{PADI}}],
240             );
241            
242             =head2 new
243            
244             my $stn = SCUBA::Table::NoDeco->new(table => "SSI");
245            
246             This class method returns a SCUBA::Table::NoDeco object. It takes
247             an optional I argument, specifying which dive table should be
248             used.
249            
250             If no dive table is supplied then the module will use the default
251             SSI table. This default may change in future releases, so you should
252             not rely upon this default.
253            
254             SSI tables are the only ones supported in the present release.
255            
256             =cut
257            
258             sub new {
259 14     14 1 1724 my $class = shift;
260 14         31 my $this = {};
261 14         37 bless($this,$class);
262 14         58 $this->_init(@_);
263 13         37 return $this;
264             }
265            
266             sub _init {
267 51     51   139 my ($this, %args) = @_;
268 51   100     225 $this->{table} = $args{table} || "SSI"; # Tables to use.
269 51   100     319 $this->{group} = $args{group} || ""; # Initial group.
270 51   50     288 $this->{surface} = $args{surface} || 0; # Surface time.
271            
272 51         95 $this->{dive_time} = 0; # Used for consequtive dives with less than...
273 51         79 $this->{last_depth}= 0; # ... MIN_SURFACE_TIME between them.
274            
275 51 100       337 croak "Non-existant table $args{table} supplied" unless exists $RESIDUAL{$this->{table}};
276            
277 50         124 return $this;
278             }
279            
280             =head2 list_tables
281            
282             my @tables = SCUBA::Table::NoDeco->list_tables();
283            
284             This method returns a list of tables that can be selected when creating
285             a new SCUBA::Table::NoDeco object.
286            
287             =cut
288            
289             sub list_tables {
290 1     1 1 1255 return keys %RESIDUAL;
291             }
292            
293             =head2 max_table_depth
294            
295             my $max_depth_ft = $stn->max_table_depth(units => "feet");
296             my $max_depth_mt = $stn->max_table_depth(units => "metres");
297            
298             This method provides the maximum depth for repetitive dives provided by the
299             tables currently in use. Some tables may provide a greater depth for
300             non-reptetitve dives. This function I supply the maximum safe
301             depth (however see L). The units argument is mandatory.
302            
303             =cut
304            
305             sub max_table_depth {
306 5     5 1 791 my ($this, %args) = @_;
307 5 100       23 if ($args{units} eq "metres") {
    50          
308 3         9 return $RESIDUAL_DEPTHS{$this->table}[-1]
309             } elsif ($args{units} eq "feet") {
310 2         7 return $RESIDUAL_DEPTHS{$this->table}[-1] / FEET2METRES;
311             } else {
312 0         0 croak "max_table_depth requires units parameter of 'metres' or 'feet'";
313             }
314             }
315            
316             sub _feet2metres {
317 242     242   443 my ($this, %args) = @_;
318            
319 242         298 local $Carp::CarpLevel = 1; # Auto-strip one level of calls.
320            
321 242 100 100     1149 if ($args{feet} and $args{metres}) {
    100          
    100          
322 1         126 croak "Both feet and metres arguments supplied to SCUBA::Table::NoDeco";
323             } elsif ($args{feet}) {
324 68 100       318 croak "Positive depth must be supplied" if $args{feet} <= 0;
325 67         220 return $args{feet} * FEET2METRES;
326             } elsif (not $args{metres}) {
327 2         300 croak "Missing mandatory 'feet' or 'metres' parameter to SCUBA::Table::NoDeco::dive";
328             }
329 171 100       503 croak "Positive depth must be supplied" if $args{metres} <= 0;
330 170         409 return $args{metres};
331             }
332            
333             # This converts the depth given into a 'standard depth' as what appears
334             # on the chart. If we're performing a repetitive dive (ie, our group
335             # is non-empty) then we'll read from RESIDUAL_DEPTHS, otherwise for
336             # a fresh dive we'll read from LIMIT_DEPTHS. This means we may treat
337             # some shallow repetitve dives as deeper than they really are. (This
338             # errs on the side of safety).
339            
340             sub _std_depth {
341 238     238   449 my ($this, %args) = @_;
342 238 100       536 die "Incorrect call to SCUBA::Table::NoDeco::_std_depth, no metres arg." unless $args{metres};
343            
344             # Find the correct table to use.
345 237         225 my @depths;
346 237 100       423 if ($this->group) {
347 56         62 @depths = @{$RESIDUAL_DEPTHS{$this->{table}}};
  56         186  
348             } else {
349 181         195 @depths = @{$LIMIT_DEPTHS{$this->{table}}};
  181         741  
350             }
351            
352 237         400 foreach my $depth (@depths) {
353 1817 100       3721 return $depth if $depth >= $args{metres};
354             }
355 2         4 local $Carp::CarpLevel = 1; # Auto-strip one level of calls.
356 2         344 croak "Supplied depth $args{metres} metres is not on $this->{table} charts.";
357             }
358            
359             =head2 clear
360            
361             $stn->clear();
362            
363             This method resets the object to its pristine state. The table used for
364             dive calculations is retained.
365            
366             =cut
367            
368             # Clear all status, except table. This is done by clearing the underlying
369             # hash entirely and recalling _init. There is almost certainly a faster
370             # and more effective way to remove all the keys without re-creating the
371             # reference.
372            
373             sub clear {
374 37     37 1 11404 my $table = $_[0]->table;
375 37         51 foreach my $key (keys %{$_[0]}) {
  37         112  
376 185         352 delete $_[0]->{$key};
377             }
378 37         114 $_[0]->_init(table => $table);
379             }
380            
381             =head2 table
382            
383             print "You are using the ",$stn->table," tables\n";
384            
385             This method simply returns the name of the dive table being used by
386             the C object.
387            
388             =cut
389            
390 512     512 1 1530 sub table { return $_[0]->{table}; }
391            
392             =head2 dive
393            
394             my $group = $stn->dive(feet => 60, minutes => 30);
395             my $group = $stn->dive(metres => 18, minutes => 30);
396            
397             This method determines and sets the diver's group for the dive information
398             provided. If the dive 'falls off' the tables, then an exception is
399             returned. This method takes into account the diver's current group,
400             surface interval, and residual nitrogen time.
401            
402             If the diver does not have a surface interval of at least 10 minutes,
403             this will consider the dive to be a continuation of the previous
404             dive. The dive times will be added, and the maximum depth of both
405             dives will be used to calculate the diver's group.
406            
407             =cut
408            
409             sub dive {
410 61     61 1 5253 my ($this, %args) = @_;
411            
412 61   100     207 $args{minutes} ||= 0;
413            
414 61 100       385 croak "Positive minutes argument required for SCUBA::Table::NoDeco::dive" if $args{minutes} <= 0;
415            
416 58         311 $args{metres} = $this->_feet2metres(%args);
417            
418             # Calculate group. This is done by looping over the list
419             # of depths until we find one equal to or greater than our
420             # current dive depth.
421            
422 53         91 my $group = "A";
423 53         131 my $depth = $this->_std_depth(metres => $args{metres});
424 52         69 my $tbt;
425            
426 52 100       110 if ($this->surface > $MIN_SURFACE_TIME{$this->table}) {
427 5         17 $tbt = $args{minutes} + $this->rnt(metres => $depth);
428             } else {
429 47         83 $tbt = $this->{dive_time} + $args{minutes};
430 47 100       122 $depth = $depth > $this->{last_depth} ? $depth : $this->{last_depth};
431             }
432            
433             # Record dive information.
434            
435 52         85 $this->{surface} = 0;
436 52         118 $this->{last_depth} = $depth;
437 52         75 $this->{dive_time} = $tbt;
438            
439 52         62 foreach my $time (@{$LIMITS{$this->{table}}{$depth}}) {
  52         150  
440             # Now walk through all our groups until
441             # we find one with a time equal to or
442             # greater than our current time.
443            
444 516 100       881 if ($time >= $tbt) {
445 51         92 $this->{group} = $group;
446 51         286 return $group;
447             }
448 465         537 $group++;
449             }
450            
451 1         78 croak "SCUBA::Table::NoDeco::dive called with a depth or time not listed on the $this->{table} table";
452             }
453            
454             =head2 group
455            
456             print "You are a ",$stn->group," diver\n";
457            
458             The group method returns the current letter designation representing
459             the amount of residual nitrogen present in the diver. The letter
460             designation is always upper-case. A diver with no residual nitrogen
461             has no group, represented by the empty string.
462            
463             =cut
464            
465             sub group {
466 410     410 1 517 my $this = shift;
467            
468             # If we're below the minimum surface time for this table,
469             # we're considered to be part of the same dive.
470 410 100       859 if ($this->{surface} <= $MIN_SURFACE_TIME{$this->table}) {
471 273         845 return $this->{group};
472             }
473            
474             # Looks like we've been off-gassing for a while. Let's
475             # find what group we're in now.
476            
477             # Make sure that we have an entry for our current state.
478 137 100       169 keys %{$SURFACE{$this->{table}}{$this->{group}}}
  137         688  
479             or croak "Incomplete table exists in $this->{table} for group $this->{group}";
480            
481             # POSSIBLE OPTIMIZATION - Cache the result of this sort,
482             # or produce a pre-sorted data strucuture.
483            
484 136         156 my @times = sort {$a <=> $b} keys %{$SURFACE{$this->{table}}{$this->{group}}};
  2948         3435  
  136         667  
485            
486 136         266 foreach my $time (@times) {
487             # XXX - Changed from '<' to '<=' to accomodate PADI
488             # tables. Does this break the SSI tables, or did
489             # they also have an off-by-one error? Check!
490 865 100       2565 if ($this->{surface} <= $time) {
491 133         740 return $SURFACE{$this->{table}}{$this->{group}}{$time};
492             }
493             }
494            
495             # If we run out of times, then they must be completely off-gassed.
496             # XXX - This is a dangerous assumption if we ever have an incomplete
497             # table.
498 3         15 return "";
499             }
500            
501            
502             =head2 surface
503            
504             $stn->surface(minutes => 60); # Spend an hour on surface.
505             print "Total surface time ",$stn->surface," minutes\n";
506            
507             This method returns the total time of the current surface interval.
508             If the optional C argument is provided, this is added to the
509             diver's current surface interval before returning the total minutes
510             elapsed.
511            
512             =cut
513            
514             # Returns total surface time.
515             sub surface {
516 115     115 1 728 my ($this, %args) = @_;
517 115 100 100     466 if (%args and ! $args{minutes}) {
518 1         74 croak "Mandatory argument 'minutes' missing from call to surface";
519             }
520 114 100       347 $args{minutes} or return $this->{surface};
521 62         106 $this->{surface} += $args{minutes};
522 62         162 return $this->{surface};
523             }
524            
525             =head2 max_time
526            
527             print "Your maximum time at 18 metres is ",$stn->max_time(metres => 18),"\n";
528             print "Your maximum time at 60 feet is ",$stn->max_time(feet => 60),"\n";
529            
530             This calculates the maximum no-decompression time for a dive to the
531             specified depth. The diver's current group is taken into account.
532            
533             If the diver cannot reach the depth supplied without breaking
534             no-decompression limits then the value '0' is returned.
535            
536             =cut
537            
538             sub max_time {
539 86     86 1 29404 my ($this, %args) = @_;
540            
541 86         209 $args{metres} = $this->_feet2metres(%args);
542 86         194 my $depth = $this->_std_depth(metres => $args{metres});
543            
544 85         176 my $rnt = $this->rnt(metres => $depth);
545 85 100       207 return 0 unless defined($rnt);
546            
547 73         242 my $max_time = $LIMITS{$this->{table}}{$depth}[-1] - $rnt;
548 73 50       138 $max_time = 0 if $max_time < 0;
549            
550 73   50     545 return $max_time || 0;
551             }
552            
553             =head2 max_depth
554            
555             print "The maximum depth you may dive is ",$stn->max_depth(units => "metres")," metres\n";
556             print "Max depth for a 20 minute dive is ",$stn->max_depth(units => "feet", minutes => 20)," feet\n";
557            
558             This method returns the maximum possible depth given the diver's current
559             group, or the maximum depth available on your table if no group is set.
560            
561             The method takes an optional argument (minutes), in which case the
562             maximum depth for a dive of that duration will be returned.
563            
564             This method returns '0' if the diver is not allowed to make *any* dive
565             for the period of time specified.
566            
567             =cut
568            
569             sub max_depth {
570 10     10 1 967 my ($this, %args) = @_;
571            
572 10   100     46 $args{minutes} ||= 1;
573            
574 10 100       98 croak "Negative minutes parameter supplied" if $args{minutes} < 0;
575 9 100 100     320 croak "Mandatory argument 'units' must be 'feet' or 'metres'"
      66        
576             unless ($args{units} and $args{units} eq 'feet' || $args{units} eq 'metres');
577            
578             # False laziness alert! Querying max_time repeatedly does
579             # provide the correct answer, but may not be particularly
580             # efficient. There could be a better way.
581            
582 7         7 foreach my $depth (reverse @{$RESIDUAL_DEPTHS{$this->table}}) {
  7         17  
583            
584             # If the diver can spend the required amount of time
585             # at that depth, then we've got a match.
586 28 100       48 if ($this->max_time(metres => $depth) >= $args{minutes}) {
587 7 100       38 return $depth if $args{units} eq "metres";
588 2         12 return $depth / FEET2METRES;
589             }
590             }
591            
592             # We've dropped off the end! These tables say that the diver is
593             # not allowed to dive at all with their current nitrogen levels
594             # for that period of time.
595            
596 0         0 return 0;
597            
598             }
599            
600             =head2 rnt
601            
602             my $rnt = $stn->rnt(metres => 12);
603             my $rnt2 = $stn->rnt(feet => 40);
604            
605             This method returns the I for a diver, in minutes.
606             The depth argument (in either metres or feet) is mandatory.
607            
608             If the depth supplied means that the diver cannot make a no-decompression
609             dive, then an undefined value is returned.
610            
611             =cut
612            
613             sub rnt {
614 98     98 1 210 my ($this, %args) = @_;
615            
616 98         203 $args{metres} = $this->_feet2metres(%args);
617            
618 98         214 my $depth = $this->_std_depth(metres => $args{metres});
619            
620             # Lookup group, returning 0 RNT if they're completely free
621             # of nitrogen.
622            
623 98 100       182 my $group = $this->group or return 0;
624            
625             # Get the group index. A is 0, B is 1, C is 2, ...
626 31         48 my $group_idx = ord($group) - ord('A');
627            
628             # Now just lookup the RNT.
629 31         117 return $RESIDUAL{$this->{table}}{$depth}[$group_idx];
630             }
631            
632             1;
633             __END__