odin-foundation 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/odin/export.rb +1 -1
- data/lib/odin/forms/accessibility.rb +95 -0
- data/lib/odin/forms/css.rb +42 -0
- data/lib/odin/forms/parser.rb +719 -0
- data/lib/odin/forms/renderer.rb +534 -0
- data/lib/odin/forms/types.rb +102 -0
- data/lib/odin/forms/units.rb +41 -0
- data/lib/odin/forms.rb +55 -0
- data/lib/odin/parsing/parser.rb +25 -1
- data/lib/odin/parsing/tokenizer.rb +38 -20
- data/lib/odin/parsing/value_parser.rb +65 -7
- data/lib/odin/resolver/import_resolver.rb +40 -12
- data/lib/odin/resolver/type_registry.rb +54 -0
- data/lib/odin/transform/format_exporters.rb +88 -48
- data/lib/odin/transform/source_parsers.rb +2 -2
- data/lib/odin/transform/transform_engine.rb +1388 -246
- data/lib/odin/transform/transform_expr.rb +222 -0
- data/lib/odin/transform/transform_parser.rb +377 -19
- data/lib/odin/transform/transform_types.rb +23 -7
- data/lib/odin/transform/verb_context.rb +19 -1
- data/lib/odin/transform/verbs/aggregation_verbs.rb +2 -1
- data/lib/odin/transform/verbs/collection_verbs.rb +164 -89
- data/lib/odin/transform/verbs/datetime_verbs.rb +86 -15
- data/lib/odin/transform/verbs/extra_verbs.rb +613 -0
- data/lib/odin/transform/verbs/financial_verbs.rb +116 -27
- data/lib/odin/transform/verbs/geo_verbs.rb +7 -0
- data/lib/odin/transform/verbs/numeric_verbs.rb +85 -64
- data/lib/odin/transform/verbs/object_verbs.rb +31 -26
- data/lib/odin/types/errors.rb +9 -1
- data/lib/odin/types/schema.rb +20 -3
- data/lib/odin/utils/format_utils.rb +31 -15
- data/lib/odin/validation/format_validators.rb +7 -9
- data/lib/odin/validation/invariant_evaluator.rb +410 -0
- data/lib/odin/validation/schema_definition_validator.rb +357 -0
- data/lib/odin/validation/schema_parser.rb +234 -21
- data/lib/odin/validation/validator.rb +281 -123
- data/lib/odin/version.rb +1 -1
- data/lib/odin.rb +100 -4
- metadata +14 -2
|
@@ -58,15 +58,16 @@ module Odin
|
|
|
58
58
|
|
|
59
59
|
# Transform definition — top-level AST node
|
|
60
60
|
class TransformDef
|
|
61
|
-
attr_reader :header, :segments, :constants, :tables, :accumulators, :passes
|
|
61
|
+
attr_reader :header, :segments, :constants, :tables, :accumulators, :passes, :imports
|
|
62
62
|
|
|
63
|
-
def initialize(header:, segments: [], constants: {}, tables: {}, accumulators: {}, passes: [])
|
|
63
|
+
def initialize(header:, segments: [], constants: {}, tables: {}, accumulators: {}, passes: [], imports: [])
|
|
64
64
|
@header = header
|
|
65
65
|
@segments = segments
|
|
66
66
|
@constants = constants
|
|
67
67
|
@tables = tables
|
|
68
68
|
@accumulators = accumulators
|
|
69
69
|
@passes = passes
|
|
70
|
+
@imports = imports
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
def direction
|
|
@@ -92,7 +93,7 @@ module Odin
|
|
|
92
93
|
class TransformHeader
|
|
93
94
|
attr_reader :odin_version, :transform_version, :direction,
|
|
94
95
|
:target_format, :enforce_confidential,
|
|
95
|
-
:source_options, :target_options,
|
|
96
|
+
:source_options, :target_options, :target_namespaces,
|
|
96
97
|
:strict_types, :id, :name
|
|
97
98
|
|
|
98
99
|
def initialize(
|
|
@@ -103,6 +104,7 @@ module Odin
|
|
|
103
104
|
enforce_confidential: ConfidentialMode::NONE,
|
|
104
105
|
source_options: {},
|
|
105
106
|
target_options: {},
|
|
107
|
+
target_namespaces: {},
|
|
106
108
|
strict_types: false,
|
|
107
109
|
id: nil,
|
|
108
110
|
name: nil
|
|
@@ -114,6 +116,7 @@ module Odin
|
|
|
114
116
|
@enforce_confidential = enforce_confidential
|
|
115
117
|
@source_options = source_options
|
|
116
118
|
@target_options = target_options
|
|
119
|
+
@target_namespaces = target_namespaces
|
|
117
120
|
@strict_types = strict_types
|
|
118
121
|
@id = id
|
|
119
122
|
@name = name
|
|
@@ -125,7 +128,9 @@ module Odin
|
|
|
125
128
|
attr_reader :name, :path, :array_index,
|
|
126
129
|
:field_mappings, :discriminator, :discriminator_value,
|
|
127
130
|
:when_condition, :each_source, :if_condition,
|
|
128
|
-
:
|
|
131
|
+
:elif_condition, :is_else,
|
|
132
|
+
:children, :pass, :counter_name, :is_array,
|
|
133
|
+
:loops, :is_literal, :literal_body
|
|
129
134
|
|
|
130
135
|
def initialize(
|
|
131
136
|
name:,
|
|
@@ -137,10 +142,15 @@ module Odin
|
|
|
137
142
|
when_condition: nil,
|
|
138
143
|
each_source: nil,
|
|
139
144
|
if_condition: nil,
|
|
145
|
+
elif_condition: nil,
|
|
146
|
+
is_else: false,
|
|
140
147
|
children: [],
|
|
141
148
|
pass: nil,
|
|
142
149
|
counter_name: nil,
|
|
143
|
-
is_array: false
|
|
150
|
+
is_array: false,
|
|
151
|
+
loops: [],
|
|
152
|
+
is_literal: false,
|
|
153
|
+
literal_body: nil
|
|
144
154
|
)
|
|
145
155
|
@name = name
|
|
146
156
|
@path = path
|
|
@@ -151,10 +161,15 @@ module Odin
|
|
|
151
161
|
@when_condition = when_condition
|
|
152
162
|
@each_source = each_source
|
|
153
163
|
@if_condition = if_condition
|
|
164
|
+
@elif_condition = elif_condition
|
|
165
|
+
@is_else = is_else
|
|
154
166
|
@children = children
|
|
155
167
|
@pass = pass
|
|
156
168
|
@counter_name = counter_name
|
|
157
169
|
@is_array = is_array
|
|
170
|
+
@loops = loops
|
|
171
|
+
@is_literal = is_literal
|
|
172
|
+
@literal_body = literal_body
|
|
158
173
|
end
|
|
159
174
|
end
|
|
160
175
|
|
|
@@ -261,12 +276,13 @@ module Odin
|
|
|
261
276
|
|
|
262
277
|
# Transform result
|
|
263
278
|
class TransformResult
|
|
264
|
-
attr_reader :output, :formatted, :errors, :output_dv
|
|
279
|
+
attr_reader :output, :formatted, :errors, :warnings, :output_dv
|
|
265
280
|
|
|
266
|
-
def initialize(output:, formatted: nil, errors: [], output_dv: nil)
|
|
281
|
+
def initialize(output:, formatted: nil, errors: [], warnings: [], output_dv: nil)
|
|
267
282
|
@output = output
|
|
268
283
|
@formatted = formatted
|
|
269
284
|
@errors = errors
|
|
285
|
+
@warnings = warnings
|
|
270
286
|
@output_dv = output_dv
|
|
271
287
|
end
|
|
272
288
|
|
|
@@ -8,6 +8,7 @@ module Odin
|
|
|
8
8
|
:loop_index, # Integer — current loop iteration (0-based)
|
|
9
9
|
:loop_length, # Integer — total loop length
|
|
10
10
|
:loop_vars, # Hash<String, DynValue> — named loop variables
|
|
11
|
+
:aliases, # Hash<String, DynValue> — :loop :as alias bindings
|
|
11
12
|
:accumulators, # Hash<String, DynValue> — accumulator state
|
|
12
13
|
:tables, # Hash<String, LookupTable> — lookup tables
|
|
13
14
|
:constants, # Hash<String, DynValue> — constant values
|
|
@@ -16,7 +17,12 @@ module Odin
|
|
|
16
17
|
:loop_depth, # Integer — nesting depth for security
|
|
17
18
|
:field_modifiers, # Hash<String, Array<Symbol>> — tracked field modifiers
|
|
18
19
|
:errors, # Array<TransformEngine::TransformError> — collected errors
|
|
19
|
-
:
|
|
20
|
+
:warnings, # Array<String> — collected warnings
|
|
21
|
+
:source_format, # String — source format for directive handling
|
|
22
|
+
:on_validation, # String — validation policy (fail / warn / skip)
|
|
23
|
+
:on_missing, # String — missing-data policy (fail / warn / skip / default)
|
|
24
|
+
:on_error, # String — error policy (fail / warn / skip)
|
|
25
|
+
:strict_types # Boolean — enforce verb argument type signatures
|
|
20
26
|
|
|
21
27
|
MAX_LOOP_DEPTH = 10
|
|
22
28
|
|
|
@@ -26,6 +32,7 @@ module Odin
|
|
|
26
32
|
@loop_index = 0
|
|
27
33
|
@loop_length = 0
|
|
28
34
|
@loop_vars = {}
|
|
35
|
+
@aliases = {}
|
|
29
36
|
@accumulators = {}
|
|
30
37
|
@tables = {}
|
|
31
38
|
@constants = {}
|
|
@@ -34,7 +41,12 @@ module Odin
|
|
|
34
41
|
@loop_depth = 0
|
|
35
42
|
@field_modifiers = {}
|
|
36
43
|
@errors = []
|
|
44
|
+
@warnings = []
|
|
37
45
|
@source_format = ""
|
|
46
|
+
@on_validation = nil
|
|
47
|
+
@on_missing = nil
|
|
48
|
+
@on_error = nil
|
|
49
|
+
@strict_types = false
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
def next_sequence(name)
|
|
@@ -72,6 +84,7 @@ module Odin
|
|
|
72
84
|
ctx = VerbContext.new
|
|
73
85
|
ctx.source = @source
|
|
74
86
|
ctx.loop_vars = @loop_vars.dup
|
|
87
|
+
ctx.aliases = @aliases.dup
|
|
75
88
|
ctx.accumulators = @accumulators # shared reference
|
|
76
89
|
ctx.tables = @tables
|
|
77
90
|
ctx.constants = @constants
|
|
@@ -80,6 +93,11 @@ module Odin
|
|
|
80
93
|
ctx.loop_depth = @loop_depth + 1
|
|
81
94
|
ctx.field_modifiers = @field_modifiers # shared reference
|
|
82
95
|
ctx.errors = @errors # shared reference
|
|
96
|
+
ctx.warnings = @warnings # shared reference
|
|
97
|
+
ctx.source_format = @source_format
|
|
98
|
+
ctx.on_validation = @on_validation
|
|
99
|
+
ctx.on_missing = @on_missing
|
|
100
|
+
ctx.on_error = @on_error
|
|
83
101
|
ctx
|
|
84
102
|
end
|
|
85
103
|
end
|
|
@@ -52,7 +52,8 @@ module Odin
|
|
|
52
52
|
items = CollectionVerbs.extract_items(args[0])
|
|
53
53
|
nums = items.filter_map { |item| NumericVerbs.to_double(item) }
|
|
54
54
|
return dv.of_null if nums.empty?
|
|
55
|
-
|
|
55
|
+
total = nums.inject(0.0) { |s, v| s + v }
|
|
56
|
+
NumericVerbs.numeric_result(total / nums.length.to_f)
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
registry["first"] = ->(args, _ctx) {
|
|
@@ -7,17 +7,22 @@ module Odin
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def extract_items(v)
|
|
10
|
-
|
|
10
|
+
as_items(v) || []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns the underlying array items, or nil when v is not array-like.
|
|
14
|
+
def as_items(v)
|
|
15
|
+
return nil if v.nil? || v.null?
|
|
11
16
|
return v.value if v.array?
|
|
12
17
|
if v.string?
|
|
13
18
|
begin
|
|
14
19
|
parsed = Types::DynValue.extract_array(v.value)
|
|
15
|
-
return parsed.value
|
|
20
|
+
return parsed.value if parsed.array?
|
|
16
21
|
rescue
|
|
17
|
-
return
|
|
22
|
+
return nil
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
|
-
|
|
25
|
+
nil
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def compare_dyn_values(a, b)
|
|
@@ -37,6 +42,24 @@ module Odin
|
|
|
37
42
|
a.to_string == b.to_string
|
|
38
43
|
end
|
|
39
44
|
|
|
45
|
+
# Field/operator/value condition matching used by find, findIndex, partition.
|
|
46
|
+
def matches_condition?(item, field, op, compare)
|
|
47
|
+
val = item.object? ? item.get(field) : item
|
|
48
|
+
val ||= Types::DynValue.of_null
|
|
49
|
+
case op
|
|
50
|
+
when "=", "==" then val.to_string == compare.to_string
|
|
51
|
+
when "!=", "<>" then val.to_string != compare.to_string
|
|
52
|
+
when "<" then (NumericVerbs.to_double(val) || 0) < (NumericVerbs.to_double(compare) || 0)
|
|
53
|
+
when "<=" then (NumericVerbs.to_double(val) || 0) <= (NumericVerbs.to_double(compare) || 0)
|
|
54
|
+
when ">" then (NumericVerbs.to_double(val) || 0) > (NumericVerbs.to_double(compare) || 0)
|
|
55
|
+
when ">=" then (NumericVerbs.to_double(val) || 0) >= (NumericVerbs.to_double(compare) || 0)
|
|
56
|
+
when "contains" then val.to_string.include?(compare.to_string)
|
|
57
|
+
when "startsWith" then val.to_string.start_with?(compare.to_string)
|
|
58
|
+
when "endsWith" then val.to_string.end_with?(compare.to_string)
|
|
59
|
+
else false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
40
63
|
def register(registry)
|
|
41
64
|
dv = Types::DynValue
|
|
42
65
|
|
|
@@ -204,9 +227,15 @@ module Odin
|
|
|
204
227
|
}
|
|
205
228
|
|
|
206
229
|
registry["every"] = ->(args, _ctx) {
|
|
207
|
-
items = CollectionVerbs.
|
|
230
|
+
items = CollectionVerbs.as_items(args[0])
|
|
231
|
+
return dv.of_null if items.nil?
|
|
208
232
|
return dv.of_bool(true) if items.empty?
|
|
209
|
-
if args.length >=
|
|
233
|
+
if args.length >= 4
|
|
234
|
+
field = args[1]&.to_string || ""
|
|
235
|
+
op = args[2]&.to_string || "="
|
|
236
|
+
compare = args[3]
|
|
237
|
+
dv.of_bool(items.all? { |item| CollectionVerbs.matches_condition?(item, field, op, compare) })
|
|
238
|
+
elsif args.length >= 2
|
|
210
239
|
field = args[1]&.to_string || ""
|
|
211
240
|
dv.of_bool(items.all? { |item| (item.object? ? item.get(field) : item)&.truthy? || false })
|
|
212
241
|
else
|
|
@@ -215,9 +244,15 @@ module Odin
|
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
registry["some"] = ->(args, _ctx) {
|
|
218
|
-
items = CollectionVerbs.
|
|
247
|
+
items = CollectionVerbs.as_items(args[0])
|
|
248
|
+
return dv.of_null if items.nil?
|
|
219
249
|
return dv.of_bool(false) if items.empty?
|
|
220
|
-
if args.length >=
|
|
250
|
+
if args.length >= 4
|
|
251
|
+
field = args[1]&.to_string || ""
|
|
252
|
+
op = args[2]&.to_string || "="
|
|
253
|
+
compare = args[3]
|
|
254
|
+
dv.of_bool(items.any? { |item| CollectionVerbs.matches_condition?(item, field, op, compare) })
|
|
255
|
+
elsif args.length >= 2
|
|
221
256
|
field = args[1]&.to_string || ""
|
|
222
257
|
dv.of_bool(items.any? { |item| (item.object? ? item.get(field) : item)&.truthy? || false })
|
|
223
258
|
else
|
|
@@ -226,24 +261,24 @@ module Odin
|
|
|
226
261
|
}
|
|
227
262
|
|
|
228
263
|
registry["find"] = ->(args, _ctx) {
|
|
264
|
+
return dv.of_null if args.length < 4
|
|
265
|
+
|
|
229
266
|
items = CollectionVerbs.extract_items(args[0])
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
found = items.find(&:truthy?)
|
|
235
|
-
end
|
|
267
|
+
field = args[1]&.to_string || ""
|
|
268
|
+
op = args[2]&.to_string || "="
|
|
269
|
+
compare = args[3]
|
|
270
|
+
found = items.find { |item| CollectionVerbs.matches_condition?(item, field, op, compare) }
|
|
236
271
|
found || dv.of_null
|
|
237
272
|
}
|
|
238
273
|
|
|
239
274
|
registry["findIndex"] = ->(args, _ctx) {
|
|
275
|
+
return dv.of_integer(-1) if args.length < 4
|
|
276
|
+
|
|
240
277
|
items = CollectionVerbs.extract_items(args[0])
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
idx = items.index(&:truthy?)
|
|
246
|
-
end
|
|
278
|
+
field = args[1]&.to_string || ""
|
|
279
|
+
op = args[2]&.to_string || "="
|
|
280
|
+
compare = args[3]
|
|
281
|
+
idx = items.index { |item| CollectionVerbs.matches_condition?(item, field, op, compare) }
|
|
247
282
|
dv.of_integer(idx || -1)
|
|
248
283
|
}
|
|
249
284
|
|
|
@@ -254,73 +289,78 @@ module Odin
|
|
|
254
289
|
}
|
|
255
290
|
|
|
256
291
|
registry["concatArrays"] = ->(args, _ctx) {
|
|
292
|
+
extracted = args.map { |a| CollectionVerbs.as_items(a) }
|
|
293
|
+
return dv.of_null if extracted.all?(&:nil?)
|
|
257
294
|
result = []
|
|
258
|
-
|
|
259
|
-
if a&.array?
|
|
260
|
-
result.concat(a.value)
|
|
261
|
-
elsif !a.nil? && !a.null?
|
|
262
|
-
result << a
|
|
263
|
-
end
|
|
264
|
-
end
|
|
295
|
+
extracted.each { |items| result.concat(items) if items }
|
|
265
296
|
dv.of_array(result)
|
|
266
297
|
}
|
|
267
298
|
|
|
268
299
|
registry["zip"] = ->(args, _ctx) {
|
|
269
|
-
|
|
270
|
-
return dv.
|
|
271
|
-
|
|
272
|
-
result = (0...
|
|
273
|
-
|
|
274
|
-
dv.of_array(pair)
|
|
300
|
+
extracted = args.map { |a| CollectionVerbs.as_items(a) }
|
|
301
|
+
return dv.of_null if extracted.any?(&:nil?) || extracted.empty?
|
|
302
|
+
min_len = extracted.map(&:length).min || 0
|
|
303
|
+
result = (0...min_len).map do |i|
|
|
304
|
+
dv.of_array(extracted.map { |arr| arr[i] })
|
|
275
305
|
end
|
|
276
306
|
dv.of_array(result)
|
|
277
307
|
}
|
|
278
308
|
|
|
279
309
|
registry["groupBy"] = ->(args, _ctx) {
|
|
310
|
+
return dv.of_null if args.length < 2
|
|
311
|
+
|
|
280
312
|
items = CollectionVerbs.extract_items(args[0])
|
|
281
313
|
field = args[1]&.to_string || ""
|
|
282
314
|
groups = {}
|
|
315
|
+
order = []
|
|
283
316
|
items.each do |item|
|
|
284
317
|
key = if item.object?
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
groups
|
|
318
|
+
(item.get(field) || dv.of_null).to_string
|
|
319
|
+
else
|
|
320
|
+
item.to_string
|
|
321
|
+
end
|
|
322
|
+
unless groups.key?(key)
|
|
323
|
+
groups[key] = []
|
|
324
|
+
order << key
|
|
325
|
+
end
|
|
290
326
|
groups[key] << item
|
|
291
327
|
end
|
|
292
|
-
|
|
293
|
-
|
|
328
|
+
result = order.map do |key|
|
|
329
|
+
dv.of_object({ "key" => dv.of_string(key), "items" => dv.of_array(groups[key]) })
|
|
330
|
+
end
|
|
331
|
+
dv.of_array(result)
|
|
294
332
|
}
|
|
295
333
|
|
|
296
334
|
registry["partition"] = ->(args, _ctx) {
|
|
335
|
+
return dv.of_null if args.length < 4
|
|
336
|
+
|
|
297
337
|
items = CollectionVerbs.extract_items(args[0])
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
pass_items, fail_items = items.partition(&:truthy?)
|
|
303
|
-
end
|
|
338
|
+
field = args[1]&.to_string || ""
|
|
339
|
+
op = args[2]&.to_string || "="
|
|
340
|
+
compare = args[3]
|
|
341
|
+
pass_items, fail_items = items.partition { |item| CollectionVerbs.matches_condition?(item, field, op, compare) }
|
|
304
342
|
dv.of_array([dv.of_array(pass_items), dv.of_array(fail_items)])
|
|
305
343
|
}
|
|
306
344
|
|
|
307
345
|
registry["take"] = ->(args, _ctx) {
|
|
308
|
-
items = CollectionVerbs.
|
|
346
|
+
items = CollectionVerbs.as_items(args[0])
|
|
309
347
|
n = NumericVerbs.to_double(args[1])&.to_i || 0
|
|
310
|
-
dv.
|
|
348
|
+
return dv.of_null if items.nil? || n < 0
|
|
349
|
+
dv.of_array(items.first(n))
|
|
311
350
|
}
|
|
312
351
|
registry["limit"] = registry["take"]
|
|
313
352
|
|
|
314
353
|
registry["drop"] = ->(args, _ctx) {
|
|
315
|
-
items = CollectionVerbs.
|
|
354
|
+
items = CollectionVerbs.as_items(args[0])
|
|
316
355
|
n = NumericVerbs.to_double(args[1])&.to_i || 0
|
|
317
|
-
dv.
|
|
356
|
+
return dv.of_null if items.nil? || n < 0
|
|
357
|
+
dv.of_array(items.drop(n))
|
|
318
358
|
}
|
|
319
359
|
|
|
320
360
|
registry["chunk"] = ->(args, _ctx) {
|
|
321
|
-
items = CollectionVerbs.
|
|
361
|
+
items = CollectionVerbs.as_items(args[0])
|
|
322
362
|
size = NumericVerbs.to_double(args[1])&.to_i || 1
|
|
323
|
-
|
|
363
|
+
return dv.of_null if items.nil? || size < 1
|
|
324
364
|
chunks = items.each_slice(size).map { |c| dv.of_array(c) }
|
|
325
365
|
dv.of_array(chunks)
|
|
326
366
|
}
|
|
@@ -360,21 +400,24 @@ module Odin
|
|
|
360
400
|
}
|
|
361
401
|
|
|
362
402
|
registry["compact"] = ->(args, _ctx) {
|
|
363
|
-
items = CollectionVerbs.
|
|
403
|
+
items = CollectionVerbs.as_items(args[0])
|
|
404
|
+
return dv.of_null if items.nil?
|
|
364
405
|
dv.of_array(items.reject { |item| item.null? || (item.string? && item.value.empty?) })
|
|
365
406
|
}
|
|
366
407
|
|
|
367
|
-
registry["rowNumber"] = ->(args,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
dv.of_integer(1)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
408
|
+
registry["rowNumber"] = ->(args, _ctx) {
|
|
409
|
+
return dv.of_null if args.empty?
|
|
410
|
+
|
|
411
|
+
items = CollectionVerbs.extract_items(args[0])
|
|
412
|
+
result = items.each_with_index.map do |item, i|
|
|
413
|
+
num = dv.of_integer(i + 1)
|
|
414
|
+
if item.object?
|
|
415
|
+
dv.of_object({ "_rowNum" => num }.merge(item.value))
|
|
416
|
+
else
|
|
417
|
+
dv.of_object({ "_rowNum" => num, "value" => item })
|
|
418
|
+
end
|
|
377
419
|
end
|
|
420
|
+
dv.of_array(result)
|
|
378
421
|
}
|
|
379
422
|
|
|
380
423
|
registry["sample"] = ->(args, _ctx) {
|
|
@@ -407,7 +450,8 @@ module Odin
|
|
|
407
450
|
}
|
|
408
451
|
|
|
409
452
|
registry["dedupe"] = ->(args, _ctx) {
|
|
410
|
-
items = CollectionVerbs.
|
|
453
|
+
items = CollectionVerbs.as_items(args[0])
|
|
454
|
+
return dv.of_null if items.nil?
|
|
411
455
|
field = args[1]&.to_string
|
|
412
456
|
result = []
|
|
413
457
|
last_key = nil
|
|
@@ -426,7 +470,8 @@ module Odin
|
|
|
426
470
|
}
|
|
427
471
|
|
|
428
472
|
registry["cumsum"] = ->(args, _ctx) {
|
|
429
|
-
items = CollectionVerbs.
|
|
473
|
+
items = CollectionVerbs.as_items(args[0])
|
|
474
|
+
return dv.of_null if items.nil?
|
|
430
475
|
sum = 0.0
|
|
431
476
|
result = items.map do |item|
|
|
432
477
|
n = NumericVerbs.to_double(item)
|
|
@@ -441,7 +486,8 @@ module Odin
|
|
|
441
486
|
}
|
|
442
487
|
|
|
443
488
|
registry["cumprod"] = ->(args, _ctx) {
|
|
444
|
-
items = CollectionVerbs.
|
|
489
|
+
items = CollectionVerbs.as_items(args[0])
|
|
490
|
+
return dv.of_null if items.nil?
|
|
445
491
|
prod = 1.0
|
|
446
492
|
result = items.map do |item|
|
|
447
493
|
n = NumericVerbs.to_double(item)
|
|
@@ -456,7 +502,8 @@ module Odin
|
|
|
456
502
|
}
|
|
457
503
|
|
|
458
504
|
registry["diff"] = ->(args, _ctx) {
|
|
459
|
-
items = CollectionVerbs.
|
|
505
|
+
items = CollectionVerbs.as_items(args[0])
|
|
506
|
+
return dv.of_null if items.nil?
|
|
460
507
|
lag = args[1] ? (NumericVerbs.to_double(args[1])&.to_i || 1) : 1
|
|
461
508
|
result = items.each_with_index.map do |item, i|
|
|
462
509
|
if i < lag
|
|
@@ -475,7 +522,8 @@ module Odin
|
|
|
475
522
|
}
|
|
476
523
|
|
|
477
524
|
registry["pctChange"] = ->(args, _ctx) {
|
|
478
|
-
items = CollectionVerbs.
|
|
525
|
+
items = CollectionVerbs.as_items(args[0])
|
|
526
|
+
return dv.of_null if items.nil?
|
|
479
527
|
lag = args[1] ? (NumericVerbs.to_double(args[1])&.to_i || 1) : 1
|
|
480
528
|
result = items.each_with_index.map do |item, i|
|
|
481
529
|
if i < lag
|
|
@@ -486,7 +534,7 @@ module Odin
|
|
|
486
534
|
if curr.nil? || prev.nil? || prev == 0.0
|
|
487
535
|
dv.of_null
|
|
488
536
|
else
|
|
489
|
-
|
|
537
|
+
NumericVerbs.numeric_result((curr - prev) / prev)
|
|
490
538
|
end
|
|
491
539
|
end
|
|
492
540
|
end
|
|
@@ -528,40 +576,62 @@ module Odin
|
|
|
528
576
|
}
|
|
529
577
|
|
|
530
578
|
registry["rank"] = ->(args, _ctx) {
|
|
579
|
+
return dv.of_null if args.empty?
|
|
580
|
+
|
|
531
581
|
items = CollectionVerbs.extract_items(args[0])
|
|
532
|
-
field = args[1]&.to_string
|
|
533
|
-
|
|
582
|
+
field = args.length > 1 ? args[1]&.to_string : nil
|
|
583
|
+
field = nil if field&.empty?
|
|
584
|
+
direction = (args.length > 2 ? args[2]&.to_string : "desc").to_s.downcase
|
|
534
585
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
586
|
+
comparable = ->(item) do
|
|
587
|
+
raw = field && item.object? ? item.get(field) : item
|
|
588
|
+
num = NumericVerbs.to_double(raw)
|
|
589
|
+
num.nil? ? (raw.nil? ? "" : raw.to_string) : num
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
indexed = items.each_index.map { |i| [i, comparable.call(items[i])] }
|
|
593
|
+
mult = direction == "asc" ? 1 : -1
|
|
594
|
+
sorted = indexed.sort do |a, b|
|
|
595
|
+
av = a[1]
|
|
596
|
+
bv = b[1]
|
|
597
|
+
if av.is_a?(Numeric) && bv.is_a?(Numeric)
|
|
598
|
+
(av <=> bv) * mult
|
|
538
599
|
else
|
|
539
|
-
|
|
600
|
+
(av.to_s <=> bv.to_s) * mult
|
|
540
601
|
end
|
|
541
602
|
end
|
|
542
603
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
604
|
+
ranks = {}
|
|
605
|
+
current_rank = 1
|
|
606
|
+
sorted.each_with_index do |(orig_idx, val), i|
|
|
607
|
+
current_rank = i + 1 if i.positive? && val != sorted[i - 1][1]
|
|
608
|
+
ranks[orig_idx] = current_rank
|
|
609
|
+
end
|
|
548
610
|
|
|
549
|
-
result =
|
|
550
|
-
|
|
611
|
+
result = items.each_with_index.map do |item, i|
|
|
612
|
+
rank_val = dv.of_integer(ranks[i] || (i + 1))
|
|
613
|
+
if item.object?
|
|
614
|
+
dv.of_object({ "_rank" => rank_val }.merge(item.value))
|
|
615
|
+
else
|
|
616
|
+
dv.of_object({ "_rank" => rank_val, "value" => item })
|
|
617
|
+
end
|
|
551
618
|
end
|
|
552
619
|
dv.of_array(result)
|
|
553
620
|
}
|
|
554
621
|
|
|
555
622
|
registry["fillMissing"] = ->(args, _ctx) {
|
|
623
|
+
return dv.of_null if args.empty?
|
|
624
|
+
|
|
556
625
|
items = CollectionVerbs.extract_items(args[0])
|
|
557
|
-
|
|
558
|
-
|
|
626
|
+
fill_val = args.length >= 2 ? args[1] : dv.of_null
|
|
627
|
+
strategy = (args.length >= 3 ? args[2]&.to_string : "value").to_s.downcase
|
|
628
|
+
nullish = ->(item) { item.nil? || item.null? }
|
|
559
629
|
|
|
560
630
|
case strategy
|
|
561
631
|
when "forward"
|
|
562
|
-
last_val =
|
|
632
|
+
last_val = fill_val
|
|
563
633
|
result = items.map do |item|
|
|
564
|
-
if item
|
|
634
|
+
if nullish.call(item)
|
|
565
635
|
last_val
|
|
566
636
|
else
|
|
567
637
|
last_val = item
|
|
@@ -569,17 +639,22 @@ module Odin
|
|
|
569
639
|
end
|
|
570
640
|
end
|
|
571
641
|
when "backward"
|
|
572
|
-
last_val =
|
|
642
|
+
last_val = fill_val
|
|
573
643
|
result = items.reverse.map do |item|
|
|
574
|
-
if item
|
|
644
|
+
if nullish.call(item)
|
|
575
645
|
last_val
|
|
576
646
|
else
|
|
577
647
|
last_val = item
|
|
578
648
|
item
|
|
579
649
|
end
|
|
580
650
|
end.reverse
|
|
651
|
+
when "mean"
|
|
652
|
+
nums = items.reject { |i| nullish.call(i) }.filter_map { |i| NumericVerbs.to_double(i) }
|
|
653
|
+
mean = nums.empty? ? 0.0 : nums.inject(0.0) { |s, v| s + v } / nums.length
|
|
654
|
+
mean_val = dv.of_float(mean)
|
|
655
|
+
result = items.map { |item| nullish.call(item) ? mean_val : item }
|
|
581
656
|
else
|
|
582
|
-
result = items.map { |item| item
|
|
657
|
+
result = items.map { |item| nullish.call(item) ? fill_val : item }
|
|
583
658
|
end
|
|
584
659
|
dv.of_array(result)
|
|
585
660
|
}
|