diff --git a/docs/mkdocs/docs/api/basic_json_document/erase.md b/docs/mkdocs/docs/api/basic_json_document/erase.md index 25249fa73..cbdc79765 100644 --- a/docs/mkdocs/docs/api/basic_json_document/erase.md +++ b/docs/mkdocs/docs/api/basic_json_document/erase.md @@ -86,8 +86,8 @@ view of a *different* document (overloads 1-2 only; overload 3 always starts fro !!! info "Duplicate keys" - Overload 1. removes *every* member with `key`, not just the last one that lookups find -- unlike [`set`](set.md), - which assigns that member and drops the rest. This is why it returns a count rather than a single view: there may be + Overload 1. removes *every* member with `key`, not just the first -- unlike [`set`](set.md), which assigns the + first occurrence and drops the rest. This is why it returns a count rather than a single view: there may be more than one member removed, or none. Like [`set`](set.md) and [`push_back`](push_back.md), `erase` never moves an element's *value*: a view still diff --git a/docs/mkdocs/docs/api/basic_json_document/set.md b/docs/mkdocs/docs/api/basic_json_document/set.md index 3f29a3f6f..ae26ed516 100644 --- a/docs/mkdocs/docs/api/basic_json_document/set.md +++ b/docs/mkdocs/docs/api/basic_json_document/set.md @@ -23,8 +23,7 @@ has `set`; calling it on a read-only `basic_json_document` fails to compile (`#! 1. Replaces the value `target` refers to with `value`. 2. Sets the member `key` of the object `object` to `value`: assigns it if `object` already has a member with this - key -- the last one, should the key occur more than once (the member - [`operator[]`](../basic_json_view/operator%5B%5D.md) returns), and the other duplicates are then dropped (see the + key -- the first one, should the key occur more than once, and the later duplicates are then dropped (see the [Notes](#notes) below) -- or appends a new member at the end otherwise. A [null](../basic_json_view/is_null.md) `object` first becomes an empty object. 3. Assigns `value` to the element at index `idx` of the array `array`, which must already exist (`#!cpp idx < @@ -147,13 +146,12 @@ document") if `target`/`object`/`array` is a [discarded](../basic_json_view/is_d !!! info "Duplicate keys" - If `object` already has more than one member with `key` (2.), `value` is assigned to the *last* one -- the member - [`operator[]`](../basic_json_view/operator%5B%5D.md), [`at`](../basic_json_view/at.md), and - [`find`](../basic_json_view/find.md) return for reading, so that a view taken from `object["key"]` before the call - shows `value` afterward -- and every other member with the same key is removed. The key stays at the - position of its *first* occurrence, where [`materialize()`](../basic_json_view/materialize.md) puts it as well. A - lookup, an iteration, and `materialize()` of `object` afterward therefore all agree on a single member for `key`. See the - [Notes on duplicate keys](../basic_json_view/operator%5B%5D.md#notes) of `operator[]`. + If `object` already has more than one member with `key` (2.), the *first* one is assigned `value` and every + later member with the same key is removed -- so that a lookup, an iteration, and + [`materialize()`](../basic_json_view/materialize.md) of `object` afterward all agree on a single value for + `key`, the same way [`operator[]`](../basic_json_view/operator%5B%5D.md) already picks the first occurrence of a + duplicate key for reading. See the [Notes on duplicate keys](../basic_json_view/operator%5B%5D.md#notes) of + `operator[]`. Setting a member (2.) or an element (3., through 4.) of an array or object whose elements have not been edited before switches it from its parsed layout to a growable block holding links to its elements; a later diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index e8bd8f253..b10869b21 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -15,7 +15,7 @@ basic_json_view at(IntegerType idx) const; basic_json_view at(const json_pointer& ptr) const; ``` -1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)). 2. Returns the array element at index `idx`. The template accepts every integer type except `#!cpp bool` and `#!cpp std::size_t` and forwards to the `size_type` overload, as for [`operator[]`](operator[].md); a negative @@ -35,7 +35,7 @@ basic_json_view at(const json_pointer& ptr) const; ## Return value -1. the value of the last member with key `key` +1. the value of the first member with key `key` 2. the element at index `idx` 3. the value `ptr` resolves to, starting at this value @@ -75,7 +75,7 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Complexity 1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after - another, in document order, scanning all of them, since the last match is wanted. Each comparison first checks the + another, in document order, stopping at the first match. Each comparison first checks the key's length -- already known from the index, without reading the key bytes -- before comparing its content. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. @@ -86,6 +86,13 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Notes +!!! warning "Duplicate keys: the first member wins" + + If the source text repeats a key, this resolves to the *first* member with it, not to the last one that + [`materialize()`](materialize.md) and `parse()` keep. See the [Notes on duplicate keys](operator[].md#notes) of + `operator[]` and [Duplicate keys](../../features/json_view.md#duplicate-keys) for the reasons and for how to get + the last value. + Unlike [`operator[]`](operator[].md), which returns a [discarded](is_discarded.md) view for a missing key or an out-of-range index, `at` always throws -- exactly as `BasicJsonType::at` does, and with the same messages, so existing error handling written against `BasicJsonType::at` keeps working unchanged when switched to a view. This diff --git a/docs/mkdocs/docs/api/basic_json_view/begin.md b/docs/mkdocs/docs/api/basic_json_view/begin.md index f9c43802f..ae2665ee9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/begin.md +++ b/docs/mkdocs/docs/api/basic_json_view/begin.md @@ -24,7 +24,7 @@ Constant. For an object, iteration visits **every** member, including all occurrences of a duplicate key -- unlike [`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), [`contains`](contains.md), and [`count`](count.md), -which all resolve to the *last* member with a given key. See the +which all resolve to the *first* member with a given key. See the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. Because objects are iterated in document order rather than sorted by key, the order seen here can differ from what diff --git a/docs/mkdocs/docs/api/basic_json_view/contains.md b/docs/mkdocs/docs/api/basic_json_view/contains.md index f44021f71..52d2a4a41 100644 --- a/docs/mkdocs/docs/api/basic_json_view/contains.md +++ b/docs/mkdocs/docs/api/basic_json_view/contains.md @@ -33,7 +33,7 @@ No-throw guarantee: this function never throws exceptions. ## Complexity 1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after - another, in document order, scanning all of them, since the last match is wanted. Each comparison first checks the + another, in document order, stopping at the first match. Each comparison first checks the key's length -- already known from the index, without reading the key bytes -- before comparing its content. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. @@ -43,6 +43,13 @@ No-throw guarantee: this function never throws exceptions. ## Notes +!!! warning "Duplicate keys: the first member wins" + + If the source text repeats a key, this resolves to the *first* member with it, not to the last one that + [`materialize()`](materialize.md) and `parse()` keep. See the [Notes on duplicate keys](operator[].md#notes) of + `operator[]` and [Duplicate keys](../../features/json_view.md#duplicate-keys) for the reasons and for how to get + the last value. + Overload 1 always returns `#!cpp false` when the value is not an object -- including a [discarded](is_discarded.md) view. diff --git a/docs/mkdocs/docs/api/basic_json_view/count.md b/docs/mkdocs/docs/api/basic_json_view/count.md index a20f3aaee..bf5bb1d21 100644 --- a/docs/mkdocs/docs/api/basic_json_view/count.md +++ b/docs/mkdocs/docs/api/basic_json_view/count.md @@ -24,13 +24,20 @@ No-throw guarantee: this function never throws exceptions. ## Complexity Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, -in document order, scanning all of them, since the last match is wanted. Each comparison first checks the key's length +in document order, stopping at the first match. Each comparison first checks the key's length -- already known from the index, without reading the key bytes -- before comparing its content. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. ## Notes +!!! warning "Duplicate keys: the first member wins" + + If the source text repeats a key, this resolves to the *first* member with it, not to the last one that + [`materialize()`](materialize.md) and `parse()` keep. See the [Notes on duplicate keys](operator[].md#notes) of + `operator[]` and [Duplicate keys](../../features/json_view.md#duplicate-keys) for the reasons and for how to get + the last value. + This method always returns `#!cpp 0` when the value is not an object -- including a [discarded](is_discarded.md) view. @@ -38,7 +45,7 @@ Unlike [`BasicJsonType::count()`](../basic_json/count.md), whose return value ca an `ObjectType` that allows multiple entries per key, `count()` here never does: it is exactly [`contains()`](contains.md) as `#!cpp 0`/`#!cpp 1`. This holds even if the source text has a duplicate key -- see the [Notes on duplicate keys](operator[].md#notes) of `operator[]` -- because a `#!cpp count() > 1` result would require -counting every member with a matching key (the lookup functions resolve to the *last* one). +counting every member with a matching key (the lookup functions resolve to the *first* one). ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/find.md b/docs/mkdocs/docs/api/basic_json_view/find.md index fb0561c10..723cad70b 100644 --- a/docs/mkdocs/docs/api/basic_json_view/find.md +++ b/docs/mkdocs/docs/api/basic_json_view/find.md @@ -6,7 +6,7 @@ iterator find(const char* key) const; iterator find(const string_t& key) const; ``` -Finds a member with key `key` -- the last one, should the key occur more than once (see +Finds a member with key `key` -- the first one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)). If the value is not an object, or no member has this key, [`end()`](end.md) is returned. @@ -26,13 +26,20 @@ No-throw guarantee: this function never throws exceptions. ## Complexity Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, -in document order, scanning all of them, since the last match is wanted. Each comparison first checks the key's length +in document order, stopping at the first match. Each comparison first checks the key's length -- already known from the index, without reading the key bytes -- before comparing its content. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. ## Notes +!!! warning "Duplicate keys: the first member wins" + + If the source text repeats a key, this resolves to the *first* member with it, not to the last one that + [`materialize()`](materialize.md) and `parse()` keep. See the [Notes on duplicate keys](operator[].md#notes) of + `operator[]` and [Duplicate keys](../../features/json_view.md#duplicate-keys) for the reasons and for how to get + the last value. + Unlike [`BasicJsonType::find`](../basic_json/find.md), which always returns `#!cpp end()` for a non-object type, this also does so for a [discarded](is_discarded.md) view -- there is no separate "invalid" iterator to return. diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index c3bb85790..d8ec333c9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -87,9 +87,9 @@ exception thrown while converting through `materialize()` (the last bullet) is d !!! info "Duplicate keys" `#!cpp std::map`/`#!cpp std::unordered_map` conversions keep the *last* value of a repeated key, like - [`materialize()`](materialize.md) and [`BasicJsonType::parse()`](../basic_json/parse.md) do. This is the member - [`operator[]`](operator[].md)/[`at`](at.md)/[`find`](find.md)/[`contains`](contains.md) resolve to, too (see the - [Notes on duplicate keys](operator[].md#notes)). + [`materialize()`](materialize.md) and [`BasicJsonType::parse()`](../basic_json/parse.md) do. This is the opposite + of [`operator[]`](operator[].md)/[`at`](at.md)/[`find`](find.md)/[`contains`](contains.md), which resolve to the + *first* occurrence (see the [Notes on duplicate keys](operator[].md#notes)). !!! info "No pointers, references, or implicit conversion" diff --git a/docs/mkdocs/docs/api/basic_json_view/items.md b/docs/mkdocs/docs/api/basic_json_view/items.md index 8e13852da..d54f8d723 100644 --- a/docs/mkdocs/docs/api/basic_json_view/items.md +++ b/docs/mkdocs/docs/api/basic_json_view/items.md @@ -48,7 +48,7 @@ Constant. As for [`begin()`](begin.md)/[`end()`](end.md), `items()` visits **every** member of an object, including all occurrences of a duplicate key -- unlike [`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), -[`contains`](contains.md), and [`count`](count.md), which resolve to the *last* member with a given key. See the +[`contains`](contains.md), and [`count`](count.md), which resolve to the *first* member with a given key. See the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. !!! danger "Lifetime issues" @@ -63,8 +63,8 @@ occurrences of a duplicate key -- unlike [`operator[]`](operator[].md), [`at`](a The example below shows a settings object whose source text records every update to a key as a duplicate member, in the order they happened. `items()` walks all of them, so the update history is visible, while - [`operator[]`](operator[].md) sees the *last* one, and so does [`materialize()`](materialize.md) -- like - [`BasicJsonType::parse()`](../basic_json/parse.md). + [`operator[]`](operator[].md) only ever sees the *first* one and [`materialize()`](materialize.md) -- like + [`BasicJsonType::parse()`](../basic_json/parse.md) -- keeps only the *last*. ```cpp --8<-- "examples/basic_json_view__items.cpp" diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 4337d95eb..ec1867c9f 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -15,7 +15,7 @@ basic_json_view operator[](IntegerType idx) const; basic_json_view operator[](const json_pointer& ptr) const; ``` -1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see the [Notes](#notes) below) -- or a [discarded](is_discarded.md) view if there is no such member. 2. Returns the array element at index `idx`, or a [discarded](is_discarded.md) view if `idx` is out of range. The template accepts every integer type except `#!cpp bool` and `#!cpp std::size_t` (`#!cpp int`, `#!cpp unsigned`, @@ -38,7 +38,7 @@ basic_json_view operator[](const json_pointer& ptr) const; ## Return value -1. the value of the last member with key `key`, or a discarded view if no member has this key (or if this view is +1. the value of the first member with key `key`, or a discarded view if no member has this key (or if this view is [discarded](is_discarded.md)) 2. the element at index `idx`, or a discarded view if `#!cpp idx >= size()` or `idx` is negative (or if this view is [discarded](is_discarded.md)) @@ -80,7 +80,7 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Complexity 1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after - another, in document order, scanning all of them, since the last match is wanted. Each comparison first checks the + another, in document order, stopping at the first match. Each comparison first checks the key's length -- already known from the index, without reading the key bytes -- before comparing its content, so a key of a different length than `key` is rejected without touching the source text. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time @@ -107,16 +107,19 @@ document. key on an array or a primitive, or an index on an object or a primitive, is `type_error.305` as for `BasicJsonType`. [`at`](at.md) still throws for a discarded view, as it does for a missing key. -!!! info "Duplicate keys" +!!! warning "Duplicate keys: the first member wins" If the source text has an object with a duplicate key, `#!cpp operator[]` (and [`at`](at.md), [`find`](find.md), [`contains`](contains.md), [`count`](count.md), [`value`](value.md), and JSON pointer resolution) all resolve to - the *last* member with that key. This is the member [`materialize()`](materialize.md) (and - [`BasicJsonType::parse()`](../basic_json/parse.md)) keeps, so a lookup in the view and in the materialized value - agree. [`begin()`](begin.md)/[`end()`](end.md) and [`items()`](items.md) iterate over *all* members, including - duplicates, in document order. A lookup in an object without a hash index scans all members for this: it cannot - stop at the first match. The hash index of a larger object (128 members or more) leads to the last member of a key - as well. See the example below and [`size()`](size.md#notes). + the *first* member with that key, because a lookup can stop as soon as it finds a match. This is different from + [`materialize()`](materialize.md) (and [`BasicJsonType::parse()`](../basic_json/parse.md)), which replay every + member in order and so keep the *last* value for a repeated key, so `#!cpp v["a"]` and + `#!cpp v.materialize()["a"]` can differ. To get the value `parse()` would give, use + [`materialize()`](materialize.md) or iterate the members with [`items()`](items.md) and keep the last match. + The hash index of a larger object (128 members or more) leads to the first member of a key as well. + [`begin()`](begin.md)/[`end()`](end.md) and [`items()`](items.md) iterate over *all* members, including + duplicates, in document order. See [Duplicate keys](../../features/json_view.md#duplicate-keys) and + [`size()`](size.md#notes). !!! info "JSON pointer resolution" diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md index 6f96f4692..b43f5c633 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md +++ b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md @@ -18,6 +18,8 @@ bool operator==(const BasicJsonType& lhs, const basic_json_view& rhs); Neither overload builds a `BasicJsonType` value for a view to do the comparison (see [Notes](#notes) below). Numbers compare by value across their types (`#!cpp 1 == 1.0`), and an object compares by its members, with duplicate keys resolved exactly as `parse()` resolves them -- the last value, at the position of the first occurrence of the key. +This is not what [`operator[]`](operator[].md) and the other lookups return for a duplicate key: they find the *first* +member (see [Duplicate keys](../../features/json_view.md#duplicate-keys)). ## Parameters diff --git a/docs/mkdocs/docs/api/basic_json_view/value.md b/docs/mkdocs/docs/api/basic_json_view/value.md index 7ec353cd5..78d459ed1 100644 --- a/docs/mkdocs/docs/api/basic_json_view/value.md +++ b/docs/mkdocs/docs/api/basic_json_view/value.md @@ -12,7 +12,7 @@ T value(const json_pointer& ptr, const T& default_value) const; string_t value(const json_pointer& ptr, const char* default_value) const; ``` -1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)) -- converted to `T`, or `default_value` if there is no such member. 2. Returns the value a JSON pointer `ptr` refers to, starting at this value, converted to `T`, or `default_value` if `ptr` cannot be resolved. @@ -39,7 +39,7 @@ equivalent) deduce `string_t`, not `const char*`, for their return type and for ## Return value -1. the last member with key `key`, converted to `T`, or `default_value` +1. the first member with key `key`, converted to `T`, or `default_value` 2. the value `ptr` resolves to, converted to `T`, or `default_value` ## Exception safety @@ -68,7 +68,7 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Complexity 1. Linear in the number of members: as for [`operator[]`](operator[].md#complexity), members are compared one after - another, in document order, scanning all of them, since the last match is wanted. Plus the complexity of converting + another, in document order, stopping at the first match. Plus the complexity of converting the found member to `T` (see [`get`](get.md)). Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. @@ -79,6 +79,13 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Notes +!!! warning "Duplicate keys: the first member wins" + + If the source text repeats a key, this resolves to the *first* member with it, not to the last one that + [`materialize()`](materialize.md) and `parse()` keep. See the [Notes on duplicate keys](operator[].md#notes) of + `operator[]` and [Duplicate keys](../../features/json_view.md#duplicate-keys) for the reasons and for how to get + the last value. + !!! info "Differences to `at` and `operator[]`" Unlike [`at`](at.md), this function does not throw if `key`/`ptr` resolves to no value. Unlike diff --git a/docs/mkdocs/docs/examples/basic_json_view__items.cpp b/docs/mkdocs/docs/examples/basic_json_view__items.cpp index 6357c7e4b..3f7779c19 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__items.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__items.cpp @@ -7,8 +7,8 @@ int main() { // a settings object whose source text records every update to a key as // a duplicate member. items() visits all of them, in document order, so - // the update history is visible; operator[] and materialize() -- like - // basic_json::parse() -- see the last one + // the update history is visible; operator[] only ever sees the first + // one, and materialize() -- like basic_json::parse() -- keeps the last json_document updates = json_document::parse(R"({"retries": 1, "timeout": 30, "retries": 5})"); const auto settings = updates.root(); @@ -17,6 +17,6 @@ int main() std::cout << item.key() << '=' << item.value().materialize().dump() << '\n'; } - std::cout << "last \"retries\" seen by operator[]: " << settings["retries"].materialize().dump() << '\n'; + std::cout << "first \"retries\" seen by operator[]: " << settings["retries"].materialize().dump() << '\n'; std::cout << "last \"retries\" kept by materialize(): " << settings.materialize()["retries"].dump() << '\n'; } diff --git a/docs/mkdocs/docs/examples/basic_json_view__items.output b/docs/mkdocs/docs/examples/basic_json_view__items.output index 8f51dbf18..78609c8ee 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__items.output +++ b/docs/mkdocs/docs/examples/basic_json_view__items.output @@ -1,5 +1,5 @@ retries=1 timeout=30 retries=5 -last "retries" seen by operator[]: 5 +first "retries" seen by operator[]: 1 last "retries" kept by materialize(): 5 diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index d62dfb354..4138442e3 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -134,17 +134,8 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. `operator[]` on a discarded view returns a discarded view without throwing: `#!cpp v["a"]["b"][0]` can be tested once at the end. Type errors on values that exist (a key on an array, an index on an object) still throw, and [`at`](../api/basic_json_view/at.md) throws for every missing value. -- **Duplicate keys are visible.** If an object in the source text repeats a key, - [`begin()`](../api/basic_json_view/begin.md)/[`end()`](../api/basic_json_view/end.md) and - [`items()`](../api/basic_json_view/items.md) visit *every* occurrence (and [`size()`](../api/basic_json_view/size.md) - counts all of them), while [`operator[]`](../api/basic_json_view/operator%5B%5D.md), - [`at`](../api/basic_json_view/at.md), [`find`](../api/basic_json_view/find.md), - [`contains`](../api/basic_json_view/contains.md), and [`count`](../api/basic_json_view/count.md) resolve to the - *last* occurrence -- the one `basic_json::parse()` (and so - [`materialize()`](../api/basic_json_view/materialize.md)) keeps for a repeated key -- which makes a lookup scan all - members instead of stopping at a match (objects with 128 members or more get a hash index that leads to the last - occurrence directly). - See the [Notes on duplicate keys](../api/basic_json_view/operator%5B%5D.md#notes) of `operator[]`. +- **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys) + below. - **No [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) path.** Exceptions thrown by `basic_json_view`'s own element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the view has no `basic_json` value to point at, so the exception is created without one, regardless of how @@ -156,6 +147,52 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. equal values for them, without ever building a tree to do it. For ordering, too, [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can compare. +## Duplicate keys + +!!! warning "A lookup in a view and in the parsed value can give different answers" + + If an object in the source text repeats a key, [`operator[]`](../api/basic_json_view/operator%5B%5D.md), + [`at`](../api/basic_json_view/at.md), [`find`](../api/basic_json_view/find.md), + [`value`](../api/basic_json_view/value.md), [`contains`](../api/basic_json_view/contains.md), + [`count`](../api/basic_json_view/count.md), and JSON pointer resolution all return the **first** member with that + key. `basic_json::parse()` instead keeps the **last** value of a repeated key, so for `#!cpp {"a":1,"a":2}`, + `#!cpp view["a"]` is `#!cpp 1` while `#!cpp parse(text)["a"]` is `#!cpp 2`. + +[`begin()`](../api/basic_json_view/begin.md)/[`end()`](../api/basic_json_view/end.md) and +[`items()`](../api/basic_json_view/items.md) visit *every* occurrence, in document order, and +[`size()`](../api/basic_json_view/size.md) counts all of them, so `#!cpp view.size()` can be larger than +`#!cpp view.materialize().size()`. + +**Why the first?** A lookup can stop as soon as it finds a match. Returning the last member would force every lookup +to scan all members of the object, even when the key is found at the very first one: this made lookups in small +objects 1.6 to 3.4 times slower. The hash index of objects with 128 members or more leads to the first member of a +key as well. Other zero-copy parsers that index the source text, such as yyjson and simdjson, also return the first +member. RFC 8259 only says that names within an object SHOULD be unique and that the behavior of a receiver that sees +duplicates is unpredictable, so neither choice is wrong. + +**What stays the same as `parse()`?** [`materialize()`](../api/basic_json_view/materialize.md) and +[`get>()`](../api/basic_json_view/get.md) replay every member in order, so they keep the *last* value +exactly like `#!cpp basic_json::parse()` (at the position of the first occurrence of the key, for an +[`ordered_json`](../api/ordered_json.md)). + +**How do I get the value `parse()` would give?** Either call `#!cpp view.materialize()` and look the key up in the +result, or iterate the members and keep the last match: + +```cpp +// the last member with the key "a", as parse() would keep it +json_view last; +for (const auto item : view.items()) +{ + if (item.key() == "a") + { + last = item.value(); + } +} +``` + +If you cannot trust the source text to have unique keys, check [`size()`](../api/basic_json_view/size.md) against the +number of distinct keys, or reject duplicates when iterating. + ## Getting values out without copying [`get()`](../api/basic_json_view/get.md) converts many `T` directly from the flat index, without ever building a @@ -185,8 +222,8 @@ Both results are only valid as long as the view -- and, for a string with no esc [`dump()`](../api/basic_json_view/dump.md) serializes a view directly from the flat index, without ever building a `basic_json` value. An object's members are written in document order, not sorted by key, and *every* occurrence of a repeated key is written, not only the last one -- the same two ways [iteration](#what-is-different) already differs -from a [`materialize()`](../api/basic_json_view/materialize.md)d value, see above. `#!cpp materialize().dump()` gives -a different result in both respects for a `json_view`. +from a [`materialize()`](../api/basic_json_view/materialize.md)d value, see [Duplicate keys](#duplicate-keys). +`#!cpp materialize().dump()` gives a different result in both respects for a `json_view`. By default, numbers are written the way [`basic_json::dump()`](../api/basic_json/dump.md) would. [`number_format::source`](../api/basic_json_view/number_format.md) instead copies every number exactly as it was diff --git a/docs/mkdocs/docs/home/architecture.md b/docs/mkdocs/docs/home/architecture.md index 8e1c71420..8ed1a86b5 100644 --- a/docs/mkdocs/docs/home/architecture.md +++ b/docs/mkdocs/docs/home/architecture.md @@ -215,7 +215,7 @@ packet-beta - **Large objects** (128 members or more) get a hash index after parsing ([`detail/view/object_index.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/object_index.hpp)): an open-addressing table whose slots hold the distance from the object's node to a key's node, so that a lookup does - not compare every key. Of duplicate keys the table leads to the last, as a linear search does. The object's `extra` + not compare every key. Of duplicate keys the table leads to the first, as a linear search does. The object's `extra` holds the number of its table. Only 65,535 tables fit into `extra`; objects beyond them are searched linearly. For example, `#!json {"a": [1, 2.5]}` becomes five nodes. Each node's elements follow it, and `next` leads from an diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index 57a0b0826..7a9daa6bc 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -152,23 +152,25 @@ class editor { become_empty(o, value_t::object); } - // an existing member: assign the one that lookups find (the last - // one, should the key occur more than once), and drop the others, so - // that lookups, iteration, and materialize() agree. The key stays at - // the position of its first occurrence, as materialize() puts it. + // an existing member: assign it (and drop later duplicates, so that + // lookups, iteration, and materialize() agree) node* slot = nullptr; - std::size_t matches = 0; + bool duplicates = false; for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) { if (key_equals(*k, key)) { + if (slot != nullptr) + { + duplicates = true; + break; + } slot = const_cast(nav::value(k + 1)); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document - ++matches; } } if (slot != nullptr) { - if (matches > 1) + if (duplicates) { erase_members(o, key, true); } @@ -314,38 +316,27 @@ class editor return k.len == key.size() && (key.size() == 0 || std::memcmp(m_doc.str(k), key.data(), key.size()) == 0); } - /// Remove the members with this key from an object: all of them, or all - /// but one. That one stays where the first occurrence is, but holds the - /// value of the last (the one that lookups find, which views may refer to). - std::size_t erase_members(node* o, string_view_t key, bool keep_one) + /// remove the members with this key (all, or all but the first) from an object + std::size_t erase_members(node* o, string_view_t key, bool keep_first) { node* const h = block_of(m_doc, o, 0); - node last_value{}; // the entry of the value of the last member - node* const end = h + h->next; - if (keep_one) - { - for (node* r = h + 1; r != end; r += 2) - { - if (key_equals(*r, key)) - { - last_value = r[1]; - } - } - } node* w = h + 1; std::size_t erased = 0; bool kept = false; - for (node* r = h + 1; r != end; r += 2) + for (node* r = h + 1, *end = h + h->next; r != end; r += 2) { const bool match = key_equals(*r, key); - if (match && (kept || !keep_one)) + if (match && (kept || !keep_first)) { ++erased; continue; } - w[0] = r[0]; - w[1] = match ? last_value : r[1]; kept = kept || match; + if (w != r) + { + w[0] = r[0]; + w[1] = r[1]; + } w += 2; } h->next = static_cast(w - h); diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 9af032071..afc43e95a 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -84,9 +84,10 @@ class short_key std::uint64_t m_b = 0; }; -/// the key node of the last member of an object with the given key, or -/// nullptr (the last one, as materialize() and parse() keep it); most keys are -/// rejected by their length, from the index alone +/// the key node of the first member of an object with the given key, or +/// nullptr (the search stops at the first match; materialize() and parse() +/// keep the last value of a duplicate key instead); most keys are rejected by +/// their length, from the index alone template const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { @@ -97,7 +98,6 @@ const node* find_member(const document_data& d, const node* object, const char* } const node* const end = nav::end(d, object); const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - const node* last = nullptr; if (NLOHMANN_VIEW_LIKELY(n <= 16)) { const short_key probe(k, n); @@ -105,19 +105,19 @@ const node* find_member(const document_data& d, const node* object, const char* { if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) { - last = m; + return m; } } - return last; + return nullptr; } for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) { if (m->len == n && std::memcmp(d.str(*m), key, n) == 0) { - last = m; + return m; } } - return last; + return nullptr; } /// whether an integer type is accepted as an array index by the view's diff --git a/include/nlohmann/detail/view/object_index.hpp b/include/nlohmann/detail/view/object_index.hpp index 721b4d62b..8888f2345 100644 --- a/include/nlohmann/detail/view/object_index.hpp +++ b/include/nlohmann/detail/view/object_index.hpp @@ -22,8 +22,8 @@ // large objects). An object with document_data::index_min_members members or // more gets an open-addressing table after parsing; its node stores the // number of the table (1-based) in `extra`. A slot holds the offset of a key -// node from its object node (0: empty). Of duplicate keys, the last is kept, -// as for the linear search, and as basic_json::parse() does. +// node from its object node (0: empty). Of duplicate keys, the first is kept, +// as for the linear search. // // The hash is not seeded, so keys chosen to collide could make the build // quadratic. A key therefore sits at most index_max_displacement slots away @@ -92,8 +92,7 @@ inline void build_object_index(document_data& d, node* obj) const node* const other = obj + slots[i]; if (other->len == k->len && (k->len == 0 || std::memcmp(d.str(*other), key, k->len) == 0)) { - duplicate = true; // keep the last: the key's slot now leads to this member - slots[i] = static_cast(k - obj); + duplicate = true; // keep the first break; } if (++distance > index_max_displacement) @@ -130,7 +129,7 @@ inline void build_object_indexes(document_data& d) } } -/// the key node of the last member with this key of an indexed object, or +/// the key node of the first member with this key of an indexed object, or /// nullptr inline const node* find_indexed(const document_data& d, const node* obj, const char* key, std::size_t n) noexcept { diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 9ffb236ad..eeff92851 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -50,11 +50,6 @@ class output_buffer { const auto size = static_cast(m_pos - m_out.data()); m_out.resize(size); - // do not keep a buffer that was sized for a much larger output - if (m_out.capacity() > 1024 && m_out.capacity() / 2 > size) - { - m_out.shrink_to_fit(); - } } NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 29fdc797f..806fd8780 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -243,7 +243,7 @@ class basic_json_view // element access // //////////////////// - /// the value of the member with this key (the last one, should the key + /// the value of the member with this key (the first one, should the key /// occur more than once); a discarded view if there is none, or if this /// is a discarded view (so that v["a"]["b"] is safe). Throws type_error.305 /// if this is any other value but an object. @@ -307,7 +307,7 @@ class basic_json_view return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); } - /// the value of the member with this key (the last one, should the key + /// the value of the member with this key (the first one, should the key /// occur more than once). Throws type_error.304 if this is not an object, /// and out_of_range.403 if there is no such member. basic_json_view at(string_view_t key) const @@ -365,7 +365,7 @@ class basic_json_view } /// the member with this key converted to T, or the default value if there - /// is no such member (the last one, should the key occur more than + /// is no such member (the first one, should the key occur more than /// once). Throws type_error.306 if this is not an object. template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > T value(string_view_t key, const T& default_value) const @@ -430,7 +430,7 @@ class basic_json_view // lookup // //////////// - /// an iterator to the member with this key (the last one, should the + /// an iterator to the member with this key (the first one, should the /// key occur more than once), or end(); end() also for non-objects iterator find(string_view_t key) const { @@ -765,7 +765,7 @@ class basic_json_view return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); } - /// the value of the last member with this key, or a discarded view + /// the value of the first member with this key, or a discarded view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 3799aec4b..ae80c320d 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -2939,8 +2939,8 @@ NLOHMANN_JSON_NAMESPACE_END // large objects). An object with document_data::index_min_members members or // more gets an open-addressing table after parsing; its node stores the // number of the table (1-based) in `extra`. A slot holds the offset of a key -// node from its object node (0: empty). Of duplicate keys, the last is kept, -// as for the linear search, and as basic_json::parse() does. +// node from its object node (0: empty). Of duplicate keys, the first is kept, +// as for the linear search. // // The hash is not seeded, so keys chosen to collide could make the build // quadratic. A key therefore sits at most index_max_displacement slots away @@ -3009,8 +3009,7 @@ inline void build_object_index(document_data& d, node* obj) const node* const other = obj + slots[i]; if (other->len == k->len && (k->len == 0 || std::memcmp(d.str(*other), key, k->len) == 0)) { - duplicate = true; // keep the last: the key's slot now leads to this member - slots[i] = static_cast(k - obj); + duplicate = true; // keep the first break; } if (++distance > index_max_displacement) @@ -3047,7 +3046,7 @@ inline void build_object_indexes(document_data& d) } } -/// the key node of the last member with this key of an indexed object, or +/// the key node of the first member with this key of an indexed object, or /// nullptr inline const node* find_indexed(const document_data& d, const node* obj, const char* key, std::size_t n) noexcept { @@ -3141,9 +3140,10 @@ class short_key std::uint64_t m_b = 0; }; -/// the key node of the last member of an object with the given key, or -/// nullptr (the last one, as materialize() and parse() keep it); most keys are -/// rejected by their length, from the index alone +/// the key node of the first member of an object with the given key, or +/// nullptr (the search stops at the first match; materialize() and parse() +/// keep the last value of a duplicate key instead); most keys are rejected by +/// their length, from the index alone template const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { @@ -3154,7 +3154,6 @@ const node* find_member(const document_data& d, const node* object, const char* } const node* const end = nav::end(d, object); const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - const node* last = nullptr; if (NLOHMANN_VIEW_LIKELY(n <= 16)) { const short_key probe(k, n); @@ -3162,19 +3161,19 @@ const node* find_member(const document_data& d, const node* object, const char* { if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) { - last = m; + return m; } } - return last; + return nullptr; } for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) { if (m->len == n && std::memcmp(d.str(*m), key, n) == 0) { - last = m; + return m; } } - return last; + return nullptr; } /// whether an integer type is accepted as an array index by the view's @@ -3361,23 +3360,25 @@ class editor { become_empty(o, value_t::object); } - // an existing member: assign the one that lookups find (the last - // one, should the key occur more than once), and drop the others, so - // that lookups, iteration, and materialize() agree. The key stays at - // the position of its first occurrence, as materialize() puts it. + // an existing member: assign it (and drop later duplicates, so that + // lookups, iteration, and materialize() agree) node* slot = nullptr; - std::size_t matches = 0; + bool duplicates = false; for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) { if (key_equals(*k, key)) { + if (slot != nullptr) + { + duplicates = true; + break; + } slot = const_cast(nav::value(k + 1)); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document - ++matches; } } if (slot != nullptr) { - if (matches > 1) + if (duplicates) { erase_members(o, key, true); } @@ -3523,38 +3524,27 @@ class editor return k.len == key.size() && (key.size() == 0 || std::memcmp(m_doc.str(k), key.data(), key.size()) == 0); } - /// Remove the members with this key from an object: all of them, or all - /// but one. That one stays where the first occurrence is, but holds the - /// value of the last (the one that lookups find, which views may refer to). - std::size_t erase_members(node* o, string_view_t key, bool keep_one) + /// remove the members with this key (all, or all but the first) from an object + std::size_t erase_members(node* o, string_view_t key, bool keep_first) { node* const h = block_of(m_doc, o, 0); - node last_value{}; // the entry of the value of the last member - node* const end = h + h->next; - if (keep_one) - { - for (node* r = h + 1; r != end; r += 2) - { - if (key_equals(*r, key)) - { - last_value = r[1]; - } - } - } node* w = h + 1; std::size_t erased = 0; bool kept = false; - for (node* r = h + 1; r != end; r += 2) + for (node* r = h + 1, *end = h + h->next; r != end; r += 2) { const bool match = key_equals(*r, key); - if (match && (kept || !keep_one)) + if (match && (kept || !keep_first)) { ++erased; continue; } - w[0] = r[0]; - w[1] = match ? last_value : r[1]; kept = kept || match; + if (w != r) + { + w[0] = r[0]; + w[1] = r[1]; + } w += 2; } h->next = static_cast(w - h); @@ -5835,11 +5825,6 @@ class output_buffer { const auto size = static_cast(m_pos - m_out.data()); m_out.resize(size); - // do not keep a buffer that was sized for a much larger output - if (m_out.capacity() > 1024 && m_out.capacity() / 2 > size) - { - m_out.shrink_to_fit(); - } } NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) @@ -7151,7 +7136,7 @@ class basic_json_view // element access // //////////////////// - /// the value of the member with this key (the last one, should the key + /// the value of the member with this key (the first one, should the key /// occur more than once); a discarded view if there is none, or if this /// is a discarded view (so that v["a"]["b"] is safe). Throws type_error.305 /// if this is any other value but an object. @@ -7215,7 +7200,7 @@ class basic_json_view return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); } - /// the value of the member with this key (the last one, should the key + /// the value of the member with this key (the first one, should the key /// occur more than once). Throws type_error.304 if this is not an object, /// and out_of_range.403 if there is no such member. basic_json_view at(string_view_t key) const @@ -7273,7 +7258,7 @@ class basic_json_view } /// the member with this key converted to T, or the default value if there - /// is no such member (the last one, should the key occur more than + /// is no such member (the first one, should the key occur more than /// once). Throws type_error.306 if this is not an object. template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > T value(string_view_t key, const T& default_value) const @@ -7338,7 +7323,7 @@ class basic_json_view // lookup // //////////// - /// an iterator to the member with this key (the last one, should the + /// an iterator to the member with this key (the first one, should the /// key occur more than once), or end(); end() also for non-objects iterator find(string_view_t key) const { @@ -7673,7 +7658,7 @@ class basic_json_view return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); } - /// the value of the last member with this key, or a discarded view + /// the value of the first member with this key, or a discarded view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 2a335623f..77c5d1210 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -577,7 +577,7 @@ std::string exception_of(F f) // compares a view with the ordered_json value materialize() gives for it: // types, sizes, elements and members (by index, key, and iteration), in -// document order; duplicate keys are found as their last occurrence +// document order; duplicate keys are found as their first occurrence void check_access(const ordered_json_view& v, const ordered_json& j) { REQUIRE(v.type() == j.type()); @@ -618,23 +618,13 @@ void check_access(const ordered_json_view& v, const ordered_json& j) const std::string key(it.key().data(), it.key().size()); CHECK(v.contains(key)); CHECK(v.count(key) == 1); - if (std::find(keys.begin(), keys.end(), key) == keys.end()) - { - keys.push_back(key); - } - // lookups find the last member with the key, which is this one if - // there is no later one - auto next = it; - ++next; - bool is_last = true; - for (; next != v.end(); ++next) - { - is_last = is_last && next.key() != it.key(); - } - if (!is_last) + // lookups find the first member with the key, which is this one if + // there is no earlier one + if (std::find(keys.begin(), keys.end(), key) != keys.end()) { continue; } + keys.push_back(key); CHECK(v.find(key) == it); CHECK(v[key].materialize() == it->materialize()); CHECK(v.at(key).materialize() == it.value().materialize()); @@ -725,20 +715,28 @@ TEST_CASE("json_view element access and iteration") #endif } - SECTION("duplicate keys: lookups find the last member, iteration all") + SECTION("duplicate keys: lookups find the first member, iteration all") { const json_document d = json_document::parse(R"({"a":1,"b":2,"a":3})"); const json_view v = d.root(); CHECK(v.size() == 3); - CHECK(v["a"].materialize() == 3); - CHECK(v.at("a").materialize() == 3); - CHECK(v.find("a") == std::next(v.begin(), 2)); - CHECK(v.find("a").value().materialize() == 3); + CHECK(v["a"].materialize() == 1); + CHECK(v.at("a").materialize() == 1); + CHECK(v.find("a") == v.begin()); + CHECK(v.find("a").value().materialize() == 1); CHECK(v.find("b") == std::next(v.begin())); CHECK(v.count("a") == 1); CHECK(v.contains("a")); - CHECK(v.value("a", 0) == 3); - CHECK(v["a"].materialize() == v.materialize()["a"]); // as materialize() + CHECK(v.value("a", 0) == 1); + CHECK(v.materialize()["a"] == 3); // materialize() keeps the last value, unlike the lookups + // JSON pointers resolve to the first member at every level + const json_document dp = json_document::parse(R"({"a":{"b":1},"a":{"b":2,"b":3}})"); + CHECK(dp.root()[json::json_pointer("/a/b")].materialize() == 1); + CHECK(dp.root().at(json::json_pointer("/a/b")).materialize() == 1); + CHECK(dp.root().value(json::json_pointer("/a/b"), 0) == 1); + CHECK(dp.root().materialize()[json::json_pointer("/a/b")] == 3); + // get keeps the last value, as materialize() + CHECK((v.get>() == std::map {{"a", 3}, {"b", 2}})); // keys of every length class (the 16-byte short compare and memcmp) for (const std::size_t n : { @@ -748,10 +746,10 @@ TEST_CASE("json_view element access and iteration") const std::string key(n, 'k'); const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); CAPTURE(n) - CHECK(dk.root()[key].materialize() == 4); - CHECK(dk.root().at(key).materialize() == 4); - CHECK(dk.root().find(key) == std::next(dk.root().begin(), 3)); - CHECK(dk.root().value(key, 0) == 4); + CHECK(dk.root()[key].materialize() == 1); + CHECK(dk.root().at(key).materialize() == 1); + CHECK(dk.root().find(key) == dk.root().begin()); + CHECK(dk.root().value(key, 0) == 1); CHECK(dk.root()[key + "x"].materialize() == 2); } std::string order; @@ -1515,6 +1513,8 @@ TEST_CASE("json_view dump") const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})"); CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})"); CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}"); + // dump() writes all members, though the lookup finds the first + CHECK(d.root()["b"].dump() == "1"); } SECTION("deep nesting") @@ -1637,6 +1637,9 @@ TEST_CASE("json_view comparison") const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})"); const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})"); CHECK(dup.root() == last.root()); + // (== compares as parse() resolves duplicates, a lookup finds the first) + CHECK(dup.root()["a"].materialize() == 1); + CHECK(dup.root()["a"] != last.root()["a"]); const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})"); const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})"); CHECK(ab.root() != ba.root()); @@ -1684,31 +1687,26 @@ TEST_CASE("json_view large objects") for (std::size_t i = 0; i < members; ++i) { const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : ""); + CHECK(v[key].get() == i); CHECK(v.contains(key)); CHECK(v.find(key).key() == key); - CHECK(!v.contains(key + "x")); - if (key == "k1") - { - continue; // repeated below: the last member wins - } - CHECK(v[key].get() == i); CHECK(v.at(key).get() == i); + CHECK(!v.contains(key + "x")); } CHECK(v[""].get_string() == "empty key"); - CHECK(v["k1"].get_string() == "a duplicate of an earlier key"); // the last of duplicate keys, as for small objects - CHECK(v.at("k1").get_string() == "a duplicate of an earlier key"); + CHECK(v["k1"].get() == 1); // the first of duplicate keys, as for small objects CHECK(!v.contains("missing")); CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&); CHECK(v == j); CHECK(v.materialize() == j); } - SECTION("duplicate keys: the last member wins, with and without a table") + SECTION("duplicate keys: the first member wins, with and without a table") { // an object of `total` members: the keys "k0".."k" in order, then // three keys repeated twice more (one copy in the middle, one at the // end), and two keys repeated once; the value of a member is its - // position, so that the last member of a key can be told apart + // position, so that the first member of a key can be told apart struct member { std::string key; @@ -1756,20 +1754,23 @@ TEST_CASE("json_view large objects") REQUIRE(members.size() >= total); REQUIRE(members.size() >= 100); std::string text = "{"; + std::map first; std::map last; for (const member& m : members) { text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position); + first.insert({m.key, m.position}); last[m.key] = m.position; } text += '}'; - REQUIRE(last.size() < members.size()); + REQUIRE(first.size() < members.size()); const json_document d = json_document::parse(text); const json_view v = d.root(); const json j = json::parse(text); CHECK(v.size() == members.size()); // every occurrence is visited - for (const auto& entry : last) + const json m = v.materialize(); + for (const auto& entry : first) { CAPTURE(entry.first) const std::size_t expected = entry.second; @@ -1784,7 +1785,9 @@ TEST_CASE("json_view large objects") CHECK(v.at(json::json_pointer(pointer)).get() == expected); CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected); CHECK(v.contains(json::json_pointer(pointer))); - CHECK(j[entry.first].get() == expected); // as materialize() and parse() keep it + // materialize() and parse() keep the last value instead + CHECK(j[entry.first].get() == last.at(entry.first)); + CHECK(m[entry.first].get() == last.at(entry.first)); } CHECK(!v.contains("k")); CHECK(v["missing"].is_discarded()); diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 328d47ece..ad2522d46 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -28,6 +28,7 @@ using ptr_t = ordered_json::json_pointer; #include #include #include +#include #include #include @@ -181,8 +182,10 @@ void compare(const ordered_json_editable_view& v, const ordered_json& j) } } -// the text of j, in which some objects repeat a key of theirs: before their -// members (the real member is then the last), or after (the repeat is) +// the text of j, in which some objects repeat a key of theirs after their +// members: lookups of the edited document find the first member of a key, so +// j (the first members) is what they must agree with, while parse() would +// keep the last value std::string text_with_duplicates(const ordered_json& j) { if (j.is_object()) @@ -197,10 +200,6 @@ std::string text_with_duplicates(const ordered_json& j) return ordered_json(keys[static_cast(r(static_cast(keys.size())))]).dump() + ":" + random_value(2).dump(); }; std::string text = "{"; - if (!keys.empty() && r(4) == 0) - { - text += repeated() + ","; - } bool first = true; for (const auto& kv : j.items()) { @@ -225,10 +224,58 @@ std::string text_with_duplicates(const ordered_json& j) return j.dump(); } -// every lookup of the edited view finds what j holds, although the view may -// have several members for a key (j has the last value, at the position of the -// first member: what parse() and materialize() make of it) -void check_lookups(const ordered_json_editable_view& v, const ordered_json& j) +// every lookup of the edited view finds the first member of its key, which +// the iteration of the view shows +void check_lookups(const ordered_json_editable_view& v) +{ + if (v.is_object()) + { + std::set seen; + for (const auto item : v.items()) + { + const std::string key(item.key().data(), item.key().size()); + if (!seen.insert(key).second) + { + continue; // (not the first member of the key) + } + const ptr_t ptr = ptr_t() / key; + CAPTURE(key) + const ordered_json expected = item.value().materialize(); + const ordered_json_editable_view m = v[key]; + REQUIRE(!m.is_discarded()); + CHECK(m.materialize() == expected); + CHECK(v.at(key).materialize() == expected); + CHECK(v[ptr].materialize() == expected); + CHECK(v.at(ptr).materialize() == expected); + CHECK(v.contains(key)); + CHECK(v.contains(ptr)); + CHECK(v.count(key) == 1); + const auto it = v.find(key); + REQUIRE(it != v.end()); + CHECK((*it).materialize() == expected); + CHECK(v.value(ptr, ordered_json(nullptr)) == expected); + if (expected.is_string()) + { + CHECK(v.value(key, std::string("-")) == expected.get()); + } + check_lookups(m); + } + CHECK(v["missing#key"].is_discarded()); + CHECK(!v.contains("missing#key")); + CHECK(v.find("missing#key") == v.end()); + } + else if (v.is_array()) + { + for (std::size_t i = 0; i < v.size(); ++i) + { + check_lookups(v[i]); + check_lookups(v.at(i)); + } + } +} + +// every lookup of the edited view finds what the first members of j hold +void check_first_members(const ordered_json_editable_view& v, const ordered_json& j) { REQUIRE(v.type() == j.type()); if (j.is_object()) @@ -238,45 +285,39 @@ void check_lookups(const ordered_json_editable_view& v, const ordered_json& j) const std::string& key = kv.key(); const ptr_t ptr = ptr_t() / key; CAPTURE(key) - const ordered_json_editable_view m = v[key]; - REQUIRE(!m.is_discarded()); - CHECK(m.materialize() == kv.value()); - CHECK(v.at(key).materialize() == kv.value()); - CHECK(v[ptr].materialize() == kv.value()); - CHECK(v.at(ptr).materialize() == kv.value()); - CHECK(v.contains(key)); + REQUIRE(v.contains(key)); CHECK(v.contains(ptr)); CHECK(v.count(key) == 1); - const auto it = v.find(key); - REQUIRE(it != v.end()); - CHECK((*it).materialize() == kv.value()); - CHECK(v.value(ptr, ordered_json(nullptr)) == kv.value()); - if (kv.value().is_string()) - { - CHECK(v.value(key, std::string("-")) == kv.value().get()); - } - check_lookups(m, kv.value()); + CHECK(v.find(key) != v.end()); + CHECK(v.at(ptr).type() == kv.value().type()); + check_first_members(v[key], kv.value()); + check_first_members(v.at(key), kv.value()); + check_first_members(v[ptr], kv.value()); } - CHECK(v["missing#key"].is_discarded()); CHECK(!v.contains("missing#key")); - CHECK(v.find("missing#key") == v.end()); } else if (j.is_array()) { + REQUIRE(v.size() == j.size()); for (std::size_t i = 0; i < j.size(); ++i) { - check_lookups(v[i], j[i]); - check_lookups(v.at(i), j[i]); + check_first_members(v[i], j[i]); } } + else + { + CHECK(v.materialize() == j); + } } -// (for documents whose objects repeat keys: dump(), size(), and iteration -// list all members, so only the lookups and materialize() are compared) +// (for documents whose objects repeat keys: j holds the first members, the +// lookups must find them; dump(), size(), and iteration list all members, but +// materialize() keeps the last value of a key, as parse() does of the dump) void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j) { - CHECK(d.root().materialize() == j); - check_lookups(d.root(), j); + check_first_members(d.root(), j); + check_lookups(d.root()); + CHECK(d.root().materialize() == ordered_json::parse(d.root().dump())); } void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep) @@ -304,17 +345,25 @@ TEST_CASE("json_view edits: differential") // materialize, and read back the same for (int n = 0; n < 250; ++n) { - // the last 100 documents repeat keys in some of their objects + // the last 100 documents repeat keys in some of their objects; j then + // holds the first member of each key, which the edits address const bool duplicates = n >= 150; ordered_json j = random_value(0); if (r(4) == 0) { j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}}); } + if (duplicates) + { + j = ordered_json::parse(j.dump()); // (normalized as the document reads it) + } const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2); CAPTURE(text) ordered_json_editable_document d = ordered_json_editable_document::parse(text); - j = ordered_json::parse(text); + if (!duplicates) + { + j = ordered_json::parse(text); + } const ordered_json_document other = ordered_json_document::parse(random_value(0).dump()); const int edits = 30; for (int e = 0; e < edits; ++e) @@ -624,7 +673,7 @@ TEST_CASE("json_view edits: views and values") SECTION("duplicate keys") { json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); - d.set(d.root(), "a", 4); // the last member is assigned (at the position of the first), the others dropped + d.set(d.root(), "a", 4); // the first member is assigned, the others dropped CHECK(d.root().dump() == R"({"a":4,"b":2})"); d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); CHECK(d.erase(d.root(), "a") == 2); @@ -911,10 +960,10 @@ TEST_CASE("json_view edits: replacing arrays and objects by scalars") namespace { -// reads of "a" in an object that repeats it: the last member wins, however the +// reads of "a" in an object that repeats it: the first member wins, however the // object is stored template -void check_last_wins(const View& o, int expected) +void check_first_wins(const View& o, int expected) { CAPTURE(expected) CHECK(o["a"].template get() == expected); @@ -935,35 +984,36 @@ TEST_CASE("json_view edits: duplicate keys") SECTION("an object in its parsed layout, then moved by edits") { json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); - check_last_wins(d.root(), 3); + check_first_wins(d.root(), 1); CHECK(d.root().size() == 3); CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})"); + CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value) // an edit of a value does not move the object d.set(d.root()["b"], 5); - check_last_wins(d.root(), 3); - d.set(d.root()["a"], 4); // the member that reads find - check_last_wins(d.root(), 4); - CHECK(d.root().dump() == R"({"a":1,"b":5,"a":4})"); + check_first_wins(d.root(), 1); + d.set(d.root()["a"], 4); // the member that lookups find + check_first_wins(d.root(), 4); + CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})"); // an appended member moves it d.set(d.root(), "c", true); - check_last_wins(d.root(), 4); + check_first_wins(d.root(), 4); CHECK(d.root().size() == 4); - CHECK(d.root().dump() == R"({"a":1,"b":5,"a":4,"c":true})"); + CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})"); d.set(d.root()["a"], 6); - check_last_wins(d.root(), 6); - CHECK(d.root().dump() == R"({"a":1,"b":5,"a":6,"c":true})"); + check_first_wins(d.root(), 6); + CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})"); d.set(json::json_pointer("/a"), 7); - check_last_wins(d.root(), 7); + check_first_wins(d.root(), 7); } - SECTION("set assigns the member that reads find") + SECTION("set assigns the member that lookups find") { - // the key keeps the position of its first occurrence (as in materialize()), the later members are dropped + // the key stays where its first occurrence is, the later members are dropped ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); const ordered_json_editable_view held = d.root()["a"]; - CHECK(held.get() == 5); + CHECK(held.get() == 1); const ordered_json_editable_view assigned = d.set(d.root(), "a", "x"); CHECK(held.get() == "x"); CHECK(assigned.get() == "x"); @@ -979,7 +1029,7 @@ TEST_CASE("json_view edits: duplicate keys") // through a pointer, below a duplicate d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})"); d.set(ptr_t("/a/x"), 4); - CHECK(d.root().dump() == R"({"a":{"x":1},"b":2,"a":{"x":4}})"); + CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})"); d.set(ptr_t("/a"), 0); CHECK(d.root().dump() == R"({"a":0,"b":2})"); } @@ -988,7 +1038,7 @@ TEST_CASE("json_view edits: duplicate keys") { json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); d.set(d.root(), "c", 4); - check_last_wins(d.root(), 3); + check_first_wins(d.root(), 1); CHECK(d.erase(d.root(), "a") == 2); CHECK(d.root().dump() == R"({"b":2,"c":4})"); CHECK(!d.root().contains("a")); @@ -1008,20 +1058,20 @@ TEST_CASE("json_view edits: duplicate keys") } text += R"(,"a":1,"k7":"last","a":2})"; json_editable_document d = json_editable_document::parse(text); - check_last_wins(d.root(), 2); - CHECK(d.root()["k7"].get_string() == "last"); + check_first_wins(d.root(), 0); + CHECK(d.root()["k7"].get_string() == "first"); CHECK(d.root()["k199"].get() == 199); // a value assigned in place: the index stays in use d.set(d.root()["a"], 3); - check_last_wins(d.root(), 3); + check_first_wins(d.root(), 3); d.set(d.root()["k7"], "changed"); CHECK(d.root()["k7"].get_string() == "changed"); CHECK(d.root()["k199"].get() == 199); // an appended member moves the members: the lookup scans them d.set(d.root(), "new", 1); - check_last_wins(d.root(), 3); + check_first_wins(d.root(), 3); CHECK(d.root()["k7"].get_string() == "changed"); CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string()); CHECK(d.root()["k199"].get() == 199); @@ -1031,7 +1081,7 @@ TEST_CASE("json_view edits: duplicate keys") d = json_editable_document::parse(text); d.set(d.root()["a"], json{{"x", 1}}); CHECK(d.root()["a"]["x"].get() == 1); - CHECK(d.root()["k7"].get_string() == "last"); + CHECK(d.root()["k7"].get_string() == "first"); CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times) CHECK(d.root()["k7"].is_discarded()); CHECK(d.root()["a"]["x"].get() == 1); @@ -1040,18 +1090,18 @@ TEST_CASE("json_view edits: duplicate keys") SECTION("objects in moved arrays and in new values") { json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])"); - check_last_wins(d.root()[0], 2); - check_last_wins(d.root()[1], 4); + check_first_wins(d.root()[0], 1); + check_first_wins(d.root()[1], 3); d.insert(d.root(), 0, json::parse(R"({"a": 0})")); d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member) - check_last_wins(d.root()[1], 2); - check_last_wins(d.root()[2], 4); + check_first_wins(d.root()[1], 1); + check_first_wins(d.root()[2], 3); CHECK(d.root()[3]["a"].get() == 6); d.set(d.root()[1], "a", 8); - check_last_wins(d.root()[1], 8); + check_first_wins(d.root()[1], 8); CHECK(d.root()[1].dump() == R"({"a":8})"); d.erase(d.root(), 0); - check_last_wins(d.root()[1], 4); + check_first_wins(d.root()[1], 3); CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])"); } @@ -1063,14 +1113,14 @@ TEST_CASE("json_view edits: duplicate keys") d.set(d.root(), "o", source.root()["o"]); // (the copies keep the repeated members) CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})"); - check_last_wins(d.root()["copy"][0], 2); - check_last_wins(d.root()["o"]["b"], 4); - check_last_wins(d.root()["o"], 6); + check_first_wins(d.root()["copy"][0], 1); + check_first_wins(d.root()["o"]["b"], 3); + check_first_wins(d.root()["o"], 5); d.set(d.root()["o"]["b"], "c", 0); - check_last_wins(d.root()["o"]["b"], 4); + check_first_wins(d.root()["o"]["b"], 3); d.set(d.root()["o"], "d", 0); - check_last_wins(d.root()["o"], 6); - CHECK(d.root()["o"]["b"]["a"].get() == 4); + check_first_wins(d.root()["o"], 5); + CHECK(d.root()["o"]["b"]["a"].get() == 3); CHECK(d.root()["o"]["d"].get() == 0); } } diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 56efd5c08..406e3b8a6 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -241,7 +241,7 @@ TEST_CASE("json_view images: round trips") { CHECK(l.root()["k" + std::to_string(i)] == d.root()["k" + std::to_string(i)]); } - CHECK(l.root()["k7"] == "a duplicate"); // the last of duplicate keys + CHECK(l.root()["k7"].get() == 7); // the first of duplicate keys CHECK(l.root()["inner"]["m199"].get() == -199); CHECK(!l.root().contains("k1000")); // the index is not part of the image @@ -251,18 +251,18 @@ TEST_CASE("json_view images: round trips") CHECK(node_at(image, 0).extra == 0); } - SECTION("duplicate keys of a large object: lookups return the last member") + SECTION("duplicate keys of a large object: lookups return the first member") { std::string text = "{"; for (int i = 0; i < 200; ++i) { text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i); } - // three members for k5 (the third is the last), and duplicates of k0 and k199 + // three members for k5 (the first is a number), and duplicates of k0 and k199 text += R"(,"k5":"two","k0":null,"k199":[],"k5":"three"})"; const json_document d = json_document::parse(text); REQUIRE(d.root().size() == 204); - CHECK(d.root()["k5"] == "three"); + CHECK(d.root()["k5"] == 5); const std::vector image = d.save(); for (const image_check check : { @@ -271,13 +271,15 @@ TEST_CASE("json_view images: round trips") { const json_document l = json_document::load(image, check); CHECK(l.root().size() == 204); - CHECK(l.root()["k5"] == "three"); - CHECK(l.root().at("k5") == "three"); - CHECK(l.root().find("k5").value() == "three"); - CHECK(l.root()["k0"].is_null()); - CHECK(l.root()["k199"] == json::array()); + CHECK(l.root()["k5"] == 5); + CHECK(l.root().at("k5") == 5); + CHECK(l.root().find("k5").value() == 5); + CHECK(l.root()["k0"] == 0); + CHECK(l.root()["k199"] == 199); CHECK(l.root()["k100"] == 100); + // materialize() keeps the semantics of parse(): the last value CHECK(l.root().materialize() == d.root().materialize()); + CHECK(l.root().materialize()["k5"] == "three"); } } @@ -355,15 +357,8 @@ TEST_CASE("json_view images: round trips") for (std::size_t i = 0; i < members; ++i) { CAPTURE(i) - const bool duplicate = std::find(duplicated.begin(), duplicated.end(), keys[i]) != duplicated.end(); - if (duplicate) - { - CHECK(l.root()[keys[i]] == "last"); - } - else - { - CHECK(l.root()[keys[i]] == i); - } + // lookups find the first member, also of the duplicated keys + CHECK(l.root()[keys[i]] == i); CHECK(l.root().at(keys[i]) == l.root()[keys[i]]); CHECK(l.root().find(keys[i]).key() == keys[i]); CHECK(!l.root().contains(keys[i] + "x"));