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

rokucommunity / brs / #72

03 Apr 2024 12:54AM UTC coverage: 89.901% (-0.4%) from 90.321%
#72

push

web-flow
Merge 5937b00d4 into ac1cd3e36

1976 of 2368 branches covered (83.45%)

Branch coverage included in aggregate %.

227 of 273 new or added lines in 5 files covered. (83.15%)

2 existing lines in 2 files now uncovered.

5742 of 6217 relevant lines covered (92.36%)

30094.98 hits per line

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

90.26
/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
/**
9
 * Gives the order for sorting different types in a mixed array
10
 *
11
 * Mixed Arrays seem to get sorted in this order (tested with Roku OS 9.4):
12
 *
13
 * numbers in numeric order
14
 * strings in alphabetical order,
15
 * assocArrays in original order
16
 * everything else in original order
17
 */
18
function getTypeSortIndex(a: BrsType): number {
19
    if (isBrsNumber(a)) {
712✔
20
        return 0;
249✔
21
    } else if (isBrsString(a)) {
463✔
22
        return 1;
289✔
23
    } else if (a instanceof RoAssociativeArray) {
174✔
24
        return 2;
119✔
25
    }
26
    return 3;
55✔
27
}
28

29
/**
30
 * Sorts two BrsTypes in the order that Roku would sort them
31
 * @param originalArray A copy of the original array. Used to get the order of items
32
 * @param a
33
 * @param b
34
 * @param caseInsensitive Should strings be compared case insensitively? defaults to false
35
 * @param sortInsideTypes Should two numbers or two strings be sorted? defaults to true
36
 * @return compare value for array.sort()
37
 */
38
function sortCompare(
39
    originalArray: BrsType[],
40
    a: BrsType,
41
    b: BrsType,
42
    caseInsensitive: boolean = false,
×
43
    sortInsideTypes: boolean = true
187✔
44
): number {
45
    let compare = 0;
356✔
46
    if (a !== undefined && b !== undefined) {
356✔
47
        const aSortOrder = getTypeSortIndex(a);
356✔
48
        const bSortOrder = getTypeSortIndex(b);
356✔
49
        if (aSortOrder < bSortOrder) {
356✔
50
            compare = -1;
186✔
51
        } else if (bSortOrder < aSortOrder) {
170✔
52
            compare = 1;
46✔
53
        } else if (sortInsideTypes && isBrsNumber(a)) {
124✔
54
            // two numbers are in numeric order
55
            compare = (a as Comparable).greaterThan(b).toBoolean() ? 1 : -1;
46✔
56
        } else if (sortInsideTypes && isBrsString(a)) {
78✔
57
            // two strings are in alphabetical order
58
            let aStr = a.toString();
46✔
59
            let bStr = b.toString();
46✔
60
            if (caseInsensitive) {
46✔
61
                aStr = aStr.toLowerCase();
18✔
62
                bStr = bStr.toLowerCase();
18✔
63
            }
64
            // roku does not use locale for sorting strings
65
            compare = aStr > bStr ? 1 : -1;
46✔
66
        } else {
67
            // everything else is in the same order as the original
68
            const aOriginalIndex = originalArray.indexOf(a);
32✔
69
            const bOriginalIndex = originalArray.indexOf(b);
32✔
70
            if (aOriginalIndex > -1 && bOriginalIndex > -1) {
32✔
71
                compare = aOriginalIndex - bOriginalIndex;
32✔
72
            }
73
        }
74
    }
75
    return compare;
356✔
76
}
77

78
export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
142✔
79
    readonly kind = ValueKind.Object;
1,603✔
80
    elements: BrsType[];
81
    enumIndex: number;
82

83
    constructor(elements: BrsType[]) {
84
        super("roArray");
1,603✔
85
        this.elements = elements;
1,603✔
86
        this.enumIndex = elements.length ? 0 : -1;
1,603✔
87
        this.registerMethods({
1,603✔
88
            ifArray: [
89
                this.peek,
90
                this.pop,
91
                this.push,
92
                this.shift,
93
                this.unshift,
94
                this.delete,
95
                this.count,
96
                this.clear,
97
                this.append,
98
            ],
99
            ifArrayGet: [this.getEntry],
100
            ifArraySet: [this.setEntry],
101
            ifArrayJoin: [this.join],
102
            ifArraySort: [this.sort, this.sortBy, this.reverse],
103
            ifEnum: [this.isEmpty, this.isNext, this.next, this.reset],
104
        });
105
    }
106

107
    toString(parent?: BrsType): string {
108
        if (parent) {
69✔
109
            return "<Component: roArray>";
62✔
110
        }
111

112
        return [
7✔
113
            "<Component: roArray> =",
114
            "[",
115
            ...Array.from(this.elements, (el: BrsType) =>
116
                el === undefined ? "    invalid" : `    ${el.toString(this)}`
15!
117
            ),
118
            "]",
119
        ].join("\n");
120
    }
121

122
    equalTo(other: BrsType) {
123
        return BrsBoolean.False;
4✔
124
    }
125

126
    getValue() {
127
        return this.elements;
61✔
128
    }
129

130
    getElements() {
131
        return this.elements.slice();
183✔
132
    }
133

134
    get(index: BrsType) {
135
        switch (index.kind) {
152!
136
            case ValueKind.Float:
137
                return this.getElements()[Math.trunc(index.getValue())] ?? BrsInvalid.Instance;
×
138
            case ValueKind.Int32:
139
                return this.getElements()[index.getValue()] ?? BrsInvalid.Instance;
102✔
140
            case ValueKind.String:
141
                return this.getMethod(index.value) ?? BrsInvalid.Instance;
50!
142
            default:
143
                throw new Error(
×
144
                    "Array indexes must be 32-bit integers or Float, or method names must be strings"
145
                );
146
        }
147
    }
148

149
    set(index: BrsType, value: BrsType) {
150
        if (index.kind === ValueKind.Int32 || index.kind === ValueKind.Float) {
13!
151
            this.elements[Math.trunc(index.getValue())] = value;
13✔
152
        } else {
153
            throw new Error("Array indexes must be 32-bit integers or Float");
×
154
        }
155
        return BrsInvalid.Instance;
13✔
156
    }
157

158
    getNext() {
159
        const index = this.enumIndex;
4✔
160
        if (index >= 0) {
4✔
161
            this.enumIndex++;
4✔
162
            if (this.enumIndex >= this.elements.length) {
4!
163
                this.enumIndex = -1;
×
164
            }
165
        }
166
        return this.elements[index];
4✔
167
    }
168

169
    updateNext() {
170
        const hasItems = this.elements.length > 0;
23✔
171
        if (this.enumIndex === -1 && hasItems) {
23✔
172
            this.enumIndex = 0;
2✔
173
        } else if (this.enumIndex >= this.elements.length || !hasItems) {
21✔
174
            this.enumIndex = -1;
2✔
175
        }
176
    }
177

178
    aaCompare(
179
        fieldName: BrsString,
180
        flags: BrsString,
181
        a: RoAssociativeArray,
182
        b: RoAssociativeArray
183
    ) {
184
        let compare = 0;
37✔
185
        const originalArrayCopy = [...this.elements];
37✔
186
        const caseInsensitive = flags.toString().indexOf("i") > -1;
37✔
187
        const aHasField = a.elements.has(fieldName.toString().toLowerCase());
37✔
188
        const bHasField = b.elements.has(fieldName.toString().toLowerCase());
37✔
189
        if (aHasField && bHasField) {
37✔
190
            const valueA = a.get(fieldName);
29✔
191
            const valueB = b.get(fieldName);
29✔
192
            compare = sortCompare(originalArrayCopy, valueA, valueB, caseInsensitive);
29✔
193
        } else if (aHasField) {
8✔
194
            // assocArray with fields come before assocArrays without
195
            compare = -1;
1✔
196
        } else if (bHasField) {
7✔
197
            // assocArray with fields come before assocArrays without
198
            compare = 1;
1✔
199
        }
200
        return compare;
37✔
201
    }
202

203
    // ifArray
204
    private peek = new Callable("peek", {
1,603✔
205
        signature: {
206
            args: [],
207
            returns: ValueKind.Dynamic,
208
        },
209
        impl: (_: Interpreter) => {
210
            return this.elements[this.elements.length - 1] || BrsInvalid.Instance;
2✔
211
        },
212
    });
213

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

226
    private push = new Callable("push", {
1,603✔
227
        signature: {
228
            args: [new StdlibArgument("talue", ValueKind.Dynamic)],
229
            returns: ValueKind.Void,
230
        },
231
        impl: (_: Interpreter, tvalue: BrsType) => {
232
            this.elements.push(tvalue);
7✔
233
            this.updateNext();
7✔
234
            return BrsInvalid.Instance;
7✔
235
        },
236
    });
237

238
    private shift = new Callable("shift", {
1,603✔
239
        signature: {
240
            args: [],
241
            returns: ValueKind.Dynamic,
242
        },
243
        impl: (_: Interpreter) => {
244
            const removed = this.elements.shift();
3✔
245
            this.updateNext();
3✔
246
            return removed || BrsInvalid.Instance;
3✔
247
        },
248
    });
249

250
    private unshift = new Callable("unshift", {
1,603✔
251
        signature: {
252
            args: [new StdlibArgument("tvalue", ValueKind.Dynamic)],
253
            returns: ValueKind.Void,
254
        },
255
        impl: (_: Interpreter, tvalue: BrsType) => {
256
            this.elements.unshift(tvalue);
3✔
257
            this.updateNext();
3✔
258
            return BrsInvalid.Instance;
3✔
259
        },
260
    });
261

262
    private delete = new Callable("delete", {
1,603✔
263
        signature: {
264
            args: [new StdlibArgument("index", ValueKind.Int32)],
265
            returns: ValueKind.Boolean,
266
        },
267
        impl: (_: Interpreter, index: Int32) => {
268
            if (index.lessThan(new Int32(0)).toBoolean()) {
4✔
269
                return BrsBoolean.False;
1✔
270
            }
271
            const deleted = this.elements.splice(index.getValue(), 1);
3✔
272
            this.updateNext();
3✔
273
            return BrsBoolean.from(deleted.length > 0);
3✔
274
        },
275
    });
276

277
    private count = new Callable("count", {
1,603✔
278
        signature: {
279
            args: [],
280
            returns: ValueKind.Int32,
281
        },
282
        impl: (_: Interpreter) => {
283
            return new Int32(this.elements.length);
34✔
284
        },
285
    });
286

287
    private clear = new Callable("clear", {
1,603✔
288
        signature: {
289
            args: [],
290
            returns: ValueKind.Void,
291
        },
292
        impl: (_: Interpreter) => {
293
            this.elements = [];
7✔
294
            this.enumIndex = -1;
7✔
295
            return BrsInvalid.Instance;
7✔
296
        },
297
    });
298

299
    private append = new Callable("append", {
1,603✔
300
        signature: {
301
            args: [new StdlibArgument("array", ValueKind.Object)],
302
            returns: ValueKind.Void,
303
        },
304
        impl: (interpreter: Interpreter, array: BrsComponent) => {
305
            if (!(array instanceof RoArray)) {
2!
NEW
306
                let location = `${interpreter.location.file}:(${interpreter.location.start.line})`;
×
NEW
307
                interpreter.stderr.write(
×
308
                    `BRIGHTSCRIPT: ERROR: roArray.Append: invalid parameter type ${array.getComponentName()}: ${location}\n`
309
                );
UNCOV
310
                return BrsInvalid.Instance;
×
311
            }
312

313
            this.elements = [
2✔
314
                ...this.elements,
315
                ...array.elements.filter((element) => !!element), // don't copy "holes" where no value exists
10✔
316
            ];
317
            this.updateNext();
2✔
318
            return BrsInvalid.Instance;
2✔
319
        },
320
    });
321

322
    // ifArrayGet
323
    private getEntry = new Callable("getEntry", {
1,603✔
324
        signature: {
325
            args: [new StdlibArgument("index", ValueKind.Int32 | ValueKind.Float)],
326
            returns: ValueKind.Dynamic,
327
        },
328
        impl: (_: Interpreter, index: Int32 | Float) => {
NEW
329
            return this.elements[Math.trunc(index.getValue())] || BrsInvalid.Instance;
×
330
        },
331
    });
332

333
    // ifArraySet
334
    private setEntry = new Callable("setEntry", {
1,603✔
335
        signature: {
336
            args: [
337
                new StdlibArgument("index", ValueKind.Int32 | ValueKind.Float),
338
                new StdlibArgument("tvalue", ValueKind.Dynamic),
339
            ],
340
            returns: ValueKind.Void,
341
        },
342
        impl: (_: Interpreter, index: Int32 | Float, tvalue: BrsType) => {
343
            return this.set(index, tvalue);
×
344
        },
345
    });
346

347
    // ifArrayJoin
348
    private join = new Callable("join", {
1,603✔
349
        signature: {
350
            args: [new StdlibArgument("separator", ValueKind.String)],
351
            returns: ValueKind.String,
352
        },
353
        impl: (interpreter: Interpreter, separator: BrsString) => {
354
            if (
3✔
355
                this.elements.some(function (element) {
356
                    return !(element instanceof BrsString);
8✔
357
                })
358
            ) {
359
                interpreter.stderr.write("roArray.Join: Array contains non-string value(s).\n");
2✔
360
                return new BrsString("");
2✔
361
            }
362
            return new BrsString(this.elements.join(separator.value));
1✔
363
        },
364
    });
365
    // ifArraySort
366
    private sort = new Callable("sort", {
1,603✔
367
        signature: {
368
            args: [new StdlibArgument("flags", ValueKind.String, new BrsString(""))],
369
            returns: ValueKind.Void,
370
        },
371
        impl: (interpreter: Interpreter, flags: BrsString) => {
372
            if (flags.toString().match(/([^ir])/g) != null) {
12✔
373
                interpreter.stderr.write("roArray.Sort: Flags contains invalid option(s).\n");
1✔
374
            } else {
375
                const caseInsensitive = flags.toString().indexOf("i") > -1;
11✔
376
                const originalArrayCopy = [...this.elements];
11✔
377
                this.elements = this.elements.sort((a, b) => {
11✔
378
                    return sortCompare(originalArrayCopy, a, b, caseInsensitive);
158✔
379
                });
380
                if (flags.toString().indexOf("r") > -1) {
11✔
381
                    this.elements = this.elements.reverse();
5✔
382
                }
383
            }
384
            return BrsInvalid.Instance;
12✔
385
        },
386
    });
387

388
    private sortBy = new Callable("sortBy", {
1,603✔
389
        signature: {
390
            args: [
391
                new StdlibArgument("fieldName", ValueKind.String),
392
                new StdlibArgument("flags", ValueKind.String, new BrsString("")),
393
            ],
394
            returns: ValueKind.Void,
395
        },
396
        impl: (interpreter: Interpreter, fieldName: BrsString, flags: BrsString) => {
397
            if (flags.toString().match(/([^ir])/g) != null) {
13✔
398
                interpreter.stderr.write("roArray.SortBy: Flags contains invalid option(s).\n");
1✔
399
            } else {
400
                const originalArrayCopy = [...this.elements];
12✔
401
                this.elements = this.elements.sort((a, b) => {
12✔
402
                    let compare = 0;
206✔
403
                    if (a instanceof RoAssociativeArray && b instanceof RoAssociativeArray) {
206✔
404
                        compare = this.aaCompare(fieldName, flags, a, b);
37✔
405
                    } else if (a !== undefined && b !== undefined) {
169✔
406
                        compare = sortCompare(originalArrayCopy, a, b, false, false);
169✔
407
                    }
408
                    return compare;
206✔
409
                });
410
                if (flags.toString().indexOf("r") > -1) {
12✔
411
                    this.elements = this.elements.reverse();
5✔
412
                }
413
            }
414
            return BrsInvalid.Instance;
13✔
415
        },
416
    });
417

418
    private reverse = new Callable("reverse", {
1,603✔
419
        signature: {
420
            args: [],
421
            returns: ValueKind.Void,
422
        },
423
        impl: (_: Interpreter, separator: BrsString) => {
424
            this.elements = this.elements.reverse();
1✔
425
            return BrsInvalid.Instance;
1✔
426
        },
427
    });
428

429
    // ifEnum
430

431
    /** Checks whether the array contains no elements. */
432
    private isEmpty = new Callable("isEmpty", {
1,603✔
433
        signature: {
434
            args: [],
435
            returns: ValueKind.Boolean,
436
        },
437
        impl: (_: Interpreter) => {
438
            return BrsBoolean.from(this.elements.length === 0);
2✔
439
        },
440
    });
441

442
    /** Checks whether the current position is not past the end of the enumeration. */
443
    private isNext = new Callable("isNext", {
1,603✔
444
        signature: {
445
            args: [],
446
            returns: ValueKind.Boolean,
447
        },
448
        impl: (_: Interpreter) => {
449
            return BrsBoolean.from(this.enumIndex >= 0);
4✔
450
        },
451
    });
452

453
    /** Resets the current position to the first element of the enumeration. */
454
    private reset = new Callable("reset", {
1,603✔
455
        signature: {
456
            args: [],
457
            returns: ValueKind.Void,
458
        },
459
        impl: (_: Interpreter) => {
460
            this.enumIndex = this.elements.length > 0 ? 0 : -1;
1!
461
            return BrsInvalid.Instance;
1✔
462
        },
463
    });
464

465
    /** Increments the position of an enumeration. */
466
    private next = new Callable("next", {
1,603✔
467
        signature: {
468
            args: [],
469
            returns: ValueKind.Dynamic,
470
        },
471
        impl: (_: Interpreter) => {
472
            return this.getNext() ?? BrsInvalid.Instance;
4!
473
        },
474
    });
475
}
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