speedcalc: v0.2 - support miles, yards. support pace.
[obnox/speedcalc.git] / speedcalc
1 #!/usr/bin/env perl
2 # vim:et:sts=4:sw=4:si:fdm=marker:tw=0
3
4 # speedcalc : calculate time, distance, speed
5 # author    : Michael Adam <obnox@samba.org>
6 # license   : GPLv3+
7 # history   :
8 #   v0.1  2005-04-25 initial version
9 #   v0.2  2015-11-03 support yards and miles, support pace
10
11 use strict;
12 use warnings;
13
14 use Getopt::Std;
15 use POSIX qw(floor);
16
17 # configuration {{{ --------------------------------------------
18
19 my $Version = "0.2";
20 my $Version_date = "2015-11-03";
21 my $Email = "obnox\@samba.org";
22 my $Real_name = "Michael Adam";
23
24 # - error constants/messages {{{ -------------------------------
25 use constant ERR_OPTIONS_INPUT_THREE    => 1;
26 use constant ERR_OPTIONS_INPUT_MISS     => 2;
27 use constant ERR_SYNTAX_TIME            => 3;
28 use constant ERR_SYNTAX_SPEED           => 4;
29 use constant ERR_SYNTAX_DISTANCE        => 5;
30 use constant ERR_OUTPUT_FORMAT          => 6;
31 use constant ERR_OUTPUT_FORMAT_SPEED    => 7;
32 use constant ERR_OUTPUT_FORMAT_DISTANCE => 8;
33 use constant ERR_OUTPUT_FORMAT_PRESENT  => 9;
34 use constant ERR_SYNTAX_PRECISION       => 10;
35 use constant ERR_NO_TARGET              => 11;
36
37 my @errors = ();
38 $errors[ERR_OPTIONS_INPUT_THREE]    = "It makes no sense to give all three input options.";
39 $errors[ERR_OPTIONS_INPUT_MISS]     = "Need at least one input option.";
40 $errors[ERR_SYNTAX_TIME]            = "Wrong time format.";
41 $errors[ERR_SYNTAX_SPEED]           = "Wrong speed syntax.";
42 $errors[ERR_SYNTAX_DISTANCE]        = "Wrong distance syntax.";
43 $errors[ERR_SYNTAX_PRECISION]       = "Wrong precision syntax.";
44 $errors[ERR_OUTPUT_FORMAT]          = "Invalid output format.";
45 $errors[ERR_OUTPUT_FORMAT_SPEED]    = "Invalid output format.";
46 $errors[ERR_OUTPUT_FORMAT_DISTANCE] = "Invalid output format.";
47 $errors[ERR_OUTPUT_FORMAT_PRESENT]  = "Option -o only allowed with target SPEED or DISTANCE.";
48 $errors[ERR_NO_TARGET]              = "No target given.";
49 # - error contstants/messages }}}
50
51 # input/target entities
52 use constant TIME     => 1;
53 use constant DISTANCE => 2;
54 use constant SPEED    => 3;
55
56 # clear text names:
57 my @Entity_text;
58 $Entity_text[TIME]     = "time";
59 $Entity_text[DISTANCE] = "distance";
60 $Entity_text[SPEED]    = "speed";
61
62 # option letter for command line:
63 my @Entity_option;
64 $Entity_option[TIME]     = "t";
65 $Entity_option[SPEED]    = "s";
66 $Entity_option[DISTANCE] = "d";
67
68 # regexes for parsing cmd line:
69 my @Entity_pattern;
70 $Entity_pattern[TIME]     = '^(\d+(?:\.\d+)?)([smhdw]?)$';
71 $Entity_pattern[SPEED]    = '^(\d+(?:\.\d+)?)(m\/s|km\/h|yd\/s|mi\/h|s\/m|s\/yd|s\/km|s\/mi)?$';
72 $Entity_pattern[DISTANCE] = '^(\d+(?:\.\d+)?)(yd|m|km|mi)?$';
73
74 # helper regex to separate pace format for speed (reciprocal):
75 my $Entity_pattern_speed_reciprocal = '^(.*)\/(m|yd|km|mi)$';
76
77 my @Entity_syntax_error;
78 $Entity_syntax_error[TIME]     = ERR_SYNTAX_TIME;
79 $Entity_syntax_error[SPEED]    = ERR_SYNTAX_SPEED;
80 $Entity_syntax_error[DISTANCE] = ERR_SYNTAX_DISTANCE;
81
82 my $Debug = 0;
83 my $Precision = 2;
84 my $Recalculate = 0;
85 my $Target;
86 my @Values;
87
88 # default unit for in-/output:
89 my @Default_unit;
90 $Default_unit[TIME]     = "s";
91 $Default_unit[DISTANCE] = "m";
92 $Default_unit[SPEED]    = "km/h";
93
94 # all units are converted to this base unit first:
95 my @Base_unit;
96 $Base_unit[TIME]     = "s";
97 $Base_unit[DISTANCE] = "m";
98 $Base_unit[SPEED]    = "m/s";
99
100 # factor to get base-unit from given unit:
101 my @Unit_factor;
102 $Unit_factor[TIME] = {
103     "s" => 1,
104     "m" => 60,
105     "h" => 3600,
106     "d" => 86400,
107     "w" => 604800,
108 };
109 $Unit_factor[DISTANCE] = {
110     "yd" => 0.9144,
111     "m" => 1,
112     "km" => 1000,
113     "mi" => 1609.344,
114 };
115 $Unit_factor[SPEED] = {
116     "m/s"  => $Unit_factor[DISTANCE]->{"m"}  / $Unit_factor[TIME]->{"s"},
117     "km/h" => $Unit_factor[DISTANCE]->{"km"} / $Unit_factor[TIME]->{"h"},
118     "yd/s" => $Unit_factor[DISTANCE]->{"yd"} / $Unit_factor[TIME]->{"s"},
119     "mi/h" => $Unit_factor[DISTANCE]->{"mi"} / $Unit_factor[TIME]->{"h"},
120     # pace options (reciprocal)
121     "s/m"  => $Unit_factor[DISTANCE]->{"m"}  / $Unit_factor[TIME]->{"s"},
122     "s/yd" => $Unit_factor[DISTANCE]->{"yd"} / $Unit_factor[TIME]->{"s"},
123     "s/km" => $Unit_factor[DISTANCE]->{"km"} / $Unit_factor[TIME]->{"s"},
124     "s/mi" => $Unit_factor[DISTANCE]->{"mi"} / $Unit_factor[TIME]->{"s"},
125     #
126     "/m"  => $Unit_factor[DISTANCE]->{"m"}  / $Unit_factor[TIME]->{"s"},
127     "/yd" => $Unit_factor[DISTANCE]->{"yd"} / $Unit_factor[TIME]->{"s"},
128     "/km" => $Unit_factor[DISTANCE]->{"km"} / $Unit_factor[TIME]->{"s"},
129     "/mi" => $Unit_factor[DISTANCE]->{"mi"} / $Unit_factor[TIME]->{"s"},
130 };
131
132 my $Speed_reciprocal = {
133     "m/s"  => 0,
134     "km/h" => 0,
135     "yd/s" => 0,
136     "mi/h" => 0,
137     "s/m"  => 1,
138     "s/yd" => 1,
139     "s/km" => 1,
140     "s/mi" => 1,
141     "/m"  => 1,
142     "/yd" => 1,
143     "/km" => 1,
144     "/mi" => 1,
145 };
146
147 # text output strings for units:
148 my @Unit_text;
149 $Unit_text[TIME] = {
150     "s" => "seconds",
151     "m" => "minutes",
152     "h" => "hours",
153     "d" => "days",
154     "w" => "weeks",
155 };
156 $Unit_text[SPEED] = {
157     "km/h" => "km/h",
158     "m/s"  => "m/sec",
159     "yd/s" => "yd/sec",
160     "mi/h" => "mph",
161     # pace
162     "s/m"  => "sec/m",
163     "s/yd" => "sec/yd",
164     "s/km" => "sec/km",
165     "s/mi" => "sec/mi",
166     # pace, for formatting time with format_time
167     "/m"  => "/m",
168     "/yd" => "/yd",
169     "/km" => "/km",
170     "/mi" => "/mi",
171
172 };
173 $Unit_text[DISTANCE] = {
174     "yd" => "yards",
175     "m" => "meters",
176     "km" => "kilometers",
177     "mi" => "miles",
178 };
179
180 my $Output_unit;
181 my @Output_unit_pattern;
182 $Output_unit_pattern[SPEED]    = '^(km\/h|m\/s|yd\/s|mi\/h|s\/m|s\/yd|s\/km|s\/mi|\/m|\/yd|\/km|\/mi)$';
183 $Output_unit_pattern[DISTANCE] = '^(yd|m|km|mi)$';
184
185 # configuration }}}
186 # analyse options {{{ ------------------------------------------
187
188 # -t <time>
189 # -d <distance>
190 # -s <speed>
191 # -p <precision>
192 # -o <output unit>
193 # -v <verbose>
194 # -h
195
196 my %options = ();
197
198 getopts("t:d:s:p:o:vh", \%options);
199
200 if (keys %options == 0 or $options{h}) {
201     help();
202 }
203
204 # - determine debug {{{ ----------------------------------------
205
206 if ($options{v}) {
207     $Debug = 1;
208     mydebug("debug: on.\n");
209 }
210
211 # - determine debug }}}
212 # - determine target {{{ ---------------------------------------
213
214 if (exists($options{t}) and exists($options{s}) and exists($options{d})) {
215     error(ERR_OPTIONS_INPUT_THREE);
216 }
217
218 elsif (exists($options{t}) and exists($options{d})) {
219     $Target = SPEED;
220 }
221 elsif (exists($options{t}) and exists($options{s})) {
222     $Target = DISTANCE;
223 }
224 elsif (exists($options{d}) and exists($options{s})) {
225     $Target = TIME;
226 }
227
228 else {
229     $Recalculate = 1;
230     if (exists($options{s})) {
231         $Target = SPEED;
232     }
233     elsif (exists($options{d})) {
234         $Target = DISTANCE;
235     }
236     elsif (exists($options{t})) {
237         $Target = TIME;
238     }
239     else {
240         error(ERR_OPTIONS_INPUT_MISS);
241     }
242 }
243 mydebug("target: $Target\n");
244
245 # - determine target }}}
246 # - get values {{{ ---------------------------------------------
247
248 get_value(TIME);
249 get_value(SPEED);
250 get_value(DISTANCE);
251
252 # - get values }}}
253 # - output options {{{ -----------------------------------------
254
255 if ($options{o}) {
256     if ($Target == SPEED or $Target == DISTANCE ) {
257         if ($options{o} =~ /$Output_unit_pattern[$Target]/ ) {
258             $Output_unit = $1;
259         }
260         else {
261             error(ERR_OUTPUT_FORMAT);
262         }
263     }
264     else {
265         error(ERR_OUTPUT_FORMAT_PRESENT);
266     }
267 }
268 else {
269     $Output_unit = $Default_unit[$Target];
270 }
271 mydebug("output unit: $Output_unit\n");
272
273 # - output options }}}
274 # - determine precision {{{ ------------------------------------
275
276 if ($options{p}) {
277     if ($options{p} =~ /^\d+$/) {
278         $Precision = $options{p};
279     }
280     else {
281         error(ERR_SYNTAX_PRECISION);
282     }
283 }
284 mydebug("precision: $Precision\n");
285
286 # - determine precision }}}
287
288 # analyse options }}}
289 # action {{{ ---------------------------------------------------
290
291 calculate($Target);
292 mydebug("Target: $Values[$Target] $Base_unit[$Target]\n");
293
294 if ($Target == TIME) {
295     print format_time($Values[TIME]) . "\n";
296 }
297 else {
298     my $Output_value = $Values[$Target] / $Unit_factor[$Target]->{$Output_unit};
299     if (($Target == SPEED) and ($Speed_reciprocal->{$Output_unit})) {
300         $Output_value = 1 / $Output_value;
301         if ($Output_unit =~ /^\//) {
302             print format_time($Output_value) . "$Output_unit\n";
303         }
304         else {
305             printf "%." . $Precision . "f "
306                    . $Unit_text[SPEED]->{$Output_unit} . "\n", $Output_value;
307         }
308     }
309     else {
310         printf "%." . $Precision . "f "
311                . $Unit_text[$Target]->{$Output_unit} . "\n", $Output_value;
312     }
313 }
314
315 # action }}}
316 # functions {{{ ------------------------------------------------
317
318 sub calculate {
319     my $target = shift;
320     my $value;
321     unless ($Recalculate) {
322         if ($target == DISTANCE) {
323             $value = $Values[SPEED] * $Values[TIME];
324         }
325         elsif ($target == SPEED) {
326             $value = ( $Values[DISTANCE] / $Values[TIME]);
327         }
328         elsif ($target == TIME) {
329             $value = $Values[DISTANCE] / $Values[SPEED];
330         }
331         else {
332             error(ERR_NO_TARGET);
333         }
334         $Values[$target] = $value;
335     }
336 }
337
338 sub get_value {
339     my $entity = shift;
340     if ($options{$Entity_option[$entity]}) {
341         if ($entity == TIME) {
342             my $time_str = $options{$Entity_option[TIME]};
343             $Values[TIME] = parse_time($time_str);
344         }
345         elsif (($entity == SPEED) and
346                ($options{$Entity_option[SPEED]} =~ /$Entity_pattern_speed_reciprocal/))
347         {
348             my $time_str = $1;
349             my $dist_unit = $2;
350             my $time = parse_time($time_str);
351             $Values[SPEED] = $Unit_factor[DISTANCE]->{$dist_unit} / $time;
352
353             mydebug("parsing reciprocal speed:\n");
354             mydebug(" - time_str:  $time_str\n");
355             mydebug(" - time:      $time sec\n");
356             mydebug(" - dist_unit: $dist_unit\n");
357         }
358         elsif ($options{$Entity_option[$entity]} =~ /$Entity_pattern[$entity]/) {
359             my $value = $1;
360             my $unit = $2 || $Default_unit[$entity];
361             $Values[$entity] = $value * $Unit_factor[$entity]->{$unit};
362         }
363         else {
364             error($Entity_syntax_error[$entity]);
365         }
366         mydebug("$Entity_text[$entity]: $Values[$entity] " .
367                 $Unit_text[$entity]->{$Base_unit[$entity]} . "\n");
368     }
369 }
370
371 sub mydebug {
372     if ($Debug) {
373         print @_;
374     }
375 }
376
377 sub help {
378     print "\n";
379     print "speedcalc - time/distance/speed calculator\n";
380     print "\n";
381     print "v$Version $Version_date $Real_name <$Email>\n";
382     print "\n";
383     usage();
384     print "\n";
385     exit(0);
386 }
387
388 sub error {
389     my $err_code = shift;
390     print "\n";
391     print "ERROR: $errors[$err_code]\n";
392     print "\n";
393     usage();
394     print "\n";
395     exit($err_code);
396 }
397
398 sub usage {
399     print <<EOF;
400 USAGE: speedcalc [-t <time>] [-d <dist>] [-s <speed>] [-p <num>] [-o <unit>] [-v] [-h]
401 EOF
402 }
403
404 sub format_time {
405     my $time = shift;
406     # round correctly:
407     $time = floor($time + 0.5);
408     my $format = "";
409     my $sec = $time % 60;
410     $time = ( $time - $sec ) / 60;
411     if ($time) {
412         $format = sprintf("%02ds", $sec);
413         my $min = $time % 60;
414         $time = ($time - $min ) / 60;
415         if ($time) {
416             $format = sprintf("%02dm$format", $min);
417             my $hour = $time % 24;
418             $time = ($time - $hour) / 24;
419             if ($time) {
420                 $format = sprintf("%02dh$format", $hour);
421                 my $day = $time % 7;
422                 $time = ($time - $day) / 7;
423                 if ($time) {
424                     $format = sprintf("%02dd$format", $day);
425                     $format = $time . "w$format";
426                 }
427                 else {
428                     $format = $day . "d$format";
429                 }
430             }
431             else {
432                 $format = $hour . "h$format";
433             }
434         }
435         else {
436             $format = $min . "m$format";
437         }
438     }
439     else {
440         $format = $sec . "s";
441     }
442     return $format;
443 }
444
445 # parse time in format [Xw][Xd][Xh][Xm][X[.Y][s]] and return seconds
446 sub parse_time {
447     my $timestr = shift;
448     my $seconds = 0;
449     foreach my $unit (qw(w d h m)) {
450         if ($timestr =~ s/^(\d+)$unit(.*)$/$2/) {
451             $seconds += $1 * $Unit_factor[TIME]->{$unit};
452         }
453     }
454     if ($timestr =~ /^(\d+(?:\.\d+)?)s?$/ ) {
455         $seconds += $1;
456     }
457     elsif ($timestr) {
458         error(ERR_SYNTAX_TIME);
459     }
460     return $seconds;
461 }
462
463 # functions }}}
464
465 # ENTE