Skip to content

Commit

Permalink
Implement ES2024 groupBy
Browse files Browse the repository at this point in the history
  • Loading branch information
camnwalter authored and gbrail committed Aug 28, 2024
1 parent c0936ca commit 4e6523b
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.mozilla.javascript;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Abstract Object Operations as defined by EcmaScript
*
Expand All @@ -24,6 +29,11 @@ enum INTEGRITY_LEVEL {
SEALED
}

enum KEY_COERCION {
PROPERTY,
COLLECTION,
}

/**
* Implementation of Abstract Object operation HasOwnProperty as defined by EcmaScript
*
Expand Down Expand Up @@ -232,4 +242,65 @@ static void put(Context cx, Scriptable o, int p, Object v, boolean isThrow) {
base.put(p, o, v);
}
}

/**
* Implement the ECMAScript abstract operation "GroupBy" defined in section 7.3.35 of ECMA262.
*
* @param cx
* @param scope
* @param items
* @param callback
* @param keyCoercion
* @see <a href="https://tc39.es/ecma262/#sec-groupby"></a>
*/
static Map<Object, List<Object>> groupBy(
Context cx,
Scriptable scope,
IdFunctionObject f,
Object items,
Object callback,
KEY_COERCION keyCoercion) {
if (cx.getLanguageVersion() >= Context.VERSION_ES6) {
ScriptRuntimeES6.requireObjectCoercible(cx, items, f);
}
if (!(callback instanceof Callable)) {
throw ScriptRuntime.typeErrorById(
"msg.isnt.function", callback, ScriptRuntime.typeof(callback));
}

// LinkedHashMap used to preserve key creation order
Map<Object, List<Object>> groups = new LinkedHashMap<>();
final Object iterator = ScriptRuntime.callIterator(items, cx, scope);
try (IteratorLikeIterable it = new IteratorLikeIterable(cx, scope, iterator)) {
double i = 0;
for (Object o : it) {
if (i > NativeNumber.MAX_SAFE_INTEGER) {
it.close();
throw ScriptRuntime.typeError("Too many values to iterate");
}

Object[] args = {o, i};
Object key =
((Callable) callback).call(cx, scope, Undefined.SCRIPTABLE_UNDEFINED, args);
if (keyCoercion == KEY_COERCION.PROPERTY) {
if (!ScriptRuntime.isSymbol(key)) {
key = ScriptRuntime.toString(key);
}
} else {
assert keyCoercion == KEY_COERCION.COLLECTION;
if ((key instanceof Number)
&& ((Number) key).doubleValue() == ScriptRuntime.negativeZero) {
key = ScriptRuntime.zeroObj;
}
}

List<Object> group = groups.computeIfAbsent(key, (k) -> new ArrayList<>());
group.add(o);

i++;
}
}

return groups;
}
}
36 changes: 35 additions & 1 deletion rhino/src/main/java/org/mozilla/javascript/NativeMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

package org.mozilla.javascript;

import java.util.List;
import java.util.Map;

public class NativeMap extends IdScriptableObject {
private static final long serialVersionUID = 1171922614280016891L;
private static final Object MAP_TAG = "Map";
Expand Down Expand Up @@ -37,6 +40,12 @@ public String getClassName() {
return "Map";
}

@Override
public void fillConstructorProperties(IdFunctionObject ctor) {
addIdFunctionProperty(ctor, MAP_TAG, ConstructorId_groupBy, "groupBy", 2);
super.fillConstructorProperties(ctor);
}

@Override
public Object execIdCall(
IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Expand Down Expand Up @@ -82,6 +91,30 @@ public Object execIdCall(
args.length > 1 ? args[1] : Undefined.instance);
case SymbolId_getSize:
return realThis(thisObj, f).js_getSize();

case ConstructorId_groupBy:
{
Object items = args.length < 1 ? Undefined.instance : args[0];
Object callback = args.length < 2 ? Undefined.instance : args[1];

Map<Object, List<Object>> groups =
AbstractEcmaObjectOperations.groupBy(
cx,
scope,
f,
items,
callback,
AbstractEcmaObjectOperations.KEY_COERCION.COLLECTION);

NativeMap map = (NativeMap) cx.newObject(scope, "Map");

for (Map.Entry<Object, List<Object>> entry : groups.entrySet()) {
Scriptable elements = cx.newArray(scope, entry.getValue().toArray());
map.entries.put(entry.getKey(), elements);
}

return map;
}
}
throw new IllegalArgumentException("Map.prototype has no method: " + f.getFunctionName());
}
Expand Down Expand Up @@ -315,7 +348,8 @@ protected int findPrototypeId(String s) {

// Note that "SymbolId_iterator" is not present here. That's because the spec
// requires that it be the same value as the "entries" prototype property.
private static final int Id_constructor = 1,
private static final int ConstructorId_groupBy = -1,
Id_constructor = 1,
Id_set = 2,
Id_get = 3,
Id_delete = 4,
Expand Down
34 changes: 34 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/NativeObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
Expand Down Expand Up @@ -85,6 +86,7 @@ protected void fillConstructorProperties(IdFunctionObject ctor) {
addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_freeze, "freeze", 1);
addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_assign, "assign", 2);
addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_is, "is", 2);
addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_groupBy, "groupBy", 2);
super.fillConstructorProperties(ctor);
}

Expand Down Expand Up @@ -687,6 +689,37 @@ && isEnumerable(stringId, sourceObj)) {
return ScriptRuntime.wrapBoolean(ScriptRuntime.same(a1, a2));
}

case ConstructorId_groupBy:
{
Object items = args.length < 1 ? Undefined.instance : args[0];
Object callback = args.length < 2 ? Undefined.instance : args[1];

Map<Object, List<Object>> groups =
AbstractEcmaObjectOperations.groupBy(
cx,
scope,
f,
items,
callback,
AbstractEcmaObjectOperations.KEY_COERCION.PROPERTY);

NativeObject obj = (NativeObject) cx.newObject(scope);
obj.setPrototype(null);

for (Map.Entry<Object, List<Object>> entry : groups.entrySet()) {
Scriptable elements = cx.newArray(scope, entry.getValue().toArray());

ScriptableObject desc = (ScriptableObject) cx.newObject(scope);
desc.put("enumerable", desc, Boolean.TRUE);
desc.put("configurable", desc, Boolean.TRUE);
desc.put("value", desc, elements);

obj.defineOwnProperty(cx, entry.getKey(), desc);
}

return obj;
}

default:
throw new IllegalArgumentException(String.valueOf(id));
}
Expand Down Expand Up @@ -1021,6 +1054,7 @@ protected int findPrototypeId(String s) {
ConstructorId_fromEntries = -20,
ConstructorId_values = -21,
ConstructorId_hasOwn = -22,
ConstructorId_groupBy = -23,
Id_constructor = 1,
Id_toString = 2,
Id_toLocaleString = 3,
Expand Down
30 changes: 3 additions & 27 deletions tests/testsrc/test262.properties
Original file line number Diff line number Diff line change
Expand Up @@ -991,19 +991,7 @@ built-ins/JSON 37/144 (25.69%)
stringify/value-object-proxy-revoked.js {unsupported: [Proxy]}
stringify/value-string-escape-unicode.js

built-ins/Map 25/171 (14.62%)
groupBy/callback-arg.js
groupBy/callback-throws.js
groupBy/emptyList.js
groupBy/evenOdd.js
groupBy/groupLength.js
groupBy/iterator-next-throws.js
groupBy/length.js
groupBy/map-instance.js
groupBy/name.js
groupBy/negativeZero.js
groupBy/string.js
groupBy/toPropertyKey.js
built-ins/Map 13/171 (7.6%)
prototype/clear/not-a-constructor.js {unsupported: [Reflect.construct]}
prototype/delete/not-a-constructor.js {unsupported: [Reflect.construct]}
prototype/entries/not-a-constructor.js {unsupported: [Reflect.construct]}
Expand Down Expand Up @@ -1110,7 +1098,7 @@ built-ins/Number 24/335 (7.16%)
S9.3.1_A3_T1_U180E.js {unsupported: [u180e]}
S9.3.1_A3_T2_U180E.js {unsupported: [u180e]}

built-ins/Object 230/3403 (6.76%)
built-ins/Object 218/3403 (6.41%)
assign/assignment-to-readonly-property-of-target-must-throw-a-typeerror-exception.js
assign/not-a-constructor.js {unsupported: [Reflect.construct]}
assign/source-own-prop-desc-missing.js {unsupported: [Proxy]}
Expand Down Expand Up @@ -1200,18 +1188,6 @@ built-ins/Object 230/3403 (6.76%)
getOwnPropertySymbols/proxy-invariant-not-extensible-absent-string-key.js {unsupported: [Proxy]}
getOwnPropertySymbols/proxy-invariant-not-extensible-extra-string-key.js {unsupported: [Proxy]}
getPrototypeOf/not-a-constructor.js {unsupported: [Reflect.construct]}
groupBy/callback-arg.js
groupBy/callback-throws.js
groupBy/emptyList.js
groupBy/evenOdd.js
groupBy/groupLength.js
groupBy/invalid-property-key.js
groupBy/iterator-next-throws.js
groupBy/length.js
groupBy/name.js
groupBy/null-prototype.js
groupBy/string.js
groupBy/toPropertyKey.js
hasOwn/length.js
hasOwn/not-a-constructor.js {unsupported: [Reflect.construct]}
hasOwn/symbol_property_toPrimitive.js
Expand Down Expand Up @@ -5306,7 +5282,7 @@ language/expressions/new 41/59 (69.49%)

~language/expressions/new.target

language/expressions/object 867/1169 (74.17%)
language/expressions/object 866/1169 (74.08%)
dstr/async-gen-meth-ary-init-iter-close.js {unsupported: [async-iteration, async]}
dstr/async-gen-meth-ary-init-iter-get-err.js {unsupported: [async-iteration]}
dstr/async-gen-meth-ary-init-iter-get-err-array-prototype.js {unsupported: [async-iteration]}
Expand Down

0 comments on commit 4e6523b

Please sign in to comment.