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