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

rokucommunity / brs / #294

24 Apr 2023 07:40PM UTC coverage: 91.705% (+5.4%) from 86.275%
#294

push

web-flow
Merge pull request #1 from rokucommunity/adoption

refactor in preparation for adoption the project

1736 of 2013 branches covered (86.24%)

Branch coverage included in aggregate %.

5152 of 5498 relevant lines covered (93.71%)

8882.84 hits per line

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

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

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

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

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

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

87
    constructor(elements: BrsType[]) {
88
        super("roArray");
1,609✔
89
        this.elements = elements;
1,609✔
90
        this.registerMethods({
1,609✔
91
            ifArray: [
92
                this.peek,
93
                this.pop,
94
                this.push,
95
                this.shift,
96
                this.unshift,
97
                this.delete,
98
                this.count,
99
                this.clear,
100
                this.append,
101
            ],
102
            ifArrayJoin: [this.join],
103
            ifArraySort: [this.sort, this.sortBy, this.reverse],
104
            ifEnum: [this.isEmpty],
105
        });
106
    }
107

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

113
        return [
2✔
114
            "<Component: roArray> =",
115
            "[",
116
            ...this.elements.map((el: BrsValue) => `    ${el.toString(this)}`),
6✔
117
            "]",
118
        ].join("\n");
119
    }
120

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

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

129
    getElements() {
130
        return this.elements.slice();
181✔
131
    }
132

133
    get(index: BrsType) {
134
        switch (index.kind) {
130!
135
            case ValueKind.Int32:
136
                return this.getElements()[index.getValue()] || BrsInvalid.Instance;
100✔
137
            case ValueKind.String:
138
                return this.getMethod(index.value) || BrsInvalid.Instance;
30!
139
            default:
140
                throw new Error(
×
141
                    "Array indexes must be 32-bit integers, or method names must be strings"
142
                );
143
        }
144
    }
145

146
    set(index: BrsType, value: BrsType) {
147
        if (index.kind !== ValueKind.Int32) {
13!
148
            throw new Error("Array indexes must be 32-bit integers");
×
149
        }
150

151
        this.elements[index.getValue()] = value;
13✔
152

153
        return BrsInvalid.Instance;
13✔
154
    }
155

156
    private peek = new Callable("peek", {
1,609✔
157
        signature: {
158
            args: [],
159
            returns: ValueKind.Dynamic,
160
        },
161
        impl: (interpreter: Interpreter) => {
162
            return this.elements[this.elements.length - 1] || BrsInvalid.Instance;
2✔
163
        },
164
    });
165

166
    private pop = new Callable("pop", {
1,609✔
167
        signature: {
168
            args: [],
169
            returns: ValueKind.Dynamic,
170
        },
171
        impl: (interpreter: Interpreter) => {
172
            return this.elements.pop() || BrsInvalid.Instance;
3✔
173
        },
174
    });
175

176
    private push = new Callable("push", {
1,609✔
177
        signature: {
178
            args: [new StdlibArgument("talue", ValueKind.Dynamic)],
179
            returns: ValueKind.Void,
180
        },
181
        impl: (interpreter: Interpreter, tvalue: BrsType) => {
182
            this.elements.push(tvalue);
3✔
183
            return BrsInvalid.Instance;
3✔
184
        },
185
    });
186

187
    private shift = new Callable("shift", {
1,609✔
188
        signature: {
189
            args: [],
190
            returns: ValueKind.Dynamic,
191
        },
192
        impl: (interpreter: Interpreter) => {
193
            return this.elements.shift() || BrsInvalid.Instance;
3✔
194
        },
195
    });
196

197
    private unshift = new Callable("unshift", {
1,609✔
198
        signature: {
199
            args: [new StdlibArgument("tvalue", ValueKind.Dynamic)],
200
            returns: ValueKind.Void,
201
        },
202
        impl: (interpreter: Interpreter, tvalue: BrsType) => {
203
            this.elements.unshift(tvalue);
2✔
204
            return BrsInvalid.Instance;
2✔
205
        },
206
    });
207

208
    private delete = new Callable("delete", {
1,609✔
209
        signature: {
210
            args: [new StdlibArgument("index", ValueKind.Int32)],
211
            returns: ValueKind.Boolean,
212
        },
213
        impl: (interpreter: Interpreter, index: Int32) => {
214
            if (index.lessThan(new Int32(0)).toBoolean()) {
4✔
215
                return BrsBoolean.False;
1✔
216
            }
217

218
            let deleted = this.elements.splice(index.getValue(), 1);
3✔
219
            return BrsBoolean.from(deleted.length > 0);
3✔
220
        },
221
    });
222

223
    private count = new Callable("count", {
1,609✔
224
        signature: {
225
            args: [],
226
            returns: ValueKind.Int32,
227
        },
228
        impl: (interpreter: Interpreter) => {
229
            return new Int32(this.elements.length);
32✔
230
        },
231
    });
232

233
    private clear = new Callable("clear", {
1,609✔
234
        signature: {
235
            args: [],
236
            returns: ValueKind.Void,
237
        },
238
        impl: (interpreter: Interpreter) => {
239
            this.elements = [];
6✔
240
            return BrsInvalid.Instance;
6✔
241
        },
242
    });
243

244
    private append = new Callable("append", {
1,609✔
245
        signature: {
246
            args: [new StdlibArgument("array", ValueKind.Object)],
247
            returns: ValueKind.Void,
248
        },
249
        impl: (interpreter: Interpreter, array: BrsComponent) => {
250
            if (!(array instanceof RoArray)) {
2!
251
                // TODO: validate against RBI
252
                return BrsInvalid.Instance;
×
253
            }
254

255
            this.elements = [
2✔
256
                ...this.elements,
257
                ...array.elements.filter((element) => !!element), // don't copy "holes" where no value exists
10✔
258
            ];
259

260
            return BrsInvalid.Instance;
2✔
261
        },
262
    });
263
    // ifArrayJoin
264
    private join = new Callable("join", {
1,609✔
265
        signature: {
266
            args: [new StdlibArgument("separator", ValueKind.String)],
267
            returns: ValueKind.String,
268
        },
269
        impl: (interpreter: Interpreter, separator: BrsString) => {
270
            if (
3✔
271
                this.elements.some(function (element) {
272
                    return !(element instanceof BrsString);
8✔
273
                })
274
            ) {
275
                interpreter.stderr.write("roArray.Join: Array contains non-string value(s).\n");
2✔
276
                return new BrsString("");
2✔
277
            }
278
            return new BrsString(this.elements.join(separator.value));
1✔
279
        },
280
    });
281
    // ifArraySort
282
    private sort = new Callable("sort", {
1,609✔
283
        signature: {
284
            args: [new StdlibArgument("flags", ValueKind.String, new BrsString(""))],
285
            returns: ValueKind.Void,
286
        },
287
        impl: (interpreter: Interpreter, flags: BrsString) => {
288
            if (flags.toString().match(/([^ir])/g) != null) {
12✔
289
                interpreter.stderr.write("roArray.Sort: Flags contains invalid option(s).\n");
1✔
290
            } else {
291
                const caseInsensitive = flags.toString().indexOf("i") > -1;
11✔
292
                const originalArrayCopy = [...this.elements];
11✔
293
                this.elements = this.elements.sort((a, b) => {
11✔
294
                    return sortCompare(originalArrayCopy, a, b, caseInsensitive);
158✔
295
                });
296
                if (flags.toString().indexOf("r") > -1) {
11✔
297
                    this.elements = this.elements.reverse();
5✔
298
                }
299
            }
300
            return BrsInvalid.Instance;
12✔
301
        },
302
    });
303

304
    private sortBy = new Callable("sortBy", {
1,609✔
305
        signature: {
306
            args: [
307
                new StdlibArgument("fieldName", ValueKind.String),
308
                new StdlibArgument("flags", ValueKind.String, new BrsString("")),
309
            ],
310
            returns: ValueKind.Void,
311
        },
312
        impl: (interpreter: Interpreter, fieldName: BrsString, flags: BrsString) => {
313
            if (flags.toString().match(/([^ir])/g) != null) {
13✔
314
                interpreter.stderr.write("roArray.SortBy: Flags contains invalid option(s).\n");
1✔
315
            } else {
316
                const caseInsensitive = flags.toString().indexOf("i") > -1;
12✔
317
                const originalArrayCopy = [...this.elements];
12✔
318
                this.elements = this.elements.sort((a, b) => {
12✔
319
                    let compare = 0;
206✔
320
                    if (a instanceof RoAssociativeArray && b instanceof RoAssociativeArray) {
206✔
321
                        const aHasField = a.elements.has(fieldName.toString().toLowerCase());
37✔
322
                        const bHasField = b.elements.has(fieldName.toString().toLowerCase());
37✔
323
                        if (aHasField && bHasField) {
37✔
324
                            const valueA = a.get(fieldName);
29✔
325
                            const valueB = b.get(fieldName);
29✔
326
                            compare = sortCompare(
29✔
327
                                originalArrayCopy,
328
                                valueA,
329
                                valueB,
330
                                caseInsensitive
331
                            );
332
                        } else if (aHasField) {
8✔
333
                            // assocArray with fields come before assocArrays without
334
                            compare = -1;
1✔
335
                        } else if (bHasField) {
7✔
336
                            // assocArray with fields come before assocArrays without
337
                            compare = 1;
1✔
338
                        }
339
                    } else if (a !== undefined && b !== undefined) {
169✔
340
                        compare = sortCompare(originalArrayCopy, a, b, false, false);
169✔
341
                    }
342
                    return compare;
206✔
343
                });
344
                if (flags.toString().indexOf("r") > -1) {
12✔
345
                    this.elements = this.elements.reverse();
5✔
346
                }
347
            }
348
            return BrsInvalid.Instance;
13✔
349
        },
350
    });
351

352
    private reverse = new Callable("reverse", {
1,609✔
353
        signature: {
354
            args: [],
355
            returns: ValueKind.Void,
356
        },
357
        impl: (interpreter: Interpreter, separator: BrsString) => {
358
            this.elements = this.elements.reverse();
1✔
359
            return BrsInvalid.Instance;
1✔
360
        },
361
    });
362
    // ifEnum
363
    private isEmpty = new Callable("isEmpty", {
1,609✔
364
        signature: {
365
            args: [],
366
            returns: ValueKind.Boolean,
367
        },
368
        impl: (interpreter: Interpreter) => {
369
            return BrsBoolean.from(this.elements.length === 0);
1✔
370
        },
371
    });
372
}
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