keisan 0.8.0 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
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