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

rokucommunity / brs / #84

04 Apr 2024 10:25PM UTC coverage: 89.562% (-0.07%) from 89.636%
#84

push

web-flow
Implement `ifArraySizeInfo` in `roArray` (#62)

* Implemented `ifArraySizeInfo` in `roArray`

* Removing empty line

* Fixed warning message

2024 of 2438 branches covered (83.02%)

Branch coverage included in aggregate %.

92 of 108 new or added lines in 7 files covered. (85.19%)

45 existing lines in 4 files now uncovered.

5801 of 6299 relevant lines covered (92.09%)

29750.15 hits per line

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

88.96
/src/brsTypes/components/RoArray.ts
1
import { BrsType, isBrsString, isBrsNumber, Int32, Float } from "..";
142✔
2
import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid, Comparable } from "../BrsType";
142✔
3
import { BrsComponent, BrsIterable } from "./BrsComponent";
142✔
4
import { Callable, StdlibArgument } from "../Callable";
142✔
5
import { Interpreter } from "../../interpreter";
6
import { RoAssociativeArray } from "./RoAssociativeArray";
142✔
7

8
export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
142✔
9
    readonly kind = ValueKind.Object;
1,629✔
10
    private maxSize = 0;
1,629✔
11
    private resizable = true;
1,629✔
12
    private enumIndex: number;
13
    elements: BrsType[];
14

15
    constructor(elements: BrsType[]);
16
    constructor(capacity: Int32 | Float, resizable: BrsBoolean);
17
    constructor(...args: any) {
18
        super("roArray");
1,629✔
19
        this.elements = [];
1,629✔
20
        if (args.length === 1 && Array.isArray(args[0])) {
1,629✔
21
            this.elements = args[0];
1,627✔
22
        } else if (
2!
23
            args.length === 2 &&
6!
24
            (args[0] instanceof Int32 || args[0] instanceof Float) &&
25
            (args[1] instanceof BrsBoolean || isBrsNumber(args[1]))
26
        ) {
27
            this.maxSize = args[0].getValue();
2✔
28
            this.resizable = args[1].toBoolean();
2✔
29
        } else {
NEW
UNCOV
30
            throw new Error(
×
31
                `BRIGHTSCRIPT: ERROR: Runtime: "roArray": invalid number of parameters:`
32
            );
33
        }
34
        this.enumIndex = this.elements.length ? 0 : -1;
1,629✔
35
        this.registerMethods({
1,629✔
36
            ifArray: [
37
                this.peek,
38
                this.pop,
39
                this.push,
40
                this.shift,
41
                this.unshift,
42
                this.delete,
43
                this.count,
44
                this.clear,
45
                this.append,
46
            ],
47
            ifArrayGet: [this.getEntry],
48
            ifArraySet: [this.setEntry],
49
            ifArrayJoin: [this.join],
50
            ifArraySort: [this.sort, this.sortBy, this.reverse],
51
            ifArraySlice: [this.slice],
52
            ifArraySizeInfo: [this.capacity, this.isResizable],
53
            ifEnum: [this.isEmpty, this.isNext, this.next, this.reset],
54
        });
55
    }
56

57
    toString(parent?: BrsType): string {
58
        if (parent) {
69✔
59
            return "<Component: roArray>";
62✔
60
        }
61

62
        return [
7✔
63
            "<Component: roArray> =",
64
            "[",
65
            ...Array.from(this.elements, (el: BrsType) =>
66
                el === undefined ? "    invalid" : `    ${el.toString(this)}`
15!
67
            ),
68
            "]",
69
        ].join("\n");
70
    }
71

72
    equalTo(other: BrsType) {
73
        return BrsBoolean.False;
4✔
74
    }
75

76
    getValue() {
77
        return this.elements;
61✔
78
    }
79

80
    getElements() {
81
        return this.elements.slice();
183✔
82
    }
83

84
    get(index: BrsType) {
85
        switch (index.kind) {
172!
86
            case ValueKind.Float:
UNCOV
87
                return this.getElements()[Math.trunc(index.getValue())] ?? BrsInvalid.Instance;
×
88
            case ValueKind.Int32:
89
                return this.getElements()[index.getValue()] ?? BrsInvalid.Instance;
102✔
90
            case ValueKind.String:
91
                return this.getMethod(index.value) ?? BrsInvalid.Instance;
70!
92
            default:
UNCOV
93
                throw new Error(
×
94
                    "Array indexes must be 32-bit integers or Float, or method names must be strings"
95
                );
96
        }
97
    }
98

99
    set(index: BrsType, value: BrsType) {
100
        if (index.kind === ValueKind.Int32 || index.kind === ValueKind.Float) {
13!
101
            this.elements[Math.trunc(index.getValue())] = value;
13✔
102
        } else {
UNCOV
103
            throw new Error("Array indexes must be 32-bit integers or Float");
×
104
        }
105
        return BrsInvalid.Instance;
13✔
106
    }
107

108
    getNext() {
109
        const index = this.enumIndex;
4✔
110
        if (index >= 0) {
4✔
111
            this.enumIndex++;
4✔
112
            if (this.enumIndex >= this.elements.length) {
4!
UNCOV
113
                this.enumIndex = -1;
×
114
            }
115
        }
116
        return this.elements[index];
4✔
117
    }
118

119
    updateNext() {
120
        const hasItems = this.elements.length > 0;
24✔
121
        if (this.enumIndex === -1 && hasItems) {
24✔
122
            this.enumIndex = 0;
3✔
123
        } else if (this.enumIndex >= this.elements.length || !hasItems) {
21✔
124
            this.enumIndex = -1;
2✔
125
        }
126
    }
127

128
    updateCapacity(growthFactor = 0) {
3✔
129
        if (this.resizable && growthFactor > 0) {
13✔
130
            if (this.elements.length > 0 && this.elements.length > this.maxSize) {
10✔
131
                let count = this.elements.length - 1;
4✔
132
                let newCap = Math.trunc(count * growthFactor);
4✔
133
                if (newCap - this.maxSize < 10) {
4!
134
                    this.maxSize = Math.trunc(10 * (count / 10 + 1));
4✔
135
                } else {
NEW
UNCOV
136
                    this.maxSize = newCap;
×
137
                }
138
            }
139
        } else {
140
            this.maxSize = Math.max(this.elements.length, this.maxSize);
3✔
141
        }
142
    }
143

144
    aaCompare(
145
        fieldName: BrsString,
146
        flags: BrsString,
147
        a: RoAssociativeArray,
148
        b: RoAssociativeArray
149
    ) {
150
        let compare = 0;
37✔
151
        const originalArrayCopy = [...this.elements];
37✔
152
        const caseInsensitive = flags.toString().indexOf("i") > -1;
37✔
153
        const aHasField = a.elements.has(fieldName.toString().toLowerCase());
37✔
154
        const bHasField = b.elements.has(fieldName.toString().toLowerCase());
37✔
155
        if (aHasField && bHasField) {
37✔
156
            const valueA = a.get(fieldName);
29✔
157
            const valueB = b.get(fieldName);
29✔
158
            compare = sortCompare(originalArrayCopy, valueA, valueB, caseInsensitive);
29✔
159
        } else if (aHasField) {
8✔
160
            // assocArray with fields come before assocArrays without
161
            compare = -1;
1✔
162
        } else if (bHasField) {
7✔
163
            // assocArray with fields come before assocArrays without
164
            compare = 1;
1✔
165
        }
166
        return compare;
37✔
167
    }
168

169
    // ifArray
170
    private peek = new Callable("peek", {
1,629✔
171
        signature: {
172
            args: [],
173
            returns: ValueKind.Dynamic,
174
        },
175
        impl: (_: Interpreter) => {
176
            return this.elements[this.elements.length - 1] || BrsInvalid.Instance;
2✔
177
        },
178
    });
179

180
    private pop = new Callable("pop", {
1,629✔
181
        signature: {
182
            args: [],
183
            returns: ValueKind.Dynamic,
184
        },
185
        impl: (_: Interpreter) => {
186
            const removed = this.elements.pop();
5✔
187
            this.updateNext();
5✔
188
            return removed || BrsInvalid.Instance;
5✔
189
        },
190
    });
191

192
    private push = new Callable("push", {
1,629✔
193
        signature: {
194
            args: [new StdlibArgument("talue", ValueKind.Dynamic)],
195
            returns: ValueKind.Void,
196
        },
197
        impl: (interpreter: Interpreter, tvalue: BrsType) => {
198
            if (this.resizable || this.elements.length < this.maxSize) {
8✔
199
                this.elements.push(tvalue);
7✔
200
                this.updateNext();
7✔
201
                this.updateCapacity(1.25);
7✔
202
            } else {
203
                let location = interpreter.formatLocation();
1✔
204
                interpreter.stderr.write(
1✔
205
                    `BRIGHTSCRIPT: ERROR: roArray.Push: set ignored for index out of bounds on non-resizable array: ${location}\n`
206
                );
207
            }
208
            return BrsInvalid.Instance;
8✔
209
        },
210
    });
211

212
    private shift = new Callable("shift", {
1,629✔
213
        signature: {
214
            args: [],
215
            returns: ValueKind.Dynamic,
216
        },
217
        impl: (_: Interpreter) => {
218
            const removed = this.elements.shift();
3✔
219
            this.updateNext();
3✔
220
            return removed || BrsInvalid.Instance;
3✔
221
        },
222
    });
223

224
    private unshift = new Callable("unshift", {
1,629✔
225
        signature: {
226
            args: [new StdlibArgument("tvalue", ValueKind.Dynamic)],
227
            returns: ValueKind.Void,
228
        },
229
        impl: (interpreter: Interpreter, tvalue: BrsType) => {
230
            if (this.resizable || this.elements.length < this.maxSize) {
3!
231
                this.elements.unshift(tvalue);
3✔
232
                this.updateNext();
3✔
233
                this.updateCapacity(1.25);
3✔
234
            } else {
NEW
UNCOV
235
                let location = interpreter.formatLocation();
×
NEW
UNCOV
236
                interpreter.stderr.write(
×
237
                    `BRIGHTSCRIPT: ERROR: roArray.Unshift: set ignored for index out of bounds on non-resizable array: ${location}\n`
238
                );
239
            }
240
            return BrsInvalid.Instance;
3✔
241
        },
242
    });
243

244
    private delete = new Callable("delete", {
1,629✔
245
        signature: {
246
            args: [new StdlibArgument("index", ValueKind.Int32)],
247
            returns: ValueKind.Boolean,
248
        },
249
        impl: (_: Interpreter, index: Int32) => {
250
            if (index.lessThan(new Int32(0)).toBoolean()) {
4✔
251
                return BrsBoolean.False;
1✔
252
            }
253
            const deleted = this.elements.splice(index.getValue(), 1);
3✔
254
            this.updateNext();
3✔
255
            return BrsBoolean.from(deleted.length > 0);
3✔
256
        },
257
    });
258

259
    private count = new Callable("count", {
1,629✔
260
        signature: {
261
            args: [],
262
            returns: ValueKind.Int32,
263
        },
264
        impl: (_: Interpreter) => {
265
            return new Int32(this.elements.length);
36✔
266
        },
267
    });
268

269
    private clear = new Callable("clear", {
1,629✔
270
        signature: {
271
            args: [],
272
            returns: ValueKind.Void,
273
        },
274
        impl: (_: Interpreter) => {
275
            this.elements = [];
8✔
276
            this.enumIndex = -1;
8✔
277
            return BrsInvalid.Instance;
8✔
278
        },
279
    });
280

281
    private append = new Callable("append", {
1,629✔
282
        signature: {
283
            args: [new StdlibArgument("array", ValueKind.Object)],
284
            returns: ValueKind.Void,
285
        },
286
        impl: (interpreter: Interpreter, array: BrsComponent) => {
287
            if (!(array instanceof RoArray)) {
3!
NEW
UNCOV
288
                let location = interpreter.formatLocation();
×
UNCOV
289
                interpreter.stderr.write(
×
290
                    `BRIGHTSCRIPT: ERROR: roArray.Append: invalid parameter type ${array.getComponentName()}: ${location}\n`
291
                );
UNCOV
292
                return BrsInvalid.Instance;
×
293
            }
294

295
            if (this.resizable || this.elements.length + array.elements.length <= this.maxSize) {
3✔
296
                this.elements = [
3✔
297
                    ...this.elements,
298
                    ...array.elements.filter((element) => !!element), // don't copy "holes" where no value exists
15✔
299
                ];
300
                this.updateNext();
3✔
301
                this.updateCapacity();
3✔
302
            }
303
            return BrsInvalid.Instance;
3✔
304
        },
305
    });
306

307
    // ifArrayGet
308
    private getEntry = new Callable("getEntry", {
1,629✔
309
        signature: {
310
            args: [new StdlibArgument("index", ValueKind.Int32 | ValueKind.Float)],
311
            returns: ValueKind.Dynamic,
312
        },
313
        impl: (_: Interpreter, index: Int32 | Float) => {
UNCOV
314
            return this.elements[Math.trunc(index.getValue())] || BrsInvalid.Instance;
×
315
        },
316
    });
317

318
    // ifArraySet
319
    private setEntry = new Callable("setEntry", {
1,629✔
320
        signature: {
321
            args: [
322
                new StdlibArgument("index", ValueKind.Int32 | ValueKind.Float),
323
                new StdlibArgument("tvalue", ValueKind.Dynamic),
324
            ],
325
            returns: ValueKind.Void,
326
        },
327
        impl: (_: Interpreter, index: Int32 | Float, tvalue: BrsType) => {
UNCOV
328
            return this.set(index, tvalue);
×
329
        },
330
    });
331

332
    // ifArrayJoin
333
    private join = new Callable("join", {
1,629✔
334
        signature: {
335
            args: [new StdlibArgument("separator", ValueKind.String)],
336
            returns: ValueKind.String,
337
        },
338
        impl: (interpreter: Interpreter, separator: BrsString) => {
339
            if (
9✔
340
                this.elements.some(function (element) {
341
                    return !(element instanceof BrsString);
26✔
342
                })
343
            ) {
344
                interpreter.stderr.write("roArray.Join: Array contains non-string value(s).\n");
2✔
345
                return new BrsString("");
2✔
346
            }
347
            return new BrsString(this.elements.join(separator.value));
7✔
348
        },
349
    });
350

351
    // ifArraySort
352
    private sort = new Callable("sort", {
1,629✔
353
        signature: {
354
            args: [new StdlibArgument("flags", ValueKind.String, new BrsString(""))],
355
            returns: ValueKind.Void,
356
        },
357
        impl: (interpreter: Interpreter, flags: BrsString) => {
358
            if (flags.toString().match(/([^ir])/g) != null) {
12✔
359
                interpreter.stderr.write("roArray.Sort: Flags contains invalid option(s).\n");
1✔
360
            } else {
361
                const caseInsensitive = flags.toString().indexOf("i") > -1;
11✔
362
                const originalArrayCopy = [...this.elements];
11✔
363
                this.elements = this.elements.sort((a, b) => {
11✔
364
                    return sortCompare(originalArrayCopy, a, b, caseInsensitive);
158✔
365
                });
366
                if (flags.toString().indexOf("r") > -1) {
11✔
367
                    this.elements = this.elements.reverse();
5✔
368
                }
369
            }
370
            return BrsInvalid.Instance;
12✔
371
        },
372
    });
373

374
    private sortBy = new Callable("sortBy", {
1,629✔
375
        signature: {
376
            args: [
377
                new StdlibArgument("fieldName", ValueKind.String),
378
                new StdlibArgument("flags", ValueKind.String, new BrsString("")),
379
            ],
380
            returns: ValueKind.Void,
381
        },
382
        impl: (interpreter: Interpreter, fieldName: BrsString, flags: BrsString) => {
383
            if (flags.toString().match(/([^ir])/g) != null) {
13✔
384
                interpreter.stderr.write("roArray.SortBy: Flags contains invalid option(s).\n");
1✔
385
            } else {
386
                const originalArrayCopy = [...this.elements];
12✔
387
                this.elements = this.elements.sort((a, b) => {
12✔
388
                    let compare = 0;
206✔
389
                    if (a instanceof RoAssociativeArray && b instanceof RoAssociativeArray) {
206✔
390
                        compare = this.aaCompare(fieldName, flags, a, b);
37✔
391
                    } else if (a !== undefined && b !== undefined) {
169✔
392
                        compare = sortCompare(originalArrayCopy, a, b, false, false);
169✔
393
                    }
394
                    return compare;
206✔
395
                });
396
                if (flags.toString().indexOf("r") > -1) {
12✔
397
                    this.elements = this.elements.reverse();
5✔
398
                }
399
            }
400
            return BrsInvalid.Instance;
13✔
401
        },
402
    });
403

404
    private reverse = new Callable("reverse", {
1,629✔
405
        signature: {
406
            args: [],
407
            returns: ValueKind.Void,
408
        },
409
        impl: (_: Interpreter, separator: BrsString) => {
410
            this.elements = this.elements.reverse();
1✔
411
            return BrsInvalid.Instance;
1✔
412
        },
413
    });
414

415
    // ifArraySlice
416

417
    /** Returns a copy of a portion of an array into a new array selected from start to end (end not included) */
418
    private slice = new Callable("slice", {
1,629✔
419
        signature: {
420
            args: [
421
                new StdlibArgument("start", ValueKind.Int32 | ValueKind.Float, new Int32(0)),
422
                new StdlibArgument("end", ValueKind.Int32 | ValueKind.Float, BrsInvalid.Instance),
423
            ],
424
            returns: ValueKind.Object,
425
        },
426
        impl: (_: Interpreter, start: Int32 | Float, end: Int32 | Float | BrsInvalid) => {
427
            if (end instanceof BrsInvalid) {
15✔
428
                return new RoArray(this.elements.slice(start.getValue()));
7✔
429
            } else {
430
                return new RoArray(this.elements.slice(start.getValue(), end.getValue()));
8✔
431
            }
432
        },
433
    });
434

435
    // ifArraySizeInfo
436

437
    /** Returns the maximum number of entries that can be stored in the array. */
438
    private capacity = new Callable("capacity", {
1,629✔
439
        signature: {
440
            args: [],
441
            returns: ValueKind.Int32,
442
        },
443
        impl: (_: Interpreter) => {
444
            return new Int32(this.maxSize);
2✔
445
        },
446
    });
447

448
    /** Returns true if the array can be resized. */
449
    private isResizable = new Callable("isResizable", {
1,629✔
450
        signature: {
451
            args: [],
452
            returns: ValueKind.Boolean,
453
        },
454
        impl: (_: Interpreter) => {
NEW
UNCOV
455
            return BrsBoolean.from(this.resizable);
×
456
        },
457
    });
458

459
    // ifEnum
460

461
    /** Checks whether the array contains no elements. */
462
    private isEmpty = new Callable("isEmpty", {
1,629✔
463
        signature: {
464
            args: [],
465
            returns: ValueKind.Boolean,
466
        },
467
        impl: (_: Interpreter) => {
468
            return BrsBoolean.from(this.elements.length === 0);
3✔
469
        },
470
    });
471

472
    /** Checks whether the current position is not past the end of the enumeration. */
473
    private isNext = new Callable("isNext", {
1,629✔
474
        signature: {
475
            args: [],
476
            returns: ValueKind.Boolean,
477
        },
478
        impl: (_: Interpreter) => {
479
            return BrsBoolean.from(this.enumIndex >= 0);
4✔
480
        },
481
    });
482

483
    /** Resets the current position to the first element of the enumeration. */
484
    private reset = new Callable("reset", {
1,629✔
485
        signature: {
486
            args: [],
487
            returns: ValueKind.Void,
488
        },
489
        impl: (_: Interpreter) => {
490
            this.enumIndex = this.elements.length > 0 ? 0 : -1;
1!
491
            return BrsInvalid.Instance;
1✔
492
        },
493
    });
494

495
    /** Increments the position of an enumeration. */
496
    private next = new Callable("next", {
1,629✔
497
        signature: {
498
            args: [],
499
            returns: ValueKind.Dynamic,
500
        },
501
        impl: (_: Interpreter) => {
502
            return this.getNext() ?? BrsInvalid.Instance;
4!
503
        },
504
    });
505
}
506

507
/**
508
 * Gives the order for sorting different types in a mixed array
509
 *
510
 * Mixed Arrays seem to get sorted in this order (tested with Roku OS 9.4):
511
 *
512
 * numbers in numeric order
513
 * strings in alphabetical order,
514
 * assocArrays in original order
515
 * everything else in original order
516
 */
517
function getTypeSortIndex(a: BrsType): number {
518
    if (isBrsNumber(a)) {
712✔
519
        return 0;
249✔
520
    } else if (isBrsString(a)) {
463✔
521
        return 1;
289✔
522
    } else if (a instanceof RoAssociativeArray) {
174✔
523
        return 2;
119✔
524
    }
525
    return 3;
55✔
526
}
527

528
/**
529
 * Sorts two BrsTypes in the order that Roku would sort them
530
 * @param originalArray A copy of the original array. Used to get the order of items
531
 * @param a
532
 * @param b
533
 * @param caseInsensitive Should strings be compared case insensitively? defaults to false
534
 * @param sortInsideTypes Should two numbers or two strings be sorted? defaults to true
535
 * @return compare value for array.sort()
536
 */
537
function sortCompare(
538
    originalArray: BrsType[],
539
    a: BrsType,
540
    b: BrsType,
541
    caseInsensitive: boolean = false,
×
542
    sortInsideTypes: boolean = true
187✔
543
): number {
544
    let compare = 0;
356✔
545
    if (a !== undefined && b !== undefined) {
356✔
546
        const aSortOrder = getTypeSortIndex(a);
356✔
547
        const bSortOrder = getTypeSortIndex(b);
356✔
548
        if (aSortOrder < bSortOrder) {
356✔
549
            compare = -1;
186✔
550
        } else if (bSortOrder < aSortOrder) {
170✔
551
            compare = 1;
46✔
552
        } else if (sortInsideTypes && isBrsNumber(a)) {
124✔
553
            // two numbers are in numeric order
554
            compare = (a as Comparable).greaterThan(b).toBoolean() ? 1 : -1;
46✔
555
        } else if (sortInsideTypes && isBrsString(a)) {
78✔
556
            // two strings are in alphabetical order
557
            let aStr = a.toString();
46✔
558
            let bStr = b.toString();
46✔
559
            if (caseInsensitive) {
46✔
560
                aStr = aStr.toLowerCase();
18✔
561
                bStr = bStr.toLowerCase();
18✔
562
            }
563
            // roku does not use locale for sorting strings
564
            compare = aStr > bStr ? 1 : -1;
46✔
565
        } else {
566
            // everything else is in the same order as the original
567
            const aOriginalIndex = originalArray.indexOf(a);
32✔
568
            const bOriginalIndex = originalArray.indexOf(b);
32✔
569
            if (aOriginalIndex > -1 && bOriginalIndex > -1) {
32✔
570
                compare = aOriginalIndex - bOriginalIndex;
32✔
571
            }
572
        }
573
    }
574
    return compare;
356✔
575
}
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