File Coverage

blib/lib/ParseCron.pm
Criterion Covered Total %
statement 12 246 4.8
branch 0 184 0.0
condition 0 126 0.0
subroutine 4 17 23.5
pod 13 13 100.0
total 29 586 4.9


line stmt bran cond sub pod time code
1             package ParseCron;
2              
3 1     1   24079 use 5.006;
  1         5  
  1         54  
4 1     1   14 use strict;
  1         3  
  1         47  
5 1     1   5 use warnings FATAL => 'all';
  1         7  
  1         55  
6              
7 1     1   11 use Exporter qw(import);
  1         2  
  1         4703  
8             our @EXPORT_OK = qw(parse_cron);
9              
10             our $VERSION = '0.02';
11              
12             # SET UP THE ENVIRONMENT ####################################################
13             #############################################################################
14            
15              
16             #############################################################################
17             our $posix = $ENV{'POSIXLY_CORRECT'} || $ENV{'POSIX_ME_HARDER'};
18              
19             our $atom;
20             if ($posix) {
21             $atom = '\d+|(?:\d+-\d+)'; # POSIX allows no stepped ranges.
22             }
23             else {
24             $atom = '\d+|(?:\d+-\d+(?:/\d+)?)';
25             }
26              
27             our $atoms = "^(?:$atom)(?:,$atom)*\$";
28             #############################################################################
29              
30              
31              
32              
33              
34              
35             our @dows = qw(Sun Mon Tue Wed Thu Fri Sat);
36             our @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
37             our ($dow, $month, %month2num, %dow2num, %num2dow, %num2month);
38              
39             our %mil2ampm;
40             @mil2ampm{0 .. 23} = ('midnight', map($_ . 'am', 1 .. 11), 'noon', map($_ . 'pm', 1 .. 11));
41              
42              
43             @dow2num{map lc($_), @dows} = (0 .. 6);
44              
45              
46              
47             push @dows, 'Sun' unless $posix;
48             # POSIX doesn't know about day 7
49             @num2dow{0 .. $#dows} = @dows;
50              
51             @month2num{map lc($_), @months} = (1 .. 12);
52             @num2month{1 .. 12} = @months;
53             unshift @months, '';
54              
55             {
56             my $x = join '|', map quotemeta($_), @dows;
57             $dow = "^($x)\$"; # regexp
58             $x = join '|', map quotemeta($_), @months;
59             $month = "^($x)\$"; # regexp
60             }
61              
62              
63              
64             # SET UP THE ENVIRONMENT ####################################################
65             #############################################################################
66              
67             sub new {
68 0     0 1   my $class = shift;
69 0           my $self = bless {}, $class;
70            
71 0           return $self;
72             }
73              
74             sub parse_cron {
75 0     0 1   my $self = shift;
76              
77 0           my $crontab = shift;
78            
79 0           my (@bits, $k, $v, $english);
80            
81 0           my %atword = ( # for latter-day Vixie-isms
82             'reboot' => 'At reboot',
83             'yearly' => 'Yearly (midnight on January 1st)',
84             'annually' => 'Yearly (midnight on January 1st)',
85             'monthly' => 'Monthly (midnight on the first of every month)',
86             'weekly' => 'Weekly (midnight every Sunday)',
87             'daily' => 'Daily, at midnight',
88             'midnight' => 'Daily, at midnight',
89             'hourly' => 'At the top of every hour',
90             # These are no longer documented in Vixie cron 3.0. Why not?
91             );
92            
93             #next if $crontab =~ m/^[ \t]*#/s or $crontab =~ m/^[ \t]*$/s;
94 0           $crontab =~ s/^[ \t]+//s; # "leading spaces and tabs are ignored"
95              
96             # The POSIX cron spec doesn't seem to mention
97             # environment-setting lines at all!
98              
99 0 0 0       if (!$posix and $crontab =~ m/^([^= \t]+)[ \t]*=[ \t]*\"(.*)\"[ \t]*$/s) {
    0 0        
    0 0        
    0 0        
    0 0        
      0        
100             # NAME = "VALUE"
101 0           $k = ($crontab =~ s/[ \t]+$//);
102             }
103             elsif (!$posix and $crontab =~ m/^([^= \t]+)[ \t]*=[ \t]*\'(.*)\'[ \t]*$/s) {
104 0           ($k, $v) = ($1, $2);
105             }
106             elsif (!$posix and $crontab =~ m/^([^= \t]+)[ \t]*=(.*)/s) {
107 0           ($k,$v) = ($1, $2);
108 0           $v = ($crontab =~ s/^[ \t]+//);
109             }
110             elsif (!$posix and $crontab =~ m/^\@(\w+)[ \t]+(.*)/s and exists $atword{lc $1}) {
111 0           $english = process_command($crontab, $atword{lc $1}, $2);
112             }
113             # for adding commands to be run to cron lines:
114             #elsif ((@bits = split m/[ \t]+/, $crontab, 6) and @bits == 6) {
115             elsif ((@bits = split m/[ \t]+/, $crontab, 5) and @bits == 5) {
116 0           $english = process_command($crontab, @bits);
117             }
118             else {
119 0           $english = 'ERROR';
120             }
121              
122 0 0         $english = 'ERROR' if scalar (@bits) > 5;
123            
124 0           return $english;
125             }
126              
127             sub process_command {
128             # 0 m, 1 h, 2 day-of-month, 3 month, 4 dow
129             ###my $line = shift;
130             ###
131             ### my $filter = shift;
132             #print Dumper(\@_);
133              
134 0     0 1   my $filter = '';
135 0           shift @_;
136 0           my $res = '';
137 0           my(@time_lines, $command_string);
138 0 0         if(@_ == 2) { # hack for funky vixieism
139 0           $command_string = $_[1];
140 0           @time_lines = ($_[0]);
141             } else {
142             # a normal line -- expand and Englishify it
143 0           my(@bits) = expand_time_bits(@_);
144             #print Dumper(\@bits);
145 0           @time_lines = bits_to_english(@bits);
146             #print Dumper(\@time_lines);
147 0           $time_lines[0] = ucfirst($time_lines[0]);
148 0 0         if(length(join ' ', @time_lines) <= 75) {
149 0           @time_lines = (join ' ', @time_lines);
150             }
151 0           for(@time_lines) { $_ = ' ' . $_ }; # indent over
  0            
152             # $time_lines[0] = "At:" . $time_lines[0];
153            
154             ###$time_lines[0] = ":" . $time_lines[0];
155 0           $time_lines[0] = $time_lines[0];
156 0           $command_string = pop @bits;
157             }
158            
159 0           my @command = split( "\n", percent_proc($command_string), -1 );
160            
161 0 0         if(@command) {
162 0 0 0       pop @command if @command == 2 and $command[1] eq '';
163             # Eliminate mention of basically null input
164             } else {
165 0           push @command, '';
166             }
167            
168 0 0         if(@command > 1) {
169 0           my $x = join "\n", splice @command, 1;
170 0           push @command, " with input \"" . esc($x) . "\"";
171             }
172 0 0         if($command[0] =~ m<^\*>s) {
    0          
173 0           push @command, " (Do you really mean the command to start with \"*\"?)";
174             } elsif($command[0] eq '') {
175 0           push @command, " (Do you really mean to run a null command?)";
176             }
177 0           $command[0] = "Run: $command[0]";
178            
179 0 0         if($filter) {
180             ### print
181 0 0         $res = map("$filter $_\n",
182             (@command == 1) ? () : (@command), # be concise for simple cases
183             @time_lines
184             );
185             ###;
186             } else {
187 0           foreach my $time_line (@time_lines) {
188 0           $time_line =~ s/\s{2,}/ /g;
189             }
190             ###print
191             ###@time_lines, "\n";
192 0           $res = join(' ', @time_lines)
193             }
194            
195 0           return $res;
196             }
197              
198             sub expand_time_bits {
199 0     0 1   my @bits = @_;
200 0           my @unparseable;
201              
202             # 0 m, 1 h, 2 day-of-month, 3 month, 4 dow
203            
204 0 0         unless($posix) {
205 0 0         if($bits[3] =~ m/($month)/oi) { $bits[3] = $month2num{lc $1} }
  0            
206 0 0         if($bits[4] =~ m/($dow)/oi ) { $bits[4] = $dow2num{lc $1} }
  0            
207             }
208              
209 0           for(my $i = 0; $i < 5 ; ++$i) {
210 0           my @segments;
211 0 0 0       if($bits[$i] eq '*') {
    0          
    0          
212 0           push @segments, ['*'];
213             } elsif(!$posix and $bits[$i] =~ m<^\*/(\d+)$>s) {
214             # a hack for "*/3" etc
215 0           push @segments, ['*', 0 + $1];
216             } elsif($bits[$i] =~ m/$atoms/ois) {
217 0           foreach my $thang (split ',', $bits[$i]) {
218 0 0         if($thang =~ m<^(?:(\d+)|(?:(\d+)-(\d+)(?:/(\d+))?))$>s) {
219 0 0         if(defined $1) {
    0          
220 0           push @segments, [0 + $1]; # "7"
221             } elsif(defined $4) {
222 0           push @segments, [0 + $2, 0 + $3, 0 + $4]; # "3-20/4"
223             } else {
224 0           push @segments, [0 + $2, 0 + $3]; # "3-20"
225             }
226             } else {
227 0           warn "GWAH? thang \"$thang\"";
228             }
229             }
230             } else {
231 0           push @unparseable, sprintf "field %s: \"%s\"", $i + 1, esc($bits[$i]);
232 0           next;
233             }
234            
235 0           $bits[$i] = \@segments;
236             }
237 0 0         return \@unparseable if @unparseable;
238 0           return @bits;
239             }
240              
241             sub bits_to_english {
242             # This is the deep ugly scary guts of this program.
243             # The older and eldritch among you might recognize this as sort of a
244             # parody of bad old Lisp style of data-structure handling.
245              
246 0     0 1   my @bits = @_;
247 0           my @time_lines;
248             #use Data::Dumper; print STDERR Dumper(\@bits);
249              
250 0 0         if (scalar(@bits) != 5) {
251 0           $time_lines[0] = 'ERROR';
252              
253 0           return @time_lines;
254             }
255              
256 0           my %num2month_long = (
257             '1' => 'January',
258             '2' => 'February',
259             '3' => 'March',
260             '4' => 'April',
261             '5' => 'May',
262             '6' => 'June',
263             '7' => 'July',
264             '8' => 'August',
265             '9' => 'September',
266             '10' => 'October',
267             '11' => 'November',
268             '12' => 'December',
269             );
270 0           my %num2dow_long = (
271             '0' => 'Sunday',
272             '1' => 'Monday',
273             '2' => 'Tuesday',
274             '3' => 'Wednesday',
275             '4' => 'Thursday',
276             '5' => 'Friday',
277             '6' => 'Saturday',
278             );
279              
280 0 0         $num2dow_long{'7'} = 'Sunday' unless $posix;
281             { # gratuitous block.
282            
283             # Render the minutes and hours ########################################
284 0 0 0       if(@{$bits[0]} == 1 and @{$bits[1]} == 1 and
  0   0        
  0   0        
  0   0        
  0   0        
285 0           @{$bits[0][0]} == 1 and @{$bits[1][0]} == 1 and
286             $bits[0][0][0] ne '*' and $bits[1][0][0] ne '*'
287             # It's a highly simplifiable time expression!
288             # This is a very common case. Like "46 13" -> 1:46pm
289             # Formally: when minute and hour are each a single number.
290             ) {
291 0           my $h = $bits[1][0][0];
292 0 0         if($bits[0][0][0] == 0) {
293             # Simply at the top of the hour, so just call it by the hour name.
294 0           push @time_lines, $mil2ampm{$h};
295             } else {
296             # Can't say "noon:02", so use an always-numeric time format:
297 0 0         push @time_lines, sprintf '%s:%02d%s',
    0          
298             ($h > 12) ? ($h - 12) : $h,
299             $bits[0][0][0],
300             ($h >= 12) ? 'pm' : 'am';
301             }
302 0           $time_lines[-1] .= ' on';
303              
304             } else { # It's not a highly simplifiable time expression
305            
306             # First, minutes:
307 0 0 0       if($bits[0][0][0] eq '*') {
  0 0          
    0          
308 0 0 0       if(1 == @{$bits[0][0]} or $bits[0][0][1] == 1) {
  0            
309 0           push @time_lines, 'every minute of';
310             } else {
311 0           push @time_lines, 'every ' . freq($bits[0][0][1]) . ' minute of';
312             }
313            
314 0           } elsif( @{$bits[0]} == 1 and $bits[0][0][0] == 0 ) {
315             # It's just a '0'. Ignore it -- instead of bothering
316             # to add a "0 minutes past"
317             } elsif( !grep @$_ > 1, @{$bits[0]} ) {
318             # it's all like 7,10,15. conjoinable
319 0 0         push @time_lines, conj_and(map $_->[0], @{$bits[0]}) . (
  0            
320             $bits[0][-1][0] == 1 ? ' minute past' : ' minutes past' );
321             } else { # it's just gonna be long.
322 0           my @hunks;
323 0           foreach my $bit (@{$bits[0]}) {
  0            
324 0 0         if(@$bit == 1) { #"7"
    0          
    0          
325 0 0         push @hunks, $bit->[0] == 1 ? '1 minute' : "$bit->[0] minutes";
326             } elsif(@$bit == 2) { #"7-9"
327 0 0         push @hunks, sprintf "from %d to %d %s", @$bit,
328             $bit->[1] == 1 ? 'minute' : 'minutes';
329             } elsif(@$bit == 3) { # "7-20/2"
330 0 0         push @hunks, sprintf "every %d %s from %d to %d",
331             $bit->[2],
332             $bit->[2] == 1 ? 'minute' : 'minutes',
333             $bit->[0], $bit->[1],
334             ;
335             }
336             }
337 0           push @time_lines, conj_and(@hunks) . ' past';
338             }
339            
340             # Now hours
341 0 0         if($bits[1][0][0] eq '*') {
342 0 0 0       if(1 == @{$bits[1][0]} or $bits[1][0][1] == 1) {
  0            
343 0           push @time_lines, 'every hour of';
344             } else {
345 0           push @time_lines, 'every ' . freq($bits[1][0][1]) . ' hour of';
346             }
347             } else {
348 0           my @hunks;
349 0           foreach my $bit (@{$bits[1]}) {
  0            
350 0 0         if(@$bit == 1) { # "7"
    0          
    0          
351 0   0       push @hunks, $mil2ampm{$bit->[0]} || "HOUR_$bit->[0]??";
352             } elsif(@$bit == 2) { # "7-9"
353 0   0       push @hunks, sprintf "from %s to %s",
      0        
354             $mil2ampm{$bit->[0]} || "HOUR_$bit->[0]??",
355             $mil2ampm{$bit->[1]} || "HOUR_$bit->[1]??",
356             } elsif(@$bit == 3) { # "7-20/2"
357 0 0 0       push @hunks, sprintf "every %d %s from %s to %s",
      0        
358             $bit->[2],
359             $bit->[2] == 1 ? 'hour' : 'hours',
360             $mil2ampm{$bit->[0]} || "HOUR_$bit->[0]??",
361             $mil2ampm{$bit->[1]} || "HOUR_$bit->[1]??",
362             }
363             }
364 0           push @time_lines, conj_and(@hunks) . ' of';
365             }
366             # End of hours and minutes
367             }
368              
369             # Day-of-month ########################################################
370 0 0         if($bits[2][0][0] eq '*') {
371 0           $time_lines[-1] =~ s/ on$//s;
372 0 0 0       if(1 == @{$bits[2][0]} or $bits[2][0][1] == 1) {
  0            
373 0           push @time_lines, 'every day of';
374             } else {
375 0           push @time_lines, 'every ' . freq($bits[2][0][1]) . ' day of';
376             }
377             } else {
378 0           my @hunks;
379 0           foreach my $bit (@{$bits[2]}) {
  0            
380 0 0         if(@$bit == 1) { # "7"
    0          
    0          
381 0           push @hunks, 'the ' . ordinate($bit->[0]);
382             } elsif(@$bit == 2) { # "7-9"
383 0           push @hunks, sprintf "from the %s to the %s",
384             ordinate($bit->[0]), ordinate($bit->[1]),
385             } elsif(@$bit == 3) { # "7-20/2"
386 0 0         push @hunks, sprintf "every %d %s from the %s to the %s",
387             $bit->[2],
388             $bit->[2] == 1 ? 'day' : 'days',
389             ordinate($bit->[0]), ordinate($bit->[1]),
390             }
391             }
392            
393             # collapse the "the"s, if all the elements have one
394 0 0 0       if(@hunks > 1 and !grep !m/^the /s, @hunks) {
395 0           for (@hunks) { s/^the //s; }
  0            
396 0           $hunks[0] = 'the '. $hunks[0];
397             }
398            
399 0           push @time_lines, conj_and(@hunks) . ' of';
400             }
401              
402             # Month ###############################################################
403 0 0         if($bits[3][0][0] eq '*') {
404 0 0 0       if(1 == @{$bits[3][0]} or $bits[3][0][1] == 1) {
  0            
405 0           push @time_lines, 'every month';
406             } else {
407 0           push @time_lines, 'every ' . freq($bits[3][0][1]) . ' month';
408             }
409             } else {
410 0           my @hunks;
411 0           foreach my $bit (@{$bits[3]}) {
  0            
412 0 0         if(@$bit == 1) { # "7"
    0          
    0          
413 0   0       push @hunks, $num2month_long{$bit->[0]} || "MONTH_$bit->[0]??"
414             } elsif(@$bit == 2) { # "7-9"
415 0   0       push @hunks, sprintf "from %s to %s",
      0        
416             $num2month_long{$bit->[0]} || "MONTH_$bit->[0]??",
417             $num2month_long{$bit->[1]} || "MONTH_$bit->[1]??",
418             } elsif(@$bit == 3) { # "7-20/2"
419 0 0 0       push @hunks, sprintf "every %d %s from %s to %s",
      0        
420             $bit->[2],
421             $bit->[2] == 1 ? 'month' : 'months',
422             $num2month_long{$bit->[0]} || "MONTH_$bit->[0]??",
423             $num2month_long{$bit->[1]} || "MONTH_$bit->[1]??",
424             }
425             }
426 0           push @time_lines, conj_and(@hunks);
427            
428             # put in semicolons in the case of complex constituency
429             #if($time_lines[-1] =~ m/every|from/) {
430             # $time_lines[-1] =~ tr/,/;/;
431             # s/ (and|or)\b/\; $1/g;
432             #}
433             }
434            
435            
436             # Weekday #############################################################
437             #
438             #
439             #
440             #
441             # From man 5 crontab:
442             # Note: The day of a command's execution can be specified by two fields
443             # -- day of month, and day of week. If both fields are restricted
444             # (ie, aren't *), the command will be run when either field matches the
445             # current time. For example, "30 4 1,15 * 5" would cause a command to
446             # be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
447             #
448             # [But if both fields ARE *, then it just means "every day".
449             # and if one but not both are *, then ignore the *'d one --
450             # so "1 2 3 4 *" means just 2:01, April 3rd
451             # and "1 2 * 4 5" means just 2:01, on every Friday in April
452             # But "1 2 3 4 5" means 2:01 of every 3rd or Friday in April. ]
453             #
454             #
455             #
456             #
457             # And that's a bit tricky.
458            
459 0 0 0       if($bits[4][0][0] eq '*' and (
      0        
460             @{$bits[4][0]} == 1 or $bits[4][0][1] == 1
461             )
462             ) {
463             # Most common case -- any weekday. Do nothing really.
464             #
465             # Hm, does "*/1" really mean "*" here, given the above note?
466             #
467            
468             # Tidy things up while we're here:
469 0 0 0       if($time_lines[-2] eq "every day of" and
470             $time_lines[-1] eq 'every month'
471             ) {
472 0           $time_lines[-2] = "every day";
473 0           pop @time_lines;
474             }
475            
476             } else {
477             # Ugh, there's some restriction on weekdays.
478            
479             # Translate the DOW-expression
480 0           my $expression;
481             my @hunks;
482 0           foreach my $bit (@{$bits[4]}) {
  0            
483 0 0         if(@$bit == 1) {
    0          
    0          
484 0   0       push @hunks, $num2dow_long{$bit->[0]} || "DOW_$bit->[0]??";
485             } elsif(@$bit == 2) {
486 0 0         if($bit->[0] eq '*') { # it's like */3
487             #push @hunks, sprintf "every %s day of the week", freq($bit->[1]);
488             # the above was ambiguous -- "every third day of the week"
489             # sounds synonymous with just "3"
490 0 0         if($bit->[1] eq 2) {
491             # common and unambiguous case.
492 0           push @hunks, "every other day of the week";
493             } else {
494             # rare cases: N > 2
495 0           push @hunks, "every $bit->[1] days of the week";
496             # sounds clunky, but it's a clunky concept
497             }
498             } else {
499             # it's like "7-9"
500 0   0       push @hunks, sprintf "%s through %s",
      0        
501             $num2dow_long{$bit->[0]} || "DOW_$bit->[0]??",
502             $num2dow_long{$bit->[1]} || "DOW_$bit->[1]??",
503             }
504             } elsif(@$bit == 3) { # "7-20/2"
505 0   0       push @hunks, sprintf "every %s %s from %s through %s",
      0        
506             ordinate_soft($bit->[2]), #$bit->[2],
507             'day', #$bit->[2] == 1 ? 'days' : 'days',
508             $num2dow_long{$bit->[0]} || "DOW_$bit->[0]??",
509             $num2dow_long{$bit->[1]} || "DOW_$bit->[1]??",
510             }
511             }
512 0           $expression = conj_or(@hunks);
513            
514             # Now figure where to put it...
515             #
516 0 0         if($time_lines[-2] eq "every day of") {
517             # Unrestricted day-of-month, hooray.
518             #
519 0 0         if($time_lines[-1] eq 'every month') {
520             # change it to "every Tuesday", killing the "of every month".
521 0           $time_lines[-2] = "every $expression";
522 0           $time_lines[-2] =~ s/every every /every /g;
523 0           pop @time_lines;
524             } else {
525             # change it to "every Tuesday in"
526 0           $time_lines[-2] = "every $expression in";
527 0           $time_lines[-2] =~ s/every every /every /g;
528             }
529             } else {
530             # This is the messy case where there's a DOM and DOW
531             # restriction
532            
533             # Was, wrongly:
534             # $time_lines[-1] .= ',';
535             # push @time_lines, "if it's also " . $expression;
536            
537 0           $time_lines[-2] .= " -- or every $expression in --";
538             # Yes, dashes look very strange, but then this is a very
539             # rare case.
540 0           $time_lines[-2] =~ s/every every /every /g;
541             }
542             }
543             #######################################################################
544             }
545             # TODO: change "3pm" -> "the 3pm hour" or something?
546 0           $time_lines[-1] =~ s/ of$//s;
547            
548 0           return @time_lines;
549             }
550              
551             sub esc {
552 0     0 1   our %pretty_form = (
553             '"' => '\"',
554             '\\' => '\\\\',
555             );
556            
557 0           my $x = $_[0];
558            
559 0 0         $x =~ s<([\x00-\x1F"\\])><$pretty_form{$1} || '\\x'.(unpack("H2",$1))>eg;
  0            
560            
561 0           return $x;
562             }
563              
564             # if($time_lines[-1] =~ m/every|from/) {
565             # $time_lines[-1] =~ tr/,/;/;
566             # s/ (and|or)\b/\; $1/g;
567             # }
568              
569             sub conj_and {
570 0 0   0 1   if(grep m/every|from/, @_) {
571             # put in semicolons in the case of complex constituency
572 0 0         return join('; and ', @_) if @_ < 2;
573 0           my $last = pop @_;
574 0           return join('; ', @_) . '; and ' . $last;
575             }
576            
577 0 0         return join(' and ', @_) if @_ < 3;
578 0           my $last = pop @_;
579 0           return join(', ', @_) . ', and ' . $last;
580             }
581              
582             sub conj_or {
583 0 0   0 1   if(grep m/every|from/, @_) {
584             # put in semicolons in the case of complex constituency
585 0 0         return join('; or ', @_) if @_ < 2;
586 0           my $last = pop @_;
587 0           return join('; ', @_) . '; or ' . $last;
588             }
589            
590 0 0         return join(' or ', @_) if @_ < 3;
591 0           my $last = pop @_;
592 0           return join(', ', @_) . ', or ' . $last;
593             }
594              
595             sub ordsuf {
596 0 0 0 0 1   return 'th' if not(defined($_[0])) or not( 0 + $_[0] );
597             # 'th' for undef, 0, or anything non-number.
598 0           my $n = abs($_[0]); # Throw away the sign.
599 0 0         return 'th' unless $n == int($n); # Best possible, I guess.
600 0           $n %= 100;
601 0 0 0       return 'th' if $n == 11 or $n == 12 or $n == 13;
      0        
602 0           $n %= 10;
603 0 0         return 'st' if $n == 1;
604 0 0         return 'nd' if $n == 2;
605 0 0         return 'rd' if $n == 3;
606 0           return 'th';
607             }
608              
609             sub ordinate {
610             # English-language overrides for common ordinals
611 0     0 1   my %ordinations = (
612             '1' => 'first',
613             '2' => 'second',
614             '3' => 'third',
615             '4' => 'fourth',
616             '5' => 'fifth',
617             '6' => 'sixth',
618             '7' => 'seventh',
619             '8' => 'eigth',
620             '9' => 'ninth',
621             '10' => 'tenth',
622             );
623            
624 0   0       my $i = $_[0] || 0;
625            
626 0 0         $ordinations{$i} || ($i . ordsuf($i));
627             }
628              
629             sub freq {
630             # English-language overrides for common ordinals
631 0     0 1   my %ordinations = (
632             '1' => 'first',
633             '2' => 'second',
634             '3' => 'third',
635             '4' => 'fourth',
636             '5' => 'fifth',
637             '6' => 'sixth',
638             '7' => 'seventh',
639             '8' => 'eigth',
640             '9' => 'ninth',
641             '10' => 'tenth',
642             );
643            
644             # frequentive form. Like ordinal, except that 2 -> 'other' (as in every other)
645 0   0       my $i = $_[0] || 0;
646            
647 0 0         return 'other' if $i == 2; # special case
648            
649 0 0         $ordinations{$i} || ($i . ordsuf($i));
650             }
651              
652             sub ordinate_soft {
653 0   0 0 1   my $i = $_[0] || 0;
654 0           $i . ordsuf($i);
655             }
656              
657             sub percent_proc {
658             # Translated literally from the C, cron/do_command.c.
659 0     0 1   my($esc,$need_newline);
660 0           my $out = '';
661 0           my $c;
662 0           for(my $i = 0; $i < length($_[0]); $i++) {
663 0           $c = substr($_[0],$i,1);
664 0 0         if($esc) {
665 0 0         $out .= "\\" unless $c eq '%';
666             } else {
667 0 0         $c = "\n" if $c eq '%';
668             }
669 0 0         unless($esc = ($c eq "\\")) {
670             # For unescaped characters,
671 0           $out .= $c;
672 0           $need_newline = ($c ne "\n");
673             }
674             }
675 0 0         $out .= "\\" if $esc;
676 0 0         $out .= "\n" if $need_newline;
677 0           return $out;
678            
679             # I think this would do the same thing:
680             # $x =~ s/((?:\\\\)+) | (\\\%) | (\%) /$3 ? "\n" : $2 ? '%' : $1/xeg;
681             # But I don't want to think about it, and I need it to work just
682             # as the original does.
683             }
684              
685              
686              
687             1;
688              
689              
690              
691             __END__