keisan 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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