keisan 0.9.0 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c77b933e609cf2ed878939973a183082fa01b87ac49a8bece5278c0df5e66aed
4
- data.tar.gz: e737e883ba1fadfa8c9c9ab19f6b995e4cc33ab4f5f2f7f7750bcd0091dd9cfc
3
+ metadata.gz: e9daac57399adbc7f0028c078c8ec1a74e6a5b7222f2473a871d2ca7ae46c1e4
4
+ data.tar.gz: 2ffd26dec791b8a88e1edce5aeaf331622ccbb2403b5f17185d3aad36065c173
5
5
  SHA512:
6
- metadata.gz: 1cbf2ba0b83b1c74b6d70530dbdebe5a6519cff81c90a2e79216fff1ced7d7ded853d88691afbcdcb75b3995349d82c7b02b0cda915cb871c545080b37c25b7f
7
- data.tar.gz: 7877b72629fe6f90c7d508451dc580f88c8f10b9d14529917fb9235c805d84c55d972188dadd85ffb6acef6a81e728911e60a0012968a66408bb8a600cfaca45
6
+ metadata.gz: 90b892b2b7602af3406bdbd84b18d211e790afcf185268f3d84800d8fe390d099870053e39f39d2c46e82da0ba7b185597c1d462af359a1fdb7a0f3aad07c80a
7
+ data.tar.gz: 29175b77265433e8e619a098e7e862472c7575bc2de91b38ed8e853355e3a133da5a33f6e43a321295cbff0a900e41502885c1fc4dbe649431fadc1baa767469
data/README.md CHANGED
@@ -280,6 +280,14 @@ calculator.evaluate("range(5,10)")
280
280
  #=> [5,6,7,8,9]
281
281
  calculator.evaluate("range(0,10,2)")
282
282
  #=> [0,2,4,6,8]
283
+ calculator.evaluate("[1, 2, 2, 3].uniq")
284
+ #=> [1,2,3]
285
+ calculator.evaluate("[1, 2, 3].difference([2, 3, 4])")
286
+ #=> [1]
287
+ calculator.evaluate("[1, 2, 3].intersection([2, 3, 4])")
288
+ #=> [2, 3]
289
+ calculator.evaluate("[1, 2, 3].union([2, 3, 4])")
290
+ #=> [1, 2, 3, 4]
283
291
  ```
284
292
 
285
293
  ##### Hashes
@@ -49,11 +49,13 @@ module Keisan
49
49
  end
50
50
 
51
51
  def unbound_variables(context = nil)
52
- variables = super(context)
52
+ context ||= Context.new
53
+
53
54
  if is_variable_definition?
54
- variables.delete(children.first.name)
55
+ variable_assignment_unbound_variables(context)
55
56
  else
56
- variables
57
+ # TODO: Should update to handle function / list assignment.
58
+ super(context)
57
59
  end
58
60
  end
59
61
 
@@ -70,6 +72,10 @@ module Keisan
70
72
  children.first.is_a?(Variable)
71
73
  end
72
74
 
75
+ def variable_name
76
+ children.first.name
77
+ end
78
+
73
79
  def is_function_definition?
74
80
  children.first.is_a?(Function)
75
81
  end
@@ -78,6 +84,10 @@ module Keisan
78
84
  children.first.is_a?(List)
79
85
  end
80
86
 
87
+ def rhs_unbound_variables(context = nil)
88
+ children.last.unbound_variables(context)
89
+ end
90
+
81
91
  private
82
92
 
83
93
  def evaluate_variable_assignment(context, lhs, rhs)
@@ -96,6 +106,16 @@ module Keisan
96
106
  def evaluate_cell_assignment(context, lhs, rhs)
97
107
  CellAssignment.new(self, context, lhs, rhs).evaluate
98
108
  end
109
+
110
+ def variable_assignment_unbound_variables(context)
111
+ rhs = rhs_unbound_variables(context)
112
+ # If the right-side is fully defined, then this is a valid assignment.
113
+ if rhs.empty?
114
+ Set.new
115
+ else
116
+ rhs | Set.new([variable_name])
117
+ end
118
+ end
99
119
  end
100
120
  end
101
121
  end
@@ -8,6 +8,19 @@ module Keisan
8
8
  @indexes = indexes
9
9
  end
10
10
 
11
+ def deep_dup
12
+ dupped = super
13
+ dupped.instance_variable_set(
14
+ :@indexes, indexes.map(&:deep_dup)
15
+ )
16
+ dupped
17
+ end
18
+
19
+ def freeze
20
+ indexes.each(&:freeze)
21
+ super
22
+ end
23
+
11
24
  def value(context = nil)
12
25
  return child.value(context).send(:[], *indexes.map {|index| index.value(context)})
13
26
  end
@@ -20,9 +20,49 @@ module Keisan
20
20
  evaluate(context)
21
21
  end
22
22
 
23
+ def unbound_variables(context = nil)
24
+ context ||= Context.new
25
+ defined_variables = Set.new
26
+
27
+ children.inject(Set.new) do |unbound_vars, child|
28
+ if child.is_a?(Assignment) && child.is_variable_definition?
29
+ line_unbound_vars = unbound_variables_for_line_variable_assignment(context, child, unbound_vars, defined_variables)
30
+ if line_unbound_vars.empty?
31
+ defined_variables.add(child.variable_name)
32
+ end
33
+ unbound_vars | line_unbound_vars
34
+ else
35
+ unbound_vars | (child.unbound_variables(context) - defined_variables)
36
+ end
37
+ end
38
+ end
39
+
23
40
  def to_s
24
41
  children.map(&:to_s).join(";")
25
42
  end
43
+
44
+ private
45
+
46
+ def unbound_variables_for_line_variable_assignment(context, line, unbound_vars, defined_variables)
47
+ child_unbound_variables = variable_assignment_unbound_variables(context, line, defined_variables)
48
+ if child_unbound_variables.empty?
49
+ defined_variables.add(line.variable_name)
50
+ unbound_vars
51
+ else
52
+ unbound_vars | child_unbound_variables
53
+ end
54
+ end
55
+
56
+ def variable_assignment_unbound_variables(context, assignment, defined_variables)
57
+ rhs_child_unbound_variables = assignment.rhs_unbound_variables(context) - defined_variables
58
+
59
+ # If there are no unbound variables, this is a properly bound assignment.
60
+ if rhs_child_unbound_variables.empty?
61
+ Set.new
62
+ else
63
+ Set.new([assignment.variable_name]) | rhs_child_unbound_variables
64
+ end
65
+ end
26
66
  end
27
67
  end
28
68
  end
@@ -47,10 +47,18 @@ module Keisan
47
47
  Evaluator.new(self, cache: @cache).evaluate(expression, definitions)
48
48
  end
49
49
 
50
+ def evaluate_ast(ast, definitions = {})
51
+ Evaluator.new(self, cache: @cache).evaluate_ast(ast, definitions: definitions)
52
+ end
53
+
50
54
  def simplify(expression, definitions = {})
51
55
  Evaluator.new(self, cache: @cache).simplify(expression, definitions)
52
56
  end
53
57
 
58
+ def simplify_ast(ast, definitions = {})
59
+ Evaluator.new(self, cache: @cache).simplify_ast(ast, definitions: definitions)
60
+ end
61
+
54
62
  def ast(expression)
55
63
  Evaluator.new(self, cache: @cache).parse_ast(expression)
56
64
  end
@@ -9,7 +9,11 @@ module Keisan
9
9
 
10
10
  def evaluate(expression, definitions = {})
11
11
  context = calculator.context.spawn_child(definitions: definitions, transient: true)
12
- ast = parse_ast(expression)
12
+ evaluate_ast(parse_ast(expression), context: context)
13
+ end
14
+
15
+ def evaluate_ast(ast, definitions: {}, context: nil)
16
+ context ||= calculator.context.spawn_child(definitions: definitions, transient: true)
13
17
  last_line = last_line(ast)
14
18
 
15
19
  evaluation = ast.evaluated(context)
@@ -25,7 +29,11 @@ module Keisan
25
29
 
26
30
  def simplify(expression, definitions = {})
27
31
  context = calculator.context.spawn_child(definitions: definitions, transient: true)
28
- ast = parse_ast(expression)
32
+ simplify_ast(parse_ast(expression), context: context)
33
+ end
34
+
35
+ def simplify_ast(ast, definitions: {}, context: nil)
36
+ context ||= calculator.context.spawn_child(definitions: definitions, transient: true)
29
37
  ast.simplified(context)
30
38
  end
31
39
 
@@ -118,10 +118,13 @@ module Keisan
118
118
  end
119
119
 
120
120
  def self.register_array_methods!(registry)
121
- %i(min max size flatten reverse).each do |method|
121
+ %i(min max size flatten reverse uniq).each do |method|
122
122
  registry.register!(method, Proc.new {|a| a.send(method)}, force: true)
123
123
  end
124
124
 
125
+ registry.register!(:difference, Proc.new {|a, b| a - b}, force: true)
126
+ registry.register!(:intersection, Proc.new {|a, b| a & b}, force: true)
127
+ registry.register!(:union, Proc.new {|a, b| a | b}, force: true)
125
128
  registry.register!("range", Functions::Range.new, force: true)
126
129
  end
127
130
 
@@ -54,7 +54,7 @@ module Keisan
54
54
  private
55
55
 
56
56
  def operand_arguments_expression_for(ast_function, context)
57
- operand = ast_function.children[0].simplify(context)
57
+ operand = ast_function.children[0].evaluate(context)
58
58
  arguments = ast_function.children[1...-1]
59
59
  expression = ast_function.children[-1]
60
60
 
@@ -14,6 +14,12 @@ module Keisan
14
14
  @transient_definitions = transient_definitions
15
15
  end
16
16
 
17
+ def freeze
18
+ @arguments.freeze
19
+ @expression.freeze
20
+ super
21
+ end
22
+
17
23
  def call(context, *args)
18
24
  validate_arguments!(args.count)
19
25
 
@@ -96,7 +102,7 @@ module Keisan
96
102
  private
97
103
 
98
104
  def argument_variables
99
- @argument_variables ||= arguments.map {|argument| AST::Variable.new(argument)}
105
+ arguments.map {|argument| AST::Variable.new(argument)}
100
106
  end
101
107
 
102
108
  def calculate_partial_derivatives(context)
@@ -44,6 +44,9 @@ module Keisan
44
44
  when Proc
45
45
  self[name] = ProcFunction.new(name, function)
46
46
  when Function
47
+ # The expression AST which represents the function should be constant,
48
+ # so we freeze it so it will always have the same behavior.
49
+ function.freeze if function.is_a?(ExpressionFunction)
47
50
  self[name] = function
48
51
  else
49
52
  raise Exceptions::InvalidFunctionError.new
data/lib/keisan/parser.rb CHANGED
@@ -211,7 +211,8 @@ module Keisan
211
211
  when :square
212
212
  @components << Parsing::List.new(arguments_from_group(token))
213
213
  when :curly
214
- if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)}
214
+ # A hash either has a colon, or is empty
215
+ if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)} || token.sub_tokens.empty?
215
216
  @components << Parsing::Hash.new(Util.array_split(token.sub_tokens) {|token| token.is_a?(Tokens::Comma)})
216
217
  else
217
218
  @components << Parsing::CurlyGroup.new(token.sub_tokens)
@@ -4,15 +4,25 @@ module Keisan
4
4
  attr_reader :key_value_pairs
5
5
 
6
6
  def initialize(key_value_pairs)
7
- @key_value_pairs = Array(key_value_pairs).map {|key_value_pair|
8
- validate_and_extract_key_value_pair(key_value_pair)
9
- }
7
+ key_value_pairs = Array(key_value_pairs)
8
+ if key_value_pairs.size == 1 && key_value_pairs.first.empty?
9
+ @key_value_pairs = []
10
+ else
11
+ @key_value_pairs = key_value_pairs.map {|key_value_pair|
12
+ validate_and_extract_key_value_pair(key_value_pair)
13
+ }
14
+ end
10
15
  end
11
16
 
12
17
  private
13
18
 
14
19
  def validate_and_extract_key_value_pair(key_value_pair)
15
- key, value = Util.array_split(key_value_pair) {|token| token.is_a?(Tokens::Colon)}
20
+ filtered_key_value_pair = key_value_pair.select {|token|
21
+ !token.is_a?(Tokens::LineSeparator)
22
+ }
23
+ key, value = Util.array_split(filtered_key_value_pair) {|token|
24
+ token.is_a?(Tokens::Colon)
25
+ }
16
26
  raise Exceptions::ParseError.new("Invalid hash") unless key.size == 1 && value.size >= 1
17
27
 
18
28
  key = key.first
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.9.0"
2
+ VERSION = "0.9.2"
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.9.0
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-26 00:00:00.000000000 Z
11
+ date: 2024-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmath