keisan 0.8.4 → 0.8.9

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: de85515fc642eede46d24bdb2347a7f8bbe32c4d317fd305404c08367e742ecf
4
- data.tar.gz: 0ce75e5555ca880e690e1cae4b76820594899706aa5608944fa5c9eedd0be9c8
3
+ metadata.gz: '0817ad5c7f153f93b34fd0c3861a1b4d9afcb49ad325310f2258f6ea020e444d'
4
+ data.tar.gz: 2d1601371bf02fdacdcbd6f29cf72ce9ec15d46a402579dc89418825fde97a5c
5
5
  SHA512:
6
- metadata.gz: cd6b7cee90edee2d0ba51eb6b4d0a223305eeaf75a85ec6c2c6dbd9c3310ecd035c00d4a86d16768303f28d61996291e6e4d3ac7e5323a3a4df0c22f6343ff58
7
- data.tar.gz: '05873d8324f622a818f5cba2b9ef260e61f3500154db3ab0ecb9570bf308a686f4b32eddcbfb664afab76845fe71da7c82f47dd511f845f33498045011024a8a'
6
+ metadata.gz: 4faa11222178814693fb58d9db1ddafa7097e49a15e5c59aa8132c26a56e8fc8f69487057ad11e2a37d6dd84d1bb7afa9af4b536ed898d9cd082f357e226098d
7
+ data.tar.gz: ba57a649fbaf1fec6dc29495f86921141c627ea1e652cccd884f30edf4f9e70096803f1ed36acab4b010930abb75dc01cb09cdd5f399c2bec0b03c6c27d1d449
@@ -0,0 +1,28 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ ruby-version: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Set up Ruby ${{ matrix.ruby-version }}
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby-version }}
24
+ bundler-cache: true
25
+ - name: Install dependencies
26
+ run: bundle install
27
+ - name: Run tests
28
+ run: bundle exec rspec
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
@@ -17,8 +17,8 @@ module Keisan
17
17
  child.unbound_functions(local)
18
18
  end
19
19
 
20
- def contains_a?(klass)
21
- super || child.contains_a?(klass)
20
+ def traverse(&block)
21
+ super(&block) || child.traverse(&block)
22
22
  end
23
23
 
24
24
  def deep_dup
@@ -15,8 +15,8 @@ module Keisan
15
15
  node.unbound_functions(context)
16
16
  end
17
17
 
18
- def contains_a?(klass)
19
- super || node.contains_a?(klass)
18
+ def traverse(&block)
19
+ super(&block) || node.traverse(&block)
20
20
  end
21
21
 
22
22
  def deep_dup
@@ -21,8 +21,16 @@ 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) }
24
+ def traverse(&block)
25
+ value = super(&block)
26
+ return value if value
27
+ @hash.each do |k, v|
28
+ value = k.to_node.traverse(&block)
29
+ return value if value
30
+ value = v.traverse(&block)
31
+ return value if value
32
+ end
33
+ false
26
34
  end
27
35
 
28
36
  def evaluate(context = nil)
@@ -37,12 +37,26 @@ module Keisan
37
37
  value(context)
38
38
  end
39
39
 
40
+ # Takes a block, and does a DFS down the AST, evaluating the received block
41
+ # at each node, passing in the node as the single argument. If the block
42
+ # returns a truthy value at any point, the DFS ends and the return value is
43
+ # percolated up the tree.
44
+ def traverse(&block)
45
+ block.call(self)
46
+ end
47
+
40
48
  def contains_a?(klass)
41
49
  case klass
42
50
  when Array
43
- klass.any? {|k| is_a?(k) }
51
+ klass.any? do |k|
52
+ traverse do |node|
53
+ node.is_a?(k)
54
+ end
55
+ end
44
56
  else
45
- is_a?(klass)
57
+ traverse do |node|
58
+ node.is_a?(klass)
59
+ end
46
60
  end
47
61
  end
48
62
 
@@ -5,6 +5,11 @@ module Keisan
5
5
 
6
6
  def initialize(number)
7
7
  @number = number
8
+ # Reduce the number if possible
9
+ case @number
10
+ when Rational
11
+ @number = @number.numerator if @number.denominator == 1
12
+ end
8
13
  end
9
14
 
10
15
  def value(context = nil)
@@ -25,8 +25,14 @@ module Keisan
25
25
  end
26
26
  end
27
27
 
28
- def contains_a?(klass)
29
- super || children.any? {|child| child.contains_a?(klass) }
28
+ def traverse(&block)
29
+ value = super(&block)
30
+ return value if value
31
+ children.each do |child|
32
+ value = child.traverse(&block)
33
+ return value if value
34
+ end
35
+ false
30
36
  end
31
37
 
32
38
  def freeze
@@ -4,11 +4,12 @@ module Keisan
4
4
 
5
5
  # Note, allow_recursive would be more appropriately named:
6
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)
7
+ def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true)
8
8
  @context = context || Context.new(
9
9
  allow_recursive: allow_recursive,
10
10
  allow_blocks: allow_blocks,
11
- allow_multiline: allow_multiline
11
+ allow_multiline: allow_multiline,
12
+ allow_random: allow_random
12
13
  )
13
14
  end
14
15
 
@@ -28,6 +29,10 @@ module Keisan
28
29
  context.allow_multiline
29
30
  end
30
31
 
32
+ def allow_random
33
+ context.allow_random
34
+ end
35
+
31
36
  def evaluate(expression, definitions = {})
32
37
  Evaluator.new(self).evaluate(expression, definitions)
33
38
  end
@@ -4,13 +4,15 @@ module Keisan
4
4
  :variable_registry,
5
5
  :allow_recursive,
6
6
  :allow_multiline,
7
- :allow_blocks
7
+ :allow_blocks,
8
+ :allow_random
8
9
 
9
10
  def initialize(parent: nil,
10
11
  random: nil,
11
12
  allow_recursive: false,
12
13
  allow_multiline: true,
13
14
  allow_blocks: true,
15
+ allow_random: true,
14
16
  shadowed: [])
15
17
  @parent = parent
16
18
  @function_registry = Functions::Registry.new(parent: @parent&.function_registry)
@@ -19,6 +21,7 @@ module Keisan
19
21
  @allow_recursive = allow_recursive
20
22
  @allow_multiline = allow_multiline
21
23
  @allow_blocks = allow_blocks
24
+ @allow_random = allow_random
22
25
  end
23
26
 
24
27
  def allow_recursive!
@@ -111,7 +114,13 @@ module Keisan
111
114
  end
112
115
 
113
116
  def random
114
- @random || @parent&.random || Random.new
117
+ raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with randomness") unless allow_random
118
+ @random ||= @parent&.random || Random.new
119
+ end
120
+
121
+ def set_random(random)
122
+ raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with randomness") unless allow_random
123
+ @random = random
115
124
  end
116
125
 
117
126
  protected
@@ -21,14 +21,17 @@ module Keisan
21
21
  context ||= Context.new
22
22
 
23
23
  operand, arguments, expression = operand_arguments_expression_for(ast_function, context)
24
+
25
+ # Extract underlying operand for cells
26
+ real_operand = operand.is_a?(AST::Cell) ? operand.node : operand
24
27
 
25
- case operand
28
+ case real_operand
26
29
  when AST::List
27
- evaluate_list(operand, arguments, expression, context).evaluate(context)
30
+ evaluate_list(real_operand, arguments, expression, context).evaluate(context)
28
31
  when AST::Hash
29
- evaluate_hash(operand, arguments, expression, context).evaluate(context)
32
+ evaluate_hash(real_operand, arguments, expression, context).evaluate(context)
30
33
  else
31
- raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{operand}")
34
+ raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{real_operand}")
32
35
  end
33
36
  end
34
37
 
@@ -9,24 +9,26 @@ module Keisan
9
9
  end
10
10
 
11
11
  class StringPortion < Portion
12
- attr_reader :string
12
+ attr_reader :string, :escaped_string
13
13
 
14
14
  def initialize(expression, start_index)
15
15
  super(start_index)
16
16
 
17
17
  @string = expression[start_index]
18
+ @escaped_string = expression[start_index]
18
19
  @end_index = start_index + 1
19
20
 
20
21
  while @end_index < expression.size
21
22
  if expression[@end_index] == quote_type
22
23
  @string << quote_type
24
+ @escaped_string << quote_type
23
25
  @end_index += 1
24
26
  # Successfully parsed the string
25
27
  return
26
28
  end
27
29
 
28
- n, c = process_next_character(expression, @end_index)
29
- @string << c
30
+ n, c = get_potentially_escaped_next_character(expression, @end_index)
31
+ @escaped_string << c
30
32
  @end_index += n
31
33
  end
32
34
 
@@ -44,9 +46,13 @@ module Keisan
44
46
  private
45
47
 
46
48
  # Returns number of processed input characters, and the output character
47
- def process_next_character(expression, index)
48
- # escape character
49
- if expression[index] == "\\"
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]
50
56
  return [2, escaped_character(expression[index + 1])]
51
57
  else
52
58
  return [1, expression[index]]
@@ -33,7 +33,7 @@ module Keisan
33
33
  @tokens = portions.inject([]) do |tokens, portion|
34
34
  case portion
35
35
  when StringAndGroupParser::StringPortion
36
- tokens << Tokens::String.new(portion.to_s)
36
+ tokens << Tokens::String.new(portion.escaped_string)
37
37
  when StringAndGroupParser::GroupPortion
38
38
  tokens << Tokens::Group.new(portion.to_s)
39
39
  when StringAndGroupParser::OtherPortion
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.8.4"
2
+ VERSION = "0.8.9"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.4
4
+ version: 0.8.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-16 00:00:00.000000000 Z
11
+ date: 2021-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmath
@@ -115,9 +115,9 @@ executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - ".github/workflows/ruby.yml"
118
119
  - ".gitignore"
119
120
  - ".rspec"
120
- - ".travis.yml"
121
121
  - Gemfile
122
122
  - MIT-LICENSE
123
123
  - README.md
@@ -328,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
328
328
  - !ruby/object:Gem::Version
329
329
  version: '0'
330
330
  requirements: []
331
- rubygems_version: 3.0.3
331
+ rubygems_version: 3.2.15
332
332
  signing_key:
333
333
  specification_version: 4
334
334
  summary: An equation parser and evaluator
data/.travis.yml DELETED
@@ -1,9 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- before_install:
4
- - gem install bundler
5
- rvm:
6
- - 2.4.9
7
- - 2.5.7
8
- - 2.6.5
9
- - 2.7.0