keisan 0.8.0 → 0.8.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2d556d5ef3e67015596fee5856e2fe0b27295b227f7d4edf8cb41c037aed98b
4
- data.tar.gz: c8c20ac6ba6a9ad27654931238a2dbafcbbbc509b58fd2a74161b7a38b3edaab
3
+ metadata.gz: a2a155be79ae75b3c67a097791c2af2bf67196cae16ef5372049b4eb54884117
4
+ data.tar.gz: b5d38c6111d1803a14c0229b0f54dc8733d581409254068ac9970af1321d9fca
5
5
  SHA512:
6
- metadata.gz: 9ccc5172adc0b5ebafabe3cd5846fb455ada6e1351c94deab4da5bfd38617c0e0692258e9384d760d25b88a899b46de1899b1688850bfac8ddcdd432091ddf50
7
- data.tar.gz: cb2ca195319a744f3ee9b56ef7df14d8dfcf36dbcafcbe1e675a858f232457d39cb3fe96d7f056a26746630d4a7f1c47a8fe6fc75a880683cd2120428a448ff6
6
+ metadata.gz: 550d595b64739c49e6fdcd12b149fa0f5706d3032f11c61f6f9b9bd3ac6ff7465398a5b04f58868dca803c9cef1b9fb14f9df029bc03c550d8616760751f3886
7
+ data.tar.gz: d2c25f61cdd3400c198cb6962cd31c0023ef5aa0081d3a0ea2699b7e7098620d8d39dc8af26bee5fe10197f84fb91218763fdc6ba830d2c97b30122a1b30fd3d
@@ -1,7 +1,9 @@
1
1
  sudo: false
2
2
  language: ruby
3
+ before_install:
4
+ - gem install bundler
3
5
  rvm:
4
- - 2.3.6
5
- - 2.4.3
6
- - 2.5.0
7
- before_install: gem install bundler -v 1.16.1
6
+ - 2.4.9
7
+ - 2.5.7
8
+ - 2.6.5
9
+ - 2.7.0
data/README.md CHANGED
@@ -178,6 +178,22 @@ calculator.evaluate("x = 11; {let x = 12}; x")
178
178
  #=> 11
179
179
  ```
180
180
 
181
+ ##### Comments
182
+
183
+ When working with multi-line blocks of code, sometimes comments are useful to include.
184
+ Comments are parts of a string from the `#` character to the end of a line (indicated by a newline character `"\n"`).
185
+
186
+ ```
187
+ calculator = Keisan::Calculator.new
188
+ calculator.evaluate("""
189
+ # This is a comment
190
+ x = 'foo'
191
+ x += '#bar' # Notice that `#` inside strings is not part of the comment
192
+ x # Should print 'foo#bar'
193
+ """)
194
+ #=> "foo#bar"
195
+ ```
196
+
181
197
  ##### Lists
182
198
 
183
199
  Just like in Ruby, lists can be defined using square brackets, and indexed using square brackets
@@ -256,6 +272,7 @@ calculator.evaluate("range(1, 6).map(x, [x, x**2]).to_h")
256
272
  Keisan supports date and time objects like in Ruby.
257
273
  You create a date object using either the method `date` (either a string to be parsed, or year, month, day numerical arguments) or `today`.
258
274
  They support methods `year`, `month`, `day`, `weekday`, `strftime`, and `to_time` to convert to a time object.
275
+ `epoch_days` computes the number of days since Unix epoch (Jan 1, 1970).
259
276
 
260
277
  ```ruby
261
278
  calculator = Keisan::Calculator.new
@@ -266,10 +283,13 @@ calculator.evaluate("today() > date(2018, 11, 1)")
266
283
  #=> true
267
284
  calculator.evaluate("date('1999-12-31').to_time + 10")
268
285
  #=> Time.new(1999, 12, 31, 0, 0, 10)
286
+ calculator.evaluate("date(1970, 1, 15).epoch_days")
287
+ #=> 14
269
288
  ```
270
289
 
271
290
  Time objects are created using `time` (either a string to be parsed, or year, month, day, hour, minute, second arguments) or `now`.
272
291
  They support methods `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`, `strftime`, and `to_date` to convert to a date object.
292
+ `epoch_seconds` computes the number of seconds since Unix epoch (00:00:00 on Jan 1, 1970).
273
293
 
274
294
  ```ruby
275
295
  calculator = Keisan::Calculator.new
@@ -279,6 +299,8 @@ calculator.evaluate("time('2000-4-15 12:34:56').minute")
279
299
  #=> 34
280
300
  calculator.evaluate("time('5000-10-10 20:30:40').strftime('%b %d, %Y')")
281
301
  #=> "Oct 10, 5000"
302
+ calculator.evaluate("time(1970, 1, 1, 2, 3, 4).epoch_seconds")
303
+ #=> 7384
282
304
  ```
283
305
 
284
306
  ##### Functional programming methods
@@ -340,7 +362,7 @@ calculator.evaluate("[2, 3, 7, 11].has_element(11)")
340
362
 
341
363
  ##### Bitwise operations
342
364
 
343
- The basic bitwise operations, NOT `~`, OR `|`, XOR `^`, and AND `&` are also available for use
365
+ The basic bitwise operations, NOT `~`, OR `|`, XOR `^`, AND `&`, and left/right bitwise shifts (`<<` and `>>`) are also available for use
344
366
 
345
367
  ```ruby
346
368
  calculator = Keisan::Calculator.new
@@ -485,6 +507,7 @@ calculator.evaluate("puts x**2") # prints "25\n" to STDOUT
485
507
 
486
508
  #### Bitwise operators
487
509
  - `&`, `|`, `^`: bitwise **and**, **or**, **xor** operators
510
+ - `<<`, `>>` bitwise shift operators
488
511
  - `~`: unary bitwise not
489
512
 
490
513
  #### Indexing of arrays/hashes
@@ -21,10 +21,12 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.required_ruby_version = ">= 2.3.0"
23
23
 
24
- spec.add_development_dependency "coveralls"
25
- spec.add_development_dependency "bundler", "~> 1.14"
26
- spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_dependency "cmath", "~> 1.0"
25
+
26
+ spec.add_development_dependency "bundler", "~> 2.0"
27
+ spec.add_development_dependency "rake", "~> 13.0"
27
28
  spec.add_development_dependency "rspec", "~> 3.0"
28
29
  spec.add_development_dependency "pry"
29
30
  spec.add_development_dependency "pry-stack_explorer"
31
+ spec.add_development_dependency "simplecov"
30
32
  end
@@ -37,6 +37,8 @@ require "keisan/ast/bitwise_operator"
37
37
  require "keisan/ast/bitwise_and"
38
38
  require "keisan/ast/bitwise_or"
39
39
  require "keisan/ast/bitwise_xor"
40
+ require "keisan/ast/bitwise_left_shift"
41
+ require "keisan/ast/bitwise_right_shift"
40
42
  require "keisan/ast/logical_operator"
41
43
  require "keisan/ast/logical_and"
42
44
  require "keisan/ast/logical_or"
@@ -78,10 +80,12 @@ require "keisan/tokens/assignment"
78
80
  require "keisan/tokens/arithmetic_operator"
79
81
  require "keisan/tokens/logical_operator"
80
82
  require "keisan/tokens/bitwise_operator"
83
+ require "keisan/tokens/bitwise_shift"
81
84
  require "keisan/tokens/word"
82
85
  require "keisan/tokens/line_separator"
83
86
  require "keisan/tokens/unknown"
84
87
 
88
+ require "keisan/string_and_group_parser"
85
89
  require "keisan/tokenizer"
86
90
 
87
91
  require "keisan/parsing/component"
@@ -128,6 +132,8 @@ require "keisan/parsing/bitwise_or"
128
132
  require "keisan/parsing/bitwise_xor"
129
133
  require "keisan/parsing/bitwise_not"
130
134
  require "keisan/parsing/bitwise_not_not"
135
+ require "keisan/parsing/bitwise_left_shift"
136
+ require "keisan/parsing/bitwise_right_shift"
131
137
  require "keisan/parsing/logical_operator"
132
138
  require "keisan/parsing/logical_less_than"
133
139
  require "keisan/parsing/logical_greater_than"
@@ -0,0 +1,17 @@
1
+ module Keisan
2
+ module AST
3
+ class BitwiseLeftShift < BitwiseOperator
4
+ def self.symbol
5
+ :<<
6
+ end
7
+
8
+ def blank_value
9
+ 0
10
+ end
11
+
12
+ def evaluate(context = nil)
13
+ children[1..-1].inject(children.first.evaluate(context)) {|total, child| total << child.evaluate(context)}
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Keisan
2
+ module AST
3
+ class BitwiseRightShift < BitwiseOperator
4
+ def self.symbol
5
+ :>>
6
+ end
7
+
8
+ def blank_value
9
+ 0
10
+ end
11
+
12
+ def evaluate(context = nil)
13
+ children[1..-1].inject(children.first.evaluate(context)) {|total, child| total >> child.evaluate(context)}
14
+ end
15
+ end
16
+ end
17
+ end
@@ -17,6 +17,10 @@ module Keisan
17
17
  child.unbound_functions(local)
18
18
  end
19
19
 
20
+ def contains_a?(klass)
21
+ super || child.contains_a?(klass)
22
+ end
23
+
20
24
  def deep_dup
21
25
  dupped = dup
22
26
  dupped.instance_variable_set(
@@ -15,6 +15,10 @@ module Keisan
15
15
  node.unbound_functions(context)
16
16
  end
17
17
 
18
+ def contains_a?(klass)
19
+ super || node.contains_a?(klass)
20
+ end
21
+
18
22
  def deep_dup
19
23
  dupped = dup
20
24
  dupped.instance_variable_set(
@@ -29,8 +29,7 @@ module Keisan
29
29
 
30
30
  def evaluate
31
31
  # Blocks might have local variable/function definitions, so skip check
32
- verify_rhs_of_function_assignment_is_valid! unless rhs.is_a?(Block)
33
-
32
+ verify_rhs_of_function_assignment_is_valid!
34
33
  context.register_function!(lhs.name, expression_function, local: local)
35
34
  rhs
36
35
  end
@@ -38,15 +37,26 @@ module Keisan
38
37
  private
39
38
 
40
39
  def verify_rhs_of_function_assignment_is_valid!
41
- # Only variables that can appear are those that are arguments to the function
42
- unless rhs.unbound_variables(context) <= Set.new(argument_names)
43
- raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
44
- end
40
+ verify_unbound_functions!
41
+ verify_unbound_variables!
42
+ end
43
+
44
+ def verify_unbound_functions!
45
45
  # Cannot have undefined functions unless allowed by context
46
46
  unless context.allow_recursive || rhs.unbound_functions(context).empty?
47
47
  raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
48
48
  end
49
49
  end
50
+
51
+ def verify_unbound_variables!
52
+ # We allow unbound variables inside block statements, as they could be temporary
53
+ # variables assigned locally
54
+ return if rhs.is_a?(Block)
55
+ # Only variables that can appear are those that are arguments to the function
56
+ unless rhs.unbound_variables(context) <= Set.new(argument_names)
57
+ raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
58
+ end
59
+ end
50
60
  end
51
61
  end
52
62
  end
@@ -21,6 +21,10 @@ module Keisan
21
21
  end
22
22
  end
23
23
 
24
+ def contains_a?(klass)
25
+ super || @hash.any? {|k, v| k.to_node.contains_a?(klass) || v.contains_a?(klass) }
26
+ end
27
+
24
28
  def evaluate(context = nil)
25
29
  context ||= Context.new
26
30
 
@@ -37,6 +37,15 @@ module Keisan
37
37
  value(context)
38
38
  end
39
39
 
40
+ def contains_a?(klass)
41
+ case klass
42
+ when Array
43
+ klass.any? {|k| is_a?(k) }
44
+ else
45
+ is_a?(klass)
46
+ end
47
+ end
48
+
40
49
  def evaluate_assignments(context = nil)
41
50
  self
42
51
  end
@@ -45,10 +54,18 @@ module Keisan
45
54
  raise Exceptions::NonDifferentiableError.new
46
55
  end
47
56
 
57
+ def differentiated(variable, context = nil)
58
+ deep_dup.differentiate(variable, context)
59
+ end
60
+
48
61
  def replace(variable, replacement)
49
62
  self
50
63
  end
51
64
 
65
+ def replaced(variable, replacement)
66
+ deep_dup.replace(variable, replacement)
67
+ end
68
+
52
69
  def coerce(other)
53
70
  [other.to_node, self]
54
71
  end
@@ -132,6 +149,14 @@ module Keisan
132
149
  BitwiseOr.new([self, other.to_node])
133
150
  end
134
151
 
152
+ def <<(other)
153
+ BitwiseLeftShift.new([self, other.to_node])
154
+ end
155
+
156
+ def >>(other)
157
+ BitwiseRightShift.new([self, other.to_node])
158
+ end
159
+
135
160
  def >(other)
136
161
  LogicalGreaterThan.new([self, other.to_node])
137
162
  end
@@ -111,6 +111,26 @@ module Keisan
111
111
  end
112
112
  end
113
113
 
114
+ def <<(other)
115
+ other = other.to_node
116
+ case other
117
+ when Number
118
+ Number.new(value << other.value)
119
+ else
120
+ super
121
+ end
122
+ end
123
+
124
+ def >>(other)
125
+ other = other.to_node
126
+ case other
127
+ when Number
128
+ Number.new(value >> other.value)
129
+ else
130
+ super
131
+ end
132
+ end
133
+
114
134
  def >(other)
115
135
  other = other.to_node
116
136
  case other
@@ -13,6 +13,8 @@ module Keisan
13
13
  "%": [2, 85, :left], # Modulo
14
14
  "+": [2, 80, :left], # Plus
15
15
  # "-": [2, 80, :left], # Minus
16
+ "<<": [2, 75, :left], # Bitwise left shift
17
+ ">>": [2, 75, :left], # Bitwise right shift
16
18
  "&": [2, 70, :left], # Bitwise and
17
19
  "^": [2, 65, :left], # Bitwise xor
18
20
  "|": [2, 65, :left], # Bitwise or
@@ -25,6 +25,10 @@ module Keisan
25
25
  end
26
26
  end
27
27
 
28
+ def contains_a?(klass)
29
+ super || children.any? {|child| child.contains_a?(klass) }
30
+ end
31
+
28
32
  def freeze
29
33
  children.each(&:freeze)
30
34
  super
@@ -19,7 +19,7 @@ module Keisan
19
19
  @children = [child.simplify(context)]
20
20
  case child
21
21
  when Number
22
- Number.new(Rational(1,child.value(context))).simplify(context)
22
+ Number.new(child.value**-1)
23
23
  else
24
24
  (child ** -1).simplify(context)
25
25
  end
@@ -13,7 +13,13 @@ module Keisan
13
13
 
14
14
  def value(context = nil)
15
15
  context ||= Context.new
16
- variable_node_from_context(context).value(context)
16
+ node = variable_node_from_context(context)
17
+ case node
18
+ when Variable
19
+ node
20
+ else
21
+ node.value(context)
22
+ end
17
23
  end
18
24
 
19
25
  def unbound_variables(context = nil)
@@ -78,9 +84,7 @@ module Keisan
78
84
 
79
85
  def variable_node_from_context(context)
80
86
  variable = context.variable(name)
81
- if variable.is_a?(Cell)
82
- variable = variable.node
83
- end
87
+ variable = variable.node if variable.is_a?(Cell)
84
88
  variable
85
89
  end
86
90
  end
@@ -2,8 +2,14 @@ module Keisan
2
2
  class Calculator
3
3
  attr_reader :context
4
4
 
5
- def initialize(context: nil, allow_recursive: false)
6
- @context = context || Context.new(allow_recursive: allow_recursive)
5
+ # Note, allow_recursive would be more appropriately named:
6
+ # allow_unbound_functions_in_function_definitions, but it is too late for that.
7
+ def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true)
8
+ @context = context || Context.new(
9
+ allow_recursive: allow_recursive,
10
+ allow_blocks: allow_blocks,
11
+ allow_multiline: allow_multiline
12
+ )
7
13
  end
8
14
 
9
15
  def allow_recursive
@@ -14,6 +20,14 @@ module Keisan
14
20
  context.allow_recursive!
15
21
  end
16
22
 
23
+ def allow_blocks
24
+ context.allow_blocks
25
+ end
26
+
27
+ def allow_multiline
28
+ context.allow_multiline
29
+ end
30
+
17
31
  def evaluate(expression, definitions = {})
18
32
  Evaluator.new(self).evaluate(expression, definitions)
19
33
  end
@@ -23,7 +37,7 @@ module Keisan
23
37
  end
24
38
 
25
39
  def ast(expression)
26
- Evaluator.new(self).ast(expression)
40
+ Evaluator.new(self).parse_ast(expression)
27
41
  end
28
42
 
29
43
  def define_variable!(name, value)
@@ -1,13 +1,24 @@
1
1
  module Keisan
2
2
  class Context
3
- attr_reader :function_registry, :variable_registry, :allow_recursive
4
-
5
- def initialize(parent: nil, random: nil, allow_recursive: false, shadowed: [])
3
+ attr_reader :function_registry,
4
+ :variable_registry,
5
+ :allow_recursive,
6
+ :allow_multiline,
7
+ :allow_blocks
8
+
9
+ def initialize(parent: nil,
10
+ random: nil,
11
+ allow_recursive: false,
12
+ allow_multiline: true,
13
+ allow_blocks: true,
14
+ shadowed: [])
6
15
  @parent = parent
7
16
  @function_registry = Functions::Registry.new(parent: @parent&.function_registry)
8
17
  @variable_registry = Variables::Registry.new(parent: @parent&.variable_registry, shadowed: shadowed)
9
18
  @random = random
10
19
  @allow_recursive = allow_recursive
20
+ @allow_multiline = allow_multiline
21
+ @allow_blocks = allow_blocks
11
22
  end
12
23
 
13
24
  def allow_recursive!
@@ -110,7 +121,13 @@ module Keisan
110
121
  end
111
122
 
112
123
  def pure_child(shadowed: [])
113
- self.class.new(parent: self, shadowed: shadowed, allow_recursive: allow_recursive)
124
+ self.class.new(
125
+ parent: self,
126
+ shadowed: shadowed,
127
+ allow_recursive: allow_recursive,
128
+ allow_multiline: allow_multiline,
129
+ allow_blocks: allow_blocks
130
+ )
114
131
  end
115
132
  end
116
133
  end
@@ -8,7 +8,7 @@ module Keisan
8
8
 
9
9
  def evaluate(expression, definitions = {})
10
10
  context = calculator.context.spawn_child(definitions: definitions, transient: true)
11
- ast = ast(expression)
11
+ ast = parse_ast(expression)
12
12
  last_line = last_line(ast)
13
13
 
14
14
  evaluation = ast.evaluated(context)
@@ -24,16 +24,28 @@ module Keisan
24
24
 
25
25
  def simplify(expression, definitions = {})
26
26
  context = calculator.context.spawn_child(definitions: definitions, transient: true)
27
- ast = AST.parse(expression)
27
+ ast = parse_ast(expression)
28
28
  ast.simplify(context)
29
29
  end
30
30
 
31
- def ast(expression)
32
- AST.parse(expression)
31
+ def parse_ast(expression)
32
+ AST.parse(expression).tap do |ast|
33
+ disallowed = disallowed_nodes
34
+ if !disallowed.empty? && ast.contains_a?(disallowed)
35
+ raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
36
+ end
37
+ end
33
38
  end
34
39
 
35
40
  private
36
41
 
42
+ def disallowed_nodes
43
+ disallowed = []
44
+ disallowed << Keisan::AST::Block unless calculator.allow_blocks
45
+ disallowed << Keisan::AST::MultiLine unless calculator.allow_multiline
46
+ disallowed
47
+ end
48
+
37
49
  def last_line(ast)
38
50
  ast.is_a?(AST::MultiLine) ? ast.children.last : ast
39
51
  end
@@ -4,7 +4,9 @@ module Keisan
4
4
  module Functions
5
5
  class CMathFunction < MathFunction
6
6
  def initialize(name, proc_function = nil)
7
- super(name, proc_function || Proc.new {|arg| CMath.send(name, arg)})
7
+ super(name, proc_function || Proc.new {|arg|
8
+ CMath.send(name, arg)
9
+ })
8
10
  end
9
11
  end
10
12
  end
@@ -147,6 +147,9 @@ module Keisan
147
147
 
148
148
  registry.register!(:to_time, Proc.new {|d| d.to_time }, force: true)
149
149
  registry.register!(:to_date, Proc.new {|t| t.to_date }, force: true)
150
+
151
+ registry.register!(:epoch_seconds, Proc.new {|d| d.to_time - Time.new(1970, 1, 1, 0, 0, 0) }, force: true)
152
+ registry.register!(:epoch_days, Proc.new {|t| t.to_date - Date.new(1970, 1, 1) }, force: true)
150
153
  end
151
154
 
152
155
  def self.register_date_time!(registry)
@@ -5,7 +5,11 @@ module Keisan
5
5
 
6
6
  def initialize(name, arguments, expression, transient_definitions)
7
7
  super(name, arguments.count)
8
- @expression = expression.deep_dup
8
+ if expression.is_a?(::String)
9
+ @expression = AST::parse(expression)
10
+ else
11
+ @expression = expression.deep_dup
12
+ end
9
13
  @arguments = arguments
10
14
  @transient_definitions = transient_definitions
11
15
  end
@@ -70,17 +74,20 @@ module Keisan
70
74
 
71
75
  local = local_context_for(context)
72
76
 
73
- # expression.differentiate(variable, context)
74
-
75
- argument_values = ast_function.children.map {|child| child.evaluate(local)}
77
+ argument_values = ast_function.children.map {|child| child.evaluated(local)}
76
78
 
77
79
  argument_derivatives = ast_function.children.map do |child|
78
- child.differentiate(variable, context)
80
+ child.differentiated(variable, context)
79
81
  end
80
82
 
83
+ partial_derivatives = calculate_partial_derivatives(context)
84
+
81
85
  AST::Plus.new(
82
86
  argument_derivatives.map.with_index {|argument_derivative, i|
83
- partial_derivative = partial_derivatives[i].replace(argument_variables[i], argument_values[i])
87
+ partial_derivative = partial_derivatives[i]
88
+ argument_variables.each.with_index {|argument_variable, j|
89
+ partial_derivative = partial_derivative.replaced(argument_variable, argument_values[j])
90
+ }
84
91
  AST::Times.new([argument_derivative, partial_derivative])
85
92
  }
86
93
  )
@@ -92,9 +99,9 @@ module Keisan
92
99
  @argument_variables ||= arguments.map {|argument| AST::Variable.new(argument)}
93
100
  end
94
101
 
95
- def partial_derivatives
96
- @partial_derivatives ||= argument_variables.map.with_index do |variable, i|
97
- partial_derivative = expression.differentiate(variable)
102
+ def calculate_partial_derivatives(context)
103
+ argument_variables.map.with_index do |variable, i|
104
+ partial_derivative = expression.differentiated(variable, context)
98
105
  end
99
106
  end
100
107
 
@@ -28,7 +28,7 @@ module Keisan
28
28
 
29
29
  ast_function.instance_variable_set(
30
30
  :@children,
31
- ast_function.children.map {|child| child.evaluate(context).to_node}
31
+ ast_function.children.map {|child| child.simplify(context).to_node}
32
32
  )
33
33
 
34
34
  if ast_function.children.all? {|child| child.well_defined?(context)}
@@ -44,7 +44,7 @@ module Keisan
44
44
 
45
45
  ast_function.instance_variable_set(
46
46
  :@children,
47
- ast_function.children.map {|child| child.evaluate(context)}
47
+ ast_function.children.map {|child| child.simplify(context)}
48
48
  )
49
49
 
50
50
  if ast_function.children.all? {|child| child.is_a?(AST::ConstantLiteral)}
@@ -10,19 +10,19 @@ module Keisan
10
10
  evaluate(ast_function, context).value(context)
11
11
  end
12
12
 
13
- def evaluate(ast_function, context = nil)
13
+ def simplify(ast_function, context = nil)
14
14
  context ||= Context.new
15
15
  expression, variable, replacement = expression_variable_replacement(ast_function)
16
16
 
17
- expression = expression.evaluate(context)
18
- replacement = replacement.evaluate(context)
17
+ expression = expression.simplify(context)
18
+ replacement = replacement.simplify(context)
19
19
 
20
- expression.replace(variable, replacement).evaluate(context)
20
+ expression.replace(variable, replacement).simplify(context)
21
21
  end
22
22
 
23
- def simplify(ast_function, context = nil)
23
+ def evaluate(ast_function, context = nil)
24
24
  context ||= Context.new
25
- evaluate(ast_function, context).simplify(context)
25
+ simplify(ast_function, context).evaluate(context)
26
26
  end
27
27
 
28
28
  private
@@ -241,6 +241,8 @@ module Keisan
241
241
  :"&" => Parsing::BitwiseAnd,
242
242
  :"|" => Parsing::BitwiseOr,
243
243
  :"^" => Parsing::BitwiseXor,
244
+ :<< => Parsing::BitwiseLeftShift,
245
+ :>> => Parsing::BitwiseRightShift,
244
246
  :"==" => Parsing::LogicalEqual,
245
247
  :"!=" => Parsing::LogicalNotEqual,
246
248
  :"&&" => Parsing::LogicalAnd,
@@ -0,0 +1,9 @@
1
+ module Keisan
2
+ module Parsing
3
+ class BitwiseLeftShift < BitwiseOperator
4
+ def node_class
5
+ AST::BitwiseLeftShift
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Keisan
2
+ module Parsing
3
+ class BitwiseRightShift < BitwiseOperator
4
+ def node_class
5
+ AST::BitwiseRightShift
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,235 @@
1
+ module Keisan
2
+ class StringAndGroupParser
3
+ class Portion
4
+ attr_reader :start_index, :end_index
5
+
6
+ def initialize(start_index)
7
+ @start_index = start_index
8
+ end
9
+ end
10
+
11
+ class StringPortion < Portion
12
+ attr_reader :string, :escaped_string
13
+
14
+ def initialize(expression, start_index)
15
+ super(start_index)
16
+
17
+ @string = expression[start_index]
18
+ @escaped_string = expression[start_index]
19
+ @end_index = start_index + 1
20
+
21
+ while @end_index < expression.size
22
+ if expression[@end_index] == quote_type
23
+ @string << quote_type
24
+ @escaped_string << quote_type
25
+ @end_index += 1
26
+ # Successfully parsed the string
27
+ return
28
+ end
29
+
30
+ n, c = get_potentially_escaped_next_character(expression, @end_index)
31
+ @escaped_string << c
32
+ @end_index += n
33
+ end
34
+
35
+ raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, no closing quote #{quote_type}")
36
+ end
37
+
38
+ def size
39
+ string.size
40
+ end
41
+
42
+ def to_s
43
+ string
44
+ end
45
+
46
+ private
47
+
48
+ # Returns number of processed input characters, and the output character
49
+ # If a sequence like '\"' is encountered, the first backslash escapes the
50
+ # second double-quote, and the two characters will act as a one double-quote
51
+ # character.
52
+ def get_potentially_escaped_next_character(expression, index)
53
+ @string << expression[index]
54
+ if expression[index] == "\\" && index + 1 < expression.size
55
+ @string << expression[index + 1]
56
+ return [2, escaped_character(expression[index + 1])]
57
+ else
58
+ return [1, expression[index]]
59
+ end
60
+ end
61
+
62
+ def quote_type
63
+ @string[0]
64
+ end
65
+
66
+ def escaped_character(character)
67
+ case character
68
+ when "\\", '"', "'"
69
+ character
70
+ when "a"
71
+ "\a"
72
+ when "b"
73
+ "\b"
74
+ when "r"
75
+ "\r"
76
+ when "n"
77
+ "\n"
78
+ when "s"
79
+ "\s"
80
+ when "t"
81
+ "\t"
82
+ else
83
+ raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unknown escape character: \"\\#{character}\"")
84
+ end
85
+ end
86
+ end
87
+
88
+ class GroupPortion < Portion
89
+ attr_reader :opening_brace, :closing_brace ,:portions, :size
90
+
91
+ OPENING_TO_CLOSING_BRACE = {
92
+ "(" => ")",
93
+ "{" => "}",
94
+ "[" => "]",
95
+ }
96
+
97
+ def initialize(expression, start_index)
98
+ super(start_index)
99
+
100
+ case expression[start_index]
101
+ when OPEN_GROUP_REGEX
102
+ @opening_brace = expression[start_index]
103
+ else
104
+ raise Keisan::Exceptions::TokenizingError.new("Internal error, GroupPortion did not start with brace")
105
+ end
106
+
107
+ @closing_brace = OPENING_TO_CLOSING_BRACE[opening_brace]
108
+
109
+ parser = StringAndGroupParser.new(expression, start_index: start_index + 1, ending_character: closing_brace)
110
+ @portions = parser.portions
111
+ @size = parser.size + 2
112
+
113
+ if start_index + size > expression.size || expression[start_index + size - 1] != closing_brace
114
+ raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, group with opening brace #{opening_brace} did not have closing brace")
115
+ end
116
+ end
117
+
118
+ def to_s
119
+ opening_brace + portions.map(&:to_s).join + closing_brace
120
+ end
121
+ end
122
+
123
+ class OtherPortion < Portion
124
+ attr_reader :string
125
+
126
+ def initialize(expression, start_index)
127
+ super(start_index)
128
+
129
+ case expression[start_index]
130
+ when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX
131
+ raise Keisan::Exceptions::TokenizingError.new("Internal error, OtherPortion should not have string/braces at start")
132
+ else
133
+ index = start_index + 1
134
+ end
135
+
136
+ while index < expression.size
137
+ case expression[index]
138
+ when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX, COMMENT_CHARACTER_REGEX
139
+ break
140
+ else
141
+ index += 1
142
+ end
143
+ end
144
+
145
+ @end_index = index
146
+ @string = expression[start_index...end_index]
147
+ end
148
+
149
+ def size
150
+ string.size
151
+ end
152
+
153
+ def to_s
154
+ string
155
+ end
156
+ end
157
+
158
+ class CommentPortion < Portion
159
+ attr_reader :string
160
+
161
+ def initialize(expression, start_index)
162
+ super(start_index)
163
+
164
+ if expression[start_index] != '#'
165
+ raise Keisan::Exceptions::TokenizingError.new("Comment should start with '#'")
166
+ else
167
+ index = start_index + 1
168
+ end
169
+
170
+ while index < expression.size
171
+ break if expression[index] == "\n"
172
+ index += 1
173
+ end
174
+
175
+ @end_index = index
176
+ @string = expression[start_index...end_index]
177
+ end
178
+
179
+ def size
180
+ string.size
181
+ end
182
+
183
+ def to_s
184
+ string
185
+ end
186
+ end
187
+
188
+ # An ordered array of "portions", which
189
+ attr_reader :portions, :size
190
+
191
+ COMMENT_CHARACTER_REGEX = /[#]/
192
+ STRING_CHARACTER_REGEX = /["']/
193
+ OPEN_GROUP_REGEX = /[\(\{\[]/
194
+ CLOSED_GROUP_REGEX = /[\)\}\]]/
195
+
196
+ # Ending character is used as a second ending condition besides expression size
197
+ def initialize(expression, start_index: 0, ending_character: nil)
198
+ index = start_index
199
+ @portions = []
200
+
201
+ while index < expression.size && (ending_character.nil? || expression[index] != ending_character)
202
+ case expression[index]
203
+ when STRING_CHARACTER_REGEX
204
+ portion = StringPortion.new(expression, index)
205
+ index = portion.end_index
206
+ @portions << portion
207
+
208
+ when OPEN_GROUP_REGEX
209
+ portion = GroupPortion.new(expression, index)
210
+ index += portion.size
211
+ @portions << portion
212
+
213
+ when CLOSED_GROUP_REGEX
214
+ raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unexpected closing brace #{expression[start_index]}")
215
+
216
+ when COMMENT_CHARACTER_REGEX
217
+ portion = CommentPortion.new(expression, index)
218
+ index += portion.size
219
+ @portions << portion
220
+
221
+ else
222
+ portion = OtherPortion.new(expression, index)
223
+ index += portion.size
224
+ @portions << portion
225
+ end
226
+ end
227
+
228
+ @size = index - start_index
229
+ end
230
+
231
+ def to_s
232
+ portions.map(&:to_s).join
233
+ end
234
+ end
235
+ end
@@ -1,13 +1,12 @@
1
1
  module Keisan
2
2
  class Tokenizer
3
3
  TOKEN_CLASSES = [
4
- Tokens::Group,
5
- Tokens::String,
6
4
  Tokens::Null,
7
5
  Tokens::Boolean,
8
6
  Tokens::Word,
9
7
  Tokens::Number,
10
8
  Tokens::Assignment,
9
+ Tokens::BitwiseShift,
11
10
  Tokens::LogicalOperator,
12
11
  Tokens::ArithmeticOperator,
13
12
  Tokens::BitwiseOperator,
@@ -25,28 +24,31 @@ module Keisan
25
24
  attr_reader :expression, :tokens
26
25
 
27
26
  def initialize(expression)
28
- @expression = self.class.normalize_expression(expression)
29
- @scan = @expression.scan(TOKEN_REGEX)
30
- @tokens = tokenize!
31
- end
27
+ @expression = expression
32
28
 
33
- def self.normalize_expression(expression)
34
- expression = normalize_line_delimiters(expression)
35
- expression = remove_comments(expression)
36
- end
29
+ portions = StringAndGroupParser.new(expression).portions.reject do |portion|
30
+ portion.is_a? StringAndGroupParser::CommentPortion
31
+ end
37
32
 
38
- private
33
+ @tokens = portions.inject([]) do |tokens, portion|
34
+ case portion
35
+ when StringAndGroupParser::StringPortion
36
+ tokens << Tokens::String.new(portion.escaped_string)
37
+ when StringAndGroupParser::GroupPortion
38
+ tokens << Tokens::Group.new(portion.to_s)
39
+ when StringAndGroupParser::OtherPortion
40
+ scan = portion.to_s.scan(TOKEN_REGEX)
41
+ tokens += tokenize!(scan)
42
+ end
39
43
 
40
- def self.normalize_line_delimiters(expression)
41
- expression.gsub(/\n/, ";")
44
+ tokens
45
+ end
42
46
  end
43
47
 
44
- def self.remove_comments(expression)
45
- expression.gsub(/#[^;]*/, "")
46
- end
48
+ private
47
49
 
48
- def tokenize!
49
- @scan.map do |scan_result|
50
+ def tokenize!(scan)
51
+ scan.map do |scan_result|
50
52
  i = scan_result.find_index {|token| !token.nil?}
51
53
  token_string = scan_result[i]
52
54
  token = TOKEN_CLASSES[i].new(token_string)
@@ -13,7 +13,9 @@ module Keisan
13
13
  \% |
14
14
  \& |
15
15
  \| |
16
- \^
16
+ \^ |
17
+ \<\< |
18
+ \>\>
17
19
  )?
18
20
  \=
19
21
  (?!\=) # negative lookahead to prevent matching ==
@@ -0,0 +1,23 @@
1
+ module Keisan
2
+ module Tokens
3
+ class BitwiseShift < Operator
4
+ LEFT_SHIFT = /(?:<<)/
5
+ RIGHT_SHIFT = /(?:>>)/
6
+
7
+ REGEX = /(#{LEFT_SHIFT}|#{RIGHT_SHIFT})/
8
+
9
+ def self.regex
10
+ REGEX
11
+ end
12
+
13
+ def operator_type
14
+ case string
15
+ when LEFT_SHIFT
16
+ :<<
17
+ when RIGHT_SHIFT
18
+ :>>
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,19 +1,13 @@
1
1
  module Keisan
2
2
  module Tokens
3
3
  class Group < Token
4
- REGEX = /(\((?:[^\[\]\(\)\{\}]*+\g<1>*+)*+\)|\[(?:[^\[\]\(\)\{\}]*+\g<1>*+)*+\]|\{(?:[^\[\]\(\)\{\}]*+\g<1>*+)*+\})/
5
-
6
4
  attr_reader :sub_tokens
7
5
 
8
6
  def initialize(string)
9
- super
7
+ @string = string
10
8
  @sub_tokens = Tokenizer.new(string[1...-1]).tokens
11
9
  end
12
10
 
13
- def self.regex
14
- REGEX
15
- end
16
-
17
11
  # Either :round, :square
18
12
  def group_type
19
13
  case string[0]
@@ -1,10 +1,8 @@
1
1
  module Keisan
2
2
  module Tokens
3
3
  class String < Token
4
- REGEX = /(\"[^\"]*\"|\'[^\']*\')/
5
-
6
- def self.regex
7
- REGEX
4
+ def initialize(string)
5
+ @string = string
8
6
  end
9
7
 
10
8
  def value
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.5"
3
3
  end
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-23 00:00:00.000000000 Z
11
+ date: 2020-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: coveralls
14
+ name: cmath
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
19
+ version: '1.0'
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.14'
33
+ version: '2.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.14'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '13.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: A library for parsing equations into an abstract syntax tree for evaluation
98
112
  email:
99
113
  - project.eutopia@gmail.com
@@ -117,8 +131,10 @@ files:
117
131
  - lib/keisan/ast/arithmetic_operator.rb
118
132
  - lib/keisan/ast/assignment.rb
119
133
  - lib/keisan/ast/bitwise_and.rb
134
+ - lib/keisan/ast/bitwise_left_shift.rb
120
135
  - lib/keisan/ast/bitwise_operator.rb
121
136
  - lib/keisan/ast/bitwise_or.rb
137
+ - lib/keisan/ast/bitwise_right_shift.rb
122
138
  - lib/keisan/ast/bitwise_xor.rb
123
139
  - lib/keisan/ast/block.rb
124
140
  - lib/keisan/ast/boolean.rb
@@ -219,10 +235,12 @@ files:
219
235
  - lib/keisan/parsing/arithmetic_operator.rb
220
236
  - lib/keisan/parsing/assignment.rb
221
237
  - lib/keisan/parsing/bitwise_and.rb
238
+ - lib/keisan/parsing/bitwise_left_shift.rb
222
239
  - lib/keisan/parsing/bitwise_not.rb
223
240
  - lib/keisan/parsing/bitwise_not_not.rb
224
241
  - lib/keisan/parsing/bitwise_operator.rb
225
242
  - lib/keisan/parsing/bitwise_or.rb
243
+ - lib/keisan/parsing/bitwise_right_shift.rb
226
244
  - lib/keisan/parsing/bitwise_xor.rb
227
245
  - lib/keisan/parsing/boolean.rb
228
246
  - lib/keisan/parsing/component.rb
@@ -266,11 +284,13 @@ files:
266
284
  - lib/keisan/parsing/unary_plus.rb
267
285
  - lib/keisan/parsing/variable.rb
268
286
  - lib/keisan/repl.rb
287
+ - lib/keisan/string_and_group_parser.rb
269
288
  - lib/keisan/token.rb
270
289
  - lib/keisan/tokenizer.rb
271
290
  - lib/keisan/tokens/arithmetic_operator.rb
272
291
  - lib/keisan/tokens/assignment.rb
273
292
  - lib/keisan/tokens/bitwise_operator.rb
293
+ - lib/keisan/tokens/bitwise_shift.rb
274
294
  - lib/keisan/tokens/boolean.rb
275
295
  - lib/keisan/tokens/colon.rb
276
296
  - lib/keisan/tokens/comma.rb
@@ -308,8 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
308
328
  - !ruby/object:Gem::Version
309
329
  version: '0'
310
330
  requirements: []
311
- rubyforge_project:
312
- rubygems_version: 2.7.7
331
+ rubygems_version: 3.0.3
313
332
  signing_key:
314
333
  specification_version: 4
315
334
  summary: An equation parser and evaluator