forthic 0.1.0 → 0.3.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/README.md +314 -14
- data/Rakefile +37 -8
- data/lib/forthic/decorators/docs.rb +69 -0
- data/lib/forthic/decorators/word.rb +331 -0
- data/lib/forthic/errors.rb +270 -0
- data/lib/forthic/grpc/client.rb +223 -0
- data/lib/forthic/grpc/errors.rb +149 -0
- data/lib/forthic/grpc/forthic_runtime_pb.rb +32 -0
- data/lib/forthic/grpc/forthic_runtime_services_pb.rb +31 -0
- data/lib/forthic/grpc/remote_module.rb +120 -0
- data/lib/forthic/grpc/remote_runtime_module.rb +148 -0
- data/lib/forthic/grpc/remote_word.rb +91 -0
- data/lib/forthic/grpc/runtime_manager.rb +60 -0
- data/lib/forthic/grpc/serializer.rb +184 -0
- data/lib/forthic/grpc/server.rb +361 -0
- data/lib/forthic/interpreter.rb +682 -133
- data/lib/forthic/literals.rb +170 -0
- data/lib/forthic/module.rb +383 -0
- data/lib/forthic/modules/standard/array_module.rb +940 -0
- data/lib/forthic/modules/standard/boolean_module.rb +176 -0
- data/lib/forthic/modules/standard/core_module.rb +362 -0
- data/lib/forthic/modules/standard/datetime_module.rb +349 -0
- data/lib/forthic/modules/standard/json_module.rb +55 -0
- data/lib/forthic/modules/standard/math_module.rb +365 -0
- data/lib/forthic/modules/standard/record_module.rb +203 -0
- data/lib/forthic/modules/standard/string_module.rb +170 -0
- data/lib/forthic/tokenizer.rb +225 -78
- data/lib/forthic/utils.rb +35 -0
- data/lib/forthic/websocket/handler.rb +548 -0
- data/lib/forthic/websocket/serializer.rb +160 -0
- data/lib/forthic/word_options.rb +141 -0
- data/lib/forthic.rb +30 -20
- data/protos/README.md +43 -0
- data/protos/v1/forthic_runtime.proto +200 -0
- metadata +76 -39
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/Guardfile +0 -42
- data/lib/forthic/code_location.rb +0 -20
- data/lib/forthic/forthic_error.rb +0 -51
- data/lib/forthic/forthic_module.rb +0 -145
- data/lib/forthic/global_module.rb +0 -2341
- data/lib/forthic/positioned_string.rb +0 -19
- data/lib/forthic/token.rb +0 -38
- data/lib/forthic/variable.rb +0 -34
- data/lib/forthic/version.rb +0 -5
- data/lib/forthic/words/definition_word.rb +0 -40
- data/lib/forthic/words/end_array_word.rb +0 -28
- data/lib/forthic/words/end_module_word.rb +0 -16
- data/lib/forthic/words/imported_word.rb +0 -27
- data/lib/forthic/words/map_word.rb +0 -169
- data/lib/forthic/words/module_memo_bang_at_word.rb +0 -22
- data/lib/forthic/words/module_memo_bang_word.rb +0 -21
- data/lib/forthic/words/module_memo_word.rb +0 -35
- data/lib/forthic/words/module_word.rb +0 -21
- data/lib/forthic/words/push_value_word.rb +0 -21
- data/lib/forthic/words/start_module_word.rb +0 -31
- data/lib/forthic/words/word.rb +0 -30
- data/sig/forthic.rbs +0 -4
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../decorators/word'
|
|
4
|
+
|
|
5
|
+
module Forthic
|
|
6
|
+
module Modules
|
|
7
|
+
# MathModule - Mathematical operations and utilities
|
|
8
|
+
#
|
|
9
|
+
# Provides arithmetic operations, aggregation functions, type conversions,
|
|
10
|
+
# and common mathematical functions.
|
|
11
|
+
class MathModule < Decorators::DecoratedModule
|
|
12
|
+
# Register module documentation
|
|
13
|
+
module_doc <<~DOC
|
|
14
|
+
Mathematical operations and utilities including arithmetic, aggregation, and conversions.
|
|
15
|
+
|
|
16
|
+
## Categories
|
|
17
|
+
- Arithmetic: +, -, *, /, ADD, SUBTRACT, MULTIPLY, DIVIDE, MOD
|
|
18
|
+
- Aggregates: MEAN, MAX, MIN, SUM
|
|
19
|
+
- Type conversion: >INT, >FLOAT, >FIXED, ROUND
|
|
20
|
+
- Special values: INFINITY, UNIFORM-RANDOM
|
|
21
|
+
- Math functions: ABS, SQRT, FLOOR, CEIL, CLAMP
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
5 3 +
|
|
25
|
+
[1 2 3 4] SUM
|
|
26
|
+
[10 20 30] MEAN
|
|
27
|
+
3.7 ROUND
|
|
28
|
+
0 100 UNIFORM-RANDOM
|
|
29
|
+
DOC
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
super("math")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ========================================
|
|
36
|
+
# Arithmetic Operations
|
|
37
|
+
# ========================================
|
|
38
|
+
|
|
39
|
+
forthic_direct_word :plus, "( a:number b:number -- sum:number ) OR ( numbers:number[] -- sum:number )", "Add two numbers or sum array", "+"
|
|
40
|
+
def plus(interp)
|
|
41
|
+
b = interp.stack_pop
|
|
42
|
+
|
|
43
|
+
# Case 1: Array on top of stack
|
|
44
|
+
if b.is_a?(Array)
|
|
45
|
+
result = 0
|
|
46
|
+
b.each do |num|
|
|
47
|
+
result += num unless num.nil?
|
|
48
|
+
end
|
|
49
|
+
interp.stack_push(result)
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Case 2: Two numbers
|
|
54
|
+
a = interp.stack_pop
|
|
55
|
+
num_a = a.nil? ? 0 : a
|
|
56
|
+
num_b = b.nil? ? 0 : b
|
|
57
|
+
interp.stack_push(num_a + num_b)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
forthic_direct_word :plus_ADD, "( a:number b:number -- sum:number ) OR ( numbers:number[] -- sum:number )", "Add two numbers or sum array", "ADD"
|
|
61
|
+
def plus_ADD(interp)
|
|
62
|
+
plus(interp)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
forthic_word :minus, "( a:number b:number -- difference:number )", "Subtract b from a", "-"
|
|
66
|
+
def minus(a, b)
|
|
67
|
+
return nil if a.nil? || b.nil?
|
|
68
|
+
a - b
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
forthic_word :minus_SUBTRACT, "( a:number b:number -- difference:number )", "Subtract b from a", "SUBTRACT"
|
|
72
|
+
def minus_SUBTRACT(a, b)
|
|
73
|
+
return nil if a.nil? || b.nil?
|
|
74
|
+
a - b
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
forthic_direct_word :times, "( a:number b:number -- product:number ) OR ( numbers:number[] -- product:number )", "Multiply two numbers or product of array", "*"
|
|
78
|
+
def times(interp)
|
|
79
|
+
b = interp.stack_pop
|
|
80
|
+
|
|
81
|
+
# Case 1: Array on top of stack
|
|
82
|
+
if b.is_a?(Array)
|
|
83
|
+
result = 1
|
|
84
|
+
b.each do |num|
|
|
85
|
+
if num.nil?
|
|
86
|
+
interp.stack_push(nil)
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
result *= num
|
|
90
|
+
end
|
|
91
|
+
interp.stack_push(result)
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Case 2: Two numbers
|
|
96
|
+
a = interp.stack_pop
|
|
97
|
+
if a.nil? || b.nil?
|
|
98
|
+
interp.stack_push(nil)
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
interp.stack_push(a * b)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
forthic_direct_word :times_MULTIPLY, "( a:number b:number -- product:number ) OR ( numbers:number[] -- product:number )", "Multiply two numbers or product of array", "MULTIPLY"
|
|
105
|
+
def times_MULTIPLY(interp)
|
|
106
|
+
times(interp)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
forthic_word :divide_by, "( a:number b:number -- quotient:number )", "Divide a by b", "/"
|
|
110
|
+
def divide_by(a, b)
|
|
111
|
+
return nil if a.nil? || b.nil?
|
|
112
|
+
return nil if b == 0
|
|
113
|
+
a.to_f / b
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
forthic_word :divide_by_DIVIDE, "( a:number b:number -- quotient:number )", "Divide a by b", "DIVIDE"
|
|
117
|
+
def divide_by_DIVIDE(a, b)
|
|
118
|
+
return nil if a.nil? || b.nil?
|
|
119
|
+
return nil if b == 0
|
|
120
|
+
a.to_f / b
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
forthic_word :MOD, "( m:number n:number -- remainder:number )", "Modulo operation (m % n)"
|
|
124
|
+
def MOD(m, n)
|
|
125
|
+
return nil if m.nil? || n.nil?
|
|
126
|
+
m % n
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# ========================================
|
|
130
|
+
# Aggregation Operations
|
|
131
|
+
# ========================================
|
|
132
|
+
|
|
133
|
+
forthic_word :MEAN, "( items:any[] -- mean:any )", "Calculate mean of array (handles numbers, strings, objects)"
|
|
134
|
+
def MEAN(items)
|
|
135
|
+
if items.nil? || (items.is_a?(Array) && items.empty?)
|
|
136
|
+
return 0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
return items unless items.is_a?(Array)
|
|
140
|
+
|
|
141
|
+
return items[0] if items.length == 1
|
|
142
|
+
|
|
143
|
+
# Filter out nil values
|
|
144
|
+
filtered = items.reject(&:nil?)
|
|
145
|
+
|
|
146
|
+
return 0 if filtered.empty?
|
|
147
|
+
|
|
148
|
+
# Check type of first non-null item
|
|
149
|
+
first = filtered[0]
|
|
150
|
+
|
|
151
|
+
# Case 1: Numbers
|
|
152
|
+
if first.is_a?(Numeric)
|
|
153
|
+
sum = filtered.reduce(0) { |acc, val| acc + val }
|
|
154
|
+
return sum.to_f / filtered.length
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Case 2: Strings - return frequency distribution
|
|
158
|
+
if first.is_a?(String)
|
|
159
|
+
counts = {}
|
|
160
|
+
filtered.each do |item|
|
|
161
|
+
counts[item] = (counts[item] || 0) + 1
|
|
162
|
+
end
|
|
163
|
+
result = {}
|
|
164
|
+
counts.each do |key, count|
|
|
165
|
+
result[key] = count.to_f / filtered.length
|
|
166
|
+
end
|
|
167
|
+
return result
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Case 3: Objects - field-wise mean
|
|
171
|
+
if first.is_a?(Hash)
|
|
172
|
+
result = {}
|
|
173
|
+
all_keys = Set.new
|
|
174
|
+
|
|
175
|
+
# Collect all keys
|
|
176
|
+
filtered.each do |obj|
|
|
177
|
+
obj.each_key { |key| all_keys.add(key) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Compute mean for each key
|
|
181
|
+
all_keys.each do |key|
|
|
182
|
+
values = filtered.map { |obj| obj[key] }.reject(&:nil?)
|
|
183
|
+
|
|
184
|
+
next if values.empty?
|
|
185
|
+
|
|
186
|
+
first_val = values[0]
|
|
187
|
+
|
|
188
|
+
if first_val.is_a?(Numeric)
|
|
189
|
+
sum = values.reduce(0) { |acc, val| acc + val }
|
|
190
|
+
result[key] = sum.to_f / values.length
|
|
191
|
+
elsif first_val.is_a?(String)
|
|
192
|
+
counts = {}
|
|
193
|
+
values.each do |val|
|
|
194
|
+
counts[val] = (counts[val] || 0) + 1
|
|
195
|
+
end
|
|
196
|
+
freqs = {}
|
|
197
|
+
counts.each do |k, count|
|
|
198
|
+
freqs[k] = count.to_f / values.length
|
|
199
|
+
end
|
|
200
|
+
result[key] = freqs
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
0
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
forthic_direct_word :MAX, "( a:number b:number -- max:number ) OR ( items:number[] -- max:number )", "Maximum of two numbers or array"
|
|
211
|
+
def MAX(interp)
|
|
212
|
+
b = interp.stack_pop
|
|
213
|
+
|
|
214
|
+
# Case 1: Array on top of stack
|
|
215
|
+
if b.is_a?(Array)
|
|
216
|
+
if b.empty?
|
|
217
|
+
interp.stack_push(nil)
|
|
218
|
+
return
|
|
219
|
+
end
|
|
220
|
+
interp.stack_push(b.max)
|
|
221
|
+
return
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Case 2: Two values
|
|
225
|
+
a = interp.stack_pop
|
|
226
|
+
interp.stack_push([a, b].max)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
forthic_direct_word :MIN, "( a:number b:number -- min:number ) OR ( items:number[] -- min:number )", "Minimum of two numbers or array"
|
|
230
|
+
def MIN(interp)
|
|
231
|
+
b = interp.stack_pop
|
|
232
|
+
|
|
233
|
+
# Case 1: Array on top of stack
|
|
234
|
+
if b.is_a?(Array)
|
|
235
|
+
if b.empty?
|
|
236
|
+
interp.stack_push(nil)
|
|
237
|
+
return
|
|
238
|
+
end
|
|
239
|
+
interp.stack_push(b.min)
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Case 2: Two values
|
|
244
|
+
a = interp.stack_pop
|
|
245
|
+
interp.stack_push([a, b].min)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
forthic_word :SUM, "( numbers:number[] -- sum:number )", "Sum of array (explicit)"
|
|
249
|
+
def SUM(numbers)
|
|
250
|
+
return 0 if numbers.nil? || !numbers.is_a?(Array)
|
|
251
|
+
|
|
252
|
+
result = 0
|
|
253
|
+
numbers.each do |num|
|
|
254
|
+
result += num unless num.nil?
|
|
255
|
+
end
|
|
256
|
+
result
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# ========================================
|
|
260
|
+
# Type Conversion
|
|
261
|
+
# ========================================
|
|
262
|
+
|
|
263
|
+
forthic_word :to_INT, "( a:any -- int:number )", "Convert to integer (returns length for arrays/objects, 0 for null)", ">INT"
|
|
264
|
+
def to_INT(a)
|
|
265
|
+
return 0 if a.nil?
|
|
266
|
+
|
|
267
|
+
if a.is_a?(Array)
|
|
268
|
+
return a.length
|
|
269
|
+
elsif a.is_a?(Hash)
|
|
270
|
+
return a.keys.length
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
Float(a).truncate
|
|
275
|
+
rescue StandardError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
forthic_word :to_FLOAT, "( a:any -- float:number )", "Convert to float", ">FLOAT"
|
|
281
|
+
def to_FLOAT(a)
|
|
282
|
+
return 0.0 if a.nil?
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
Float(a)
|
|
286
|
+
rescue StandardError
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
forthic_word :to_FIXED, "( num:number digits:number -- result:string )", "Format number with fixed decimal places", ">FIXED"
|
|
292
|
+
def to_FIXED(num, digits)
|
|
293
|
+
return nil if num.nil?
|
|
294
|
+
|
|
295
|
+
format("%.#{digits}f", num)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
forthic_word :ROUND, "( num:number -- int:number )", "Round to nearest integer"
|
|
299
|
+
def ROUND(num)
|
|
300
|
+
return nil if num.nil?
|
|
301
|
+
|
|
302
|
+
num.round
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# ========================================
|
|
306
|
+
# Special Values
|
|
307
|
+
# ========================================
|
|
308
|
+
|
|
309
|
+
forthic_word :INFINITY, "( -- infinity:number )", "Push Infinity value"
|
|
310
|
+
def INFINITY
|
|
311
|
+
Float::INFINITY
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
forthic_word :UNIFORM_RANDOM, "( low:number high:number -- random:number )", "Generate random number in range [low, high)", "UNIFORM-RANDOM"
|
|
315
|
+
def UNIFORM_RANDOM(low, high)
|
|
316
|
+
rand * (high - low) + low
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# ========================================
|
|
320
|
+
# Math Functions
|
|
321
|
+
# ========================================
|
|
322
|
+
|
|
323
|
+
forthic_direct_word :ABS, "( n:number -- abs:number )", "Absolute value"
|
|
324
|
+
def ABS(interp)
|
|
325
|
+
n = interp.stack_pop
|
|
326
|
+
result = n.nil? ? nil : n.abs
|
|
327
|
+
interp.stack_push(result)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
forthic_direct_word :SQRT, "( n:number -- sqrt:number )", "Square root"
|
|
331
|
+
def SQRT(interp)
|
|
332
|
+
n = interp.stack_pop
|
|
333
|
+
result = n.nil? ? nil : Math.sqrt(n)
|
|
334
|
+
interp.stack_push(result)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
forthic_direct_word :FLOOR, "( n:number -- floor:number )", "Round down to integer"
|
|
338
|
+
def FLOOR(interp)
|
|
339
|
+
n = interp.stack_pop
|
|
340
|
+
result = n.nil? ? nil : n.floor
|
|
341
|
+
interp.stack_push(result)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
forthic_direct_word :CEIL, "( n:number -- ceil:number )", "Round up to integer"
|
|
345
|
+
def CEIL(interp)
|
|
346
|
+
n = interp.stack_pop
|
|
347
|
+
result = n.nil? ? nil : n.ceil
|
|
348
|
+
interp.stack_push(result)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
forthic_direct_word :CLAMP, "( value:number min:number max:number -- clamped:number )", "Constrain value to range [min, max]"
|
|
352
|
+
def CLAMP(interp)
|
|
353
|
+
max = interp.stack_pop
|
|
354
|
+
min = interp.stack_pop
|
|
355
|
+
value = interp.stack_pop
|
|
356
|
+
result = if value.nil? || min.nil? || max.nil?
|
|
357
|
+
nil
|
|
358
|
+
else
|
|
359
|
+
[[min, value].max, max].min
|
|
360
|
+
end
|
|
361
|
+
interp.stack_push(result)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../decorators/word'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Forthic
|
|
7
|
+
module Modules
|
|
8
|
+
# RecordModule - Record (object/dictionary) manipulation operations
|
|
9
|
+
#
|
|
10
|
+
# Provides operations for working with key-value data structures (hashes).
|
|
11
|
+
class RecordModule < Decorators::DecoratedModule
|
|
12
|
+
# Register module documentation
|
|
13
|
+
module_doc <<~DOC
|
|
14
|
+
Record (object/dictionary) manipulation operations for working with key-value data structures.
|
|
15
|
+
|
|
16
|
+
## Categories
|
|
17
|
+
- Core: REC, REC@, |REC@, <REC!
|
|
18
|
+
- Transform: RELABEL, INVERT-KEYS, REC-DEFAULTS, <DEL
|
|
19
|
+
- Access: KEYS, VALUES
|
|
20
|
+
DOC
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
super("record")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Helper function to drill down into nested record structure
|
|
27
|
+
#
|
|
28
|
+
# @param record [Hash] The record to drill into
|
|
29
|
+
# @param fields [Array<String>] Array of field names to traverse
|
|
30
|
+
# @return [Any] The value at the end of the field path, or nil if not found
|
|
31
|
+
def self.drill_for_value(record, fields)
|
|
32
|
+
result = record
|
|
33
|
+
fields.each do |field|
|
|
34
|
+
return nil if result.nil?
|
|
35
|
+
result = result[field]
|
|
36
|
+
end
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
forthic_word :REC, "( key_vals:any[] -- rec:any )", "Create record from [[key, val], ...] pairs"
|
|
41
|
+
def REC(key_vals)
|
|
42
|
+
_key_vals = key_vals || []
|
|
43
|
+
|
|
44
|
+
result = {}
|
|
45
|
+
_key_vals.each do |pair|
|
|
46
|
+
key = nil
|
|
47
|
+
val = nil
|
|
48
|
+
if pair
|
|
49
|
+
key = pair[0] if pair.length >= 1
|
|
50
|
+
val = pair[1] if pair.length >= 2
|
|
51
|
+
end
|
|
52
|
+
result[key] = val
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
forthic_word :REC_at, "( rec:any field:any -- value:any )", "Get value from record by field or array of fields", "REC@"
|
|
59
|
+
def REC_at(rec, field)
|
|
60
|
+
return nil unless rec
|
|
61
|
+
|
|
62
|
+
fields = field.is_a?(Array) ? field : [field]
|
|
63
|
+
|
|
64
|
+
self.class.drill_for_value(rec, fields)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
forthic_word :pipe_REC_at, "( records:any field:any -- values:any )", "Map REC@ over array of records", "|REC@"
|
|
68
|
+
def pipe_REC_at(records, field)
|
|
69
|
+
# Push records back and field, then use MAP with REC@
|
|
70
|
+
interp.stack_push(records)
|
|
71
|
+
interp.run("'#{field.to_json} REC@' MAP")
|
|
72
|
+
nil # Result already on stack from MAP
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
forthic_word :l_REC_bang, "( rec:any value:any field:any -- rec:any )", "Set value in record at field path", "<REC!"
|
|
76
|
+
def l_REC_bang(rec, value, field)
|
|
77
|
+
_rec = rec || {}
|
|
78
|
+
|
|
79
|
+
fields = field.is_a?(Array) ? field : [field]
|
|
80
|
+
|
|
81
|
+
# Helper to ensure field exists
|
|
82
|
+
ensure_field = lambda do |record, field_name|
|
|
83
|
+
res = record[field_name]
|
|
84
|
+
if res.nil?
|
|
85
|
+
res = {}
|
|
86
|
+
record[field_name] = res
|
|
87
|
+
end
|
|
88
|
+
res
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
cur_rec = _rec
|
|
92
|
+
# Drill down up until the last value
|
|
93
|
+
(0...fields.length - 1).each do |i|
|
|
94
|
+
cur_rec = ensure_field.call(cur_rec, fields[i])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Set the value at the right depth within rec
|
|
98
|
+
cur_rec[fields.last] = value
|
|
99
|
+
|
|
100
|
+
_rec
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
forthic_word :RELABEL, "( container:any old_keys:any[] new_keys:any[] -- container:any )", "Rename record keys"
|
|
104
|
+
def RELABEL(container, old_keys, new_keys)
|
|
105
|
+
return container unless container
|
|
106
|
+
|
|
107
|
+
if old_keys.length != new_keys.length
|
|
108
|
+
raise StandardError, "RELABEL: old_keys and new_keys must be same length"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
new_to_old = {}
|
|
112
|
+
old_keys.each_with_index do |old_key, i|
|
|
113
|
+
new_to_old[new_keys[i]] = old_key
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if container.is_a?(Array)
|
|
117
|
+
result = []
|
|
118
|
+
new_to_old.keys.sort.each do |k|
|
|
119
|
+
result.push(container[new_to_old[k]])
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
result = {}
|
|
123
|
+
new_to_old.each do |new_key, old_key|
|
|
124
|
+
result[new_key] = container[old_key]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
forthic_word :INVERT_KEYS, "( record:any -- inverted:any )", "Invert two-level nested record structure", "INVERT-KEYS"
|
|
132
|
+
def INVERT_KEYS(record)
|
|
133
|
+
result = {}
|
|
134
|
+
record.each do |first_key, sub_record|
|
|
135
|
+
sub_record.each do |second_key, value|
|
|
136
|
+
result[second_key] ||= {}
|
|
137
|
+
result[second_key][first_key] = value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
result
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
forthic_word :REC_DEFAULTS, "( record:any key_vals:any[] -- record:any )", "Set default values for missing/empty fields", "REC-DEFAULTS"
|
|
145
|
+
def REC_DEFAULTS(record, key_vals)
|
|
146
|
+
key_vals.each do |key_val|
|
|
147
|
+
key = key_val[0]
|
|
148
|
+
value = record[key]
|
|
149
|
+
if value.nil? || value == ""
|
|
150
|
+
record[key] = key_val[1]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
record
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
forthic_word :l_DEL, "( container:any key:any -- container:any )", "Delete key from record or index from array", "<DEL"
|
|
158
|
+
def l_DEL(container, key)
|
|
159
|
+
return container unless container
|
|
160
|
+
|
|
161
|
+
if container.is_a?(Array)
|
|
162
|
+
container.delete_at(key)
|
|
163
|
+
else
|
|
164
|
+
container.delete(key)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
container
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
forthic_word :KEYS, "( container:any -- keys:any[] )", "Get keys from record or indices from array"
|
|
171
|
+
def KEYS(container)
|
|
172
|
+
_container = container || []
|
|
173
|
+
|
|
174
|
+
if _container.is_a?(Array)
|
|
175
|
+
result = []
|
|
176
|
+
_container.length.times do |i|
|
|
177
|
+
result.push(i)
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
result = _container.keys
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
forthic_word :VALUES, "( container:any -- values:any[] )", "Get values from record or elements from array"
|
|
187
|
+
def VALUES(container)
|
|
188
|
+
_container = container || []
|
|
189
|
+
|
|
190
|
+
if _container.is_a?(Array)
|
|
191
|
+
result = _container
|
|
192
|
+
else
|
|
193
|
+
result = []
|
|
194
|
+
_container.each_key do |k|
|
|
195
|
+
result.push(_container[k])
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
result
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|