keisan 0.7.0 → 0.8.0

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