File Coverage

blib/lib/Time/Decimal.pm
Criterion Covered Total %
statement 9 90 10.0
branch 0 42 0.0
condition 0 6 0.0
subroutine 3 16 18.7
pod 1 11 9.0
total 13 165 7.8


line stmt bran cond sub pod time code
1             #! /usr/local/bin/perl
2              
3             =encoding utf-8
4              
5             =head1 NAME
6              
7             Time::Decimal -- Handle french revolutionary ten hour days
8              
9             L|POD2::EO::Time::Decimal>
10              
11              
12              
13             =head1 SYNOPSIS
14              
15             use Time::Decimal qw($precision h24s_h10 h24_h10 h10s_h24 h10_h24
16             transform now_h10 loop);
17             $precision = 'ms';
18              
19             $dec = h24s_h10( 1234.5678 );
20             $dec = h24_h10( 13, 23, 45, 345_678 );
21             $bab = h10s_h24( 1234.5678 );
22             $bab = h10_h24( 1, 50, 75, 345_678 );
23              
24             $dec = transform( '13:23' );
25             $dec = transform( '1:23:45.345_678 pm' );
26             $bab = transform( '1_50_75.345_678' );
27              
28             $dec = now_h10;
29             $dec = now_h10( time + 60 );
30              
31             $precision = 's';
32             loop { print "$_[0]\t" . localtime() . "\n" };
33              
34             or
35              
36             perl /Time/Decimal.pm [-option ...] [time ...]
37             ln /Time/Decimal.pm dectime
38             dectime [-option ...] [time ...]
39              
40              
41              
42             =head1 DESCRIPTION
43              
44             The Babyloninan 24 hour clock is one of the last complicated vestiges of the
45             pre-decimal age. The french revolution, when it created decimal measures for
46             everything, also invented a division of the day into ten hours of 100 minutes
47             and again 100 seconds each. The nice thing is that seconds and (to a lesser
48             degree) minutes are roughly as long as those we know. Hours are of course
49             more than twice as long.
50              
51             So as to be able to automatically recognize decimal time, we use C<_> instead
52             of C<:> as a separator. This character is usable in many more computer
53             contexts. In Perl it is a possible separator between digits. And that's what
54             it means here, because a decimal time H_MM is nothing else than a three digit
55             number of minutes. The same applies to five digit numbers of seconds.
56              
57             For the purpose of transformation it doesn't matter whether we see 1:30 as an
58             early morning time, or as a duration of one and a half hours. Thus a time
59             like 84:00 or 35_00 meaning three and a half days is allowed.
60              
61             =cut
62              
63             package Time::Decimal;
64              
65 1     1   831 use warnings;
  1         2  
  1         40  
66 1     1   6 use strict;
  1         2  
  1         1736  
67              
68             our $VERSION = 0.07;
69              
70             sub FACTOR() { .86400 } # One day has 86400 babylonian seconds.
71              
72             =head2 Module Interface
73              
74             Nothing is exported by default, but all of the following may be imported by
75             the C statement:
76              
77             =over
78              
79             =item $precision
80              
81             '' minutes (the default)
82             's' seconds
83             'ds' deciseconds
84             'cs' centiseconds
85             'ms' milliseconds
86             'µs', 'us' microseconds
87              
88             Where the µ-sign may be in UTF-8, Latin-1, -3, -5, -7 or Latin-9.
89              
90             =cut
91              
92             our $precision = '';
93              
94             # Format seconds in the range 0 <= $sec < $modulo as two digits plus fraction
95             # as mandated by $precision. Seconds are truncated, but fractions are rounded.
96             # If seconds were almost $modulo, but for floating imprecision, they are incremented
97             # and the fraction becomes .0, which may lead to an overflow, which is why we want
98             # a reference to $min. These rules are too complex to be handled by sprintf.
99             {
100             my %fmt = qw(ds %.1f
101             cs %.2f
102             ms %.3f
103             µs %f
104             us %f);
105             $fmt{"\xb5s"} = '%f'; # Latin-[13579] µ
106             sub _seconds(\$$$) {
107 0     0     my( $minref, $modulo, $sec ) = @_;
108 0 0         if( $precision ) {
109 0 0         if( $precision eq 's' ) {
110 0           my $usec = $sec - int $sec;
111 0           $sec = int $sec;
112 0 0 0       if( $usec > .999_999 && ++$sec == $modulo ) { # Compensate float fuzzyness.
113 0           $sec = 0;
114 0           $$minref++;
115             }
116             } else {
117 0           $sec = sprintf $fmt{$precision}, $sec;
118 0 0         if( $sec == $modulo ) { # Rounding overflowed.
119 0           $sec = sprintf $fmt{$precision}, 0;
120 0           $$minref++;
121             }
122 0 0 0       substr( $sec, -3, 0 ) = '_'
123             if $precision eq 'µs' || $precision eq 'us';
124             }
125 0 0         $sec = "0$sec" if eval $sec < 10; # eval understands '_'
126 0           $sec;
127             } else {
128 0 0         $$minref++ if sprintf( '%f', $sec ) == $modulo;
129 0           '';
130             }
131             }
132             }
133              
134              
135             sub h10s_h10($) {
136 0     0 0   my $sec = $_[0];
137 0           my $min = int $sec / 100;
138 0           $sec = _seconds $min, 100, $sec - 100 * $min;
139 0           $min = sprintf "%d_%02d", $min / 100, $min % 100;
140 0 0         $min .= "_$sec" if $precision;
141 0           $min;
142             }
143              
144             sub h24s_h10($) {
145 0     0 0   h10s_h10 $_[0] / FACTOR;
146             }
147              
148             sub h24_h10(@) {
149 0     0 0   my( $h, $min, $sec, $usec ) = (@_, 0, 0, 0, 0);
150 0           h24s_h10 $h * 3600 + $min * 60 + $sec + .000_001 * $usec;
151             }
152              
153              
154             sub h10s_h24($) {
155 0     0 0   my $sec = $_[0] * FACTOR;
156 0           my $min = int $sec / 60;
157 0           $sec = _seconds $min, 60, $sec - 60 * $min;
158 0           $min = sprintf "%02d:%02d", $min / 60, $min % 60;
159 0 0         $min .= ":$sec" if $precision;
160 0           $min;
161             }
162              
163             sub h10_h24(@) {
164 0     0 0   my( $h, $min, $sec, $usec ) = (@_, 0, 0, 0, 0);
165 0           h10s_h24 $h * 10000 + $min * 100 + $sec + .000_001 * $usec;
166             }
167              
168              
169             # Perl is fussy about what strings it accepts as a number. We allow both
170             # leading zeroes(not as octal) and underscores, which Perl's @#!% string to
171             # number automatism refuses to accept, unlike in literal numbers.
172             sub _cleanup($) {
173 0 0   0     if ( $_[0] ) {
174 0           for ( my $copy = $_[0] ) {
175 0           tr/_//d;
176 0           s/^0+(?=.)//;
177 0           return $_ + 0;
178             }
179             } else {
180 0           '00';
181             }
182             }
183              
184             my $h10re = qr/^(\d+) _ (\d\d) (?: _ (\d\d (?: \.\d+_?\d* )?) )?$/x;
185             sub transform($) {
186 0 0   0 0   if( $_[0] =~ /^(\d+) : ([0-5]\d) (?: : ([0-5]\d (?: \.\d+_?\d* )?) )? \s*(?:(am)|(pm))? $/ix ) {
    0          
187 0 0         h24_h10 $4 ? $1 % 12 : $5 ? $1 % 12 + 12 : $1, $2, _cleanup $3;
    0          
188             } elsif( $_[0] =~ /$h10re/o ) {
189 0           h10_h24 $1, $2, _cleanup $3;
190             } else {
191 0           die "$0: invalid time format `$_[0]'\n";
192             }
193             }
194              
195             sub h10_h10s($) {
196 0     0 0   $_[0] =~ /$h10re/o;
197 0           0 + ($1 . $2 . _cleanup $3);
198             }
199              
200             sub difference(@) {
201 0     0 0   my $acc;
202 0           for( @_ ) {
203 0 0         my $sec = h10_h10s( /:/ ? transform $_ : $_ );
204 0 0         if( defined $acc ) {
205 0           $acc -= $sec;
206             } else {
207 0           $acc = $sec;
208             }
209             }
210 0           h10s_h10 $acc;
211             }
212              
213             sub sum(@) {
214 0     0 0   my $acc = 0;
215 0           for( @_ ) {
216 0 0         $acc += h10_h10s( /:/ ? transform $_ : $_ );
217             }
218 0           h10s_h10 $acc;
219             }
220              
221              
222             sub now_h10(;$) {
223             my( $usec, $sec, $min, $h ) = @_ ? @_ :
224 0 0   0 0   do { require Time::HiRes; Time::HiRes::time() };
  0            
  0            
225 0           $sec = int $usec;
226 0           $usec -= $sec;
227 0           ($sec, $min, $h) = localtime $sec;
228 0           h24_h10 $h, $min, $sec + $usec;
229             }
230              
231             =item loop { I }
232              
233              
234              
235             =cut
236              
237             my %delta = ('' => 100,
238             s => 1,
239             ds => .1,
240             cs => .01,
241             ms => .001,
242             'µs' => .000_001,
243             us => .000_001,
244             "\xb5s" => .000_001); # Latin-[13579] µ
245             sub loop(&) {
246 0     0 1   my $callback = $_[0];
247 0           require Time::HiRes;
248 0           my $last = '';
249 0           while( 1 ) {
250 0           my( $usec, $sec, $min, $h ) = Time::HiRes::time();
251 0           my $orig = $usec;
252 0           $sec = int $usec;
253 0           $usec -= $sec;
254 0           ($sec, $min, $h) = localtime $sec;
255 0           $sec = $h * 3600 + $min * 60 + $sec + $usec;
256 0           my $cur = h24s_h10( $sec );
257 0 0         redo if $cur eq $last; # Rarely select sleeps a bit too short, how about T::HR::sleep?
258 0 0         last if !&$callback( $cur );
259 0           $last = $cur;
260 0           $sec = ($sec / FACTOR + $delta{$precision}) / $delta{$precision};
261 0           $sec = $orig + (1 - $sec + int $sec) * $delta{$precision} * FACTOR -
262             Time::HiRes::time(); # Compensate callback time and our overhead
263 0 0         Time::HiRes::sleep( $sec ) if $sec > 0; # Callback may have taken longer than 1 unit
264             }
265             }
266              
267             =item See SYNOPSIS above
268              
269             I
270              
271             =back
272              
273              
274              
275             =head2 Command Line Interface
276              
277             =over
278              
279             =item -s, --seconds
280              
281             =item -d, --ds, --deciseconds
282              
283             =item -c, --cs, --centiseconds
284              
285             =item -m, --ms, --milliseconds
286              
287             =item -u, --us, --microseconds
288              
289             Output times at the given precision, instead of minutes.
290              
291              
292             =item -e, --echo
293              
294             Output the transformed time along with the transformation.
295              
296              
297             =item -r, --reverse
298              
299             Retransform the transformation to see possible loss due to insufficient
300             precision.
301              
302              
303             =item -l, --loop
304              
305             Output the time again each time the result changes at the wanted precision.
306             Can be used as a clock, but if the precision is too small, the terminal
307             emulation may have problems, either flickering or repeatedly stalling (C
308             family).
309              
310              
311             =item -o, --old, --old-table, --babylonian, --babylonian-table
312              
313             =item -n, --new, --new-table, --decimal, --decimal-table
314              
315             Supplies overviews of about 70 times of common interest each. Implies
316             C<--echo>.
317              
318             =back
319              
320             =cut
321              
322             if( caller ) {
323 1     1   16 use Exporter 'import';
  1         2  
  1         926  
324             our @EXPORT_OK = qw($precision h24s_h10 h24_h10 h10s_h24 h10_h24
325             transform now_h10 loop);
326             } else {
327             require Getopt::Long;
328             Getopt::Long::config( qw(bundling no_getopt_compat require_order) );
329              
330             my( $echo, $reverse, $loop );
331             Getopt::Long::GetOptions
332             ('s|seconds' => sub { $precision = 's' },
333             'd|ds|deciseconds' => sub { $precision = 'ds' },
334             'c|cs|centiseconds' => sub { $precision = 'cs' },
335             'm|ms|milliseconds' => sub { $precision = 'ms' },
336             'u|us|microseconds' => sub { $precision = 'µs' },
337              
338             'e|echo' => \$echo,
339             'r|reverse' => \$reverse,
340             'l|loop' => \$loop,
341              
342             'o|old|old-table|babylonian|babylonian-table' =>
343             sub { $echo = push @ARGV,
344             sort map( ("00:00:0$_", "00:0$_:00", "0$_:00:00", "0$_:30:00"), 1..9 ),
345             map( ("00:${_}0:00", "00:${_}5:00", "00:00:${_}0", "00:00:${_}5"), 1..5 ),
346             map "$_:00:00", 10..23 },
347             'n|new|new-table|decimal|decimal-table' =>
348             sub { $echo = push @ARGV,
349             sort map( ("0_00_0${_}", "0_00_${_}0", "0_00_${_}5",
350             "0_0${_}_00", "0_${_}0_00", "0_${_}5_00",
351             "${_}_00_00", "${_}_50_00"), 1..9 ) } );
352             if( @ARGV ) {
353             for( @ARGV ) {
354             print "$_ ->\t" if $echo;
355             print $_ = transform( $_ );
356             print " ->\t", transform( $_ ) if $reverse;
357             print "\n";
358             }
359             } elsif( $loop ) {
360             $| = 1;
361             my $callback = -t STDOUT ? sub { print "\r$_[0]" } : sub { print "$_[0]\n" };
362             loop \&$callback;
363             } else {
364             print now_h10, "\n";
365             }
366             }
367              
368             1;
369             __END__