keisan 0.7.0 → 0.8.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +4 -3
  3. data/README.md +39 -2
  4. data/keisan.gemspec +1 -3
  5. data/lib/keisan.rb +3 -3
  6. data/lib/keisan/ast.rb +25 -0
  7. data/lib/keisan/ast/boolean.rb +1 -1
  8. data/lib/keisan/ast/builder.rb +2 -2
  9. data/lib/keisan/ast/cell.rb +6 -0
  10. data/lib/keisan/ast/date.rb +23 -0
  11. data/lib/keisan/ast/date_time_methods.rb +75 -0
  12. data/lib/keisan/ast/function.rb +9 -0
  13. data/lib/keisan/ast/logical_and.rb +20 -3
  14. data/lib/keisan/ast/logical_equal.rb +6 -5
  15. data/lib/keisan/ast/logical_greater_than.rb +6 -4
  16. data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +6 -4
  17. data/lib/keisan/ast/logical_less_than.rb +6 -4
  18. data/lib/keisan/ast/logical_less_than_or_equal_to.rb +6 -4
  19. data/lib/keisan/ast/logical_not_equal.rb +6 -5
  20. data/lib/keisan/ast/logical_operator.rb +24 -0
  21. data/lib/keisan/ast/logical_or.rb +18 -1
  22. data/lib/keisan/ast/number.rb +4 -0
  23. data/lib/keisan/ast/operator.rb +1 -1
  24. data/lib/keisan/ast/parent.rb +1 -1
  25. data/lib/keisan/ast/plus.rb +10 -0
  26. data/lib/keisan/ast/time.rb +23 -0
  27. data/lib/keisan/ast/unary_operator.rb +1 -1
  28. data/lib/keisan/ast/variable.rb +2 -5
  29. data/lib/keisan/context.rb +6 -6
  30. data/lib/keisan/exceptions.rb +3 -0
  31. data/lib/keisan/function.rb +6 -0
  32. data/lib/keisan/functions/break.rb +11 -0
  33. data/lib/keisan/functions/continue.rb +11 -0
  34. data/lib/keisan/functions/default_registry.rb +36 -0
  35. data/lib/keisan/functions/enumerable_function.rb +10 -2
  36. data/lib/keisan/functions/filter.rb +6 -0
  37. data/lib/keisan/functions/loop_control_flow_function.rb +22 -0
  38. data/lib/keisan/functions/map.rb +6 -0
  39. data/lib/keisan/functions/reduce.rb +5 -0
  40. data/lib/keisan/functions/while.rb +7 -1
  41. data/lib/keisan/parser.rb +5 -5
  42. data/lib/keisan/parsing/function.rb +1 -1
  43. data/lib/keisan/parsing/hash.rb +2 -2
  44. data/lib/keisan/token.rb +1 -1
  45. data/lib/keisan/util.rb +19 -0
  46. data/lib/keisan/variables/default_registry.rb +2 -1
  47. data/lib/keisan/version.rb +1 -1
  48. metadata +11 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 900d86c9daad1cab75e6140a21785f6de67ead1c
4
- data.tar.gz: f2c5f2abc85d46ce71aa651f0cc860363a8790bf
2
+ SHA256:
3
+ metadata.gz: f2d556d5ef3e67015596fee5856e2fe0b27295b227f7d4edf8cb41c037aed98b
4
+ data.tar.gz: c8c20ac6ba6a9ad27654931238a2dbafcbbbc509b58fd2a74161b7a38b3edaab
5
5
  SHA512:
6
- metadata.gz: 4f5e7ea1fd844e7927af6eb5b66edacc53dac30541cb670d6208e43d0a9edb4611417943be232ba8188c275fa8890ee3c3e7f43a874ae32085f80b05092ecef6
7
- data.tar.gz: 588daa93a53120d0456f38e66f5fa27d50b5e517516c3e0747519e51aeb5ca49dc317187e85ec420e5eec5bc722732138499e16188bf7271afbc4481ba1980bd
6
+ metadata.gz: 9ccc5172adc0b5ebafabe3cd5846fb455ada6e1351c94deab4da5bfd38617c0e0692258e9384d760d25b88a899b46de1899b1688850bfac8ddcdd432091ddf50
7
+ data.tar.gz: cb2ca195319a744f3ee9b56ef7df14d8dfcf36dbcafcbe1e675a858f232457d39cb3fe96d7f056a26746630d4a7f1c47a8fe6fc75a880683cd2120428a448ff6
data/.travis.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.5
5
- - 2.4.2
6
- before_install: gem install bundler -v 1.16.0
4
+ - 2.3.6
5
+ - 2.4.3
6
+ - 2.5.0
7
+ before_install: gem install bundler -v 1.16.1
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  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.
11
11
  This allows for safe evaluation of string representations of mathematical/logical expressions.
12
- It has support for variables, functions, conditionals, and loops, making it a Turing complete programming language.
12
+ It has support for variables, functions, conditionals, and loops, making it a [Turing complete](https://github.com/project-eutopia/keisan/blob/master/spec/keisan/turing_machine_spec.rb) programming language.
13
13
 
14
14
  ## Installation
15
15
 
@@ -119,6 +119,9 @@ calculator.evaluate("f(0-a)", a: 2)
119
119
  #=> -20
120
120
  calculator.evaluate("n") # n only exists in the definition of f(x)
121
121
  #=> Keisan::Exceptions::UndefinedVariableError: n
122
+ calculator.evaluate("includes(a, element) = a.reduce(false, found, x, found || (x == element))")
123
+ calculator.evaluate("[3, 9].map(x, [1, 3, 5].includes(x))").value
124
+ #=> [true, false]
122
125
  ```
123
126
 
124
127
  This form even supports recursion, but you must explicitly allow it.
@@ -248,6 +251,36 @@ calculator.evaluate("range(1, 6).map(x, [x, x**2]).to_h")
248
251
  #=> {1 => 1, 2 => 4, 3 => 9, 4 => 16, 5 => 25}
249
252
  ```
250
253
 
254
+ ##### Date and time objects
255
+
256
+ Keisan supports date and time objects like in Ruby.
257
+ 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
+ They support methods `year`, `month`, `day`, `weekday`, `strftime`, and `to_time` to convert to a time object.
259
+
260
+ ```ruby
261
+ calculator = Keisan::Calculator.new
262
+ calculator.evaluate("x = 11")
263
+ calculator.evaluate("(5 + date(2018, x, 2*x)).day")
264
+ #=> 27
265
+ calculator.evaluate("today() > date(2018, 11, 1)")
266
+ #=> true
267
+ calculator.evaluate("date('1999-12-31').to_time + 10")
268
+ #=> Time.new(1999, 12, 31, 0, 0, 10)
269
+ ```
270
+
271
+ Time objects are created using `time` (either a string to be parsed, or year, month, day, hour, minute, second arguments) or `now`.
272
+ They support methods `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`, `strftime`, and `to_date` to convert to a date object.
273
+
274
+ ```ruby
275
+ calculator = Keisan::Calculator.new
276
+ calculator.evaluate("time(2018, 11, 22, 12, 0, 0).to_date <= date(2018, 11, 22)")
277
+ #=> true
278
+ calculator.evaluate("time('2000-4-15 12:34:56').minute")
279
+ #=> 34
280
+ calculator.evaluate("time('5000-10-10 20:30:40').strftime('%b %d, %Y')")
281
+ #=> "Oct 10, 5000"
282
+ ```
283
+
251
284
  ##### Functional programming methods
252
285
 
253
286
  Keisan also supports the basic functional programming operators `map` (or `collect`), `filter` (or `select`), and `reduce` (or `inject`).
@@ -293,12 +326,16 @@ calculator.evaluate("2 + if(1 > 0, 10, 29)")
293
326
  ```
294
327
 
295
328
  For looping, you can use the basic `while` loop, which has an expression that evaluates to a boolean as the first argument, and any expression in the second argument.
329
+ One can use the keywords `break` and `continue` to control loop flow as well.
296
330
 
297
331
  ```ruby
298
332
  calculator = Keisan::Calculator.new
299
333
  calculator.evaluate("my_sum(a) = {let i = 0; let total = 0; while(i < a.size, {total += a[i]; i += 1}); total}")
300
334
  calculator.evaluate("my_sum([1,3,5,7,9])")
301
335
  #=> 25
336
+ calculator.evaluate("has_element(a, x) = {let i=0; let found=false; while(i<a.size, if(a[i] == x, found = true; break); i+=1); found}")
337
+ calculator.evaluate("[2, 3, 7, 11].has_element(11)")
338
+ #=> true
302
339
  ```
303
340
 
304
341
  ##### Bitwise operations
@@ -368,7 +405,7 @@ calculator.evaluate("log10(1000)")
368
405
  #=> 3.0
369
406
  ```
370
407
 
371
- Furthermore, the constants `PI`, `E`, and `I` are included.
408
+ Furthermore, the constants `PI`, `E`, `I`, and `INF` are included.
372
409
 
373
410
  ```ruby
374
411
  calculator = Keisan::Calculator.new
data/keisan.gemspec CHANGED
@@ -19,9 +19,7 @@ Gem::Specification.new do |spec|
19
19
  end
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.required_ruby_version = ">= 2.0.0"
23
-
24
- spec.add_dependency "activesupport", ">= 4.2.2"
22
+ spec.required_ruby_version = ">= 2.3.0"
25
23
 
26
24
  spec.add_development_dependency "coveralls"
27
25
  spec.add_development_dependency "bundler", "~> 1.14"
data/lib/keisan.rb CHANGED
@@ -1,8 +1,6 @@
1
- require "active_support"
2
- require "active_support/core_ext"
3
-
4
1
  require "keisan/version"
5
2
  require "keisan/exceptions"
3
+ require "keisan/util"
6
4
 
7
5
  require "keisan/ast/node"
8
6
  require "keisan/ast/cell"
@@ -14,6 +12,8 @@ require "keisan/ast/number"
14
12
  require "keisan/ast/string"
15
13
  require "keisan/ast/null"
16
14
  require "keisan/ast/boolean"
15
+ require "keisan/ast/date"
16
+ require "keisan/ast/time"
17
17
 
18
18
  require "keisan/ast/block"
19
19
  require "keisan/ast/parent"
data/lib/keisan/ast.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require "date"
2
+ require "time"
3
+
1
4
  module Keisan
2
5
  module AST
3
6
  def self.parse(expression)
@@ -76,6 +79,26 @@ module KeisanHash
76
79
  end
77
80
  end
78
81
 
82
+ module KeisanDate
83
+ def to_node
84
+ Keisan::AST::Date.new(self)
85
+ end
86
+
87
+ def value(context = nil)
88
+ self
89
+ end
90
+ end
91
+
92
+ module KeisanTime
93
+ def to_node
94
+ Keisan::AST::Time.new(self)
95
+ end
96
+
97
+ def value(context = nil)
98
+ self
99
+ end
100
+ end
101
+
79
102
  class Numeric; prepend KeisanNumeric; end
80
103
  class String; prepend KeisanString; end
81
104
  class TrueClass; prepend KeisanTrueClass; end
@@ -83,3 +106,5 @@ class FalseClass; prepend KeisanFalseClass; end
83
106
  class NilClass; prepend KeisanNilClass; end
84
107
  class Array; prepend KeisanArray; end
85
108
  class Hash; prepend KeisanHash; end
109
+ class Date; prepend KeisanDate; end
110
+ class Time; prepend KeisanTime; end
@@ -12,7 +12,7 @@ module Keisan
12
12
  end
13
13
 
14
14
  def true?
15
- false
15
+ bool
16
16
  end
17
17
 
18
18
  def !
@@ -12,10 +12,10 @@ module Keisan
12
12
  elsif !parser.nil?
13
13
  @components = parser.components
14
14
  else
15
- @components = Array.wrap(components)
15
+ @components = Array(components)
16
16
  end
17
17
 
18
- @lines = @components.split {|component|
18
+ @lines = Util.array_split(@components) {|component|
19
19
  component.is_a?(Parsing::LineSeparator)
20
20
  }.reject(&:empty?)
21
21
 
@@ -72,6 +72,12 @@ module Keisan
72
72
  def to_node
73
73
  node
74
74
  end
75
+
76
+ %i(< <= > >= equal not_equal).each do |sym|
77
+ define_method(sym) {|other|
78
+ node.send(sym, other.to_node)
79
+ }
80
+ end
75
81
  end
76
82
  end
77
83
  end
@@ -0,0 +1,23 @@
1
+ require_relative "date_time_methods"
2
+
3
+ module Keisan
4
+ module AST
5
+ class Date < ConstantLiteral
6
+ include DateTimeMethods
7
+
8
+ attr_reader :date
9
+
10
+ def initialize(date)
11
+ @date = date
12
+ end
13
+
14
+ def value(context = nil)
15
+ date
16
+ end
17
+
18
+ def to_s
19
+ value.to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,75 @@
1
+ module Keisan
2
+ module AST
3
+ module DateTimeMethods
4
+ def +(other)
5
+ other = other.to_node
6
+ case other
7
+ when Number
8
+ self.class.new(value + other.value)
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def >(other)
15
+ other = other.to_node
16
+ case other
17
+ when self.class
18
+ Boolean.new(value.to_time > other.value.to_time)
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def <(other)
25
+ other = other.to_node
26
+ case other
27
+ when self.class
28
+ Boolean.new(value.to_time < other.value.to_time)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def >=(other)
35
+ other = other.to_node
36
+ case other
37
+ when self.class
38
+ Boolean.new(value.to_time >= other.value.to_time)
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def <=(other)
45
+ other = other.to_node
46
+ case other
47
+ when self.class
48
+ Boolean.new(value.to_time <= other.value.to_time)
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def equal(other)
55
+ other = other.to_node
56
+ case other
57
+ when self.class
58
+ Boolean.new(value.to_time == other.value.to_time)
59
+ else
60
+ super
61
+ end
62
+ end
63
+
64
+ def not_equal(other)
65
+ other = other.to_node
66
+ case other
67
+ when self.class
68
+ Boolean.new(value.to_time != other.value.to_time)
69
+ else
70
+ super
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -13,6 +13,15 @@ module Keisan
13
13
  function_from_context(context).value(self, context)
14
14
  end
15
15
 
16
+ def unbound_variables(context = nil)
17
+ context ||= Context.new
18
+ if context.has_function?(name)
19
+ function_from_context(context).unbound_variables(children, context)
20
+ else
21
+ super
22
+ end
23
+ end
24
+
16
25
  def unbound_functions(context = nil)
17
26
  context ||= Context.new
18
27
 
@@ -5,18 +5,35 @@ module Keisan
5
5
  :"&&"
6
6
  end
7
7
 
8
+ def blank_value
9
+ true
10
+ end
11
+
8
12
  def evaluate(context = nil)
9
- children[0].evaluate(context).and(children[1].evaluate(context))
13
+ short_circuit_do(:evaluate, context)
10
14
  end
11
15
 
12
- def blank_value
13
- true
16
+ def simplify(context = nil)
17
+ short_circuit_do(:simplify, context)
14
18
  end
15
19
 
16
20
  def value(context = nil)
17
21
  context ||= Context.new
18
22
  children[0].value(context) && children[1].value(context)
19
23
  end
24
+
25
+ private
26
+
27
+ def short_circuit_do(method, context)
28
+ context ||= Context.new
29
+ lhs = children[0].send(method, context)
30
+ case lhs
31
+ when AST::Boolean
32
+ lhs.false? ? AST::Boolean.new(false) : children[1].send(method, context)
33
+ else
34
+ lhs.and(children[1].send(method, context))
35
+ end
36
+ end
20
37
  end
21
38
  end
22
39
  end
@@ -5,13 +5,14 @@ module Keisan
5
5
  :"=="
6
6
  end
7
7
 
8
- def evaluate(context = nil)
9
- children[0].evaluate(context).equal(children[1].evaluate(context))
8
+ protected
9
+
10
+ def value_operator
11
+ :==
10
12
  end
11
13
 
12
- def value(context=nil)
13
- context ||= Context.new
14
- children[0].value(context) == children[1].value(context)
14
+ def operator
15
+ :equal
15
16
  end
16
17
  end
17
18
  end
@@ -5,12 +5,14 @@ module Keisan
5
5
  :">"
6
6
  end
7
7
 
8
- def evaluate(context = nil)
9
- children[0].evaluate(context) > children[1].evaluate(context)
8
+ protected
9
+
10
+ def value_operator
11
+ :>
10
12
  end
11
13
 
12
- def value(context = nil)
13
- children.first.value(context) > children.last.value(context)
14
+ def operator
15
+ :>
14
16
  end
15
17
  end
16
18
  end
@@ -5,12 +5,14 @@ module Keisan
5
5
  :">="
6
6
  end
7
7
 
8
- def evaluate(context = nil)
9
- children[0].evaluate(context) >= children[1].evaluate(context)
8
+ protected
9
+
10
+ def value_operator
11
+ :>=
10
12
  end
11
13
 
12
- def value(context = nil)
13
- children.first.value(context) >= children.last.value(context)
14
+ def operator
15
+ :>=
14
16
  end
15
17
  end
16
18
  end
@@ -5,12 +5,14 @@ module Keisan
5
5
  :"<"
6
6
  end
7
7
 
8
- def evaluate(context = nil)
9
- children[0].evaluate(context) < children[1].evaluate(context)
8
+ protected
9
+
10
+ def value_operator
11
+ :<
10
12
  end
11
13
 
12
- def value(context = nil)
13
- children.first.value(context) < children.last.value(context)
14
+ def operator
15
+ :<
14
16
  end
15
17
  end
16
18
  end
@@ -5,12 +5,14 @@ module Keisan
5
5
  :"<="
6
6
  end
7
7
 
8
- def evaluate(context = nil)
9
- children[0].evaluate(context) <= children[1].evaluate(context)
8
+ protected
9
+
10
+ def value_operator
11
+ :<=
10
12
  end
11
13
 
12
- def value(context = nil)
13
- children.first.value(context) <= children.last.value(context)
14
+ def operator
15
+ :<=
14
16
  end
15
17
  end
16
18
  end
@@ -5,13 +5,14 @@ module Keisan
5
5
  :"!="
6
6
  end
7
7
 
8
- def evaluate(context = nil)
9
- children[0].evaluate(context).not_equal(children[1].evaluate(context))
8
+ protected
9
+
10
+ def value_operator
11
+ :!=
10
12
  end
11
13
 
12
- def value(context=nil)
13
- context ||= Context.new
14
- children[0].value(context) != children[1].value(context)
14
+ def operator
15
+ :not_equal
15
16
  end
16
17
  end
17
18
  end
@@ -1,6 +1,30 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class LogicalOperator < Operator
4
+ def evaluate(context = nil)
5
+ context ||= Context.new
6
+ children[0].evaluate(context).send(operator, children[1].evaluate(context))
7
+ end
8
+
9
+ def simplify(context = nil)
10
+ context ||= Context.new
11
+ children[0].simplify(context).send(operator, children[1].simplify(context))
12
+ end
13
+
14
+ def value(context=nil)
15
+ context ||= Context.new
16
+ children[0].value(context).send(value_operator, children[1].value(context))
17
+ end
18
+
19
+ protected
20
+
21
+ def value_operator
22
+ raise Exceptions::NotImplementedError.new
23
+ end
24
+
25
+ def operator
26
+ raise Exceptions::NotImplementedError.new
27
+ end
4
28
  end
5
29
  end
6
30
  end
@@ -10,13 +10,30 @@ module Keisan
10
10
  end
11
11
 
12
12
  def evaluate(context = nil)
13
- children[0].evaluate(context).or(children[1].evaluate(context))
13
+ short_circuit_do(:evaluate, context)
14
+ end
15
+
16
+ def simplify(context = nil)
17
+ short_circuit_do(:simplify, context)
14
18
  end
15
19
 
16
20
  def value(context = nil)
17
21
  context ||= Context.new
18
22
  children[0].value(context) || children[1].value(context)
19
23
  end
24
+
25
+ private
26
+
27
+ def short_circuit_do(method, context)
28
+ context ||= Context.new
29
+ lhs = children[0].send(method, context)
30
+ case lhs
31
+ when AST::Boolean
32
+ lhs.true? ? AST::Boolean.new(true) : children[1].send(method, context)
33
+ else
34
+ lhs.or(children[1].send(method, context))
35
+ end
36
+ end
20
37
  end
21
38
  end
22
39
  end
@@ -24,6 +24,10 @@ module Keisan
24
24
  case other
25
25
  when Number
26
26
  Number.new(value + other.value)
27
+ when Date
28
+ Date.new(other.value + value)
29
+ when Time
30
+ Time.new(other.value + value)
27
31
  else
28
32
  super
29
33
  end
@@ -41,7 +41,7 @@ module Keisan
41
41
  raise Exceptions::ASTError.new("Mismatch of children and operators")
42
42
  end
43
43
 
44
- children = Array.wrap(children)
44
+ children = Array(children)
45
45
  super(children)
46
46
 
47
47
  @parsing_operators = parsing_operators
@@ -4,7 +4,7 @@ module Keisan
4
4
  attr_reader :children
5
5
 
6
6
  def initialize(children = [])
7
- children = Array.wrap(children).map do |child|
7
+ children = Array(children).map do |child|
8
8
  child.is_a?(Cell) ? child : child.to_node
9
9
  end
10
10
  raise Exceptions::InternalError.new unless children.is_a?(Array)
@@ -22,6 +22,10 @@ module Keisan
22
22
  # Special case of array concatenation
23
23
  elsif children_values.all? {|child| child.is_a?(::Array)}
24
24
  children_values.inject([], &:+)
25
+ elsif children_values.one? {|child| child.is_a?(::Date)}
26
+ date_time_plus(children_values, ::Date)
27
+ elsif children_values.one? {|child| child.is_a?(::Time)}
28
+ date_time_plus(children_values, ::Time)
25
29
  else
26
30
  children_values.inject(0, &:+)
27
31
  end.to_node.value(context)
@@ -77,6 +81,12 @@ module Keisan
77
81
 
78
82
  private
79
83
 
84
+ def date_time_plus(elements, klass)
85
+ date_time = elements.select {|child| child.is_a?(klass)}.first
86
+ others = elements.select {|child| !child.is_a?(klass)}
87
+ date_time + others.inject(0, &:+)
88
+ end
89
+
80
90
  def convert_minus_to_plus!
81
91
  @parsing_operators.each.with_index do |parsing_operator, index|
82
92
  if parsing_operator.is_a?(Parsing::Minus)
@@ -0,0 +1,23 @@
1
+ require_relative "date_time_methods"
2
+
3
+ module Keisan
4
+ module AST
5
+ class Time < ConstantLiteral
6
+ include DateTimeMethods
7
+
8
+ attr_reader :time
9
+
10
+ def initialize(time)
11
+ @time = time
12
+ end
13
+
14
+ def value(context = nil)
15
+ time
16
+ end
17
+
18
+ def to_s
19
+ value.strftime("%Y-%m-%d %H:%M:%S")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,7 +2,7 @@ module Keisan
2
2
  module AST
3
3
  class UnaryOperator < Operator
4
4
  def initialize(children = [])
5
- children = Array.wrap(children)
5
+ children = Array(children)
6
6
  super(children)
7
7
  if children.count != 1
8
8
  raise Exceptions::ASTError.new("Unary operator takes has a single child")
@@ -60,11 +60,8 @@ module Keisan
60
60
  end
61
61
 
62
62
  def replace(variable, replacement)
63
- if name == variable.name
64
- replacement
65
- else
66
- self
67
- end
63
+ to_replace_name = variable.is_a?(::String) ? variable : variable.name
64
+ name == to_replace_name ? replacement : self
68
65
  end
69
66
 
70
67
  def differentiate(variable, context = nil)
@@ -4,8 +4,8 @@ module Keisan
4
4
 
5
5
  def initialize(parent: nil, random: nil, allow_recursive: false, shadowed: [])
6
6
  @parent = parent
7
- @function_registry = Functions::Registry.new(parent: @parent.try(:function_registry))
8
- @variable_registry = Variables::Registry.new(parent: @parent.try(:variable_registry), shadowed: shadowed)
7
+ @function_registry = Functions::Registry.new(parent: @parent&.function_registry)
8
+ @variable_registry = Variables::Registry.new(parent: @parent&.variable_registry, shadowed: shadowed)
9
9
  @random = random
10
10
  @allow_recursive = allow_recursive
11
11
  end
@@ -47,7 +47,7 @@ module Keisan
47
47
 
48
48
  def transient_definitions
49
49
  return {} unless @transient
50
- parent_definitions = @parent.present? ? @parent.transient_definitions : {}
50
+ parent_definitions = @parent.nil? ? {} : @parent.transient_definitions
51
51
  parent_definitions.merge(
52
52
  @variable_registry.locals
53
53
  ).merge(
@@ -73,7 +73,7 @@ module Keisan
73
73
 
74
74
  def register_variable!(name, value, local: false)
75
75
  if !@variable_registry.shadowed.member?(name) && (transient? || !local && @parent&.variable_is_modifiable?(name))
76
- @parent.register_variable!(name, value)
76
+ @parent.register_variable!(name, value, local: local)
77
77
  else
78
78
  @variable_registry.register!(name, value)
79
79
  end
@@ -93,14 +93,14 @@ module Keisan
93
93
 
94
94
  def register_function!(name, function, local: false)
95
95
  if transient? || !local && @parent&.function_is_modifiable?(name)
96
- @parent.register_function!(name, function)
96
+ @parent.register_function!(name, function, local: local)
97
97
  else
98
98
  @function_registry.register!(name.to_s, function)
99
99
  end
100
100
  end
101
101
 
102
102
  def random
103
- @random || @parent.try(:random) || Random.new
103
+ @random || @parent&.random || Random.new
104
104
  end
105
105
 
106
106
  protected
@@ -18,5 +18,8 @@ module Keisan
18
18
  class InvalidExpression < StandardError; end
19
19
  class TypeError < StandardError; end
20
20
  class NonDifferentiableError < StandardError; end
21
+
22
+ class BreakError < StandardError; end
23
+ class ContinueError < StandardError; end
21
24
  end
22
25
  end
@@ -23,6 +23,12 @@ module Keisan
23
23
  raise Exceptions::NotImplementedError.new
24
24
  end
25
25
 
26
+ def unbound_variables(children, context)
27
+ children.inject(Set.new) do |vars, child|
28
+ vars | child.unbound_variables(context)
29
+ end
30
+ end
31
+
26
32
  protected
27
33
 
28
34
  def validate_arguments!(count)
@@ -0,0 +1,11 @@
1
+ require_relative "loop_control_flow_function"
2
+
3
+ module Keisan
4
+ module Functions
5
+ class Break < LoopControlFlowFuntion
6
+ def initialize
7
+ super("break", Exceptions::BreakError)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "loop_control_flow_function"
2
+
3
+ module Keisan
4
+ module Functions
5
+ class Continue < LoopControlFlowFuntion
6
+ def initialize
7
+ super("continue", Exceptions::ContinueError)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,7 @@
1
1
  require_relative "let"
2
2
  require_relative "puts"
3
+ require_relative "break"
4
+ require_relative "continue"
3
5
 
4
6
  require_relative "if"
5
7
  require_relative "while"
@@ -49,6 +51,8 @@ module Keisan
49
51
  def self.register_defaults!(registry)
50
52
  registry.register!(:let, Let.new, force: true)
51
53
  registry.register!(:puts, Puts.new, force: true)
54
+ registry.register!(:break, Break.new, force: true)
55
+ registry.register!(:continue, Continue.new, force: true)
52
56
 
53
57
  registry.register!(:if, If.new, force: true)
54
58
  registry.register!(:while, While.new, force: true)
@@ -65,6 +69,7 @@ module Keisan
65
69
  register_math!(registry)
66
70
  register_array_methods!(registry)
67
71
  register_random_methods!(registry)
72
+ register_date_time_methods!(registry)
68
73
  end
69
74
 
70
75
  def self.register_math!(registry)
@@ -124,6 +129,37 @@ module Keisan
124
129
  registry.register!(:rand, Rand.new, force: true)
125
130
  registry.register!(:sample, Sample.new, force: true)
126
131
  end
132
+
133
+ def self.register_date_time_methods!(registry)
134
+ register_date_time!(registry)
135
+
136
+ registry.register!(:today, Proc.new { ::Date.today }, force: true)
137
+ registry.register!(:day, Proc.new {|d| d.mday }, force: true)
138
+ registry.register!(:weekday, Proc.new {|d| d.wday }, force: true)
139
+ registry.register!(:month, Proc.new {|d| d.month }, force: true)
140
+ registry.register!(:year, Proc.new {|d| d.year }, force: true)
141
+
142
+ registry.register!(:now, Proc.new { ::Time.now }, force: true)
143
+ registry.register!(:hour, Proc.new {|t| t.hour }, force: true)
144
+ registry.register!(:minute, Proc.new {|t| t.min }, force: true)
145
+ registry.register!(:second, Proc.new {|t| t.sec }, force: true)
146
+ registry.register!(:strftime, Proc.new {|*args| args.first.strftime(*args[1..-1]) }, force: true)
147
+
148
+ registry.register!(:to_time, Proc.new {|d| d.to_time }, force: true)
149
+ registry.register!(:to_date, Proc.new {|t| t.to_date }, force: true)
150
+ end
151
+
152
+ def self.register_date_time!(registry)
153
+ [::Date, ::Time].each do |klass|
154
+ registry.register!(klass.to_s.downcase.to_sym, Proc.new {|*args|
155
+ if args.count == 1 && args.first.is_a?(::String)
156
+ AST.const_get(klass.to_s).new(klass.parse(args.first))
157
+ else
158
+ AST.const_get(klass.to_s).new(klass.new(*args))
159
+ end
160
+ }, force: true)
161
+ end
162
+ end
127
163
  end
128
164
  end
129
165
  end
@@ -12,6 +12,10 @@ module Keisan
12
12
  evaluate(ast_function, context)
13
13
  end
14
14
 
15
+ def unbound_variables(children, context)
16
+ super - Set.new(shadowing_variable_names(children).map(&:name))
17
+ end
18
+
15
19
  def evaluate(ast_function, context = nil)
16
20
  validate_arguments!(ast_function.children.count)
17
21
  context ||= Context.new
@@ -20,9 +24,9 @@ module Keisan
20
24
 
21
25
  case operand
22
26
  when AST::List
23
- evaluate_list(operand, arguments, expression, context)
27
+ evaluate_list(operand, arguments, expression, context).evaluate(context)
24
28
  when AST::Hash
25
- evaluate_hash(operand, arguments, expression, context)
29
+ evaluate_hash(operand, arguments, expression, context).evaluate(context)
26
30
  else
27
31
  raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{operand}")
28
32
  end
@@ -34,6 +38,10 @@ module Keisan
34
38
 
35
39
  protected
36
40
 
41
+ def shadowing_variable_names(children)
42
+ raise Exceptions::NotImplementedError.new
43
+ end
44
+
37
45
  def verify_arguments!(arguments)
38
46
  unless arguments.all? {|argument| argument.is_a?(AST::Variable)}
39
47
  raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
@@ -10,6 +10,12 @@ module Keisan
10
10
  super("filter")
11
11
  end
12
12
 
13
+ protected
14
+
15
+ def shadowing_variable_names(children)
16
+ children.size == 3 ? children[1..1] : children[1..2]
17
+ end
18
+
13
19
  private
14
20
 
15
21
  def evaluate_list(list, arguments, expression, context)
@@ -0,0 +1,22 @@
1
+ module Keisan
2
+ module Functions
3
+ class LoopControlFlowFuntion < Function
4
+ def initialize(name, exception_class)
5
+ super(name, 0)
6
+ @exception_class = exception_class
7
+ end
8
+
9
+ def value(ast_function, context = nil)
10
+ raise @exception_class.new
11
+ end
12
+
13
+ def evaluate(ast_function, context = nil)
14
+ raise @exception_class.new
15
+ end
16
+
17
+ def simplify(ast_function, context = nil)
18
+ raise @exception_class.new
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,6 +10,12 @@ module Keisan
10
10
  super("map")
11
11
  end
12
12
 
13
+ protected
14
+
15
+ def shadowing_variable_names(children)
16
+ children.size == 3 ? children[1..1] : children[1..2]
17
+ end
18
+
13
19
  private
14
20
 
15
21
  def evaluate_list(list, arguments, expression, context)
@@ -6,12 +6,17 @@ module Keisan
6
6
  # Reduces (list, initial, accumulator, variable, expression)
7
7
  # e.g. reduce([1,2,3,4], 0, total, x, total+x)
8
8
  # should give 10
9
+ # When hash: (hash, initial, accumulator, key, value, expression)
9
10
  def initialize
10
11
  super("reduce")
11
12
  end
12
13
 
13
14
  protected
14
15
 
16
+ def shadowing_variable_names(children)
17
+ children.size == 5 ? children[2..3] : children[2..4]
18
+ end
19
+
15
20
  def verify_arguments!(arguments)
16
21
  unless arguments[1..-1].all? {|argument| argument.is_a?(AST::Variable)}
17
22
  raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
@@ -28,7 +28,13 @@ module Keisan
28
28
  current = Keisan::AST::Null.new
29
29
 
30
30
  while logical_node_evaluates_to_true(logical_node, context)
31
- current = body_node.evaluated(context)
31
+ begin
32
+ current = body_node.evaluated(context)
33
+ rescue Exceptions::BreakError
34
+ break
35
+ rescue Exceptions::ContinueError
36
+ next
37
+ end
32
38
  end
33
39
 
34
40
  current
data/lib/keisan/parser.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Keisan
2
2
  class Parser
3
- KEYWORDS = %w(let puts).freeze
3
+ KEYWORDS = %w(let puts break continue).freeze
4
4
 
5
5
  attr_reader :tokens, :components
6
6
 
@@ -39,7 +39,7 @@ module Keisan
39
39
  end
40
40
 
41
41
  def parse_multi_line!
42
- line_parsers = @tokens.split {|token| token.is_a?(Tokens::LineSeparator)}.map {|tokens| self.class.new(tokens: tokens)}
42
+ line_parsers = Util.array_split(@tokens) {|token| token.is_a?(Tokens::LineSeparator)}.map {|tokens| self.class.new(tokens: tokens)}
43
43
  @components = []
44
44
  line_parsers.each.with_index do |line_parser, i|
45
45
  @components += line_parser.components
@@ -52,7 +52,7 @@ module Keisan
52
52
  def parse_keyword!
53
53
  keyword = tokens.first.string
54
54
  arguments = if tokens[1].is_a?(Tokens::Group)
55
- tokens[1].sub_tokens.split {|token| token.is_a?(Tokens::Comma)}.map {|argument_tokens|
55
+ Util.array_split(tokens[1].sub_tokens) {|token| token.is_a?(Tokens::Comma)}.map {|argument_tokens|
56
56
  Parsing::Argument.new(argument_tokens)
57
57
  }
58
58
  else
@@ -212,7 +212,7 @@ module Keisan
212
212
  @components << Parsing::List.new(arguments_from_group(token))
213
213
  when :curly
214
214
  if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)}
215
- @components << Parsing::Hash.new(token.sub_tokens.split {|token| token.is_a?(Tokens::Comma)})
215
+ @components << Parsing::Hash.new(Util.array_split(token.sub_tokens) {|token| token.is_a?(Tokens::Comma)})
216
216
  else
217
217
  @components << Parsing::CurlyGroup.new(token.sub_tokens)
218
218
  end
@@ -280,7 +280,7 @@ module Keisan
280
280
  if token.sub_tokens.empty?
281
281
  []
282
282
  else
283
- token.sub_tokens.split {|sub_token| sub_token.is_a?(Tokens::Comma)}.map do |sub_tokens|
283
+ Util.array_split(token.sub_tokens) {|sub_token| sub_token.is_a?(Tokens::Comma)}.map do |sub_tokens|
284
284
  Parsing::Argument.new(sub_tokens)
285
285
  end
286
286
  end
@@ -5,7 +5,7 @@ module Keisan
5
5
 
6
6
  def initialize(name, arguments)
7
7
  @name = name
8
- @arguments = Array.wrap(arguments)
8
+ @arguments = Array(arguments)
9
9
  end
10
10
  end
11
11
  end
@@ -4,7 +4,7 @@ module Keisan
4
4
  attr_reader :key_value_pairs
5
5
 
6
6
  def initialize(key_value_pairs)
7
- @key_value_pairs = Array.wrap(key_value_pairs).map {|key_value_pair|
7
+ @key_value_pairs = Array(key_value_pairs).map {|key_value_pair|
8
8
  validate_and_extract_key_value_pair(key_value_pair)
9
9
  }
10
10
  end
@@ -12,7 +12,7 @@ module Keisan
12
12
  private
13
13
 
14
14
  def validate_and_extract_key_value_pair(key_value_pair)
15
- key, value = key_value_pair.split {|token| token.is_a?(Tokens::Colon)}
15
+ key, value = Util.array_split(key_value_pair) {|token| token.is_a?(Tokens::Colon)}
16
16
  raise Exceptions::ParseError.new("Invalid hash") unless key.size == 1 && value.size >= 1
17
17
 
18
18
  key = key.first
data/lib/keisan/token.rb CHANGED
@@ -12,7 +12,7 @@ module Keisan
12
12
  end
13
13
 
14
14
  def self.type
15
- @type ||= self.to_s.split("::").last.underscore.to_sym
15
+ @type ||= Util.underscore(self.to_s.split("::").last).to_sym
16
16
  end
17
17
 
18
18
  def regex
@@ -0,0 +1,19 @@
1
+ module Keisan
2
+ class Util
3
+ def self.underscore(str)
4
+ str.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').downcase
5
+ end
6
+
7
+ def self.array_split(array, &block)
8
+ array.inject([[]]) do |results, element|
9
+ if yield(element)
10
+ results << []
11
+ else
12
+ results.last << element
13
+ end
14
+
15
+ results
16
+ end
17
+ end
18
+ end
19
+ end
@@ -4,7 +4,8 @@ module Keisan
4
4
  VARIABLES = {
5
5
  "PI" => Math::PI,
6
6
  "E" => Math::E,
7
- "I" => Complex(0,1)
7
+ "I" => Complex(0,1),
8
+ "INF" => Float::INFINITY
8
9
  }
9
10
 
10
11
  def self.registry
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.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: 2018-01-29 00:00:00.000000000 Z
11
+ date: 2018-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activesupport
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 4.2.2
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: 4.2.2
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: coveralls
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -140,6 +126,8 @@ files:
140
126
  - lib/keisan/ast/cell.rb
141
127
  - lib/keisan/ast/cell_assignment.rb
142
128
  - lib/keisan/ast/constant_literal.rb
129
+ - lib/keisan/ast/date.rb
130
+ - lib/keisan/ast/date_time_methods.rb
143
131
  - lib/keisan/ast/exponent.rb
144
132
  - lib/keisan/ast/function.rb
145
133
  - lib/keisan/ast/function_assignment.rb
@@ -166,6 +154,7 @@ files:
166
154
  - lib/keisan/ast/parent.rb
167
155
  - lib/keisan/ast/plus.rb
168
156
  - lib/keisan/ast/string.rb
157
+ - lib/keisan/ast/time.rb
169
158
  - lib/keisan/ast/times.rb
170
159
  - lib/keisan/ast/unary_bitwise_not.rb
171
160
  - lib/keisan/ast/unary_identity.rb
@@ -182,8 +171,10 @@ files:
182
171
  - lib/keisan/exceptions.rb
183
172
  - lib/keisan/function.rb
184
173
  - lib/keisan/functions/abs.rb
174
+ - lib/keisan/functions/break.rb
185
175
  - lib/keisan/functions/cbrt.rb
186
176
  - lib/keisan/functions/cmath_function.rb
177
+ - lib/keisan/functions/continue.rb
187
178
  - lib/keisan/functions/cos.rb
188
179
  - lib/keisan/functions/cosh.rb
189
180
  - lib/keisan/functions/cot.rb
@@ -201,6 +192,7 @@ files:
201
192
  - lib/keisan/functions/imag.rb
202
193
  - lib/keisan/functions/let.rb
203
194
  - lib/keisan/functions/log.rb
195
+ - lib/keisan/functions/loop_control_flow_function.rb
204
196
  - lib/keisan/functions/map.rb
205
197
  - lib/keisan/functions/math_function.rb
206
198
  - lib/keisan/functions/proc_function.rb
@@ -292,6 +284,7 @@ files:
292
284
  - lib/keisan/tokens/string.rb
293
285
  - lib/keisan/tokens/unknown.rb
294
286
  - lib/keisan/tokens/word.rb
287
+ - lib/keisan/util.rb
295
288
  - lib/keisan/variables/default_registry.rb
296
289
  - lib/keisan/variables/registry.rb
297
290
  - lib/keisan/version.rb
@@ -308,7 +301,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
308
301
  requirements:
309
302
  - - ">="
310
303
  - !ruby/object:Gem::Version
311
- version: 2.0.0
304
+ version: 2.3.0
312
305
  required_rubygems_version: !ruby/object:Gem::Requirement
313
306
  requirements:
314
307
  - - ">="
@@ -316,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
316
309
  version: '0'
317
310
  requirements: []
318
311
  rubyforge_project:
319
- rubygems_version: 2.6.14
312
+ rubygems_version: 2.7.7
320
313
  signing_key:
321
314
  specification_version: 4
322
315
  summary: An equation parser and evaluator