File Coverage

lib/Weather/GHCN/Options.pm
Criterion Covered Total %
statement 138 139 99.2
branch 66 72 97.2
condition 32 36 88.8
subroutine 18 18 100.0
pod 7 7 100.0
total 261 272 97.4


line stmt bran cond sub pod time code
1             # Weather::GHCN::Options.pm - class for GHCN options
2            
3             ## no critic (Documentation::RequirePodAtEnd)
4            
5             =head1 NAME
6            
7             Weather::GHCN::Options - create and manage option lists/objects used by GHCN modules and scripts
8              
9             =head1 VERSION
10              
11             version v0.0.010
12            
13             =head1 SYNOPSIS
14            
15             use Weather::GHCN::Options;
16            
17            
18             =head1 DESCRIPTION
19            
20             The B module provides a class and methods that are
21             used within GHCN modules or from application scripts that use GHCN
22             modules to create and manage options that determine the behaviour of
23             GHCN methods.
24            
25             The module is primarily for use by module Weather::GHCN::StationTable.
26            
27             =cut
28            
29             # these are needed because perlcritic fails to detect that Object::Pad handles these things
30             ## no critic [ValuesAndExpressions::ProhibitVersionStrings]
31             ## no critic [TestingAndDebugging::RequireUseWarnings]
32            
33 5     5   2207 use v5.18; # minimum for Object::Pad
  5         20  
34 5     5   29 use warnings;
  5         8  
  5         184  
35 5     5   28 use Object::Pad 0.66 qw( :experimental(init_expr) );
  5         79  
  5         28  
36            
37             package Weather::GHCN::Options;
38             class Weather::GHCN::Options;
39            
40             our $VERSION = 'v0.0.010';
41            
42 5     5   2048 use Carp qw(carp croak);
  5         26  
  5         327  
43 5     5   37 use Const::Fast;
  5         11  
  5         36  
44 5     5   3224 use Hash::Wrap {-lvalue => 1, -defined => 1, -as => '_wrap_hash'};
  5         21385  
  5         32  
45 5     5   9810 use Text::Abbrev;
  5         199  
  5         333  
46 5     5   1182 use Weather::GHCN::CountryCodes qw( search_country );
  5         15  
  5         421  
47 5     5   1029 use Weather::GHCN::Common qw( :all );
  5         12  
  5         784  
48 5     5   2847 use YAML::Tiny;
  5         31405  
  5         30110  
49            
50             ######################################################################
51             # Constants
52             ######################################################################
53            
54             const my $TRUE => 1; # perl's usual TRUE
55             const my $FALSE => not $TRUE; # a dual-var consisting of '' and 0
56             const my $SPACE => q( );
57             const my $EMPTY => q();
58             const my $DASH => q(-);
59             const my $BAR => q(|);
60             const my $BANG => q(!);
61             const my $NEWLINE => qq(\n);
62            
63             const my $DEFAULT_PROFILE_FILE => '~/.ghcn_fetch.yaml';
64             const my $ALIAS_NAME_RE => qr{ \A [_]?[[:lower:]]+ \Z }xms;
65             const my @REPORT_TYPE_LIST => qw(detail daily monthly yearly url curl kml stn id);
66            
67             =head1 METHODS
68            
69             =head2 new
70            
71             Create a new Options object.
72            
73             =cut
74            
75             ######################################################################
76             # Set up the default Tk::Getopt option table, which we will use for
77             # both Tk::Getopt and to derive an options list of Getopt::Long
78             # for when Tk::Getopt is not installed.
79             ######################################################################
80            
81             ## no critic [ValuesAndExpressions::ProhibitMagicNumbers]
82             ## no critic [ValuesAndExpressions::ProhibitNoisyQuotes]
83             ## no critic [ValuesAndExpressions::ProhibitEmptyQuotes]
84            
85             my $Tk_opt_table = [
86             'Basic options',
87             ['location', '=s', undef, label => 'Filter stations by their location name (regex)'],
88             ['state', '=s', undef, label => 'Filter stations by state or province',
89             alias => ['province'] ],
90             ['country', '=s', undef, label => 'Filter station by the country they are in'],
91             ['gsn', '!', undef, label => 'Include only GSN reference stations'],
92             ['', '', '-'],
93             ['report', '=s', '',
94             label => 'Type of report',
95             strict => 1,
96             choices => [
97             [ 'station list', '' ],
98             [ 'yearly summary', 'yearly' ],
99             [ 'monthly summary', 'monthly' ],
100             [ 'daily summary', 'daily' ],
101             [ 'detail level', 'detail' ],
102             [ 'station KML', 'kml' ],
103             [ 'station url list', 'url' ],
104             [ 'station curl list', 'curl' ],
105             [ 'station list only', 'stn' ],
106             ]
107             ],
108             ['', '', '-'],
109             ['dataonly', '!', undef, label => 'Only print the data table'],
110             ['performance', '!', undef, label => 'Report performance statistics'],
111             ['verbose', '!', undef, label => 'Print information messages'],
112            
113             'Date filters',
114             ['range', '=s', undef, label => 'Filter selected station data by year range'],
115             ['active', '=s', undef, label => 'Filter stations by their active year range'],
116             ['partial', '!', undef, label => 'Allow stations only active for part of the active range'],
117             ['quality', '=i', 90, label => 'Quality threshold (percent as an integer)'],
118             ['', '','-'],
119             ['fday', '=s', undef, label => 'Filter output to include a specific day'],
120             ['fmonth', '=s', undef, label => 'Filter output to include a specific month'],
121            
122             'GIS filters',
123             ['gps', '=s', undef, label => 'Filter stations by latitude and longitude',
124             help => 'Enter decimal latitude and longitude'],
125             ['radius', '=i', 50, label => 'Radius to search for stations near coordinates'],
126            
127            
128             'Analysis Options',
129             ['anomalies', '!', undef, label => 'Provide calculated anomalies in the output'],
130             ['baseline', '=s', '1971-2000',
131             label => 'Baseline year range'],
132             ['precip', '!', undef, label => 'Include precipitation stats in the results'],
133             ['tavg', '!', undef, label => 'Include TAVG in the results'],
134             ['nogaps', '!', undef, label => 'Emit extra rows for missing months or days'],
135            
136             'Other Options',
137             ['kmlcolor', '=s', 'red', label => 'Color to use for KML placemarks' ],
138             ['profile', '=s', $DEFAULT_PROFILE_FILE,
139             label => 'Profile file location (for option preloading)'], #, nogui => 1],
140             ['cachedir', '=s', undef, label => 'Directory for cached files'],
141             ['refresh', '=s', 'yearly',
142             help => 'Refresh yearly, (default), never, always, or if N days old (N > 1)',
143             label => 'Cache refresh option',
144             choices => [ 'yearly', 'always', 'never', '' ],
145             ],
146             ];
147            
148             ## use critic [ValuesAndExpressions::ProhibitMagicNumbers]
149             ## use critic [ValuesAndExpressions::ProhibitNoisyQuotes]
150             ## use critic [ValuesAndExpressions::ProhibitEmptyQuotes]
151            
152             #####################################################################
153             # Class fields
154             ######################################################################
155            
156 23     23 1 7554 field $_opt_href :mutator; # a hashref of merged options (with default values applied))
  23         119  
157 23     23 1 96 field $_opt_obj :mutator; # a Hash::Wrap object derived from $_opt_href
  23         109  
158 285     285 1 1195 field $_profile_href :mutator; # a hash containing profile file options
  285         642  
159             field $_tk_opt_aref; # the Tk:Getopt array that defines all GHCN options
160            
161             =head1 FIELD ACCESSORS
162            
163             Writeable (mutator) access is provided for some fields primarily so
164             an Options object can be tested independantly from StationTable.
165             In general, an Options object is set by the StationTable set_options
166             method and should not be modified directly by the consuming application
167             using these mutators.
168            
169             =over 4
170            
171             =item opt_href
172            
173             This writable field is set by StationTable->set_options and is a
174             hashref of user options merged with default values.
175            
176             For programmatic access to option values, use of B is
177             preferred to prevent mispellings. (See B.)
178            
179             =item opt_obj
180            
181             This writable field is set by StationTable->set_options and is a
182             Hash::Wrap object composed from the B hashref field. It
183             provides accessor field methods for user options (merged with default
184             values).
185            
186             Using this object, rather than B, for access to option
187             values is safer programming choice as any misspelling of an option
188             name will result in a run time error. In contrast, mispelling a hash
189             key will simply result in an undef being returned.
190            
191             =item profile_href
192            
193             This writable field is set by StationTable->set_options and contains
194             the profile options it was given.
195            
196             =back
197            
198             =cut
199            
200             ######################################################################
201             =head1 CLASS METHODS
202            
203             The following class methods are supported. Class Options uses
204             Object::Perl, so these class methods (specified using the :common
205             method attribute) should be accessed using -> not :: because ->
206             will shift off the $class argument and :: won't.
207            
208             =head2 get_tk_options_table
209            
210             Returns: @tk_opttable or \@tk_opttable
211            
212             Provides access to the predefined TK::Getopt options list that define
213             the Getopt::Long arguments supported by class StationTable for user
214             options.
215            
216             The table is a list of lists and strings. The strings define
217             sections that Tk::Getopt renders as panels or tabs in the GUI it
218             constructs. The lists contain option names and types (in Getopt::Long
219             style) as well as default values, aliases, and labels to be displayed
220             in the GUI, choices for multi-select options, and other extensions.
221             See Tk::Getopt OPTTABLE ARGUMENTS for details.
222            
223             =cut
224            
225             method get_tk_options_table :common () {
226             return wantarray ? ( $Tk_opt_table->@* ) : $Tk_opt_table;
227             }
228            
229             =head2 get_getopt_list
230            
231             Returns: @options or \@options
232            
233             In scalar context, return a list reference to a translation of the
234             TK::Getopt options list into the simpler list used by Getopt::Long.
235             This gives application authors a choice between using Tk::Getopt and
236             the non-GUI and more traditional Getopt::Long.
237            
238             In list context, this method returns a Getopt::Long-style list
239             options.
240            
241             Typically, this method would be called prior to Getopt::Long in order
242             to obtain an options list for using the StationTable class; e.g.
243            
244             my %opt;
245             my @options = ( Weather::GHCN::Options->get_getopt_list() );
246             GetOptions( \%opt, @options);
247            
248             =cut
249            
250             method get_getopt_list :common () {
251             ## no critic [ProhibitDoubleSigils]
252             my @options_text;
253             my @options_list;
254             my @options;
255            
256             # According to https://metacpan.org/pod/Tk::Getopt -opttable
257             # should be a reference to an array containing all options.
258             # Elements of this array may be strings, which indicate the
259             # beginning of a new group, or array references describing the
260             # options. The first element of this array is the name of the
261             # option, the second is the type (=s for string, =i for integer,
262             # ! for boolean, =f for float etc., see Getopt::Long) for a
263             # detailed list. The third element is optional and contains the
264             # default value (otherwise the default is undefined).
265             # Further elements are optional too and describe more attributes. For a
266             # complete list of these attributes refer to "OPTTABLE ARGUMENTS".
267            
268             foreach my $row ( $Tk_opt_table->@* ) {
269             next if ref $row ne 'ARRAY';
270            
271             # pick off the first three values, then slurp the rest
272             my ($opt_kw, $opt_type, $default, @other) = $row->@*;
273             # skip the group dividers
274             next if not $opt_kw;
275            
276             my %h;
277             while (my $item = shift @other) {
278             if (ref $item eq 'HASH') {
279             while (my ($k,$v) = each $item->%*) {
280             $h{$k} = $v;
281             }
282             } else {
283             my $value = shift @other;
284             $h{$item} = $value;
285             }
286             }
287            
288             my $label = $h{'label'} // $SPACE;
289             my $alias_aref = $h{'alias'} // [];
290             my $opt_kw_with_aliases = join $BAR, $opt_kw, $alias_aref->@*;
291            
292             push @options_list, $opt_kw_with_aliases . $opt_type;
293             push @options, [$opt_kw_with_aliases, $opt_type, $label];
294             }
295            
296             # calculate the width of the option spec column so the labels,
297             # which we print as comment in the text output, will line up
298            
299             my $colwidth = 0;
300             foreach my $opt_aref (@options) {
301             my ($opt_kw_with_aliases, $opt_type, $label) = $opt_aref->@*;
302             my $len = length( q(') . $opt_kw_with_aliases . $opt_type . q(', ) );
303             $colwidth = $len if $len > $colwidth;
304             }
305            
306             my $fmt = sprintf '%%-%ds', $colwidth;
307            
308             foreach my $opt_aref (@options) {
309             my ($opt_kw_with_aliases, $opt_type, $label) = $opt_aref->@*;
310             my $kw = sprintf $fmt, q(') . $opt_kw_with_aliases . $opt_type . q(',);
311             push @options_text, $kw . '# ' . $label;
312             }
313            
314             return wantarray ? ( @options_list ) : join $NEWLINE, sort @options_text;
315             }
316            
317            
318             =head2 get_option_choices ( $option )
319            
320             Returns: \%choices
321            
322             Find all the options which have a multiple choice response, and return
323             a hash keyed on the option name and with a values consisting
324             of a hash of the valid responses as value/label pairs.
325            
326             =cut
327            
328             method get_option_choices :common () {
329             my %choices;
330            
331             foreach my $row ( $Tk_opt_table->@* ) {
332             next if ref $row ne 'ARRAY';
333            
334             # pick off the first three values, then slurp the rest
335             my ($opt_kw, $opt_type, $default, @others) = $row->@*;
336             # skip the group dividers
337             next if not $opt_kw;
338            
339             my $href;
340             if (@others and ref $others[0] eq 'HASH' ) {
341             $href = $others[0];
342             } elsif (@others % 2 == 0) {
343             $href = { @others };
344             } else {
345             croak "*E* unable to parse opttable: @others";
346             }
347            
348             my %hv;
349             if ( $href->{'choices'} and ref $href->{'choices'} eq 'ARRAY' ) {
350             foreach my $slot ( $href->{'choices'}->@* ) {
351             if (ref $slot eq 'ARRAY') {
352             $hv{ $slot->[1] } = $slot->[0];
353             }
354             elsif (ref $slot eq $EMPTY) {
355             $hv{ $slot } = $TRUE;
356             }
357             }
358             $choices{$opt_kw} = \%hv;
359             }
360             }
361            
362             return \%choices;
363             }
364            
365             =head2 get_option_defaults
366            
367             Returns: \%defaults
368            
369             Returns the option defaults, obtained from the same predefined list
370             of lists/strings returned by get_tk_options_table.
371            
372             =cut
373            
374             method get_option_defaults :common () {
375            
376             my %defaults = ();
377             foreach my $slot ($Tk_opt_table->@*) {
378             next if ref $slot ne 'ARRAY';
379             my $key = $slot->[0];
380             next if not $key;
381             my $default_value = $slot->[2];
382             $defaults{$key} = $default_value;
383             }
384            
385             return \%defaults;
386             }
387            
388             =head2 valid_report_type ($rt, \@opttable)
389            
390             This function is used to validate the report type. Valid values are
391             defined in the built-in Tk options table, which can be obtained by
392             calling:
393            
394             my @opttable = ( Weather::GHCN::Options->get_tk_options_table() );
395            
396             =cut
397            
398             method valid_report_type :common ($rt, $opttable_aref) {
399             my $choices_href = Weather::GHCN::Options->get_option_choices;
400             return $choices_href->{'report'}->{ lc $rt };
401             }
402            
403             =head2 deabbrev_report_type ($rt)
404            
405             The report types supported by the -report option can be abbrevated,
406             so long as the abbrevation is unambiquous. For example, 'daily' can
407             be abbreviated to 'dail', 'dai', or 'da', but not 'd' because 'detail'
408             is also a valid report type and 'd' would not disambiguate the two.
409            
410             This function takes a (possibly abbreviated) report type and returns
411             an unabbreviated report type.
412            
413             =cut
414            
415             method deabbrev_report_type :common ($rt) {
416             my %r_abbrev = abbrev( @REPORT_TYPE_LIST );
417             my $deabbreved = $r_abbrev{ lc $rt };
418             return $deabbreved;
419             }
420            
421             =head2 valid_refresh_option ($refresh, \@opttable)
422            
423             This function is used to validate the refresh option. Valid values are
424             defined in the built-in Tk options table, which can be obtained by
425             calling:
426            
427             my @opttable = ( Weather::GHCN::Options->get_tk_options_table() );
428            
429             =cut
430            
431             method valid_refresh_option :common ($refresh, $opttable_aref) {
432             my $choices_href = Weather::GHCN::Options->get_option_choices;
433             # we only validate the non-numeric options
434             return $TRUE if $refresh =~ m{ \A \d+ \Z }xms;
435             return $choices_href->{'refresh'}->{ lc $refresh };
436             }
437            
438             =head2 deabbrev_refresh_option ($refresh)
439            
440             The refresh option values can be abbrevated, so long as the abbrevation
441             is unambiquous. For example, 'yearly' can
442             be abbreviated to 'y', 'ye', 'yea', etc.
443            
444             This function takes a (possibly abbreviated) refresh option and returns
445             an unabbreviated refresh option.
446            
447             =cut
448            
449             method deabbrev_refresh_option :common ($refresh) {
450             # we only deabbreviate the non-numeric options
451             return $refresh if $refresh =~ m{ \A \d+ \Z }xms;
452             my %r_abbrev = abbrev( qw(yearly never always) );
453             my $deabbreved = $r_abbrev{ lc $refresh };
454             return $deabbreved;
455             }
456            
457            
458             ######################################################################
459             =head1 INSTANCE METHODS
460            
461             =over 4
462            
463             =item combine_options ( $user_opt_href, $profile_href={} )
464            
465             Returns: ($opt_href, $opt_obj)
466            
467             This method takes a hash reference containing user options, and optionally
468             a hash reference of profile options, and combines them with default
469             values. The end result is a complete set of all the options
470             supported by Weather::GHCN::StationTable with user-specified options taking
471             precedence over profile options, and profile options taking precedence
472             over defaults.
473            
474             This set of options is returned as both a hash reference and as a
475             Hash::Wrap object. The latter is preferred for use by consuming
476             applications, because it provides accessor methods for each option.
477             In addition, an ->defined( "
478             your code can determine whether an option value was set to B.
479            
480             The advantage to using an option object rather than an option hash
481             is that a misspelled option name will cause a runtime error.
482            
483             =back
484            
485             =cut
486            
487 124     124 1 197971 method combine_options ( $user_opt_href, $profile_href={} ) {
  124         224  
  124         223  
  124         203  
  124         173  
488             # assign the class-level tk_options_table aref, generated before BUILD, to the instance field
489 124         208 $_tk_opt_aref = $Tk_opt_table;
490            
491             # start with the user options
492 124         476 my %merged_options = ( $user_opt_href->%* );
493            
494             # merge in the profile options
495 124         544 while ( my ($k,$v) = each $profile_href->%* ) {
496 8   33     76 $merged_options{$k} //= $v;
497             }
498            
499 124         324 my $defaults_href = get_option_defaults();
500            
501             # merge in the defaults
502 124         554 while ( my ($k,$v) = each $defaults_href->%* ) {
503 3100   100     10807 $merged_options{$k} //= $v;
504             }
505            
506 124         290 $_opt_href = \%merged_options;
507 124         3318 $_opt_obj = _wrap_hash \%merged_options;
508            
509 124         2376 return ($_opt_href, $_opt_obj);
510             }
511            
512             =head2 initialize
513            
514             Returns: @errors
515            
516             This method initializes options that can't simply be initialized by
517             constants. Specifically:
518            
519             =over 4
520            
521             =item Aliases
522            
523             Alias entries defined in the user profile are matched against
524             the -location option value. If a match is found to the alias name,
525             the alias value is substituted for the location value.
526            
527             Alias names must be lowercase letters only. An optional underscore
528             prefix is permitted. Names not matching this rule will be silently
529             ignored by initialize().
530            
531             =item country
532            
533             The B option value can be:
534            
535             * a 2-character GEC (FIPS) country code
536            
537             * a 3-character alpha ISO 3166 country code
538            
539             * a 3-digit numeric ISO 3166 country number
540            
541             * an internet domain country suffix (e.g. '.ca')
542            
543             * a 3-character regex string
544            
545             If a regex string is given, then it will be matched (unanchored and
546             case insensitve) against country names. If multiple matches are
547             found, then an error is returned and the user will need to provide a
548             more specific pattern.
549            
550             =item active
551            
552             The B option filters stations according to the years that
553             they were active. If the B option is specified, but the
554             B option is not, then B will set the B
555             option value to the B option value so that only stations that
556             were active during the requested data range will be selected.
557            
558             =item quality
559            
560             The B option determines whether a station's data will be
561             included in the output when it has missing data. Quality is
562             expressed as a number between 0 and 100, representing the percentage
563             of data that cannot be missing; 90% is the default For example, if
564             you have a range of 3 years (1095 days) when B is 90, then
565             you need 90% x 1095 = 985 days of data. Anything less and the
566             station is rejected.
567            
568             When filters fmonth and fday are used, the amount of data included
569             will typically drop far below 90% thereby rejecting all stations.
570             To avoid this nuisance, B will set quality to 0% if
571             either the B or B options are present.
572            
573             =back
574            
575             =cut
576            
577 124     124 1 379 method initialize () {
  124         214  
  124         182  
578 124         191 my @errors;
579            
580 124 100       2152 if ( $_opt_obj->country ) {
581             # using undef as the search type so it will figure it out based
582             # on the value pattern and length
583 8         213 my @cou = search_country( $_opt_obj->country, undef );
584            
585 8 100       40 push @errors, '*E* unrecognized country code or name'
586             if not @cou;
587            
588 8 100       36 push @errors, '*E* ambiguous country code or name'
589             if @cou > 1;
590            
591             # return the GEC (FIPS) country code, which is what GHCN uses
592 8         222 $_opt_obj->country = $cou[0]->{gec};
593             }
594            
595             # default the station active range to the year filter range if its value is an empty string
596 124 100 100     4717 if ( $_opt_obj->defined('active') and $_opt_obj->active eq $EMPTY ) {
597 5         256 $_opt_obj->active = $_opt_obj->range;
598             }
599            
600 124 100 100     3315 $_opt_obj->quality = 0
601             if $_opt_obj->fmonth or $_opt_obj->fday;
602            
603 124         7269 return @errors;
604             }
605            
606             =head2 options_as_string
607            
608             This option returns a string that contains all the options and their
609             values, in a format similar to what they would look like when entered
610             as command-line arguments. For boolean options only the option name
611             is include (no value). Option values containing whitespace are
612             enclosed in double quotes. Option/value pairs are separated by
613             two spaces.
614            
615             This method is primarily provided so the consuming application can
616             print the options that were used during a run, perhaps to a log or
617             in the output.
618            
619             =cut
620            
621 6     6 1 467 method options_as_string () {
  6         15  
  6         12  
622 6         11 my @options;
623 6         26 my $boolean = _get_boolean_options($Tk_opt_table);
624            
625 6         95 foreach my $k ( sort keys $_opt_href->%* ) {
626 153 100       270 next if $k eq 'aliases';
627 150 100       280 next if $k eq 'cachedir';
628 144 100       235 next if $k eq 'profile';
629 138         218 my $v = $_opt_href->{$k};
630 138 100       248 next if not defined $v;
631            
632 49 100       98 if ( $boolean->{$k} ) {
633 2         6 push @options, $DASH . $k;
634 2         5 next;
635             }
636            
637 47         74 my $val = $v;
638            
639 47 50       141 if ( $val =~ m{\A \s* \Z}xms ) {
640 0         0 $val = q(") . $val . q(");
641             }
642 47         128 push @options, $DASH . $k. $SPACE . $val;
643             }
644 6         79 return join $SPACE x 2, @options;
645             }
646            
647             =head2 validate
648            
649             Returns: @errors
650            
651             This method is called by StationTable->set_options to make sure all
652             the options that were provided to B are valid. It also
653             handles abbreviations for options color and report. Any errors
654             arising from invalid value or from problems detected during
655             B (which is called at the end of B) are returned
656             in a list.
657            
658             =cut
659            
660 124     124 1 4610 method validate () {
  124         200  
  124         173  
661 124         204 my @errors;
662 124         197 my $bad_range_cnt = 0;
663            
664 124 100       2528 if ( $_opt_obj->defined('aliases') ) {
665 9         239 foreach my $alias_name ( keys $_opt_obj->aliases->%* ) {
666 23         1679 my $errmsg = '*E* alias names in profile must be lowercase letters with optional underscore prefix: ' . $alias_name;
667 23 100       166 push @errors, $errmsg
668             unless $alias_name =~ $ALIAS_NAME_RE;
669             }
670             }
671            
672 124 100       2598 if ( $_opt_obj->active ) {
673 24 100       640 if ( not $_opt_obj->active =~ m{ \A (18|19|20)\d\d [-] (18|19|20)\d\d }xms ) {
674 7         183 push @errors, '*E* invalid -active year range ' . $_opt_obj->active;
675 7         74 $bad_range_cnt++;
676             }
677             }
678            
679 124 100       4668 if ( $_opt_obj->range ) {
680 33 100       883 if ( not $_opt_obj->range =~ m{ \A (18|19|20)\d\d [-,] (18|19|20)\d\d }xms ) {
681 7         185 push @errors, '*E* invalid -range ' . $_opt_obj->range;
682 7         74 $bad_range_cnt++;
683             }
684             }
685            
686 124 100 100     4413 push @errors, '*E* invalid 2-character state or province code ' . $_opt_obj->state
687             if $_opt_obj->defined('state') and not $_opt_obj->state =~ m{ \A [[:alpha:]]{2} \Z }xms;
688            
689 124 100 100     3505 push @errors, '*E* -partial only allowed if -active specified'
690             if $_opt_obj->partial and not $_opt_obj->defined('active');
691            
692             # Note: full Condition Coverage in Devel::Cover seems impossible if these two ifs are combined
693             # (I tried every combination of uncoverable branch and condition I could think of to
694             # to suppress the missing case. In the end, this was the only thing that worked.)
695 124 100 100     4520 if ( $_opt_obj->range and $_opt_obj->active ) {
696             # uncoverable branch false
697 9 50       317 if ( $bad_range_cnt == 0 ) {
698 9         135 my $r = rng_new( $_opt_obj->range );
699 9         197 my $a = rng_new( $_opt_obj->active );
700            
701 9 100       29 push @errors, '*E* -range must be a subset of -active'
702             if not $r->subset($a);
703             }
704             }
705            
706 124 100 100     7472 push @errors, '*E* -gps argument must be decimal lat/long, separated by spaces or punctuation'
707             if $_opt_obj->gps and $_opt_obj->gps !~ m{ \A [+-]? \d{1,3} [.] \d+ (?: [[:punct:]] | \s+ ) [+-]? \d{1,3} [.] \d+ \Z }xms;
708            
709 124         3176 my %report_abbrev = abbrev( @REPORT_TYPE_LIST );
710            
711 124         28411 my $report = lc $_opt_obj->report;
712            
713             # uncoverable branch true
714 124 50       3039 croak '*E* undef report type'
715             if not defined $report;
716            
717             push @errors, '*E* invalid report option: ' . $report
718 124 100 100     431 if $report and not $report_abbrev{ $report };
719            
720 124         2080 $_opt_obj->report = $report_abbrev{ $report };
721            
722            
723 124         1401 my %refresh_abbrev = abbrev( qw(yearly never always) );
724            
725 124         14754 my $refresh = lc $_opt_obj->refresh;
726            
727             # uncoverable branch true
728 124 50       1948 croak '*E* undef refresh option'
729             if not defined $refresh;
730            
731             push @errors, '*E* invalid refresh option: ' . $refresh
732 124 50 33     601 if $refresh and not $refresh_abbrev{ $refresh };
733            
734 124         2041 $_opt_obj->refresh = $refresh_abbrev{ $refresh };
735            
736             #-----------------------------------------------------------------
737             # end of noted section
738             #-----------------------------------------------------------------
739            
740 124         1374 my %color_abbrev = abbrev( qw(blue green azure purple red white yellow) );
741            
742             # uncoverable branch false
743 124 50       24687 if ( $_opt_obj->defined('kmlcolor') ) {
744 124         2577 my $kmlcolor = $_opt_obj->kmlcolor;
745 124 100       2912 if ( $kmlcolor eq $EMPTY ) {
746 1         3 push @errors, '*E* invalid -kmlcolor value ""'
747             } else {
748             push @errors, '*E* invalid -kmlcolor value'
749 123 100       331 if not $color_abbrev{ $kmlcolor };
750             }
751 124         1941 $_opt_obj->kmlcolor = $color_abbrev{ $kmlcolor };
752             }
753            
754            
755 124 100       2954 if ( $_opt_obj->defined('fmonth') ) {
756 15 100 100     355 push @errors, '*E* -fmonth must be a single number or valid range spec (e.g. 1-5,9)'
757             if not rng_valid($_opt_obj->fmonth)
758             or not rng_within($_opt_obj->fmonth, '1-12');
759             }
760            
761 124 100       9041 if ( $_opt_obj->defined('fday') ) {
762 15 100 100     321 push @errors, '*E* -fday must be a single number or valid range spec (e.g. 3,15,20-31)'
763             if not rng_valid($_opt_obj->fday)
764             or not rng_within($_opt_obj->fday, '1-31');
765             }
766            
767 124         7204 my @init_errors = $self->initialize();
768            
769 124         1869 return (@errors, @init_errors);
770             }
771            
772             =head2 DOES
773            
774             Defined by Object::Pad. Included for POD::Coverage.
775            
776             =head2 META
777            
778             Defined by Object::Pad. Included for POD::Coverage.
779            
780             =cut
781            
782             ######################################################################
783             # Subroutines
784             ######################################################################
785            
786 7     7   17 sub _get_boolean_options ($_tk_opt_aref) {
  7         13  
  7         16  
787            
788 7         12 my %boolean;
789            
790 7         23 foreach my $row ( $_tk_opt_aref->@* ) {
791 231 100       435 next unless ref $row eq 'ARRAY';
792 196         467 my ($name, $type) = $row->@*;
793 196 100       462 $boolean{$name}++ if $type eq $BANG;
794             }
795            
796 7         29 return \%boolean;
797             }
798            
799             1;