File Coverage

blib/lib/RDF/Query/Functions/Xpath.pm
Criterion Covered Total %
statement 61 267 22.8
branch 7 104 6.7
condition 0 99 0.0
subroutine 10 32 31.2
pod 1 1 100.0
total 79 503 15.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             RDF::Query::Functions::Xpath - XPath functions
4              
5             =head1 VERSION
6              
7             This document describes RDF::Query::Functions::Xpath version 2.915_01.
8              
9             =head1 DESCRIPTION
10              
11             Defines the following function:
12              
13             =over
14              
15             =item * http://www.w3.org/2005/xpath-functions#matches
16              
17             =item * http://www.w3.org/2005/xpath-functions#abs
18              
19             =item * http://www.w3.org/2005/xpath-functions#ceiling
20              
21             =item * http://www.w3.org/2005/xpath-functions#floor
22              
23             =item * http://www.w3.org/2005/xpath-functions#round
24              
25             =item * http://www.w3.org/2005/xpath-functions#round-half-to-even
26              
27             =item * http://www.w3.org/2005/xpath-functions#compare
28              
29             =item * http://www.w3.org/2005/xpath-functions#concat
30              
31             =item * http://www.w3.org/2005/xpath-functions#substring
32              
33             =item * http://www.w3.org/2005/xpath-functions#string-length
34              
35             =item * http://www.w3.org/2005/xpath-functions#upper-case
36              
37             =item * http://www.w3.org/2005/xpath-functions#lower-case
38              
39             =item * http://www.w3.org/2005/xpath-functions#encode-for-uri
40              
41             =item * http://www.w3.org/2005/xpath-functions#contains
42              
43             =item * http://www.w3.org/2005/xpath-functions#starts-with
44              
45             =item * http://www.w3.org/2005/xpath-functions#ends-with
46              
47             =item * http://www.w3.org/2005/xpath-functions#year-from-dateTime
48              
49             =item * http://www.w3.org/2005/xpath-functions#month-from-dateTime
50              
51             =item * http://www.w3.org/2005/xpath-functions#day-from-dateTime
52              
53             =item * http://www.w3.org/2005/xpath-functions#hours-from-dateTime
54              
55             =item * http://www.w3.org/2005/xpath-functions#minutes-from-dateTime
56              
57             =item * http://www.w3.org/2005/xpath-functions#seconds-from-dateTime
58              
59             =item * http://www.w3.org/2005/xpath-functions#timezone-from-dateTime
60              
61              
62             =back
63              
64             =cut
65              
66             package RDF::Query::Functions::Xpath;
67              
68 35     35   30424 use strict;
  35         81  
  35         915  
69 35     35   191 use warnings;
  35         75  
  35         860  
70 35     35   189 use Log::Log4perl;
  35         81  
  35         251  
71 35     35   1537 use POSIX;
  35         80  
  35         356  
72 35     35   95926 use URI::Escape;
  35         79  
  35         2287  
73              
74 35     35   216 use RDF::Trine::Namespace qw(xsd);
  35         84  
  35         428  
75              
76             our ($VERSION, $l);
77             BEGIN {
78 35     35   4393 $l = Log::Log4perl->get_logger("rdf.query.functions.xpath");
79 35         14854 $VERSION = '2.915_01';
80             }
81              
82 35     35   221 use Scalar::Util qw(blessed reftype refaddr looks_like_number);
  35         75  
  35         111607  
83              
84             =begin private
85              
86             =item C<< install >>
87              
88             Documented in L<RDF::Query::Functions>.
89              
90             =end private
91              
92             =cut
93              
94             sub install {
95             # # fn:compare
96             # $RDF::Query::functions{"http://www.w3.org/2005/04/xpath-functionscompare"} = sub {
97             # my $query = shift;
98             # my $nodea = shift;
99             # my $nodeb = shift;
100             # my $cast = 'sop:str';
101             # return ($RDF::Query::functions{$cast}->($query, $nodea) cmp $RDF::Query::functions{$cast}->($query, $nodeb));
102             # };
103             #
104             # # fn:not
105             # $RDF::Query::functions{"http://www.w3.org/2005/04/xpath-functionsnot"} = sub {
106             # my $query = shift;
107             # my $nodea = shift;
108             # my $nodeb = shift;
109             # my $cast = 'sop:str';
110             # return (0 != ($RDF::Query::functions{$cast}->($query, $nodea) cmp $RDF::Query::functions{$cast}->($query, $nodeb)));
111             # };
112              
113             # fn:matches
114             RDF::Query::Functions->install_function(
115             "http://www.w3.org/2005/xpath-functions#matches",
116             sub {
117 1     1   3 my $query = shift;
118 1         3 my $node = shift;
119 1         2 my $match = shift;
120 1         2 my $f = shift;
121            
122 1         2 my $string;
123 1 50       6 if ($node->isa('RDF::Query::Node::Resource')) {
    0          
124 1         4 $string = $node->uri_value;
125             } elsif ($node->isa('RDF::Query::Node::Literal')) {
126 0         0 $string = $node->literal_value;
127             } else {
128 0         0 throw RDF::Query::Error::TypeError -text => "xpath:matches called without a literal or resource";
129             }
130            
131 1         7 my $pattern = $match->literal_value;
132 1 50       11 return undef if (index($pattern, '(?{') != -1);
133 1 50       5 return undef if (index($pattern, '(??{') != -1);
134 1 50       7 my $flags = blessed($f) ? $f->literal_value : '';
135            
136 1         7 my $matches;
137 1 50       4 if ($flags) {
138 0         0 $pattern = "(?${flags}:${pattern})";
139 0         0 warn 'pattern: ' . $pattern;
140 0         0 $matches = $string =~ /$pattern/;
141             } else {
142 1 50       20 $matches = ($string =~ /$pattern/) ? 1 : 0;
143             }
144              
145 1 50       7 return ($matches)
146             ? RDF::Query::Node::Literal->new('true', undef, 'http://www.w3.org/2001/XMLSchema#boolean')
147             : RDF::Query::Node::Literal->new('false', undef, 'http://www.w3.org/2001/XMLSchema#boolean');
148              
149             }
150 35     35 1 269 );
151              
152             # fn:abs
153             RDF::Query::Functions->install_function(
154             "http://www.w3.org/2005/xpath-functions#abs",
155             sub {
156 0     0   0 my $query = shift;
157 0         0 my $node = shift;
158 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal') and $node->is_numeric_type) {
      0        
159 0         0 my $value = $node->numeric_value;
160 0         0 return RDF::Query::Node::Literal->new( abs($value), undef, $node->literal_datatype );
161             } else {
162 0         0 throw RDF::Query::Error::TypeError -text => "xpath:abs called without a numeric literal";
163             }
164             }
165 35         231 );
166            
167             # fn:ceiling
168             RDF::Query::Functions->install_function(
169             "http://www.w3.org/2005/xpath-functions#ceiling",
170             sub {
171 0     0   0 my $query = shift;
172 0         0 my $node = shift;
173 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal') and $node->is_numeric_type) {
      0        
174 0         0 my $value = $node->numeric_value;
175 0         0 return RDF::Query::Node::Literal->new( ceil($value), undef, $node->literal_datatype );
176             } else {
177 0         0 throw RDF::Query::Error::TypeError -text => "xpath:ceiling called without a numeric literal";
178             }
179             }
180 35         216 );
181            
182             # fn:floor
183             RDF::Query::Functions->install_function(
184             "http://www.w3.org/2005/xpath-functions#floor",
185             sub {
186 0     0   0 my $query = shift;
187 0         0 my $node = shift;
188 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal') and $node->is_numeric_type) {
      0        
189 0         0 my $value = $node->numeric_value;
190 0         0 return RDF::Query::Node::Literal->new( floor($value), undef, $node->literal_datatype );
191             } else {
192 0         0 throw RDF::Query::Error::TypeError -text => "xpath:floor called without a numeric literal";
193             }
194             }
195 35         221 );
196            
197             # fn:round
198             RDF::Query::Functions->install_function(
199             "http://www.w3.org/2005/xpath-functions#round",
200             sub {
201 0     0   0 my $query = shift;
202 0         0 my $node = shift;
203 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal') and $node->is_numeric_type) {
      0        
204 0         0 my $value = $node->numeric_value;
205 0         0 my $mult = 1;
206 0 0       0 if ($value < 0) {
207 0         0 $mult = -1;
208 0         0 $value = -$value;
209             }
210 0         0 my $round = $mult * POSIX::floor($value + 0.50000000000008);
211 0         0 return RDF::Query::Node::Literal->new( $round, undef, $node->literal_datatype );
212             } else {
213 0         0 throw RDF::Query::Error::TypeError -text => "xpath:round called without a numeric literal";
214             }
215             }
216 35         255 );
217            
218             # fn:round-half-to-even
219             RDF::Query::Functions->install_function(
220             "http://www.w3.org/2005/xpath-functions#round-half-to-even",
221             sub {
222 0     0   0 my $query = shift;
223 0         0 my $node = shift;
224 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal') and $node->is_numeric_type) {
      0        
225 0         0 my $value = $node->numeric_value;
226 0         0 return RDF::Query::Node::Literal->new( sprintf('%.0f', $value), undef, $node->literal_datatype );
227             } else {
228 0         0 throw RDF::Query::Error::TypeError -text => "xpath:round-half-to-even called without a numeric literal";
229             }
230             }
231 35         249 );
232            
233             # fn:compare
234             RDF::Query::Functions->install_function(
235             "http://www.w3.org/2005/xpath-functions#compare",
236             sub {
237 0     0   0 my $query = shift;
238 0         0 my $nodea = shift;
239 0         0 my $nodeb = shift;
240 0 0 0     0 unless (blessed($nodea) and $nodea->isa('RDF::Query::Node::Literal')) {
241 0         0 throw RDF::Query::Error::TypeError -text => "xpath:compare called without a literal arg1 term";
242             }
243 0 0 0     0 unless (blessed($nodeb) and $nodeb->isa('RDF::Query::Node::Literal')) {
244 0         0 throw RDF::Query::Error::TypeError -text => "xpath:compare called without a literal arg2 term";
245             }
246            
247 0         0 my $value = ($nodea->literal_value cmp $nodeb->literal_value);
248 0         0 return RDF::Query::Node::Literal->new( $value, undef, $xsd->integer );
249             }
250 35         270 );
251            
252             # fn:concat
253             RDF::Query::Functions->install_function(
254             "http://www.w3.org/2005/xpath-functions#concat",
255             sub {
256 0     0   0 my $query = shift;
257 0         0 my $model = $query->model;
258 0         0 my @nodes = @_;
259 0         0 my @strings = map { $query->call_function($model, {}, 'sparql:str', $_) } @nodes;
  0         0  
260 0         0 my $value = join('', map { $_->literal_value } @strings);
  0         0  
261 0         0 return RDF::Query::Node::Literal->new($value);
262             }
263 35         245 );
264            
265             # fn:substring
266             RDF::Query::Functions->install_function(
267             "http://www.w3.org/2005/xpath-functions#substring",
268             sub {
269 0     0   0 my $query = shift;
270 0         0 my $node = shift;
271 0         0 my @args = @_;
272 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
273 0         0 throw RDF::Query::Error::TypeError -text => "xpath:substring called without a literal arg1 term";
274             }
275 0         0 my $value = $node->literal_value;
276 0         0 my @nums;
277 0         0 foreach my $i (0 .. $#args) {
278 0         0 my $argnum = $i + 2;
279 0         0 my $arg = $args[ $i ];
280 0 0 0     0 unless (blessed($arg) and $arg->isa('RDF::Query::Node::Literal') and $arg->is_numeric_type) {
      0        
281 0         0 throw RDF::Query::Error::TypeError -text => "xpath:substring called without a numeric literal arg${argnum} term";
282             }
283 0         0 push(@nums, $arg->numeric_value);
284             }
285 0         0 my $substring = substr($value, @nums);
286 0         0 return RDF::Query::Node::Literal->new($substring);
287             }
288 35         348 );
289            
290             # fn:string-length
291             RDF::Query::Functions->install_function(
292             "http://www.w3.org/2005/xpath-functions#string-length",
293             sub {
294 0     0   0 my $query = shift;
295 0         0 my $node = shift;
296 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
297 0         0 my $value = $node->literal_value;
298 0         0 return RDF::Query::Node::Literal->new( length($value), undef, $xsd->integer );
299             } else {
300 0         0 throw RDF::Query::Error::TypeError -text => "xpath:string-length called without a literal term";
301             }
302             }
303 35         251 );
304            
305             # fn:upper-case
306             RDF::Query::Functions->install_function(
307             "http://www.w3.org/2005/xpath-functions#upper-case",
308             sub {
309 0     0   0 my $query = shift;
310 0         0 my $node = shift;
311 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
312 0         0 my $value = $node->literal_value;
313 0         0 return RDF::Query::Node::Literal->new( uc($value) );
314             } else {
315 0         0 throw RDF::Query::Error::TypeError -text => "xpath:upper-case called without a literal term";
316             }
317             }
318 35         245 );
319            
320             # fn:lower-case
321             RDF::Query::Functions->install_function(
322             "http://www.w3.org/2005/xpath-functions#lower-case",
323             sub {
324 0     0   0 my $query = shift;
325 0         0 my $node = shift;
326 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
327 0         0 my $value = $node->literal_value;
328 0         0 return RDF::Query::Node::Literal->new( lc($value) );
329             } else {
330 0         0 throw RDF::Query::Error::TypeError -text => "xpath:lower-case called without a literal term";
331             }
332             }
333 35         314 );
334            
335             # fn:encode-for-uri
336             RDF::Query::Functions->install_function(
337             "http://www.w3.org/2005/xpath-functions#encode-for-uri",
338             sub {
339 0     0   0 my $query = shift;
340 0         0 my $node = shift;
341 0 0 0     0 if (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
342 0         0 my $value = $node->literal_value;
343 0         0 return RDF::Query::Node::Literal->new( uri_escape($value) );
344             } else {
345 0         0 throw RDF::Query::Error::TypeError -text => "xpath:escape-for-uri called without a literal term";
346             }
347             }
348 35         246 );
349            
350             # fn:contains
351             RDF::Query::Functions->install_function(
352             "http://www.w3.org/2005/xpath-functions#contains",
353             sub {
354 0     0   0 my $query = shift;
355 0         0 my $node = shift;
356 0         0 my $pat = shift;
357 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
358 0         0 throw RDF::Query::Error::TypeError -text => "xpath:contains called without a literal arg1 term";
359             }
360 0 0 0     0 unless (blessed($pat) and $pat->isa('RDF::Query::Node::Literal')) {
361 0         0 throw RDF::Query::Error::TypeError -text => "xpath:contains called without a literal arg2 term";
362             }
363 0         0 my $lit = $node->literal_value;
364 0         0 my $plit = $pat->literal_value;
365 0         0 my $pos = index($lit, $plit);
366 0 0       0 if ($pos >= 0) {
367 0         0 return RDF::Query::Node::Literal->new('true', undef, $xsd->boolean);
368             } else {
369 0         0 return RDF::Query::Node::Literal->new('false', undef, $xsd->boolean);
370             }
371             }
372 35         250 );
373            
374             # fn:starts-with
375             RDF::Query::Functions->install_function(
376             "http://www.w3.org/2005/xpath-functions#starts-with",
377             sub {
378 0     0   0 my $query = shift;
379 0         0 my $node = shift;
380 0         0 my $pat = shift;
381 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
382 0         0 throw RDF::Query::Error::TypeError -text => "xpath:starts-with called without a literal arg1 term";
383             }
384 0 0 0     0 unless (blessed($pat) and $pat->isa('RDF::Query::Node::Literal')) {
385 0         0 throw RDF::Query::Error::TypeError -text => "xpath:starts-with called without a literal arg2 term";
386             }
387 0 0       0 if (index($node->literal_value, $pat->literal_value) == 0) {
388 0         0 return RDF::Query::Node::Literal->new('true', undef, $xsd->boolean);
389             } else {
390 0         0 return RDF::Query::Node::Literal->new('false', undef, $xsd->boolean);
391             }
392             }
393 35         242 );
394            
395             # fn:ends-with
396             RDF::Query::Functions->install_function(
397             "http://www.w3.org/2005/xpath-functions#ends-with",
398             sub {
399 0     0   0 my $query = shift;
400 0         0 my $node = shift;
401 0         0 my $pat = shift;
402 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
403 0         0 throw RDF::Query::Error::TypeError -text => "xpath:ends-with called without a literal arg1 term";
404             }
405 0 0 0     0 unless (blessed($pat) and $pat->isa('RDF::Query::Node::Literal')) {
406 0         0 throw RDF::Query::Error::TypeError -text => "xpath:ends-with called without a literal arg2 term";
407             }
408            
409 0         0 my $lit = $node->literal_value;
410 0         0 my $plit = $pat->literal_value;
411 0         0 my $pos = length($lit) - length($plit);
412 0 0       0 if (rindex($lit, $plit) == $pos) {
413 0         0 return RDF::Query::Node::Literal->new('true', undef, $xsd->boolean);
414             } else {
415 0         0 return RDF::Query::Node::Literal->new('false', undef, $xsd->boolean);
416             }
417             }
418 35         277 );
419            
420             # op:dateTime-equal
421             # op:dateTime-less-than
422             # op:dateTime-greater-than
423            
424             # fn:year-from-dateTime
425             RDF::Query::Functions->install_function(
426             "http://www.w3.org/2005/xpath-functions#year-from-dateTime",
427             sub {
428 0     0   0 my $query = shift;
429 0         0 my $node = shift;
430 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
431 0         0 throw RDF::Query::Error::TypeError -text => "xpath:year-from-dateTime called without a literal term";
432             }
433 0         0 my $dt = $node->datetime;
434 0 0       0 if ($dt) {
435 0         0 return RDF::Query::Node::Literal->new($dt->year);
436             } else {
437 0         0 throw RDF::Query::Error::TypeError -text => "xpath:year-from-dateTime called without a valid dateTime";
438             }
439             }
440 35         253 );
441            
442             # fn:month-from-dateTime
443             RDF::Query::Functions->install_function(
444             "http://www.w3.org/2005/xpath-functions#month-from-dateTime",
445             sub {
446 0     0   0 my $query = shift;
447 0         0 my $node = shift;
448 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
449 0         0 throw RDF::Query::Error::TypeError -text => "xpath:month-from-dateTime called without a literal term";
450             }
451 0         0 my $dt = $node->datetime;
452 0 0       0 if ($dt) {
453 0         0 return RDF::Query::Node::Literal->new($dt->month);
454             } else {
455 0         0 throw RDF::Query::Error::TypeError -text => "xpath:month-from-dateTime called without a valid dateTime";
456             }
457             }
458 35         226 );
459            
460             # fn:day-from-dateTime
461             RDF::Query::Functions->install_function(
462             "http://www.w3.org/2005/xpath-functions#day-from-dateTime",
463             sub {
464 0     0   0 my $query = shift;
465 0         0 my $node = shift;
466 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
467 0         0 throw RDF::Query::Error::TypeError -text => "xpath:day-from-dateTime called without a literal term";
468             }
469 0         0 my $dt = $node->datetime;
470 0 0       0 if ($dt) {
471 0         0 return RDF::Query::Node::Literal->new($dt->day);
472             } else {
473 0         0 throw RDF::Query::Error::TypeError -text => "xpath:day-from-dateTime called without a valid dateTime";
474             }
475             }
476 35         250 );
477            
478             # fn:hours-from-dateTime
479             RDF::Query::Functions->install_function(
480             "http://www.w3.org/2005/xpath-functions#hours-from-dateTime",
481             sub {
482 0     0   0 my $query = shift;
483 0         0 my $node = shift;
484 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
485 0         0 throw RDF::Query::Error::TypeError -text => "xpath:hours-from-dateTime called without a literal term";
486             }
487 0         0 my $dt = $node->datetime;
488 0 0       0 if ($dt) {
489 0         0 return RDF::Query::Node::Literal->new($dt->hour);
490             } else {
491 0         0 throw RDF::Query::Error::TypeError -text => "xpath:hours-from-dateTime called without a valid dateTime";
492             }
493             }
494 35         233 );
495            
496             # fn:minutes-from-dateTime
497             RDF::Query::Functions->install_function(
498             "http://www.w3.org/2005/xpath-functions#minutes-from-dateTime",
499             sub {
500 0     0   0 my $query = shift;
501 0         0 my $node = shift;
502 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
503 0         0 throw RDF::Query::Error::TypeError -text => "xpath:minutes-from-dateTime called without a literal term";
504             }
505 0         0 my $dt = $node->datetime;
506 0 0       0 if ($dt) {
507 0         0 return RDF::Query::Node::Literal->new($dt->minute);
508             } else {
509 0         0 throw RDF::Query::Error::TypeError -text => "xpath:minutes-from-dateTime called without a valid dateTime";
510             }
511             }
512 35         247 );
513            
514             # fn:seconds-from-dateTime
515             RDF::Query::Functions->install_function(
516             "http://www.w3.org/2005/xpath-functions#seconds-from-dateTime",
517             sub {
518 0     0   0 my $query = shift;
519 0         0 my $node = shift;
520 0 0 0     0 unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
521 0         0 throw RDF::Query::Error::TypeError -text => "xpath:seconds-from-dateTime called without a literal term";
522             }
523 0         0 my $dt = $node->datetime;
524 0 0       0 if ($dt) {
525 0         0 return RDF::Query::Node::Literal->new($dt->second);
526             } else {
527 0         0 throw RDF::Query::Error::TypeError -text => "xpath:seconds-from-dateTime called without a valid dateTime";
528             }
529             }
530 35         233 );
531            
532             # fn:timezone-from-dateTime
533             RDF::Query::Functions->install_function(
534             "http://www.w3.org/2005/xpath-functions#timezone-from-dateTime",
535             sub {
536 0     0     my $query = shift;
537 0           my $node = shift;
538 0 0 0       unless (blessed($node) and $node->isa('RDF::Query::Node::Literal')) {
539 0           throw RDF::Query::Error::TypeError -text => "xpath:timezone-from-dateTime called without a literal term";
540             }
541 0           my $dt = $node->datetime;
542 0 0         if ($dt) {
543 0           my $tz = $dt->time_zone;
544 0 0         if ($tz) {
545 0           my $offset = $tz->offset_for_datetime( $dt );
546 0           my $minus = '';
547 0 0         if ($offset < 0) {
548 0           $minus = '-';
549 0           $offset = -$offset;
550             }
551              
552 0           my $duration = "${minus}PT";
553 0 0         if ($offset >= 60*60) {
554 0           my $h = int($offset / (60*60));
555 0 0         $duration .= "${h}H" if ($h > 0);
556 0           $offset = $offset % (60*60);
557             }
558 0 0         if ($offset >= 60) {
559 0           my $m = int($offset / 60);
560 0 0         $duration .= "${m}M" if ($m > 0);
561 0           $offset = $offset % 60;
562             }
563 0           my $s = int($offset);
564 0 0 0       $duration .= "${s}S" if ($s > 0 or $duration eq 'PT');
565            
566 0           return RDF::Query::Node::Literal->new($duration);
567             }
568             }
569 0           throw RDF::Query::Error::TypeError -text => "xpath:timezone-from-dateTime called without a valid dateTime";
570             }
571 35         335 );
572              
573             }
574              
575              
576              
577             1;
578              
579             __END__
580              
581             =head1 AUTHOR
582              
583             Gregory Williams <gwilliams@cpan.org>.
584              
585             =cut