-
Notifications
You must be signed in to change notification settings - Fork 0
/
simple-math-parser.pl
412 lines (319 loc) · 14.9 KB
/
simple-math-parser.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
#!/usr/bin/env perl
# simple-math-parser.pl - Simple math parser written in Perl 5
# License: C.C. Attribution NonCommercial ShareAlike 3.0 Unported
# Revision: 120705
#---------------------------------------------------------------------
# important note
#---------------------------------------------------------------------
# This software is provided on an AS IS basis with ABSOLUTELY NO WAR-
# RANTY. The entire risk as to the quality and performance of the
# software is with you. Should the software prove defective, you as-
# sume the cost of all necessary servicing, repair or correction. In
# no event will any of the developers, or any other party, be liable
# to anyone for damages arising out of use of the software, or inabil-
# ity to use the software.
#---------------------------------------------------------------------
# overview
#---------------------------------------------------------------------
my $USAGE_TEXT = << 'END_OF_USAGE_TEXT';
Usage: simple-math-parser.pl "1.5+(2/3)*pi-sqrt(2)"
This is a CLI calculator program that takes a single arithmetic ex-
pression as an argument, evaluates it, and prints the result to stand-
ard output.
As a general rule, expressions should be quoted as shown here. Other-
wise, "shell meta-character" problems may occur.
The point of the program is to illustrate the structure of a simple
math parser. A recursive-descent approach is used; the core consists
of a single recursive routine (ParseMath).
Numbers may be integers, ordinary real numbers, or real numbers in
scientific notation. Examples of scientific notation: 1.3e+0 is equal
to 1.3, 12e-1 is equal to 1.2, and 5e+1 is equal to 50. The "+" sign
is optional in this context.
Supported operators include + (add), - (subtract or unary minus), *
(multiply), / (divide), and ** (exponentiate).
Six functions are supported: sqrt (square root), cbrt (cube root), log
(natural logarithm), sin, cos, and tan. The last three functions take
angles in radians.
Three standard constants may be used: e, phi (the Golden Ratio), and
pi.
This is the first public release of the program. Therefore, it should
be considered alpha and bugs may exist.
END_OF_USAGE_TEXT
#---------------------------------------------------------------------
# standard module setup
#---------------------------------------------------------------------
require 5.8.1;
use strict;
use Carp;
use warnings;
# Trap warnings
$SIG{__WARN__} = sub { die @_; };
#---------------------------------------------------------------------
# basic constants
#---------------------------------------------------------------------
use constant ZERO => 0; # Zero
use constant ONE => 1; # One
use constant TWO => 2; # Two
use constant FALSE => 0; # Boolean FALSE
use constant TRUE => 1; # Boolean TRUE
#---------------------------------------------------------------------
# common math constants
#---------------------------------------------------------------------
# This table maps one or more symbol names to associated numeric val-
# ues.
# Note: Symbol names should consist of a letter followed by zero or
# more alphanumeric characters. Letters should be specified in lower
# case.
my %MathConstants =
(
'e' => '2.7182818284590452353603' ,
'phi' => '1.6180339887498948482046' ,
'pi' => '3.1415926535897932384626'
);
#---------------------------------------------------------------------
# program parameters
#---------------------------------------------------------------------
# $IE = Internal-error message prefix
# $MAXPRE = Maximum precedence level
# $PURPOSE = Short description of purpose
# $REVISION = Revision string
# $USE_LESS = Flag: Use "less" for usage text
my $IE = 'Internal error' ;
my $MAXPRE = 9999 ;
my $PURPOSE = 'Simple Perl math parser' ;
my $REVISION = '120705' ;
my $USE_LESS = TRUE ;
#---------------------------------------------------------------------
# token-related patterns
#---------------------------------------------------------------------
# $PatNumScience = Matches a non-negative number in scientific nota-
# tion
# $PatNumRegular = Matches an ordinary non-negative number
# $PatSymbol = Matches a symbol
# $PatOperator = Matches a parenthesis or an operator
# Note: In this context, exponentiation is represented by the single-
# character operator "~" as opposed to "**", which is used at a higher
# level. This simplifies the code.
my $PatNumScience = '\b\d+\.?\d*e[\+\-]?\d+' ;
my $PatNumRegular = '\b\d+\.?\d*' ;
my $PatSymbol = '\b[a-z]\w+\b' ;
my $PatOperator = '[\(\)\+\-\*/~]' ;
#---------------------------------------------------------------------
# @TokenPatterns is a list of all of the patterns that are used to
# match tokens.
my @TokenPatterns =
( # Note: Order is significant here
$PatNumScience , $PatNumRegular , $PatSymbol , $PatOperator
);
#---------------------------------------------------------------------
# $TokenPatterns is a pattern that matches a token (of any supported
# type). The pattern omits enclosing parentheses.
my $TokenPatterns = join '|', @TokenPatterns;
#---------------------------------------------------------------------
# misc. global variables
#---------------------------------------------------------------------
my $PROGNAME; # Program name (without path)
$PROGNAME = $0;
$PROGNAME =~ s@.*/@@;
#---------------------------------------------------------------------
# support routines
#---------------------------------------------------------------------
# Routine: UsageError
# Purpose: Prints program usage text and exits
# Usage: &UsageError();
# If the global parameter $USE_LESS is TRUE, and if standard output is
# a terminal, usage text is piped through "less" (with some "less"-
# related instructions added). Otherwise, usage text is simply sent to
# standard output.
#---------------------------------------------------------------------
sub UsageError
{
$USAGE_TEXT =~ s@^\s+@@s; # Remove leading white space
$USAGE_TEXT = << "END"; # "END" must be double-quoted here
$PROGNAME $REVISION - $PURPOSE
$USAGE_TEXT
END
# Adjust trailing white space
$USAGE_TEXT =~ s@\s*\z@\n@s;
if ($USE_LESS && (-t STDOUT) && open (OFD, "|/usr/bin/less"))
{ # Handle output with "less"
# "END" must be double-quoted here
$USAGE_TEXT = << "END";
To exit this "help" text, press "q" or "Q". To scroll up or down, use
PGUP, PGDN, or the arrow keys.
$USAGE_TEXT
END
print OFD $USAGE_TEXT;
close OFD;
}
else
{ # Handle output without "less"
print "\n", $USAGE_TEXT, "\n";
}
exit ONE;
}
#---------------------------------------------------------------------
# parser routine
#---------------------------------------------------------------------
# Routine: ParseMath
# Purpose: Parses a list of math-related tokens
# Usage:
#
# my @tokens = ( '1', '+', '2', '/', '3' );
# my $result = &ParseMath (\@tokens, 0);
# Note: "ParseMath" is recursive.
# This routine takes two arguments: A reference (i.e., pointer) to a
# list of tokens and an integer, which should be zero unless the rou-
# tine happens to be calling itself (in which case it may use other
# values internally).
# Tokens may be non-negative integer or real numbers, plus or minus
# signs, a multiplication or division or exponentiation operator
# (*, /, or ~), parentheses, or the names of supported functions or
# constants.
# Note: "~" is used at this level instead of the more usual "**" as a
# matter of convenience. Higher-level code may map "**" or other char-
# acters or sequences to "~".
# Six functions are supported: sqrt (square root), cbrt (cube root),
# log (natural logarithm), sin, cos, and tan. The last three functions
# take angles in radians.
# Three standard constants may be used: e, phi (the Golden Ratio),
# and pi.
# Sub-expressions may be parenthesized. PEMDAS (i.e., standard prece-
# dence) rules are supported.
# For numbers, scientific notation is supported. Examples of scienti-
# fic notation: 1.3e+0 is equal to 1.3, 12e-1 is equal to 1.2, and
# 5e+1 is equal to 50. The "+" sign is optional in this context.
# Minus signs, as in the unary minus operator, must be specified as
# separate tokens.
#---------------------------------------------------------------------
sub ParseMath
{
# Argument list
my ($p_tokens, $plevel) = @_;
my $left; # Left operand (or token)
my $right; # Right operand
my $result; # Result
my $str; # Scratch
#---------------------------------------------------------------------
# Initial setup.
$left = shift (@$p_tokens); # Get first token
# Consistency check
die "$IE #0001\n" unless defined $left;
#---------------------------------------------------------------------
# Various cases.
# This block handles symbolic constants (such as pi), parenthesized
# sub-expressions, the unary-minus operator, and functions such as
# "cos" or "sqrt".
if (defined ($str = $MathConstants {$left}))
{ $left = $str; }
elsif ($left eq '(')
{ $left = &ParseMath ($p_tokens, ZERO ); }
elsif ($left eq '-')
{ $left = &ParseMath ($p_tokens, $MAXPRE); $left = (-$left); }
elsif ($left =~ m@^(sqrt|cbrt|log|sin|cos|tan)\z@)
{ # Function
$str = $left; # Name of function
# Function argument
$left = &ParseMath ($p_tokens, TWO);
eval
{ # "eval" traps most errors
$left = sqrt ($left) if $str eq 'sqrt' ;
$left = $left ** (1/3) if $str eq 'cbrt' ;
$left = log ($left) if $str eq 'log' ;
$left = sin ($left) if $str eq 'sin' ;
$left = cos ($left) if $str eq 'cos' ;
$left = sin ($left) / cos ($left) if $str eq 'tan' ;
};
die $@ if $@; # Handle trapped errors
}
#---------------------------------------------------------------------
# Adjust and/or check intermediate result.
# This statement verifies that the current (left) operand has been re-
# duced to a number.
die "Error: Invalid syntax\n"
unless $left =~ m@^-?($PatNumScience|$PatNumRegular)\z@;
# This statement translates numbers that are still in scientific nota-
# tion to ordinary values (if possible).
$left = $left + ZERO if $left =~ m@e@;
#---------------------------------------------------------------------
# Handle binary operators.
while (TRUE)
{
# Get operator token
my $op = shift (@$p_tokens);
# Are we there yet?
if (!defined ($op) || ($op eq ')')) { $result = $left; last; }
# Consistency check
die "$IE #0002: $op\n" unless $op =~ m@[\+\-\*/~]\z@;
my $nlevel = ZERO; # Precedence level
$nlevel = ONE if ($op eq '*') || ($op eq '/');
$nlevel = TWO if ($op eq '~');
# Stop here due to precedence?
if ($plevel && ($plevel >= $nlevel))
{ # Yes
unshift (@$p_tokens, $op);
$result = $left;
last;
}
$plevel = $nlevel; # Step to new precedence level
# Parse right side of sub-expression
$right = &ParseMath ($p_tokens, $plevel);
eval
{ # Note: "eval" traps most errors
$result = $left + $right if $op eq '+';
$result = $left - $right if $op eq '-';
$result = $left * $right if $op eq '*';
$result = $left / $right if $op eq '/';
$result = $left ** $right if $op eq '~';
};
die $@ if $@; # Handle trapped errors
# Consistency check
die "$IE #0003: $op\n" unless defined $result;
# Are we there yet?
last unless scalar @$p_tokens;
$plevel = ZERO; # We've resolved the left operand
$left = $result; # Result is left side of another sub-
# expression
}
#---------------------------------------------------------------------
# Wrap it up.
$result; # Return the result
}
#---------------------------------------------------------------------
# main routine
#---------------------------------------------------------------------
sub Main
{
my $data; # Input string
my @tokens; # Input tokens
#---------------------------------------------------------------------
# Initial setup.
select STDERR; $| = ONE; # Force STDERR flush on write
select STDOUT; $| = ONE; # Force STDOUT flush on write
# Check the command line
&UsageError() unless scalar (@ARGV) == ONE;
$data = shift (@ARGV); # Input string
$data =~ s@\s+@ @gs; # Adjust white space
# Check characters used
die "Invalid character in expression: $1\n"
if $data =~ m@([^a-z0-9\.\(\)\+\-\*/~ ])@i;
#---------------------------------------------------------------------
# Perform operations.
$data = lc ($data); # Map input to lower case
$data =~ s@\*\*@~@g; # Map "**" (exponentiation) to "~"
# (this simplifies the code)
# Put spaces around tokens
$data =~ s@($TokenPatterns)@ $1 @gi;
# Split data into a tokens list
@tokens = split m@\s+@, $data;
# Discard empty strings
@tokens = grep { length; } @tokens;
# Parse tokens and print result
print &ParseMath (\@tokens, ZERO), "\n";
undef;
}
#---------------------------------------------------------------------
# main program
#---------------------------------------------------------------------
&Main(); # Call the main routine
exit ZERO; # Normal exit