keisan 0.1.0 → 0.2.0

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
  SHA1:
3
- metadata.gz: 5118e044fef1a400782707bed4cff70265f601ed
4
- data.tar.gz: 54db3071552f280ed42f2b86e714ebb1b8a356da
3
+ metadata.gz: cdf00ba25b7bc64cde85408ef0f56ad56bece933
4
+ data.tar.gz: ebccd470d71300d5968b2efaf38cd7dccd39b675
5
5
  SHA512:
6
- metadata.gz: 1219fa0069d41b5b5295320d82ebb5ec82a9c11c01b94eae5adfafa932cc603db4185fcd47676bb36b45716a276436b8a71ec8aa175d0de2b2058f2b816440eb
7
- data.tar.gz: aad7604a738e66b385c40dd3cf78a6d2a8c28ebeb8c35e206f35d81d5ae8aed3d8256de70dfd34ff47161d53ef671f1380631588adb20c65086739953d34d8d2
6
+ metadata.gz: 64889b4e5947a90b46ad1a404b8ee4401cc0ac34d7ef18ca26f150f44208dd61a130cc5c2fed6dcba3c5ffa9ce4ff36b26721e5c64516feb146d15afe8ebaf93
7
+ data.tar.gz: 114a84f7120b53c028d7f0fa5226aa01e034e593c911713bd5a394feef3ff436b52045c753a1c1dd796652f2cf9680b7c1d8a4dc6fa44fe5c53e2caedcd1775a
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /keisan-*.gem
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Keisan
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/keisan.svg)](https://badge.fury.io/rb/keisan)
3
4
  [![Build Status](https://travis-ci.org/project-eutopia/keisan.png?branch=master)](https://travis-ci.org/project-eutopia/keisan)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
5
6
  [![Hakiri](https://hakiri.io/github/project-eutopia/keisan/master.svg)](https://hakiri.io/github/project-eutopia/keisan)
6
7
 
7
8
  Keisan ([計算, to calculate](https://en.wiktionary.org/wiki/%E8%A8%88%E7%AE%97#Japanese)) is a Ruby library for parsing equations into an abstract syntax tree. This allows for safe evaluation of string representations of mathematical/logical expressions.
@@ -122,6 +123,41 @@ calculator.evaluate("'hello'[1]")
122
123
  #=> "e"
123
124
  ```
124
125
 
126
+ ##### Binary, octal, and hexadecimal numbers
127
+
128
+ Using the prefixes `0b`, `0o`, and `0x` (standard in Ruby) indicates binary, octal, and hexadecimal numbers respectively.
129
+
130
+ ```ruby
131
+ calculator.evaluate("0b1100")
132
+ #=> 12
133
+ calculator.evaluate("0o775")
134
+ #=> 504
135
+ calculator.evaluate("0x1f0")
136
+ #=> 496
137
+ ```
138
+
139
+ ##### Random numbers
140
+
141
+ `keisan` has a couple methods for doing random operations, `rand` and `sample`. For example,
142
+
143
+ ```ruby
144
+ calculator.evaluate("rand(10)")
145
+ #=> 3
146
+ calculator.evaluate("sample([2, 4, 6, 8])")
147
+ #=> 8
148
+ ```
149
+
150
+ If you want reproducibility, you can pass in your own `Random` object to the calculator's context.
151
+
152
+ ```ruby
153
+ calculator1 = Keisan::Calculator.new(Keisan::Context.new(random: Random.new(1234)))
154
+ calculator2 = Keisan::Calculator.new(Keisan::Context.new(random: Random.new(1234)))
155
+ 5.times.map {calculator1.evaluate("rand(1000)")}
156
+ #=> [815, 723, 294, 53, 204]
157
+ 5.times.map {calculator2.evaluate("rand(1000)")}
158
+ #=> [815, 723, 294, 53, 204]
159
+ ```
160
+
125
161
  ##### Builtin variables and functions
126
162
 
127
163
  `keisan` includes all standard methods given by the Ruby `Math` class.
@@ -1,4 +1,3 @@
1
- require "cmath"
2
1
  require "active_support"
3
2
  require "active_support/core_ext"
4
3
 
@@ -3,32 +3,22 @@ module Keisan
3
3
  class Builder
4
4
  # Build from parser
5
5
  def initialize(string: nil, parser: nil, components: nil)
6
- if string.nil? && parser.nil? && components.nil?
7
- raise Keisan::Exceptions::InternalError.new("Require parser or components")
6
+ if [string, parser, components].select(&:nil?).size != 2
7
+ raise Keisan::Exceptions::InternalError.new("Require one of string, parser or components")
8
8
  end
9
9
 
10
- if string.present?
10
+ if !string.nil?
11
11
  @components = Keisan::Parser.new(string: string).components
12
- elsif parser.present?
12
+ elsif !parser.nil?
13
13
  @components = parser.components
14
14
  else
15
15
  @components = Array.wrap(components)
16
16
  end
17
17
 
18
- @nodes = @components.split {|component|
19
- component.is_a?(Keisan::Parsing::Operator)
20
- }.map {|group_of_components|
21
- node_from_components(group_of_components)
22
- }
18
+ @nodes = nodes_split_by_operators(@components)
23
19
  @operators = @components.select {|component| component.is_a?(Keisan::Parsing::Operator)}
24
20
 
25
- @priorities = @operators.map(&:priority)
26
-
27
- while @operators.count > 0
28
- priorities = @operators.map(&:priority)
29
- max_priority = priorities.uniq.max
30
- consume_operators_with_priority!(max_priority)
31
- end
21
+ consume_operators!
32
22
 
33
23
  unless @nodes.count == 1
34
24
  raise Keisan::Exceptions::ASTError.new("Should end up with a single node")
@@ -45,7 +35,41 @@ module Keisan
45
35
 
46
36
  private
47
37
 
38
+ def nodes_split_by_operators(components)
39
+ components.split {|component|
40
+ component.is_a?(Keisan::Parsing::Operator)
41
+ }.map {|group_of_components|
42
+ node_from_components(group_of_components)
43
+ }
44
+ end
45
+
48
46
  def node_from_components(components)
47
+ unary_components, node, indexing_components = *unarys_node_indexings(components)
48
+
49
+ # Apply postfix indexing operators
50
+ indexing_components.each do |indexing_component|
51
+ node = indexing_component.node_class.new(
52
+ node,
53
+ indexing_component.arguments.map {|parsing_argument|
54
+ Builder.new(components: parsing_argument.components).node
55
+ }
56
+ )
57
+ end
58
+
59
+ # Apply prefix unary operators
60
+ unary_components.reverse.each do |unary_component|
61
+ node = unary_component.node_class.new(node)
62
+ end
63
+
64
+ node
65
+ end
66
+
67
+ # Returns an array of the form
68
+ # [unary_operators, middle_node, postfix_indexings]
69
+ # unary_operators is an array of Keisan::Parsing::UnaryOperator objects
70
+ # middle_node is the main node which will be modified by prefix and postfix operators
71
+ # postfix_indexings is an array of Keisan::Parsing::Indexing objects
72
+ def unarys_node_indexings(components)
49
73
  index_of_unary_components = components.map.with_index {|c,i| [c,i]}.select {|c,i| c.is_a?(Keisan::Parsing::UnaryOperator)}.map(&:last)
50
74
  # Must be all in the front
51
75
  unless index_of_unary_components.map.with_index.all? {|i,j| i == j}
@@ -64,25 +88,11 @@ module Keisan
64
88
  raise Keisan::Exceptions::ASTError.new("have too many components")
65
89
  end
66
90
 
67
- unary_components = index_of_unary_components.map {|i| components[i]}
68
- indexing_components = index_of_indexing_components.map {|i| components[i]}
69
-
70
- node = node_of_component(components[unary_components.size])
71
-
72
- indexing_components.each do |indexing_component|
73
- node = indexing_component.node_class.new(
74
- node,
75
- indexing_component.arguments.map {|parsing_argument|
76
- Builder.new(components: parsing_argument.components).node
77
- }
78
- )
79
- end
80
-
81
- unary_components.reverse.each do |unary_component|
82
- node = unary_component.node_class.new(node)
83
- end
84
-
85
- node
91
+ [
92
+ index_of_unary_components.map {|i| components[i]},
93
+ node_of_component(components[index_of_unary_components.size]),
94
+ index_of_indexing_components.map {|i| components[i]}
95
+ ]
86
96
  end
87
97
 
88
98
  def node_of_component(component)
@@ -117,10 +127,20 @@ module Keisan
117
127
  end
118
128
  end
119
129
 
130
+ def consume_operators!
131
+ while @operators.count > 0
132
+ priorities = @operators.map(&:priority)
133
+ max_priority = priorities.uniq.max
134
+ consume_operators_with_priority!(max_priority)
135
+ end
136
+ end
137
+
120
138
  def consume_operators_with_priority!(priority)
121
139
  # Treat back-to-back operators with same priority as one single call (e.g. 1 + 2 + 3 is add(1,2,3))
122
140
  while @operators.any? {|operator| operator.priority == priority}
123
- next_operator_group = @operators.each.with_index.to_a.split {|operator,i| operator.priority != priority}.select(&:present?).first
141
+ next_operator_group = @operators.each.with_index.to_a.split {|operator,i|
142
+ operator.priority != priority
143
+ }.select {|ops| !ops.empty?}.first
124
144
  operator_group_indexes = next_operator_group.map(&:last)
125
145
 
126
146
  first_index = operator_group_indexes.first
@@ -7,16 +7,18 @@ module Keisan
7
7
  end
8
8
 
9
9
  def evaluate(expression, definitions = {})
10
- local_context = context.spawn_child
11
- definitions.each do |name, value|
12
- case value
13
- when Proc
14
- local_context.register_function!(name, value)
15
- else
16
- local_context.register_variable!(name, value)
10
+ context.spawn_child do |local|
11
+ definitions.each do |name, value|
12
+ case value
13
+ when Proc
14
+ local.register_function!(name, value)
15
+ else
16
+ local.register_variable!(name, value)
17
+ end
17
18
  end
19
+
20
+ Keisan::AST::Builder.new(string: expression).ast.value(local)
18
21
  end
19
- Keisan::AST::Builder.new(string: expression).ast.value(local_context)
20
22
  end
21
23
 
22
24
  def define_variable!(name, value)
@@ -10,7 +10,11 @@ module Keisan
10
10
  end
11
11
 
12
12
  def spawn_child
13
- self.class.new(parent: self)
13
+ if block_given?
14
+ yield self.class.new(parent: self)
15
+ else
16
+ self.class.new(parent: self)
17
+ end
14
18
  end
15
19
 
16
20
  def function(name)
@@ -28,15 +28,6 @@ module Keisan
28
28
  end
29
29
  )
30
30
  end
31
-
32
- %i(exp sin cos).each do |method|
33
- registry.register!(
34
- :"c#{method}",
35
- Proc.new do |z|
36
- CMath.send(method, z)
37
- end
38
- )
39
- end
40
31
  end
41
32
 
42
33
  def self.register_branch_methods!(registry)
@@ -13,7 +13,7 @@ module Keisan
13
13
 
14
14
  def [](name)
15
15
  return @hash[name] if @hash.has_key?(name)
16
- return @parent[name] if @parent.present? && @parent.has_name?(name)
16
+ return @parent[name] if !@parent.nil? && @parent.has_name?(name)
17
17
  return default_registry[name] if @use_defaults && default_registry.has_name?(name)
18
18
  raise Keisan::Exceptions::UndefinedFunctionError.new name
19
19
  end
@@ -7,7 +7,7 @@ module Keisan
7
7
  raise Keisan::Exceptions::InternalError.new("Invalid arguments")
8
8
  end
9
9
 
10
- if string.present?
10
+ if !string.nil?
11
11
  @tokens = Tokenizer.new(string).tokens
12
12
  else
13
13
  raise Keisan::Exceptions::InternalError.new("Invalid argument: tokens = #{tokens}") if tokens.nil? || !tokens.is_a?(Array)
@@ -67,8 +67,10 @@ module Keisan
67
67
  end
68
68
 
69
69
  elsif @components[-1].is_a?(Parsing::Element)
70
+ # A word followed by a "round group" is actually a function: e.g. sin(x)
70
71
  if @components[-1].is_a?(Parsing::Variable) && token.type == :group && token.group_type == :round
71
72
  add_function_to_components!(token)
73
+ # Here it is a postfix Indexing (access elements by index)
72
74
  elsif token.type == :group && token.group_type == :square
73
75
  add_indexing_to_components!(token)
74
76
  else
@@ -78,7 +80,7 @@ module Keisan
78
80
  end
79
81
 
80
82
  else
81
- raise Keisan::Exceptions::InternalError.new("Invalid parsing!")
83
+ raise Keisan::Exceptions::ParseError.new("Token cannot be parsed, #{token.string}")
82
84
  end
83
85
  end
84
86
 
@@ -20,13 +20,26 @@ module Keisan
20
20
  attr_reader :expression, :tokens
21
21
 
22
22
  def initialize(expression)
23
- @expression = expression.split(Keisan::Tokens::String.regex).map.with_index {|s,i| i.even? ? s.gsub(/\s+/, "") : s}.join
24
-
23
+ @expression = self.class.strip_whitespace(expression)
25
24
  @scan = @expression.scan(TOKEN_REGEX)
25
+ @tokens = tokenize!
26
+ end
26
27
 
28
+ def self.strip_whitespace(expression)
29
+ # Do not allow whitespace between variables, numbers, and the like; they must be joined by operators
30
+ raise Keisan::Exceptions::TokenizingError.new if expression.gsub(Tokens::String.regex, "").match /\w\s+\w/
31
+
32
+ # Only strip whitespace outside of strings, e.g.
33
+ # "1 + 2 + 'hello world'" => "1+2+'hello world'"
34
+ expression.split(Keisan::Tokens::String.regex).map.with_index {|s,i| i.even? ? s.gsub(/\s+/, "") : s}.join
35
+ end
36
+
37
+ private
38
+
39
+ def tokenize!
27
40
  tokenizing_check = ""
28
41
 
29
- @tokens = @scan.map do |scan_result|
42
+ tokens = @scan.map do |scan_result|
30
43
  i = scan_result.find_index {|token| !token.nil?}
31
44
  token_string = scan_result[i]
32
45
  tokenizing_check << token_string
@@ -36,6 +49,8 @@ module Keisan
36
49
  unless tokenizing_check == @expression
37
50
  raise Keisan::Exceptions::TokenizingError.new("Expected \"#{@expression}\", tokenized \"#{tokenizing_check}\"")
38
51
  end
52
+
53
+ tokens
39
54
  end
40
55
  end
41
56
  end
@@ -2,10 +2,13 @@ module Keisan
2
2
  module Tokens
3
3
  class Number < Token
4
4
  INTEGER_REGEX = /\d+/
5
+ BINARY_REGEX = /0b[0-1]+/
6
+ OCTAL_REGEX = /0o[0-7]+/
7
+ HEX_REGEX = /0x[0-9a-fA-F]+/
5
8
  FLOATING_POINT_REGEX = /\d+\.\d+/
6
- SCIENTIFC_NOTATION_REGEX = /\d+(?:\.\d+)e(?:\+|\-)?\d+/
9
+ SCIENTIFIC_NOTATION_REGEX = /\d+(?:\.\d+)?e(?:\+|\-)?\d+/
7
10
 
8
- REGEX = /(\d+(?:\.\d+)?(?:e(?:\+|\-)?\d+)?)/
11
+ REGEX = /(#{BINARY_REGEX}|#{OCTAL_REGEX}|#{HEX_REGEX}|\d+(?:\.\d+)?(?:e(?:\+|\-)?\d+)?)/
9
12
 
10
13
  def self.regex
11
14
  REGEX
@@ -13,9 +16,9 @@ module Keisan
13
16
 
14
17
  def value
15
18
  case string
16
- when SCIENTIFC_NOTATION_REGEX, FLOATING_POINT_REGEX
19
+ when /\A#{SCIENTIFIC_NOTATION_REGEX}\z/.freeze, /\A#{FLOATING_POINT_REGEX}\z/.freeze
17
20
  Float(string)
18
- when INTEGER_REGEX
21
+ else
19
22
  Integer(string)
20
23
  end
21
24
  end
@@ -1,7 +1,7 @@
1
1
  module Keisan
2
2
  module Tokens
3
3
  class Word < Token
4
- REGEX = /([a-zA-Z0-9_]*[a-zA-Z][a-zA-Z0-9_]*)/
4
+ REGEX = /([a-zA-Z_]\w*)/
5
5
 
6
6
  def self.regex
7
7
  REGEX
@@ -13,7 +13,7 @@ module Keisan
13
13
 
14
14
  def [](name)
15
15
  return @hash[name] if @hash.has_key?(name)
16
- return @parent[name] if @parent.present? && @parent.has_name?(name)
16
+ return @parent[name] if !@parent.nil? && @parent.has_name?(name)
17
17
  return default_registry[name] if @use_defaults && default_registry.has_name?(name)
18
18
  raise Keisan::Exceptions::UndefinedVariableError.new name
19
19
  end
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-24 00:00:00.000000000 Z
11
+ date: 2017-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport