thefourtheye's weblog

opinions are my own; try code/suggestions at your own risk

TypedArrays and Canonical Numeric Index Strings

| Comments

This week, I saw a Tweet by Benedikt Meurer which was kind of a quiz.

At the first look, I thought all of them would be the properties of the object and so all are correct answers. This comes from my understanding of the Array objects. As per the specification’s 9.4.2 Array Exotic Objects section,

A String property name P is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 232-1.

Any String property name which when converted to an unsigned 32 bit integer and stringified again should be equal to the original String property name, to be qualified as a valid array index. Otherwise it will be treated as a normal property of the array object.

But this is not applicable to TypedArrays. To TypedArrays the array indices in string format, should be canonical string numeric indices. In ECMAScript 6 specification, value of a property retrieval from a typed array is defined like this

When the [[Get]] internal method of an Integer Indexed exotic object (Typed Arrays) O is called with property key P, as per the section 9.4.5.4 [[Get]] (P, Receiver) the following steps are taken:

  1. Assert: IsPropertyKey(P) is true.
  2. If Type(P) is String and if SameValue(O, Receiver) is true, then
    a. Let numericIndex be CanonicalNumericIndexString (P).
    b. Assert: numericIndex is not an abrupt completion.
    c. If numericIndex is not undefined, then
    i. Return IntegerIndexedElementGet (O, numericIndex).
  3. Return the result of calling the default ordinary object [[Get]] internal method (9.1.8) on O passing P and Receiver as arguments.

As we see here, if the property being retrieved is a String, it is first converted to its canonical numeric index string format. It is done as per the section 7.1.16 CanonicalNumericIndexString(argument)

  1. Assert: Type(argument) is String.
  2. If argument is “-0”, return −0.
  3. Let n be ToNumber(argument).
  4. If SameValue(ToString(n), argument) is false, return undefined.
  5. Return n.

This basically converts the string to a number object and then compares the stringified version of that number with the original string. If they are equal, then it is a canonical numeric index and the number is returned, otherwise undefined will be returned. For example, '3.14' will be converted to numeric 3.14 and then converted to string again, which will be '3.14' which is the same as the original string. So the number 3.14 it will be considered as an index of the typed array.

In the question we see above, the strings '.9' and '1.0' become '0.9' and 1 respectively and '1.1' and '1.2' will become the same. We can confirm that like this

['.9', '0.9', '1.0', '1.', '1.1', '1.2'].forEach(i => console.log(Number(i).toString()));
// will print the following
0.9
0.9
1
1
1.1
1.2

As the strings '1.1' and '1.2' are already in the canonical numeric index string format, they are considered as the array indices and the '0.9' and '1.0' become property names. As per the 9.4.5.8 IntegerIndexedElementGet section, the passed number should be an integer. So, 1.1 and 1.2 are also not valid array indices and they are ignored. That is why the answer to the question is, both array['.9'] and array['1.0'], as only '.9' and '1.0' are treated as property names during retrieval.

To confirm this behaviour I wrote the following programs.

function TestIntegerIndexedObjects(obj) {
    console.log(Object.prototype.toString.call(obj));
    const items = ['.9', '1.0', '1.1', '1.2'];
    items.forEach((i, idx) => obj[i] = idx);
    console.log(obj);
    console.log(Object.getOwnPropertyNames(obj));
    console.log('\n');
}

TestIntegerIndexedObjects(new Int8Array(10));
TestIntegerIndexedObjects(new String("abcd"));
TestIntegerIndexedObjects([]);

will print the following

[object Int8Array]
Int8Array [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '.9': 0, '1.0': 1 ]
[ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.9', '1.0' ]


[object String]
[String: 'abcd']
[ '0', '1', '2', '3', 'length', '.9', '1.0', '1.1', '1.2' ]


[object Array]
[ '.9': 0, '1.0': 1, '1.1': 2, '1.2': 3 ]
[ 'length', '.9', '1.0', '1.1', '1.2' ]

CanonicalNumericIndexString

const assert = require('assert');

function CanonicalNumericIndexString(argument) {
    const NumericValue = +argument;                                          // The unary + operator converts to a number
    return NumericValue.toString() === argument ? NumericValue : undefined;
}

assert(CanonicalNumericIndexString('.9')  === undefined);
assert(CanonicalNumericIndexString('0.9') === 0.9);
assert(CanonicalNumericIndexString('1.0') === undefined);
assert(CanonicalNumericIndexString('1.')  === undefined);
assert(CanonicalNumericIndexString('1.1') === 1.1);
assert(CanonicalNumericIndexString('1.2') === 1.2);

Comments