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

rokucommunity / brs / #26

01 Dec 2023 12:59PM UTC coverage: 91.487% (-0.2%) from 91.648%
#26

push

web-flow
Merge 73f2d2beb into e98151c1d

1772 of 2065 branches covered (0.0%)

Branch coverage included in aggregate %.

78 of 85 new or added lines in 2 files covered. (91.76%)

2 existing lines in 1 file now uncovered.

5235 of 5594 relevant lines covered (93.58%)

8975.08 hits per line

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

90.08
/src/brsTypes/components/RoArray.ts
1
import { BrsType, isBrsString, isBrsNumber, Int32 } from "..";
138✔
2
import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid, Comparable } from "../BrsType";
138✔
3
import { BrsComponent, BrsIterable } from "./BrsComponent";
138✔
4
import { Callable, StdlibArgument } from "../Callable";
138✔
5
import { Interpreter } from "../../interpreter";
6
import { RoAssociativeArray } from "./RoAssociativeArray";
138✔
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 {
54
            // a and b have the same type
55
            if (sortInsideTypes && isBrsNumber(a)) {
124✔
56
                // two numbers are in numeric order
57
                compare = (a as Comparable).greaterThan(b).toBoolean() ? 1 : -1;
46✔
58
            } else if (sortInsideTypes && isBrsString(a)) {
78✔
59
                // two strings are in alphabetical order
60
                let aStr = a.toString();
46✔
61
                let bStr = b.toString();
46✔
62
                if (caseInsensitive) {
46✔
63
                    aStr = aStr.toLowerCase();
18✔
64
                    bStr = bStr.toLowerCase();
18✔
65
                }
66
                // roku does not use locale for sorting strings
67
                compare = aStr > bStr ? 1 : -1;
46✔
68
            } else {
69
                // everything else is in the same order as the original
70

71
                const aOriginalIndex = originalArray.indexOf(a);
32✔
72
                const bOriginalIndex = originalArray.indexOf(b);
32✔
73
                if (aOriginalIndex > -1 && bOriginalIndex > -1) {
32✔
74
                    compare = aOriginalIndex - bOriginalIndex;
32✔
75
                }
76
            }
77
        }
78
    }
79
    return compare;
356✔
80
}
81

82
export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
138✔
83
    readonly kind = ValueKind.Object;
1,607✔
84
    elements: BrsType[];
85
    enumIndex: number;
86

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

111
    toString(parent?: BrsType): string {
112
        if (parent) {
69✔
113
            return "<Component: roArray>";
62✔
114
        }
115

116
        return [
7✔
117
            "<Component: roArray> =",
118
            "[",
119
            ...this.elements.map((el: BrsValue) => `    ${el.toString(this)}`),
15✔
120
            "]",
121
        ].join("\n");
122
    }
123

124
    equalTo(other: BrsType) {
125
        return BrsBoolean.False;
3✔
126
    }
127

128
    getValue() {
129
        return this.elements;
61✔
130
    }
131

132
    getElements() {
133
        return this.elements.slice();
190✔
134
    }
135

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

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

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

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

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

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

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

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

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

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

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

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

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

301
    private append = new Callable("append", {
1,607✔
302
        signature: {
303
            args: [new StdlibArgument("array", ValueKind.Object)],
304
            returns: ValueKind.Void,
305
        },
306
        impl: (_: Interpreter, array: BrsComponent) => {
307
            if (!(array instanceof RoArray)) {
2!
UNCOV
308
                return BrsInvalid.Instance;
×
309
            }
310

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

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

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

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

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

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

431
    /** Checks whether the array contains no elements. */
432
    private isEmpty = new Callable("isEmpty", {
1,607✔
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,607✔
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,607✔
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,607✔
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