dmn 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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 +17 -19
- data/lib/dmn/output.rb +27 -0
- data/lib/{spot_feel → dmn}/parser.rb +7 -7
- 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 -17
- metadata +29 -33
- 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: 83ab9c509d78447fc0dcf11d993e79c9105baf97904af53e932e16ae7eaee2df
|
4
|
+
data.tar.gz: be469b68cdeab12f0ffee64e6a1c884ee9b67d10e2d9b899f1f3ecfa7905a975
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5b1b6a24a4c6d2f305d41e94ac83bcb951f1dbfe8fc4ddccc6d5675ee5f3af70529778f288db5c7dea989a5ef7af685250ad6b7abd5dea7b80bef1afe20d019
|
7
|
+
data.tar.gz: 62e2cecb612379245854e09684e1632ae6fb670d0618577685d00e92c944aaf44c9e8a5e59173372280a5887879a190603bd1ec47e07f34bebb9e533f91d9a3b
|
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
|