quecto_calc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/quecto_calc/bin_op_node.rb +14 -0
- data/lib/quecto_calc/char_rules.rb +15 -0
- data/lib/quecto_calc/evaluator.rb +86 -0
- data/lib/quecto_calc/lexer.rb +111 -0
- data/lib/quecto_calc/number_node.rb +12 -0
- data/lib/quecto_calc/parser.rb +87 -0
- data/lib/quecto_calc/quecto_error.rb +46 -0
- data/lib/quecto_calc/token.rb +13 -0
- data/lib/quecto_calc/token_types.rb +16 -0
- data/lib/quecto_calc/version.rb +5 -0
- data/lib/quecto_calc.rb +71 -0
- data/quecto_calc.gemspec +37 -0
- metadata +68 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3f56437ed3d312aa5c0508c4e787d2b33cb0ea8f6943fa6198ff201d8fc6f579
|
4
|
+
data.tar.gz: c089f9abbc586cada86442f88c6fdbd9c063609613643388bca93ed6e5eb87a3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ec5be8c5bcbbba76dcea6a2b13cd75ce58a4f96d67d1cc2861cc909f9194982b25b15762a22200a7cab3ccb312fbecf7b55c6fee219369fcd14898832b2ae51
|
7
|
+
data.tar.gz: 41348a16b6f58ecadc075f9e06e74e3f96dbdd1f61b5e1d8b8c10b9bccc8a3b06eb170bfb5e36e61905fe7e580bc25dfb1f6dadd01f24aaba22668870614dcd7
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
quecto_calc (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.2)
|
10
|
+
minitest (5.16.3)
|
11
|
+
parallel (1.22.1)
|
12
|
+
parser (3.1.3.0)
|
13
|
+
ast (~> 2.4.1)
|
14
|
+
rainbow (3.1.1)
|
15
|
+
rake (13.0.6)
|
16
|
+
regexp_parser (2.6.1)
|
17
|
+
rexml (3.2.5)
|
18
|
+
rubocop (0.93.1)
|
19
|
+
parallel (~> 1.10)
|
20
|
+
parser (>= 2.7.1.5)
|
21
|
+
rainbow (>= 2.2.2, < 4.0)
|
22
|
+
regexp_parser (>= 1.8)
|
23
|
+
rexml
|
24
|
+
rubocop-ast (>= 0.6.0)
|
25
|
+
ruby-progressbar (~> 1.7)
|
26
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
27
|
+
rubocop-ast (1.24.0)
|
28
|
+
parser (>= 3.1.1.0)
|
29
|
+
ruby-progressbar (1.11.0)
|
30
|
+
unicode-display_width (1.8.0)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
x86_64-linux
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
minitest (~> 5.0)
|
37
|
+
quecto_calc!
|
38
|
+
rake (~> 13.0)
|
39
|
+
rubocop (~> 0.80)
|
40
|
+
|
41
|
+
BUNDLED WITH
|
42
|
+
2.2.3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Mate
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# QuectoCalc
|
2
|
+
|
3
|
+
A simple calculator that evaluates primitive arithmetic expressions represented in a text form, simular to the Ruby's native method _eval_.
|
4
|
+
|
5
|
+
Although QuectoCalc could perform only a _very_ limited set of operations, it parses input expressions in a way that resembles a run of a real interpreter. QuectoCalc performs lexical analysis, builds an abstract syntax tree (AST), and then evaluates the expression based on the AST. Thus, it’s functionality could be expanded later on.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'quecto_calc'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install quecto_calc
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
obj.evaluate(expression [, constants]) -> result
|
26
|
+
|
27
|
+
#### Arguments
|
28
|
+
|
29
|
+
+ _expression_ [String]
|
30
|
+
|
31
|
+
Expression to evaluate.
|
32
|
+
|
33
|
+
+ _constants_ [Hash{ String => Numeric }]
|
34
|
+
|
35
|
+
Constant names and their respective values.
|
36
|
+
|
37
|
+
#### Returns
|
38
|
+
|
39
|
+
+ _result_ [Numeric]
|
40
|
+
|
41
|
+
## Supported operators
|
42
|
+
|
43
|
+
+ _-_
|
44
|
+
|
45
|
+
Subtraction.
|
46
|
+
|
47
|
+
+ _+_
|
48
|
+
|
49
|
+
Addition.
|
50
|
+
|
51
|
+
## Examples:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
require "quecto_calc"
|
55
|
+
|
56
|
+
calc = QuectoCalc.new
|
57
|
+
|
58
|
+
# Evaluate an expression:
|
59
|
+
calc.evaluate("1 + 2 + 3 + 4 + 5 - 6") # => 9
|
60
|
+
|
61
|
+
# Evaluate an expression with constants:
|
62
|
+
foo = 9000
|
63
|
+
bar = 1234
|
64
|
+
calc.evaluate("foo - bar", { "foo" => foo, "bar" => bar }) # => 7766
|
65
|
+
```
|
66
|
+
|
67
|
+
## Development
|
68
|
+
|
69
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/8bit-mate/quecto_calc.
|
74
|
+
|
75
|
+
## Acknowledges
|
76
|
+
|
77
|
+
The lexer and the parser were inspired by the David Callanan's "[Make Your Own Programming Language](https://github.com/davidcallanan/py-myopl-code)" series.
|
78
|
+
|
79
|
+
## License
|
80
|
+
|
81
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "rubocop/rake_task"
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
task default: %i[test rubocop]
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "quecto_calc"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Stores a node with a binary operation (between two numbers).
|
5
|
+
#
|
6
|
+
class BinOpNode
|
7
|
+
attr_reader :left_node, :operator, :right_node
|
8
|
+
|
9
|
+
def initialize(left_node, operator, right_node)
|
10
|
+
@left_node = left_node
|
11
|
+
@operator = operator
|
12
|
+
@right_node = right_node
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Provides character rules that are used by the lexer.
|
5
|
+
#
|
6
|
+
module CharRules
|
7
|
+
NUM_CHAR = /[[:digit:]]/.freeze # characters that could form a number: digits only
|
8
|
+
CONST_FIRST_CHAR = /[[:alpha:]]/.freeze # characters that could be used as a 1st char. in the constant name
|
9
|
+
CONTS_CHAR = /[[:alpha:][:digit:]_]/.freeze # characters that could form a constant: letters, digits, underscore char.
|
10
|
+
|
11
|
+
ADD_CHAR = "+" # character that marks an addition operator
|
12
|
+
SUB_CHAR = "-" # character that marks an subtraction operator
|
13
|
+
|
14
|
+
IGNOR_CHAR = /\s/.freeze # characters that are ignored: all whitespace
|
15
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "quecto_error"
|
4
|
+
require_relative "token_types"
|
5
|
+
|
6
|
+
#
|
7
|
+
# Evaluates expression from the abstract syntax tree.
|
8
|
+
#
|
9
|
+
class Evaluator
|
10
|
+
include TokenTypes
|
11
|
+
|
12
|
+
#
|
13
|
+
# Initialize an evaluator instance.
|
14
|
+
#
|
15
|
+
# @param [Hash{ String => Numeric }] consts
|
16
|
+
# List of constants and their values to put in the expression.
|
17
|
+
#
|
18
|
+
def initialize(consts = {})
|
19
|
+
@consts = consts
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# @param [BinOpNode, NumberNode] node
|
24
|
+
#
|
25
|
+
def visit(node)
|
26
|
+
send("_visit_#{node.class}", node)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
#
|
32
|
+
# Retrieve result of a binary operation node.
|
33
|
+
#
|
34
|
+
# @param [BinOpNode] node
|
35
|
+
#
|
36
|
+
# @return [Numeric]
|
37
|
+
# Result of the binary operation.
|
38
|
+
#
|
39
|
+
def _visit_BinOpNode(node)
|
40
|
+
left = visit(node.left_node)
|
41
|
+
right = visit(node.right_node)
|
42
|
+
|
43
|
+
case node.operator.type
|
44
|
+
when TT_PLUS
|
45
|
+
left + right
|
46
|
+
when TT_MINUS
|
47
|
+
left - right
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Retrieve value of a number node.
|
53
|
+
#
|
54
|
+
# @param [NumberNode] node
|
55
|
+
#
|
56
|
+
# @return [Numeric]
|
57
|
+
#
|
58
|
+
def _visit_NumberNode(node)
|
59
|
+
case node.token.type
|
60
|
+
when TT_INT
|
61
|
+
node.token.value
|
62
|
+
when TT_CONST
|
63
|
+
_init_constant(node)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Replace constant with an associated value.
|
69
|
+
#
|
70
|
+
# @param [NumberNode] node
|
71
|
+
# A numeric node with @token of TT_CONST type.
|
72
|
+
#
|
73
|
+
# @return [Numeric]
|
74
|
+
#
|
75
|
+
# @raise [CalcError]
|
76
|
+
# Raises if constant is not found in the @consts.
|
77
|
+
#
|
78
|
+
def _init_constant(node)
|
79
|
+
if @consts.key?(node.token.value)
|
80
|
+
@consts[node.token.value]
|
81
|
+
else
|
82
|
+
error_msg = "undefined constant '#{node.token.value}'"
|
83
|
+
raise CalcError, error_msg
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "char_rules"
|
4
|
+
require_relative "quecto_error"
|
5
|
+
require_relative "token"
|
6
|
+
require_relative "token_types"
|
7
|
+
|
8
|
+
#
|
9
|
+
# Performs lexical analysis of a given string.
|
10
|
+
#
|
11
|
+
class Lexer
|
12
|
+
include TokenTypes
|
13
|
+
include CharRules
|
14
|
+
|
15
|
+
#
|
16
|
+
# Initialize a lexer instance.
|
17
|
+
#
|
18
|
+
# @param [String] str
|
19
|
+
# String to parse.
|
20
|
+
#
|
21
|
+
def initialize(str)
|
22
|
+
@str = str
|
23
|
+
@pos = 0
|
24
|
+
@cur_char = nil
|
25
|
+
|
26
|
+
_next_char
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Create a list of tokens from a string.
|
31
|
+
#
|
32
|
+
# @return [Array<Token>] tokens
|
33
|
+
#
|
34
|
+
# @raise [IllegalCharError]
|
35
|
+
# Raises when @str has an unsupported character.
|
36
|
+
#
|
37
|
+
def build_tokens
|
38
|
+
tokens = []
|
39
|
+
|
40
|
+
until @cur_char.nil?
|
41
|
+
if @cur_char == ADD_CHAR
|
42
|
+
tokens.append(Token.new(type: TT_PLUS))
|
43
|
+
elsif @cur_char == SUB_CHAR
|
44
|
+
tokens.append(Token.new(type: TT_MINUS))
|
45
|
+
elsif @cur_char.match?(NUM_CHAR)
|
46
|
+
num = Token.new(type: TT_INT, value: _build_word(NUM_CHAR).to_i)
|
47
|
+
tokens.append(num)
|
48
|
+
elsif @cur_char.match?(CONST_FIRST_CHAR)
|
49
|
+
num = Token.new(type: TT_CONST, value: _build_word(CONTS_CHAR))
|
50
|
+
tokens.append(num)
|
51
|
+
elsif @cur_char.match?(IGNOR_CHAR)
|
52
|
+
# Ignore whitespace.
|
53
|
+
else
|
54
|
+
error_msg = "illegal character '#{@cur_char}' at the position: #{@pos - 1}"
|
55
|
+
raise IllegalCharError, error_msg
|
56
|
+
end
|
57
|
+
_next_char
|
58
|
+
end
|
59
|
+
|
60
|
+
tokens.append(Token.new(type: TT_EOF))
|
61
|
+
|
62
|
+
tokens
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
#
|
68
|
+
# Build a 'word' out of a sequence of characters.
|
69
|
+
#
|
70
|
+
# A 'word' is a sequence of characters followed one by another without break characters (e.g. a space or a supported
|
71
|
+
# math operator: '+', '-', etc).
|
72
|
+
#
|
73
|
+
# A 'word' could be represented in a form of:
|
74
|
+
#
|
75
|
+
# 1. a sequence of alphabetical characters optionally mixed with digits and joined into one string using a connective
|
76
|
+
# character '_'. Examples:
|
77
|
+
# - 'ruby' (a sequence of alphabetical characters only);
|
78
|
+
# - 'foobar9000' (a mix of alphabetical characters and digits);
|
79
|
+
# - 'this_is_a_3rd_example' (long string joined with a connective character).
|
80
|
+
#
|
81
|
+
# 2. an integer number, e.g.: '31337' (a sequence of digits without break characters).
|
82
|
+
#
|
83
|
+
# @param [Regexp] regexp
|
84
|
+
# Defines that kind of character sequence the method should be looking for: NUM_CHAR (search for a digit) or
|
85
|
+
# CONTS_CHAR (search for a constant).
|
86
|
+
#
|
87
|
+
# @return [String] word
|
88
|
+
#
|
89
|
+
def _build_word(regexp)
|
90
|
+
word = ""
|
91
|
+
|
92
|
+
until @cur_char.nil?
|
93
|
+
break unless @cur_char.match?(regexp)
|
94
|
+
|
95
|
+
word += @cur_char
|
96
|
+
_next_char
|
97
|
+
end
|
98
|
+
_next_char
|
99
|
+
|
100
|
+
@pos -= 1
|
101
|
+
word
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Process next character in the string.
|
106
|
+
#
|
107
|
+
def _next_char
|
108
|
+
@cur_char = @pos < @str.length ? @str[@pos] : nil
|
109
|
+
@pos += 1
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "bin_op_node"
|
4
|
+
require_relative "number_node"
|
5
|
+
require_relative "quecto_error"
|
6
|
+
require_relative "token_types"
|
7
|
+
|
8
|
+
#
|
9
|
+
# Parses list of tokens to build an abstract syntax tree.
|
10
|
+
#
|
11
|
+
class Parser
|
12
|
+
include TokenTypes
|
13
|
+
|
14
|
+
#
|
15
|
+
# Initialize a parser instance.
|
16
|
+
#
|
17
|
+
# @param [Array<Token>] tokens
|
18
|
+
# List of tokens to parse.
|
19
|
+
#
|
20
|
+
def initialize(tokens)
|
21
|
+
@tokens = tokens
|
22
|
+
@cur_token = @tokens[0]
|
23
|
+
@idx = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Parse list of tokens.
|
28
|
+
#
|
29
|
+
def parse
|
30
|
+
_expr
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
#
|
36
|
+
# Buid a node for the expression.
|
37
|
+
#
|
38
|
+
# @return [BinOpNode, NumberNode] left
|
39
|
+
# Node for an expression. The left.class corresponds to the expression type: a single number or a binary operation
|
40
|
+
# (an operation between two numbers).
|
41
|
+
#
|
42
|
+
def _expr
|
43
|
+
# retrieve left part of the expression:
|
44
|
+
left = _term
|
45
|
+
|
46
|
+
while BIN_OPS.include?(@cur_token.type)
|
47
|
+
# retrieve operator between two number nodes:
|
48
|
+
op_tok = @cur_token
|
49
|
+
|
50
|
+
# retrieve right part of the expression:
|
51
|
+
_next_token
|
52
|
+
right = _term
|
53
|
+
|
54
|
+
left = BinOpNode.new(left, op_tok, right)
|
55
|
+
end
|
56
|
+
|
57
|
+
left
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Search for a term in the expression.
|
62
|
+
#
|
63
|
+
# @return [NumberNode] token
|
64
|
+
# Found term.
|
65
|
+
#
|
66
|
+
# @raise [InvalidSyntaxError]
|
67
|
+
#
|
68
|
+
def _term
|
69
|
+
if TERMS.include?(@cur_token.type)
|
70
|
+
token = NumberNode.new(@cur_token)
|
71
|
+
else
|
72
|
+
error_msg = "expected TT_INT or TT_LBL, but got #{@cur_token.type}"
|
73
|
+
raise InvalidSyntaxError, error_msg
|
74
|
+
end
|
75
|
+
|
76
|
+
_next_token
|
77
|
+
token
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Process next token.
|
82
|
+
#
|
83
|
+
def _next_token
|
84
|
+
@idx += 1
|
85
|
+
@cur_token = @tokens[@idx] if @idx < @tokens.length
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Errors rised by the quecto_calc.
|
5
|
+
#
|
6
|
+
class QuectoError < ::StandardError
|
7
|
+
attr_reader :error_name, :message
|
8
|
+
|
9
|
+
def initialize(error_name, message)
|
10
|
+
@error_name = error_name
|
11
|
+
@message = message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Raised by the lexer if an illegal character is found.
|
17
|
+
#
|
18
|
+
class IllegalCharError < QuectoError
|
19
|
+
attr_reader :message
|
20
|
+
|
21
|
+
def initialize(message = "")
|
22
|
+
super("Illegal Character:", message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Raised by the parser if an illegal syntax is found.
|
28
|
+
#
|
29
|
+
class InvalidSyntaxError < QuectoError
|
30
|
+
attr_reader :message
|
31
|
+
|
32
|
+
def initialize(message = "")
|
33
|
+
super("Syntax Error:", message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Raised by the evaluator on an illegal operation.
|
39
|
+
#
|
40
|
+
class CalcError < QuectoError
|
41
|
+
attr_reader :message
|
42
|
+
|
43
|
+
def initialize(message = "")
|
44
|
+
super("Runtime Error:", message)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Provides supported token types.
|
5
|
+
#
|
6
|
+
module TokenTypes
|
7
|
+
TT_INT = :TT_INT # integer number
|
8
|
+
TT_CONST = :TT_CONST # constant (placeholder for a numeric value)
|
9
|
+
TT_PLUS = :TT_PLUS # addition operator
|
10
|
+
TT_MINUS = :TT_MINUS # subtraction operator
|
11
|
+
TT_EOF = :TT_EOF # end of input
|
12
|
+
|
13
|
+
TERMS = [TT_INT, TT_CONST].freeze
|
14
|
+
|
15
|
+
BIN_OPS = [TT_PLUS, TT_MINUS].freeze
|
16
|
+
end
|
data/lib/quecto_calc.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "quecto_calc/evaluator"
|
4
|
+
require_relative "quecto_calc/lexer"
|
5
|
+
require_relative "quecto_calc/parser"
|
6
|
+
require_relative "quecto_calc/version"
|
7
|
+
|
8
|
+
#
|
9
|
+
# A simple calculator that evaluates primitive arithmetic expressions represented in a text form.
|
10
|
+
#
|
11
|
+
# Supported operators:
|
12
|
+
# + -- add a number
|
13
|
+
# - -- subtract a number.
|
14
|
+
#
|
15
|
+
# Supported types:
|
16
|
+
# Integer;
|
17
|
+
# Constant (a string placeholder for an Integer).
|
18
|
+
#
|
19
|
+
# Parser/lexer stuff was inspired by the David Callanan's "Make Your Own Programming Language" series
|
20
|
+
# @ https://github.com/davidcallanan/py-myopl-code
|
21
|
+
#
|
22
|
+
class QuectoCalc
|
23
|
+
#
|
24
|
+
# Evaluate an expression.
|
25
|
+
#
|
26
|
+
# @param [String] expr
|
27
|
+
# Expression to parse and evaluate.
|
28
|
+
#
|
29
|
+
# @option [Hash{ String => Numeric }] consts
|
30
|
+
# List on constants and their values to put in the expression.
|
31
|
+
#
|
32
|
+
def evaluate(expr, consts = {})
|
33
|
+
tokens = build_tokens(expr)
|
34
|
+
ast = build_ast(tokens)
|
35
|
+
evaluate_ast(ast, consts)
|
36
|
+
rescue QuectoError => e
|
37
|
+
puts "#{e.error_name} #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Get a list of tokens from the string.
|
42
|
+
#
|
43
|
+
# @param [String] expr
|
44
|
+
#
|
45
|
+
def build_tokens(expr)
|
46
|
+
Lexer.new(expr).build_tokens
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Build an abstract syntax tree from a list of tokens.
|
51
|
+
#
|
52
|
+
# @param [Array<Token>] tokens
|
53
|
+
#
|
54
|
+
def build_ast(tokens)
|
55
|
+
Parser.new(tokens).parse
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Evaluate expression from the AST.
|
60
|
+
#
|
61
|
+
# @param [NumberNode, BinOpNode] ast
|
62
|
+
# Expression (in a form of AST) to evaluate.
|
63
|
+
#
|
64
|
+
# @option [Hash{ String => Numeric }] consts
|
65
|
+
# List on constants and their values to put in the expression.
|
66
|
+
#
|
67
|
+
def evaluate_ast(ast, consts = {})
|
68
|
+
evaluator = Evaluator.new(consts)
|
69
|
+
evaluator.visit(ast)
|
70
|
+
end
|
71
|
+
end
|
data/quecto_calc.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/quecto_calc/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "quecto_calc"
|
7
|
+
spec.version = QuectoCalc::VERSION
|
8
|
+
spec.authors = ["Mate"]
|
9
|
+
#spec.email = ["mate@example.com"]
|
10
|
+
|
11
|
+
spec.summary = "A very simple calculator."
|
12
|
+
spec.description = "Evaluates primitive arithmetic expressions represented in a text form."
|
13
|
+
spec.homepage = "https://github.com/8bit-mate/quecto_calc.rb/"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/8bit-mate/quecto_calc.rb/blob/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
# Uncomment to register a new dependency of your gem
|
33
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
34
|
+
|
35
|
+
# For more information and examples about making a new gem, checkout our
|
36
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: quecto_calc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mate
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Evaluates primitive arithmetic expressions represented in a text form.
|
14
|
+
email:
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- ".gitignore"
|
20
|
+
- ".rubocop.yml"
|
21
|
+
- CHANGELOG.md
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- bin/console
|
28
|
+
- bin/setup
|
29
|
+
- lib/quecto_calc.rb
|
30
|
+
- lib/quecto_calc/bin_op_node.rb
|
31
|
+
- lib/quecto_calc/char_rules.rb
|
32
|
+
- lib/quecto_calc/evaluator.rb
|
33
|
+
- lib/quecto_calc/lexer.rb
|
34
|
+
- lib/quecto_calc/number_node.rb
|
35
|
+
- lib/quecto_calc/parser.rb
|
36
|
+
- lib/quecto_calc/quecto_error.rb
|
37
|
+
- lib/quecto_calc/token.rb
|
38
|
+
- lib/quecto_calc/token_types.rb
|
39
|
+
- lib/quecto_calc/version.rb
|
40
|
+
- quecto_calc.gemspec
|
41
|
+
homepage: https://github.com/8bit-mate/quecto_calc.rb/
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata:
|
45
|
+
allowed_push_host: https://rubygems.org/
|
46
|
+
homepage_uri: https://github.com/8bit-mate/quecto_calc.rb/
|
47
|
+
source_code_uri: https://github.com/8bit-mate/quecto_calc.rb/
|
48
|
+
changelog_uri: https://github.com/8bit-mate/quecto_calc.rb/blob/main/CHANGELOG.md
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 3.0.0
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubygems_version: 3.2.3
|
65
|
+
signing_key:
|
66
|
+
specification_version: 4
|
67
|
+
summary: A very simple calculator.
|
68
|
+
test_files: []
|