File Coverage

blib/lib/App/olson.pm
Criterion Covered Total %
statement 86 176 48.8
branch 16 74 21.6
condition 7 17 41.1
subroutine 21 43 48.8
pod 1 1 100.0
total 131 311 42.1


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             App::olson - query the Olson timezone database
4              
5             =head1 SYNOPSIS
6              
7             olson list ... ...
8             olson version
9              
10             =head1 DESCRIPTION
11              
12             This module implements the L command-line utility. See L
13             for details of usage.
14              
15             =head1 FUNCTIONS
16              
17             =over
18              
19             =item App::olson::run(@ARGV)
20              
21             Performs the job of the L program. The interface to this function
22             may change in the future.
23              
24             =back
25              
26             =cut
27              
28             package App::olson;
29              
30 3     3   60985 { use 5.006; }
  3         12  
  3         139  
31 3     3   16 use warnings;
  3         5  
  3         89  
32 3     3   15 use strict;
  3         18  
  3         147  
33              
34 3     3   25013 use Date::ISO8601 0.000 qw(ymd_to_cjdn present_ymd);
  3         14042  
  3         298  
35 3         277 use DateTime::TimeZone::Olson 0.003 qw(
36             olson_version olson_all_names olson_canonical_names olson_links
37             olson_country_selection olson_tz
38 3     3   3095 );
  3         8664  
39 3     3   3297 use DateTime::TimeZone::SystemV 0.002 ();
  3         17263  
  3         145  
40 3     3   2993 use DateTime::TimeZone::Tzfile 0.007 ();
  3         56684  
  3         105  
41 3     3   27 use Params::Classify 0.000 qw(is_string);
  3         41  
  3         153  
42 3     3   16 use Time::OlsonTZ::Data 0.201012 ();
  3         51  
  3         74  
43 3     3   2818 use Time::Unix 1.02 qw(time);
  3         11200  
  3         807  
44              
45             our $VERSION = "0.000";
46              
47             #
48             # list utilities
49             #
50              
51             sub _all(&@) {
52 0     0   0 my $match = shift(@_);
53 0         0 foreach(@_) {
54 0 0       0 return 0 unless $match->($_);
55             }
56 0         0 return 1;
57             }
58              
59             #
60             # exceptions
61             #
62              
63             sub _is_exception($) {
64 0   0 0   0 return is_string($_[0]) && $_[0] =~ /\A[!?~]\z/;
65             }
66              
67 0     0   0 sub _cmp_exception($$) { $_[0] cmp $_[1] }
68              
69             #
70             # calendar dates
71             #
72              
73             sub _caltime_offset($$) {
74 0     0   0 my($rdns, $offset) = @_;
75 0 0       0 return $rdns if _is_exception($rdns);
76 0 0       0 return $offset if _is_exception($offset);
77 0         0 my($rdn, $sod) = @$rdns;
78 0         0 $sod += $offset;
79 3     3   24 use integer;
  3         6  
  3         21  
80 0 0       0 my $doff = $sod < 0 ? -((86399-$sod) / 86400) : $sod / 86400;
81 0         0 $rdn += $doff;
82 0         0 $sod -= 86400*$doff;
83 0         0 return [$rdn, $sod];
84             }
85              
86             #
87             # querying timezones
88             #
89              
90             my %translate_exception = (
91             "zone disuse" => "!",
92             "missing data" => "?",
93             "offset change" => "~",
94             );
95              
96             sub _handle_exception($$$) {
97 0     0   0 my($val, $expect_rx, $err) = @_;
98 0 0       0 if($err eq "") {
    0          
99 0         0 return $val;
100             } elsif($err =~ /\A
101             $expect_rx\ in\ the\ [!-~]+\ timezone
102             \ due\ to\ (offset\ change|zone\ disuse|missing\ data)\b
103             /x) {
104 0         0 return $translate_exception{$1};
105             } else {
106 0         0 die $err;
107             }
108             }
109              
110             {
111             package App::olson::UtcDateTime;
112             sub new {
113 9     9   49 my($class, $rdns) = @_;
114 9         43 return bless({ rdn => $rdns->[0], sod => $rdns->[1] }, $class);
115             }
116 0     0   0 sub utc_rd_values { ($_[0]->{rdn}, $_[0]->{sod}, 0) }
117             }
118              
119             sub _handle_forward_exception($$) {
120 0     0   0 return _handle_exception($_[0],
121             qr/time [-:TZ0-9]+ is not represented/, $_[1]);
122             }
123              
124             {
125             package App::olson::LocalDateTime;
126             sub new {
127 0     0   0 my($class, $rdns) = @_;
128 0         0 return bless({ rdn => $rdns->[0], sod => $rdns->[1] }, $class);
129             }
130 0     0   0 sub local_rd_values { ($_[0]->{rdn}, $_[0]->{sod}, 0) }
131             }
132              
133             sub _handle_backward_exception($$) {
134 0     0   0 return _handle_exception($_[0],
135             qr/local time [-:T0-9]+ does not exist/, $_[1]);
136             }
137              
138             #
139             # data type metadata
140             #
141              
142             our %type;
143              
144             $type{string} = {
145             desc => "string",
146             present => sub { ${$_[0]} },
147             present_exception_width => 5,
148             cmp => sub { ${$_[0]} cmp ${$_[1]} },
149             };
150              
151             $type{zone_name} = {
152             desc => "timezone name",
153             present => sub { $_[0] },
154             present_exception_width => 5,
155             present_field_width => 32,
156             rx => qr#[\+\-0-9A-Z_a-z]+(?:/[\+\-0-9A-Z_a-z]+)*#,
157             parse => sub { $_[0] },
158             cmp => sub { $_[0] cmp $_[1] },
159             };
160              
161 2   66 2   13 { my $areas; sub _areas() { $areas ||= do {
162 1         2 my %areas;
163 1         2 foreach my $country (values %{olson_country_selection()}) {
  1         7  
164 247         10361 foreach my $region (values %{$country->{regions}}) {
  247         460  
165 415 50       1471 $areas{$1} = undef
166             if $region->{timezone_name} =~ m#\A([^/]+)/#;
167             }
168             }
169 1         37 \%areas;
170             } } }
171              
172             $type{area_name} = {
173             desc => "area name",
174             present => sub { $_[0] },
175             present_exception_width => 5,
176             present_field_width => 10,
177             rx => qr/[A-Za-z]+/,
178             parse => sub { ucfirst(lc($_[0])) },
179             cmp => sub { $_[0] cmp $_[1] },
180             };
181              
182             my $rdn_epoch_cjdn = 1721425;
183              
184             sub _present_caltime($) {
185 0     0   0 my($rdns) = @_;
186 0         0 my($rdn, $sod) = @$rdns;
187 3     3   2810 use integer;
  3         5  
  3         13  
188 0         0 return present_ymd($rdn + $rdn_epoch_cjdn).
189             "T".sprintf("%02d:%02d:%02d", $sod/3600, $sod/60%60, $sod%60);
190             }
191              
192             my $caltime_rx = qr/
193             [0-9]{4}
194             (?:-[0-9]{2}
195             (?:-[0-9]{2}
196             (?:(?:\ +|\ *[Tt]\ *)[0-9]{2}
197             (?::[0-9]{2}
198             (?::[0-9]{2}
199             )?)?)?)?)?
200             |
201             [0-9]{4}
202             (?:[0-9]{2}
203             (?:[0-9]{2}
204             (?:\ *(?:[Tt]\ *)?[0-9]{2}
205             (?:[0-9]{2}
206             (?:[0-9]{2}
207             )?)?)?)?)?
208             /x;
209              
210             sub _parse_caltime($) {
211 0     0   0 my($txt) = @_;
212 0         0 my($y, $mo, $d, $h, $mi, $s) = ($txt =~ /\A
213             ([0-9]{4})
214             (?:.*?([0-9]{2})
215             (?:.*?([0-9]{2})
216             (?:.*?([0-9]{2})
217             (?:.*?([0-9]{2})
218             (?:.*?([0-9]{2})
219             )?)?)?)?)?
220             /sx);
221 0 0       0 $mo = "01" unless defined $mo;
222 0 0       0 $d = "01" unless defined $d;
223 0         0 my $rdn = eval {
224 0         0 local $SIG{__DIE__};
225 0         0 ymd_to_cjdn($y, $mo, $d) - $rdn_epoch_cjdn;
226             };
227 0 0       0 if($@ ne "") {
228 0         0 my $err = $@;
229 0         0 $err =~ s/ at .*\z/\n/s;
230 0         0 die $err;
231             }
232 0 0       0 $h = "00" unless defined $h;
233 0 0       0 $mi = "00" unless defined $mi;
234 0 0       0 $s = "00" unless defined $s;
235 0 0       0 die "hour number $h is out of the range [0, 24)\n" unless $h < 24;
236 0 0       0 die "minute number $mi is out of the range [0, 60)\n" unless $mi < 60;
237 0 0       0 die "second number $s is out of the range [0, 60)\n" unless $s < 60;
238 0         0 return [ $rdn, 3600*$h + 60*$mi + $s ];
239             }
240              
241             $type{calendar_time} = {
242             desc => "calendar time",
243             present => \&_present_caltime,
244             present_exception_width => 19,
245             rx => $caltime_rx,
246             parse => \&_parse_caltime,
247             cmp => sub { $_[0]->[0] <=> $_[1]->[0] || $_[0]->[1] <=> $_[1]->[1] },
248             };
249              
250             my $unix_epoch_rdn = 719163;
251              
252             my $now_absolute_time;
253             sub _now_absolute_time() {
254 10   66 10   60 return $now_absolute_time ||= do {
255 1         6 my $nowu = time;
256 1         23 [ int($nowu/86400) + $unix_epoch_rdn, $nowu % 86400 ];
257             };
258             }
259              
260             $type{absolute_time} = {
261             desc => "absolute time",
262             present => sub { _present_caltime($_[0])."Z" },
263             present_exception_width => 20,
264             rx => qr/(?:(?:$caltime_rx) *[Zz]|now)/o,
265             parse => sub {
266             if($_[0] eq "now") {
267             return _now_absolute_time();
268             } else {
269             return _parse_caltime($_[0]);
270             }
271             },
272             cmp => $type{calendar_time}->{cmp},
273             };
274              
275             $type{country_code} = {
276             desc => "country code",
277             present => sub { $_[0] },
278             present_exception_width => 2,
279             rx => qr/[A-Za-z]{2}/,
280             parse => sub { uc($_[0]) },
281             cmp => sub { $_[0] cmp $_[1] },
282             };
283              
284             $type{initialism} = {
285             desc => "initialism",
286             present => sub { $_[0] },
287             present_exception_width => 3,
288             present_field_width => 6,
289             rx => qr/[\+\-0-9A-Za-z]{3,}/,
290             parse => sub { $_[0] },
291             cmp => sub { $_[0] cmp $_[1] },
292             };
293              
294             $type{offset} = {
295             desc => "offset",
296             present => sub {
297             my($offset) = @_;
298             my $sign = $offset < 0 ? "-" : "+";
299             $offset = abs($offset);
300 3     3   2822 use integer;
  3         68  
  3         13  
301             my $disp = sprintf("%s%02d:%02d:%02d", $sign,
302             $offset/3600, $offset/60%60, $offset%60);
303             $disp =~ s/(?::00)+\z//;
304             return $disp;
305             },
306             present_exception_width => 3,
307             present_field_width => 9,
308             rx => qr/[-+][0-9]{2}
309             (?:[0-9]{2}(?:[0-9]{2})?|:[0-9]{2}(?::[0-9]{2})?)?
310             /x,
311             parse => sub {
312             my($txt) = @_;
313             my($sign, $h, $m, $s) = ($txt =~ /\A
314             ([-+])
315             ([0-9]{2})
316             (?:.*?([0-9]{2})
317             (?:.*?([0-9]{2})
318             )?)?
319             /sx);
320             $m = 0 unless defined $m;
321             $s = 0 unless defined $s;
322             die "minute number $m is out of the range [0, 60)\n"
323             unless $m < 60;
324             die "second number $s is out of the range [0, 60)\n"
325             unless $s < 60;
326             return (3600*$h + 60*$m + $s) * ($sign eq "-" ? -1 : +1);
327             },
328             cmp => sub { $_[0] <=> $_[1] },
329             };
330              
331             $type{truth} = {
332             desc => "truth value",
333             present => sub { $_[0] ? "+" : "-" },
334             rx => qr/[-+]/,
335             parse => sub { $_[0] eq "+" ? 1 : 0 },
336             cmp => sub { $_[0] <=> $_[1] },
337             };
338              
339             sub _type_parse_from_gmatch($$) {
340 10     10   15 my($type, $rtxt) = @_;
341 10 50       28 my $typerx = $type->{rx} or die "can't input a @{[$type->{desc}]}\n";
  0         0  
342 10 50       37 $$rtxt =~ /\G(
343             [\+\-\/0-9\:A-Z_a-z]
344             (?:[\ \+\-\/0-9\:A-Z_a-z]*[\+\-\/0-9\:A-Z_a-z])?
345             )/xgc or die "missing value\n";
346 10         20 my $valtxt = $1;
347 10 50       136 $valtxt =~ /\A$typerx\z/ or die "malformed @{[$type->{desc}]}\n";
  0         0  
348 10         27 return $type->{parse}->($valtxt);
349             }
350              
351             sub _type_curry_xpresent($) {
352 0     0   0 my($type) = @_;
353 0 0       0 my $pew = exists($type->{present_exception_width}) ?
354             $type->{present_exception_width} : 1;
355 0 0       0 my $pfw = exists($type->{present_field_width}) ?
356             $type->{present_field_width} : 0;
357             return $type->{t_present} ||= sub {
358 0     0   0 my($value) = @_;
359 0 0       0 my $txt = _is_exception($value) ?
360             $value x $pew : $type->{present}->($value);
361 0 0       0 $txt .= " " x ($pfw - length($txt)) if $pfw > length($txt);
362 0         0 return $txt;
363 0   0     0 };
364             }
365              
366             sub _type_curry_xcmp($) {
367 0     0   0 my($type) = @_;
368 0         0 my $cmp_normal = $type->{cmp};
369             return $type->{t_cmp} ||= sub {
370 0     0   0 my($x, $y) = @_;
371 0 0       0 if(_is_exception($x)) {
372 0 0       0 if(_is_exception($y)) {
373 0         0 return _cmp_exception($x, $y);
374             } else {
375 0         0 return -1;
376             }
377             } else {
378 0 0       0 if(_is_exception($y)) {
379 0         0 return +1;
380             } else {
381 0         0 return $cmp_normal->($x, $y);
382             }
383             }
384 0   0     0 };
385             }
386              
387             #
388             # timezone attribute classes
389             #
390              
391             our %attrclass;
392              
393             $attrclass{z} = $attrclass{zone_name} = {
394             desc => "timezone name",
395             params => {},
396             type => "zone_name",
397             check_value => sub {
398             die "no such timezone `$_[0]'\n"
399             unless exists olson_all_names()->{$_[0]};
400             },
401             cost => 0,
402             get_needs => { z=>undef },
403             curry_get => sub { sub { $_[0]->{z} } },
404             };
405              
406             $attrclass{a} = $attrclass{area_name} = {
407             desc => "area name",
408             params => {},
409             type => "area_name",
410             check_value => sub {
411             die "no such area `$_[0]'\n" unless exists _areas()->{$_[0]};
412             },
413             cost => 1,
414             get_needs => { z=>undef },
415             curry_get => sub {
416             my $areas = join("|", map { "\Q$_\E" } keys %{_areas()});
417             my $arearx = qr#\A($areas)/#o;
418             return sub { $_[0]->{z} =~ $arearx ? "$1" : "!" };
419             },
420             };
421              
422             $attrclass{c} = $attrclass{country_code} = {
423             desc => "country code",
424             params => {},
425             type => "country_code",
426             check_value => sub {
427             die "no such country code `$_[0]'\n"
428             unless exists olson_country_selection()->{$_[0]};
429             },
430             cost => 0,
431             get_needs => { c=>undef },
432             curry_get => sub { sub { $_[0]->{c} } },
433             };
434              
435             $attrclass{cn} = $attrclass{country_name} = {
436             desc => "country name",
437             params => {},
438             type => "string",
439             cost => 1,
440             get_needs => { c=>undef },
441             curry_get => sub {
442             my $sel = olson_country_selection();
443             return sub { \$sel->{$_[0]->{c}}->{olson_name} };
444             },
445             };
446              
447             $attrclass{rd} = $attrclass{region_description} = {
448             desc => "region description",
449             params => {},
450             type => "string",
451             cost => 1,
452             get_needs => { region=>undef },
453             curry_get => sub { sub { \$_[0]->{region}->{olson_description} } },
454             };
455              
456             $attrclass{k} = $attrclass{canonical_zone_name} = {
457             desc => "canonical timezone name",
458             params => {},
459             type => "zone_name",
460             check_value => sub {
461             die "no such canonical timezone `$_[0]'\n"
462             unless exists olson_canonical_names()->{$_[0]};
463             },
464             cost => 1,
465             get_needs => { z=>undef },
466             curry_get => sub {
467             my $links = olson_links();
468             return sub {
469             my $z = $_[0]->{z};
470             return exists($links->{$z}) ? $links->{$z} : $z;
471             };
472             },
473             };
474              
475             $attrclass{o} = $attrclass{offset} = {
476             desc => "offset",
477             params => { "\@" => "absolute_time" },
478             type => "offset",
479             cost => 10,
480             get_needs => { z=>undef },
481             curry_get => sub {
482             my($when) = $_[0]->{"\@"};
483             my $whendt = App::olson::UtcDateTime->new($when);
484             return sub {
485             my $zone = olson_tz($_[0]->{z});
486             return _handle_forward_exception(eval {
487             local $SIG{__DIE__};
488             0+$zone->offset_for_datetime($whendt);
489             }, $@);
490             };
491             },
492             };
493              
494             $attrclass{i} = $attrclass{initialism} = {
495             desc => "initialism",
496             params => { "\@" => "absolute_time" },
497             type => "initialism",
498             cost => 10,
499             get_needs => { z=>undef },
500             curry_get => sub {
501             my($when) = $_[0]->{"\@"};
502             my $whendt = App::olson::UtcDateTime->new($when);
503             return sub {
504             my $zone = olson_tz($_[0]->{z});
505             return _handle_forward_exception(eval {
506             local $SIG{__DIE__};
507             $zone->short_name_for_datetime($whendt);
508             }, $@);
509             };
510             },
511             };
512              
513             $attrclass{d} = $attrclass{dst_status} = {
514             desc => "DST status",
515             params => { "\@" => "absolute_time" },
516             type => "truth",
517             cost => 10,
518             get_needs => { z=>undef },
519             curry_get => sub {
520             my($when) = $_[0]->{"\@"};
521             my $whendt = App::olson::UtcDateTime->new($when);
522             return sub {
523             my $zone = olson_tz($_[0]->{z});
524             return _handle_forward_exception(eval {
525             local $SIG{__DIE__};
526             $zone->is_dst_for_datetime($whendt) ? 1 : 0;
527             }, $@);
528             };
529             },
530             };
531              
532             $attrclass{t} = $attrclass{local_time} = {
533             desc => "local time",
534             params => { "\@" => "absolute_time" },
535             type => "calendar_time",
536             cost => 11,
537             get_needs => { z=>undef },
538             curry_get => sub {
539             my($when) = $_[0]->{"\@"};
540             my $get_offs = $attrclass{offset}->{curry_get}->($_[0]);
541             return sub { _caltime_offset($when, $get_offs->($_[0])) };
542             },
543             };
544              
545             sub _parse_attribute_from_gmatch($) {
546 28     28   18085 my($rtxt) = @_;
547 28 100       139 $$rtxt =~ /\G([a-zA-Z0-9_]+)/gc or die "missing attribute name\n";
548 26         55 my $classname = $1;
549 26 100       4848 my $ac = $attrclass{$classname}
550             or die "no such attribute class `$classname'\n";
551 24         30 my %pval;
552 24         90 while($$rtxt =~ /\G *([\@]) */gc) {
553 12         20 my $pkey = $1;
554 12 100       38 die "clashing `$pkey' parameters for ".
555 1         8 "@{[$ac->{desc}]} attribute\n"
556             if exists $pval{$pkey};
557 11 100       38 my $ptype = $ac->{params}->{$pkey}
558             or die "unwanted `$pkey' parameter for ".
559 1         10 "@{[$ac->{desc}]} attribute\n";
560 10         27 $pval{$pkey} = _type_parse_from_gmatch($type{$ptype}, $rtxt);
561             }
562 22         25 foreach my $pkey (keys %{$ac->{params}}) {
  22         79  
563 10 100       35 die "@{[$ac->{desc}]} attribute needs a `$pkey' parameter\n"
  1         8  
564             unless exists $pval{$pkey};
565             }
566 21         68 my $get = $ac->{curry_get}->(\%pval);
567             return {
568             type => $type{$ac->{type}},
569 0     0   0 check_value => $ac->{check_value} || sub { },
570             cost => $ac->{cost},
571             get_needs => $ac->{get_needs},
572             xget => sub {
573 4     4   4112 foreach(keys %{$ac->{get_needs}}) {
  4         16  
574 4 50       19 return "!" unless exists $_[0]->{$_};
575             }
576 4         13 return &$get;
577             },
578 21   100     253 };
579             }
580              
581             my %cmp_ok = (
582             "<" => sub { $_[0] < 0 },
583             ">" => sub { $_[0] > 0 },
584             "<=" => sub { $_[0] <= 0 },
585             ">=" => sub { $_[0] >= 0 },
586             "=" => sub { $_[0] == 0 },
587             );
588              
589             sub _parse_criterion_from_gmatch($) {
590 0     0   0 my($rtxt) = @_;
591 0         0 my $attr = _parse_attribute_from_gmatch($rtxt);
592 0 0       0 $$rtxt =~ /\G *(!)?([<>]=?|=|\?)/gc
593             or die "syntax error in criterion\n";
594 0         0 my($neg, $op) = ($1, $2);
595 0         0 my $get = $attr->{xget};
596 0         0 my $posxmatch;
597 0 0       0 if($op eq "?") {
598 0     0   0 $posxmatch = sub { !_is_exception(&$get) };
  0         0  
599             } else {
600 0         0 my $cmpok = $cmp_ok{$op};
601 0         0 $$rtxt =~ /\G +/gc;
602 0         0 my $cmpval = _type_parse_from_gmatch($attr->{type}, $rtxt);
603 0         0 $attr->{check_value}->($cmpval);
604 0         0 my $cmp = $attr->{type}->{cmp};
605             $posxmatch = sub {
606 0     0   0 my $val = &$get;
607 0 0       0 return 0 if _is_exception($val);
608 0         0 return $cmpok->($cmp->($val, $cmpval));
609 0         0 };
610             }
611             return {
612             cost => $attr->{cost},
613             match_needs => $attr->{get_needs},
614 0 0   0   0 xmatch => $neg ? sub { !&$posxmatch } : $posxmatch,
  0         0  
615             };
616             }
617              
618             #
619             # top-level commands
620             #
621              
622             my %command;
623              
624             $command{version} = sub {
625             die "bad arguments\n" if @_;
626             print "modules:\n";
627             foreach my $mod (qw(
628             App::olson
629             DateTime::TimeZone::Olson
630             DateTime::TimeZone::SystemV
631             DateTime::TimeZone::Tzfile
632             Time::OlsonTZ::Data
633             )) {
634 3     3   10002 no strict "refs";
  3         7  
  3         4647  
635             print " $mod ${qq(${mod}::VERSION)}\n";
636             }
637             print "Olson database: @{[olson_version]}\n";
638             };
639              
640             $command{list} = sub {
641             my(@criteria, @output_attrs, @display_attrs, @sort_attrs);
642             foreach my $arg (@_) {
643             if($arg =~ /\A *[-+]/) {
644             pos($arg) = undef;
645             $arg =~ /\G *([-+]) */gc;
646             my $neg = $1 eq "-";
647             my $attr = _parse_attribute_from_gmatch(\$arg);
648             $arg =~ /\G *\z/gc
649             or die "syntax error in sort directive\n";
650             push @output_attrs, $attr;
651             push @sort_attrs, { index=>$#output_attrs, neg=>$neg };
652             next;
653             }
654             if($arg =~ /\A *[0-9A-Z_a-z]/) {
655             pos($arg) = undef;
656             $arg =~ /\G +/gc;
657             my $attr = _parse_attribute_from_gmatch(\$arg);
658             if($arg =~ /\G *\z/gc) {
659             push @output_attrs, $attr;
660             push @display_attrs, $#output_attrs;
661             next;
662             }
663             }
664             pos($arg) = undef;
665             $arg =~ /\G +/gc;
666             my $crit = _parse_criterion_from_gmatch(\$arg);
667             $arg =~ /\G *\z/gc or die "syntax error in criterion\n";
668             push @criteria, $crit;
669             }
670             die "must list at least one attribute\n" unless @display_attrs;
671             push @sort_attrs, map { { index=>$_, neg=>0 } } @display_attrs;
672             @criteria = sort { $a->{cost} <=> $b->{cost} } @criteria;
673             my %need = (
674             (map { %{$_->{match_needs}} } @criteria),
675             (map { %{$_->{get_needs}} } @output_attrs),
676             );
677             my @joined;
678             if(exists($need{region}) || exists($need{c})) {
679             # Fully joining zones, regions, and countries is pretty
680             # cheap, so don't try to be cleverer about doing less
681             # join work.
682             my %zleft = %{olson_all_names()};
683             my $sel = olson_country_selection();
684             foreach(sort keys %$sel) {
685             my $csel = $sel->{$_};
686             if(keys(%{$csel->{regions}}) == 0) {
687             push @joined, { c => $csel->{alpha2_code} };
688             next;
689             }
690             foreach(sort keys %{$csel->{regions}}) {
691             my $reg = $csel->{regions}->{$_};
692             my $zname = $reg->{timezone_name};
693             push @joined, {
694             c => $csel->{alpha2_code},
695             region => $reg,
696             z => $zname,
697             };
698             delete $zleft{$zname};
699             }
700             }
701             push @joined, {z=>$_} foreach sort keys %zleft;
702             } else {
703             @joined = map { {z=>$_} } sort keys %{olson_all_names()};
704             }
705             my @presenters =
706             map { _type_curry_xpresent($_->{type}) } @output_attrs;
707             my @sorters = map { _type_curry_xcmp($_->{type}) } @output_attrs;
708             my %output;
709             foreach my $item (@joined) {
710             next unless _all { $_->{xmatch}->($item) } @criteria;
711             my @vals = map { $_->{xget}->($item) } @output_attrs;
712             next if _all { _is_exception($_) && $_ eq "!" } @vals;
713             $output{
714             join("\0", map { $presenters[$_]->($vals[$_]) }
715             0..$#output_attrs)
716             } = \@vals;
717             }
718             foreach(sort {
719             my $av = $output{$a};
720             my $bv = $output{$b};
721             my $res = 0;
722             foreach(@sort_attrs) {
723             $res = $sorters[$_->{index}]
724             ->($av->[$_->{index}], $bv->[$_->{index}]);
725             $res = -$res if $_->{neg};
726             last if $res != 0;
727             }
728             $res;
729             } keys %output) {
730             my $vals = $output{$_};
731             my $line = join(" ", map { $presenters[$_]->($vals->[$_]) }
732             @display_attrs);
733             $line =~ s/ +\z//;
734             print $line, "\n";
735             }
736             };
737              
738             sub run(@) {
739 1     1 1 1827 my $cmd = shift(@_);
740 1 50       7 defined $cmd or die "no subcommand specified\n";
741 1   50 0   10 ($command{$cmd} || sub { die "unrecognised subcommand\n" })->(@_);
  0            
742             }
743              
744             =head1 SEE ALSO
745              
746             L,
747             L,
748             L
749              
750             =head1 AUTHOR
751              
752             Andrew Main (Zefram)
753              
754             =head1 COPYRIGHT
755              
756             Copyright (C) 2012 Andrew Main (Zefram)
757              
758             =head1 LICENSE
759              
760             This module is free software; you can redistribute it and/or modify it
761             under the same terms as Perl itself.
762              
763             =cut
764              
765             1;