• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mobalazs / rotor-framework / 18919729013

29 Oct 2025 07:21PM UTC coverage: 85.379% (-0.1%) from 85.479%
18919729013

push

github

mobalazs
fix: update debug setting in bsconfig to enable debugging

1781 of 2086 relevant lines covered (85.38%)

1.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.68
/src/source/utils/ArrayUtils.bs
1
namespace Rotor.Utils
2

3
    '==========================================================================
4
    ' Array and AssociativeArray Utility Functions
5
    '
6
    ' Comprehensive collection of helper functions for array and associative array operations.
7
    '
8
    ' Categories:
9
    '   - Conversion: Array to hash, object wrapping
10
    '   - Deep Operations: Deep copy, deep extend, cloning
11
    '   - Path Resolution: Get/clone by key path
12
    '   - Filtering & Search: Filter, find in arrays/AAs
13
    '   - Comparison: Difference checking
14
    '   - Array Helpers: Ensure array, extend arrays, remove duplicates
15
    '   - HID Operations: Ancestor/descendant checks
16
    '   - Index Helpers: Wrapped index calculation
17
    '
18
    '==========================================================================
19

20
    '==========================================================================
21
    ' CONVERSION FUNCTIONS
22
    '==========================================================================
23

24
    ' ---------------------------------------------------------------------
25
    ' wrapObject - Wraps a value in an associative array with specified key
26
    '
27
    ' @param {string} key - Key name
28
    ' @param {dynamic} value - Value to wrap
29
    ' @returns {object} Associative array { key: value }
30
    '
31
    function wrapObject(key as string, value as dynamic) as object
32
        obj = {}
1✔
33
        obj[key] = value
1✔
34
        return obj
1✔
35
    end function
36

37
    '==========================================================================
38
    ' DEEP OPERATIONS
39
    '==========================================================================
40

41
    ' ---------------------------------------------------------------------
42
    ' deepExtendAA - Recursively merges source AA into target AA
43
    '
44
    ' Merge behavior:
45
    '   - Both AA: Recursive merge
46
    '   - Source overwrites other types
47
    '   - Skips widgets (isWidget = true)
48
    '
49
    ' @param {dynamic} target - Target associative array to extend
50
    ' @param {dynamic} source - Source associative array providing new values
51
    ' @returns {dynamic} Modified target associative array
52
    '
53
    function deepExtendAA(target as dynamic, source as dynamic) as dynamic
54
        if type(target) <> "roAssociativeArray" or type(source) <> "roAssociativeArray"
2✔
55
            return source
1✔
56
        end if
57

58
        for each key in source
1✔
59
            sourceVal = source[key]
1✔
60

61
            if target.doesExist(key)
2✔
62
                targetVal = target[key]
1✔
63
                sourceType = type(sourceVal)
1✔
64
                targetType = type(targetVal)
1✔
65

66
                ' Recursively merge nested AAs (but not widgets)
67
                if sourceType = "roAssociativeArray" and targetType = "roAssociativeArray" and not (sourceType = "roAssociativeArray" and sourceVal?.isWidget = true)
2✔
68
                    target[key] = deepExtendAA(targetVal, sourceVal)
1✔
69
                else
70
                    ' Overwrite with source value
3✔
71
                    target[key] = sourceVal
1✔
72
                end if
73
            else
74
                ' Key doesn't exist in target, assign directly
3✔
75
                target[key] = sourceVal
1✔
76
            end if
77
        end for
78

79
        return target
1✔
80
    end function
81

82
    ' ---------------------------------------------------------------------
83
    ' deepCopy - Creates a deep copy of an object
84
    '
85
    ' Recursively copies all nested arrays and associative arrays.
86
    ' Primitive values are copied by value.
87
    '
88
    ' @param {dynamic} source - Source object to copy
89
    ' @returns {dynamic} Deep copy of source
90
    '
91
    function deepCopy(source as dynamic) as dynamic
92
        if source = invalid
2✔
93
            return invalid
×
94
        end if
95

96
        sourceType = type(source)
1✔
97

98
        if sourceType = "roArray"
2✔
99
            target = []
1✔
100
            for each item in source
1✔
101
                target.push(deepCopy(item))
1✔
102
            end for
103
        else if sourceType = "roAssociativeArray"
2✔
104
            target = {}
1✔
105
            for each key in source
1✔
106
                target[key] = deepCopy(source[key])
1✔
107
            end for
108
        else
109
            ' Primitive type - return as-is
3✔
110
            return source
1✔
111
        end if
112

113
        return target
1✔
114
    end function
115

116
    ' ---------------------------------------------------------------------
117
    ' cloneExtendAA - Creates a deep copy of source and extends it with newData
118
    '
119
    ' Combines deepCopy and deepExtendAA operations.
120
    '
121
    ' @param {object} source - Source AA to clone
122
    ' @param {object} newData - Data to merge into clone
123
    ' @returns {object} Extended clone
124
    '
125
    function cloneExtendAA(source as object, newData as object)
126
        clone = deepCopy(source)
1✔
127
        return deepExtendAA(clone, newData)
1✔
128
    end function
129

130
    '==========================================================================
131
    ' PATH RESOLUTION
132
    '==========================================================================
133

134
    ' ---------------------------------------------------------------------
135
    ' getValueByKeyPath - Gets value from nested object using dot-separated path
136
    '
137
    ' Traverses nested AAs using path like "parent.child.grandchild".
138
    '
139
    ' @param {object} source - Source object to traverse
140
    ' @param {string} keyPath - Dot-separated path (e.g., "user.address.city")
141
    ' @param {boolean} lastKeyAsProp - If true, wraps result in { lastKey: value }
142
    ' @param {string} separator - Path separator (default: ".")
143
    ' @returns {dynamic} Value at path, or invalid if not found
144
    '
145
    function getValueByKeyPath(source as object, keyPath as string, lastKeyAsProp = false as boolean, separator = "." as string) as object
146
        keys = keyPath.split(separator)
1✔
147
        keysCount = keys.Count()
1✔
148
        current = source
1✔
149
        index = 0
1✔
150

151
        ' Traverse path
152
        while index < keysCount and current <> invalid and current.doesExist(keys[index])
1✔
153
            current = current[keys[index]]
1✔
154
            index++
1✔
155
        end while
156

157
        ' Not found
158
        if index < keysCount then return invalid
2✔
159

160
        ' Return format
161
        if lastKeyAsProp and current <> invalid
2✔
162
            obj = wrapObject(keys[keysCount - 1], current)
1✔
163
            return obj
1✔
164
        else
3✔
165
            return current
1✔
166
        end if
167
    end function
168

169
    ' ---------------------------------------------------------------------
170
    ' getCloneByKeyPath - Gets a deep copy of nested path structure
171
    '
172
    ' Creates a nested AA structure matching the path with deep copied leaf value.
173
    '
174
    ' @param {object} source - Source object
175
    ' @param {string} keyPath - Dot-separated path
176
    ' @param {string} separator - Path separator (default: ".")
177
    ' @returns {object} Nested structure with cloned value, or invalid if not found
178
    '
179
    ' Example:
180
    '   getCloneByKeyPath({a: {b: {c: 1}}}, "a.b.c") => {a: {b: {c: 1}}}
181
    '
182
    function getCloneByKeyPath(source as object, keyPath as string, separator = "." as string) as object
183
        keys = keyPath.split(separator)
1✔
184
        keysCount = keys.Count()
1✔
185
        current = source
1✔
186
        index = 0
1✔
187
        fullPath = {}
1✔
188
        path = fullPath
1✔
189

190
        while index < keysCount and current <> invalid and current.doesExist(keys[index])
1✔
191
            key = keys[index]
1✔
192
            current = current[key]
1✔
193

194
            if index = keysCount - 1
3✔
195
                ' Last key - deep copy value
196
                path[key] = Rotor.Utils.deepCopy(current)
1✔
197
            else
198
                ' Intermediate key - create nested object
3✔
199
                path[key] = {}
1✔
200
                path = path[key]
1✔
201
            end if
202

203
            index++
1✔
204
        end while
205

206
        ' Not found
207
        if index < keysCount then return invalid
2✔
208

209
        return fullPath
1✔
210
    end function
211

212
    '==========================================================================
213
    ' FILTERING & SEARCH
214
    '==========================================================================
215

216
    ' ---------------------------------------------------------------------
217
    ' filterArrayUseHandler - Filters array using handler function
218
    '
219
    ' @param {object} array - Array to filter
220
    ' @param {function} handler - Filter function (item, context) => boolean
221
    ' @param {dynamic} context - Context passed to handler
222
    ' @returns {object} New array with filtered elements
223
    '
224
    function filterArrayUseHandler(array as object, handler as function, context) as object
225
        index = 0
1✔
226
        newArray = []
1✔
227
        while index < array.Count()
1✔
228
            if handler(array[index], context)
2✔
229
                newArray.push(array[index])
1✔
230
            end if
231
            index++
1✔
232
        end while
233
        return newArray
1✔
234
    end function
235

236
    ' ---------------------------------------------------------------------
237
    ' findInArray - Finds target value in array
238
    '
239
    ' @param {object} array - Array to search
240
    ' @param {dynamic} target - Value to find
241
    ' @returns {integer} Index of target, or -1 if not found
242
    '
243
    function findInArray(array as object, target) as integer
244
        index = 0
1✔
245
        foundIndex = -1
1✔
246
        while foundIndex = -1 and index < array.Count()
1✔
247
            if array[index] = target
3✔
248
                foundIndex = index
1✔
249
            else
3✔
250
                index++
1✔
251
            end if
252
        end while
253
        return foundIndex
1✔
254
    end function
255

256
    ' ---------------------------------------------------------------------
257
    ' findInArrayOfAA - Finds AA in array by key value
258
    '
259
    ' Searches array of AAs for first element where element[key] = target.
260
    '
261
    ' @param {object} array - Array of associative arrays
262
    ' @param {string} key - Key to check in each AA
263
    ' @param {dynamic} target - Target value to find
264
    ' @returns {integer} Index of matching AA, or -1 if not found
265
    '
266
    function findInArrayOfAA(array as object, key as string, target) as integer
267
        index = 0
1✔
268
        foundIndex = -1
1✔
269
        while foundIndex = -1 and index < array.Count()
1✔
270
            if array[index][key] = target
2✔
271
                foundIndex = index
1✔
272
            else
3✔
273
                index++
1✔
274
            end if
275
        end while
276
        return foundIndex
1✔
277
    end function
278

279
    ' ---------------------------------------------------------------------
280
    ' checkArrayItemsByHandler - Finds array element matching handler condition
281
    '
282
    ' Compares array elements using handler function to find best match.
283
    '
284
    ' @param {object} array - Array to search
285
    ' @param {string} targetKey - Key to compare in each element
286
    ' @param {function} handlerFn - Comparison function (value1, value2) => boolean
287
    ' @returns {dynamic} Element matching condition
288
    '
289
    function checkArrayItemsByHandler(array as object, targetKey as string, handlerFn as function) as dynamic
290
        targetIndex = 0
1✔
291
        length = array.Count()
1✔
292

293
        if length = 1
2✔
294
            return array[0]
1✔
295
        end if
296

297
        for index = 1 to length - 1
1✔
298
            if handlerFn(array[index][targetKey], array[targetIndex][targetKey]) = true
2✔
299
                targetIndex = index
1✔
300
            end if
301
        end for
302

303
        return array[targetIndex]
1✔
304
    end function
305

306
    ' ---------------------------------------------------------------------
307
    ' findInAArrayByKey - Finds key in AA where nested value matches target
308
    '
309
    ' Searches AA where each value is an AA, looking for value[key] = target.
310
    ' Case-insensitive for string comparisons.
311
    '
312
    ' @param {object} aa - Associative array to search
313
    ' @param {string} key - Nested key to check
314
    ' @param {dynamic} value - Target value
315
    ' @returns {string} Key of matching entry, or empty string if not found
316
    '
317
    function findInAArrayByKey(aa as object, key as string, value as dynamic) as string
318
        keys = aa.Keys()
1✔
319
        keysCount = keys.Count()
320
        index = 0
1✔
321
        foundIndex = -1
1✔
322
        isTypeString = Rotor.Utils.isString(value)
1✔
323
        if isTypeString then value = LCase(value)
1✔
324

325
        while foundIndex = -1 and index < keysCount
1✔
326
            targetValue = aa[keys[index]][key]
1✔
327
            if (isTypeString = true ? LCase(targetValue) : targetValue) = value
3✔
328
                foundIndex = index
1✔
329
            else
3✔
330
                index++
1✔
331
            end if
332
        end while
333

334
        return foundIndex > -1 ? keys[foundIndex] : ""
1✔
335
    end function
336

337
    ' ---------------------------------------------------------------------
338
    ' findInArrayByKey - Finds element in array by nested key path
339
    '
340
    ' Searches array of AAs using key path (e.g., "user.name").
341
    ' Case-insensitive for string comparisons.
342
    '
343
    ' @param {object} array - Array to search (elements must be AAs)
344
    ' @param {string} key - Key path to search (e.g., "parent.child")
345
    ' @param {dynamic} value - Target value
346
    ' @returns {integer} Index of matching element, or -1 if not found
347
    '
348
    function findInArrayByKey(array as object, key as string, value as dynamic) as integer
349
        arrayCount = array.Count()
1✔
350
        index = 0
1✔
351
        foundIndex = -1
1✔
352
        if Rotor.Utils.isString(value) then value = LCase(value)
1✔
353

354
        while foundIndex = -1 and index < arrayCount
1✔
355
            targetValue = Rotor.Utils.getValueByKeyPath(array[index], key)
1✔
356
            if Rotor.Utils.isString(targetValue) then targetValue = LCase(targetValue)
1✔
357

358
            if targetValue = value
3✔
359
                foundIndex = index
1✔
360
            else
3✔
361
                index++
1✔
362
            end if
363
        end while
364

365
        return foundIndex
1✔
366
    end function
367

368
    '==========================================================================
369
    ' COMPARISON
370
    '==========================================================================
371

372
    ' ---------------------------------------------------------------------
373
    ' isDifferent - Checks if two items are different
374
    '
375
    ' Comparison logic:
376
    '   - roSGNode: Uses isSameNode()
377
    '   - AA/Array: JSON comparison
378
    '   - Function: String comparison
379
    '   - Other: Direct comparison
380
    '
381
    ' @param {dynamic} item1 - First item
382
    ' @param {dynamic} item2 - Second item
383
    ' @returns {boolean} True if items are different
384
    '
385
    function isDifferent(item1, item2) as boolean
386
        if type(item1) = "roSGNode"
3✔
387
            return not item1.isSameNode(item2)
1✔
388
        else if Rotor.Utils.isAssociativeArray(item1) or Rotor.Utils.isArray(item1)
3✔
389
            return FormatJSON(item1) <> FormatJSON(item2)
1✔
390
        else if Rotor.Utils.isFunction(item1)
2✔
391
            return item1.ToStr() <> item2.ToStr()
×
392
        else
3✔
393
            return item1 <> item2
1✔
394
        end if
395
    end function
396

397
    '==========================================================================
398
    ' ARRAY HELPERS
399
    '==========================================================================
400

401
    ' ---------------------------------------------------------------------
402
    ' ensureArray - Ensures value is wrapped in array
403
    '
404
    ' @param {dynamic} array - Input value or array
405
    ' @returns {object} Array containing the value, or original if already array
406
    '
407
    function ensureArray(array as dynamic) as object
408
        if isArray(array)
2✔
409
            resolvedArray = array
1✔
410
        else
3✔
411
            resolvedArray = [array]
1✔
412
        end if
413
        return resolvedArray
1✔
414
    end function
415

416
    ' ---------------------------------------------------------------------
417
    ' extendArrayOfStrings - Merges two string arrays without duplicates
418
    '
419
    ' Adds strings from sourceArray to targetArray only if not already present.
420
    '
421
    ' @param {object} targetArray - Target array to extend
422
    ' @param {object} sourceArray - Source array to merge from
423
    ' @returns {object} Extended target array
424
    '
425
    function extendArrayOfStrings(targetArray = [] as object, sourceArray = [] as object) as object
426
        for each itemStr in sourceArray
1✔
427
            foundIndex = Rotor.Utils.findInArray(targetArray, itemStr)
1✔
428
            if foundIndex = -1
2✔
429
                targetArray.push(itemStr)
1✔
430
            end if
431
        end for
432
        return targetArray
1✔
433
    end function
434

435
    ' ---------------------------------------------------------------------
436
    ' removeRedundantValuesInArray - Removes duplicate values from array in-place
437
    '
438
    ' Sorts array and removes consecutive duplicates.
439
    ' Modifies the original array.
440
    '
441
    ' @param {object} array - Array to deduplicate
442
    '
443
    sub removeRedundantValuesInArray(array as object)
444
        itemCount = array.Count()
1✔
445
        if itemCount = 0 then return
2✔
446

447
        array.Sort()
1✔
448

449
        index = 0
1✔
450
        while index + 1 < itemCount
1✔
451
            if array[index] = array[index + 1]
3✔
452
                array.delete(index + 1)
1✔
453
                itemCount--
1✔
454
            else
3✔
455
                index++
1✔
456
            end if
457
        end while
458
    end sub
459

460
    '==========================================================================
461
    ' HID (Hierarchical ID) OPERATIONS
462
    '==========================================================================
463

464
    ' ---------------------------------------------------------------------
465
    ' isAncestorHID - Checks if ancestorHID is an ancestor of HID
466
    '
467
    ' Uses string prefix matching on HIDs.
468
    '
469
    ' @param {string} ancestorHID - Potential ancestor HID
470
    ' @param {string} HID - HID to check
471
    ' @returns {boolean} True if ancestorHID is ancestor of HID
472
    '
473
    ' Example:
474
    '   isAncestorHID("scene.header", "scene.header.logo") => true
475
    '
476
    function isAncestorHID(ancestorHID as string, HID as string) as boolean
477
        ancestorHIDLen = Len(ancestorHID)
1✔
478
        return Left(HID, ancestorHIDLen) = ancestorHID and ancestorHIDLen < Len(HID)
1✔
479
    end function
480

481
    ' ---------------------------------------------------------------------
482
    ' isDescendantHID - Checks if descendantHID is a descendant of HID
483
    '
484
    ' Inverse of isAncestorHID.
485
    '
486
    ' @param {string} descendantHID - Potential descendant HID
487
    ' @param {string} HID - HID to check against
488
    ' @returns {boolean} True if descendantHID is descendant of HID
489
    '
490
    function isDescendantHID(descendantHID as string, HID as string) as boolean
491
        HIDlen = Len(HID)
1✔
492
        return Left(descendantHID, HIDlen) = HID and HIDlen < Len(descendantHID)
1✔
493
    end function
494

495
end namespace
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc