Browse Source

LibJS: Implement Array.prototype.groupBy

Luke Wilde 3 years ago
parent
commit
48cc1c97d5

+ 86 - 1
Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp

@@ -72,6 +72,7 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
     define_native_function(vm.names.keys, keys, 0, attr);
     define_native_function(vm.names.keys, keys, 0, attr);
     define_native_function(vm.names.entries, entries, 0, attr);
     define_native_function(vm.names.entries, entries, 0, attr);
     define_native_function(vm.names.copyWithin, copy_within, 2, attr);
     define_native_function(vm.names.copyWithin, copy_within, 2, attr);
+    define_native_function(vm.names.groupBy, group_by, 1, attr);
 
 
     // Use define_direct_property here instead of define_native_function so that
     // Use define_direct_property here instead of define_native_function so that
     // Object.is(Array.prototype[Symbol.iterator], Array.prototype.values)
     // Object.is(Array.prototype[Symbol.iterator], Array.prototype.values)
@@ -80,7 +81,8 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
     define_direct_property(*vm.well_known_symbol_iterator(), get_without_side_effects(vm.names.values), attr);
     define_direct_property(*vm.well_known_symbol_iterator(), get_without_side_effects(vm.names.values), attr);
 
 
     // 23.1.3.35 Array.prototype [ @@unscopables ], https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
     // 23.1.3.35 Array.prototype [ @@unscopables ], https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
-    // With proposal, https://tc39.es/proposal-array-find-from-last/#sec-array.prototype-@@unscopables
+    // With find from last proposal, https://tc39.es/proposal-array-find-from-last/#sec-array.prototype-@@unscopables
+    // With array grouping proposal, https://tc39.es/proposal-array-grouping/#sec-array.prototype-@@unscopables
     auto* unscopable_list = Object::create(global_object, nullptr);
     auto* unscopable_list = Object::create(global_object, nullptr);
     MUST(unscopable_list->create_data_property_or_throw(vm.names.at, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.at, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.copyWithin, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.copyWithin, Value(true)));
@@ -92,6 +94,7 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
     MUST(unscopable_list->create_data_property_or_throw(vm.names.findLastIndex, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.findLastIndex, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.flat, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.flat, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.flatMap, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.flatMap, Value(true)));
+    MUST(unscopable_list->create_data_property_or_throw(vm.names.groupBy, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.includes, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.includes, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.keys, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.keys, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.values, Value(true)));
     MUST(unscopable_list->create_data_property_or_throw(vm.names.values, Value(true)));
@@ -1665,4 +1668,86 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::at)
     return TRY(this_object->get(index.value()));
     return TRY(this_object->get(index.value()));
 }
 }
 
 
+// 2.3 AddValueToKeyedGroup ( groups, key, value ), https://tc39.es/proposal-array-grouping/#sec-add-value-to-keyed-group
+template<typename GroupsType, typename KeyType>
+static void add_value_to_keyed_group(GlobalObject& global_object, GroupsType& groups, KeyType key, Value value)
+{
+    // 1. For each Record { [[Key]], [[Elements]] } g of groups, do
+    //      a. If ! SameValue(g.[[Key]], key) is true, then
+    //      NOTE: This is performed in KeyedGroupTraits::equals for groupByToMap and Traits<JS::PropertyKey>::equals for groupBy.
+    auto existing_elements_iterator = groups.find(key);
+    if (existing_elements_iterator != groups.end()) {
+        // i. Assert: exactly one element of groups meets this criteria.
+        // NOTE: This is done on insertion into the hash map, as only `set` tells us if we overrode an entry.
+
+        // ii. Append value as the last element of g.[[Elements]].
+        existing_elements_iterator->value.append(value);
+
+        // iii. Return.
+        return;
+    }
+
+    // 2. Let group be the Record { [[Key]]: key, [[Elements]]: « value » }.
+    MarkedValueList new_elements { global_object.heap() };
+    new_elements.append(value);
+
+    // 3. Append group as the last element of groups.
+    auto result = groups.set(key, move(new_elements));
+    VERIFY(result == AK::HashSetResult::InsertedNewEntry);
+}
+
+// 2.1 Array.prototype.groupBy ( callbackfn [ , thisArg ] ), https://tc39.es/proposal-array-grouping/#sec-array.prototype.groupby
+JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::group_by)
+{
+    auto callback_function = vm.argument(0);
+    auto this_arg = vm.argument(1);
+
+    // 1. Let O be ? ToObject(this value).
+    auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
+
+    // 2. Let len be ? LengthOfArrayLike(O).
+    auto length = TRY(length_of_array_like(global_object, *this_object));
+
+    // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
+    if (!callback_function.is_function())
+        return vm.throw_completion<TypeError>(global_object, ErrorType::NotAFunction, callback_function.to_string_without_side_effects());
+
+    // 5. Let groups be a new empty List.
+    OrderedHashMap<PropertyKey, MarkedValueList> groups;
+
+    // 4. Let k be 0.
+    // 6. Repeat, while k < len
+    for (size_t index = 0; index < length; ++index) {
+        // a. Let Pk be ! ToString(𝔽(k)).
+        auto index_property = PropertyKey { index };
+
+        // b. Let kValue be ? Get(O, Pk).
+        auto k_value = TRY(this_object->get(index_property));
+
+        // c. Let propertyKey be ? ToPropertyKey(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)).
+        auto property_key_value = TRY(vm.call(callback_function.as_function(), this_arg, k_value, Value(index), this_object));
+        auto property_key = TRY(property_key_value.to_property_key(global_object));
+
+        // d. Perform ! AddValueToKeyedGroup(groups, propertyKey, kValue).
+        add_value_to_keyed_group(global_object, groups, property_key, k_value);
+
+        // e. Set k to k + 1.
+    }
+
+    // 7. Let obj be ! OrdinaryObjectCreate(null).
+    auto* object = Object::create(global_object, nullptr);
+
+    // 8. For each Record { [[Key]], [[Elements]] } g of groups, do
+    for (auto& group : groups) {
+        // a. Let elements be ! CreateArrayFromList(g.[[Elements]]).
+        auto* elements = Array::create_from(global_object, group.value);
+
+        // b. Perform ! CreateDataPropertyOrThrow(obj, g.[[Key]], elements).
+        MUST(object->create_data_property_or_throw(group.key, elements));
+    }
+
+    // 9. Return obj.
+    return object;
+}
+
 }
 }

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ArrayPrototype.h

@@ -54,6 +54,7 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(keys);
     JS_DECLARE_NATIVE_FUNCTION(keys);
     JS_DECLARE_NATIVE_FUNCTION(entries);
     JS_DECLARE_NATIVE_FUNCTION(entries);
     JS_DECLARE_NATIVE_FUNCTION(copy_within);
     JS_DECLARE_NATIVE_FUNCTION(copy_within);
+    JS_DECLARE_NATIVE_FUNCTION(group_by);
 };
 };
 
 
 }
 }

+ 1 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -241,6 +241,7 @@ namespace JS {
     P(global)                                \
     P(global)                                \
     P(globalThis)                            \
     P(globalThis)                            \
     P(group)                                 \
     P(group)                                 \
+    P(groupBy)                               \
     P(groupCollapsed)                        \
     P(groupCollapsed)                        \
     P(groupEnd)                              \
     P(groupEnd)                              \
     P(groups)                                \
     P(groups)                                \

+ 13 - 0
Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype-generic-functions.js

@@ -304,4 +304,17 @@ describe("ability to work with generic non-array objects", () => {
             2,
             2,
         ]);
         ]);
     });
     });
+
+    test("groupBy", () => {
+        const visited = [];
+        const o = { length: 5, 0: "foo", 1: "bar", 3: "baz" };
+        const result = Array.prototype.groupBy.call(o, (value, _, object) => {
+            expect(object).toBe(o);
+            visited.push(value);
+            return value !== undefined ? value.startsWith("b") : false;
+        });
+        expect(visited).toEqual(["foo", "bar", undefined, "baz", undefined]);
+        expect(result.false).toEqual(["foo", undefined, undefined]);
+        expect(result.true).toEqual(["bar", "baz"]);
+    });
 });
 });

+ 101 - 0
Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.groupBy.js

@@ -0,0 +1,101 @@
+test("length is 1", () => {
+    expect(Array.prototype.groupBy).toHaveLength(1);
+});
+
+describe("errors", () => {
+    test("callback must be a function", () => {
+        expect(() => {
+            [].groupBy(undefined);
+        }).toThrowWithMessage(TypeError, "undefined is not a function");
+    });
+
+    test("null or undefined this value", () => {
+        expect(() => {
+            Array.prototype.groupBy.call();
+        }).toThrowWithMessage(TypeError, "ToObject on null or undefined");
+
+        expect(() => {
+            Array.prototype.groupBy.call(undefined);
+        }).toThrowWithMessage(TypeError, "ToObject on null or undefined");
+
+        expect(() => {
+            Array.prototype.groupBy.call(null);
+        }).toThrowWithMessage(TypeError, "ToObject on null or undefined");
+    });
+});
+
+describe("normal behavior", () => {
+    test("basic functionality", () => {
+        const array = [1, 2, 3, 4, 5, 6];
+        const visited = [];
+
+        const firstResult = array.groupBy(value => {
+            visited.push(value);
+            return value % 2 === 0;
+        });
+
+        expect(visited).toEqual([1, 2, 3, 4, 5, 6]);
+        expect(firstResult.true).toEqual([2, 4, 6]);
+        expect(firstResult.false).toEqual([1, 3, 5]);
+
+        const firstKeys = Object.keys(firstResult);
+        expect(firstKeys).toHaveLength(2);
+        expect(firstKeys[0]).toBe("false");
+        expect(firstKeys[1]).toBe("true");
+
+        const secondResult = array.groupBy((_, index) => {
+            return index < array.length / 2;
+        });
+
+        expect(secondResult.true).toEqual([1, 2, 3]);
+        expect(secondResult.false).toEqual([4, 5, 6]);
+
+        const secondKeys = Object.keys(secondResult);
+        expect(secondKeys).toHaveLength(2);
+        expect(secondKeys[0]).toBe("true");
+        expect(secondKeys[1]).toBe("false");
+
+        const thisArg = [7, 8, 9, 10, 11, 12];
+        const thirdResult = array.groupBy(function (_, __, arrayVisited) {
+            expect(arrayVisited).toBe(array);
+            expect(this).toBe(thisArg);
+        }, thisArg);
+
+        expect(thirdResult.undefined).not.toBe(array);
+        expect(thirdResult.undefined).not.toBe(thisArg);
+        expect(thirdResult.undefined).toEqual(array);
+
+        const thirdKeys = Object.keys(thirdResult);
+        expect(thirdKeys).toHaveLength(1);
+        expect(thirdKeys[0]).toBe("undefined");
+    });
+
+    test("is unscopable", () => {
+        expect(Array.prototype[Symbol.unscopables].groupBy).toBeTrue();
+        const array = [];
+        with (array) {
+            expect(() => {
+                groupBy;
+            }).toThrowWithMessage(ReferenceError, "'groupBy' is not defined");
+        }
+    });
+
+    test("never calls callback with empty array", () => {
+        var callbackCalled = 0;
+        expect(
+            [].groupBy(() => {
+                callbackCalled++;
+            })
+        ).toEqual({});
+        expect(callbackCalled).toBe(0);
+    });
+
+    test("calls callback once for every item", () => {
+        var callbackCalled = 0;
+        const result = [1, 2, 3].groupBy(() => {
+            callbackCalled++;
+        });
+        expect(result.undefined).toEqual([1, 2, 3]);
+        expect(callbackCalled).toBe(3);
+    });
+});