release 1.05pre
[descalc.git] / sdc.pl
1 #!/usr/bin/perl
2
3 ### curses rpn desktop calculator ###
4
5 # by Shiar <shiar.org>
6
7 # 06-18       - start
8 # 06-25       -
9 # 08-04 14:45 - error dialog (don't mess up screen)
10
11 use strict;
12 use warnings;
13 use utf8;
14
15 use Term::ReadKey;
16 use Curses;
17
18 initscr;
19 ReadMode 3;  # cbreak mode
20 END {
21         ReadMode 0;
22         endwin;
23 } # restore terminal on quit
24
25 my $height = $LINES<3 ? 4 : $LINES-3;  # stack depth (lines of stack plus one)
26 my $width = $COLS || 42;  # limit value precision, stetch menu
27
28 my %val = qw(i 0  frac 0);  # i, frac
29 my @stack;
30 my %var;
31 my @menu;
32 my $menumin;
33 my %set = (
34         base     => 10,
35         numb     =>  0,  # fixed scientific engineering
36         card     =>  1,  # degrees radians grades
37         coord    =>  0,  # cartesian polar spherical
38         complex  =>  0,  # real complex
39         menushow =>  6,
40 ); # %set
41
42 my @menus = (
43         [qw(refresh math>8 prog> mode>7 unit>11)],
44         [qw(main>0 log alog ln exp sin cos tan 0 asin acos atan)], #1 math
45         [qw(main>0 dec bin oct hex logic>3 bit>4)], #2 base
46         [qw(base>2 and or xor not)], #3 base logic
47         [qw(base>2 rl sl asr sr rr)], #4 base bit
48         [qw(base>2 rlb slb srb rrb)], #5 base byte
49         [qw(main>0 sq sqrt ^ xroot)], #6
50         [qw(main>0 number_format angle_measure coord_system)], #7 mode
51         [qw(main>0
52                 vector> matrix> list> hyperbolic>9 real>10 base>2
53                 probability> fft> complex> constants>
54         )], #8 math
55         [qw(math>8
56                 sinh cosh tanh asinh acosh atanh
57                 expm lnp1
58         )], #9 math hyperbolic
59         [qw(math>8
60                 % %ch %t min max mod
61                 abs sign mant xpon ip fp
62                 rnd trnc floor ceil r>d d>r
63         )], #10 math real
64         [qw(main>0
65                 tools> length>12 area>13 volume>14 time>15 speed>16
66                 mass>17 force>18 energy>19 power>20 pressure>21 temperature>22
67                 electric_current>23 angle>24 light>25 radiation>26 viscosity>27
68         )], #11 units
69 #               mm cm m in ft yd km mile mmile lt-yr mil Ang fermi rod fath)],
70         [qw(unit>11
71                 _m _cm _mm _yd _ft _in _Mpc _pc _lyr _au _km _mi
72                 _nmi _miUS _chain _rd _fath _ftUS _Mil _μ _Å _fermi
73         )], #12 length
74         [qw(unit>11
75                 _m^2 _cm^2 _b _yd^2 _ft^2 _in^2
76                 _km^2 _ha _a _mi^2 _miUS^2 _acre
77         )], #13 area
78         [qw(unit>11
79                 _m^3 _st _cm^3 _yd^3 _ft^3 _in^3
80                 _l _galUK _galC _gal _qt _pt
81                 _ml _cu _ozfl _ozUK _tbsp _tsp
82                 _bbl _bu _pk _fbm
83         )], #14 volume
84         [qw(unit>11
85                 _yr _d _h _min _s _Hz
86         )], #15 time
87         [qw(unit>11
88                 _m/s _cm/s _ft/s _kph _mph _knot
89                 _c _ga
90         )], #16 speed
91         [qw(unit>11
92                 _kg _g _Lb _oz _slug _lbt
93                 _ton _tonUS _t _ozt _ct _grain
94                 _u _mol
95         )], #17 mass
96         [qw(unit>11
97                 _N _dyn _gf _kip _lbf _pdl
98         )], #18 force
99         [qw(unit>11
100                 _J _erg _Kcal _Cal _Btu _ftxlbf
101                 _therm _MeV _eV
102         )], #19 energy
103         [qw(unit>11
104                 _W _hp
105         )], #20 power
106         [qw(unit>11
107                 _Pa _atm _bar _psi _torr _mmHg
108                 _inHg _inH2O
109         )], #21 pressure
110         [qw(unit>11
111         )], #22 temperature
112         [qw(unit>11
113         )], #23 electric_current
114         [qw(unit>11
115         )], #24 angle
116         [qw(unit>11
117         )], #25 light
118         [qw(unit>11
119         )], #26 radiation
120         [qw(unit>11
121         )], #27 viscosity
122
123
124
125 ); # @menus
126 @menu = @{$menus[0]};
127 $menumin = 0;
128
129 my %falias = (
130         "\033"                         =>  0, # esc
131         "\033\117\120"                 =>  1, # f1
132         "\033\133\061\061\176"         =>  1, # f1
133         "\033\133\061\062\176"         =>  2, # f2
134         "\033\133\061\063\176"         =>  3, # f3
135         "\033\133\061\064\176"         =>  4, # f4
136         "\033\117\121"                 =>  2, # f2
137         "\033\117\122"                 =>  3, # f3
138         "\033\117\123"                 =>  4, # f4
139         "\033\133\061\065\176"         =>  5, # f5
140         "\033\133\061\067\176"         =>  6, # f6
141         "\033\133\061\070\176"         =>  7, # f7
142         "\033\133\061\071\176"         =>  8, # f8
143         "\033\133\062\060\176"         =>  9, # f9
144         "\033\133\062\061\176"         => 10, # f10
145         "\033\133\062\063\176"         => 11, # f11/F1
146         "\033\133\062\064\176"         => 12, # f12/F2
147         "\033\133\062\065\176"         => 13, # F3
148         "\033\133\062\066\176"         => 14, # F4
149         "\033\133\062\070\176"         => 15, # F5
150         "\033\133\062\071\176"         => 16, # F6
151         "\033\133\063\061\176"         => 17, # F7
152         "\033\133\063\062\176"         => 18, # F8
153         "\033\133\063\063\176"         => 19, # F9
154         "\033\133\063\064\176"         => 20, # F10
155         "\033\133\062\063\073\062\176" => 21, # F11
156         "\033\133\062\064\073\062\176" => 22, # F12
157 ); # %falias
158
159 my %alias = (
160         chr 4 => 'quit', # ^D
161         chr 9 => 'more', # tab
162         '_' => 'chs', # change sign; 48: y
163         'e' => 'eex', # exponent; 48: z
164 #       "\033\133\062\176" => 'swap', # ins
165         chr(27).chr(91).chr(51).chr(126) => 'clear', # del
166         chr 127 => 'drop', # backspace
167         chr 8 => 'drop', # backspace
168         chr 13 => ' ', # enter
169         "\014" => 'refresh', # ^L
170 #       "\033\133\110" => 'refresh', # home
171
172 #       "\033\133\101" => '', # up; 48: k (stack)
173 #       "\033\133\104" => '', # left; 48: p (picture)
174 #       "\033\133\102" => '', # down; 48: q (view)
175         "\033\133\103" => 'swap', # right; 48: r (swap)
176
177         '&' => 'and',
178         '|' => 'or',
179         '#' => 'xor',
180         '~' => 'not',
181
182                 's' => 'sin',
183         chr(27).'s' => 'asin',
184                 'c' => 'cos',
185         chr(27).'c' => 'acos',
186                 't' => 'tan',
187         chr(27).'t' => 'atan',
188                 'l' => 'log',
189         chr(27).'l' => 'alog',
190                 'n' => 'ln',
191         chr(27).'n' => 'exp',
192                 'q' => 'sq',
193         chr(27).'q' => 'sqrt',
194         chr(27).'^' => 'xroot',
195 ); # %alias
196
197 =cut
198 HP48 keys:
199     S     T     U      V     W     X
200  -  sin   cos   tan    sqrt  ^     1/x
201  <  asin  acos  atan   sq    alog  exp
202  >  [a]   ∫     ∑      xroot log   ln
203 =cut
204
205 my %action = (
206         'more' => [0, sub {
207                 $menumin += $set{menushow};
208                 $menumin = 0 if $menumin>=$#menu;
209                 showmenu();
210         }], # tab
211         'digit'=> [-2, sub { $val{i} = ($val{frac} *= 10) ? $val{i}+$_/$val{frac} : $val{i}*10+$_ }],
212         '.'    => [-2, sub { $val{frac} = 1 }], # decimal point
213         'eex'  => [-2, sub {}], # exponent
214         'chs'  => [0, sub {
215                 if (defined $val{i}) {
216                         $val{i} = -$val{i};
217                 } else {
218                         $stack[0] = -$stack[0];
219                 }
220         }], # negative
221
222         'drop' => [0, sub {
223                 if (defined $val{i}) {
224                         $val{i} = ($val{frac} = int $val{frac}/10)
225                                 ? int($val{i}*$val{frac})/$val{frac} : int $val{i}/10
226                 } else {
227                         shift @stack;
228                 }
229         }], # backspace
230         'clear'  => [0, sub {
231                 #todo: if (val{i}) delete char after cursor
232                 @stack = (); %val = (i=>undef, frac=>0)
233         }], # clear all
234
235         ' '    => [0, sub {
236                 unshift @stack, $val{i};
237                 %val = (i=>undef, frac=>0);
238         }], # duplication
239
240         'swap' => [1, sub {@stack[0, 1] = @stack[1, 0]}], # swap x<->y
241
242         '='    => [1, sub {$var{a} = $stack[0]}], # copy
243         '>'    => [1, sub {$var{a} = shift @stack}], # assign
244
245         '+'    => [2, sub {$stack[1] += shift @stack}], # addition
246         '-'    => [2, sub {$stack[1] -= shift @stack}], # substraction
247         '*'    => [2, sub {$stack[1] *= shift @stack}], # multiplication
248         '/'    => [2, sub {$stack[1] /= shift @stack}], # division
249         'mod'  => [2, sub {$stack[1] %= shift @stack}], # modulo
250
251         'sqrt' => [1, sub {$stack[0] = sqrt $stack[0]}], # square root
252         'sq'   => [1, sub {$stack[0] *= $stack[0]}], # squared
253         '^'    => [2, sub {$stack[1] **= shift @stack}], # exponentiation
254         'xroot'=> [2, sub {$stack[1] **= 1 / shift @stack}], # x-root of y
255
256         'log'  => [1, sub {$stack[0] = log($stack[0]) / log(10)}], # logarithm
257         'alog' => [1, sub {$stack[0] = 10 ** $stack[0]}], # 10^x
258         'ln'   => [1, sub {$stack[0] = log $stack[0]}], # natural logaritm
259         'lnp1' => [1, sub {$stack[0] = log($stack[0]+1)}], # ln(x+1)
260         'exp'  => [1, sub {$stack[0] = exp($stack[0])}], # e^x
261         'expm' => [1, sub {$stack[0] = exp($stack[0])-1}], # exp(x)-1
262
263         'sin'  => [1, sub {$stack[0] = sin $stack[0]}], # sine
264         'asin' => [1, sub {$stack[0] = atan2($stack[0], sqrt(1 - $stack[0]*$stack[0]))}], # inverse sine
265         'cos'  => [1, sub {$stack[0] = cos $stack[0]}], # cosine
266         'acos' => [1, sub {$stack[0] = atan2(sqrt(1 - $stack[0]*$stack[0]), $stack[0])}], # inverse cosine
267         'tan'  => [1, sub {$stack[0] = sin($stack[0]) / cos($stack[0])}], # tangent
268 #       'atan' => [1, sub {}], # arctangent
269
270         '%'    => [2, sub {$stack[0] /= shift @stack}], # percentage
271         '%ch'  => [2, sub {$val{i} = 100*(shift(@stack)-$val{i})/$val{i}}], # percentage change
272         '%t'   => [2, sub {$val{i} = 100*$val{i}/shift(@stack)}], # percentage total
273
274         'and'  => [2, sub {$stack[1] &= shift @stack}], # bitwise and
275         'or'   => [2, sub {$stack[1] |= shift @stack}], # bitwise or
276         'xor'  => [2, sub {$stack[1] ^= shift @stack}], # bitwise xor
277         'not'  => [2, sub {$stack[0] = ~$stack[0]}], # bitwise not
278
279         'abs'  => [1, sub {$stack[0] = abs $stack[0]}], # absolute #todo
280         'sign' => [1, sub {$stack[0] = $stack[0] <=> 0}], # sign
281         'ip'   => [1, sub {$stack[0] = int $stack[0]}], # integer part
282         'fp'   => [1, sub {$stack[0] -= int $stack[0]}], # fractional part
283
284         'rnd'  => [1, sub {local $_ = 10**shift @stack; $val{i} = int(($val{i}+.5)*$_)/$_}], # round
285         'trnc' => [1, sub {local $_ = 10**shift @stack; $val{i} = int($val{i}*$_)/$_}], # truncate
286         'floor'=> [1, sub {$stack[0] = int $stack[0]}], # floor
287         'ceil' => [1, sub {$stack[0] = int $stack[0]+.9999}], # ceil
288
289         'min'  => [2, sub {
290                 local $_ = shift @stack;
291                 $stack[0] = $_ if $_<$stack[0];
292         }], # minimum
293         'max'  => [2, sub {
294                 local $_ = shift @stack;
295                 $stack[0] = $_ if $_>$stack[0];
296         }], # maximum
297
298         'dec'  => [0, sub {$set{base} = 10}], # decimal
299         'bin'  => [0, sub {$set{base} = 2}], # binary
300         'oct'  => [0, sub {$set{base} = 8}], # octal
301         'hex'  => [0, sub {$set{base} = 16}], # hexadecimal
302         'base36' => [0, sub {$set{base} = 36}], # alphanumerical
303 ); # %action
304
305 my %unit;
306 {
307 my $i = 0;
308 $unit{$_->[0]} = { name=>$_->[0], type=>$i, val=>$_->[1] } for map {$i++; @$_} (
309         [
310                 ['m', 1],
311                 ['cm', .01],
312                 ['mm', .001],
313                 ['km', 1000],
314                 ['ft', .3048],
315                 ['in', .0254],
316                 ['yd', .9144],
317                 ['mile', 1609.344],
318                 ['nmile', 1852],
319                 ['lyr', 9.46052840488e+15],
320                 ['mil', 2.54e-5],
321         #               _m _cm _mm _yd _ft _in _Mpc _pc _lyr _au _km _mi
322         #               _nmi _miUS _chain _rd _fath _ftUS _Mil _μ _Å _fermi
323         ], # lengths
324         [
325                 ['m^3', 1],
326                 ['cm^3', 1e-6],
327                 ['ft^3', .028316846592],
328                 ['in^3', 1.6387064e-5],
329         ], # volume
330 );
331 } # create unit table
332
333
334 sub showval($$);
335 sub showval($$) {
336         my ($val, $base) = @_;
337         return '' unless defined $val;
338         return $val if $base==10;
339
340         my $int = int $val;
341         my $frac = $val-$int;
342         my $exp = 0;
343
344         my $txt = '';
345         while ($int>$base**10) {
346                 $int /= $base;
347                 $exp++;
348         } # exponent part
349         while ($int>=1) {
350                 my $char = $int%$base;
351                 $txt = ($char<10 ? $char : chr($char+55)).$txt;
352                 $int /= $base;
353         } # integer part
354
355         $txt .= '.' if $frac>0;
356         for (my $i = 0; length $txt<$width-2 && $frac>0; $i++) {
357                 $frac *= $base;
358                 my $char = int $frac;
359                 $frac -= $char;
360                 $txt .= $char<10 ? $char : chr($char+55);
361         } # fraction part
362
363         $txt .= 'e'.showval($exp, $base) if $exp;
364
365         return $txt;
366 } # showval
367
368 sub showstack() {
369         for (0..@stack-1) {
370                 addstr($height-$_, 1, "$_: ".showval($stack[$_], $set{base}));
371                 clrtoeol;
372         } # show stack
373         clrtoeol($height-$#stack-1, 1);
374 } # showstack
375
376 sub showmenu() {
377         clrtoeol($height+2, 1);
378         my $nr = 0;
379         for (grep exists $menu[$_], $menumin+1..$menumin+$set{menushow}) {
380                 my $sub = (my $s = $menu[$_]) =~ s/>\d+$//;
381                 addstr($height+2, $width/$set{menushow}*($nr++), $_);
382                 attron(A_REVERSE);
383                 addstr($s);
384                 attroff(A_REVERSE);
385                 addch('>') if $sub;
386         } # display menu txts
387 } # showmenu
388
389
390 DRAW:
391 clear;
392 showmenu();
393 showstack();
394 addstr($height+1, 0, "> ");
395
396 while (1) {
397         addstr($height+1, 2, showval($val{i}, $set{base}));
398         addstr('_'.$val{unit}{name}) if exists $val{unit};
399         addstr($val{bla}) if exists $val{bla};
400         clrtoeol;
401         refresh;
402
403         $_ = ReadKey;
404         if ($_ eq chr 27) {
405                 while (defined (my $key = ReadKey -1)) {
406                         $_ .= $key;
407                 } # read additional keys
408         } # escape sequence
409
410         exists $alias{$_}  and $_ = $alias{$_};
411         exists $falias{$_} and $_ = $menu[$falias{$_}];
412
413         $_ = delete $val{bla} if exists $val{bla} and $_ eq ' ';
414
415         if ($_ eq 'quit') {
416                 last;
417         } # quit
418         elsif ($_ eq 'refresh') {
419                 goto DRAW;
420         } # refresh
421
422         elsif (/>(\d+)$/) {
423                 @menu = @{ $menus[$1] };
424                 $menumin = 0;
425                 showmenu();
426         } # submenu
427
428         elsif (exists $val{bla} or /^[A-Z]$/) {
429                 if (defined $val{i}) {
430                         unshift @stack, $val{i};
431                         %val = (i=>undef, frac=>0);
432                         showstack();
433                 }
434                 $val{bla} .= lc $_;
435         } # manual command
436
437         elsif (exists $action{$_} or /^\d$/) {
438                 my ($type, $cmd) = @{ $action{$_} || $action{digit} };
439                 if ($type==-2) {
440                         $val{i} = 0 unless defined $val{i};
441                 } # modify value
442                 if ($type>0 and defined $val{i}) {
443                         unshift @stack, $val{i};
444                         %val = (i=>undef, frac=>0);
445                 } # auto enter
446                 $cmd->();
447                 showstack() if $type>=0;
448         } # some operation
449
450         elsif ($_ =~ /^_/) {{
451                 $_ = $unit{substr $_, 1} or next;
452                 if (exists $val{unit} and $val{unit}{type}==$_->{type}) {
453                         unshift @stack, $val{i} and showstack() if defined $val{i};
454                         $stack[0] *= delete($val{unit})->{val} / $_->{val};
455                         %val = (i=>undef, frac=>0);
456                 } # convert
457                 else {
458                         $val{unit} = $_;
459                 } # set source unit
460         }} # conversion
461
462         else {
463                 attron(A_REVERSE);
464                 addstr($height+1, 0, "* error: ".join(' ', map ord, split //, $_)." *");
465                 attroff(A_REVERSE);
466                 clrtoeol;
467                 refresh;
468
469                 ReadKey; # wait for confirm
470                 1 while defined (ReadKey -1); # clear key buffer
471                 goto DRAW; # screen messed up
472         } # error
473 } # input loop
474