summaryrefslogtreecommitdiff
path: root/dviware/dvisvgm/src/Calculator.cpp
blob: db7241245dfcb5010b7d28227522a8b579206c76 (plain)
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
/*************************************************************************
** Calculator.cpp                                                       **
**                                                                      **
** This file is part of dvisvgm -- a fast DVI to SVG converter          **
** Copyright (C) 2005-2023 Martin Gieseking <martin.gieseking@uos.de>   **
**                                                                      **
** This program is free software; you can redistribute it and/or        **
** modify it under the terms of the GNU General Public License as       **
** published by the Free Software Foundation; either version 3 of       **
** the License, or (at your option) any later version.                  **
**                                                                      **
** This program is distributed in the hope that it will be useful, but  **
** WITHOUT ANY WARRANTY; without even the implied warranty of           **
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the         **
** GNU General Public License for more details.                         **
**                                                                      **
** You should have received a copy of the GNU General Public License    **
** along with this program; if not, see <http://www.gnu.org/licenses/>. **
*************************************************************************/

#include <cmath>
#include <sstream>
#include <stdexcept>
#include "Calculator.hpp"

using namespace std;

/** Evaluates a given arithmetic expression and returns its value.
 *  The evaluator is implemented as a recursive descent parser.
 *  @param[in] is reads expression from this stream
 *  @return expression value */
double Calculator::eval (istream &is) const {
	double ret = expr(is, false);
	try {
		// check if expression has been fully evaluated (next token is of type bool)
		mpark::get<bool>(lookAhead(is));
	}
	catch (mpark::bad_variant_access &e) {
		throw CalculatorException("expression syntax error");
	}
	return ret;
}


/** Evaluates a given arithmetic expression and returns its value.
 *  @param[in] expr expression to evaluate
 *  @return expression value */
double Calculator::eval (const string &expr) const {
	istringstream iss;
	iss.str(expr);
	return eval(iss);
}


/** Evaluates the root rule of the expression grammar. */
double Calculator::expr (istream &is, bool skip) const {    // expr:
	double left = term(is, skip);
	bool ready=false;
	while (!ready) {
		Token token = lookAhead(is);
		char *op = mpark::get_if<char>(&token);
		if (!op)
			ready = true;
		else {
			switch (*op) {
				case '+': left += term(is, true); break;  // term '+' term => $1 + $3
				case '-': left -= term(is, true); break;  // term '-' term => $1 - $3
				default : ready = true;
			}
		}
	}
	return left;  // term => $1
}


double Calculator::term (istream &is, bool skip) const {    // term:
	double left = prim(is, skip);
	bool ready=false;
	while (!ready) {
		Token token = lookAhead(is);
		char *op = mpark::get_if<char>(&token);
		if (!op)
			ready = true;
		else {
			switch (*op) {
				case '*': left *= prim(is, true); break;  // prim '*' prim => $1 * $3
				case '(': left *= prim(is, false); break; // prim '(' prim => $1 * $3
				case '/': {                               // prim '/' prim => $1 / $3
					double denom = prim(is, true);
					if (denom == 0)
						throw CalculatorException("division by zero");
					left /= denom;
					break;
				}
				case '%': {                               // prim '%' prim => $1 mod $3
					double denom = prim(is, true);
					if (denom == 0)
						throw CalculatorException("division by zero");
					left -= denom * floor(left / denom);
					break;
				}
				default:
					ready = true;
			}
		}
	}
	return left;
}


/** Evaluates a primary expression of the grammar which doesn't contain any binary operators. */
double Calculator::prim (istream &is, bool skip) const { // prim:
	if (skip)
		lex(is);
	Token token = lookAhead(is);
	if (mpark::get_if<double>(&token)) {               //  NUMBER => $1
		double ret = mpark::get<double>(lex(is));
		token = lookAhead(is);
		if (mpark::get_if<string>(&token))              // NUMBER STRING => $1 * $2
			ret *= getVariable(mpark::get<string>(lex(is)));
		return ret;
	}
	if (mpark::get_if<string>(&token))                 // STRING => getVariable($1)
		return getVariable(mpark::get<string>(lex(is)));
	if (char *op = mpark::get_if<char>(&token)) {
		switch (*op) {
			case '-':                                    // '-' prim => -$2
				return -prim(is, true);
			case '(': {                                  // '(' expr ')' => $2
				double e = expr(is, true);
				try {
					if (mpark::get<char>(lookAhead(is)) != ')')
						throw CalculatorException("')' expected");
				}
				catch (mpark::bad_variant_access &e) {
					throw CalculatorException("')' expected");
				}
				is.get();   // skip processed char
				return e;
			}
		}
	}
	throw CalculatorException("primary expression expected");
}


/** Returns the value of a previously defined variable. If there
 *  is no variable of the given name, a CalculatorException is thrown.
 *  @param[in] name name of variable
 *  @return assigned value */
double Calculator::getVariable (const string &name) const {
	auto it = _variables.find(name);
	if (it == _variables.end())
		throw CalculatorException("undefined variable '" + name + "'");
	return it->second;
}


/** Determines the type of the next token without swallowing it. That means
 *  the same token will be read again next time. The value of the returned token
 *  object is only set for character tokens and should be considered undefined
 *  for the other types. */
Calculator::Token Calculator::lookAhead (istream &is) {
	is >> ws;
	int c = is.peek();
	if (is.eof())
		return false;
	if (isdigit(c) || c == '.')
		return double(0);
	if (isalpha(c))
		return string();
	return char(c);
}


/** Reads and swallows the next token. The returned Token object is represented by
 *  a variant and thus contains both its type and value.
 *  @param[in] is next token is read from this stream
 *  @return the read token */
Calculator::Token Calculator::lex (istream &is) {
	Token token = lookAhead(is);
	if (mpark::get_if<char>(&token))
		is.get();  // token already contains the value from look ahead
	else if (mpark::get_if<double>(&token)) {
		string str;
		while (isdigit(is.peek()) || is.peek() == '.')
			str += char(is.get());
		try {
			token.emplace<double>(stod(str));
		}
		catch (const exception&) {
			throw CalculatorException("invalid number: "+str);
		}
	}
	else if (mpark::get_if<string>(&token)) {
		string name;
		while (isalpha(is.peek()))
			name += char(is.get());
		token.emplace<string>(name);
	}
	return token;
}