dmn 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -10
- data/lib/{spot_feel → dmn}/configuration.rb +1 -1
- data/lib/dmn/decision.rb +48 -0
- data/lib/dmn/decision_table.rb +51 -0
- data/lib/dmn/definitions.rb +66 -0
- data/lib/{spot_feel/spot_feel.treetop → dmn/dmn.treetop} +1 -1
- data/lib/dmn/information_requirement.rb +27 -0
- data/lib/dmn/input.rb +26 -0
- data/lib/dmn/literal_expression.rb +372 -0
- data/lib/{spot_feel → dmn}/nodes.rb +4 -4
- data/lib/dmn/output.rb +27 -0
- data/lib/{spot_feel → dmn}/parser.rb +4 -4
- data/lib/dmn/rule.rb +61 -0
- data/lib/dmn/unary_tests.rb +25 -0
- data/lib/dmn/variable.rb +25 -0
- data/lib/dmn/version.rb +5 -0
- data/lib/{spot_feel.rb → dmn.rb} +22 -12
- metadata +19 -20
- data/lib/spot_feel/dmn/decision.rb +0 -50
- data/lib/spot_feel/dmn/decision_table.rb +0 -53
- data/lib/spot_feel/dmn/definitions.rb +0 -68
- data/lib/spot_feel/dmn/information_requirement.rb +0 -29
- data/lib/spot_feel/dmn/input.rb +0 -28
- data/lib/spot_feel/dmn/literal_expression.rb +0 -374
- data/lib/spot_feel/dmn/output.rb +0 -29
- data/lib/spot_feel/dmn/rule.rb +0 -63
- data/lib/spot_feel/dmn/unary_tests.rb +0 -27
- data/lib/spot_feel/dmn/variable.rb +0 -27
- data/lib/spot_feel/dmn.rb +0 -17
- data/lib/spot_feel/version.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49694c6ba7cf12162a99192f8a7080d0b7134e8e5a2a0a5d0f88ad1c14bb59f0
|
4
|
+
data.tar.gz: 63d487600e8b8019d96d7596407a71a4c0e188c07e9ee901c914242cfdcf3bc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29eb3d6a96514f1056bb3d37779afaeb4d26a62eb5cafe87cb179425d3791d8acea77934cf8f48d51ad94341726c4e38a43221f8e3913e5df8ed6a77306a5036
|
7
|
+
data.tar.gz: 3b965748993649d0af143dbcc1eb237f203dedf2e37de054e5622d04010d81293ddf116ae4128c3dfef3f37ecbfe5d5e74ee0f33e951cd59ff43208f7a5f8a57
|
data/README.md
CHANGED
@@ -18,7 +18,7 @@ This project was inspired by these excellent libraries:
|
|
18
18
|
To evaluate an expression:
|
19
19
|
|
20
20
|
```ruby
|
21
|
-
|
21
|
+
DMN.evaluate('"👋 Hello " + name', variables: { name: "World" })
|
22
22
|
# => "👋 Hello World"
|
23
23
|
```
|
24
24
|
|
@@ -31,36 +31,36 @@ variables = {
|
|
31
31
|
age: 59,
|
32
32
|
}
|
33
33
|
}
|
34
|
-
|
34
|
+
DMN.evaluate('if person.age >= 18 then "adult" else "minor"', variables:)
|
35
35
|
# => "adult"
|
36
36
|
```
|
37
37
|
|
38
38
|
Calling a built-in function:
|
39
39
|
|
40
40
|
```ruby
|
41
|
-
|
41
|
+
DMN.evaluate('sum([1, 2, 3])')
|
42
42
|
# => 6
|
43
43
|
```
|
44
44
|
|
45
45
|
Calling a user-defined function:
|
46
46
|
|
47
47
|
```ruby
|
48
|
-
|
48
|
+
DMN.config.functions = {
|
49
49
|
"reverse": ->(s) { s.reverse }
|
50
50
|
}
|
51
|
-
|
51
|
+
DMN.evaluate('reverse("Hello World!")', functions:)
|
52
52
|
# => "!dlroW olleH"
|
53
53
|
```
|
54
54
|
|
55
55
|
To evaluate a unary tests:
|
56
56
|
|
57
57
|
```ruby
|
58
|
-
|
58
|
+
DMN.test(3, '<= 10, > 50'))
|
59
59
|
# => true
|
60
60
|
```
|
61
61
|
|
62
62
|
```ruby
|
63
|
-
|
63
|
+
DMN.test("Eric", '"Bob", "Holly", "Eric"')
|
64
64
|
# => true
|
65
65
|
```
|
66
66
|
|
@@ -76,7 +76,7 @@ variables = {
|
|
76
76
|
speed_limit: 65,
|
77
77
|
}
|
78
78
|
}
|
79
|
-
result =
|
79
|
+
result = DMN.decide('fine_decision', definitions_xml: fixture_source("fine.dmn"), variables:)
|
80
80
|
# => { "amount" => 1000, "points" => 7 })
|
81
81
|
```
|
82
82
|
|
@@ -155,13 +155,13 @@ UnaryTests.new(text: '> speed - speed_limit').variable_names
|
|
155
155
|
Execute:
|
156
156
|
|
157
157
|
```bash
|
158
|
-
$ bundle add "
|
158
|
+
$ bundle add "dmn"
|
159
159
|
```
|
160
160
|
|
161
161
|
Or install it directly:
|
162
162
|
|
163
163
|
```bash
|
164
|
-
$ gem install
|
164
|
+
$ gem install dmn
|
165
165
|
```
|
166
166
|
|
167
167
|
### Setup
|
data/lib/dmn/decision.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class Decision
|
5
|
+
attr_reader :id, :name, :decision_table, :variable, :literal_expression, :information_requirements
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
information_requirements = Array.wrap(json[:information_requirement]).map { |ir| InformationRequirement.from_json(ir) } if json[:information_requirement]
|
9
|
+
decision_table = DecisionTable.from_json(json[:decision_table]) if json[:decision_table]
|
10
|
+
literal_expression = LiteralExpression.from_json(json[:literal_expression]) if json[:literal_expression]
|
11
|
+
variable = Variable.from_json(json[:variable]) if json[:variable]
|
12
|
+
Decision.new(id: json[:id], name: json[:name], decision_table:, variable:, literal_expression:, information_requirements:)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(id:, name:, decision_table:, variable:, literal_expression:, information_requirements:)
|
16
|
+
@id = id
|
17
|
+
@name = name
|
18
|
+
@decision_table = decision_table
|
19
|
+
@variable = variable
|
20
|
+
@literal_expression = literal_expression
|
21
|
+
@information_requirements = information_requirements
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate(variables = {})
|
25
|
+
if literal_expression.present?
|
26
|
+
result = literal_expression.evaluate(variables)
|
27
|
+
variable.present? ? { variable.name => result } : result
|
28
|
+
elsif decision_table.present?
|
29
|
+
decision_table.evaluate(variables)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def required_decision_ids
|
34
|
+
information_requirements&.map(&:required_decision_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def as_json
|
38
|
+
{
|
39
|
+
id: id,
|
40
|
+
name: name,
|
41
|
+
decision_table: decision_table.as_json,
|
42
|
+
variable: variable.as_json,
|
43
|
+
literal_expression: literal_expression.as_json,
|
44
|
+
information_requirements: information_requirements&.map(&:as_json),
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class DecisionTable
|
5
|
+
attr_reader :id, :hit_policy, :inputs, :outputs, :rules
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
inputs = Array.wrap(json[:input]).map { |input| Input.from_json(input) }
|
9
|
+
outputs = Array.wrap(json[:output]).map { |output| Output.from_json(output) }
|
10
|
+
rules = Array.wrap(json[:rule]).map { |rule| Rule.from_json(rule) }
|
11
|
+
DecisionTable.new(id: json[:id], hit_policy: json[:hit_policy], inputs: inputs, outputs: outputs, rules: rules)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(id:, hit_policy:, inputs:, outputs:, rules:)
|
15
|
+
@id = id
|
16
|
+
@hit_policy = hit_policy&.downcase&.to_sym || :unique
|
17
|
+
@inputs = inputs
|
18
|
+
@outputs = outputs
|
19
|
+
@rules = rules
|
20
|
+
end
|
21
|
+
|
22
|
+
def evaluate(variables = {})
|
23
|
+
output_values = []
|
24
|
+
|
25
|
+
input_values = inputs.map do |input|
|
26
|
+
input.input_expression.evaluate(variables)
|
27
|
+
end
|
28
|
+
|
29
|
+
rules.each do |rule|
|
30
|
+
results = rule.evaluate(input_values, variables)
|
31
|
+
if results.all?
|
32
|
+
output_value = rule.output_value(outputs, variables)
|
33
|
+
return output_value if hit_policy == :first || hit_policy == :unique
|
34
|
+
output_values << output_value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
output_values.empty? ? nil : output_values
|
39
|
+
end
|
40
|
+
|
41
|
+
def as_json
|
42
|
+
{
|
43
|
+
id: id,
|
44
|
+
hit_policy: hit_policy,
|
45
|
+
inputs: inputs.map(&:as_json),
|
46
|
+
outputs: outputs.map(&:as_json),
|
47
|
+
rules: rules.map(&:as_json),
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class Definitions
|
5
|
+
attr_reader :id, :name, :namespace, :exporter, :exporter_version, :execution_platform, :execution_platform_version
|
6
|
+
attr_reader :decisions
|
7
|
+
|
8
|
+
def self.from_xml(xml)
|
9
|
+
XmlHasher.configure do |config|
|
10
|
+
config.snakecase = true
|
11
|
+
config.ignore_namespaces = true
|
12
|
+
config.string_keys = false
|
13
|
+
end
|
14
|
+
json = XmlHasher.parse(xml)
|
15
|
+
Definitions.from_json(json[:definitions])
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_json(json)
|
19
|
+
decisions = Array.wrap(json[:decision]).map { |decision| Decision.from_json(decision) }
|
20
|
+
Definitions.new(id: json[:id], name: json[:name], namespace: json[:namespace], exporter: json[:exporter], exporter_version: json[:exporter_version], execution_platform: json[:execution_platform], execution_platform_version: json[:execution_platform_version], decisions: decisions)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(id:, name:, namespace:, exporter:, exporter_version:, execution_platform:, execution_platform_version:, decisions:)
|
24
|
+
@id = id
|
25
|
+
@name = name
|
26
|
+
@namespace = namespace
|
27
|
+
@exporter = exporter
|
28
|
+
@exporter_version = exporter_version
|
29
|
+
@execution_platform = execution_platform
|
30
|
+
@execution_platform_version = execution_platform_version
|
31
|
+
@decisions = decisions
|
32
|
+
end
|
33
|
+
|
34
|
+
def evaluate(decision_id, variables: {}, already_evaluated_decisions: {})
|
35
|
+
decision = decisions.find { |d| d.id == decision_id }
|
36
|
+
raise EvaluationError, "Decision #{decision_id} not found" unless decision
|
37
|
+
|
38
|
+
# Evaluate required decisions recursively
|
39
|
+
decision.required_decision_ids&.each do |required_decision_id|
|
40
|
+
next if already_evaluated_decisions[required_decision_id]
|
41
|
+
next if decisions.find { |d| d.id == required_decision_id }.nil?
|
42
|
+
|
43
|
+
result = evaluate(required_decision_id, variables:, already_evaluated_decisions:)
|
44
|
+
|
45
|
+
variables.merge!(result) if result.is_a?(Hash)
|
46
|
+
|
47
|
+
already_evaluated_decisions[required_decision_id] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
decision.evaluate(variables)
|
51
|
+
end
|
52
|
+
|
53
|
+
def as_json
|
54
|
+
{
|
55
|
+
id: id,
|
56
|
+
name: name,
|
57
|
+
namespace: namespace,
|
58
|
+
exporter: exporter,
|
59
|
+
exporter_version: exporter_version,
|
60
|
+
execution_platform: execution_platform,
|
61
|
+
execution_platform_version: execution_platform_version,
|
62
|
+
decisions: decisions.map(&:as_json),
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class InformationRequirement
|
5
|
+
attr_reader :id, :required_input_id, :required_decision_id
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
required_input_id = json[:required_input][:href].delete_prefix("#") if json[:required_input]
|
9
|
+
required_decision_id = json[:required_decision][:href].delete_prefix("#") if json[:required_decision]
|
10
|
+
InformationRequirement.new(id: json[:id], required_input_id: required_input_id, required_decision_id: required_decision_id)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(id:, required_input_id:, required_decision_id:)
|
14
|
+
@id = id
|
15
|
+
@required_input_id = required_input_id
|
16
|
+
@required_decision_id = required_decision_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json
|
20
|
+
{
|
21
|
+
id: id,
|
22
|
+
required_decision_id: required_decision_id,
|
23
|
+
required_input_id: required_input_id,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/dmn/input.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class Input
|
5
|
+
attr_reader :id, :label, :input_expression
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
input_expression = LiteralExpression.from_json(json[:input_expression]) if json[:input_expression]
|
9
|
+
Input.new(id: json[:id], label: json[:label], input_expression:)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(id:, label:, input_expression:)
|
13
|
+
@id = id
|
14
|
+
@label = label
|
15
|
+
@input_expression = input_expression
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_json
|
19
|
+
{
|
20
|
+
id: id,
|
21
|
+
label: label,
|
22
|
+
input_expression: input_expression.as_json,
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,372 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class LiteralExpression
|
5
|
+
attr_reader :id, :text
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
LiteralExpression.new(id: json[:id], text: json[:text])
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id: nil, text:)
|
12
|
+
@id = id
|
13
|
+
@text = text&.strip
|
14
|
+
end
|
15
|
+
|
16
|
+
def tree
|
17
|
+
@tree ||= DMN::Parser.parse(text)
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
return false if text.blank?
|
22
|
+
tree.present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def evaluate(variables = {})
|
26
|
+
tree.eval(functions.merge(variables))
|
27
|
+
end
|
28
|
+
|
29
|
+
def functions
|
30
|
+
builtins = LiteralExpression.builtin_functions
|
31
|
+
custom = (DMN.config.functions || {})
|
32
|
+
ActiveSupport::HashWithIndifferentAccess.new(builtins.merge(custom))
|
33
|
+
end
|
34
|
+
|
35
|
+
def named_functions
|
36
|
+
# Initialize a set to hold the qualified names
|
37
|
+
function_names = Set.new
|
38
|
+
|
39
|
+
# Define a lambda for the recursive function
|
40
|
+
walk_tree = lambda do |node|
|
41
|
+
# If the node is a qualified name, add it to the set
|
42
|
+
if node.is_a?(DMN::FunctionInvocation)
|
43
|
+
function_names << node.fn_name.text_value
|
44
|
+
end
|
45
|
+
|
46
|
+
# Recursively walk the child nodes
|
47
|
+
node.elements&.each do |child|
|
48
|
+
walk_tree.call(child)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Start walking the tree from the root
|
53
|
+
walk_tree.call(tree)
|
54
|
+
|
55
|
+
# Return the array of functions
|
56
|
+
function_names.to_a
|
57
|
+
end
|
58
|
+
|
59
|
+
def named_variables
|
60
|
+
# Initialize a set to hold the qualified names
|
61
|
+
qualified_names = Set.new
|
62
|
+
|
63
|
+
# Define a lambda for the recursive function
|
64
|
+
walk_tree = lambda do |node|
|
65
|
+
# If the node is a qualified name, add it to the set
|
66
|
+
if node.is_a?(DMN::QualifiedName)
|
67
|
+
qualified_names << node.text_value
|
68
|
+
end
|
69
|
+
|
70
|
+
# Recursively walk the child nodes
|
71
|
+
node.elements&.each do |child|
|
72
|
+
walk_tree.call(child)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Start walking the tree from the root
|
77
|
+
walk_tree.call(tree)
|
78
|
+
|
79
|
+
# Return the array of qualified names
|
80
|
+
qualified_names.to_a
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.builtin_functions
|
84
|
+
HashWithIndifferentAccess.new({
|
85
|
+
# Conversion functions
|
86
|
+
"string": ->(from) {
|
87
|
+
return if from.nil?
|
88
|
+
from.to_s
|
89
|
+
},
|
90
|
+
"number": ->(from) {
|
91
|
+
return if from.nil?
|
92
|
+
from.include?(".") ? from.to_f : from.to_i
|
93
|
+
},
|
94
|
+
# Boolean functions
|
95
|
+
"not": ->(value) {
|
96
|
+
if value == true || value == false
|
97
|
+
!value
|
98
|
+
end
|
99
|
+
},
|
100
|
+
"is defined": ->(value) {
|
101
|
+
return if value.nil?
|
102
|
+
!value.nil?
|
103
|
+
},
|
104
|
+
"get or else": ->(value, default) {
|
105
|
+
value.nil? ? default : value
|
106
|
+
},
|
107
|
+
# String functions
|
108
|
+
"substring": ->(string, start, length) {
|
109
|
+
return if string.nil? || start.nil?
|
110
|
+
return "" if length.nil?
|
111
|
+
string[start - 1, length]
|
112
|
+
},
|
113
|
+
"substring before": ->(string, match) {
|
114
|
+
return if string.nil? || match.nil?
|
115
|
+
string.split(match).first
|
116
|
+
},
|
117
|
+
"substring after": ->(string, match) {
|
118
|
+
return if string.nil? || match.nil?
|
119
|
+
string.split(match).last
|
120
|
+
},
|
121
|
+
"string length": ->(string) {
|
122
|
+
return if string.nil?
|
123
|
+
string.length
|
124
|
+
},
|
125
|
+
"upper case": ->(string) {
|
126
|
+
return if string.nil?
|
127
|
+
string.upcase
|
128
|
+
},
|
129
|
+
"lower case": -> (string) {
|
130
|
+
return if string.nil?
|
131
|
+
string.downcase
|
132
|
+
},
|
133
|
+
"contains": ->(string, match) {
|
134
|
+
return if string.nil? || match.nil?
|
135
|
+
string.include?(match)
|
136
|
+
},
|
137
|
+
"starts with": ->(string, match) {
|
138
|
+
return if string.nil? || match.nil?
|
139
|
+
string.start_with?(match)
|
140
|
+
},
|
141
|
+
"ends with": ->(string, match) {
|
142
|
+
return if string.nil? || match.nil?
|
143
|
+
string.end_with?(match)
|
144
|
+
},
|
145
|
+
"matches": ->(string, match) {
|
146
|
+
return if string.nil? || match.nil?
|
147
|
+
string.match?(match)
|
148
|
+
},
|
149
|
+
"replace": ->(string, match, replacement) {
|
150
|
+
return if string.nil? || match.nil? || replacement.nil?
|
151
|
+
string.gsub(match, replacement)
|
152
|
+
},
|
153
|
+
"split": ->(string, match) {
|
154
|
+
return if string.nil? || match.nil?
|
155
|
+
string.split(match)
|
156
|
+
},
|
157
|
+
"strip": -> (string) {
|
158
|
+
return if string.nil?
|
159
|
+
string.strip
|
160
|
+
},
|
161
|
+
"extract": -> (string, pattern) {
|
162
|
+
return if string.nil? || pattern.nil?
|
163
|
+
string.match(pattern).captures
|
164
|
+
},
|
165
|
+
# Numeric functions
|
166
|
+
"decimal": ->(n, scale) {
|
167
|
+
return if n.nil? || scale.nil?
|
168
|
+
n.round(scale)
|
169
|
+
},
|
170
|
+
"floor": ->(n) {
|
171
|
+
return if n.nil?
|
172
|
+
n.floor
|
173
|
+
},
|
174
|
+
"ceiling": ->(n) {
|
175
|
+
return if n.nil?
|
176
|
+
n.ceil
|
177
|
+
},
|
178
|
+
"round up": ->(n) {
|
179
|
+
return if n.nil?
|
180
|
+
n.ceil
|
181
|
+
},
|
182
|
+
"round down": ->(n) {
|
183
|
+
return if n.nil?
|
184
|
+
n.floor
|
185
|
+
},
|
186
|
+
"abs": ->(n) {
|
187
|
+
return if n.nil?
|
188
|
+
n.abs
|
189
|
+
},
|
190
|
+
"modulo": ->(n, divisor) {
|
191
|
+
return if n.nil? || divisor.nil?
|
192
|
+
n % divisor
|
193
|
+
},
|
194
|
+
"sqrt": ->(n) {
|
195
|
+
return if n.nil?
|
196
|
+
Math.sqrt(n)
|
197
|
+
},
|
198
|
+
"log": ->(n) {
|
199
|
+
return if n.nil?
|
200
|
+
Math.log(n)
|
201
|
+
},
|
202
|
+
"exp": ->(n) {
|
203
|
+
return if n.nil?
|
204
|
+
Math.exp(n)
|
205
|
+
},
|
206
|
+
"odd": ->(n) {
|
207
|
+
return if n.nil?
|
208
|
+
n.odd?
|
209
|
+
},
|
210
|
+
"even": ->(n) {
|
211
|
+
return if n.nil?
|
212
|
+
n.even?
|
213
|
+
},
|
214
|
+
"random number": ->(n) {
|
215
|
+
return if n.nil?
|
216
|
+
rand(n)
|
217
|
+
},
|
218
|
+
# List functions
|
219
|
+
"list contains": ->(list, match) {
|
220
|
+
return if list.nil?
|
221
|
+
return false if match.nil?
|
222
|
+
list.include?(match)
|
223
|
+
},
|
224
|
+
"count": ->(list) {
|
225
|
+
return if list.nil?
|
226
|
+
return 0 if list.empty?
|
227
|
+
list.length
|
228
|
+
},
|
229
|
+
"min": ->(list) {
|
230
|
+
return if list.nil?
|
231
|
+
list.min
|
232
|
+
},
|
233
|
+
"max": ->(list) {
|
234
|
+
return if list.nil?
|
235
|
+
list.max
|
236
|
+
},
|
237
|
+
"sum": ->(list) {
|
238
|
+
return if list.nil?
|
239
|
+
list.sum
|
240
|
+
},
|
241
|
+
"product": ->(list) {
|
242
|
+
return if list.nil?
|
243
|
+
list.inject(:*)
|
244
|
+
},
|
245
|
+
"mean": ->(list) {
|
246
|
+
return if list.nil?
|
247
|
+
list.sum / list.length
|
248
|
+
},
|
249
|
+
"median": ->(list) {
|
250
|
+
return if list.nil?
|
251
|
+
list.sort[list.length / 2]
|
252
|
+
},
|
253
|
+
"stddev": ->(list) {
|
254
|
+
return if list.nil?
|
255
|
+
mean = list.sum / list.length.to_f
|
256
|
+
Math.sqrt(list.map { |n| (n - mean)**2 }.sum / list.length)
|
257
|
+
},
|
258
|
+
"mode": ->(list) {
|
259
|
+
return if list.nil?
|
260
|
+
list.group_by(&:itself).values.max_by(&:size).first
|
261
|
+
},
|
262
|
+
"all": ->(list) {
|
263
|
+
return if list.nil?
|
264
|
+
list.all?
|
265
|
+
},
|
266
|
+
"any": ->(list) {
|
267
|
+
return if list.nil?
|
268
|
+
list.any?
|
269
|
+
},
|
270
|
+
"sublist": ->(list, start, length) {
|
271
|
+
return if list.nil? || start.nil?
|
272
|
+
return [] if length.nil?
|
273
|
+
list[start - 1, length]
|
274
|
+
},
|
275
|
+
"append": ->(list, item) {
|
276
|
+
return if list.nil?
|
277
|
+
list + [item]
|
278
|
+
},
|
279
|
+
"concatenate": ->(list1, list2) {
|
280
|
+
return [nil, nil] if list1.nil? && list2.nil?
|
281
|
+
return [nil] + list2 if list1.nil?
|
282
|
+
return list1 + [nil] if list2.nil?
|
283
|
+
Array.wrap(list1) + Array.wrap(list2)
|
284
|
+
},
|
285
|
+
"insert before": ->(list, position, item) {
|
286
|
+
return if list.nil? || position.nil?
|
287
|
+
list.insert(position - 1, item)
|
288
|
+
},
|
289
|
+
"remove": ->(list, position) {
|
290
|
+
return if list.nil? || position.nil?
|
291
|
+
list.delete_at(position - 1); list
|
292
|
+
},
|
293
|
+
"reverse": ->(list) {
|
294
|
+
return if list.nil?
|
295
|
+
list.reverse
|
296
|
+
},
|
297
|
+
"index of": ->(list, match) {
|
298
|
+
return if list.nil?
|
299
|
+
return [] if match.nil?
|
300
|
+
list.index(match) + 1
|
301
|
+
},
|
302
|
+
"union": ->(list1, list2) {
|
303
|
+
return if list1.nil? || list2.nil?
|
304
|
+
list1 | list2
|
305
|
+
},
|
306
|
+
"distinct values": ->(list) {
|
307
|
+
return if list.nil?
|
308
|
+
list.uniq
|
309
|
+
},
|
310
|
+
"duplicate values": ->(list) {
|
311
|
+
return if list.nil?
|
312
|
+
list.select { |e| list.count(e) > 1 }.uniq
|
313
|
+
},
|
314
|
+
"flatten": ->(list) {
|
315
|
+
return if list.nil?
|
316
|
+
list.flatten
|
317
|
+
},
|
318
|
+
"sort": ->(list) {
|
319
|
+
return if list.nil?
|
320
|
+
list.sort
|
321
|
+
},
|
322
|
+
"string join": ->(list, separator) {
|
323
|
+
return if list.nil?
|
324
|
+
list.join(separator)
|
325
|
+
},
|
326
|
+
# Context functions
|
327
|
+
"get value": ->(context, name) {
|
328
|
+
return if context.nil? || name.nil?
|
329
|
+
context[name]
|
330
|
+
},
|
331
|
+
"context put": ->(context, name, value) {
|
332
|
+
return if context.nil? || name.nil?
|
333
|
+
context[name] = value; context
|
334
|
+
},
|
335
|
+
"context merge": ->(context1, context2) {
|
336
|
+
return if context1.nil? || context2.nil?
|
337
|
+
context1.merge(context2)
|
338
|
+
},
|
339
|
+
"get entries": ->(context) {
|
340
|
+
return if context.nil?
|
341
|
+
context.entries
|
342
|
+
},
|
343
|
+
# Temporal functions
|
344
|
+
"now": ->() { Time.now },
|
345
|
+
"today": ->() { Date.today },
|
346
|
+
"day of week": ->(date) {
|
347
|
+
return if date.nil?
|
348
|
+
date.wday
|
349
|
+
},
|
350
|
+
"day of year": ->(date) {
|
351
|
+
return if date.nil?
|
352
|
+
date.yday
|
353
|
+
},
|
354
|
+
"week of year": ->(date) {
|
355
|
+
return if date.nil?
|
356
|
+
date.cweek
|
357
|
+
},
|
358
|
+
"month of year": ->(date) {
|
359
|
+
return if date.nil?
|
360
|
+
date.month
|
361
|
+
},
|
362
|
+
})
|
363
|
+
end
|
364
|
+
|
365
|
+
def as_json
|
366
|
+
{
|
367
|
+
id: id,
|
368
|
+
text: text,
|
369
|
+
}
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|