Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ declare class JsonValue {
private items: JsonValue[];
private names: string[];
private absent: JsonValue[];
private slots: int[];

constructor();

Expand Down
81 changes: 78 additions & 3 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,8 @@ const JSON_ARRAY = 4;
const JSON_OBJECT = 5;
// A parser and a writer that recurse: past this a deep input would run off the native stack.
const JSON_MAX_DEPTH = 512;
// An object with this many members or more finds a member by its name through a table.
const JSON_INDEX_FROM = 16;

function jsonAppend(output: char[], text: string): void {
// the length once: it is a walk to the text's end each time it is asked for
Expand Down Expand Up @@ -2327,6 +2329,10 @@ export class JsonValue {
// holds the JSON null `at` and `get` give for what is not there: under -mm=own what they return
// is lent by this value, so a new one could not go back the same way
private absent: JsonValue[];
// For an object of JSON_INDEX_FROM members or more: an open-addressed table of the names, each
// slot a name's position plus one, 0 an empty one. Empty for anything smaller. Not a Map: one
// built into the library would be built again into a program that uses the same Map.
private slots: int[];

// JSON's null
public constructor() {
Expand All @@ -2337,6 +2343,7 @@ export class JsonValue {
this.items = [];
this.names = [];
this.absent = [];
this.slots = [];
}

public static ofBoolean(value: boolean): JsonValue {
Expand Down Expand Up @@ -2494,6 +2501,7 @@ export class JsonValue {
position = this.items.length;
// a copy: only this path takes the key, and under -mm=own a parameter is taken on every path or none
this.names.push(key + "");
this.indexAdded(position);
this.items.length = position + 1;
}
}
Expand All @@ -2519,6 +2527,8 @@ export class JsonValue {
result.names.push(name + "");
}

// the copy finds its names as this one does
result.indexAdded(result.names.length - 1);
return result;
}

Expand Down Expand Up @@ -2562,12 +2572,77 @@ export class JsonValue {
return this.absent[0];
}

// Where the key is among the names, or -1. A few names are looked through; from JSON_INDEX_FROM
// on they are found through `slots`, as looking through them for every member made reading an
// object of n members cost n * n.
private indexOf(key: string): int {
for (let i = 0; i < this.names.length; i++) {
if (this.names[i] == key) return i;
const size: int = this.slots.length;
if (size == 0) {
const count: int = this.names.length;
for (let i = 0; i < count; i++) {
if (this.names[i] == key) return i;
}

return -1;
}

return -1;
let slot: int = <int>(<uint>HashHelpers.hashCodeString(key) % <uint>size);
while (true) {
const at = this.slots[slot];
if (at == 0) {
return -1;
}

if (this.names[at - 1] == key) {
return at - 1;
}

slot++;
if (slot == size) {
slot = 0;
}
}
}

// puts the name at `position` into the table, which has a free slot for it
private addSlot(position: int): void {
const size: int = this.slots.length;
let slot: int = <int>(<uint>HashHelpers.hashCodeString(this.names[position]) % <uint>size);
while (this.slots[slot] != 0) {
slot++;
if (slot == size) {
slot = 0;
}
}

this.slots[slot] = position + 1;
}

// Called once a name has been added at `position`. The table is made when there are
// JSON_INDEX_FROM names, and again, twice the size, whenever it is half full: it then holds
// every name, so indexOf only reads it.
private indexAdded(position: int): void {
const count: int = this.names.length;
if (count < JSON_INDEX_FROM) {
return;
}

if (this.slots.length >= count * 2) {
this.addSlot(position);
return;
}

const size: int = count * 4;
let slots: int[] = [];
slots.length = size;
for (let i = 0; i < size; i++) {
slots[i] = 0;
}

this.slots = slots;
for (let i = 0; i < count; i++) {
this.addSlot(i);
}
}

private expect(tag: int, name: string): void {
Expand Down
67 changes: 67 additions & 0 deletions tests/json_many_keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// An object with many members finds each by its name through a table (16 members or more), where
// every name was looked through: reading an object of n members cost n * n.
function build(count: int): string {
let text = "{";
for (let i = 0; i < count; i++) {
if (i > 0) text += ",";
text += `"key${i}":${i}`;
}

return text + "}";
}

function check(count: int) {
const value = JSON.parse(build(count));
assert(value.length == count, "every member is there");
for (let i = 0; i < count; i++) {
assert(value.has(`key${i}`), "has each key");
assert(value.get(`key${i}`).asNumber() == i, "each key has its own value");
}

assert(!value.has("key"), "a name that is the start of others");
assert(!value.has(`key${count}`), "one past the last");
assert(value.get("missing").isNull(), "null for what is not there");
}

function main() {
// around the size the table starts at, and well past it, where it has grown several times
check(0);
check(1);
check(15);
check(16);
check(17);
check(33);
check(5000);

// a name given twice keeps its place and takes the last value, small and large
const twice = JSON.parse(`{"a":1,"b":2,"a":3}`);
assert(twice.length == 2 && twice.get("a").asNumber() == 3, "the last value, a few members");

const large = JSON.parse(build(100));
large.set("key7", JsonValue.ofNumber(700));
assert(large.length == 100, "a member set again is not added");
assert(large.get("key7").asNumber() == 700, "and has the new value");
large.set("extra", JsonValue.ofString("x"));
assert(large.length == 101 && large.get("extra").asString() == "x", "a member added");
assert(large.get("key99").asNumber() == 99, "the others are as they were");

// built by hand, past the size the table starts at
const made = JsonValue.newObject();
for (let i = 0; i < 40; i++) {
made.set(`n${i}`, JsonValue.ofNumber(i * 2));
}

assert(made.get("n39").asNumber() == 78 && made.get("n0").asNumber() == 0, "built by hand");
const names = made.keys();
assert(names.length == 40 && names[0] == "n0" && names[39] == "n39", "the keys keep their order");

// a copy finds its members too
const copy = made.clone();
assert(copy.length == 40 && copy.get("n20").asNumber() == 40 && !copy.has("n40"), "a copy");

// and the text is the same after a round trip
const text = made.stringify();
assert(JSON.parse(text).stringify() == text, "a round trip");

console.log("ALL DONE");
}
Loading